import React, { useState, useMemo, useRef, useEffect } from 'react'; import { Target, Plus, Minus, FileUp, Trash2, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, CheckCircle2, Crosshair, UploadCloud, X, List } from 'lucide-react'; // --- Firebase SDKのインポート --- import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, doc, setDoc, deleteDoc, onSnapshot } from 'firebase/firestore'; // --- Firebase初期化 --- let app, auth, db; const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; try { const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; app = initializeApp(firebaseConfig); auth = getAuth(app); db = getFirestore(app); } catch (error) { console.error("Firebase initialization error:", error); } // デバウンス関数(Firestoreへの過剰な書き込みを防ぐ) const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; }; const App = () => { // iOS (PWA) 用のメタタグを動的に追加 useEffect(() => { const metaTags = [ { name: 'apple-mobile-web-app-capable', content: 'yes' }, { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }, { name: 'apple-mobile-web-app-title', content: '建方キング' }, { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' } ]; metaTags.forEach(tag => { let existingTag = document.querySelector(`meta[name="${tag.name}"]`); if (!existingTag) { const meta = document.createElement('meta'); meta.name = tag.name; meta.content = tag.content; document.head.appendChild(meta); } else { existingTag.content = tag.content; } }); }, []); // --- State管理 --- const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [projects, setProjects] = useState([]); const [activeProjectId, setActiveProjectId] = useState(null); const [projectToDelete, setProjectToDelete] = useState(null); // UI系State const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [importText, setImportText] = useState(""); const [isEditingProjectName, setIsEditingProjectName] = useState(false); const [editingName, setEditingName] = useState(""); const fileInputRef = useRef(null); // --- 認証 (Auth Before Queries) --- useEffect(() => { if (!auth) return; const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Authentication failed:", error); setIsLoading(false); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (usr) => { setUser(usr); setIsLoading(false); }); return () => unsubscribe(); }, []); // --- Firestoreからのデータ購読 --- useEffect(() => { if (!user || !db) return; // RULE 1: 厳密なパスを使用 const projectsRef = collection(db, 'artifacts', appId, 'users', user.uid, 'projects'); const unsubscribe = onSnapshot(projectsRef, (snapshot) => { const loadedProjects = snapshot.docs.map(doc => doc.data()); // RULE 2: メモリ上でソート(Firestoreの複雑なクエリを避ける) loadedProjects.sort((a, b) => b.updatedAt - a.updatedAt); setProjects(loadedProjects); }, (error) => { console.error("Firestore Snapshot Error:", error); }); return () => unsubscribe(); }, [user]); // --- Firestore 保存ロジック (Debounce) --- const saveToFirestore = useRef( debounce((uid, projId, data) => { if (!db) return; const projectRef = doc(db, 'artifacts', appId, 'users', uid, 'projects', projId); setDoc(projectRef, data).catch(e => console.error("Save Error:", e)); }, 800) // 0.8秒間操作がなければクラウドに保存 ).current; // 現在選択されているプロジェクトを取得 const activeProject = useMemo(() => projects.find(p => p.id === activeProjectId), [projects, activeProjectId]); // プロジェクトのデータを更新する共通関数 const updateProjectData = (updates) => { if (!activeProjectId || !user) return; const currentProject = projects.find(p => p.id === activeProjectId); if (!currentProject) return; const updatedProject = { ...currentProject, ...updates, updatedAt: Date.now() }; // 1. ローカルStateを即座に更新 (UIにすぐ反映させる) setProjects(prev => prev.map(p => p.id === activeProjectId ? updatedProject : p)); // 2. バックグラウンドでクラウドへ保存 saveToFirestore(user.uid, activeProjectId, updatedProject); }; // --- プロジェクト操作 --- const createProject = async () => { if (!user || !db) return; const newId = Date.now().toString(); const newProject = { id: newId, name: `新規現場 ${new Date().toLocaleDateString()}`, updatedAt: Date.now(), columns: [{ id: 1, name: '1節-A1', designX: 100.0000, designY: 200.0000, measuredX: 100.0000, measuredY: 200.0000 }], activeColumnId: 1, xAxisIs: 'N', tolerance: 5.0000, perfectThreshold: 1.0000, stepUnit: 0.001 }; // 新規作成時はすぐにFirestoreに保存 await setDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'projects', newId), newProject); setActiveProjectId(newId); }; const confirmDeleteProject = async (id) => { if (!user || !db) return; // UI上から先に消す setProjects(prev => prev.filter(p => p.id !== id)); if (activeProjectId === id) setActiveProjectId(null); // クラウド上から削除 await deleteDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'projects', id)); setProjectToDelete(null); }; // --- アクティブ現場のデータ取得と操作 --- const columns = activeProject?.columns || []; const activeColumnId = activeProject?.activeColumnId || (columns[0]?.id); const activeColumn = useMemo(() => columns.find(c => c.id === activeColumnId) || columns[0], [columns, activeColumnId]); const xAxisIs = activeProject?.xAxisIs || 'N'; const tolerance = activeProject?.tolerance || 5.0; const perfectThreshold = activeProject?.perfectThreshold || 1.0; const stepUnit = activeProject?.stepUnit || 0.001; const setXAxisIs = (val) => updateProjectData({ xAxisIs: val }); const setStepUnit = (val) => updateProjectData({ stepUnit: val }); const setActiveColumnId = (id) => updateProjectData({ activeColumnId: id }); // 実測値入力更新 const updateActiveColumn = (field, value) => { if (!activeColumn) return; const val = value === "" ? "" : parseFloat(value); const newColumns = columns.map(c => c.id === activeColumn.id ? { ...c, [field]: val } : c); updateProjectData({ columns: newColumns }); }; // +/- ボタンでの調整 const adjustValue = (field, delta) => { if (!activeColumn) return; const currentVal = parseFloat(activeColumn[field]) || 0; const newVal = (Math.round(currentVal * 10000) + Math.round(delta * 10000)) / 10000; updateActiveColumn(field, newVal); }; // --- 現場名の編集 --- const startEditName = () => { if (!activeProject) return; setEditingName(activeProject.name); setIsEditingProjectName(true); }; const commitEditName = () => { if (editingName.trim()) { updateProjectData({ name: editingName.trim() }); } setIsEditingProjectName(false); }; // --- モニター計算 --- const stats = useMemo(() => { if (!activeColumn) return null; // 空文字の場合は0として計算しクラッシュ回避 const mX = parseFloat(activeColumn.measuredX) || 0; const dX = parseFloat(activeColumn.designX) || 0; const mY = parseFloat(activeColumn.measuredY) || 0; const dY = parseFloat(activeColumn.designY) || 0; const dx = (mX - dX) * 1000; const dy = (mY - dY) * 1000; let labels = { top: '', bottom: '', right: '', left: '' }; let fixTextX = ""; let fixTextY = ""; if (xAxisIs === 'N') { labels = { top: '北 (X+)', bottom: '南 (X-)', right: '東 (Y+)', left: '西 (Y-)' }; fixTextX = -dx > 0 ? "北" : "南"; fixTextY = -dy > 0 ? "東" : "西"; } else if (xAxisIs === 'S') { labels = { top: '南 (X+)', bottom: '北 (X-)', right: '西 (Y+)', left: '東 (Y-)' }; fixTextX = -dx > 0 ? "南" : "北"; fixTextY = -dy > 0 ? "西" : "東"; } else if (xAxisIs === 'E') { labels = { top: '東 (X+)', bottom: '西 (X-)', right: '南 (Y+)', left: '北 (Y-)' }; fixTextX = -dx > 0 ? "東" : "西"; fixTextY = -dy > 0 ? "南" : "北"; } else if (xAxisIs === 'W') { labels = { top: '西 (X+)', bottom: '東 (X-)', right: '北 (Y+)', left: '南 (Y-)' }; fixTextX = -dx > 0 ? "西" : "東"; fixTextY = -dy > 0 ? "北" : "南"; } return { dx, dy, fixX: -dx, fixY: -dy, fixTextX, fixTextY, labels, isOk: Math.abs(dx) <= tolerance && Math.abs(dy) <= tolerance, isPerfect: Math.abs(dx) <= perfectThreshold && Math.abs(dy) <= perfectThreshold }; }, [activeColumn, xAxisIs, tolerance, perfectThreshold]); const formatWithSign = (val, dec = 1) => (val > 0 ? "+" : (val < 0 ? "" : "+")) + val.toFixed(dec); // --- インポート/エクスポート --- const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => setImportText(event.target.result); reader.readAsText(file, 'Shift-JIS'); } }; const handleImport = () => { const lines = importText.split('\n').filter(l => l.trim() !== ""); const newPillars = []; lines.forEach((line, index) => { const parts = line.split(',').map(p => p.trim()); let name, x, y; if (parts[0] === 'A01' && parts.length >= 5) { name = parts[2]; x = parseFloat(parts[3]); y = parseFloat(parts[4]); } else { const spaceParts = line.split(/[\t, ]+/).map(p => p.trim()); if (spaceParts.length >= 3) { name = spaceParts[0]; x = parseFloat(spaceParts[1]); y = parseFloat(spaceParts[2]); } } if (!isNaN(x) && !isNaN(y)) { newPillars.push({ id: Date.now() + index, name: name || `柱-${columns.length + index + 1}`, designX: x, designY: y, measuredX: x, measuredY: y }); } }); if (newPillars.length > 0) { updateProjectData({ columns: [...columns, ...newPillars], activeColumnId: newPillars[0].id }); setImportText(""); setIsImportModalOpen(false); } }; const exportToSIMA = () => { let content = "Z00,*** SIMA測量データ By HO_CADpao ***,\r\n"; content += `G00, 01,無題0(Pao標準テンプレート)(Zone03),\r\n`; content += "Z00,----- 座標データ -----\r\n"; content += "A00,\r\n"; columns.forEach((col, idx) => { const mX = parseFloat(col.measuredX) || 0; const mY = parseFloat(col.measuredY) || 0; content += `A01, ${(idx + 1).toString().padStart(4, ' ')}, ${col.name}, ${mX.toFixed(4)}, ${mY.toFixed(4)}, 0.000,\r\n`; }); content += "A99,\r\n"; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${activeProject?.name || '建方実測値'}_${new Date().getTime()}.sim`; a.click(); URL.revokeObjectURL(url); }; // --- ロード中の画面 --- if (isLoading) { return (

建方キング Pro

クラウド同期中...

); } // --- 【画面1】現場一覧画面 --- if (!activeProjectId) { return (

建方キング Pro

保存された現場データ

{projects.length === 0 ? (

保存された現場はありません

右上のボタンから新しく作成してください

) : ( projects.map(p => (
setActiveProjectId(p.id)}>

{p.name}

最終保存: {new Date(p.updatedAt).toLocaleString([], {year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit'})}

)) )}
{/* 削除確認モーダル */} {projectToDelete && (

現場を削除しますか?

この現場のデータは完全に消去され、
元に戻すことはできません。

)}
); } // --- 【画面2】現場詳細 (計測) 画面 --- return (
{/* ヘッダー */}
{isEditingProjectName ? (
setEditingName(e.target.value)} className="flex-1 bg-slate-800 text-white px-4 py-2 rounded-xl outline-none font-black text-base border-2 border-blue-500" />
) : (

{activeProject?.name}

最終保存: {new Date(activeProject?.updatedAt).toLocaleTimeString()}

)}
{/* タイトル中央揃えのためのダミー */}

現場のX軸プラス(上)の方角を指定

{['N','S','E','W'].map(d => { const labels = { 'N':'北','S':'南','E':'東','W':'西' }; return ( ); })}
{/* モニター */}

{activeColumn?.name || "---"}

{stats?.labels.top}
{stats?.labels.bottom}
{stats?.labels.right}
{stats?.labels.left}
{stats && (
)}

X軸ズレ (上下方向)

tolerance ? 'text-red-500' : 'text-blue-600'}`}>{formatWithSign(stats?.dx || 0, 1)}mm

Y軸ズレ (左右方向)

tolerance ? 'text-red-500' : 'text-blue-600'}`}>{formatWithSign(stats?.dy || 0, 1)}mm

{/* 指示パネル */}
{stats?.isPerfect ? '完全合格' : (stats?.isOk ? '精度内 (ゼロへ追い込み中)' : '建ち直し指示が必要です')}
{stats?.isPerfect ? (

固定OK

) : (
{Math.abs(stats?.fixX || 0) > 0.0001 && (
{stats?.fixX > 0 ? : }

{stats?.fixTextX}へ

{Math.abs(stats?.fixX || 0).toFixed(1)}mm

)}
{Math.abs(stats?.fixY || 0) > 0.0001 && (
{stats?.fixY > 0 ? : }

{stats?.fixTextY}へ

{Math.abs(stats?.fixY || 0).toFixed(1)}mm

)}
)}
{/* 実測入力 */}

実測座標入力 (m)

北方向 X軸入力設計: {(parseFloat(activeColumn?.designX) || 0).toFixed(4)}
updateActiveColumn('measuredX', e.target.value)} className="w-full bg-transparent font-mono-precision font-black text-blue-900 text-3xl outline-none text-center tracking-tighter" />
東方向 Y軸入力設計: {(parseFloat(activeColumn?.designY) || 0).toFixed(4)}
updateActiveColumn('measuredY', e.target.value)} className="w-full bg-transparent font-mono-precision font-black text-blue-900 text-3xl outline-none text-center tracking-tighter" />
{/* 精度管理ログ */}

精度管理ログ

{columns.map(col => { const mX = parseFloat(col.measuredX) || 0; const dX = parseFloat(col.designX) || 0; const mY = parseFloat(col.measuredY) || 0; const dY = parseFloat(col.designY) || 0; const dx = (mX - dX) * 1000; const dy = (mY - dY) * 1000; const isP = Math.abs(dx) <= perfectThreshold && Math.abs(dy) <= perfectThreshold; return ( setActiveColumnId(col.id)} className={`transition-all ${activeColumnId === col.id ? 'active-site-card' : ''}`}> ); })}
点名設計座標(m)ズレ(mm)
{col.name}
X:{dX.toFixed(4)}
Y:{dY.toFixed(4)}
tolerance ? 'text-red-500' : (isP ? 'text-green-600' : 'text-blue-600')}`}> X:{formatWithSign(dx, 1)}
tolerance ? 'text-red-500' : (isP ? 'text-green-600' : 'text-blue-600')}`}> Y:{formatWithSign(dy, 1)}
{/* SIMAインポートモーダル */} {isImportModalOpen && (

SIMAデータ読込

fileInputRef.current.click()} className="border-4 border-dashed border-slate-200 rounded-[2.5rem] p-16 flex flex-col items-center justify-center gap-8 bg-slate-50 active:bg-slate-100 transition-all cursor-pointer group shadow-inner">

ファイルを指定

)}
); }; export default App;