diff --git a/client/src/components/overlay/StationOverlay.tsx b/client/src/components/overlay/StationOverlay.tsx index 2686e75..a8b1399 100644 --- a/client/src/components/overlay/StationOverlay.tsx +++ b/client/src/components/overlay/StationOverlay.tsx @@ -91,11 +91,26 @@ const CATEGORY_EMOJI: Record = { '์—ญ์‚ฌ': '๐Ÿš‰', '์ฒ ๋„์—ญ': '๐Ÿš‰', '์ง€์žฅ๋ฌผ': '๐Ÿข', + '์ง€์žฅ๋ฌผL': '๐Ÿข', '์ธก์ ': '๐Ÿ“', '๊ตฌ๊ต': '๐ŸŒ‰', '์ถœ์ž…๋ฌธ': '๐Ÿšช', + '๊ต์ฐจ๋กœ': '๐Ÿšฆ', + '๋„๋กœ': '๐Ÿ›ฃ๏ธ', + '๊ฐ•.ํ•˜์ฒœ': '๐ŸŒŠ', + '๋งˆ์„': '๐Ÿ˜๏ธ', + '์‚ฐ': 'โ›ฐ๏ธ', }; +/** ๋ผ๋ฒจ ์ƒ‰ ๋ฐ๊ธฐ ํŒ์ • โ€” ์–ด๋‘์šด ์ƒ‰์ด๋ฉด ์™ธ๊ณฝ์„ ์„ ๋ฐ๊ฒŒ ๋ฐ˜์ „(๊ฐ€๋…์„ฑ). */ +function isDarkLabelColor(hex: string): boolean { + const m = hex.match(/^#([0-9a-f]{6})$/i); + if (!m) return false; + const n = parseInt(m[1], 16); + const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; + return 0.299 * r + 0.587 * g + 0.114 * b < 96; +} + // ์˜์ƒ ์˜ค๋ฒ„๋ ˆ์ด์—” ๋ชจ๋“  ์„ ๋กœ ๊ตฌ์กฐ๋ฌผ(๊ต๋Ÿ‰/ํ„ฐ๋„/๊ตฌ๊ต)์„ ๋ผ๋ฒจ๋กœ ํ‘œ์ถœํ•œ๋‹ค. // ์‹œ์„ค๋“ฑ๊ธ‰(์‹œ์„ค์ข…๋ณ„) ํ‘œ์‹œ ํ•„ํ„ฐ๋Š” ํ•˜๋‹จ ์Šคํ…Œ์ด์…˜๋ฐ”ยทRoutePanel ์ „์šฉ โ†’ ์˜์ƒ์€ ํ•ญ์ƒ ์ „๋ถ€ ํ‘œ์ถœ. // (์—ญ์‚ฌ=์ฒ ๋„์—ญ์€ type='station'์ด๋ผ ์—ฌ๊ธฐ ๋ฏธํฌํ•จ โ†’ ์Šคํ…Œ์ด์…˜๋ฐ” ์ „์šฉ ์œ ์ง€.) @@ -113,8 +128,10 @@ const DISPLAY_DEFAULTS = { smoothSpeedRef: 0.010, // ์ฆ‰์‹œ์ถ”์ข… ๊ธฐ์ค€ ์†๋„(ํด์ˆ˜๋ก ๋” ๊ฐ•ํ•˜๊ฒŒ ํ‰ํ™œ) maxPoiRange: 1000, // false = ์ง€๋ฉด๊ณ ๋„ ๊ธฐ์ค€(POI๋ฅผ ์ง€๋ฉด๊ณ ๋„ z์— ๊ณ ์ • โ†’ gap=๋“œ๋ก ๊ณ ๋„โˆ’์ง€๋ฉด๊ณ ๋„ ์ž๋™๋ณ€๋™, ๋“œ๋ก  ์˜ค๋ฅด๋‚ด๋ ค๋„ ์ง€๋ฌผ์— ๋ถ™์Œ). + // ํ‘œ๊ณ  ๋ฏธ์ƒ(z=0) ๋ผ๋ฒจ์€ '๊ฒฝ๋กœ ํ‘œ๊ณ ' ํ‰๋ฉด์— ์•ต์ปค โ†’ ๊ถค์ ๊ณผ ๊ฐ™์€ ์Šฌ๋ผ์ด๋”๋กœ ํ•จ๊ป˜ ๋ณด์ •๋จ. // true = ๋“œ๋ก โˆ’์ด๊ฒฉ๊ฑฐ๋ฆฌ ๊ณ ์ •(gap ์ƒ์ˆ˜). ๋“œ๋ก  ๊ณ ๋„ ๋ณ€๋™ ์‹œ ๋ถ€์ •ํ™•. - poiDroneHeight: true, + // ๊ธฐ๋ณธ false: ์‹ค์ธก z(ํšŒ๋•)๋Š” ์ง€๋ฌผ์— ์ •ํ™•ํžˆ ๋ถ™๊ณ , ๋ฏธ์ƒ(์ œ์ฃผ)์€ ๊ฒฝ๋กœํ‘œ๊ณ ๋กœ ์ผ๊ด„ ๋ณด์ • ๊ฐ€๋Šฅ. + poiDroneHeight: false, droneHeightDrop: 24, }; @@ -123,7 +140,7 @@ const DISPLAY_DEFAULTS = { // ํฌ์ฆˆ(poseAt)๋กœ ์ง์ ‘ ํˆฌ์˜ โ†’ ๋ผ์ธ์ฒ˜๋Ÿผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ด๋™. (precompute = ๊ฐ€์‹œ์„ฑ/๊ฒน์นจ ๊ฒฐ์ • ์ „์šฉ) interface LabelCache { stationLabels: { title: string; lat: number; lon: number; z: number }[]; - poiMarkers: { title: string; label: string; category: string; lat: number; lon: number; gz: number; compact: { k: string; v: string }[]; labelRow: number; labelRowCount: number }[]; + poiMarkers: { title: string; label: string; category: string; lat: number; lon: number; gz: number; compact: { k: string; v: string }[]; labelRow: number; labelRowCount: number; color?: string }[]; } // ์ค‘์‹ฌ์„  + ๋‚˜์นจ๋ฐ˜ ๋ Œ๋” ์บ์‹œ (per-frame, renderCacheRef) @@ -322,6 +339,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // FOV(์ดˆ์ ) ๋ณด์ • ๋ชจ๋“œ: POI ๋ผ๋ฒจ์„ ์‹ค์ œ ์œ„์น˜๋กœ ๋Œ๋ฉด ๊ทธ ๋Œ€์‘์ ์œผ๋กœ focal ์„ ์—ญ์‚ฐ. const [fovMode, setFovMode] = useState(false); const fovModeRef = useRef(false); + // Yaw ๋ณด์ • ๋ชจ๋“œ: POI ๋ผ๋ฒจ์„ ์‹ค์ œ ์œ„์น˜(์ขŒ์šฐ)๋กœ ๋Œ๋ฉด ํ•„์š”ํ•œ Yawยฑ ๋ฅผ ์—ญ์‚ฐ. + const [yawMode, setYawMode] = useState(false); + const yawModeRef = useRef(false); // ๋ผ๋ฒจ ์†์„ฑ ํŒ์—…(๋‹ค์ค‘) โ€” ํ•ญ์ƒ ํ™œ์„ฑ(๋ชจ๋“œ ๋ฒ„ํŠผ ์—†์Œ). ๋ผ๋ฒจ ์œ„์—์„œ๋งŒ ์บ”๋ฒ„์Šค๊ฐ€ ์ž…๋ ฅ์„ ๋ฐ›์•„ ํด๋ฆญโ†’ํŒ์—…. // id=`${kind}:${title}`. ์œ„์น˜๋Š” RAF ๊ฐ€ ๋ผ๋ฒจ displayed ์ขŒํ‘œ๋กœ ๋งค ํ”„๋ ˆ์ž„ ๊ฐฑ์‹ (์žฌ์ƒ ์ค‘ ๋”ฐ๋ผ ์ด๋™). type InfoPopup = { id: string; kind: 'poi' | 'station'; title: string; category: string; lat: number; lon: number; z: number; dist: number | null; sx: number; sy: number; props?: { k: string; v: string }[]; @@ -392,6 +412,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible useEffect(() => { maxPoiRangeRef.current = maxPoiRange; }, [maxPoiRange]); useEffect(() => { editModeRef.current = editMode; }, [editMode]); useEffect(() => { fovModeRef.current = fovMode; }, [fovMode]); + useEffect(() => { yawModeRef.current = yawMode; }, [yawMode]); // ์ปดํŒฉํŠธ ์ž๋™ ํŒ์—… ๋™๊ธฐํ™” โ€” visStructRef(์—ฐ์† ์˜จ์Šคํฌ๋ฆฐ ๋ผ๋ฒจ) ๋งˆ๋‹ค ๋ผ๋ฒจ ์˜† DOM ํŒ์—…์„ ์ž๋™ '์ƒ์„ฑ'๋งŒ. // ์ œ๊ฑฐ๋Š” RAF ๊ฐ€ ๋‹จ์ผ ๊ถŒํ•œ(onScreenLabels + 4ํ”„๋ ˆ์ž„ ์œ ์˜ˆ)์œผ๋กœ ๋‹ด๋‹น โ†’ add/remove ์ถฉ๋Œ(๋ฒˆ์ฉ์ž„) ์ œ๊ฑฐ. @@ -480,27 +501,35 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible useEffect(() => { overlapExcludeRef.current = poiOverlapExclude; }, [poiOverlapExclude]); // ์ขŒ์ธก ๋…ธ์„  ํŒจ๋„(RoutePanel) ํ‘œ์‹œ ํ† ๊ธ€์€ ํ•˜๋‹จ ์žฌ์ƒ๋ฐ”(VideoPlayer)๋กœ ์ด๋™. const storeBaseName = useGeoStore(s => s.baseName); + const storeCameraInfo = useGeoStore(s => s.cameraInfo); + const storeCameraJson = useGeoStore(s => s.cameraJson); const storePoiOverrides = useGeoStore(s => s.poiOverrides); const setPoiOverride = useGeoStore(s => s.setPoiOverride); const setPoiOverrides = useGeoStore(s => s.setPoiOverrides); const clearPoiOverride = useGeoStore(s => s.clearPoiOverride); - // ๊ฒฝ๋กœ ํ‘œ๊ณ  ์Šฌ๋ผ์ด๋” ์ƒํ•œ โ€” ๋ฐ์ดํ„ฐ(์ค‘์‹ฌ์„ ) ์ตœ๋Œ€ ์ง€๋ฉดํ‘œ๊ณ  + ์—ฌ์œ  (์—†์œผ๋ฉด 60 ํด๋ฐฑ) + // ๊ฒฝ๋กœ ํ‘œ๊ณ  ์Šฌ๋ผ์ด๋” ์ƒํ•œ โ€” ๋ฐ์ดํ„ฐ ์ตœ๋Œ€ ํ‘œ๊ณ  + ์—ฌ์œ  (์—†์œผ๋ฉด 60 ํด๋ฐฑ). + // ์ค‘์‹ฌ์„ ์ด ์—†๊ฑฐ๋‚˜ z=0 ์ธ ๋ฐ์ดํ„ฐ์…‹(์˜ˆ: ์ œ์ฃผ DJI ๋กœ๊ทธ)์€ ๋“œ๋ก  ๊ณ ๋„(ASL)๊ฐ€ ์œ ์ผํ•œ ์ƒํ•œ ๊ทผ๊ฑฐ โ€” + // ์ง€๋ฉด์€ ๋“œ๋ก ๋ณด๋‹ค ๋‚ฎ์œผ๋ฏ€๋กœ max(๋“œ๋ก ๊ณ ๋„)๊นŒ์ง€ ์—ด์–ด๋‘๋ฉด ์–ด๋–ค ์ง€ํ˜•์ด๋“  ์Šฌ๋ผ์ด๋”๋กœ ๋„๋‹ฌ ๊ฐ€๋Šฅ. const pathZMax = useMemo(() => { let m = 0; for (const c of storeCenterline) if (typeof c.z === 'number' && c.z > m) m = c.z; + for (const f of storeFrames) if (typeof f.altitude === 'number' && f.altitude > m) m = f.altitude; return m > 0 ? Math.ceil(m + 10) : 60; - }, [storeCenterline]); + }, [storeCenterline, storeFrames]); useEffect(() => { overridesRef.current = storePoiOverrides; }, [storePoiOverrides]); // โ”€โ”€ ๋ณด์ •๊ฐ’ ์˜์†ํ™” (baseName๋ณ„ localStorage) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // ์นด๋ฉ”๋ผ ๊ฐ๋„/๋‚ด๋ถ€ํ‘œ์ •(Yawยฑ/Pitchยฑ/focal ๋“ฑ) + ํ‘œ์‹œ์„ค์ •์„ ๋ฐ์ดํ„ฐ์…‹๋ณ„๋กœ ์ €์žฅยท๋ณต์›. // ํ•œ ๋ฒˆ ํŠœ๋‹ํ•˜๋ฉด ์ƒˆ๋กœ๊ณ ์นจ/์žฌ๋กœ๋“œํ•ด๋„ ์œ ์ง€ โ†’ "์ฒ˜์Œ๋ถ€ํ„ฐ ๋งž๊ฒŒ" ํ‘œ์‹œ. const calibLoadedFor = useRef(null); + // ์ด ๋ฐ์ดํ„ฐ์…‹์— ์ €์žฅ๋œ ๋ณด์ •๊ฐ’์ด ์žˆ์—ˆ๋Š”์ง€ โ€” ์นด๋ฉ”๋ผ ๊ฐ์ง€๊ฐ’ ์ž๋™ ์ ์šฉ ์—ฌ๋ถ€ ํŒ๋‹จ(์‚ฌ์šฉ์ž ํŠœ๋‹ ์šฐ์„ ). + const calibHadSavedRef = useRef(false); useEffect(() => { if (!storeBaseName || calibLoadedFor.current === storeBaseName) return; calibLoadedFor.current = storeBaseName; try { const raw = localStorage.getItem(`ghivideo:calib:${storeBaseName}`); + calibHadSavedRef.current = !!raw; if (!raw) return; const c = JSON.parse(raw); if (c.params && typeof c.params === 'object') setParams(p => ({ ...p, ...c.params })); @@ -525,6 +554,100 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible return () => clearTimeout(t); }, [storeBaseName, params, maxPoiRange, smoothHalf, emaAlpha, smoothMinAlpha, smoothSpeedRef]); + // ์˜์ƒ(djmd)์—์„œ ๊ฐ์ง€ํ•œ ์นด๋ฉ”๋ผ ์ดˆ์ ๊ฑฐ๋ฆฌ ์ž๋™ ์ ์šฉ. + // ์‚ฌ์šฉ์ž๊ฐ€ f ๋ฅผ ์ง์ ‘ ํŠœ๋‹ํ•œ ๊ฐ’(๊ธฐ๋ณธ๊ฐ’ 24 ๊ฐ€ ์•„๋‹Œ ์ €์žฅ๊ฐ’)๋งŒ ์šฐ์„ ํ•˜๊ณ , ๊ทธ ์™ธ(์ฒซ ๋กœ๋“œ, + // ๋˜๋Š” ์ €์žฅ์€ ๋์ง€๋งŒ f ๊ฐ€ ๊ธฐ๋ณธ๊ฐ’ ๊ทธ๋Œ€๋กœ์ธ ๊ฒฝ์šฐ)๋Š” ๊ฐ์ง€๊ฐ’์„ ์ ์šฉํ•œ๋‹ค. + // (๋ณด์ • ์ž๋™์ €์žฅ์€ ํด๋”๋ฅผ ์—ด๊ธฐ๋งŒ ํ•ด๋„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ '์ €์žฅ ์กด์žฌ' ๋งŒ์œผ๋ก  ํŠœ๋‹ ์—ฌ๋ถ€ ํŒ๋‹จ ๋ถˆ๊ฐ€) + const camAppliedFor = useRef(null); + useEffect(() => { + if (!storeBaseName || !storeCameraInfo?.focalLen35) return; + if (camAppliedFor.current === storeBaseName) return; + camAppliedFor.current = storeBaseName; + const f = storeCameraInfo.focalLen35; + const model = storeCameraInfo.model; + setParams(p => { + const focalTuned = calibHadSavedRef.current && + Math.abs(p.focalLen - DEFAULT_CAMERA_PARAMS.focalLen) > 0.01; + if (focalTuned) { + console.log(`[camera] ${model} ๊ฐ์ง€ (${f}mm) โ€” ์‚ฌ์šฉ์ž ํŠœ๋‹ f=${p.focalLen}mm ์œ ์ง€ (ํŒจ๋„ '์ ์šฉ'์œผ๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ)`); + return p; + } + console.log(`[camera] ${model} ๊ฐ์ง€ โ†’ focal ${f}mm ์ž๋™ ์ ์šฉ`); + return { ...p, focalLen: f }; + }); + }, [storeBaseName, storeCameraInfo]); + + // ํด๋”์˜ <์˜์ƒ base>.camera.json ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ โ€” ์ตœ์šฐ์„  ์ ์šฉ(localStorage/๊ฐ์ง€๊ฐ’ ์œ„์—). + // ์ด effect ๋Š” calib ๋ณต์›ยท๊ฐ์ง€๊ฐ’ ์ ์šฉ effect ๋’ค์— ์„ ์–ธ๋˜์–ด ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๊ฐ€ ๋งˆ์ง€๋ง‰์— ๋ฐ˜์˜๋œ๋‹ค. + const camJsonAppliedFor = useRef(null); + useEffect(() => { + if (!storeBaseName || !storeCameraJson) return; + if (camJsonAppliedFor.current === storeBaseName) return; + camJsonAppliedFor.current = storeBaseName; + const cam = storeCameraJson; + setParams(p => { + const next = { ...p }; + for (const [k, v] of Object.entries(cam)) { + if (k in next && typeof v === 'number' && isFinite(v)) (next as unknown as Record)[k] = v; + } + return next; + }); + console.log('[camera] .camera.json ํŒŒ๋ผ๋ฏธํ„ฐ ์ ์šฉ(์ตœ์šฐ์„ ):', cam); + }, [storeBaseName, storeCameraJson]); + + // ํ˜„์žฌ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž PC ์— <์˜์ƒ base>.camera.json ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ ์ €์žฅ. + // Yawยฑ(์ž๋™์ถ”์ •/๋“œ๋ž˜๊ทธ ์—ญ์‚ฐ ๊ฒฐ๊ณผ) ํฌํ•จ ์ „์ฒด ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋‹ด๊ธด๋‹ค. + // ๋ฐ›์€ ํŒŒ์ผ์„ ์˜์ƒ ํด๋”์— ๋„ฃ์–ด๋‘๋ฉด ๋‹ค์Œ ํด๋” ์„ ํƒ ๋•Œ ์ตœ์šฐ์„ ์œผ๋กœ ์ž๋™ ์ ์šฉ๋œ๋‹ค. + const [camSaveState, setCamSaveState] = useState<'idle' | 'ok'>('idle'); + const saveCameraToPc = useCallback(() => { + const geo = useGeoStore.getState(); + const videoName = geo.videoFiles[geo.videoIndex]?.name ?? (geo.baseName ? `${geo.baseName}.mp4` : 'camera.mp4'); + const base = videoName.replace(/\.[^.]+$/, ''); + const payload = { + camera: paramsRef.current, + model: geo.cameraInfo?.model, + savedAt: new Date().toISOString(), + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${base}.camera.json`; + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 5000); + setCamSaveState('ok'); + setTimeout(() => setCamSaveState('idle'), 2500); + }, []); + + // Yaw ์ž๋™ ์ถ”์ • โ€” ์ „์ง„ ๋น„ํ–‰ ๊ตฌ๊ฐ„์—์„œ GPS ์ง„ํ–‰ ๋ฐฉ์œ„(์ •ํ™•) vs ์ง๋ฒŒ ํ—ค๋”ฉ(๋‚˜์นจ๋ฐ˜ ์˜ค์ฐจ ํฌํ•จ)์˜ + // ์ฐจ์ด ์ค‘์•™๊ฐ’์„ Yawยฑ ๋กœ ์„ค์ •. ์นด๋ฉ”๋ผ๊ฐ€ ์ง„ํ–‰๋ฐฉํ–ฅ์„ ๋ณด๋ฉฐ ์ดฌ์˜ํ•˜๋Š” ๋„๋กœ ์ถ”์ข… ๋น„ํ–‰ ๊ฐ€์ •(๊ทผ์‚ฌ์น˜). + const autoEstimateYaw = useCallback(() => { + const frames = allDroneFramesRef.current; + if (!frames || frames.length < 30) { alert('๋“œ๋ก  ๋กœ๊ทธ๊ฐ€ ๋ถ€์กฑํ•ด ์ž๋™ ์ถ”์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); return; } + const fps = fpsRef.current; + const cosLat = Math.cos((frames[0].lat * Math.PI) / 180); + const STEP = 10; // ์ง„ํ–‰๋ฐฉ์œ„ ๊ณ„์‚ฐ ์ฐฝ(โ‰ˆ1์ดˆ) โ€” GPS ์ง€ํ„ฐ ํ‰ํ™œ + const diffs: number[] = []; + for (let i = STEP; i < frames.length; i++) { + const a = frames[i - STEP], b = frames[i]; + const dt = (b.frame - a.frame) / fps; + if (dt <= 0) continue; + const dx = (b.lon - a.lon) * 111000 * cosLat; + const dy = (b.lat - a.lat) * 111000; + if (Math.hypot(dx, dy) / dt < 2) continue; // ์ •์ง€/ํ˜ธ๋ฒ„ ๊ตฌ๊ฐ„ ์ œ์™ธ + const course = (Math.atan2(dx, dy) * 180) / Math.PI; // ์ง„ํ–‰ ๋ฐฉ์œ„ (North=0, CW) + let d = course - frames[i - (STEP >> 1)].yaw; // ์ง„ํ–‰๋ฐฉ์œ„ โˆ’ ์ง๋ฒŒ ํ—ค๋”ฉ + d = ((d + 540) % 360) - 180; + if (Math.abs(d) < 60) diffs.push(d); // ์˜†์„ ๋ณด๋ฉฐ ์ฐ์€ ๊ตฌ๊ฐ„ ์ œ์™ธ + } + if (diffs.length < 20) { alert('์ „์ง„ ์ดฌ์˜ ๊ตฌ๊ฐ„์ด ๋ถ€์กฑํ•ด ์ž๋™ ์ถ”์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); return; } + diffs.sort((x, y) => x - y); + const med = diffs[diffs.length >> 1]; + const next = Math.round(med * 10) / 10; + setParam('yawOffset', next); + console.log(`[yaw] ์ž๋™ ์ถ”์ •: ์œ ํšจ ์ƒ˜ํ”Œ ${diffs.length}๊ฐœ, ์ค‘์•™๊ฐ’ ${med.toFixed(2)}ยฐ โ†’ Yawยฑ ${next}ยฐ ์ ์šฉ`); + }, [setParam]); + // POI ๋ณด์ •(override: DEM ํ‘œ๊ณ /๋“œ๋ž˜๊ทธ)๋„ baseName๋ณ„ ์ž๋™ ์ €์žฅยท๋ณต์› โ†’ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€. const ovLoadedFor = useRef(null); useEffect(() => { @@ -728,6 +851,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const CLIP_Z = 0.1; const SMOOTH_HALF = currentSmoothHalf; const MAX_RANGE = currentMaxRange; // POI ํ‘œ์‹œ ๋ฒ”์œ„ โ€” ๋“œ๋ก  ์ˆ˜ํ‰ ์ง์„ ๊ฑฐ๋ฆฌ(m) + // ํ‘œ๊ณ  ๋ฏธ์ƒ(z=0) ๋ผ๋ฒจ์˜ ์•ต์ปค ํ‰๋ฉด โ€” ๋“œ๋ก  ๊ถค์ ๊ณผ ๋™์ผํ•œ '๊ฒฝ๋กœ ํ‘œ๊ณ ' ๊ฐ’. + // ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒฝ๋กœํ‘œ๊ณ  ์Šฌ๋ผ์ด๋”๋กœ ๊ถค์ ์„ ๋„๋กœ์— ๋งž์ถ”๋ฉด ์ธก์ /POI ๋ผ๋ฒจ๋„ ํ•จ๊ป˜ ๋งž๋Š”๋‹ค. + const PATH_Z = dronePathZRef.current; if (!frames.length) return; @@ -752,20 +878,31 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const stationLabels: LabelCache['stationLabels'] = []; for (const st of allSt) { const snap = nearestCL(st.lat, st.lon); - const slat = snap?.lat ?? st.lat, slon = snap?.lon ?? st.lon, sz = snap?.z ?? st.z; + const slat = snap?.lat ?? st.lat, slon = snap?.lon ?? st.lon; + const szRaw = snap?.z ?? st.z; + // ํ‘œ๊ณ  ๋ฏธ์ƒ(0 ์ดํ•˜)์ด๋ฉด ๊ฒฝ๋กœํ‘œ๊ณ  ํ‰๋ฉด์— ์•ต์ปค โ€” ๊ณต์ค‘๋ถ€์–‘/์‹œ์ฐจ ๋ฐ€๋ฆผ ๋ฐฉ์ง€. + const sz = szRaw > 0 ? szRaw : PATH_Z; const cc = toCameraCoords(drone, slat, slon, sz, currentParams, worldOrigin); if (cc.Zc < CLIP_Z) continue; + // ์ธก์ ๋„ POI ์™€ ๋™์ผํ•˜๊ฒŒ ํ‘œ์‹œ ๋ฒ”์œ„ ์ œํ•œ โ€” ์›๊ฑฐ๋ฆฌ(์ˆ˜ km) ์ธก์ ์ด ์ˆ˜ํ‰์„ ์— ๋ญ‰์น˜๋Š” ๊ฒƒ ๋ฐฉ์ง€. + if ((cc.distH ?? 0) > MAX_RANGE) continue; const { pxRaw, pyRaw } = pixelFromCamera(cc, currentParams); - if (pxRaw < -0.05 || pxRaw > 1.05 || pyRaw < -0.05 || pyRaw > 1.05) continue; + // ํฌํ•จ ๊ฒฝ๊ณ„ ยฑ0.15 ๋กœ ๋„‰๋„‰ํžˆ โ€” ๊ฒฝ๊ณ„์— ๊ฑธ์นœ ๋ผ๋ฒจ์ด ํ˜ธ๋ฒ„(์ •์ง€) ์ค‘ GPS ์ง€ํ„ฐ๋กœ + // ์บ์‹œ ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋“ค๋ฝ๊ฑฐ๋ฆฌ๋ฉฐ ๊นœ๋นก์ด๋Š” ๊ฒƒ ๋ฐฉ์ง€. ์‹ค์ œ ํ™”๋ฉด ๋ฐ– ์ปฌ๋ง์€ ๋“œ๋กœ์šฐ๊ฐ€ ๋‹ด๋‹น. + if (pxRaw < -0.15 || pxRaw > 1.15 || pyRaw < -0.15 || pyRaw > 1.15) continue; // ํ™”๋ฉด์ขŒํ‘œ ๋Œ€์‹  (์Šค๋ƒ…๋œ)์›”๋“œ์ขŒํ‘œ ์ €์žฅ โ†’ RAF ๊ฐ€ ์—ฐ์† ํฌ์ฆˆ๋กœ ์žฌํˆฌ์˜. stationLabels.push({ title: st.title, lat: slat, lon: slon, z: sz }); } // ํ›„๋ณด ์ˆ˜์ง‘ (๊ฑฐ๋ฆฌ dist + ์›”๋“œ์ขŒํ‘œ + ์ง€๋ฉด๊ณ ๋„ gz) โ†’ ๊ฒน์นจ ์–ต์ œ์šฉ - const poiCand: { x: number; y: number; title: string; label: string; category: string; dist: number; lat: number; lon: number; gz: number; compact: { k: string; v: string }[] }[] = []; + const poiCand: { x: number; y: number; title: string; label: string; category: string; dist: number; lat: number; lon: number; gz: number; compact: { k: string; v: string }[]; color?: string }[] = []; for (const poi of allPoi) { - // ์ง€๋ฉด๊ณ ๋„ gz: ๋ณด์ •๊ฐ’(DEM/๋“œ๋ž˜๊ทธ) ๋˜๋Š” ์ตœ๊ทผ์ ‘ ์„ ๋กœํ‘œ๊ณ . RAF ํˆฌ์˜ ๊ธฐ์ค€๊ฐ’. - const gz = overridesRef.current[poi.title]?.z ?? nearestCL(poi.lat, poi.lon)?.z ?? poi.z; + // ์ง€๋ฉด๊ณ ๋„ gz: ๋ณด์ •๊ฐ’(DEM/๋“œ๋ž˜๊ทธ) โ†’ ์ตœ๊ทผ์ ‘ ์„ ๋กœํ‘œ๊ณ (์œ ํšจ๊ฐ’) โ†’ POI ์ž์ฒด z โ†’ ๊ฒฝ๋กœํ‘œ๊ณ  ์•ต์ปค. + // ํ‘œ๊ณ  ๋ฏธ์ƒ(0)์ธ ๋ฐ์ดํ„ฐ์…‹(์ œ์ฃผ)์€ ๊ฒฝ๋กœํ‘œ๊ณ  ํ‰๋ฉด์— ์•ต์ปค๋˜์–ด ๊ถค์ ๊ณผ ํ•จ๊ป˜ ๋ณด์ •๋œ๋‹ค. + const clz = nearestCL(poi.lat, poi.lon)?.z; + const gz = overridesRef.current[poi.title]?.z + ?? (clz !== undefined && clz > 0 ? clz : undefined) + ?? (poi.z > 0 ? poi.z : PATH_Z); // ๊ฐ€์‹œ์„ฑ ํŒ์ •์šฉ poiZ: ๋“œ๋ก ๋†’์ด ๋ชจ๋“œ๋ฉด ๋“œ๋ก โˆ’์ด๊ฒฉ๊ฑฐ๋ฆฌ, ์•„๋‹ˆ๋ฉด gz. const poiZ = currentDroneHeight ? drone.altitude - (currentParams.geoidOffset ?? 0) - currentDroneDrop @@ -775,13 +912,18 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // ํ‘œ์‹œ ๋ฒ”์œ„: ๋“œ๋ก ๊ณผ์˜ ์ˆ˜ํ‰ ์ง์„ ๊ฑฐ๋ฆฌ(distH)๊ฐ€ ๋ฒ”์œ„ ์•ˆ์ด๋ฉด ํ‘œ์‹œ, ๋ฐ–์ด๋ฉด ์ œ์™ธ. if ((cc.distH ?? 0) > MAX_RANGE) continue; const { pxRaw, pyRaw } = pixelFromCamera(cc, currentParams); - if (pxRaw < -0.02 || pxRaw > 1.02 || pyRaw < -0.02 || pyRaw > 1.02) continue; - poiCand.push({ x: pxRaw, y: pyRaw, title: poi.title, label: guboonOf(poi.props) ?? cleanTitle(poi.title), category: poi.category, dist: cc.distH ?? 0, lat: poi.lat, lon: poi.lon, gz, compact: compactFieldsOf(poi.props) }); + // ํฌํ•จ ๊ฒฝ๊ณ„ ยฑ0.15 โ€” ๊ฒฝ๊ณ„ ๊ฑธ์นจ ๋ผ๋ฒจ์˜ ํ˜ธ๋ฒ„ ์ค‘ 10Hz ๊นœ๋นก์ž„ ๋ฐฉ์ง€(ํ™”๋ฉด ๋ฐ– ์ปฌ๋ง์€ ๋“œ๋กœ์šฐ ๋‹ด๋‹น). + if (pxRaw < -0.15 || pxRaw > 1.15 || pyRaw < -0.15 || pyRaw > 1.15) continue; + poiCand.push({ x: pxRaw, y: pyRaw, title: poi.title, label: guboonOf(poi.props) ?? cleanTitle(poi.title), category: poi.category, dist: cc.distH ?? 0, lat: poi.lat, lon: poi.lon, gz, compact: compactFieldsOf(poi.props), ...(poi.labelColor ? { color: poi.labelColor } : {}) }); } // ๊ฒน์นจ ์–ต์ œ: KAKAO_RAIL(์ฒ ๋„์—ญ/์—ญ์‚ฌ) ๋ผ๋ฒจ์„ ์ตœ์šฐ์„  ์œ ์ง€ โ†’ ๊ฒน์น˜๋ฉด ์ผ๋ฐ˜ POI ๊ฐ€ ์ˆจ๊ฒจ์ง„๋‹ค. // ๊ฐ™์€ ์šฐ์„ ์ˆœ์œ„ ๋‚ด์—์„œ๋Š” ๊ฐ€๊นŒ์šด(dist ์ž‘์€) ๊ฒƒ ์šฐ์„ . ์ž„๊ณ„ ์ด๋‚ด๋ฉด ๋’ค(๋‚ฎ์€์šฐ์„ /๋จผ) ๊ฒƒ ์ˆจ๊น€. + // ๊ฑฐ๋ฆฌ์ฐจ 1m ๋ฏธ๋งŒ์€ ์ œ๋ชฉ์ˆœ ๊ณ ์ • โ€” ํ˜ธ๋ฒ„(์ •์ง€) ์ค‘ GPS ์ง€ํ„ฐ๋กœ ์Šน์ž๊ฐ€ ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋’ค๋ฐ”๋€Œ๋ฉฐ + // ๋ผ๋ฒจ ์Œ์ด ๊ต๋Œ€๋กœ ๊นœ๋นก์ด๋Š” ๊ฒƒ ๋ฐฉ์ง€. const railRank = (cat: string): number => (cat === '์ฒ ๋„์—ญ' || cat === '์—ญ์‚ฌ' ? 0 : 1); - poiCand.sort((a, b) => railRank(a.category) - railRank(b.category) || a.dist - b.dist); + poiCand.sort((a, b) => + railRank(a.category) - railRank(b.category) || + (Math.abs(a.dist - b.dist) > 1 ? a.dist - b.dist : (a.title < b.title ? -1 : a.title > b.title ? 1 : 0))); // ๊ฒน์นจ ์‹œ ์ง„ํ–‰๋ฐฉํ–ฅ ๋ณ€ํ˜• ์šฐ์„ : ๊ฐ™์€ base ํ˜•์ œ(์˜ˆ: ํšŒ๋•์ œ1๊ฐ€๋„๊ต ์ƒ/ํ•˜)๊ฐ€ ๊ฒน์น˜๋ฉด // ํ•˜ํ–‰='(ํ•˜)' / ์ƒํ–‰='(์ƒ)' ์ชฝ์„ ๋‚จ๊ธด๋‹ค(๋“œ๋ก ์ด ์‹ค์ œ ์ง€๋‚˜๋Š” ์„ ๋กœ). ๋ฌด๊ด€ํ•œ POI์—” ์˜ํ–ฅ ์—†์Œ. // ์ง„ํ–‰๋ฐฉํ–ฅ ๋ณ€ํ˜• ํƒœ๊ทธ(๋‹ซ๋Š” ๊ด„ํ˜ธ๊นŒ์ง€) โ€” ํ•˜ํ–‰='(ํ•˜)', ์ƒํ–‰='(์ƒ)'. '(ํ•˜์ธ์ƒ)' ๋“ฑ์€ ์ œ์™ธ. @@ -816,7 +958,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const ck = coordKey(c); const row = groupSeen.get(ck) ?? 0; groupSeen.set(ck, row + 1); - return { title: c.title, label: c.label, category: c.category, lat: c.lat, lon: c.lon, gz: c.gz, compact: c.compact, labelRow: row, labelRowCount: groupCount.get(ck) ?? 1 }; + return { title: c.title, label: c.label, category: c.category, lat: c.lat, lon: c.lon, gz: c.gz, compact: c.compact, labelRow: row, labelRowCount: groupCount.get(ck) ?? 1, ...(c.color ? { color: c.color } : {}) }; }); newMap.set(drone.frame, { stationLabels, poiMarkers }); @@ -841,17 +983,20 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible }, [nearestCL]); // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ ์‚ฌ์ „ ๊ณ„์‚ฐ ์‹œ์ž‘ + // storeFrames ์˜์กด ํ•„์ˆ˜: ๋ถ„ํ•  ์˜์ƒ ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜(loadSegment)์œผ๋กœ frames ๊ฐ€ ๊ต์ฒด๋˜๋ฉด + // ๋ผ๋ฒจ ์บ์‹œ(ํ”„๋ ˆ์ž„๋ณ„ ๊ฐ€์‹œ POI)๋ฅผ ์ƒˆ ๊ตฌ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ์žฌ๊ณ„์‚ฐํ•ด์•ผ ํ•จ โ€” ์—†์œผ๋ฉด ์ด์ „ ์„ธ๊ทธ๋จผํŠธ + // ์บ์‹œ๊ฐ€ ๋‚จ์•„ ๋‘ ๋ฒˆ์งธ ์˜์ƒ๋ถ€ํ„ฐ ์‹œ์„ค๋ฌผ์ด ํ‘œ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค. useEffect(() => { if (!droneFramesLoaded || !geoDataLoaded || !clDataLoaded) return; startLabelPrecompute(paramsRef.current, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop); - }, [droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop, poiOverlapExclude]); + }, [droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop, poiOverlapExclude, dronePathZ, storeFrames]); - // params / smoothHalf / maxPoiDist ๋ณ€๊ฒฝ ์‹œ ์‚ฌ์ „ ๊ณ„์‚ฐ ์žฌ์‹œ์ž‘ (500ms debounce) + // params / smoothHalf / maxPoiDist / ๊ฒฝ๋กœํ‘œ๊ณ (ํ‘œ๊ณ  ๋ฏธ์ƒ ๋ผ๋ฒจ ์•ต์ปค) ๋ณ€๊ฒฝ ์‹œ ์‚ฌ์ „ ๊ณ„์‚ฐ ์žฌ์‹œ์ž‘ (500ms debounce) useEffect(() => { if (!droneFramesLoaded || !geoDataLoaded || !clDataLoaded) return; const timer = setTimeout(() => startLabelPrecompute(params, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop), 500); return () => clearTimeout(timer); - }, [params, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop, storePois, storeStructures, droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute, poiOverlapExclude]); + }, [params, smoothHalf, maxPoiRange, poiDroneHeight, droneHeightDrop, storePois, storeStructures, droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute, poiOverlapExclude, dronePathZ, storeFrames]); // ํ˜„์žฌ ์žฌ์ƒ ์‹œ๊ฐ„ โ†’ ๋“œ๋ก  ํ”„๋ ˆ์ž„ ref ๊ฐฑ์‹  useEffect(() => { @@ -1131,31 +1276,32 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const py = (prevDpoi?.ry !== undefined && Math.abs(psy0 - prevDpoi.ry) <= 0.75) ? prevDpoi.ry : Math.round(psy0); d.rx = px; d.ry = py; displayedPoiRef.current.set(poiA.title, d); - const r = 10; - // ์‹ญ์ž ๋งˆ์ปค - ctx.strokeStyle = '#64c8ff'; ctx.lineWidth = 2.5; - ctx.beginPath(); - ctx.moveTo(px-r, py); ctx.lineTo(px+r, py); - ctx.moveTo(px, py-r); ctx.lineTo(px, py+r); - ctx.stroke(); - // ์ด๋ชจ์ง€ + ํ…์ŠคํŠธ - const emoji = CATEGORY_EMOJI[poiA.category] ?? '๐Ÿ“Œ'; + // ํ…์ŠคํŠธ ๋ฐ•์Šค๋งŒ ํ‘œ์ถœ(์‹ญ์ž ๋งˆ์ปค/์ด๋ชจ์ง€ ์•„์ด์ฝ˜ ์—†์Œ): + // ๋ฐฐ๊ฒฝ = KML ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ(poiA.color, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ํ•˜๋Š˜์ƒ‰), ๊ธ€์ž = ํฐ์ƒ‰. + // ๋ฐ•์Šค๋Š” POI ์ง€์ (px)์— ๊ฐ€๋กœ ์ค‘์•™ ์ •๋ ฌ. + const boxCol = poiA.color ?? '#64c8ff'; // ๋ผ๋ฒจ: ์†์„ฑ์— '๊ตฌ๋ถ„'์ด ์žˆ์œผ๋ฉด ๊ทธ ๊ฐ’(์›๋ฌธ ๊ทธ๋Œ€๋กœ), ์—†์œผ๋ฉด cleanTitle(title). poiCand์—์„œ ํ™•์ •. - const label = `${emoji} ${poiA.label}`; - const lx = Math.max(2, px + 14); - // ๋™์ผ์ขŒํ‘œ ๋ผ๋ฒจ ์„ธ๋กœ ๋ฐฐ์น˜: ํ–‰ ์ค‘์•™์„ ์‹ญ์ž์— ๋งž์ถฐ ์œ„โ†’์•„๋ž˜๋กœ ์•ˆ ๊ฒน์น˜๊ฒŒ ์˜คํ”„์…‹. - const ROW_H = 24; + const label = poiA.label; + // ๋™์ผ์ขŒํ‘œ ๋ผ๋ฒจ ์„ธ๋กœ ๋ฐฐ์น˜: ํ–‰ ์ค‘์•™์„ ์ง€์ ์— ๋งž์ถฐ ์œ„โ†’์•„๋ž˜๋กœ ์•ˆ ๊ฒน์น˜๊ฒŒ ์˜คํ”„์…‹. + const ROW_H = 28; const labelY = py + (poiA.labelRow - (poiA.labelRowCount - 1) / 2) * ROW_H; - ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 4; - ctx.lineJoin = 'round'; - ctx.textBaseline = 'middle'; - ctx.strokeText(label, lx, labelY); - ctx.fillStyle = '#64c8ff'; - ctx.fillText(label, lx, labelY); - // ํžˆํŠธ๋ฐ•์Šค(์‹ญ์ž ๋งˆ์ปค ~ ๊ธ€์ž ๋) โ€” ์•„์ด์ฝ˜ยท๊ธ€์ž ์–ด๋””๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ์„ ํƒ๋˜๊ฒŒ. (๋ผ๋ฒจ ํ–‰ ์œ„์น˜ ๊ธฐ์ค€) const tw = ctx.measureText(label).width; - hitBoxes.push({ kind: 'poi', title: poiA.title, x0: px - r, y0: labelY - 13, x1: lx + tw + 2, y1: labelY + 13 }); - const poiVisible = labelOnScreen(px - r, labelY - 13, lx + tw + 2, labelY + 13); + // ๋ฐ•์Šค ๋†’์ด = ๊ธ€์ž ํฌ๊ธฐ(20px) ร— 1.2 โ€” ๋ผ๋ฒจ ์ƒํ•˜ ์—ฌ์œ . ํ–‰ ๊ฐ„๊ฒฉ(ROW_H)๋„ ๊ฐ™์ด ๋ฒŒ๋ฆผ. + const PAD_X = 7, BOX_H = 24; + const bx0 = Math.max(2, px - tw / 2 - PAD_X); + const bx1 = bx0 + tw + PAD_X * 2; + const by0 = labelY - BOX_H / 2, by1 = labelY + BOX_H / 2; + ctx.fillStyle = boxCol; + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(bx0, by0, bx1 - bx0, BOX_H, 5); + else ctx.rect(bx0, by0, bx1 - bx0, BOX_H); + ctx.fill(); + ctx.fillStyle = '#ffffff'; + ctx.textBaseline = 'middle'; + ctx.fillText(label, bx0 + PAD_X, labelY); + // ํžˆํŠธ๋ฐ•์Šค = ํ…์ŠคํŠธ ๋ฐ•์Šค ์˜์—ญ (์–ด๋””๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ์„ ํƒ) + hitBoxes.push({ kind: 'poi', title: poiA.title, x0: bx0, y0: by0 - 2, x1: bx1 + 2, y1: by1 + 2 }); + const poiVisible = labelOnScreen(bx0, by0, bx1, by1); if (poiVisible) onScreenLabels.add(`poi:${poiA.title}`); // ์—ฐ์† ์˜จ์Šคํฌ๋ฆฐ ํ”„๋ ˆ์ž„ ์ˆ˜ ๊ฐฑ์‹ (ํ™”๋ฉด ๋ฐ–์ด๋ฉด 0์œผ๋กœ ๋ฆฌ์…‹). const streak = poiVisible ? (poiOnScreenStreakRef.current.get(poiA.title) ?? 0) + 1 : 0; @@ -1179,7 +1325,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible } // end if(cache) โ€” ์ค‘์‹ฌ์„ /๋“œ๋ก ๊ฒฝ๋กœ/์ธก์ /POI // ํŽธ์ง‘/FOV ๋ณด์ • ๋ชจ๋“œ: ๋“œ๋ž˜๊ทธ ์ค‘ ๋งˆ์ปค ํ”ผ๋“œ๋ฐฑ (๋งˆ์šฐ์Šค ๋”ฐ๋ผ๋‹ค๋‹˜) - if ((editModeRef.current || fovModeRef.current) && dragRef.current && dragPosRef.current) { + if ((editModeRef.current || fovModeRef.current || yawModeRef.current) && dragRef.current && dragPosRef.current) { const dx = vx(dragPosRef.current.x), dy = vy(dragPosRef.current.y); ctx.strokeStyle = '#ffe14d'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(dx, dy, 14, 0, Math.PI * 2); ctx.stroke(); @@ -1224,7 +1370,10 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // ์žก์œผ๋ฉด ๋ผ๋ฒจ ๋†’์ด๋งŒํผ ๋จนํ˜€ ํŒ์—…์ด ๊ธ€์ž์— ๋ถ™๋Š”๋‹ค โ†’ ๋ผ๋ฒจ '๋ฐ”๊นฅ ๊ฐ€์žฅ์ž๋ฆฌ' ๊ธฐ์ค€์œผ๋กœ ๋„์šด๋‹ค. const LABEL_HALF = 15; // ๋ผ๋ฒจ ๋ฐ˜๋†’์ด(px) โ€” sy ์ค‘์‹ฌ ยฑ ์ด ๊ฐ’์ด ๊ธ€์ž/์•„์ด์ฝ˜ ์˜์—ญ const GAP = 12; // ๋ผ๋ฒจ ๊ฐ€์žฅ์ž๋ฆฌ ~ ํŒ์—… ์‚ฌ์ด ์‹ค์ œ ์—ฌ๋ฐฑ - const tx = Math.min(Math.max(8, sx - 10), Math.max(8, W - pw - 4)); + // POI ๋ผ๋ฒจ์€ ์ง€์ (sx) ๊ธฐ์ค€ ๊ฐ€๋กœ ์ค‘์•™ ์ •๋ ฌ ๋ฐ•์Šค โ†’ ํŒ์—…๋„ ๋ฐ•์Šค ์ค‘์•™ ์•„๋ž˜๋กœ ์ •๋ ฌ. + // ์ธก์  ๋ผ๋ฒจ์€ ๋งˆ์ปค ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๊ทธ๋ ค์ง€๋ฏ€๋กœ ๊ธฐ์กด(์ขŒ์ธก ์—ฌ์œ  10px) ์œ ์ง€. + const txRaw = kind === 'poi' ? sx - pw / 2 : sx - 10; + const tx = Math.min(Math.max(8, txRaw), Math.max(8, W - pw - 4)); // ๊ธฐ๋ณธ์€ ๋ผ๋ฒจ '์•„๋ž˜'. ์•„๋ž˜ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•˜๋ฉด(ํ™”๋ฉด ํ•˜๋‹จ ๊ทผ์ ‘) ๋ผ๋ฒจ '์œ„'๋กœ ํ”Œ๋ฆฝ. // โ€ป ํžˆ์Šคํ…Œ๋ฆฌ์‹œ์Šค(sticky): ๊ฒฝ๊ณ„์—์„œ sy ๊ฐ€ ๋ฏธ์„ธํ•˜๊ฒŒ ์˜ค๋ฅด๋‚ด๋ ค๋„ ์œ„โ†”์•„๋ž˜๋กœ ์™•๋ณตํ•˜์ง€ ์•Š๋„๋ก, // '์œ„'๋กœ ๊ฐ„ ๋’ค์—” ์•„๋ž˜์— HYST ์ด์ƒ ์—ฌ์œ ๊ฐ€ ์ƒ๊ฒจ์•ผ๋งŒ '์•„๋ž˜'๋กœ ๋ณต๊ท€. (์™•๋ณต=ํŒ์—… ์ˆœ๊ฐ„์ด๋™=๋ฒˆ์ฉ์ž„ ๋ฐฉ์ง€) @@ -1299,7 +1448,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const { x: nx, y: ny } = clientToVideoNorm(e.clientX, e.clientY); // ํŽธ์ง‘/์„ธ๋กœํ™”๊ฐ ๋ณด์ •: POI ์„ ํƒ + ๋“œ๋ž˜๊ทธ - if (editModeRef.current || fovModeRef.current) { + if (editModeRef.current || fovModeRef.current || yawModeRef.current) { const canvas = canvasRef.current; if (!canvas) return; const hit = hitTestLabel(nx, ny); if (!hit) return; @@ -1366,15 +1515,15 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // ์˜์ƒ์ด ์ค€๋น„๋˜๊ธฐ ์ „(ํด๋” ์„ ํƒ ํ™”๋ฉด ๋“ฑ)์—” ์ž…๋ ฅ์„ ๋ฐ›์ง€ ์•Š์Œ โ†’ ํ•˜์œ„ UI(ํด๋” ์„ ํƒ/๋“œ๋กญ) ํด๋ฆญ ํ†ต๊ณผ. if (!videoReady) { canvas.style.pointerEvents = 'none'; return; } canvas.style.pointerEvents = 'auto'; - canvas.style.cursor = (editMode || fovMode) ? 'move' : 'default'; - if (editMode || fovMode) { setInfoPopups([]); return; } + canvas.style.cursor = (editMode || fovMode || yawMode) ? 'move' : 'default'; + if (editMode || fovMode || yawMode) { setInfoPopups([]); return; } const onMove = (e: MouseEvent) => { const { x: nx, y: ny } = clientToVideoNorm(e.clientX, e.clientY); canvas.style.cursor = hitTestLabel(nx, ny) ? 'pointer' : 'default'; }; parent.addEventListener('mousemove', onMove); return () => { parent.removeEventListener('mousemove', onMove); }; - }, [videoReady, editMode, fovMode, clientToVideoNorm, hitTestLabel]); + }, [videoReady, editMode, fovMode, yawMode, clientToVideoNorm, hitTestLabel]); const onPoiPointerUp = useCallback(() => { const drag = dragRef.current, pos = dragPosRef.current; @@ -1385,6 +1534,27 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const drone = getSmoothedDrone(); if (!drone) return; const p = paramsRef.current, wo = worldOriginRef.current; + // โ”€โ”€ Yaw(๋ฐฉ์œ„ ์˜คํ”„์…‹) ๋ณด์ • โ”€โ”€ + // ๋“œ๋กญํ•œ ํ™”๋ฉด์ ์˜ '๊ฐ€๋กœ'(px)๋งŒ ๋งž์ถ˜๋‹ค. ๋ผ๋ฒจ์˜ ์ ˆ๋Œ€ ๋ฐฉ์œ„๋Š” ๊ณ ์ •์ด๋ฏ€๋กœ, + // ํ˜„์žฌ ์นด๋ฉ”๋ผ์ถ• ๊ธฐ์ค€ ์ˆ˜ํ‰๊ฐ ฮธ0 = atan2(Xc, Zc) ๊ณผ ๋“œ๋กญ ์ง€์ ์ด ์š”๊ตฌํ•˜๋Š” + // ์ˆ˜ํ‰๊ฐ ฮธ1 = atan((px* โˆ’ 0.5 โˆ’ cx0)ยทsW / f) ์˜ ์ฐจ์ด๋งŒํผ ์นด๋ฉ”๋ผ ๋ฐฉ์œ„๋ฅผ ๋Œ๋ฆฌ๋ฉด ๋จ: + // yawOffset_new = yawOffset_old + (ฮธ0 โˆ’ ฮธ1) + if (yawModeRef.current) { + const poiZ = poiDroneHeightRef.current + ? drone.altitude - (p.geoidOffset ?? 0) - droneHeightDropRef.current + : drag.z0; + const cc = toCameraCoords(drone, drag.lat, drag.lon, poiZ, p, wo); + if (cc.Zc < 0.1) return; + const th0 = Math.atan2(cc.Xc, cc.Zc); + const th1 = Math.atan(((pos.x - 0.5 - (p.cx0 ?? 0)) * (p.sensorW ?? 36)) / p.focalLen); + const dYaw = (th0 - th1) * 180 / Math.PI; + if (!isFinite(dYaw) || Math.abs(dYaw) > 45) { alert('๋ณด์ •๋Ÿ‰์ด ๋น„์ •์ƒ์ ์œผ๋กœ ํฝ๋‹ˆ๋‹ค โ€” ์˜ฌ๋ฐ”๋ฅธ ์ง€๋ฌผ ์œ„์น˜๋กœ ๋Œ์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.'); return; } + const next = Math.max(-180, Math.min(180, (p.yawOffset ?? 0) + dYaw)); + setParam('yawOffset', Math.round(next * 10) / 10); + console.log(`[yaw] ๋“œ๋ž˜๊ทธ ์—ญ์‚ฐ: ฮ”${dYaw.toFixed(2)}ยฐ โ†’ Yawยฑ ${next.toFixed(1)}ยฐ`); + return; + } + // โ”€โ”€ ์„ธ๋กœ ํ™”๊ฐ(sensorH) ๋ณด์ • โ”€โ”€ // ๋“œ๋กญํ•œ ํ™”๋ฉด์ ์˜ '์„ธ๋กœ'(py)๋งŒ ๋งž์ถ˜๋‹ค. ๊ฐ€๋กœ(f/sW)๋Š” ์ด๋ฏธ ์ •ํ™•ํ•˜๋ฏ€๋กœ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ // (f ๋ฅผ ๋ฐ”๊พธ๋ฉด ๊ฐ€๋กœ๊นŒ์ง€ ํ‹€์–ด์ ธ ์Šคํ…Œ์ด์…˜์„ ์ด ์ขŒ์šฐ๋กœ ๋ฒ—์–ด๋‚จ). @@ -1632,26 +1802,32 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible {poiDroneHeight ? `โ–  ๋“œ๋ก  ๋†’์ด์™€ POI ์ด๊ฒฉ๊ฑฐ๋ฆฌ ${droneHeightDrop}m` : 'โ–ถ ๋“œ๋ก  ๋†’์ด์™€ POI ์ด๊ฒฉ๊ฑฐ๋ฆฌ'} {poiDroneHeight && ( - setDroneHeightDrop(Math.round(v))} /> + setDroneHeightDrop(Math.round(v))} /> )}
POI ์œ„์น˜ ํŽธ์ง‘ (๋งˆ์šฐ์Šค)
- - +
โ„น๏ธ ๋ผ๋ฒจ ํด๋ฆญ โ†’ ์†์„ฑ ํŒ์—…(์—ฌ๋Ÿฌ ๊ฐœ ๋ˆ„์ , ์žฌ์ƒ ๋”ฐ๋ผ ์ด๋™). ๋นˆ ๊ณณ ํด๋ฆญ โ†’ ํŒ์—… ๋‹ซ๊ธฐ, ํŒ์—… ์—†์œผ๋ฉด ์žฌ์ƒ/์ •์ง€.
{dragTitle ? `๋“œ๋ž˜๊ทธ ์ค‘: ${dragTitle}` : fovMode ? `๐ŸŽฏ POI๋ฅผ ์‹ค์ œ ์œ„์น˜๋กœ ๋Œ๋ฉด ์„ธ๋กœํ™”๊ฐ ์ž๋™๋ณด์ • (ํ˜„์žฌ senH=${(params.sensorH ?? 20.25).toFixed(2)}mm)` + : yawMode ? `๐Ÿงญ POI๋ฅผ ์‹ค์ œ ์œ„์น˜(์ขŒ์šฐ)๋กœ ๋Œ๋ฉด Yaw ์ž๋™๋ณด์ • (ํ˜„์žฌ Yawยฑ=${params.yawOffset.toFixed(1)}ยฐ)` : `๋ณด์ •๋œ POI ${Object.keys(storePoiOverrides).length}๊ฐœ`}
{editMode && selPoi && ( @@ -1700,6 +1876,13 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
์ž์„ธ ๋ณด์ • ์˜คํ”„์…‹(SRT + offset)
setParam('yawOffset', v)} /> + {/* Yaw ์ž๋™ ์ถ”์ •(2๋‹จ๊ณ„ ๋ณด์ •์˜ 1๋‹จ๊ณ„) โ€” ๋Œ€๋žต ๋งž์ถ˜ ๋’ค 'Yaw ๋ณด์ •' ๋“œ๋ž˜๊ทธ ๋ชจ๋“œ๋กœ ๋งˆ๋ฌด๋ฆฌ */} + setParam('pitch', v)} /> setParam('roll', v)} />
@@ -1712,6 +1895,30 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
์ •ํ‘œ๊ณ โ†’ํƒ€์›์ฒด๊ณ  (๋Œ€์ „โ‰ˆ25.8)
๋‚ด๋ถ€ํ‘œ์ • (์ดˆ์ ยท์ฃผ์ ยท์„ผ์„œ)
+ {/* ์˜์ƒ(djmd) ์ž๋™ ๊ฐ์ง€ ์ •๋ณด + ์นด๋ฉ”๋ผ๊ฐ’ ์„œ๋ฒ„ ์ €์žฅ('<์˜์ƒ base>.camera.json' โ€” ์žฌ๋กœ๋“œ ์‹œ ์ž๋™ ์ ์šฉ) */} +
+ + ๐Ÿ“ท {storeCameraInfo + ? `${storeCameraInfo.model}${storeCameraInfo.focalLen35 ? ` ยท ${storeCameraInfo.focalLen35}mm` : ''}${storeCameraInfo.fps ? ` ยท ${storeCameraInfo.fps}fps` : ''}` + : '์นด๋ฉ”๋ผ ๋ฏธ๊ฐ์ง€'} + +
+ {storeCameraInfo?.focalLen35 !== undefined && ( + + )} + +
+
setParam('focalLen', v)} /> setParam('cx0', v)} /> diff --git a/client/src/components/player/VideoPlayer.tsx b/client/src/components/player/VideoPlayer.tsx index 4950cc1..6e2328a 100644 --- a/client/src/components/player/VideoPlayer.tsx +++ b/client/src/components/player/VideoPlayer.tsx @@ -43,6 +43,12 @@ export interface VideoPlayerHandle { getVideoElement: () => HTMLVideoElement | null; } +/** ์ดˆ โ†’ "m:ss" (์žฌ์ƒ๋ชฉ๋ก ๊ธธ์ด ํ‘œ์‹œ์šฉ). ๋ฏธ์ƒ์ด๋ฉด "--:--". */ +function fmtDur(d: number | null | undefined): string { + if (d == null || !isFinite(d)) return '--:--'; + return `${Math.floor(d / 60)}:${String(Math.floor(d % 60)).padStart(2, '0')}`; +} + interface VideoPlayerProps { onAddMemo: (time: number) => void; onToggleHelp?: () => void; @@ -119,6 +125,12 @@ const VideoPlayer = forwardRef( const geoLoaded = useGeoStore((s) => s.loaded); // ๋“œ๋ก  ์ •๋ณด โ€” ํ•˜๋‹จ๋ฐ”์— GPS/๊ณ ๋„ ํ•ญ์ƒ ํ‘œ์‹œ. const storeFrames = useGeoStore((s) => s.frames); + // ๋ถ„ํ•  ์˜์ƒ ์žฌ์ƒ๋ชฉ๋ก โ€” ํด๋” ๋กœ๋“œ ์‹œ ์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ. ๋ชฉ๋ก ํด๋ฆญ์œผ๋กœ ์„ธ๊ทธ๋จผํŠธ ์ง์ ‘ ์„ ํƒ ์žฌ์ƒ. + const videoFiles = useGeoStore((s) => s.videoFiles); + const videoDurations = useGeoStore((s) => s.videoDurations); + const videoIndex = useGeoStore((s) => s.videoIndex); + // ์˜์ƒ๋ชฉ๋ก ์ฝค๋ณด(์ขŒ์ƒ๋‹จ ํƒ€์ดํ‹€ ์•„๋ž˜) ํŽผ์นจ ์ƒํƒœ โ€” ๊ธฐ๋ณธ ์ ‘ํž˜. + const [showPlaylist, setShowPlaylist] = useState(false); // ์‹œ์„ค๋“ฑ๊ธ‰(์‹œ์„ค์ข…๋ณ„) ํ‘œ์‹œ ํ•„ํ„ฐ โ€” ์žฌ์ƒ๋ฐ”/ํŒจ๋„์ด ๊ตฌ๋…ํ•˜๋Š” ์„ค์ • ์Šคํ† ์–ด. const gradeFilter = useSettingsStore((s) => s.gradeFilter); const setGradeFilter = useSettingsStore((s) => s.setGradeFilter); @@ -155,6 +167,48 @@ const VideoPlayer = forwardRef( } }; + // ์žฌ์ƒ๋ชฉ๋ก์—์„œ ์„ธ๊ทธ๋จผํŠธ ์„ ํƒ โ€” ๊ฐ™์€ ํ•ญ๋ชฉ์ด๋ฉด ์ฒ˜์Œ๋ถ€ํ„ฐ ์žฌ์ƒ, ๋‹ค๋ฅธ ํ•ญ๋ชฉ์ด๋ฉด + // ๋“œ๋ก  ๋กœ๊ทธ๋ฅผ ํ•ด๋‹น ๊ตฌ๊ฐ„์œผ๋กœ ์žฌ์ •๋ ฌ(loadSegment)ํ•œ ๋’ค ์ „ํ™˜ ์žฌ์ƒ. + const handleSelectSegment = async (i: number): Promise => { + const geo = useGeoStore.getState(); + if (usePlayerStore.getState().source?.kind !== 'local') return; + if (i === geo.videoIndex) { + playerRef.current?.currentTime(0); + } else { + const file = await geo.loadSegment(i); + if (!file) return; + loadLocalFile(file); + } + const p = playerRef.current?.play(); + if (p && typeof p.catch === 'function') p.catch(() => {}); + }; + + // ๋ถ„ํ•  ์˜์ƒ ์—ฐ์†์žฌ์ƒ โ€” ์˜์ƒ์ด ๋๋‚˜๋ฉด ๊ฐ™์€ ํด๋”์˜ ๋‹ค์Œ ์˜์ƒ(์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ)์„ ์ž๋™ ์žฌ์ƒ. + // ๋“œ๋ก  ๋กœ๊ทธ(์‹œ๊ฐ„ ๊ธฐ๋ฐ˜)๋Š” loadSegment ๊ฐ€ ๋ˆ„์  ์˜คํ”„์…‹์œผ๋กœ ์žฌ์ •๋ ฌํ•ด ๊ถค์  ์ •ํ•ฉ์„ ์œ ์ง€ํ•œ๋‹ค. + useEffect(() => { + const player = playerRef.current; + if (!player) return; + const onEnded = (): void => { + void (async () => { + const geo = useGeoStore.getState(); + if (!geo.loaded || geo.videoFiles.length < 2) return; + // ํด๋” ๊ธฐ๋ฐ˜ ๋กœ์ปฌ ์žฌ์ƒ์—์„œ๋งŒ ๋™์ž‘ (์„œ๋ฒ„ ์ŠคํŠธ๋ฆผ/๋‹จ์ผ ํŒŒ์ผ์€ ๋Œ€์ƒ ์•„๋‹˜) + if (usePlayerStore.getState().source?.kind !== 'local') return; + const next = geo.videoIndex + 1; + if (next >= geo.videoFiles.length) return; + const file = await geo.loadSegment(next); + if (!file) return; + loadLocalFile(file); + // ended ์งํ›„๋Š” paused ์ƒํƒœ๋ผ loadLocalFile ์˜ ์ด์–ด์žฌ์ƒ ๋กœ์ง์ด ์•ˆ ๊ฑธ๋ฆผ โ†’ ์ง์ ‘ ์žฌ์ƒ. + const p = player.play(); + if (p && typeof p.catch === 'function') p.catch(() => {}); + })(); + }; + player.on('ended', onEnded); + return () => { player.off('ended', onEnded); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playerRef, loadLocalFile]); + // Expose methods to parent via ref useImperativeHandle(ref, () => ({ loadLocalFile, @@ -425,6 +479,54 @@ const VideoPlayer = forwardRef( />
+ {/* ์˜์ƒ ๋ชฉ๋ก ์ฝค๋ณด โ€” ์ขŒ์ƒ๋‹จ ํƒ€์ดํ‹€(๋…ธ์„  ๋ฐฐ๋„ˆ) ์•„๋ž˜. ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชฉ๋ก์ด ์•„๋ž˜๋กœ ํŽผ์ณ์ง€๊ณ  + ํ•ญ๋ชฉ ์„ ํƒ ์‹œ ํ•ด๋‹น ์˜์ƒ์œผ๋กœ ์ฆ‰์‹œ ์ „ํ™˜(๋“œ๋ก  ๋กœ๊ทธ๋„ ์„ธ๊ทธ๋จผํŠธ์— ๋งž์ถฐ ์ž๋™ ์žฌ์ •๋ ฌ). + ํ˜„์žฌ ์žฌ์ƒ ํ•ญ๋ชฉ ์žฌ์„ ํƒ ์‹œ ์ฒ˜์Œ๋ถ€ํ„ฐ ์žฌ์ƒ. */} + {source?.kind === 'local' && videoFiles.length > 0 && ( +
+ + {showPlaylist && ( +
    + {videoFiles.map((f, i) => ( +
  • + +
  • + ))} +
+ )} +
+ )} + {/* Empty state placeholder โ€” ๊ฐ€์šด๋ฐ ๋ฐ˜ํˆฌ๋ช… ํด๋” ์„ ํƒ */} {!source && (
diff --git a/client/src/stationbar/StationBar.tsx b/client/src/stationbar/StationBar.tsx index 365ee76..ce6fc52 100755 --- a/client/src/stationbar/StationBar.tsx +++ b/client/src/stationbar/StationBar.tsx @@ -33,11 +33,13 @@ interface GeoPoint { type: string; } -/** ํ”„๋ ˆ์ž„๋ณ„ precompute ๊ฒฐ๊ณผ. km=๋ฐฐ์ง€์šฉ ์ตœ๊ทผ์ ‘์ธก์ , chain=๊ตฌ๊ฐ„๋ฐฉํ–ฅ์šฉ ์—ฐ์† ์ฒด์ด๋‹ˆ์ง€. */ +/** ํ”„๋ ˆ์ž„๋ณ„ precompute ๊ฒฐ๊ณผ. km=๋ฐฐ์ง€์šฉ ์ตœ๊ทผ์ ‘์ธก์ , chain=๊ตฌ๊ฐ„๋ฐฉํ–ฅ์šฉ ์—ฐ์† ์ฒด์ด๋‹ˆ์ง€, + * offM=์ธก์  ํด๋ฆฌ๋ผ์ธ๊นŒ์ง€ ์ˆ˜ํ‰๊ฑฐ๋ฆฌ(m) โ€” ๋…ธ์„  ๋ฐ–(์ ‘๊ทผ/์ดํƒˆ) ํŒ๋ณ„์šฉ. */ interface ViewedPoint { frameNum: number; km: number; chain: number; + offM: number; time: number; } @@ -47,9 +49,10 @@ interface StationLine { k: number; // ๊ฒฝ๋„โ†’m ํ™˜์‚ฐ (cos(lat0)*111000) } -/** ๋“œ๋ก  lat/lon ์„ ์ธก์  ํด๋ฆฌ๋ผ์ธ์— ํˆฌ์˜ โ†’ ์—ฐ์† ์ฒด์ด๋‹ˆ์ง€(m). - * 100m ์–‘์žํ™”๋œ ์ตœ๊ทผ์ ‘์ธก์ ๊ณผ ๋‹ฌ๋ฆฌ ์ „์ง„/ํ›„์ง„ ์ „ํ™˜ ์‹œ์ ์ด ์‹ค์ œ ์ด๋™๊ณผ ์ผ์น˜. */ -function projectChainage(lat: number, lon: number, line: StationLine): number { +/** ๋“œ๋ก  lat/lon ์„ ์ธก์  ํด๋ฆฌ๋ผ์ธ์— ํˆฌ์˜ โ†’ ์—ฐ์† ์ฒด์ด๋‹ˆ์ง€(m) + ํด๋ฆฌ๋ผ์ธ๊นŒ์ง€ ์ˆ˜ํ‰๊ฑฐ๋ฆฌ(m). + * 100m ์–‘์žํ™”๋œ ์ตœ๊ทผ์ ‘์ธก์ ๊ณผ ๋‹ฌ๋ฆฌ ์ „์ง„/ํ›„์ง„ ์ „ํ™˜ ์‹œ์ ์ด ์‹ค์ œ ์ด๋™๊ณผ ์ผ์น˜. + * distM ์€ ๋…ธ์„  ๋ฐ–(์ ‘๊ทผ/์ดํƒˆ) ํŒ๋ณ„์— ์‚ฌ์šฉ โ€” ๋ฐ–์—์„œ๋Š” chain ์ด ๋์ ์— ํด๋žจํ”„๋จ. */ +function projectChainageDist(lat: number, lon: number, line: StationLine): { chain: number; distM: number } { const px = lon * line.k; const py = lat * 111000; const pts = line.pts; @@ -70,7 +73,11 @@ function projectChainage(lat: number, lon: number, line: StationLine): number { bestKm = a.km + (b.km - a.km) * t; } } - return bestKm; + return { chain: bestKm, distM: isFinite(bestD) ? Math.sqrt(bestD) : Infinity }; +} + +function projectChainage(lat: number, lon: number, line: StationLine): number { + return projectChainageDist(lat, lon, line).chain; } /** ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํŠธ๋ž™ ๊ตฌ๊ฐ„: km ์ฆ๊ฐ€(dir 1, ์ฃผํ™ฉ) / ๊ฐ์†Œ(dir -1, ํ•˜๋Š˜์ƒ‰). px๋Š” ์‹œ๊ฐ„์ถ•. */ @@ -232,32 +239,79 @@ export function StationBar({ useEffect(() => { const frames = storeFrames; - if (!frames.length || !stations.length || !stationLine) { setReady(false); return; } + if (!frames.length || !stations.length || !stationLine) { + // ์ธก์  ์—†๋Š” ๋ฐ์ดํ„ฐ์…‹(์˜ˆ: ์ œ์ฃผ DJI ๋กœ๊ทธ) ํด๋ฐฑ โ€” ์ธก์  ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ(๋ฐฐ์ง€/๊ตฌ๊ฐ„์ƒ‰/๋งˆํฌ)์€ + // ready=false ๋กœ ๋น„ํ™œ์„ฑ์ด์ง€๋งŒ, ์ด๋™๊ฑฐ๋ฆฌ์ถ•(chainRef)์€ ๋“œ๋ก  GPS ๋ˆ„์  ์ด๋™๊ฑฐ๋ฆฌ๋กœ ๊ตฌ์„ฑํ•œ๋‹ค. + // โ†’ ์ปค์„œ ์ง„ํ–‰๋„๊ฐ€ ์‹œ๊ฐ„ ์„ ํ˜•์ด ์•„๋‹Œ ์‹ค์ œ ์ด๋™๋Ÿ‰์„ ๋”ฐ๋ผ๊ฐ€ ํ˜ธ๋ฒ„(์ •์ง€) ์‹œ ์ปค์„œ๋„ ๋ฉˆ์ถ˜๋‹ค. + setReady(false); + viewedRef.current = []; + if (!frames.length) { chainRef.current = null; return; } + const n = frames.length; + const W = 8; // ํ‰ํ™œ ๋ฐ˜ํญ(ํ”„๋ ˆ์ž„) โ€” GPS ์ง€ํ„ฐ๊ฐ€ ์ด๋™๊ฑฐ๋ฆฌ๋กœ ๋ˆ„์ ๋˜๋Š” ๊ฒƒ ๋ฐฉ์ง€(์ธก์  ๊ฒฝ๋กœ์™€ ๋™์ผ ์ •์ฑ…) + const preLat = new Float64Array(n + 1); + const preLon = new Float64Array(n + 1); + for (let i = 0; i < n; i++) { + preLat[i + 1] = preLat[i] + frames[i].lat; + preLon[i + 1] = preLon[i] + frames[i].lon; + } + const cosLat = Math.cos((frames[0].lat * Math.PI) / 180); + const time = new Float64Array(n); + const frac = new Float64Array(n); + let cum = 0, pLat = 0, pLon = 0; + for (let i = 0; i < n; i++) { + const lo = Math.max(0, i - W), hi = Math.min(n, i + W + 1); + const la = (preLat[hi] - preLat[lo]) / (hi - lo); + const ln = (preLon[hi] - preLon[lo]) / (hi - lo); + if (i > 0) cum += Math.hypot((la - pLat) * 111000, (ln - pLon) * 111000 * cosLat); + pLat = la; pLon = ln; + time[i] = frames[i].frame / videoFps; + frac[i] = cum; + } + const total = cum > 0 ? cum : 1; + for (let i = 0; i < n; i++) frac[i] /= total; + // depChain/arrChain ์€ ๋ฐฉํ–ฅ์ƒ‰(์ธก์  ๊ธฐ๋ฐ˜) ์ „์šฉ โ€” ํด๋ฐฑ์—์„  ์˜๋ฏธ ์—†์Œ(๋‹จ์กฐ ์ฆ๊ฐ€ ๊ฐ€์ •๊ฐ’). + chainRef.current = { time, frac, depChain: 0, arrChain: 1 }; + return; + } const n = frames.length; const out: ViewedPoint[] = new Array(n); for (let i = 0; i < n; i++) { const f = frames[i]; + const pj = projectChainageDist(f.lat, f.lon, stationLine); out[i] = { frameNum: f.frame, km: nearestStationKm(f, stations), - chain: projectChainage(f.lat, f.lon, stationLine), + chain: pj.chain, + offM: pj.distM, time: f.frame / videoFps, }; } - // ์ด๋™๊ฑฐ๋ฆฌ ์ถ•: ํ‰ํ™œ ์ธก์ ๊ฐ’(ยฑW ์ด๋™ํ‰๊ท )์˜ ํ”„๋ ˆ์ž„๊ฐ„ |ฮ”| ๋ˆ„์  = ์‹ค์ œ ์ด๋™๋Ÿ‰ โ†’ ์ „์ฒด๋กœ ์ •๊ทœํ™”(frac). - // ํ˜ธ๋ฒ„(์ด๋™ ็„ก)๋ฉด frac ์ •์ฒด โ†’ ์ปค์„œ ์ •์ง€. ์ด๋™ํ•˜๋ฉด(์ „์ง„/ํ›„์ง„ ๋ฌด๊ด€) frac ์ฆ๊ฐ€ โ†’ ์ปค์„œ ํ•ญ์ƒ ์šฐ์ธก. - // depChain/arrChain(์•žยท๋’ค 5% ํ‰๊ท )์€ ๋ฐฉํ–ฅ์ƒ‰ ๊ธฐ์ค€. + // ์ด๋™๊ฑฐ๋ฆฌ ์ถ•(frac): ์ฒด์ด๋‹ˆ์ง€ |ฮ”| ๊ฐ€ ์•„๋‹ˆ๋ผ GPS ์‹ค์ด๋™๊ฑฐ๋ฆฌ ๋ˆ„์ (ยฑW ์ด๋™ํ‰๊ท  ํ‰ํ™œ). + // ์ฒด์ด๋‹ˆ์ง€ ํˆฌ์˜์€ ๋…ธ์„  ๋ฐ–(์ ‘๊ทผ/์ดํƒˆ ๊ตฌ๊ฐ„)์—์„œ ํด๋ฆฌ๋ผ์ธ ๋์ (0k000 ๋“ฑ)์— ํด๋žจํ”„๋˜์–ด + // ๋“œ๋ก ์ด ์›€์ง์—ฌ๋„ ์ปค์„œ๊ฐ€ ๋ฉˆ์ถ”๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Œ(์ œ์ฃผ โ€” ๋น„ํ–‰ ์‹œ์ž‘์ ์ด ๋…ธ์„  ๋ฐ– 700m). + // GPS ๋ˆ„์ ๊ฑฐ๋ฆฌ๋Š” ํ˜ธ๋ฒ„ ์‹œ ์ •์ฒด(์ปค์„œ ์ •์ง€) + ๋…ธ์„  ์•ˆํŒŽ ๋ฌด๊ด€ํ•˜๊ฒŒ ์‹ค์ œ ์ด๋™์„ ๋”ฐ๋ผ๊ฐ„๋‹ค. + // depChain/arrChain(์•žยท๋’ค 5% ํ‰๊ท , ์ฒด์ด๋‹ˆ์ง€ ๊ธฐ์ค€)์€ ๋ฐฉํ–ฅ์ƒ‰ ํŒ์ •์—๋งŒ ์‚ฌ์šฉ. const W = 8; // ํ‰ํ™œ ๋ฐ˜ํญ(ํ”„๋ ˆ์ž„) const pre = new Float64Array(n + 1); - for (let i = 0; i < n; i++) pre[i + 1] = pre[i] + out[i].chain; + const preLat = new Float64Array(n + 1); + const preLon = new Float64Array(n + 1); + for (let i = 0; i < n; i++) { + pre[i + 1] = pre[i] + out[i].chain; + preLat[i + 1] = preLat[i] + frames[i].lat; + preLon[i + 1] = preLon[i] + frames[i].lon; + } + const cosLat = Math.cos((frames[0].lat * Math.PI) / 180); const time = new Float64Array(n); const sm = new Float64Array(n); const frac = new Float64Array(n); - let cum = 0; + let cum = 0, pLat = 0, pLon = 0; for (let i = 0; i < n; i++) { const lo = Math.max(0, i - W), hi = Math.min(n, i + W + 1); sm[i] = (pre[hi] - pre[lo]) / (hi - lo); - if (i > 0) cum += Math.abs(sm[i] - sm[i - 1]); + const la = (preLat[hi] - preLat[lo]) / (hi - lo); + const ln = (preLon[hi] - preLon[lo]) / (hi - lo); + if (i > 0) cum += Math.hypot((la - pLat) * 111000, (ln - pLon) * 111000 * cosLat); + pLat = la; pLon = ln; time[i] = out[i].time; frac[i] = cum; } @@ -297,6 +351,13 @@ export function StationBar({ return idx >= 0 ? viewedRef.current[idx].chain : null; }, [currentTime, ready, viewedIdxAtTime]); + // ํ˜„์žฌ ๋“œ๋ก ์˜ ๋…ธ์„ (์ธก์  ํด๋ฆฌ๋ผ์ธ) ์ดํƒˆ ๊ฑฐ๋ฆฌ(m) โ€” ์ ‘๊ทผ/์ดํƒˆ ๊ตฌ๊ฐ„ ๋ฐฐ์ง€ ํ‘œ๊ธฐ์šฉ. + const realOffM = useMemo(() => { + if (!ready) return null; + const idx = viewedIdxAtTime(currentTime); + return idx >= 0 ? viewedRef.current[idx].offM : null; + }, [currentTime, ready, viewedIdxAtTime]); + // ์ข…์ ์—ญ '๋ฏธ๋„์ฐฉ' ํญ(px) โ€” ์˜์ƒ์ด ์ข…์ ์—ญ ๋„์ฐฉ ์ „ ๋๋‚  ๋•Œ, ํŠธ๋ž™ ์šฐ์ธก์— ์ด๋งŒํผ ๋น„์›Œ๋‘”๋‹ค. // ์˜์ƒ ํƒ€์ž„๋ผ์ธ์€ [START โ€ฆ ENDโˆ’endGapPx] ์—๋งŒ ๋งคํ•‘๋˜๊ณ , ์ข…์ ์—ญ ๋งˆ์ปค๋Š” END(๋ฏธ๋„์ฐฉ)์— ๋‘”๋‹ค. // ํญ = ๋ฏธ๋„์ฐฉ๊ฑฐ๋ฆฌ(m) / ๋…ธ์„ ๊ธธ์ด(m) ร— ํŠธ๋ž™ํญ (์˜ต์…˜ 2: ์‹ค์ œ ๋น„๋ก€). ์•ˆ์ „์ƒ 15%๋กœ ์ƒํ•œ. @@ -335,11 +396,12 @@ export function StationBar({ return null; }, [routeMeta, directionChanges, duration]); - // ์ง„ํ–‰๋„(์ด๋™๊ฑฐ๋ฆฌ์ถ•): ์‹œ๊ฐ„ t โ†’ ๋ˆ„์  ์ด๋™๊ฑฐ๋ฆฌ ๋น„์œจ(frac, 0~1). chainRef ๋ฏธ์ค€๋น„ ์‹œ ์‹œ๊ฐ„์„ ํ˜• ํด๋ฐฑ. + // ์ง„ํ–‰๋„(์ด๋™๊ฑฐ๋ฆฌ์ถ•): ์‹œ๊ฐ„ t โ†’ ๋ˆ„์  ์ด๋™๊ฑฐ๋ฆฌ ๋น„์œจ(frac, 0~1). chainRef ๋ฏธ๊ตฌ์„ฑ ์‹œ ์‹œ๊ฐ„์„ ํ˜• ํด๋ฐฑ. + // chainRef ๋Š” ์ธก์  ์œ ๋ฌด์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋“œ๋ก  ํ”„๋ ˆ์ž„๋งŒ ์žˆ์œผ๋ฉด ๊ตฌ์„ฑ๋œ๋‹ค(์œ„ ํด๋ฐฑ) โ†’ ready ๋น„์˜์กด. const cumFracAtTime = useCallback( (t: number): number => { const c = chainRef.current; - if (!c || !ready || duration <= 0) + if (!c || duration <= 0) return duration > 0 ? clamp(t / duration, 0, 1) : 0; const { time, frac } = c; const n = time.length; @@ -355,10 +417,11 @@ export function StationBar({ [ready, duration], ); // ์—ญ๋ณ€ํ™˜(์ด๋™๊ฑฐ๋ฆฌ ๋น„์œจ โ†’ ์‹œ๊ฐ„): ๋ฐ” ํด๋ฆญ seek ์šฉ. frac ๋‹จ์กฐ ๋น„๊ฐ์†Œ โ†’ ์ด์ง„ ํƒ์ƒ‰. + // cumFracAtTime ๊ณผ ๋™์ผํ•˜๊ฒŒ ready ๋น„์˜์กด(์ธก์  ์—†๋Š” ํด๋ฐฑ chainRef ๋„ ์‚ฌ์šฉ). const timeAtFrac = useCallback( (f: number): number => { const c = chainRef.current; - if (!c || !ready || duration <= 0) return clamp(f, 0, 1) * duration; + if (!c || duration <= 0) return clamp(f, 0, 1) * duration; const { time, frac } = c; const n = frac.length; if (n === 0) return 0; @@ -387,8 +450,15 @@ export function StationBar({ const cursorPx = progressPx; // ์ปค์„œ ๋ฐฐ์ง€ = ํด๋” ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์—ฐ์† ์ฒด์ด๋‹ˆ์ง€(realChain)๋ฅผ 10m ๋‹จ์œ„๋กœ ํ‘œ์‹œ. ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด. + // ๋“œ๋ก ์ด ๋…ธ์„ (์ธก์  ํด๋ฆฌ๋ผ์ธ)์—์„œ OFF_ROUTE_M ์ด์ƒ ๋ฒ—์–ด๋‚œ ๊ตฌ๊ฐ„(์ ‘๊ทผ/์ดํƒˆ)์€ ์ฒด์ด๋‹ˆ์ง€๊ฐ€ + // ๋์ ์— ํด๋žจํ”„๋˜์–ด ๋ฌด์˜๋ฏธ โ†’ "๋…ธ์„ ๋ฐ– โ—‹โ—‹m" ๋กœ ํ‘œ๊ธฐํ•ด ์ธก์ ์ด ์•ˆ ๋ณ€ํ•˜๋Š” ์ด์œ ๋ฅผ ๋“œ๋Ÿฌ๋‚ธ๋‹ค. + const OFF_ROUTE_M = 60; const cursorText = - realChain !== null && realChain >= 0 ? formatMileage10(realChain) : ''; + realChain !== null && realChain >= 0 + ? (realOffM !== null && realOffM > OFF_ROUTE_M + ? `๋…ธ์„ ๋ฐ– ${Math.round(realOffM)}m` + : formatMileage10(realChain)) + : ''; // viewedRef(ํ”„๋ ˆ์ž„โ†’์ธก์  km) ์ถ”์ด๋กœ ๊ตฌ๊ฐ„ ์ƒ‰ยท์ „ํ™˜์  ์‚ฐ์ถœ. // km ์ฆ๊ฐ€ ๊ตฌ๊ฐ„ = dir 1(์ฃผํ™ฉ), ๊ฐ์†Œ ๊ตฌ๊ฐ„ = dir -1(ํ•˜๋Š˜์ƒ‰). HYST๋กœ ์ตœ๊ทผ์ ‘ ์ง€ํ„ฐ ๋ฌด์‹œ. @@ -498,6 +568,13 @@ export function StationBar({ if (d < gmin) gmin = d; } if (!isFinite(gmin)) return []; + // ์ด ์˜์ƒ(์„ธ๊ทธ๋จผํŠธ)์—์„œ ์‹ค์ œ๋กœ ๊ทผ์ ‘ ํ†ต๊ณผํ•˜์ง€ ์•Š์€ ๊ตฌ์กฐ๋ฌผ์€ ๋งˆํฌ ์ƒ๋žต. + // ์ข…์ „์—” TH ๊ฐ€ ์ตœ๊ทผ์ ‘๊ฑฐ๋ฆฌ์— ๋น„๋ก€ํ•ด ๋Š˜์–ด๋‚˜(gminร—3) ์ˆ˜ km ๋ฐ– ๊ตฌ์กฐ๋ฌผ๋„ ์ตœ๊ทผ์ ‘ + // ํ”„๋ ˆ์ž„(์„ธ๊ทธ๋จผํŠธ ๋)์— ๋งˆํฌ๊ฐ€ ์ฐํ˜€, ์‹ค์ œ ํ†ต๊ณผ ๊ตฌ์กฐ๋ฌผ๊ณผ ๊ฒน์ณ ๋ณด์˜€์Œ + // (์ œ์ฃผ: ์„ธ๊ทธ1 ๋ ๊ท€๋•์ฒœ๊ต ์œ„์— ๊ธˆ์„ฑ์ฒœ๊ตยท์–ด์Œ์ฒœ๊ต๊ฐ€ ๊ฒน์นจ โ€” ์ด๋“ค์€ ์„ธ๊ทธ3 ํ†ต๊ณผ๋ถ„). + // ํ•ด๋‹น ๊ตฌ์กฐ๋ฌผ์€ ํ†ต๊ณผํ•˜๋Š” ์„ธ๊ทธ๋จผํŠธ์˜ ๋ฐ”์—์„œ ์ •์ƒ ํ‘œ์‹œ๋œ๋‹ค. + const PASS_MAX_M = 300; + if (gmin > PASS_MAX_M) return []; const TH = Math.max(100, gmin * 3); // ํ†ต๊ณผ ์ธ์ • ๊ฑฐ๋ฆฌ(m) // ํ†ต๊ณผ ํ”„๋ ˆ์ž„์—์„œ ์ง„ํ–‰๋ฐฉํ–ฅ์œผ๋กœ offsetM ๋งŒํผ ์ด๋™(GPS ๋ˆ„์ ๊ฑฐ๋ฆฌ ๊ธฐ์ค€). const shift = (idx: number): number => { diff --git a/client/src/stationbar/components/TimelineCursor/TimelineCursor.module.scss b/client/src/stationbar/components/TimelineCursor/TimelineCursor.module.scss index d797f12..ceae280 100755 --- a/client/src/stationbar/components/TimelineCursor/TimelineCursor.module.scss +++ b/client/src/stationbar/components/TimelineCursor/TimelineCursor.module.scss @@ -41,9 +41,11 @@ align-items: center; justify-content: center; color: #fff; - font-size: 20px; + /* "๋…ธ์„ ๋ฐ– 737m" ๊ฐ™์€ ๊ธด ํ‘œ๊ธฐ๋„ ํ•œ ์ค„ ์œ ์ง€ โ€” ๊ธ€์ž ์ถ•์†Œ + ์ค„๋ฐ”๊ฟˆ ๊ธˆ์ง€(ํญ์€ ๋‚ด์šฉ์— ๋งž๊ฒŒ ๋Š˜์–ด๋‚จ) */ + font-size: 17px; font-weight: 700; letter-spacing: 0.4px; + white-space: nowrap; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), 0 2px 6px rgba(0, 0, 0, 0.45); diff --git a/client/src/store/geoStore.ts b/client/src/store/geoStore.ts index cbd47b5..26027d5 100644 --- a/client/src/store/geoStore.ts +++ b/client/src/store/geoStore.ts @@ -23,8 +23,9 @@ import type { DirectionChange, PoiOverride, PoiOverrideMap, + DetectedCameraInfo, } from '../types/geo'; -import { loadFolderGeoData, applyPoiOverrides } from '../utils/geoData'; +import { loadFolderGeoData, applyPoiOverrides, parseDroneFrames } from '../utils/geoData'; interface GeoStore { loaded: boolean; @@ -43,12 +44,30 @@ interface GeoStore { directionChanges: DirectionChange[]; /** ๋งˆ์ง€๋ง‰ ํด๋” ๋กœ๋“œ์—์„œ KMZ(POIยท๊ตฌ์กฐ๋ฌผ ์›๋ณธ)๊ฐ€ ๋ˆ„๋ฝ๋๋Š”์ง€. true๋ฉด ์žฌ๊ตฌ์ถ• ํ•„์š”. */ kmzMissing: boolean; + /** ํด๋” ๋‚ด ์˜์ƒ ํŒŒ์ผ ์ „์ฒด(์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ) โ€” ๋ถ„ํ• ๋ณธ ์—ฐ์†์žฌ์ƒ์šฉ. */ + videoFiles: File[]; + /** videoFiles ๊ฐ ์˜์ƒ ๊ธธ์ด(์ดˆ). ์ธก์ • ์‹คํŒจ ์‹œ null. */ + videoDurations: (number | null)[]; + /** ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ์„ธ๊ทธ๋จผํŠธ ์ธ๋ฑ์Šค (videoFiles ๊ธฐ์ค€). */ + videoIndex: number; + /** ํด๋” ์›๋ณธ ํŒŒ์ผ ์ „์ฒด โ€” ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ ์‹œ ๋“œ๋ก  CSV ์žฌํŒŒ์‹ฑ์šฉ. */ + folderFiles: File[]; + /** ์˜์ƒ djmd ํŠธ๋ž™์—์„œ ๊ฐ์ง€ํ•œ ์นด๋ฉ”๋ผ ์ •๋ณด (์˜ˆ: ZenmuseP1, ์ดˆ์ ๊ฑฐ๋ฆฌ). */ + cameraInfo: DetectedCameraInfo | null; + /** ํด๋”์˜ `<์˜์ƒ base>.camera.json` ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ โ€” ๊ฐ์ง€๊ฐ’/์ €์žฅ๊ฐ’๋ณด๋‹ค ์ตœ์šฐ์„  ์ ์šฉ. */ + cameraJson: Record | null; /** * ํด๋” ์„ ํƒ ํŒŒ์ผ์—์„œ ์ง€๋ฆฌ์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•ด ์Šคํ† ์–ด์— ์ ์žฌํ•œ๋‹ค. * ๋ฐœ๊ฒฌํ•œ ์˜์ƒ File ์„ ๋ฐ˜ํ™˜(์—†์œผ๋ฉด null) โ€” ํ˜ธ์ถœ์ž๊ฐ€ loadLocalFile ๋กœ ์žฌ์ƒ. */ loadFromFolder: (files: FileList | File[]) => Promise; + /** + * ๋ถ„ํ•  ์˜์ƒ ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ โ€” ๋“œ๋ก  ๋กœ๊ทธ(์‹œ๊ฐ„ ๊ธฐ๋ฐ˜)๋ฅผ ํ•ด๋‹น ์˜์ƒ ๊ตฌ๊ฐ„(๋ˆ„์  ์˜คํ”„์…‹)์œผ๋กœ + * ์žฌ์ •๋ ฌํ•ด frames ๋ฅผ ๊ต์ฒดํ•œ๋‹ค. ์žฌ์ƒํ•  ์˜์ƒ File ์„ ๋ฐ˜ํ™˜(๋ฒ”์œ„ ๋ฐ–์ด๋ฉด null). + * baseName ์€ ํด๋”(์ฒซ ์˜์ƒ) ๊ธฐ์ค€์„ ์œ ์ง€ํ•œ๋‹ค โ€” ์นด๋ฉ”๋ผ ๋ณด์ • localStorage ์—ฐ์†์„ฑ. + */ + loadSegment: (index: number) => Promise; /** ๋‹จ์ผ POI ๋ณด์ • ์„ค์ •/๊ฐฑ์‹  (๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ์‹œ). title ํ‚ค. */ setPoiOverride: (title: string, ov: PoiOverride) => void; /** ๋‹จ์ผ POI ๋ณด์ • ํ•ด์ œ. */ @@ -72,9 +91,15 @@ const EMPTY = { structures: [] as RouteStructure[], directionChanges: [] as DirectionChange[], kmzMissing: false, + videoFiles: [] as File[], + videoDurations: [] as (number | null)[], + videoIndex: 0, + folderFiles: [] as File[], + cameraInfo: null as DetectedCameraInfo | null, + cameraJson: null as Record | null, }; -export const useGeoStore = create((set) => ({ +export const useGeoStore = create((set, get) => ({ ...EMPTY, loadFromFolder: async (files) => { @@ -93,10 +118,47 @@ export const useGeoStore = create((set) => ({ structures: data.structures, directionChanges: data.directionChanges, kmzMissing: data.kmzMissing, + videoFiles: data.videoFiles, + videoDurations: data.videoDurations, + videoIndex: 0, + folderFiles: data.folderFiles, + cameraInfo: data.cameraInfo, + cameraJson: data.cameraJson, }); return data.videoFile; }, + loadSegment: async (index) => { + const s = get(); + const file = s.videoFiles[index]; + if (!file) return null; + + // ๋ˆ„์  ์˜คํ”„์…‹ = ์•ž ์„ธ๊ทธ๋จผํŠธ ๊ธธ์ด ํ•ฉ. ํ•˜๋‚˜๋ผ๋„ ์ธก์ • ์‹คํŒจ๋ฉด ์ •๋ ฌ ๋ถˆ๊ฐ€ โ†’ ๊ถค์ ๋งŒ ๋น„์šด๋‹ค(์žฌ์ƒ์€ ๊ณ„์†). + let offsetSec = 0; + let offsetKnown = true; + for (let i = 0; i < index; i++) { + const d = s.videoDurations[i]; + if (d == null) { offsetKnown = false; break; } + offsetSec += d; + } + + if (!offsetKnown) { + console.warn(`[geo] ์„ธ๊ทธ๋จผํŠธ ${index + 1} ์˜คํ”„์…‹ ๊ณ„์‚ฐ ๋ถˆ๊ฐ€(์˜์ƒ ๊ธธ์ด ์ธก์ • ์‹คํŒจ) โ€” ๋“œ๋ก  ๊ถค์  ๋ฏธํ‘œ์‹œ`); + set({ frames: [], videoIndex: index }); + return file; + } + + const frames = await parseDroneFrames(s.folderFiles, s.baseName, { + offsetSec, + durationSec: s.videoDurations[index], + }); + console.log( + `[geo] ์„ธ๊ทธ๋จผํŠธ ${index + 1}/${s.videoFiles.length} ์ „ํ™˜ โ€” ๋กœ๊ทธ ${offsetSec.toFixed(1)}s~ ์ •๋ ฌ, ${frames.length}ํ–‰`, + ); + set({ frames, videoIndex: index }); + return file; + }, + setPoiOverride: (title, ov) => set((s) => { const poiOverrides = { ...s.poiOverrides, [title]: ov }; diff --git a/client/src/types/geo.ts b/client/src/types/geo.ts index faa3a88..9014fad 100644 --- a/client/src/types/geo.ts +++ b/client/src/types/geo.ts @@ -32,6 +32,8 @@ export interface GeoPoint { type: 'poi' | 'station'; /** ์›๋ณธ CSV ์†์„ฑ(๋ผ๋ฒจ ํด๋ฆญ ํŒ์—…์— ํ‘œ์‹œ). ์ˆœ์„œ ๋ณด์กด ์œ„ํ•ด ๋ฐฐ์—ด. */ props?: { k: string; v: string }[]; + /** ๋ผ๋ฒจ ํ‘œ์‹œ ์ƒ‰(CSS). KML ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ/IconStyle ์œ ๋ž˜. ์—†์œผ๋ฉด ๊ธฐ๋ณธ์ƒ‰. */ + labelColor?: string; } /** ์˜๋ฏธ์ƒ ๋ณ„์นญ (POI = GeoPoint, Station = GeoPoint with type==='station') */ @@ -152,6 +154,17 @@ export interface RouteMeta { structures?: RouteStructure[]; } +/** ์˜์ƒ ํŒŒ์ผ(djmd ํŠธ๋ž™)์—์„œ ์ž๋™ ๊ฐ์ง€ํ•œ ๋“œ๋ก  ์นด๋ฉ”๋ผ ์ •๋ณด */ +export interface DetectedCameraInfo { + /** ์นด๋ฉ”๋ผ ๋ชจ๋ธ (์˜ˆ: "DJI ZenmuseP1") */ + model: string; + /** 35mm ํ™˜์‚ฐ ์ดˆ์ ๊ฑฐ๋ฆฌ(mm) โ€” djmd ํ•„๋“œ 8.6(0.1mm ๋‹จ์œ„) ์œ ๋ž˜ */ + focalLen35?: number; + width?: number; + height?: number; + fps?: number; +} + /** loadFolderGeoData ํŒŒ์‹ฑ ๊ฒฐ๊ณผ (v2.0 ๋ฐ์ดํ„ฐ ํ˜•์ƒ) */ export interface FolderGeoData { videoFile: File | null; @@ -170,4 +183,14 @@ export interface FolderGeoData { poiOverrides: PoiOverrideMap; /** KMZ(POIยท๊ตฌ์กฐ๋ฌผ ์›๋ณธ)๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋น„์–ด ์žˆ์Œ โ†’ ๋ฐ์ดํ„ฐ ๋ˆ„๋ฝ(์žฌ๊ตฌ์ถ• ํ•„์š”). ์ธก์ ยท๋“œ๋ก ์€ ๋ณ„๊ฐœ. */ kmzMissing: boolean; + /** ํด๋” ๋‚ด ์˜์ƒ ํŒŒ์ผ ์ „์ฒด(์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ) โ€” ๋ถ„ํ• ๋ณธ ์—ฐ์†์žฌ์ƒ์šฉ. videoFile === videoFiles[0]. */ + videoFiles: File[]; + /** videoFiles ์™€ ์ง์„ ์ด๋ฃจ๋Š” ๊ฐ ์˜์ƒ ๊ธธ์ด(์ดˆ). ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ธก์ • ์‹คํŒจ ์‹œ null. */ + videoDurations: (number | null)[]; + /** ํด๋” ์›๋ณธ ํŒŒ์ผ ์ „์ฒด โ€” ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ ์‹œ ๋“œ๋ก  CSV ์žฌํŒŒ์‹ฑ์šฉ. */ + folderFiles: File[]; + /** ์˜์ƒ djmd ํŠธ๋ž™์—์„œ ๊ฐ์ง€ํ•œ ์นด๋ฉ”๋ผ ์ •๋ณด (DJI ์•„๋‹Œ ์˜์ƒ์ด๋ฉด null). */ + cameraInfo: DetectedCameraInfo | null; + /** ํด๋”์˜ `<์˜์ƒ base>.camera.json` ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ (์—†์œผ๋ฉด null). ์ตœ์šฐ์„  ์ ์šฉ. */ + cameraJson: Record | null; } diff --git a/client/src/utils/cameraMeta.ts b/client/src/utils/cameraMeta.ts new file mode 100644 index 0000000..dccfc5b --- /dev/null +++ b/client/src/utils/cameraMeta.ts @@ -0,0 +1,108 @@ +/** + * DJI ์˜์ƒ ์นด๋ฉ”๋ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ž๋™ ๊ฐ์ง€ + * + * DJI ๋“œ๋ก  MP4 ์—๋Š” 'djmd' ๋ฐ์ดํ„ฐ ํŠธ๋ž™(ํ”„๋ ˆ์ž„๋ณ„ protobuf ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ)์ด mdat ์— + * ๋น„๋””์˜ค ํ”„๋ ˆ์ž„๊ณผ ์ธํ„ฐ๋ฆฌ๋ธŒ๋˜์–ด ์žˆ๋‹ค. ํŒŒ์ผ ์•ž๋ถ€๋ถ„(8MB)์—์„œ ์ฒซ djmd ์ƒ˜ํ”Œ์„ ์ฐพ์•„ ์ถ”์ถœ: + * - ์นด๋ฉ”๋ผ ๋ชจ๋ธ ๋ฌธ์ž์—ด (ํ•„๋“œ 1.1.2, ์˜ˆ: "DJI ZenmuseP1") + * - ๋ Œ์ฆˆ ์ดˆ์ ๊ฑฐ๋ฆฌ (ํ•„๋“œ 8.6, 0.1mm ๋‹จ์œ„ 35mm ํ™˜์‚ฐ) + * - ํ•ด์ƒ๋„/fps (ํ•„๋“œ 1.7/1.6/1.8) + * + * ์‹ค์ธก ๊ฒ€์ฆ(2026-07-09, ์ œ์ฃผ ๋ฐ์ดํ„ฐ): DJI_20260629121835_0002.MP4 โ†’ + * model "DJI ZenmuseP1", 8.6=299(=29.9mm โ€” DL 24mm ๋ Œ์ฆˆ ร— 4K ๋น„๋””์˜ค ํฌ๋กญ โ‰ˆ1.25), + * 3840ร—2160, 59.94fps. (djmd ํ•„๋“œ 6.4/6.5 ๋Š” ์œ„๊ฒฝ๋„ ๋ผ๋””์•ˆ, 6.6 ์€ ํƒ€์›์ฒด๊ณ  mm) + */ + +import type { DetectedCameraInfo } from '../types/geo'; + +const HEAD_BYTES = 8 * 1024 * 1024; + +function readVarint(b: Uint8Array, i: number): [number, number] | null { + let v = 0, s = 0; + while (i < b.length && (b[i] & 0x80)) { + v += (b[i] & 0x7f) * 2 ** s; + s += 7; i++; + if (s > 42) return null; + } + if (i >= b.length) return null; + v += (b[i] & 0x7f) * 2 ** s; + return [v, i + 1]; +} + +/** [start,end) ๊ตฌ๊ฐ„์„ protobuf ํ•„๋“œ๋กœ ํŒŒ์‹ฑํ•ด ๊ฒฝ๋กœ("8.6")โ†’๊ฐ’ ์ˆ˜์ง‘. ๊นจ์ง„ ๊ตฌ๊ฐ„์€ ์กฐ์šฉํžˆ ์ค‘๋‹จ. */ +function parseFields( + b: Uint8Array, dv: DataView, + start: number, end: number, + prefix: string, out: Map, +): void { + let i = start; + while (i < end) { + const kv = readVarint(b, i); + if (!kv) return; + const [key, ni] = kv; + i = ni; + const tag = key >> 3, wire = key & 7; + if (tag === 0 || tag > 60) return; + const path = prefix + tag; + if (wire === 0) { + const r = readVarint(b, i); if (!r) return; + out.set(path, r[0]); i = r[1]; + } else if (wire === 5) { + if (i + 4 > end) return; + out.set(path, dv.getFloat32(i, true)); i += 4; + } else if (wire === 1) { + if (i + 8 > end) return; + out.set(path, dv.getFloat64(i, true)); i += 8; + } else if (wire === 2) { + const r = readVarint(b, i); if (!r) return; + const [len, p] = r; + if (p + len > end) return; + // ํ•˜์œ„ ๋ฉ”์‹œ์ง€ ์‹œ๋„ โ€” ๋ฌธ์ž์—ด/๋ฐ”์ด๋„ˆ๋ฆฌ๋ฉด ๋‚ด๋ถ€์—์„œ ์•Œ์•„์„œ ์ค‘๋‹จ๋จ(๊ฒฝ๋กœ prefix ๋กœ ์ถฉ๋Œ ์—†์Œ) + parseFields(b, dv, p, p + len, path + '.', out); + i = p + len; + } else return; + } +} + +/** ์˜์ƒ File ์•ž๋ถ€๋ถ„์—์„œ DJI djmd ์นด๋ฉ”๋ผ ์ •๋ณด๋ฅผ ๊ฐ์ง€ํ•œ๋‹ค. DJI ์˜์ƒ์ด ์•„๋‹ˆ๋ฉด null. */ +export async function detectCameraInfo(file: File): Promise { + try { + const buf = new Uint8Array(await file.slice(0, HEAD_BYTES).arrayBuffer()); + const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + + // ๋ชจ๋ธ ๋ฌธ์ž์—ด: 0x12 "DJI ..." (ํ•„๋“œ 1.1.2) + let m = -1; + for (let i = 2; i < buf.length - 4; i++) { + if (buf[i] === 0x44 && buf[i + 1] === 0x4a && buf[i + 2] === 0x49 && buf[i + 3] === 0x20 && + buf[i - 2] === 0x12 && buf[i - 1] >= 4 && buf[i - 1] <= 40) { m = i; break; } + } + if (m < 0) return null; + const model = new TextDecoder().decode(buf.slice(m, m + buf[m - 1])); + + // ์ƒ˜ํ”Œ ์‹œ์ž‘(์ตœ์ƒ์œ„ ํ•„๋“œ1, 0x0a 0x0a ...)์„ ๋ชจ๋ธ ๋ฌธ์ž์—ด์—์„œ ์—ญ๋ฐฉํ–ฅ ํƒ์ƒ‰ + let sampleStart = -1; + for (let back = 4; back <= 16; back++) { + const i = m - 2 - back; + if (i < 0) break; + if (buf[i] !== 0x0a) continue; + const r = readVarint(buf, i + 1); + if (!r) continue; + const [len, p] = r; + if (buf[p] === 0x0a && p < m && p + len > m && p + len <= buf.length) { sampleStart = i; break; } + } + if (sampleStart < 0) return { model }; + + const out = new Map(); + parseFields(buf, dv, sampleStart, Math.min(buf.length, sampleStart + 512), '', out); + + const info: DetectedCameraInfo = { model }; + const focal10 = out.get('8.6'); + if (focal10 !== undefined && focal10 > 80 && focal10 < 3000) info.focalLen35 = focal10 / 10; // 8~300mm ๋งŒ ์‹ ๋ขฐ + const w = out.get('1.7'), h = out.get('1.6'), fps = out.get('1.8'); + if (w && w > 100) info.width = w; + if (h && h > 100) info.height = h; + if (typeof fps === 'number' && fps > 1 && fps < 1000) info.fps = Math.round(fps * 100) / 100; + return info; + } catch { + return null; + } +} diff --git a/client/src/utils/geoData.ts b/client/src/utils/geoData.ts index ba652e9..9404ea0 100644 --- a/client/src/utils/geoData.ts +++ b/client/src/utils/geoData.ts @@ -33,6 +33,7 @@ import type { PoiOverrideMap, } from '../types/geo'; import { stationOrder, getWorldOrigin } from './geoSearch'; +import { detectCameraInfo } from './cameraMeta'; import { unzipSync, strFromU8 } from 'fflate'; // โ”€โ”€ CSV ํŒŒ์‹ฑ ํ—ฌํผ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -120,11 +121,33 @@ function findBuildingFile(files: File[], keyword: string): File | null { const VIDEO_EXT = /\.(mp4|webm)$/i; -/** ์˜์ƒ ํŒŒ์ผ ์ฐพ๊ธฐ (mp4/webm, building ์ œ์™ธ). */ +/** ํด๋” ๋‚ด ๋ชจ๋“  ์˜์ƒ ํŒŒ์ผ (์ด๋ฆ„์ˆœ ์ •๋ ฌ โ€” DJI ๋ถ„ํ• ๋ณธ _0002, _0003โ€ฆ ์ˆœ์„œ ๋ณด์žฅ). */ +export function findVideoFiles(files: File[]): File[] { + return files + .filter((f) => !isInBuilding(f) && VIDEO_EXT.test(baseNameOf(relPath(f)))) + .sort((a, b) => baseNameOf(relPath(a)).localeCompare(baseNameOf(relPath(b)))); +} + +/** ์˜์ƒ ํŒŒ์ผ ์ฐพ๊ธฐ (mp4/webm, building ์ œ์™ธ). ์—ฌ๋Ÿฌ ๊ฐœ๋ฉด ์ด๋ฆ„์ˆœ ์ฒซ ๋ฒˆ์งธ. */ export function findVideoFile(files: File[]): File | null { - return ( - files.find((f) => !isInBuilding(f) && VIDEO_EXT.test(baseNameOf(relPath(f)))) ?? null - ); + return findVideoFiles(files)[0] ?? null; +} + +/** ์˜์ƒ File ์˜ ์žฌ์ƒ ๊ธธ์ด(์ดˆ) โ€” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ ์ฝ๋Š”๋‹ค(์ „์ฒด ๋กœ๋“œ ์—†์Œ). ์‹คํŒจ ์‹œ null. */ +export function probeVideoDuration(file: File): Promise { + return new Promise((resolve) => { + const url = URL.createObjectURL(file); + const v = document.createElement('video'); + v.preload = 'metadata'; + const done = (d: number | null): void => { + URL.revokeObjectURL(url); + v.removeAttribute('src'); + resolve(d); + }; + v.onloadedmetadata = () => done(isFinite(v.duration) ? v.duration : null); + v.onerror = () => done(null); + v.src = url; + }); } /** ์˜์ƒ ํŒŒ์ผ๋ช…์—์„œ base(ํ™•์žฅ์ž ์ œ๊ฑฐ) ์ถ”์ถœ. */ @@ -133,17 +156,44 @@ export function deriveBaseName(videoFile: File | null): string | null { return baseNameOf(relPath(videoFile)).replace(VIDEO_EXT, ''); } -// โ”€โ”€ ํŒŒ์„œ: ๋“œ๋ก  ํ”„๋ ˆ์ž„ (๋ณ€๊ฒฝ ์—†์Œ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// โ”€โ”€ ํŒŒ์„œ: ๋“œ๋ก  ํ”„๋ ˆ์ž„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /** - * ๋“œ๋ก  ํ”„๋ ˆ์ž„ CSV ํŒŒ์‹ฑ (UTF-8 BOM, ํ—ค๋” ์ด๋ฆ„ ์ธ๋ฑ์Šค). - * ํ—ค๋”: frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len + * DJI ๋น„ํ–‰๋กœ๊ทธ(์‹œ๊ฐ„ ๊ธฐ๋ฐ˜) โ†’ ํ”„๋ ˆ์ž„ ํ™˜์‚ฐ์šฉ ๊ฐ€์ƒ fps. + * frame = ์˜์ƒ ์ƒ๋Œ€์‹œ๊ฐ„(์ดˆ) ร— ์ด ๊ฐ’. ์†Œ๋น„์ธก(effectiveFps = maxFrame รท ์˜์ƒ๊ธธ์ด)์ด + * ๊ฐ™์€ ๊ฐ’์„ ์—ญ์‚ฐํ•ด ์‹œ๊ฐ„์œผ๋กœ ๋˜๋Œ๋ฆฌ๋ฏ€๋กœ ์ƒ์ˆ˜ ์ž์ฒด๋Š” ์ž„์˜์—ฌ๋„ ์ •ํ•ฉ์ด ์œ ์ง€๋œ๋‹ค. + * 59.94 ๋Š” ์ด ๋ฐ์ดํ„ฐ์…‹(DJI 4K60) ์‹ค์ œ fps ์™€ ์ผ์น˜์‹œ์ผœ HUD ํ‘œ๊ธฐ๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ•œ ๊ฒƒ. + */ +const DJI_VIRTUAL_FPS = 59.94; + +/** ์˜์ƒ ์„ธ๊ทธ๋จผํŠธ ์ฐฝ โ€” DJI ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ๋ฅผ ํŠน์ • ์˜์ƒ ๊ตฌ๊ฐ„์— ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉ. */ +export interface SegmentWindow { + /** ์ „์ฒด ๋…นํ™” ๊ธฐ์ค€ ์ด ์˜์ƒ์˜ ์‹œ์ž‘ ์˜คํ”„์…‹(์ดˆ). ์ฒซ ์„ธ๊ทธ๋จผํŠธ๋Š” 0. */ + offsetSec: number; + /** ์ด ์˜์ƒ์˜ ๊ธธ์ด(์ดˆ). ๋ฏธ์ƒ(null)์ด๋ฉด ํด๋ฆฌํ•‘ ์—†์ด ์ „์ฒด ๋กœ๊ทธ๋ฅผ ์“ด๋‹ค. */ + durationSec: number | null; +} + +/** + * ๋“œ๋ก  ํ”„๋ ˆ์ž„ CSV ํŒŒ์‹ฑ (UTF-8 BOM, ํ—ค๋” ์ด๋ฆ„ ์ธ๋ฑ์Šค). ๋‘ ํ˜•์‹ ์ง€์›: + * + * โ‘  ํ”„๋ ˆ์ž„ ๊ธฐ๋ฐ˜(ํšŒ๋• v2.0): frame_cnt,latitude,longitude,altitude,yaw,pitch,roll,focal_len + * โ‘ก ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜(DJI ๋น„ํ–‰๋กœ๊ทธ, ์ œ์ฃผ): time(millisecond),latitude,longitude,โ€ฆ, + * altitude_above_seaLevel/compass_heading/gimbal_heading(degrees) ๋“ฑ. + * - ์ฒซ ํ–‰ time = ๋…นํ™” ์‹œ์ž‘์œผ๋กœ ๋ณด๊ณ (๋กœ๊ทธ ํญ โ‰ˆ ๋ถ„ํ• ์˜์ƒ ์ด๊ธธ์ด ๊ฒ€์ฆ๋จ), + * frame = ((timeโˆ’t0)/1000 โˆ’ offsetSec) ร— DJI_VIRTUAL_FPS ๋กœ ํ™˜์‚ฐ. + * - ์ž์„ธ๋Š” ์นด๋ฉ”๋ผ ๋ฐฉํ–ฅ์ธ gimbal_* ์šฐ์„ , ์—†์œผ๋ฉด ๊ธฐ์ฒด compass/pitch/roll ํด๋ฐฑ. + * - focal_len ์ด ์—†์–ด 35mm ํ™˜์‚ฐ 24mm(DJI ๊ด‘๊ฐ ๊ธฐ๋ณธ)๋กœ ๋‘”๋‹ค โ†’ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ UI๋กœ ๋ณด์ •. + * - segmentWindow.durationSec ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ์˜์ƒ ๊ตฌ๊ฐ„ ๋ฐ– ํ–‰์„ ๋ฒ„๋ฆฐ๋‹ค + * (๋ถ„ํ•  ์˜์ƒ์—์„œ effectiveFps ์ž๊ธฐ๋ณด์ •์ด ์„ฑ๋ฆฝํ•˜๋ ค๋ฉด ํ•„์ˆ˜). + * * ๋ฃจํŠธ(building ์ œ์™ธ)์˜ .csv ๋ฅผ ์‹๋ณ„ํ•œ๋‹ค. base ๊ฐ€ ์—†์œผ๋ฉด ๋ฃจํŠธ .csv ์ค‘ * POI ๊ฐ€ ์•„๋‹Œ ์ฒซ ํŒŒ์ผ์„ ์‚ฌ์šฉํ•œ๋‹ค(์˜์ƒ๋ช… ๋น„์˜์กด ํด๋ฐฑ). */ export async function parseDroneFrames( files: File[], baseName: string | null, + segmentWindow: SegmentWindow | null = null, ): Promise { const rootCsv = files.filter((f) => { if (isInBuilding(f)) return false; @@ -164,6 +214,51 @@ export async function parseDroneFrames( const fi = makeFieldIndexer(rows[0]); + // โ‘ก ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜(DJI ๋น„ํ–‰๋กœ๊ทธ) โ€” time(millisecond) ํ—ค๋”๋กœ ๊ฐ์ง€. + const iTime = fi('time(millisecond)'); + if (iTime >= 0) { + const iLat = fi('latitude'); + const iLon = fi('longitude'); + const iAlt = fi('altitude_above_seaLevel(meter)'); + const iYaw = fi('gimbal_heading(degrees)') >= 0 ? fi('gimbal_heading(degrees)') : fi('compass_heading(degrees)'); + const iPitch = fi('gimbal_pitch(degrees)') >= 0 ? fi('gimbal_pitch(degrees)') : fi('pitch(degrees)'); + const iRoll = fi('gimbal_roll(degrees)') >= 0 ? fi('gimbal_roll(degrees)') : fi('roll(degrees)'); + + const raw = rows + .slice(1) + .map((r) => ({ + timeMs: parseFloat(cell(r, iTime)), + lat: parseFloat(cell(r, iLat)), + lon: parseFloat(cell(r, iLon)), + altitude: parseFloat(cell(r, iAlt)), + yaw: parseFloat(cell(r, iYaw)), + pitch: parseFloat(cell(r, iPitch)), + roll: parseFloat(cell(r, iRoll)) || 0, + focalLen: 24, + })) + .filter((f) => !isNaN(f.lat) && !isNaN(f.timeMs)); + if (!raw.length) return []; + + const t0 = raw[0].timeMs; + const offsetSec = segmentWindow?.offsetSec ?? 0; + const durationSec = segmentWindow?.durationSec ?? null; + const maxFrame = durationSec != null ? durationSec * DJI_VIRTUAL_FPS : Infinity; + + const frames = raw + .map(({ timeMs, ...rest }) => ({ + ...rest, + frame: Math.round(((timeMs - t0) / 1000 - offsetSec) * DJI_VIRTUAL_FPS), + })) + .filter((f) => f.frame >= 0 && f.frame <= maxFrame); + + console.log( + `[geo] DJI ์‹œ๊ฐ„๊ธฐ๋ฐ˜ ๋กœ๊ทธ: ${raw.length}ํ–‰ ์ค‘ ${frames.length}ํ–‰ ์‚ฌ์šฉ ` + + `(๊ตฌ๊ฐ„ ${offsetSec.toFixed(1)}s~${durationSec != null ? (offsetSec + durationSec).toFixed(1) : '๋'}s, ๊ฐ€์ƒfps ${DJI_VIRTUAL_FPS})`, + ); + return frames; + } + + // โ‘  ํ”„๋ ˆ์ž„ ๊ธฐ๋ฐ˜(ํšŒ๋• v2.0) return rows .slice(1) .map((r) => ({ @@ -290,48 +385,91 @@ function childLocal(el: Element, local: string): Element | null { return null; } -/** KMZ/KML ํŒŒ์‹ฑ โ€” ์žˆ์œผ๋ฉด { pois, structures }, ์—†๊ฑฐ๋‚˜ ์‹คํŒจ ์‹œ null. - * bare .kml(๊ตฌ๊ธ€ ์ง์ ‘ ๋‹ค์šด๋กœ๋“œ) ์šฐ์„ , ์—†์œผ๋ฉด .kmz(zip) ํ•ด์ œ. */ +/** ํ•œ๊ตญ์–ด ์ƒ‰์ƒ๋ช… โ†’ CSS ์ƒ‰ (KML ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ ์†์„ฑ). ์–ด๋‘์šด ์ƒ‰์€ ๋ Œ๋” ์‹œ ๋ฐ์€ ์™ธ๊ณฝ์„  ์ฒ˜๋ฆฌ. */ +const KOR_LABEL_COLORS: Record = { + ๊ฒ€์ •: '#1f2937', ํฐ์ƒ‰: '#ffffff', ํšŒ์ƒ‰: '#9ca3af', + ๋นจ๊ฐ•: '#ef4444', ์ฃผํ™ฉ: '#fb923c', ๋…ธ๋ž‘: '#facc15', + ์—ฐ๋‘: '#a3e635', ์ดˆ๋ก: '#22c55e', ํ•˜๋Š˜: '#38bdf8', + ํŒŒ๋ž‘: '#3b82f6', ๋‚จ์ƒ‰: '#3730a3', ๋ณด๋ผ: '#a855f7', ๋ถ„ํ™: '#f472b6', +}; + +function koreanColorToCss(v?: string): string | undefined { + if (!v) return undefined; + const key = v.trim(); + if (KOR_LABEL_COLORS[key]) return KOR_LABEL_COLORS[key]; + for (const k in KOR_LABEL_COLORS) if (key.includes(k)) return KOR_LABEL_COLORS[k]; + return undefined; +} + +/** Placemark ์ธ๋ผ์ธ IconStyle ์ƒ‰ โ€” KML aabbggrr โ†’ CSS #rrggbb. */ +function inlineIconColor(pm: Element): string | undefined { + const icon = pm.getElementsByTagName('IconStyle')[0]; + const c = icon?.getElementsByTagName('color')[0]?.textContent?.trim(); + if (!c || !/^[0-9a-fA-F]{8}$/.test(c)) return undefined; + const bb = c.slice(2, 4), gg = c.slice(4, 6), rr = c.slice(6, 8); + return `#${rr}${gg}${bb}`.toLowerCase(); +} + +/** KMZ/KML ํŒŒ์‹ฑ โ€” ์žˆ์œผ๋ฉด { pois, structures, stations }, ์—†๊ฑฐ๋‚˜ ์‹คํŒจ ์‹œ null. + * bare .kml(๊ตฌ๊ธ€ ์ง์ ‘ ๋‹ค์šด๋กœ๋“œ) ์šฐ์„ , ์—†์œผ๋ฉด .kmz(zip) ํ•ด์ œ. + * + * ์†์„ฑ ์†Œ์Šค 2์ข…์„ ๋ชจ๋‘ props ๋กœ ์ˆ˜์ง‘ํ•œ๋‹ค: + * - description CDATA HTML ํ‘œ (๊ตฌ๊ธ€์–ด์Šค/์ง€์˜ค์ฝ”๋”ฉ ์‚ฐ์ถœ๋ฌผ โ€” ํšŒ๋• v2.0) + * - ExtendedData/SchemaData/SimpleData (CADยทGIS ๋‚ด๋ณด๋‚ด๊ธฐ โ€” ์ œ์ฃผ ๋„๋กœ KML) + * + * ์ธก์ (STA): ์ด๋ฆ„์ด ๋„๋กœ ์ฒด์ด๋‹ˆ์ง€(`6+998`) ํ˜•์‹์ด๊ฑฐ๋‚˜ STA ์†์„ฑ์ด ์žˆ๋Š” Point ๋Š” + * POI ๊ฐ€ ์•„๋‹ˆ๋ผ ์ธก์ (type='station')์œผ๋กœ ์ˆ˜์ง‘ํ•œ๋‹ค. km ์†์„ฑ(๋‹จ์œ„ km) ์šฐ์„ , ์—†์œผ๋ฉด + * ์ด๋ฆ„์—์„œ ๋ฏธํ„ฐ๊ฐ’์„ ํŒŒ์‹ฑํ•ด ๊ธฐ์กด ์ฒ ๋„์‹ ์ œ๋ชฉ(`6k998`)์œผ๋กœ ๋ณ€ํ™˜ โ€” stationOrder/stationKm + * ๋“ฑ ์ธก์  ํŒŒ์ดํ”„๋ผ์ธ(์Šคํ…Œ์ด์…˜๋ฐ”ยท๋…ธ์„ ํŒจ๋„ยท๊ฒ€์ƒ‰)๊ณผ ๊ทธ๋Œ€๋กœ ํ˜ธํ™˜๋œ๋‹ค. */ export async function parseKmz( files: File[], -): Promise<{ pois: GeoPoint[]; structures: RouteStructure[] } | null> { - const kmlFile = files.find( +): Promise<{ pois: GeoPoint[]; structures: RouteStructure[]; stations: GeoPoint[] } | null> { + // ํด๋” ๋‚ด ๋ชจ๋“  KML/KMZ ๋ฅผ ํŒŒ์‹ฑํ•ด ๋ณ‘ํ•ฉํ•œ๋‹ค (์˜ˆ: ์ œ์ฃผ โ€” ์ธก์  KML + ์ง€์žฅ๋ฌผ KML ๋ณ„๋„ ํŒŒ์ผ). + const kmlFiles = files.filter( (f) => !isInBuilding(f) && /\.kml$/i.test(baseNameOf(relPath(f))), ); - const kmzFile = files.find( + const kmzFiles = files.filter( (f) => !isInBuilding(f) && /\.kmz$/i.test(baseNameOf(relPath(f))), ); - if (!kmlFile && !kmzFile) return null; + if (!kmlFiles.length && !kmzFiles.length) return null; - let kmlText: string; - try { - if (kmlFile) { - kmlText = await kmlFile.text(); // ๊ตฌ๊ธ€ KML ์€ UTF-8 - } else { - const buf = new Uint8Array(await kmzFile!.arrayBuffer()); + const kmlTexts: string[] = []; + for (const kf of kmlFiles) { + try { + kmlTexts.push(await kf.text()); // ๊ตฌ๊ธ€ KML ์€ UTF-8 + } catch (e) { + console.warn(`[KML] ์ฝ๊ธฐ ์‹คํŒจ: ${baseNameOf(relPath(kf))}`, e); + } + } + for (const kf of kmzFiles) { + try { + const buf = new Uint8Array(await kf.arrayBuffer()); const entries = unzipSync(buf); const kmlName = Object.keys(entries).find((n) => /(^|\/)doc\.kml$/i.test(n)) ?? Object.keys(entries).find((n) => /\.kml$/i.test(n)); - if (!kmlName || !entries[kmlName]) return null; - kmlText = strFromU8(entries[kmlName]); + if (kmlName && entries[kmlName]) kmlTexts.push(strFromU8(entries[kmlName])); + } catch (e) { + console.warn(`[KMZ] ์ฝ๊ธฐ ์‹คํŒจ: ${baseNameOf(relPath(kf))}`, e); } - } catch (e) { - console.warn('[KMZ/KML] ์ฝ๊ธฐ ์‹คํŒจ โ†’ CSV ํด๋ฐฑ', e); - return null; } - - const doc = new DOMParser().parseFromString(kmlText, 'application/xml'); - if (doc.getElementsByTagName('parsererror').length) return null; + if (!kmlTexts.length) return null; const pois: GeoPoint[] = []; const structures: RouteStructure[] = []; + const stations: GeoPoint[] = []; let nStruct = 0; const handlePlacemark = (pm: Element, folder: string): void => { const name = childLocal(pm, 'name')?.textContent?.trim() ?? ''; const desc = childLocal(pm, 'description')?.textContent ?? ''; const props = parseKmlDescProps(desc); + // ExtendedData/SchemaData/SimpleData ์†์„ฑ (CADยทGIS ๋‚ด๋ณด๋‚ด๊ธฐ) โ€” HTML ํ‘œ props ์™€ ํ†ตํ•ฉ. + for (const sd of Array.from(pm.getElementsByTagName('SimpleData'))) { + const k = sd.getAttribute('name') ?? ''; + const v = sd.textContent?.trim() ?? ''; + if (k) props.push({ k, v }); + } const pget = (k: string): string | undefined => props.find((p) => p.k === k)?.v; // ์ขŒํ‘œ: lon,lat[,alt] ์šฐ์„ , ์—†์œผ๋ฉด ์†์„ฑ lat/lon. @@ -347,6 +485,32 @@ export async function parseKmz( } if (isNaN(lat) || isNaN(lon) || lat < 33 || lat > 39 || lon < 124 || lon > 132) return; + // ์ƒˆ ํฌ๋งท: ๊ตฌ๊ธ€์–ด์Šค CSV ๋ณ€ํ™˜ ์ง€์žฅ๋ฌผ KML โ€” Schema(ํƒ€์ž…/ํƒ€์ž…์ƒ์„ธ/์ด๋ฆ„/์—ฐ์žฅ/ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ/๋†’์ด), + // ํด๋” ๊ตฌ๋ถ„ ์—†์ด Placemark ์ง์†. 'ํƒ€์ž…' ํ‚ค ์กด์žฌ๋กœ ๊ฐ์ง€ํ•œ๋‹ค(ํšŒ๋• descํ‘œ/STA ์Šคํ‚ค๋งˆ์—” ์—†์Œ). + // ์ขŒํ‘œ๋Š” SimpleData X/Y(์ €์ •๋ฐ€)๊ฐ€ ์•„๋‹ˆ๋ผ Point coordinates(๊ณ ์ •๋ฐ€)๋ฅผ ์“ด๋‹ค(์œ„์—์„œ ์ด๋ฏธ ์ถ”์ถœ). + if (pget('ํƒ€์ž…') !== undefined) { + const title = pget('์ด๋ฆ„') || name; + if (!title) return; + const category = pget('ํƒ€์ž…') || '์ง€์žฅ๋ฌผ'; + const lenM = parseFloat(pget('์—ฐ์žฅ') ?? ''); + // ๋ผ๋ฒจ ์ƒ‰: ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ(ํ•œ๊ตญ์–ด๋ช…) ์šฐ์„ , ์—†์œผ๋ฉด ์ธ๋ผ์ธ IconStyle ์ƒ‰. + const labelColor = koreanColorToCss(pget('ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ')) ?? inlineIconColor(pm); + // z: '๋†’์ด'๋Š” ํ‘œ๊ณ ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ตฌ์กฐ๋ฌผ ๋†’์ด๋ผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ(ํŒ์—… props ๋กœ๋งŒ ๋…ธ์ถœ). + // ์ง€๋ฉดํ‘œ๊ณ  ๋ฏธ์ƒ(0) โ†’ ์˜ค๋ฒ„๋ ˆ์ด ๊ธฐ๋ณธ(๋“œ๋ก ๊ณ ๋„โˆ’์ด๊ฒฉ) ๋ชจ๋“œ ๋ฐ ์ค‘์‹ฌ์„  z ํด๋ฐฑ์œผ๋กœ ํ‘œ์‹œ. + pois.push({ + title, category, lat, lon, z: 0, type: 'poi', + ...(labelColor ? { labelColor } : {}), + props, + }); + // ๊ต๋Ÿ‰/ํ„ฐ๋„์€ ์Šคํ…Œ์ด์…˜๋ฐ”ยท๋…ธ์„ ํŒจ๋„ ๋งˆํฌ์šฉ ๊ตฌ์กฐ๋ฌผ๋กœ๋„ ๋“ฑ๋ก. + if (category.includes('๊ต๋Ÿ‰')) { + structures.push({ id: `๊ต๋Ÿ‰-${nStruct++}`, type: 'bridge', category: '๊ต๋Ÿ‰', name: title, lat, lon, ...(isFinite(lenM) ? { lengthM: lenM } : {}), props }); + } else if (category.includes('ํ„ฐ๋„')) { + structures.push({ id: `ํ„ฐ๋„-${nStruct++}`, type: 'tunnel', category: 'ํ„ฐ๋„', name: title, lat, lon, ...(isFinite(lenM) ? { lengthM: lenM } : {}), props }); + } + return; + } + const lenNum = parseFloat(pget('์—ฐ์žฅ(m)') ?? ''); if (folder.includes('๊ต๋Ÿ‰')) { structures.push({ id: `๊ต๋Ÿ‰-${nStruct++}`, type: 'bridge', category: '๊ต๋Ÿ‰', name: pget('๊ตฌ๋ถ„') || name, lat, lon, ...(isNaN(lenNum) ? {} : { lengthM: lenNum }), ...(pget('์‹œ์„ค์ข…๋ณ„') ? { grade: pget('์‹œ์„ค์ข…๋ณ„') } : {}), props }); @@ -358,6 +522,21 @@ export async function parseKmz( const title = pget('์ถœ์ž…๋ฌธ๋ฒˆํ˜ธ') || name; if (title) pois.push({ title, category: '์ถœ์ž…๋ฌธ', lat, lon, z: parseFloat(pget('z') ?? pget('์ ˆ๋Œ€๊ณ ๋„') ?? '0') || 0, type: 'poi', props }); } else { + // ๋„๋กœ ์ฒด์ด๋‹ˆ์ง€ ์ธก์  (์˜ˆ: "0+100" ๋˜๋Š” STA ์†์„ฑ) โ†’ type='station' ์œผ๋กœ ์ˆ˜์ง‘. + // km ์†์„ฑ(๋‹จ์œ„ km) ์šฐ์„ , ์—†์œผ๋ฉด ์ด๋ฆ„์˜ "k+m" ํŒŒ์‹ฑ. ์ œ๋ชฉ์€ ์ฒ ๋„์‹ NkNNN ์œผ๋กœ ๋ณ€ํ™˜. + const staName = pget('STA') || name; + const staMatch = staName.match(/^(\d+)\+(\d+)$/); + if (staMatch || pget('STA') !== undefined) { + const kmProp = parseFloat(pget('km') ?? ''); + const meters = isFinite(kmProp) + ? Math.round(kmProp * 1000) + : staMatch ? parseInt(staMatch[1], 10) * 1000 + parseInt(staMatch[2], 10) : NaN; + if (!isFinite(meters)) return; + const title = `${Math.floor(meters / 1000)}k${String(meters % 1000).padStart(3, '0')}`; + // z ๋ฏธ์ƒ(KML ๊ณ ๋„ 0) โ€” ์†Œ๋น„์ธก(loadFolderGeoData)์—์„œ ๋“œ๋ก  ๊ณ ๋„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณด์ •. + stations.push({ title, category: '์ธก์ ', lat, lon, z: 0, type: 'station', props }); + return; + } // 02)์ง€์žฅ๋ฌผ ๋“ฑ = ์ง€์˜ค์ฝ”๋”ฉ POI. ๋ชจ๋“  POI ๋Š” ์˜์ƒ์— ํ‘œ์ถœ(์ฒ ๋„์—ญ ํฌํ•จ). if (!name) return; const src = pget('source'); @@ -384,9 +563,52 @@ export async function parseKmz( } } }; - if (doc.documentElement) walk(doc.documentElement, ''); + for (const text of kmlTexts) { + const doc = new DOMParser().parseFromString(text, 'application/xml'); + if (doc.getElementsByTagName('parsererror').length) { + console.warn('[KML] XML ํŒŒ์‹ฑ ์‹คํŒจ โ€” ํ•ด๋‹น ํŒŒ์ผ ๊ฑด๋„ˆ๋œ€'); + continue; + } + if (doc.documentElement) walk(doc.documentElement, ''); + } - return { pois, structures }; + return { pois, structures, stations }; +} + +// โ”€โ”€ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์ผ (.camera.json) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * ํด๋”์—์„œ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์ผ์„ ์ฝ๋Š”๋‹ค. ์„œ๋ฒ„ '์นด๋ฉ”๋ผ๊ฐ’ ์ €์žฅ'(PUT /api/camera/:videoId)์ด + * ์˜์ƒ ์˜†์— ๋งŒ๋“  `<์˜์ƒ base>.camera.json`(๋˜๋Š” `<์˜์ƒ base>.json`) ํ˜•์‹: + * { "camera": { focalLen, sensorW, sensorH, yawOffset, pitch, roll, offX, offY, offZ, geoidOffset, cx0, cy0 }, ... } + * ํด๋” ๋‚ด ์–ด๋–ค ์˜์ƒ์˜ base ์™€๋„ ๋งค์นญ ํ—ˆ์šฉ(๋ถ„ํ• ๋ณธ ๊ณต์šฉ). ์ˆซ์ž ํ•„๋“œ๋งŒ ํ†ต๊ณผ์‹œํ‚จ๋‹ค. + */ +export async function parseCameraJson( + files: File[], + videoFiles: File[], +): Promise | null> { + const bases = videoFiles.map((v) => baseNameOf(relPath(v)).replace(VIDEO_EXT, '').toLowerCase()); + const file = files.find((f) => { + if (isInBuilding(f)) return false; + const name = baseNameOf(relPath(f)).toLowerCase(); + const m = name.match(/^(.+?)(\.camera)?\.json$/); + if (!m) return false; + if (/\.route\.json$|_poi_overrides\.json$/.test(name)) return false; + return bases.includes(m[1]); + }); + if (!file) return null; + try { + const obj = JSON.parse(await file.text()); + const cam = obj && typeof obj === 'object' ? (obj.camera ?? obj) : null; + if (!cam || typeof cam !== 'object') return null; + const out: Record = {}; + for (const [k, v] of Object.entries(cam)) { + if (typeof v === 'number' && isFinite(v)) out[k] = v; + } + return Object.keys(out).length ? out : null; + } catch { + return null; + } } // โ”€โ”€ route.json ๋ณด์ • โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -409,7 +631,9 @@ export async function parseRouteMeta( const file = (wantBase && rootJson.find((f) => baseNameOf(relPath(f)).toLowerCase() === wantBase)) || - rootJson.find((f) => baseNameOf(relPath(f)).toLowerCase() === 'route.json'); + rootJson.find((f) => baseNameOf(relPath(f)).toLowerCase() === 'route.json') || + // ์ž„์˜ ์ ‘๋‘ `<์ด๋ฆ„>.route.json` ํ—ˆ์šฉ (์˜ˆ: ์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„๋„๋กœ ๊ฒฝ๋กœ 1-1.route.json) + rootJson.find((f) => /\.route\.json$/i.test(baseNameOf(relPath(f)))); if (!file) return null; try { @@ -533,33 +757,80 @@ export async function loadFolderGeoData( ): Promise { const files = Array.from(input); - const videoFile = findVideoFile(files); + const videoFiles = findVideoFiles(files); + const videoFile = videoFiles[0] ?? null; const baseName = deriveBaseName(videoFile); - const [frames, stationResult, routeMeta, poiOverrides, kmz] = await Promise.all([ - parseDroneFrames(files, baseName), + // ๊ฐ ์˜์ƒ ๊ธธ์ด๋ฅผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ ์ธก์ • โ€” DJI ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์˜ ์„ธ๊ทธ๋จผํŠธ ์ •๋ ฌ(๋ˆ„์  ์˜คํ”„์…‹)๊ณผ + // ended ์—ฐ์†์žฌ์ƒ(geoStore.loadSegment)์— ์“ด๋‹ค. ๋ถ„ํ• ๋ณธ์ด ์•„๋‹ˆ๋ฉด 1๊ฐœ๋งŒ ์ธก์ •. + const videoDurations: (number | null)[] = []; + for (const vf of videoFiles) videoDurations.push(await probeVideoDuration(vf)); + + // ์ฒซ ์„ธ๊ทธ๋จผํŠธ ์ฐฝ โ€” ๋กœ๊ทธ๋Š” ์ „์ฒด ๋…นํ™”(๋ถ„ํ• ๋ณธ ํ•ฉ)๋ฅผ ์ปค๋ฒ„ํ•˜๋ฏ€๋กœ ์ฒซ ์˜์ƒ ๊ตฌ๊ฐ„๋งŒ ์ž˜๋ผ ์“ด๋‹ค. + const segmentWindow: SegmentWindow | null = videoFile + ? { offsetSec: 0, durationSec: videoDurations[0] } + : null; + if (videoFiles.length > 1) { + console.log( + `[geo] ๋ถ„ํ•  ์˜์ƒ ${videoFiles.length}๊ฐœ ๊ฐ์ง€ (์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ ์—ฐ์†์žฌ์ƒ): ` + + videoFiles.map((vf, i) => `${baseNameOf(relPath(vf))}(${videoDurations[i]?.toFixed(1) ?? '?'}s)`).join(' โ†’ '), + ); + } + + const [frames, stationResult, routeMeta, poiOverrides, kmz, cameraInfo, cameraJson] = await Promise.all([ + parseDroneFrames(files, baseName, segmentWindow), parseStations(files), parseRouteMeta(files, baseName), parsePoiOverrides(files, baseName), parseKmz(files), + videoFile ? detectCameraInfo(videoFile) : Promise.resolve(null), + parseCameraJson(files, videoFiles), ]); + if (cameraJson) { + console.log('[camera] .camera.json ๋ฐœ๊ฒฌ โ€” ์ €์žฅ๋œ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ ์šฉ:', cameraJson); + } + + // ๊ฐ์ง€๋œ ์ดˆ์ ๊ฑฐ๋ฆฌ๋ฅผ ๋“œ๋ก  ํ”„๋ ˆ์ž„ focalLen ์—๋„ ๋ฐ˜์˜ (๊ฒ€์ƒ‰/ํˆฌ์˜ FOV ํŒ์ • ์ผ๊ด€์„ฑ). + if (cameraInfo?.focalLen35) { + for (const f of frames) f.focalLen = cameraInfo.focalLen35; + console.log( + `[camera] ${cameraInfo.model} ๊ฐ์ง€ โ€” focal ${cameraInfo.focalLen35}mm` + + (cameraInfo.width ? `, ${cameraInfo.width}x${cameraInfo.height}@${cameraInfo.fps ?? '?'}fps` : ''), + ); + } else if (cameraInfo) { + console.log(`[camera] ${cameraInfo.model} ๊ฐ์ง€ (์ดˆ์ ๊ฑฐ๋ฆฌ ์ •๋ณด ์—†์Œ)`); + } + // POI/๊ตฌ์กฐ๋ฌผ ์ถœ์ฒ˜: KMZ(์›๋ณธ)๊ฐ€ ์œ ์ผํ•œ ์†Œ์Šค๋‹ค(์ง€์žฅ๋ฌผยท์ถœ์ž…๋ฌธยท๊ต๋Ÿ‰/ํ„ฐ๋„/๊ตฌ๊ตยท์ฒ ๋„์—ญ). - // KMZ ์ •์ฑ…์ƒ ํ•„์ˆ˜ โ€” ์—†๊ฑฐ๋‚˜ ๋น„์–ด ์žˆ์œผ๋ฉด "๋ฐ์ดํ„ฐ ๋ˆ„๋ฝ"์œผ๋กœ ๋ณด๊ณ  ๊ฒฝ๊ณ ๋งŒ ๋‚จ๊ธฐ๊ณ  ๋นˆ ์ƒํƒœ๋กœ ๋‘”๋‹ค - // (CSV ํด๋ฐฑ ์—†์Œ. building/ POIยท๊ตฌ์กฐ๋ฌผ CSV๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ). ์ธก์ ยท์ค‘์‹ฌ์„ ์€ ํ•ญ์ƒ CSV(์ธก์ )์—์„œ ์˜จ๋‹ค. - const kmzMissing = !kmz || (kmz.pois.length === 0 && kmz.structures.length === 0); + // KMZ ํŒŒ์ผ ์ž์ฒด๊ฐ€ ์—†์„ ๋•Œ๋งŒ "๋ฐ์ดํ„ฐ ๋ˆ„๋ฝ"(kmzMissing=alert ๋Œ€์ƒ)์œผ๋กœ ๋ณธ๋‹ค. + // ํŒŒ์ผ์€ ์žˆ๋Š”๋ฐ POI/๊ตฌ์กฐ๋ฌผ์ด 0๊ฑด์ธ ๊ฒฝ์šฐ(์˜ˆ: ์ œ์ฃผ โ€” ๋…ธ์„  KML๋งŒ ์žˆ๊ณ  POI ์—†์Œ)๋Š” + // ์ •์ƒ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ์ฝ˜์†” ๊ฒฝ๊ณ ๋งŒ ๋‚จ๊ธด๋‹ค. + const kmzMissing = !kmz; if (kmzMissing) { console.warn( - '[KMZ] ๋ˆ„๋ฝ/๋น„์–ด์žˆ์Œ โ€” POIยท๊ตฌ์กฐ๋ฌผ์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ' + + '[KMZ] ๋ˆ„๋ฝ โ€” POIยท๊ตฌ์กฐ๋ฌผ์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ' + 'KMZ(์›๋ณธ)๋ฅผ ํฌํ•จํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ๊ตฌ์ถ•ยท์ „๋‹ฌํ•˜์„ธ์š”. (์ธก์ ยท๋“œ๋ก  ์ •๋ณด๋Š” ์ •์ƒ ๋กœ๋“œ)', ); + } else if (kmz!.pois.length === 0 && kmz!.structures.length === 0 && kmz!.stations.length === 0) { + console.warn('[KMZ] POIยท๊ตฌ์กฐ๋ฌผยท์ธก์  0๊ฑด โ€” ์ด ๋ฐ์ดํ„ฐ์…‹์—” ํ‘œ์ถœํ•  ์ง€๋ฌผ์ด ์—†์Šต๋‹ˆ๋‹ค(๋“œ๋ก  ๊ถค์ ์€ ๋ฌด๊ด€).'); } else { - console.log(`[KMZ] POI ${kmz!.pois.length} ยท ๊ตฌ์กฐ๋ฌผ ${kmz!.structures.length} ๋กœ๋“œ(์›๋ณธ)`); + console.log(`[KMZ] POI ${kmz!.pois.length} ยท ๊ตฌ์กฐ๋ฌผ ${kmz!.structures.length} ยท ์ธก์  ${kmz!.stations.length} ๋กœ๋“œ(์›๋ณธ)`); } const pois = kmz?.pois ?? []; const baseStructures = kmz?.structures ?? []; - const stations = stationResult.stations.sort( + // ์ธก์  ์†Œ์Šค: CSV(01)์ธก์  โ€” ์‹ค์ธก ํ‘œ๊ณ  ๋ณด์œ ) ์šฐ์„ , ์—†์œผ๋ฉด KML ์ฒด์ด๋‹ˆ์ง€ ์ธก์ (STA) ์‚ฌ์šฉ. + // KML ์ธก์ ์€ ํ‘œ๊ณ  ๋ฏธ์ƒ โ†’ z=0 ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค(๋ฏธ์ƒ ํ‘œ์‹). ๋“œ๋ก ๊ณ ๋„ ๊ธฐ๋ฐ˜ ๊ทผ์‚ฌ(โˆ’24m)๋Š” + // ์‹ค์ œ ์ง€๋ฉด๊ณผ ์ˆ˜์‹ญ m ์–ด๊ธ‹๋‚˜ ๋ผ๋ฒจ์ด ๊ณต์ค‘์— ๋– ์„œ ์‹œ์ฐจ ๋ถˆ์ผ์น˜(์นด๋ฉ”๋ผ ์ „์ง„ ์‹œ ๋ผ๋ฒจ ๋ฐ€๋ฆผ)๋ฅผ + // ์œ ๋ฐœํ–ˆ์Œ โ†’ ์˜ค๋ฒ„๋ ˆ์ด๊ฐ€ z=0 ๋ผ๋ฒจ์„ '๊ฒฝ๋กœ ํ‘œ๊ณ ' ํ‰๋ฉด์— ์•ต์ปคํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋Œ€์ฒด. + let rawStations = stationResult.stations; + if (!rawStations.length && kmz?.stations.length) { + rawStations = kmz.stations; + console.log(`[KMZ] ์ฒด์ด๋‹ˆ์ง€ ์ธก์  ${rawStations.length}๊ฐœ ๋กœ๋“œ (ํ‘œ๊ณ  ๋ฏธ์ƒ โ†’ ์˜ค๋ฒ„๋ ˆ์ด '๊ฒฝ๋กœ ํ‘œ๊ณ ' ํ‰๋ฉด ์•ต์ปค)`); + } + + const stations = rawStations.sort( (a, b) => stationOrder(a.title) - stationOrder(b.title), ); const directionChanges = stationResult.directionChanges; @@ -591,5 +862,10 @@ export async function loadFolderGeoData( directionChanges, poiOverrides, kmzMissing, + videoFiles, + videoDurations, + folderFiles: files, + cameraInfo, + cameraJson, }; } diff --git a/docs/history/2026-07-09_1510_54000ํฌํŠธ-์™ธ๋ถ€์ ‘๊ทผ-์…‹ํŒ….md b/docs/history/2026-07-09_1510_54000ํฌํŠธ-์™ธ๋ถ€์ ‘๊ทผ-์…‹ํŒ….md new file mode 100644 index 0000000..405fecc --- /dev/null +++ b/docs/history/2026-07-09_1510_54000ํฌํŠธ-์™ธ๋ถ€์ ‘๊ทผ-์…‹ํŒ….md @@ -0,0 +1,36 @@ +# 2026-07-09 54000 ํฌํŠธ ์™ธ๋ถ€ ์ ‘๊ทผ ์…‹ํŒ… + +**์†Œ์š” ์‹œ๊ฐ„**: 15๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 95k / output 10k tokens + +## ์ž‘์—… ๋‚ด์šฉ + +GhiVideo_v4๋ฅผ 54000 ํฌํŠธ๋กœ ์„œ๋น„์Šคํ•˜๊ณ  ๋‹ค๋ฅธ ์ปดํ“จํ„ฐ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์…‹ํŒ…. + +### ์™„๋ฃŒ ํ•ญ๋ชฉ + +1. **์˜์กด์„ฑ ์„ค์น˜ + ๋นŒ๋“œ** + - ์‹œ์Šคํ…œ ๊ธฐ๋ณธ node(v12)๋กœ `npm install` ์‹คํŒจ (better-sqlite3 gyp ์˜ค๋ฅ˜) โ†’ nvm Node 20 (v20.20.2)์œผ๋กœ ์žฌ์„ค์น˜ ์„ฑ๊ณต + - `npm run build` (shared โ†’ server โ†’ client) ์™„๋ฃŒ + +2. **PM2 ์•ฑ ์ด๋ฆ„ ์ถฉ๋Œ ํ•ด๊ฒฐ** + - ์›๋ณธ GhiVideo์™€ v4์˜ ecosystem.config.js๊ฐ€ ๋‘˜ ๋‹ค `name: "ghiVideo"` โ†’ `pm2 start` ์‹œ ์›๋ณธ ์•ฑ์ด v4 env(PORT=54000)๋กœ ์žฌ์‹œ์ž‘๋˜๋Š” ์‚ฌ๊ณ  ๋ฐœ์ƒ + - v4์˜ ecosystem.config.js ์ด๋ฆ„์„ `ghiVideo-v4`๋กœ ๋ณ€๊ฒฝ + - ๊ผฌ์ธ ์—”ํŠธ๋ฆฌ ์‚ญ์ œ ํ›„ ์–‘์ชฝ ์žฌ๊ธฐ๋™: ์›๋ณธ `ghiVideo`(55000) ๋ณต๊ตฌ + `ghiVideo-v4`(54000) ์‹ ๊ทœ ๊ธฐ๋™ + - ๋‘˜ ๋‹ค `/api/videos` 200 ์‘๋‹ต ํ™•์ธ, `pm2 save` ์™„๋ฃŒ + +3. **Windows ํฌํŠธํฌ์›Œ๋”ฉ (๋ฏธ์™„ โ€” ์‚ฌ์šฉ์ž ์‹คํ–‰ ํ•„์š”)** + - WSL2 NAT ๋ชจ๋“œ (WSL IP: 172.21.50.250). ๊ธฐ์กด ํฌํŠธ๋“ค(55000, 5173, 8080 ๋“ฑ)์€ netsh portproxy + ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™์œผ๋กœ ๊ฐœ๋ฐฉ๋œ ํŒจํ„ด ํ™•์ธ + - 54000์šฉ ์Šคํฌ๋ฆฝํŠธ๋ฅผ `C:\Users\Public\ghivideo-v4-54000-setup.ps1`์— ์ž‘์„ฑ + - ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ์ž๋™ ์‹คํ–‰์€ ๊ถŒํ•œ ์ •์ฑ…์ƒ ์ฐจ๋‹จ โ†’ **์‚ฌ์šฉ์ž๊ฐ€ ๊ด€๋ฆฌ์ž PowerShell์—์„œ ์ง์ ‘ ์‹คํ–‰ ํ•„์š”** + +### ์žฌ๋ถ€ํŒ… ๋‚ด๊ตฌ์„ฑ ๊ฒ€ํ†  (๊ฐ™์€ ์„ธ์…˜ ํ›„์† ์ž‘์—…) + +- Windows ๋ฃจํ”„๋ฐฑ โ†’ WSL ํฌ์›Œ๋”ฉ ํ™•์ธ: `127.0.0.1:54000` 200 ์‘๋‹ต โ†’ portproxy `connectaddress=127.0.0.1` ๋ฐฉ์‹์ด๋ฉด **WSL IP๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ ๊ทœ์น™์ด ์•ˆ ๊นจ์ง**. ์„ค์ • ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ด ๋ฐฉ์‹์œผ๋กœ ๊ฐฑ์‹ ํ•จ +- ๊ธฐ์กด ๊ทœ์น™๋“ค(55000, 5173 ๋“ฑ)์€ WSL IP(172.21.50.250) ํ•˜๋“œ์ฝ”๋”ฉ โ€” ์žฌ๋ถ€ํŒ…์œผ๋กœ IP ๋ฐ”๋€Œ๋ฉด ๊นจ์งˆ ์ˆ˜ ์žˆ์Œ. ๋™์ผํ•˜๊ฒŒ 127.0.0.1๋กœ ๋ฐ”๊พธ๋Š” ๊ฒƒ ๊ถŒ์žฅ +- **pm2 ์ž๋™ ์‹œ์ž‘ ์—†์Œ**: pm2 systemd ์œ ๋‹› ์—†์Œ, ์‹œ์ž‘ํ”„๋กœ๊ทธ๋žจ์˜ AI-Video-Brief.vbs๋Š” ai_video_brief ์ „์šฉ. ์žฌ๋ถ€ํŒ… ํ›„ `pm2 resurrect` ์ˆ˜๋™ ์‹คํ–‰ ํ•„์š”. ์‹œ์ž‘ํ”„๋กœ๊ทธ๋žจ vbs ์ž๋™ ์ƒ์„ฑ์€ ๋ณด์•ˆ ์ •์ฑ… ์ฐจ๋‹จ โ†’ ์‚ฌ์šฉ์ž ์ง์ ‘ ์ƒ์„ฑ ํ•„์š” + +### ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ๊ด€๋ฆฌ์ž PowerShell์—์„œ 1ํšŒ ์‹คํ–‰: `powershell -ExecutionPolicy Bypass -File C:\Users\Public\ghivideo-v4-54000-setup.ps1` (portproxy 127.0.0.1 ๋ฐฉ์‹ + ๋ฐฉํ™”๋ฒฝ ๊ทœ์น™) +- v4 ์„œ๋ฒ„ ์ฝ”๋“œ ๊ธฐ๋ณธ ํฌํŠธ๋Š” 3030(`server/src/config.ts`)์ด๋‚˜, PM2 env๋กœ 54000 ์ฃผ์ž…๋จ diff --git a/docs/history/2026-07-09_1525_์ œ์ฃผ๋ฐ์ดํ„ฐ-ํ”Œ๋ ˆ์ด์–ด-์—ฐ๊ฒฐ.md b/docs/history/2026-07-09_1525_์ œ์ฃผ๋ฐ์ดํ„ฐ-ํ”Œ๋ ˆ์ด์–ด-์—ฐ๊ฒฐ.md new file mode 100644 index 0000000..e628353 --- /dev/null +++ b/docs/history/2026-07-09_1525_์ œ์ฃผ๋ฐ์ดํ„ฐ-ํ”Œ๋ ˆ์ด์–ด-์—ฐ๊ฒฐ.md @@ -0,0 +1,37 @@ +# 2026-07-09 ์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„ ๋„๋กœ ๋ฐ์ดํ„ฐ ํ”Œ๋ ˆ์ด์–ด ์—ฐ๊ฒฐ + +**์†Œ์š” ์‹œ๊ฐ„**: 10๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 130k / output 6k tokens + +## ์ž‘์—… ๋‚ด์šฉ + +`D:\_project\...\์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„ ๋„๋กœ ๋…ธ์„  ๋™์˜์ƒ ๋น„ํ–‰ ๋กœ๊ทธ\๊ฒฝ๋กœ 1-1` ๋ฐ์ดํ„ฐ๋ฅผ GhiVideo_v4 ํ”Œ๋ ˆ์ด์–ด์— ์—ฐ๊ฒฐ. + +### ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + +- DJI ๋“œ๋ก  ์˜์ƒ 4๊ฐœ (DJI_20260629121835_0002~0005.MP4, ์ด 12GB) + - H.264, 4K(3840x2160), 59.94fps, ๊ฐœ๋‹น ์•ฝ 197์ดˆ(0005๋Š” ๋” ์งง์Œ) +- ๋น„ํ–‰๋กœ๊ทธ CSV: `์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„๋„๋กœ ๋™์˜์ƒ ๊ฒฝ๋กœ 1-1.csv` โ€” **์‹œ๊ฐ„ ๊ธฐ๋ฐ˜**(100ms ๊ฐ„๊ฒฉ), ์ปฌ๋Ÿผ: `time(millisecond), latitude, longitude, X(E), Y(N), altitude_above_seaLevel, compass_heading, pitch, roll, gimbal_heading, gimbal_pitch, gimbal_roll` +- `์ œ์ฃผ_์ค‘์‚ฐ๊ฐ„๋„๋กœ.kml` โ€” ๋„๋กœ ๋…ธ์„  + +### ์™„๋ฃŒ ํ•ญ๋ชฉ + +- ecosystem.config.js์˜ `VIDEOS_DIR`/`GEO_DATA_DIR`๋ฅผ `/mnt/d/...` ์ œ์ฃผ ๊ฒฝ๋กœ๋กœ ๋ณ€๊ฒฝ, pm2 ์žฌ๊ธฐ๋™ + save +- `/api/videos` 4๊ฐœ ์˜์ƒ ์ธ์‹, `/api/meta` ์ •์ƒ (H.264 ํ™•์ธ โ†’ HLS ๋ณ€ํ™˜ ์‹œ `-c copy` ๊ฐ€๋Šฅ) +- Range ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฒ€์ฆ: ์•ž๋ถ€๋ถ„ 55MB/s, 2GB ์ง€์  seek 34MB/s (4K60 ๋น„ํŠธ๋ ˆ์ดํŠธ ์•ฝ 19MB/s ๋Œ€๋น„ ์ถฉ๋ถ„) + +### ๋ฏธ์™„๋ฃŒ / ์•Œ๋ ค์ง„ ์ œ์•ฝ (geo ๋น„ํ–‰๋กœ๊ทธ ์—ฐ๋™) + +ํ˜„์žฌ geoMatch ์„œ๋น„์Šค๋Š” ํšŒ๋• ๋ฐ์ดํ„ฐ์…‹ ์ „์šฉ์ด๋ผ ์ œ์ฃผ CSV๋ฅผ ์ฝ์ง€ ๋ชปํ•จ: + +1. ๋“œ๋ก  CSV ํŒŒ์ผ๋ช… `'ํšŒ๋•'` ํ•˜๋“œ์ฝ”๋”ฉ ํ•„ํ„ฐ (`geoMatch.ts` loadFrames) +2. ์ปฌ๋Ÿผ ๋ถˆ์ผ์น˜: ๊ธฐ๋Œ€ `frame_cnt/latitude/longitude/altitude/yaw/pitch/roll/focal_len` vs ์ œ์ฃผ `time(millisecond)/compass_heading/gimbal_heading/...` โ†’ ์‹œ๊ฐ„โ†’ํ”„๋ ˆ์ž„ ๋ณ€ํ™˜ ์–ด๋Œ‘ํ„ฐ ํ•„์š” +3. POI/์ธก์ (`building/` ํด๋”) ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ์ฃผ ๋ฐ์ดํ„ฐ์…‹์— ์—†์Œ โ†’ POI ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์€ ๋ฐ์ดํ„ฐ ๋ถ€์žฌ๋กœ ๋ถˆ๊ฐ€ +4. ์˜์ƒ์ด 4๊ฐœ๋กœ ๋ถ„ํ•  โ†’ ๋กœ๊ทธ ์‹œ๊ฐ„์„ (์˜์ƒ ํŒŒ์ผ, ์˜์ƒ ๋‚ด ์‹œ์ )์œผ๋กœ ๋งคํ•‘ํ•˜๋Š” ์˜คํ”„์…‹ ๊ณ„์‚ฐ ํ•„์š” +5. KML์€ ์ค‘์‹ฌ์„ (center.csv ๋Œ€์ฒด) ์†Œ์Šค๋กœ ํ™œ์šฉ ๊ฐ€๋Šฅ + +### ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์—…๋กœ๋“œ ๊ธฐ๋Šฅ(tus) ์‚ฌ์šฉ ์‹œ D: ํด๋”(VIDEOS_DIR)์— ์ €์žฅ๋จ โ€” ์˜๋„ ํ™•์ธ ํ•„์š” +- HLS/ํ”„๋ ˆ์ž„/์ธ๋„ค์ผ ์‚ฐ์ถœ๋ฌผ์€ ๊ธฐ์กด๋Œ€๋กœ ํ”„๋กœ์ ํŠธ `storage/`์— ์ƒ์„ฑ๋จ +- ์ œ์ฃผ geo ์–ด๋Œ‘ํ„ฐ ์ž‘์—… ์‹œ ์š”๊ตฌ ๊ธฐ๋Šฅ(๊ฒฝ๋กœ ์‹œ๊ฐํ™”, ํ”„๋ ˆ์ž„๋ณ„ ์œ„์น˜ ํ‘œ์‹œ ๋“ฑ) ๋ฒ”์œ„ ํ™•์ • ํ•„์š” diff --git a/docs/history/2026-07-09_1558_์ œ์ฃผ-๋“œ๋ก ๊ถค์ -์–ด๋Œ‘ํ„ฐ.md b/docs/history/2026-07-09_1558_์ œ์ฃผ-๋“œ๋ก ๊ถค์ -์–ด๋Œ‘ํ„ฐ.md new file mode 100644 index 0000000..8bbaa0a --- /dev/null +++ b/docs/history/2026-07-09_1558_์ œ์ฃผ-๋“œ๋ก ๊ถค์ -์–ด๋Œ‘ํ„ฐ.md @@ -0,0 +1,49 @@ +# 2026-07-09 ์ œ์ฃผ DJI ๋น„ํ–‰๋กœ๊ทธ ๋“œ๋ก ๊ถค์  ์–ด๋Œ‘ํ„ฐ + +**์†Œ์š” ์‹œ๊ฐ„**: 30๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 175k / output 12k tokens + +## ๋ฌธ์ œ + +์ œ์ฃผ ๋ฐ์ดํ„ฐ ํด๋” ์„ ํƒ ์‹œ ๋“œ๋ก  ๊ถค์  ์˜ค๋ฒ„๋ ˆ์ด๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ. + +## ์›์ธ + +ํด๋ผ์ด์–ธํŠธ ์ง€๋ฆฌ์ •๋ณด ํŒŒ์ดํ”„๋ผ์ธ(ํด๋” ์„ ํƒ โ†’ `geoData.ts` ํŒŒ์‹ฑ โ†’ geoStore โ†’ StationOverlay ํˆฌ์˜)์ด ํšŒ๋• v2.0 ํ”„๋ ˆ์ž„ ๊ธฐ๋ฐ˜ CSV(`frame_cnt,latitude,โ€ฆ,yaw,pitch,roll,focal_len`) ์ „์šฉ์ด์—ˆ์Œ. ์ œ์ฃผ CSV๋Š” DJI ๋น„ํ–‰๋กœ๊ทธ ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜(`time(millisecond),latitude,โ€ฆ,compass_heading,gimbal_heading,โ€ฆ`)์ด๋ผ: + +1. ์œ„์น˜ ํด๋ฐฑ ์ธ๋ฑ์Šค๋กœ ์ปฌ๋Ÿผ์ด ๋ฐ€๋ฆผ โ€” frameโ†ms๊ฐ’(89500~), altitudeโ†X(E) ํ‰๋ฉด์ขŒํ‘œ, yawโ†Y(N), focalLenโ†pitch ๋“ฑ ์ „๋ถ€ ์˜ค์—ผ +2. `effectiveFps`(=๋งˆ์ง€๋ง‰ frame รท ์˜์ƒ๊ธธ์ด) ์ž๊ธฐ๋ณด์ •์ด ๊นจ์ ธ ์‹œ๊ฐ„โ†”ํ”„๋ ˆ์ž„ ๋ณ€ํ™˜ ์‹คํŒจ +3. ๋ถ„ํ•  ์˜์ƒ 4๊ฐœ(_0002~_0005) ์ค‘ ์ฒซ ์˜์ƒ ๊ธธ์ด(197.1s)์— ๋กœ๊ทธ ์ „์ฒด(672.6s)๊ฐ€ ์••์ถ• ๋งคํ•‘๋จ +4. ๊ฒฝ๋กœํ‘œ๊ณ  ์Šฌ๋ผ์ด๋” ์ƒํ•œ(์ค‘์‹ฌ์„  z ๊ธฐ๋ฐ˜, ํด๋ฐฑ 60m)์ด ์ œ์ฃผ ์ง€ํ˜•(๋“œ๋ก  148m ASL)์— ๋ชป ๋ฏธ์นจ + +## ์ˆ˜์ • ๋‚ด์šฉ + +### client/src/utils/geoData.ts + +- `parseDroneFrames`: `time(millisecond)` ํ—ค๋” ๊ฐ์ง€ ์‹œ DJI ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ๋ธŒ๋žœ์น˜๋กœ ๋ถ„๊ธฐ + - ์ปฌ๋Ÿผ: lat/lon/`altitude_above_seaLevel(meter)`, ์ž์„ธ๋Š” gimbal_* ์šฐ์„ (๊ธฐ์ฒด compass/pitch/roll ํด๋ฐฑ), focalLen=24mm ๊ธฐ๋ณธ + - `frame = ((timeโˆ’t0)/1000 โˆ’ offsetSec) ร— 59.94(๊ฐ€์ƒfps)` โ€” effectiveFps ์ž๊ธฐ๋ณด์ •๊ณผ ์ •ํ•ฉ + - `SegmentWindow`(offsetSec/durationSec)๋กœ ์žฌ์ƒ ์˜์ƒ ๊ตฌ๊ฐ„ ๋ฐ– ํ–‰ ํด๋ฆฌํ•‘ +- `findVideoFiles`: ์˜์ƒ ์ด๋ฆ„์ˆœ ์ •๋ ฌ(DJI ๋ถ„ํ• ๋ณธ _0002 ๋จผ์ € ํ™•์ •) +- `probeVideoDuration`: objectURL + `preload=metadata`๋กœ ์˜์ƒ ๊ธธ์ด๋งŒ ์ธก์ • +- `loadFolderGeoData`: ์ฒซ ์˜์ƒ ๊ธธ์ด ์ธก์ • โ†’ ์„ธ๊ทธ๋จผํŠธ ์ฐฝ ์ „๋‹ฌ, ๋ถ„ํ• ๋ณธ ๊ฐ์ง€ ์‹œ ์ฝ˜์†” ๊ฒฝ๊ณ  +- `parseRouteMeta`: ์ž„์˜ ์ ‘๋‘ `*.route.json` ์ธ์‹ (์ œ์ฃผ route.json ํŒŒ์ผ๋ช… ๋Œ€์‘) +- `parseKmz`: ์ฒด์ด๋‹ˆ์ง€ ํ‘œ์ (`0+100` ํ˜•์‹) Point๋Š” POI ์ œ์™ธ (z=0์ด๋ผ ํˆฌ์˜ ๊นจ์ง ๋ฐฉ์ง€) +- `kmzMissing`: KML/KMZ ํŒŒ์ผ ์ž์ฒด๊ฐ€ ์—†์„ ๋•Œ๋งŒ alert (ํŒŒ์ผ์€ ์žˆ์œผ๋‚˜ POI 0๊ฑด์€ ์ฝ˜์†” ๊ฒฝ๊ณ ๋งŒ) + +### client/src/components/overlay/StationOverlay.tsx + +- `pathZMax`: ์ค‘์‹ฌ์„  z์— ๋”ํ•ด ๋“œ๋ก  ๊ณ ๋„(ASL) ์ตœ๋Œ€๊ฐ’๋„ ์ƒํ•œ ๊ทผ๊ฑฐ์— ํฌํ•จ โ†’ ์ œ์ฃผ์—์„œ ์Šฌ๋ผ์ด๋” 159m๊นŒ์ง€ + +## ๊ฒ€์ฆ + +- `npm run build -w client` (tsc + vite) ํ†ต๊ณผ, 54000 ํฌํŠธ๊ฐ€ ์ƒˆ ๋ฒˆ๋“ค(index-Cp4zEgX_.js) ์„œ๋น™ ํ™•์ธ +- ์‹ค์ œ CSV๋กœ ํŒŒ์‹ฑ ์ˆ˜ํ•™ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: 6649ํ–‰ โ†’ ์„ธ๊ทธ๋จผํŠธ ๋‚ด 1947ํ–‰, frame 0~11814, effectiveFps 59.9248โ†’59.94 ์Šค๋ƒ…, ์˜์ƒ t=100s โ†” ๋กœ๊ทธ t=100.00s ์™•๋ณต ์ •ํ•ฉ, ์ž์„ธ๊ฐ’ ์ •์ƒ(yaw 65.1ยฐ, pitch โˆ’15ยฐ, alt 147.9m) + +## ๋‚จ์€ ์ œ์•ฝ / ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- **๋ถ„ํ•  ์˜์ƒ 2~4๋ฒˆ์งธ(_0003~_0005) ๋ฏธ์ง€์›** โ€” ์„ธ๊ทธ๋จผํŠธ ์„ ํƒ UI + offsetSec(๋ˆ„์  ๊ธธ์ด) ์—ฐ๊ฒฐ ํ•„์š”. ๋กœ๊ทธ ํญ(672.6s) โ‰ˆ ์˜์ƒ ์ด๊ธธ์ด(671.7s) ๊ฒ€์ฆ๋จ, ์˜คํ”„์…‹: 0 / 197.147 / 394.211 / 591.475s +- ๋“œ๋ก  ๊ณ ๋„ ๊ฑฐ์˜ ์ผ์ •(147.8~148.6m ASL) โ€” ๋„๋กœ ํ‘œ๊ณ ๋Š” ๊ทธ ์•„๋ž˜. ๊ฒฝ๋กœํ‘œ๊ณ  ์Šฌ๋ผ์ด๋”๋กœ 90~140m ๋ถ€๊ทผ ํŠœ๋‹ ํ•„์š” +- KML LineString(๋„๋กœ ์ค‘์‹ฌ์„ )์€ ๊ณ ๋„๊ฐ€ ์ „๋ถ€ 0์ด๋ผ ์ค‘์‹ฌ์„ (์„ ํ˜• ์˜ค๋ฒ„๋ ˆ์ด) ์†Œ์Šค๋กœ ๋ฏธ์‚ฌ์šฉ +- ์ธก์ /POI ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ์ฃผ ๋ฐ์ดํ„ฐ์…‹์— ์—†์Œ โ€” KML ์ฒด์ด๋‹ˆ์ง€ ํ‘œ์ (0+000~) ํ™œ์šฉํ•˜๋ ค๋ฉด ํ‘œ๊ณ  ๋ถ€์—ฌ ํ•„์š” +- route.json์˜ durationSec(568)์ด ์‹ค์ œ ์˜์ƒ ์ด๊ธธ์ด(671.7s)์™€ ๋ถˆ์ผ์น˜ โ€” ์‚ฌ์šฉ์ž ํ™•์ธ ํ•„์š” diff --git a/docs/history/2026-07-09_1637_๋ถ„ํ• ์˜์ƒ-์—ฐ์†์žฌ์ƒ.md b/docs/history/2026-07-09_1637_๋ถ„ํ• ์˜์ƒ-์—ฐ์†์žฌ์ƒ.md new file mode 100644 index 0000000..ef53dc5 --- /dev/null +++ b/docs/history/2026-07-09_1637_๋ถ„ํ• ์˜์ƒ-์—ฐ์†์žฌ์ƒ.md @@ -0,0 +1,48 @@ +# 2026-07-09 ๋ถ„ํ•  ์˜์ƒ ์—ฐ์†์žฌ์ƒ (์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ ์ž๋™ ์ „ํ™˜) + +**์†Œ์š” ์‹œ๊ฐ„**: 25๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 210k / output 8k tokens + +## ์ž‘์—… ๋‚ด์šฉ + +ํด๋” ๋‚ด ๋ถ„ํ•  ์˜์ƒ(DJI _0002~_0005)์ด ๋๋‚˜๋ฉด ์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ๋‹ค์Œ ์˜์ƒ์„ ์ž๋™ ์žฌ์ƒ. +์ „ํ™˜ ์‹œ ๋“œ๋ก  ๋กœ๊ทธ(์‹œ๊ฐ„ ๊ธฐ๋ฐ˜)๋ฅผ ํ•ด๋‹น ์„ธ๊ทธ๋จผํŠธ์˜ ๋ˆ„์  ์˜คํ”„์…‹์œผ๋กœ ์žฌ์ •๋ ฌํ•ด ๊ถค์  ์ •ํ•ฉ ์œ ์ง€. + +## ์ˆ˜์ • ํŒŒ์ผ + +### client/src/types/geo.ts +- `FolderGeoData`์— `videoFiles`(์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ), `videoDurations`, `folderFiles` ์ถ”๊ฐ€ + +### client/src/utils/geoData.ts +- `loadFolderGeoData`: ํด๋” ๋‚ด **๋ชจ๋“ ** ์˜์ƒ ๊ธธ์ด๋ฅผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋กœ ์ธก์ •(๊ธฐ์กด์—” ์ฒซ ์˜์ƒ๋งŒ), + ๋ถ„ํ• ๋ณธ ๊ฐ์ง€ ์‹œ ์žฌ์ƒ ์ˆœ์„œ ์ฝ˜์†” ๋กœ๊ทธ. ์ธก์ •๊ฐ’ยท์›๋ณธ ํŒŒ์ผ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜๊ฐ’์— ํฌํ•จ + +### client/src/store/geoStore.ts +- ์ƒํƒœ ์ถ”๊ฐ€: `videoFiles` / `videoDurations` / `videoIndex` / `folderFiles` +- **`loadSegment(index)` ์•ก์…˜ ์‹ ์„ค**: ๋ˆ„์  ์˜คํ”„์…‹(์•ž ์„ธ๊ทธ๋จผํŠธ ๊ธธ์ด ํ•ฉ) ๊ณ„์‚ฐ โ†’ + `parseDroneFrames`๋ฅผ ์„ธ๊ทธ๋จผํŠธ ์ฐฝ์œผ๋กœ ์žฌ์‹คํ–‰ํ•ด frames ๊ต์ฒด, videoIndex ๊ฐฑ์‹ , ์žฌ์ƒํ•  File ๋ฐ˜ํ™˜ + - ๊ธธ์ด ์ธก์ • ์‹คํŒจ ์„ธ๊ทธ๋จผํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ๊ถค์ ๋งŒ ๋น„์šฐ๊ณ (frames=[]) ์žฌ์ƒ์€ ๊ณ„์† + - `baseName`์€ ํด๋”(์ฒซ ์˜์ƒ) ๊ธฐ์ค€ ์œ ์ง€ โ€” ์นด๋ฉ”๋ผ ๋ณด์ • localStorage ์—ฐ์†์„ฑ + - `origin`๋„ ํด๋” ๋กœ๋“œ ์‹œ์  ๊ฐ’ ์œ ์ง€ โ€” ENU ๊ธฐ์ค€์  ์ผ๊ด€์„ฑ(์ „ํ™˜ ์‹œ ํˆฌ์˜ ์ ํ”„ ๋ฐฉ์ง€) + +### client/src/components/player/VideoPlayer.tsx +- Video.js `ended` ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€: ํด๋” ๊ธฐ๋ฐ˜ ๋กœ์ปฌ ์žฌ์ƒ + ๋ถ„ํ• ๋ณธ 2๊ฐœ ์ด์ƒ์ผ ๋•Œ๋งŒ + `loadSegment(next)` โ†’ `loadLocalFile(file)` โ†’ `player.play()` (ended ์งํ›„๋Š” paused๋ผ ์ง์ ‘ ์žฌ์ƒ) +- ์„œ๋ฒ„ ์ŠคํŠธ๋ฆผ/๋‹จ์ผ ํŒŒ์ผ ์žฌ์ƒ์€ ๋Œ€์ƒ ์•„๋‹˜ (source.kind !== 'local' ๊ฐ€๋“œ) + +## ๊ฒ€์ฆ + +- `npm run build -w client` (tsc + vite) ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-BA4g81-1.js) ์„œ๋น™ ํ™•์ธ +- ์‹ค์ œ CSV๋กœ 4๊ฐœ ์„ธ๊ทธ๋จผํŠธ ์˜คํ”„์…‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: + - ์„ธ๊ทธ๋จผํŠธ 1: offset 0s, 1947ํ–‰, effFps 59.925 โ†’ 59.94 ์Šค๋ƒ… โœ“ + - ์„ธ๊ทธ๋จผํŠธ 2: offset 197.1s, 1963ํ–‰, effFps 59.935 โœ“ + - ์„ธ๊ทธ๋จผํŠธ 3: offset 394.2s, 1936ํ–‰, effFps 59.920 โœ“ + - ์„ธ๊ทธ๋จผํŠธ 4: offset 591.5s, 793ํ–‰, effFps 59.877 โœ“ (ยฑ10% ์Šค๋ƒ… ๋ฒ”์œ„ ๋‚ด) + - ํ–‰ ํ•ฉ๊ณ„ 6639/6649 โ€” ๊ฒฝ๊ณ„ ์ธ์ ‘ ์†Œ์ˆ˜ ํ–‰๋งŒ ์ œ์™ธ, ์ •์ƒ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์„ธ๊ทธ๋จผํŠธ ์ˆ˜๋™ ์„ ํƒ UI(๋ชฉ๋ก์—์„œ ํŠน์ • ์„ธ๊ทธ๋จผํŠธ๋กœ ์ ํ”„)๋Š” ๋ฏธ๊ตฌํ˜„ โ€” `loadSegment`๊ฐ€ ์ด๋ฏธ ์žˆ์–ด + UI๋งŒ ๋ถ™์ด๋ฉด ๋จ (`useGeoStore.getState().loadSegment(i)` + `loadLocalFile`) +- ๋งˆ์ง€๋ง‰ ์„ธ๊ทธ๋จผํŠธ(_0005) ์ข…๋ฃŒ ์‹œ ์ •์ง€ (๋ฐ˜๋ณต ์žฌ์ƒ ์—†์Œ โ€” ์š”๊ตฌ์‚ฌํ•ญ ๋ฐ–) +- ์ „ํ™˜ ์‹œ 1~2์ดˆ ๋กœ๋”ฉ ๊ณต๋ฐฑ ๊ฐ€๋Šฅ(objectURL ๊ต์ฒด) โ€” ํ”„๋ฆฌ๋กœ๋“œ ์ตœ์ ํ™”๋Š” ํ•„์š” ์‹œ diff --git a/docs/history/2026-07-09_1648_์Šคํ…Œ์ด์…˜๋ฐ”-์ด๋™๊ฑฐ๋ฆฌ-์ง„ํ–‰๋„.md b/docs/history/2026-07-09_1648_์Šคํ…Œ์ด์…˜๋ฐ”-์ด๋™๊ฑฐ๋ฆฌ-์ง„ํ–‰๋„.md new file mode 100644 index 0000000..66de9ce --- /dev/null +++ b/docs/history/2026-07-09_1648_์Šคํ…Œ์ด์…˜๋ฐ”-์ด๋™๊ฑฐ๋ฆฌ-์ง„ํ–‰๋„.md @@ -0,0 +1,44 @@ +# 2026-07-09 ์Šคํ…Œ์ด์…˜๋ฐ” ์ง„ํ–‰๋„ โ€” ๋“œ๋ก  ์ •์ง€ ์‹œ ์ปค์„œ ์ •์ง€ (์ด๋™๊ฑฐ๋ฆฌ์ถ• ํด๋ฐฑ) + +**์†Œ์š” ์‹œ๊ฐ„**: 10๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 240k / output 5k tokens + +## ๋ฌธ์ œ + +๋“œ๋ก ์ด ์ด๋™์„ ๋ฉˆ์ท„๋Š”๋ฐ(ํ˜ธ๋ฒ„๋ง) ํ•˜๋‹จ ์Šคํ…Œ์ด์…˜๋ฐ” ์ปค์„œ๋Š” ๊ณ„์† ์ฆ๊ฐ€. + +## ์›์ธ + +StationBar์˜ ์ง„ํ–‰๋„ ์ถ•(chainRef: ์‹œ๊ฐ„โ†’๋ˆ„์  ์ด๋™๊ฑฐ๋ฆฌ ๋น„์œจ)์€ **์ธก์  ํด๋ฆฌ๋ผ์ธ ์ฒด์ด๋‹ˆ์ง€ ๊ธฐ๋ฐ˜**์œผ๋กœ๋งŒ +๊ตฌ์„ฑ๋จ (`frames + stations + stationLine` ํ•„์š”). ์ œ์ฃผ ๋ฐ์ดํ„ฐ์…‹์€ ์ธก์ ์ด ์—†์–ด `ready=false` +โ†’ `cumFracAtTime`์ด **์‹œ๊ฐ„ ์„ ํ˜• ํด๋ฐฑ**(`t / duration`) ์‚ฌ์šฉ โ†’ ๋“œ๋ก ์ด ๋ฉˆ์ถฐ๋„ ์ปค์„œ๋Š” ์‹œ๊ฐ„ ๋น„๋ก€๋กœ ์ „์ง„. + +์‹ค๋ฐ์ดํ„ฐ ํ™•์ธ: ์„ธ๊ทธ๋จผํŠธ 2์—์„œ ๋“œ๋ก ์ด t=8~128s(120์ดˆ), t=178~198s ๋‘ ๋ฒˆ ํ˜ธ๋ฒ„๋ง โ€” +์‚ฌ์šฉ์ž๊ฐ€ ๊ด€์ธกํ•œ ๊ตฌ๊ฐ„๊ณผ ์ผ์น˜. + +## ์ˆ˜์ • ๋‚ด์šฉ โ€” client/src/stationbar/StationBar.tsx + +- precompute useEffect์— **์ธก์  ์—†์Œ ํด๋ฐฑ** ์ถ”๊ฐ€: ๋“œ๋ก  ํ”„๋ ˆ์ž„๋งŒ ์žˆ์œผ๋ฉด GPS(์œ„๊ฒฝ๋„)๋ฅผ + ยฑ8ํ”„๋ ˆ์ž„ ์ด๋™ํ‰๊ท ์œผ๋กœ ํ‰ํ™œ ํ›„ ํ”„๋ ˆ์ž„ ๊ฐ„ ์ˆ˜ํ‰๊ฑฐ๋ฆฌ ๋ˆ„์  โ†’ ์ •๊ทœํ™”(frac)ํ•ด chainRef ๊ตฌ์„ฑ. + ์ธก์  ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ(๋ฐฐ์ง€/๊ตฌ๊ฐ„์ƒ‰/๊ตฌ์กฐ๋ฌผ ๋งˆํฌ)์€ ๊ธฐ์กด๋Œ€๋กœ ready=false ๋น„ํ™œ์„ฑ ์œ ์ง€ +- `cumFracAtTime` / `timeAtFrac` ๊ฐ€๋“œ์—์„œ `!ready` ์ œ๊ฑฐ โ€” chainRef ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ™•์ธ + (ํด๋ฐฑ chainRef๋„ ์‚ฌ์šฉ). ๋ฐ” ํด๋ฆญ seek๋„ ์ด๋™๊ฑฐ๋ฆฌ์ถ• ์—ญ๋ณ€ํ™˜์œผ๋กœ ๋™์ž‘ +- depChain/arrChain์€ ๋ฐฉํ–ฅ์ƒ‰ ์ „์šฉ์ด๋ผ ํด๋ฐฑ์—์„  ๋”๋ฏธ(0/1) + +## ๊ฒ€์ฆ (์‹ค๋ฐ์ดํ„ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜, ์„ธ๊ทธ๋จผํŠธ 2) + +| ์‹œ์  | ๊ธฐ์กด(์‹œ๊ฐ„์„ ํ˜•) | ์ˆ˜์ •(์ด๋™๊ฑฐ๋ฆฌ) | +|------|--------------|--------------| +| t=8s (ํ˜ธ๋ฒ„ ์‹œ์ž‘) | 4.1% | 8.0% | +| t=68s (ํ˜ธ๋ฒ„ ์ค‘) | 34.5% | 8.4% | +| t=128s (ํ˜ธ๋ฒ„ ๋) | 65.0% | 9.0% | + +ํ˜ธ๋ฒ„ 120์ดˆ ๋™์•ˆ ์ปค์„œ๊ฐ€ ์‚ฌ์‹ค์ƒ ์ •์ง€(+1.0%p). ์„ธ๊ทธ๋จผํŠธ 2 ์ด ์ด๋™๊ฑฐ๋ฆฌ 460.2m. +tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-quVWnwfd.js) ์„œ๋น™ ํ™•์ธ. + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์ •์ง€ ๊ตฌ๊ฐ„์—์„œ ์ปค์„œ๊ฐ€ ๋ฉˆ์ถ”๋ฏ€๋กœ, ๊ทธ ๋™์•ˆ ์‹œ๊ฐ„์€ ํ๋ฅด๋Š”๋ฐ ์ปค์„œ๊ฐ€ ์•ˆ ์›€์ง์ด๋Š” ๊ฒŒ ์ •์ƒ ๋™์ž‘ + (์ขŒ์ธก ์‹œ๊ฐ„ ๋ฐฐ์ง€๋Š” ๊ณ„์† ์ฆ๊ฐ€ โ€” ์„œ๋กœ ๋‹ค๋ฅธ ์ถ•) +- ๋“œ๋ก ์ด ์ „ ๊ตฌ๊ฐ„ ์ •์ง€(์ด ์ด๋™๊ฑฐ๋ฆฌ 0)์ธ ๊ทน๋‹จ ์ผ€์ด์Šค๋Š” frac ์ „๋ถ€ 0 โ†’ ์ปค์„œ ์‹œ์ž‘์  ๊ณ ์ • +- ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜(loadSegment) ์‹œ frames ๊ต์ฒด โ†’ chainRef ์ž๋™ ์žฌ๊ตฌ์„ฑ๋จ diff --git a/docs/history/2026-07-09_1702_KML-์‹œ์„ค๋ฌผ-์ธก์ -์ž„ํฌํŠธ.md b/docs/history/2026-07-09_1702_KML-์‹œ์„ค๋ฌผ-์ธก์ -์ž„ํฌํŠธ.md new file mode 100644 index 0000000..22d5d53 --- /dev/null +++ b/docs/history/2026-07-09_1702_KML-์‹œ์„ค๋ฌผ-์ธก์ -์ž„ํฌํŠธ.md @@ -0,0 +1,53 @@ +# 2026-07-09 KML ์‹œ์„ค๋ฌผ/์ธก์  ์ •๋ณด ์ž„ํฌํŠธ (ExtendedData ์ง€์›) + +**์†Œ์š” ์‹œ๊ฐ„**: 12๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 265k / output 6k tokens + +## ์ž‘์—… ๋‚ด์šฉ + +KML ํŒŒ์ผ์—์„œ ์‹œ์„ค๋ฌผ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก parseKmz ํ™•์žฅ. ์ œ์ฃผ KML์˜ STA(์ฒด์ด๋‹ˆ์ง€ ์ธก์ ) +71๊ฐœ๊ฐ€ ์ธก์ ์œผ๋กœ ๋กœ๋“œ๋˜์–ด ์ธก์  ํŒŒ์ดํ”„๋ผ์ธ ์ „์ฒด(์Šคํ…Œ์ด์…˜๋ฐ”ยท๋…ธ์„ ํŒจ๋„ยท๊ฒ€์ƒ‰ยท์˜ค๋ฒ„๋ ˆ์ด) ํ™œ์„ฑํ™”. + +## ๋ฐฐ๊ฒฝ โ€” ์ œ์ฃผ KML ๊ตฌ์กฐ + +``` +Folder ์„ ํ˜•์ค‘์‹ฌ์„ : LineString (๋„๋กœ ์ค‘์‹ฌ์„ , CAD ๋‚ด๋ณด๋‚ด๊ธฐ) +Folder STA: Point 71๊ฐœ (0+000 ~ 6+998, 100m ๊ฐ„๊ฒฉ) + โ”” ExtendedData/SchemaData/SimpleData: STA(์ด๋ฆ„), lat, lon, 5186_x/y, km(float) +``` + +๊ธฐ์กด parseKmz ๋Š” description CDATA HTML ํ‘œ(ํšŒ๋• ์ง€์˜ค์ฝ”๋”ฉ ์‚ฐ์ถœ๋ฌผ)๋งŒ ์†์„ฑ์œผ๋กœ ์ฝ์—ˆ๊ณ , +์ฒด์ด๋‹ˆ์ง€ Point ๋Š” ์ง์ „ ์ž‘์—…์—์„œ POI ์˜ค์—ผ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์Šคํ‚ต ์ฒ˜๋ฆฌํ–ˆ์—ˆ์Œ. + +## ์ˆ˜์ • ๋‚ด์šฉ โ€” client/src/utils/geoData.ts + +1. **SimpleData ์†์„ฑ ์ˆ˜์ง‘**: `ExtendedData/SchemaData/SimpleData` ๋ฅผ description HTML ํ‘œ์™€ + ๋™์ผํ•˜๊ฒŒ props ๋กœ ํ†ตํ•ฉ โ€” CADยทGIS ๋‚ด๋ณด๋‚ด๊ธฐ KML์˜ ์†์„ฑ์ด ๊ธฐ์กด ํด๋” ๋ถ„๊ธฐ(๊ต๋Ÿ‰/ํ„ฐ๋„/๊ตฌ๊ต/์ถœ์ž…๋ฌธ)์—์„œ๋„ + ๊ทธ๋Œ€๋กœ ๋™์ž‘ (ํ–ฅํ›„ ์‹œ์„ค๋ฌผ ํด๋”๊ฐ€ ์žˆ๋Š” KML ๋Œ€์‘) +2. **์ฒด์ด๋‹ˆ์ง€ ์ธก์  โ†’ station**: ์ด๋ฆ„ `N+NNN` ํ˜•์‹ ๋˜๋Š” STA ์†์„ฑ ๋ณด์œ  Point ๋ฅผ + `type='station'` ์œผ๋กœ ์ˆ˜์ง‘. km ์†์„ฑ(๋‹จ์œ„ km) ์šฐ์„ , ์—†์œผ๋ฉด ์ด๋ฆ„ ํŒŒ์‹ฑ. + ์ œ๋ชฉ์„ ์ฒ ๋„์‹ `NkNNN`("0k100")์œผ๋กœ ๋ณ€ํ™˜ โ†’ stationOrder/stationKm ํŒŒ์ดํ”„๋ผ์ธ ๊ทธ๋Œ€๋กœ ํ˜ธํ™˜ +3. **parseKmz ๋ฐ˜ํ™˜์— `stations` ์ถ”๊ฐ€**, ๋กœ๋“œ ๋กœ๊ทธ์— ์ธก์  ์ˆ˜ ํ‘œ๊ธฐ +4. **loadFolderGeoData**: CSV ์ธก์ (01)์ธก์  โ€” ์‹ค์ธก ํ‘œ๊ณ ) ์šฐ์„ , ์—†์œผ๋ฉด KML ์ธก์  ์‚ฌ์šฉ. + KML ์ธก์ ์€ ํ‘œ๊ณ  ๋ฏธ์ƒ(z=0) โ†’ **์ตœ๊ทผ์ ‘ ๋“œ๋ก  ํ”„๋ ˆ์ž„ ๊ณ ๋„ โˆ’ 24m ๊ทผ์‚ฌ** + (๋„๋กœ ์ถ”์ข… ์ €๊ณ ๋„ ๋น„ํ–‰ ๊ฐ€์ •, ์˜ค๋ฒ„๋ ˆ์ด ๊ธฐ๋ณธ ๋ชจ๋“œ๊ฐ€ ๋“œ๋ก ๊ณ ๋„ ๊ธฐ์ค€์ด๋ผ ์˜ค์ฐจ ์˜ํ–ฅ ์ž‘์Œ) + +## ํ™œ์„ฑํ™”๋˜๋Š” ๊ธฐ๋Šฅ (์ œ์ฃผ ๋ฐ์ดํ„ฐ) + +- ํ•˜๋‹จ ์Šคํ…Œ์ด์…˜๋ฐ”: ์ธก์  ์ฒด์ด๋‹ˆ์ง€ ๊ธฐ๋ฐ˜ ์ด๋™๊ฑฐ๋ฆฌ์ถ•(ready=true), ์ตœ๊ทผ์ ‘ ์ธก์  ๋ฐฐ์ง€, ๋ฐ” ํด๋ฆญ seek +- ์ขŒ์ธก ๋…ธ์„  ํŒจ๋„(RoutePanel): ์ธก์  71๊ฐœ (0k000~6k998), ์‹œ์ /์ข…์ ๋ช…์€ route.json(A/B) +- ์˜์ƒ ์˜ค๋ฒ„๋ ˆ์ด: ์ธก์  ๋ผ๋ฒจ ํˆฌ์˜ (๋“œ๋ก ๊ณ ๋„ โˆ’ ์ด๊ฒฉ ๋ชจ๋“œ ๊ธฐ๋ณธ) +- GeoSearch: ์ธก์ ๋ช… ๊ฒ€์ƒ‰/์ž๋™์™„์„ฑ + +## ๊ฒ€์ฆ + +- tsc + vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-BvbrGp3O.js) ์„œ๋น™ +- KML STA ๋ฐ์ดํ„ฐ ๋ ˆ๋ฒจ ๊ฒ€์ฆ: 71๊ฐœ ์ „๋ถ€ ๋ณ€ํ™˜, ์ œ๋ชฉ 0k000~6k998, stationKm ํŒŒ์‹ฑ ์œ ํšจ, ๋‹จ์กฐ์ฆ๊ฐ€ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- KML ๋…ธ์„ ์€ 0~7km์ธ๋ฐ ๊ฒฝ๋กœ 1-1 ๋น„ํ–‰์€ ๊ทธ์ค‘ ์•ฝ 1.9km ๊ตฌ๊ฐ„๋งŒ ์ปค๋ฒ„ โ€” RoutePanel ๋งˆ์ปค๊ฐ€ + ์ผ๋ถ€ ๊ตฌ๊ฐ„์—์„œ๋งŒ ์›€์ง์ด๋Š” ๊ฒƒ์ด ์ •์ƒ +- ๊ต๋Ÿ‰/ํ„ฐ๋„ ๋“ฑ ์‹ค์ œ ์‹œ์„ค๋ฌผ Placemark ๊ฐ€ ์žˆ๋Š” KML ์ด ์˜ค๋ฉด ๊ธฐ์กด ํด๋”๋ช… ๋ถ„๊ธฐ(๊ต๋Ÿ‰/ํ„ฐ๋„/๊ตฌ๊ต/์ถœ์ž…๋ฌธ)๋กœ + ์ž๋™ ์ž„ํฌํŠธ๋จ โ€” SimpleData ์†์„ฑ(์—ฐ์žฅ(m), ๊ตฌ๋ถ„ ๋“ฑ)๋„ ์ด์ œ ์ธ์‹ +- ์„ ํ˜•์ค‘์‹ฌ์„  LineString ์€ ๋ฏธ์‚ฌ์šฉ (์ธก์  ํด๋ฆฌ๋ผ์ธ์œผ๋กœ ์ค‘์‹ฌ์„  ์ƒ์„ฑ โ€” ๊ธฐ์กด ๋กœ์ง) diff --git a/docs/history/2026-07-09_1715_์ง€์žฅ๋ฌผKML-์‹ ํฌ๋งท-ํŒŒ์‹ฑ.md b/docs/history/2026-07-09_1715_์ง€์žฅ๋ฌผKML-์‹ ํฌ๋งท-ํŒŒ์‹ฑ.md new file mode 100644 index 0000000..7a32835 --- /dev/null +++ b/docs/history/2026-07-09_1715_์ง€์žฅ๋ฌผKML-์‹ ํฌ๋งท-ํŒŒ์‹ฑ.md @@ -0,0 +1,51 @@ +# 2026-07-09 ์ง€์žฅ๋ฌผ KML ์‹ ํฌ๋งท ํŒŒ์‹ฑ (๋‹ค์ค‘ KML + ๋ผ๋ฒจ ์ƒ‰์ƒ) + +**์†Œ์š” ์‹œ๊ฐ„**: 13๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 300k / output 8k tokens + +## ๋ฌธ์ œ + +์ƒˆ๋กœ ๋ฐ›์€ `์ œ์ฃผ_์ค‘์‚ฐ๊ฐ„๋„๋กœ_์ง€์žฅ๋ฌผ.kml`(๊ตฌ๊ธ€์–ด์Šค Pro์—์„œ CSV ๋ณ€ํ™˜, ์ง€์žฅ๋ฌผ 33๊ฐœ)์˜ +์ขŒํ‘œ/๋ผ๋ฒจ ์ƒ‰์ƒ์ด ํ™”๋ฉด์— ํ‘œ์‹œ๋˜์ง€ ์•Š์Œ. + +## ์›์ธ + +1. **parseKmz ๊ฐ€ KML ์„ 1๊ฐœ๋งŒ ์ฝ์Œ** โ€” `files.find(...)` ๋ผ ํด๋”์— KML ์ด 2๊ฐœ(์ธก์  KML + ์ง€์žฅ๋ฌผ KML)๋ฉด + ์ฒซ ํŒŒ์ผ๋งŒ ํŒŒ์‹ฑ๋˜๊ณ  ์ง€์žฅ๋ฌผ KML ์€ ํ†ต์งธ๋กœ ๋ฌด์‹œ๋จ (ํ•ต์‹ฌ ์›์ธ) +2. ์‹ ํฌ๋งท์€ ํด๋”(๊ต๋Ÿ‰/ํ„ฐ๋„ ๋“ฑ) ๊ตฌ๋ถ„์ด ์—†๊ณ  Placemark ์ง์† + Schema + (`ํƒ€์ž…/ํƒ€์ž…์ƒ์„ธ/ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ/์ด๋ฆ„/์—ฐ์žฅ/X/Y/๋†’์ด`) โ€” ๊ธฐ์กด desc HTML ํ‘œ ๊ธฐ๋ฐ˜ ๋ถ„๊ธฐ์™€ ๋ถˆ์ผ์น˜ +3. ๋ผ๋ฒจ ์ƒ‰์ƒ ๊ฐœ๋…์ด ๋ Œ๋”๋Ÿฌ์— ์—†์Œ โ€” ์บ”๋ฒ„์Šค ๋ผ๋ฒจ์ด ๊ณ ์ •์ƒ‰(#64c8ff) ๋‹จ์ผ + +## ์ˆ˜์ • ๋‚ด์šฉ + +### client/src/utils/geoData.ts +- **parseKmz ๋‹ค์ค‘ ํŒŒ์ผ ๋ณ‘ํ•ฉ**: ํด๋” ๋‚ด ๋ชจ๋“  .kml/.kmz ๋ฅผ ์ˆœํšŒ ํŒŒ์‹ฑํ•ด pois/structures/stations ๋ณ‘ํ•ฉ + (ํŒŒ์ผ ๋‹จ์œ„ ์‹คํŒจ๋Š” ๊ฒฝ๊ณ  ํ›„ ๊ฑด๋„ˆ๋œ€) +- **์‹ ํฌ๋งท(์ง€์žฅ๋ฌผ ์Šคํ‚ค๋งˆ) ๋ธŒ๋žœ์น˜**: `ํƒ€์ž…` SimpleData ์กด์žฌ๋กœ ๊ฐ์ง€ โ€” + - title=`์ด๋ฆ„`(ํด๋ฐฑ name), category=`ํƒ€์ž…`(๊ต์ฐจ๋กœ/๋„๋กœ/๊ต๋Ÿ‰/๊ฐ•.ํ•˜์ฒœ/๋งˆ์„/์‚ฐ/์ง€์žฅ๋ฌผ/์ง€์žฅ๋ฌผL) + - ์ขŒํ‘œ๋Š” SimpleData X/Y(์ €์ •๋ฐ€)๊ฐ€ ์•„๋‹Œ **Point coordinates(๊ณ ์ •๋ฐ€)** ์‚ฌ์šฉ + - `๋†’์ด`๋Š” ํ‘œ๊ณ ๊ฐ€ ์•„๋‹Œ ๊ตฌ์กฐ๋ฌผ ๋†’์ด โ†’ z=0 ์œ ์ง€(ํŒ์—… props ๋กœ๋งŒ ๋…ธ์ถœ) + - ํƒ€์ž… ๊ต๋Ÿ‰/ํ„ฐ๋„์€ ๊ตฌ์กฐ๋ฌผ(์Šคํ…Œ์ด์…˜๋ฐ”ยท๋…ธ์„ ํŒจ๋„ ๋งˆํฌ)๋กœ๋„ ์ค‘๋ณต ๋“ฑ๋ก (์—ฐ์žฅโ†’lengthM) +- **๋ผ๋ฒจ ์ƒ‰**: `ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ` ํ•œ๊ตญ์–ด๋ช…(๊ฒ€์ •/๋‚จ์ƒ‰/๋นจ๊ฐ•/์—ฐ๋‘/์ฃผํ™ฉ/์ดˆ๋ก/ํŒŒ๋ž‘ ๋“ฑ 13์ƒ‰) โ†’ CSS ๋งคํ•‘, + ์—†์œผ๋ฉด ์ธ๋ผ์ธ IconStyle ์ƒ‰(KML aabbggrr โ†’ #rrggbb) ํด๋ฐฑ โ†’ `GeoPoint.labelColor` + +### client/src/types/geo.ts +- `GeoPoint.labelColor?: string` ์ถ”๊ฐ€ + +### client/src/components/overlay/StationOverlay.tsx +- labelColor ๋ฅผ poiCand โ†’ poiMarkers(๋ผ๋ฒจ ์บ์‹œ) โ†’ ์บ”๋ฒ„์Šค ๋ Œ๋”๋กœ ์ „๋‹ฌ +- ์‹ญ์ž ๋งˆ์ปคยท๋ผ๋ฒจ ํ…์ŠคํŠธ๋ฅผ ์ง€์ • ์ƒ‰์œผ๋กœ ๋ Œ๋”, ์–ด๋‘์šด ์ƒ‰(๊ฒ€์ •ยท๋‚จ์ƒ‰)์€ ์™ธ๊ณฝ์„ ์„ ๋ฐ๊ฒŒ ๋ฐ˜์ „(๊ฐ€๋…์„ฑ) +- CATEGORY_EMOJI ํ™•์žฅ: ๊ต์ฐจ๋กœ๐Ÿšฆ ๋„๋กœ๐Ÿ›ฃ๏ธ ๊ฐ•.ํ•˜์ฒœ๐ŸŒŠ ๋งˆ์„๐Ÿ˜๏ธ ์‚ฐโ›ฐ๏ธ ์ง€์žฅ๋ฌผL๐Ÿข + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-CLmojEn3.js) ์„œ๋น™ +- ์ง€์žฅ๋ฌผ KML ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: 33๊ฐœ ์ „๋ถ€ ํŒŒ์‹ฑ, ์ƒ‰์ƒ ๋งคํ•‘ 33/33, ๊ต๋Ÿ‰ 3๊ฑด ๊ตฌ์กฐ๋ฌผ ์ค‘๋ณต ๋“ฑ๋ก, + ์ขŒํ‘œ ๋ถˆ๋Ÿ‰ 0 (Point coordinates ๊ณ ์ •๋ฐ€ ์‚ฌ์šฉ) +- ๊ธฐ์กด ์ธก์  KML(STA 71๊ฐœ)๊ณผ ๋ณ‘ํ•ฉ ๋กœ๋“œ ํ™•์ธ (๋‹ค์ค‘ KML) + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์‹ ํฌ๋งท ๊ฐ์ง€ ํ‚ค๋Š” `ํƒ€์ž…` SimpleData โ€” ํšŒ๋• descํ‘œ/STA ์Šคํ‚ค๋งˆ์™€ ์ถฉ๋Œ ์—†์Œ +- styleUrl(#pointStyleMap1) โ†’ StyleMap ์ฐธ์กฐ ํ•ด์„์€ ๋ฏธ๊ตฌํ˜„ (์ธ๋ผ์ธ Style ์ด ํ•ญ์ƒ ์กด์žฌํ•ด ๋ถˆํ•„์š”) +- ์ƒ‰์ƒ๋ช… ๋ฏธ์ •์˜ ์‹œ(์˜ˆ: ์ƒˆ๋กœ์šด ์ƒ‰ ์ด๋ฆ„) ์ธ๋ผ์ธ IconStyle ์ƒ‰ ํด๋ฐฑ โ†’ ๊ทธ๊ฒƒ๋„ ์—†์œผ๋ฉด ๊ธฐ๋ณธ ํ•˜๋Š˜์ƒ‰ diff --git a/docs/history/2026-07-09_1725_๋ผ๋ฒจ-ํ‘œ๊ณ ์•ต์ปค-์ธก์ ๊ฑฐ๋ฆฌ์ œํ•œ.md b/docs/history/2026-07-09_1725_๋ผ๋ฒจ-ํ‘œ๊ณ ์•ต์ปค-์ธก์ ๊ฑฐ๋ฆฌ์ œํ•œ.md new file mode 100644 index 0000000..0335b12 --- /dev/null +++ b/docs/history/2026-07-09_1725_๋ผ๋ฒจ-ํ‘œ๊ณ ์•ต์ปค-์ธก์ ๊ฑฐ๋ฆฌ์ œํ•œ.md @@ -0,0 +1,52 @@ +# 2026-07-09 ์˜ค๋ฒ„๋ ˆ์ด ๋ผ๋ฒจ ํ‘œ๊ณ  ์•ต์ปค + ์ธก์  ๊ฑฐ๋ฆฌ ์ œํ•œ + +**์†Œ์š” ์‹œ๊ฐ„**: 10๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 340k / output 6k tokens + +## ๋ฌธ์ œ (์Šคํฌ๋ฆฐ์ƒท ์ œ๋ณด) + +1. ๋นจ๊ฐ„ ์ธก์  ๋ผ๋ฒจ(0k000~6k998)์ด ์ˆ˜ํ‰์„  ๋ถ€๊ทผ์— ์ „๋ถ€ ๊ฒน์ณ์„œ ๊นจ์ง„ ๊ธ€์ž์ฒ˜๋Ÿผ ๋ณด์ž„ +2. POI ๋ผ๋ฒจ์ด ์‹ค์ œ ์ง€๋ฌผ๋ณด๋‹ค ๋†’๊ฒŒ ๋– ์„œ, ์นด๋ฉ”๋ผ(๋“œ๋ก )๊ฐ€ ์ „์ง„ํ•˜๋ฉด ๋ผ๋ฒจ์ด ๊ณ„์† ๋’ค๋กœ ๋ฐ€๋ฆผ(์‹œ์ฐจ ๋ถˆ์ผ์น˜) + +## ์›์ธ + +1. **์ธก์  ๋ผ๋ฒจ์—” ๊ฑฐ๋ฆฌ ์ œํ•œ์ด ์—†์Œ** โ€” POI๋Š” maxPoiRange(๊ธฐ๋ณธ 1000m) ํ•„ํ„ฐ๊ฐ€ ์žˆ์ง€๋งŒ ์ธก์ ์€ ์—†์–ด์„œ + 7km ์•ž ์ธก์ ๊นŒ์ง€ ์ „๋ถ€ FOV ์•ˆ ์ˆ˜ํ‰์„  ๊ทผ์ฒ˜์— ํˆฌ์˜ยท์ค‘์ฒฉ๋จ +2. **๋ผ๋ฒจ ํ‘œ๊ณ  ๊ทผ์‚ฌ๊ฐ€ ํ‹€๋ฆผ** โ€” ์ง์ „ ์ž‘์—…์—์„œ ์ธก์  z๋ฅผ "๋“œ๋ก ๊ณ ๋„โˆ’24m โ‰ˆ 124m"๋กœ ๊ทผ์‚ฌํ–ˆ์œผ๋‚˜ + ์‹ค์ œ ์ง€๋ฉด์€ โ‰ˆ60m(๊ฒฝ๋กœํ‘œ๊ณ  ๊ธฐ๋ณธ๊ฐ’ 58์ด ๋„๋กœ์— ๋ถ™๋Š” ๊ฒƒ์œผ๋กœ ํ™•์ธ) โ†’ ๋ผ๋ฒจ์ด ์ง€๋ฉด๋ณด๋‹ค + ์•ฝ 60m ๊ณต์ค‘์— ๋– ์„œ ์›๊ทผ ์‹œ์ฐจ๊ฐ€ ์•ˆ ๋งž์•„ ์ „์ง„ ์‹œ ๋ผ๋ฒจ์ด ๋’ค๋กœ ํ๋ฆ„. + ๋“œ๋ก ๊ณ ๋„ ๋ชจ๋“œ(pz=๋“œ๋ก ๊ณ ๋„โˆ’์ด๊ฒฉ24)๋„ ๋™์ผํ•˜๊ฒŒ 124m ํ‰๋ฉด์ด๋ผ ๊ฐ™์€ ์ฆ์ƒ + +## ์ˆ˜์ • ๋‚ด์šฉ + +### client/src/utils/geoData.ts +- KML ์ธก์ ์˜ z ๊ทผ์‚ฌ(๋“œ๋ก ๊ณ ๋„โˆ’24) ์ œ๊ฑฐ โ†’ z=0(ํ‘œ๊ณ  ๋ฏธ์ƒ ํ‘œ์‹) ์œ ์ง€ + +### client/src/components/overlay/StationOverlay.tsx +- **ํ‘œ๊ณ  ๋ฏธ์ƒ(zโ‰ค0) ๋ผ๋ฒจ์„ '๊ฒฝ๋กœ ํ‘œ๊ณ '(dronePathZ) ํ‰๋ฉด์— ์•ต์ปค**: + - ์ธก์  ๋ผ๋ฒจ sz: ์œ ํšจ z ์—†์œผ๋ฉด PATH_Z(๊ฒฝ๋กœํ‘œ๊ณ ) ์‚ฌ์šฉ + - POI gz ํด๋ฐฑ ์ฒด์ธ: ๋ณด์ •๊ฐ’ โ†’ ์ค‘์‹ฌ์„  z(>0๋งŒ) โ†’ POI z(>0๋งŒ) โ†’ PATH_Z + - ๊ฒฝ๋กœํ‘œ๊ณ  ์Šฌ๋ผ์ด๋” ๋ณ€๊ฒฝ ์‹œ ๋ผ๋ฒจ ์บ์‹œ ์žฌ๊ณ„์‚ฐ(500ms ๋””๋ฐ”์šด์Šค, ๊ธฐ์กด ํŠธ๋ฆฌ๊ฑฐ์— dronePathZ ์ถ”๊ฐ€) + - โ†’ **๊ถค์ ์„ ๋„๋กœ์— ๋งž์ถ”๋Š” ์Šฌ๋ผ์ด๋” ํ•˜๋‚˜๋กœ ์ธก์ /POI ๋ผ๋ฒจ ๋†’์ด๊ฐ€ ํ•จ๊ป˜ ๋ณด์ •๋จ** +- **์ธก์  ๋ผ๋ฒจ์—๋„ maxPoiRange ๊ฑฐ๋ฆฌ ์ œํ•œ ์ ์šฉ** โ€” ์›๊ฑฐ๋ฆฌ ์ธก์  ์ˆ˜ํ‰์„  ๋ญ‰์นจ ํ•ด์†Œ +- **poiDroneHeight ๊ธฐ๋ณธ๊ฐ’ trueโ†’false(์ง€๋ฉด๊ณ ๋„ ๊ธฐ์ค€)**: + - ์‹ค์ธก z ๋ฐ์ดํ„ฐ์…‹(ํšŒ๋•)์€ ์ง€๋ฌผ์— ์ •ํ™•ํžˆ ๋ถ™์Œ(์›๋ž˜ ์ด ๋ชจ๋“œ๊ฐ€ ์ •ํ™•) + - ํ‘œ๊ณ  ๋ฏธ์ƒ ๋ฐ์ดํ„ฐ์…‹(์ œ์ฃผ)์€ ๊ฒฝ๋กœํ‘œ๊ณ  ์•ต์ปค๋กœ ์ผ๊ด„ ๋ณด์ • ๊ฐ€๋Šฅ + - ๋“œ๋ก ๊ณ ๋„ ๋ชจ๋“œ๋Š” UI ํ† ๊ธ€๋กœ ์—ฌ์ „ํžˆ ์„ ํƒ ๊ฐ€๋Šฅ + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-Nv1HLuS3.js) ์„œ๋น™ +- ๊ธฐํ•˜ ๊ฒ€์ฆ(๊ณ„์‚ฐ): ์ง€๋ฉด 60m/๋“œ๋ก  148m ๊ธฐ์ค€, 124m ๋ผ๋ฒจ์€ 300m ์ „๋ฐฉ ์ง€๋ฌผ ๋Œ€๋น„ ํ™”๋ฉด์ƒ ์•ฝ + 12% ์œ„์ชฝ์— ํˆฌ์˜(์ˆ˜ํ‰์„  ๋ฐฉํ–ฅ) โ†’ ์ „์ง„ ์‹œ ๋ฒŒ์–ด์ง. PATH_Z(58) ์•ต์ปค ์‹œ ์ง€๋ฌผ๊ณผ ๋™์ผ ํ‰๋ฉด โ†’ ์‹œ์ฐจ ์ผ์น˜ + +## ์‚ฌ์šฉ๋ฒ• (์‚ฌ์šฉ์ž ์•ˆ๋‚ด) + +- ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ํด๋” ์žฌ์„ ํƒ โ†’ '๊ฒฝ๋กœ ํ‘œ๊ณ ' ์Šฌ๋ผ์ด๋”๋กœ ํŒŒ๋ž€ ๊ถค์ ์„ ๋„๋กœ ๋…ธ๋ฉด์— ๋งž์ถ”๋ฉด + ์ธก์ ยทPOI ๋ผ๋ฒจ๋„ ๊ฐ™์€ ํ‰๋ฉด์œผ๋กœ ๋”ฐ๋ผ์˜ด (๊ธฐ๋ณธ 58m๊ฐ€ ๋Œ€๋žต ๋งž์Œ) +- ์ธก์  ๋ผ๋ฒจ ํ‘œ์‹œ ๊ฑฐ๋ฆฌ๋Š” 'POI ํ‘œ์‹œ๋ฒ”์œ„'(maxPoiRange) ์Šฌ๋ผ์ด๋”๋กœ ์กฐ์ ˆ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์ง€ํ˜• ๊ธฐ๋ณต์ด ํฐ ๊ตฌ๊ฐ„์€ ๋‹จ์ผ ํ‰๋ฉด ์•ต์ปค์˜ ํ•œ๊ณ„ ์žˆ์Œ โ€” ํ•„์š” ์‹œ ๊ตฌ๊ฐ„๋ณ„ z ๋˜๋Š” DEM ๋„์ž… ๊ฒ€ํ†  +- RoutePanel ๊ฐ€์‹œ๋ฒ”์œ„(์ดˆ๋ก ๋ฐ•์Šค) ๊ณ„์‚ฐ์€ ์—ฌ์ „ํžˆ pt.z(0) ์‚ฌ์šฉ โ€” ๋ถ€์ •ํ™•ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ๋ถ€์ฐจ์  diff --git a/docs/history/2026-07-09_1738_์˜์ƒ-์žฌ์ƒ๋ชฉ๋ก-ํŒจ๋„.md b/docs/history/2026-07-09_1738_์˜์ƒ-์žฌ์ƒ๋ชฉ๋ก-ํŒจ๋„.md new file mode 100644 index 0000000..9d771c3 --- /dev/null +++ b/docs/history/2026-07-09_1738_์˜์ƒ-์žฌ์ƒ๋ชฉ๋ก-ํŒจ๋„.md @@ -0,0 +1,31 @@ +# 2026-07-09 ์˜์ƒ ์žฌ์ƒ๋ชฉ๋ก ํŒจ๋„ (๋ถ„ํ•  ์˜์ƒ ์„ ํƒ ์žฌ์ƒ) + +**์†Œ์š” ์‹œ๊ฐ„**: 10๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 380k / output 5k tokens + +## ์ž‘์—… ๋‚ด์šฉ + +ํด๋” ๋กœ๋“œ ์‹œ ํด๋” ๋‚ด ์˜์ƒ ํŒŒ์ผ ๋ชฉ๋ก(์ด๋ฆ„ ์˜ค๋ฆ„์ฐจ์ˆœ)์„ ํŒจ๋„๋กœ ํ‘œ์‹œํ•˜๊ณ , +๋ชฉ๋ก์—์„œ ์›ํ•˜๋Š” ์˜์ƒ์„ ํด๋ฆญํ•ด ๋ฐ”๋กœ ์„ ํƒ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•จ. + +## ์ˆ˜์ • ๋‚ด์šฉ โ€” client/src/components/player/VideoPlayer.tsx + +- **์žฌ์ƒ๋ชฉ๋ก ํŒจ๋„** (์šฐํ•˜๋‹จ, ์žฌ์ƒ๋ฐ” ์œ„): + - ํด๋” ๋‚ด ์˜์ƒ ํŒŒ์ผ๋ช… + ๊ธธ์ด(m:ss), ํ˜„์žฌ ์žฌ์ƒ ํ•ญ๋ชฉ โ–ถ + ์ฃผํ™ฉ ํ•˜์ด๋ผ์ดํŠธ + - ํ•ญ๋ชฉ ํด๋ฆญ โ†’ ํ•ด๋‹น ์„ธ๊ทธ๋จผํŠธ๋กœ ์ฆ‰์‹œ ์ „ํ™˜ ์žฌ์ƒ (geoStore.loadSegment ๋กœ ๋“œ๋ก  ๋กœ๊ทธ๋„ + ํ•ด๋‹น ๊ตฌ๊ฐ„ ๋ˆ„์  ์˜คํ”„์…‹์œผ๋กœ ์žฌ์ •๋ ฌ โ€” ended ์ž๋™ ์ „ํ™˜๊ณผ ๋™์ผ ๊ฒฝ๋กœ) + - ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ํ•ญ๋ชฉ ์žฌํด๋ฆญ โ†’ ์ฒ˜์Œ(0์ดˆ)๋ถ€ํ„ฐ ์žฌ์ƒ + - ๋ชฉ๋ก์ด ๊ธธ๋ฉด ์Šคํฌ๋กค (max-h-48) + - ํ—ค๋”์— "N/M" ์ง„ํ–‰ ํ‘œ์‹œ. ended ์ž๋™ ์ „ํ™˜ ์‹œ์—๋„ videoIndex ๊ตฌ๋…์œผ๋กœ ํ•˜์ด๋ผ์ดํŠธ ์ž๋™ ๊ฐฑ์‹  +- **'์˜์ƒ๋ชฉ๋ก N/M' ํ† ๊ธ€ ๋ฒ„ํŠผ**: ํ•˜๋‹จ ์ปจํŠธ๋กค ํ–‰(๋“œ๋ก ๊ถค์  ๋ฒ„ํŠผ ์˜†). ๊ธฐ๋ณธ ํ‘œ์‹œ(ON) +- `fmtDur` ํ—ฌํผ(์ดˆโ†’"m:ss"), geoStore videoFiles/videoDurations/videoIndex ๊ตฌ๋… ์ถ”๊ฐ€ +- ๋กœ์ปฌ(ํด๋”) ์žฌ์ƒ์ผ ๋•Œ๋งŒ ํ‘œ์‹œ (source.kind === 'local') + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-DLUIvK3Q.js) ์„œ๋น™ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ ์‹œ ์•ฝ 1์ดˆ ๋กœ๋”ฉ ๊ณต๋ฐฑ(objectURL ๊ต์ฒด) โ€” ํ•„์š” ์‹œ ๋‹ค์Œ ์„ธ๊ทธ๋จผํŠธ ํ”„๋ฆฌ๋กœ๋“œ ๊ฒ€ํ†  +- ํŒจ๋„ ์œ„์น˜๋Š” ์šฐํ•˜๋‹จ ๊ณ ์ • โ€” ์บก์ฒ˜ ๋ชฉ๋ก ๋“ฑ ๋‹ค๋ฅธ ์šฐ์ธก UI ์™€ ๊ฒน์น˜๋ฉด ์œ„์น˜ ์กฐ์ • ํ•„์š” diff --git a/docs/history/2026-07-09_1754_์นด๋ฉ”๋ผ๊ฐ์ง€-๋ผ๋ฒจ๋ฐ•์Šค-์˜์ƒ์ฝค๋ณด.md b/docs/history/2026-07-09_1754_์นด๋ฉ”๋ผ๊ฐ์ง€-๋ผ๋ฒจ๋ฐ•์Šค-์˜์ƒ์ฝค๋ณด.md new file mode 100644 index 0000000..19b8891 --- /dev/null +++ b/docs/history/2026-07-09_1754_์นด๋ฉ”๋ผ๊ฐ์ง€-๋ผ๋ฒจ๋ฐ•์Šค-์˜์ƒ์ฝค๋ณด.md @@ -0,0 +1,50 @@ +# 2026-07-09 ๋“œ๋ก  ์นด๋ฉ”๋ผ ์ž๋™ ๊ฐ์ง€ + ๋ผ๋ฒจ ๋ฐ•์Šค ์Šคํƒ€์ผ + ์˜์ƒ๋ชฉ๋ก ์ฝค๋ณด + +**์†Œ์š” ์‹œ๊ฐ„**: 16๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 430k / output 12k tokens + +## ์ž‘์—… ๋‚ด์šฉ (์š”์ฒญ 4๊ฑด) + +### 1. ๋“œ๋ก  ์นด๋ฉ”๋ผ ์ •๋ณด ์ž๋™ ๊ฐ์ง€ โ†’ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ ์šฉ (POI ๋งคํ•‘ ์–ด๊ธ‹๋‚จ์˜ ํ•ต์‹ฌ ์›์ธ) + +DJI MP4์˜ `djmd` ๋ฐ์ดํ„ฐ ํŠธ๋ž™(ํ”„๋ ˆ์ž„๋ณ„ protobuf ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ)์„ ๋ถ„์„ํ•ด ํ•„๋“œ ๊ตฌ์กฐ ํ•ด๋…: + +| ํ•„๋“œ | ๋‚ด์šฉ | ์ œ์ฃผ ๊ฐ’ | +|------|------|--------| +| 1.1.2 | ์นด๋ฉ”๋ผ ๋ชจ๋ธ | **DJI ZenmuseP1** (ํ’€ํ”„๋ ˆ์ž„ ์ธก๋Ÿ‰ ์นด๋ฉ”๋ผ) | +| 1.7 / 1.6 / 1.8 | ํ•ด์ƒ๋„ / fps | 3840ร—2160 / 59.94 | +| 6.4 / 6.5 / 6.6 | ์œ„๊ฒฝ๋„(๋ผ๋””์•ˆ) / ํƒ€์›์ฒด๊ณ (mm) | 33.40ยฐ/126.29ยฐ/174.4m | +| **8.6** | **์ดˆ์ ๊ฑฐ๋ฆฌ (0.1mm, 35mm ํ™˜์‚ฐ)** | **299 โ†’ 29.9mm** | + +- ๊ธฐ์กด ๊ธฐ๋ณธ๊ฐ’ 24mm vs ์‹ค์ œ 29.9mm(DL 24mm ๋ Œ์ฆˆ ร— 4K ๋น„๋””์˜ค ํฌ๋กญ โ‰ˆ1.25) โ†’ FOV ๊ณผ๋Œ€ โ†’ POI ์–ด๊ธ‹๋‚จ +- `client/src/utils/cameraMeta.ts` ์‹ ๊ทœ: ๋ธŒ๋ผ์šฐ์ €์—์„œ ์˜์ƒ File ์•ž 8MB๋ฅผ ์ฝ์–ด djmd ์ฒซ ์ƒ˜ํ”Œ ํŒŒ์‹ฑ + (๋ชจ๋ธ ๋ฌธ์ž์—ด ํƒ์ƒ‰ โ†’ ์ƒ˜ํ”Œ ๊ฒฝ๊ณ„ ์—ญ์ถ”์  โ†’ protobuf ํ•„๋“œ ์ˆ˜์ง‘). ์‹คํŒŒ์ผ ๊ฒ€์ฆ ์™„๋ฃŒ +- `loadFolderGeoData`: ๊ฐ์ง€ ์ •๋ณด๋ฅผ FolderGeoData/geoStore(cameraInfo)์— ์ ์žฌ, ๋“œ๋ก  ํ”„๋ ˆ์ž„ focalLen์—๋„ ๋ฐ˜์˜ +- StationOverlay: **์ €์žฅ๋œ ๋ณด์ •(localStorage calib)์ด ์—†๋Š” ๋ฐ์ดํ„ฐ์…‹์€ ๊ฐ์ง€ focal ์ž๋™ ์ ์šฉ**, + ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„์— "๐Ÿ“ท ๋ชจ๋ธ ยท ์ดˆ์  ยท fps" ํ‘œ์‹œ + **[์ ์šฉ] ๋ฒ„ํŠผ**(์ˆ˜๋™ ์ ์šฉ โ€” ์ด๋ฏธ ํŠœ๋‹ํ•œ + ๋ฐ์ดํ„ฐ์…‹์€ ์ž๋™ ์ ์šฉ์„ ๊ฑด๋„ˆ๋›ฐ๋ฏ€๋กœ ์ด ๋ฒ„ํŠผ ์‚ฌ์šฉ) + +### 2. '๋“œ๋ก  ๋†’์ด์™€ POI ์ด๊ฒฉ๊ฑฐ๋ฆฌ' ์Šฌ๋ผ์ด๋” ์ตœ๋Œ€๊ฐ’ 120 โ†’ 300 + +### 3. ์˜์ƒ๋ชฉ๋ก โ†’ ์ขŒ์ƒ๋‹จ ํƒ€์ดํ‹€ ์•„๋ž˜ ์ฝค๋ณด ๋“œ๋กญ๋‹ค์šด + +- ์šฐํ•˜๋‹จ ํŒจ๋„/ํ•˜๋‹จ ๋ฒ„ํŠผ ์ œ๊ฑฐ โ†’ ๋…ธ์„  ๋ฐฐ๋„ˆ(ํƒ€์ดํ‹€) ์•„๋ž˜ `left:8, top:paramTop` ์ฝค๋ณด ๋ฒ„ํŠผ + (ํ˜„์žฌ ํŒŒ์ผ๋ช… + n/m + โ–ผ). ํด๋ฆญ ์‹œ ๋ชฉ๋ก์ด ์•„๋ž˜๋กœ ํŽผ์ณ์ง€๊ณ  ์„ ํƒ ์‹œ ์ฆ‰์‹œ ์ „ํ™˜ยท์ ‘ํž˜ +- RoutePanel์ด paramTop+40๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด ๊ฒน์นจ ์—†์Œ + +### 4. POI ๋ผ๋ฒจ: ์‹ญ์ž/์•„์ด์ฝ˜ ์ œ๊ฑฐ, ์ง€์ •์ƒ‰ ๋ฐฐ๊ฒฝ + ํฐ ๊ธ€์ž + +- ์บ”๋ฒ„์Šค ๋ Œ๋”๋ฅผ "ํ…์ŠคํŠธ ๋ฐ•์Šค"๋กœ ๋ณ€๊ฒฝ: ๋ฐฐ๊ฒฝ = KML ํ…์ŠคํŠธ๋ฐ•์Šค_์ƒ‰์ƒ(๋ผ์šด๋“œ ์‚ฌ๊ฐํ˜•), ๊ธ€์ž = ํฐ์ƒ‰ +- POI ์ง€์ ์— ๊ฐ€๋กœ ์ค‘์•™ ์ •๋ ฌ, ํžˆํŠธ๋ฐ•์Šค๋„ ๋ฐ•์Šค ์˜์—ญ์œผ๋กœ ๊ฐฑ์‹  + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-Bn6Gs0g2.js) ์„œ๋น™ +- ์นด๋ฉ”๋ผ ๊ฐ์ง€ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‹คํŒŒ์ผ ์•ž 8MB๋กœ ์žฌํ˜„ ๊ฒ€์ฆ: ZenmuseP1 / 29.9mm / 3840ร—2160@59.94 ์ •ํ™• ์ถ”์ถœ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- **์ด๋ฏธ ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜์ด ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ์…‹(์ด๋ฒˆ ์„ธ์…˜์—์„œ ์Šฌ๋ผ์ด๋” ๋งŒ์ง„ ๊ฒฝ์šฐ)์€ focal ์ž๋™ ์ ์šฉ์ด + ๊ฑด๋„ˆ๋›ฐ์–ด์ง** โ†’ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„์˜ [์ ์šฉ] ๋ฒ„ํŠผ์œผ๋กœ 29.9mm ๋ฐ˜์˜ ํ•„์š” +- djmd 6.x(์œ„๊ฒฝ๋„ ๋ผ๋””์•ˆ/ํƒ€์›์ฒด๊ณ )๋Š” CSV๋ณด๋‹ค ์ •๋ฐ€ํ•œ ํฌ์ฆˆ ์†Œ์Šค โ€” ํ–ฅํ›„ CSV ๋Œ€์ฒด ํ›„๋ณด +- 8.8(๋ณ€ํ™” ํ•„๋“œ)์€ ํฌ์ปค์Šค ๊ฑฐ๋ฆฌ๋กœ ์ถ”์ • โ€” ๋ฏธ์‚ฌ์šฉ diff --git a/docs/history/2026-07-09_1758_์นด๋ฉ”๋ผfocal-์ž๋™์ ์šฉ-๊ฐœ์„ .md b/docs/history/2026-07-09_1758_์นด๋ฉ”๋ผfocal-์ž๋™์ ์šฉ-๊ฐœ์„ .md new file mode 100644 index 0000000..0c193b5 --- /dev/null +++ b/docs/history/2026-07-09_1758_์นด๋ฉ”๋ผfocal-์ž๋™์ ์šฉ-๊ฐœ์„ .md @@ -0,0 +1,30 @@ +# 2026-07-09 ์นด๋ฉ”๋ผ focal ์ž๋™ ์ ์šฉ ์กฐ๊ฑด ๊ฐœ์„  + +**์†Œ์š” ์‹œ๊ฐ„**: 4๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 460k / output 3k tokens + +## ๋ฌธ์ œ + +"๋“œ๋ก  ์นด๋ฉ”๋ผ ์†์„ฑ์ด ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ ์šฉ๋œ ๊ฑด๊ฐ€?" ํ™•์ธ ์š”์ฒญ. +์ง์ „ ๊ตฌํ˜„์€ "์ €์žฅ๋œ ๋ณด์ •(localStorage calib)์ด ์žˆ์œผ๋ฉด ์ž๋™ ์ ์šฉ ๊ฑด๋„ˆ๋œ€"์ด์—ˆ๋Š”๋ฐ, +**๋ณด์ • ์ž๋™์ €์žฅ์€ ํด๋”๋ฅผ ์—ด๊ธฐ๋งŒ ํ•ด๋„ ๊ธฐ๋ณธ params ๋กœ ์ €์žฅ**๋˜๋ฏ€๋กœ(800ms ๋””๋ฐ”์šด์Šค ์ €์žฅ effect๊ฐ€ +์ฒซ ๋กœ๋“œ์—๋„ ์‹คํ–‰) ์ œ์ฃผ ๋ฐ์ดํ„ฐ์…‹์€ ์ด๋ฏธ calib ์ด ์กด์žฌ โ†’ ๊ฐ์ง€ focal(29.9mm)์ด ์ ์šฉ๋˜์ง€ ์•Š์•˜์„ +๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•˜์Œ. + +## ์ˆ˜์ • โ€” StationOverlay.tsx ์ž๋™ ์ ์šฉ ์กฐ๊ฑด + +- ๊ธฐ์กด: ์ €์žฅ๋œ calib ์กด์žฌ โ†’ ๋ฌด์กฐ๊ฑด ๊ฑด๋„ˆ๋œ€ +- ๋ณ€๊ฒฝ: **์‚ฌ์šฉ์ž๊ฐ€ f ๋ฅผ ์‹ค์ œ๋กœ ํŠœ๋‹ํ•œ ๊ฒฝ์šฐ(์ €์žฅ๋œ f โ‰  ๊ธฐ๋ณธ๊ฐ’ 24)๋งŒ ์œ ์ง€**, ๊ทธ ์™ธ + (์ฒซ ๋กœ๋“œ / ์ €์žฅ์€ ๋์ง€๋งŒ f ๊ธฐ๋ณธ๊ฐ’ ๊ทธ๋Œ€๋กœ)๋Š” ๊ฐ์ง€๊ฐ’ ์ž๋™ ์ ์šฉ +- setParams ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ ์•ˆ์—์„œ ํŒ๋‹จ โ†’ calib ๋ณต์›๊ฐ’ ๊ธฐ์ค€์œผ๋กœ ์ •ํ™•ํžˆ ๋น„๊ต + +## ํ™•์ธ ๋ฐฉ๋ฒ• (์‚ฌ์šฉ์ž ์•ˆ๋‚ด) + +- ์ƒˆ๋กœ๊ณ ์นจ โ†’ ํด๋” ์žฌ์„ ํƒ โ†’ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„: **f = 29.9mm** ์ด๊ณ  + ๐Ÿ“ท ์ค„์— "DJI ZenmuseP1 ยท 29.9mm" ํ‘œ์‹œ๋˜๋ฉด ์ ์šฉ๋œ ๊ฒƒ +- ์ฝ˜์†” ๋กœ๊ทธ: `[camera] DJI ZenmuseP1 ๊ฐ์ง€ โ†’ focal 29.9mm ์ž๋™ ์ ์šฉ` +- f ๋ฅผ ์ด๋ฏธ ์ง์ ‘ ํŠœ๋‹ํ–ˆ๋˜ ๊ฒฝ์šฐ์—” ์œ ์ง€๋˜๋ฉฐ, [์ ์šฉ] ๋ฒ„ํŠผ์œผ๋กœ ๊ฐ์ง€๊ฐ’ ๊ต์ฒด ๊ฐ€๋Šฅ + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-Ba4WSfaE.js) ์„œ๋น™ diff --git a/docs/history/2026-07-09_1802_์Šคํ…Œ์ด์…˜๋ฐ”-๋…ธ์„ ๋ฐ–-์ง„ํ–‰์ •์ง€-์ˆ˜์ •.md b/docs/history/2026-07-09_1802_์Šคํ…Œ์ด์…˜๋ฐ”-๋…ธ์„ ๋ฐ–-์ง„ํ–‰์ •์ง€-์ˆ˜์ •.md new file mode 100644 index 0000000..352b821 --- /dev/null +++ b/docs/history/2026-07-09_1802_์Šคํ…Œ์ด์…˜๋ฐ”-๋…ธ์„ ๋ฐ–-์ง„ํ–‰์ •์ง€-์ˆ˜์ •.md @@ -0,0 +1,33 @@ +# 2026-07-09 ์Šคํ…Œ์ด์…˜๋ฐ” ๋…ธ์„  ๋ฐ– ๊ตฌ๊ฐ„ ์ง„ํ–‰ ์ •์ง€ ์ˆ˜์ • + +**์†Œ์š” ์‹œ๊ฐ„**: 4๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 500k / output 3k tokens + +## ๋ฌธ์ œ (์Šคํฌ๋ฆฐ์ƒท ์ œ๋ณด) + +๋“œ๋ก ์ด ์ „์ง„ ์ค‘์ธ๋ฐ(์˜์ƒ 1:17, GPS 33.4084/126.2945) ํ•˜๋‹จ ์Šคํ…Œ์ด์…˜๋ฐ” ์ปค์„œยท๋ฐฐ์ง€(0k000)๊ฐ€ ๊ณ ์ •. + +## ์›์ธ + +์ธก์ ์ด ๋กœ๋“œ๋˜๋ฉด์„œ ์Šคํ…Œ์ด์…˜๋ฐ” ์ด๋™๊ฑฐ๋ฆฌ์ถ•์ด **์ธก์  ํด๋ฆฌ๋ผ์ธ ์ฒด์ด๋‹ˆ์ง€ ํˆฌ์˜ |ฮ”| ๋ˆ„์ ** ๋ฐฉ์‹์œผ๋กœ +๋™์ž‘ํ•˜๋Š”๋ฐ, ์ œ์ฃผ ๋น„ํ–‰์€ **๋…ธ์„  ์‹œ์ž‘์ (0k000)์—์„œ ๋‚จ์„œ์ชฝ ์•ฝ 700m ๋ฐ–์—์„œ ์ด๋ฅ™ํ•ด ์ ‘๊ทผ**ํ•œ๋‹ค +(0k000 ๋„๋‹ฌ์€ 87.3์ดˆ ์‹œ์ ). ๋…ธ์„  ๋ฐ–์—์„œ๋Š” ํˆฌ์˜์ด ํด๋ฆฌ๋ผ์ธ ๋์ (0k000)์— ํด๋žจํ”„ โ†’ ์ฒด์ด๋‹ˆ์ง€ +๋ณ€ํ™”๋Ÿ‰ 0 โ†’ ์ ‘๊ทผ ๊ตฌ๊ฐ„ ๋‚ด๋‚ด(์ฒซ 87์ดˆ) ์ปค์„œยท๋ฐฐ์ง€ ์ •์ง€. + +## ์ˆ˜์ • โ€” client/src/stationbar/StationBar.tsx + +- ์ด๋™๊ฑฐ๋ฆฌ์ถ•(frac)์„ ์ฒด์ด๋‹ˆ์ง€ |ฮ”| ๋Œ€์‹  **GPS ์‹ค์ด๋™๊ฑฐ๋ฆฌ ๋ˆ„์ **(ยฑ8ํ”„๋ ˆ์ž„ ์ด๋™ํ‰๊ท  ํ‰ํ™œ)์œผ๋กœ ๋ณ€๊ฒฝ + - ํ˜ธ๋ฒ„ ์‹œ ์ •์ฒด(์ปค์„œ ์ •์ง€) ํŠน์„ฑ ์œ ์ง€ + ๋…ธ์„  ์•ˆํŒŽ ๋ฌด๊ด€ํ•˜๊ฒŒ ์‹ค์ œ ์ด๋™ ๋ฐ˜์˜ + - ์ธก์  ๋ฐฐ์ง€(์ตœ๊ทผ์ ‘ ์ธก์ )ยท๊ตฌ๊ฐ„ ๋ฐฉํ–ฅ์ƒ‰(depChain/arrChain)์€ ๊ธฐ์กด ์ฒด์ด๋‹ˆ์ง€ ๊ธฐ์ค€ ์œ ์ง€ + - ์ธก์  ์—†๋Š” ๋ฐ์ดํ„ฐ์…‹์šฉ GPS ํด๋ฐฑ(์ง์ „ ์ž‘์—…)๊ณผ ๊ณ„์‚ฐ ๋ฐฉ์‹ ์ผ์›ํ™” + +## ๊ฒ€์ฆ (์‹ค๋ฐ์ดํ„ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜, ์„ธ๊ทธ๋จผํŠธ 1) + +- ์Šคํฌ๋ฆฐ์ƒท ์‹œ์  t=77.8s: ์ด๋™ 719m โ†’ ์ปค์„œ 37.7% (๊ธฐ์กด ๋ฐฉ์‹์€ 0% ๊ณ ์ •) +- ์ด ์ด๋™๊ฑฐ๋ฆฌ 1909m, 0k000 ๋„๋‹ฌ 87.3s โ€” ๋„๋‹ฌ ์ „ ๋ฐฐ์ง€๊ฐ€ 0k000์ธ ๊ฒƒ์€ ์ •์ƒ(์ตœ๊ทผ์ ‘ ์ธก์ ) +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-C1t_EPA4.js) ์„œ๋น™ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ๋ฐฐ์ง€๋Š” "๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ธก์ "์ด๋ผ ๋…ธ์„  ์ ‘๊ทผ ์ „์—๋Š” 0k000 ๊ณ ์ •์ด ๋งž๋Š” ๋™์ž‘ โ€” ์ ‘๊ทผ ๊ตฌ๊ฐ„์„ + ๋ณ„๋„ ํ‘œ์‹œ(์˜ˆ: "๋…ธ์„  ์ง„์ž… ์ „")ํ•˜๊ณ  ์‹ถ์œผ๋ฉด chain ํด๋žจํ”„ ์—ฌ๋ถ€๋กœ ํŒ๋ณ„ ๊ฐ€๋Šฅ diff --git a/docs/history/2026-07-09_1842_์Šคํ…Œ์ด์…˜๋ฐ”-์›๊ฑฐ๋ฆฌ๊ตฌ์กฐ๋ฌผ-๊ฒน์นจ์ˆ˜์ •.md b/docs/history/2026-07-09_1842_์Šคํ…Œ์ด์…˜๋ฐ”-์›๊ฑฐ๋ฆฌ๊ตฌ์กฐ๋ฌผ-๊ฒน์นจ์ˆ˜์ •.md new file mode 100644 index 0000000..57e6026 --- /dev/null +++ b/docs/history/2026-07-09_1842_์Šคํ…Œ์ด์…˜๋ฐ”-์›๊ฑฐ๋ฆฌ๊ตฌ์กฐ๋ฌผ-๊ฒน์นจ์ˆ˜์ •.md @@ -0,0 +1,40 @@ +# 2026-07-09 ์Šคํ…Œ์ด์…˜๋ฐ” ์›๊ฑฐ๋ฆฌ ๊ตฌ์กฐ๋ฌผ ๋งˆํฌ ๊ฒน์นจ ์ˆ˜์ • + +**์†Œ์š” ์‹œ๊ฐ„**: 6๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 540k / output 3k tokens + +## ๋ฌธ์ œ (์Šคํฌ๋ฆฐ์ƒท ์ œ๋ณด) + +์Šคํ…Œ์ด์…˜๋ฐ”์— ๊ต๋Ÿ‰ ๋งˆํฌ 2๊ฐœ๊ฐ€ ๋ถ™์–ด ์žˆ๊ณ  ๋ผ๋ฒจ์ด "๊ท€์–ด์Œ์ฒœ๊ต"์ฒ˜๋Ÿผ ๋ณด์ž„ โ€” ์‹ค์ œ๋กœ๋Š” +**"๊ท€๋•์ฒœ๊ต"์™€ "์–ด์Œ์ฒœ๊ต" ๋ผ๋ฒจ์ด ๊ฐ™์€ ์œ„์น˜์— ๊ฒน์ณ** ์ฐํžŒ ๊ฒƒ. (๊ท€์–ด์Œ์ฒœ๊ต๋ผ๋Š” ์‹œ์„ค๋ฌผ์€ ์—†์Œ) + +## ์›์ธ + +์ง€์žฅ๋ฌผ KML์˜ ๊ต๋Ÿ‰ 3๊ฐœ ํ†ต๊ณผ ์‹œ์ (์‹ค๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ): + +| ๊ต๋Ÿ‰ | ์„ธ๊ทธ1 ์ตœ๊ทผ์ ‘ | ์‹ค์ œ ํ†ต๊ณผ | +|------|------------|----------| +| ๊ท€๋•์ฒœ๊ต | t=194.6s, 24m | ์„ธ๊ทธ๋จผํŠธ 1 ๋ (์ •์ƒ) | +| ๊ธˆ์„ฑ์ฒœ๊ต | t=197.1s(๋ ํด๋žจํ”„), **1358m** | ์„ธ๊ทธ๋จผํŠธ 3 (t=510.8s, 3m) | +| ์–ด์Œ์ฒœ๊ต | t=197.1s(๋ ํด๋žจํ”„), **1880m** | ์„ธ๊ทธ๋จผํŠธ 3 (t=565.2s, 59m) | + +`pxPassesTo`์˜ ํ†ต๊ณผ ์ธ์ • ๊ฑฐ๋ฆฌ(TH)๊ฐ€ `max(100, ์ตœ๊ทผ์ ‘๊ฑฐ๋ฆฌร—3)` โ€” ๊ทผ์ ‘ ํ†ต๊ณผ๊ฐ€ ์—†๋Š” ๊ตฌ์กฐ๋ฌผ์€ +TH ๊ฐ€ ์ˆ˜ km ๋กœ ๋Š˜์–ด๋‚˜ ์ „์ฒด ๋น„ํ–‰์ด "ํ†ต๊ณผ"๋กœ ์ธ์ • โ†’ ์ตœ๊ทผ์ ‘ ํ”„๋ ˆ์ž„(์„ธ๊ทธ๋จผํŠธ ๋)์— ๋งˆํฌ ์ƒ์„ฑ โ†’ +์„ธ๊ทธ1 ๋์—์„œ ์‹ค์ œ ํ†ต๊ณผํ•˜๋Š” ๊ท€๋•์ฒœ๊ต์™€ ๊ฐ™์€ px ์— 3๊ฐœ ๋งˆํฌยท๋ผ๋ฒจ์ด ๊ฒน์นจ. + +## ์ˆ˜์ • โ€” client/src/stationbar/StationBar.tsx + +- `pxPassesTo`์— ๊ทผ์ ‘ ํ†ต๊ณผ ์ƒํ•œ `PASS_MAX_M = 300` ์ถ”๊ฐ€: ์ด ์˜์ƒ(์„ธ๊ทธ๋จผํŠธ)์—์„œ 300m ์ด๋‚ด๋กœ + ์ ‘๊ทผํ•˜์ง€ ์•Š์€ ๊ตฌ์กฐ๋ฌผ์€ ๋งˆํฌ ์ƒ๋žต. ํ•ด๋‹น ๊ตฌ์กฐ๋ฌผ์€ ์‹ค์ œ ํ†ต๊ณผํ•˜๋Š” ์„ธ๊ทธ๋จผํŠธ์˜ ๋ฐ”์— ์ •์ƒ ํ‘œ์‹œ๋จ + (์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ ์‹œ frames ๊ต์ฒด โ†’ ๋งˆํฌ ์žฌ๊ณ„์‚ฐ) + +## ๊ฒ€์ฆ + +- ์‹ค๋ฐ์ดํ„ฐ ๊ธฐ์ค€: ์„ธ๊ทธ1 โ†’ ๊ท€๋•์ฒœ๊ต๋งŒ(24m โœ“), ๊ธˆ์„ฑยท์–ด์Œ ์ œ์™ธ(1358/1880m) โœ“ + ์„ธ๊ทธ3 โ†’ ๊ธˆ์„ฑ์ฒœ๊ต(3m)ยท์–ด์Œ์ฒœ๊ต(59m) ํ‘œ์‹œ โœ“ +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, 54000 ํฌํŠธ ์ƒˆ ๋ฒˆ๋“ค(index-CFrrDGW7.js) ์„œ๋น™ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- PASS_MAX_M(300m)์€ ์—ญ์‚ฌ์ฒ˜๋Ÿผ ๋…ธ์„  ์˜† ๊ตฌ์กฐ๋ฌผ๋„ ํฌํ•จํ•˜๋„๋ก ์—ฌ์œ  ์žˆ๊ฒŒ ์„ค์ • โ€” ๋„๋กœ/์ฒ ๋„ + ๊ณตํ†ต์œผ๋กœ ๋ฌธ์ œ์—†์„ ๊ฐ’. ํŠน์ˆ˜ ๋ฐ์ดํ„ฐ์…‹์—์„œ ์กฐ์ • ํ•„์š” ์‹œ route.json ํŒŒ๋ผ๋ฏธํ„ฐํ™” ๊ฒ€ํ†  diff --git a/docs/history/2026-07-09_1850_์Šคํ…Œ์ด์…˜๋ฐฐ์ง€-๋…ธ์„ ๋ฐ–-ํ‘œ๊ธฐ.md b/docs/history/2026-07-09_1850_์Šคํ…Œ์ด์…˜๋ฐฐ์ง€-๋…ธ์„ ๋ฐ–-ํ‘œ๊ธฐ.md new file mode 100644 index 0000000..a2d780e --- /dev/null +++ b/docs/history/2026-07-09_1850_์Šคํ…Œ์ด์…˜๋ฐฐ์ง€-๋…ธ์„ ๋ฐ–-ํ‘œ๊ธฐ.md @@ -0,0 +1,38 @@ +# 2026-07-09 ์Šคํ…Œ์ด์…˜ ๋ฐฐ์ง€ ๋…ธ์„  ๋ฐ–(์ ‘๊ทผ ๊ตฌ๊ฐ„) ํ‘œ๊ธฐ + +**์†Œ์š” ์‹œ๊ฐ„**: 6๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 580k / output 3k tokens + +## ๋ฌธ์ œ (์Šคํฌ๋ฆฐ์ƒท ์ œ๋ณด) + +์˜์ƒ 54.8์ดˆ(์ปค์„œ๋Š” ์ •์ƒ ์ง„ํ–‰ ์ค‘)์ธ๋ฐ ์Šคํ…Œ์ด์…˜ ๋ฐฐ์ง€๊ฐ€ "0k000"์—์„œ ์•ˆ ๋ณ€ํ•จ. + +## ์›์ธ (๋ฒ„๊ทธ ์•„๋‹˜ โ€” ํ‘œ๊ธฐ ๋ฌธ์ œ) + +์ด ๋น„ํ–‰์€ ๋…ธ์„  ์‹œ์ž‘์ (0k000)์—์„œ ๋‚จ์„œ์ชฝ ~700m ๋ฐ–์—์„œ ์ด๋ฅ™ํ•ด **87์ดˆ์— ๋…ธ์„  ์ง„์ž…**. +์ง„์ž… ์ „์—๋Š” "๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ธก์  = 0k000"์ด๋ผ ๋ฐฐ์ง€๊ฐ€ 0k000์œผ๋กœ ๊ณ ์ •๋˜๋Š” ๊ฒŒ ๊ณ„์‚ฐ์ƒ ๋งž์ง€๋งŒ, +์‚ฌ์šฉ์ž๋Š” ๊ณ ์žฅ์œผ๋กœ ์ธ์‹ โ†’ ์ ‘๊ทผ ๊ตฌ๊ฐ„์ž„์„ ๋ฐฐ์ง€์— ๋“œ๋Ÿฌ๋‚ด๊ธฐ๋กœ ํ•จ. + +## ์ˆ˜์ • โ€” client/src/stationbar/StationBar.tsx + +- `projectChainageDist` ์‹ ์„ค: ์ฒด์ด๋‹ˆ์ง€ + **ํด๋ฆฌ๋ผ์ธ๊นŒ์ง€ ์ˆ˜ํ‰๊ฑฐ๋ฆฌ(distM)** ๋ฐ˜ํ™˜ + (๊ธฐ์กด `projectChainage`๋Š” ๋ž˜ํผ๋กœ ์œ ์ง€ โ€” ๊ตฌ์กฐ๋ฌผ ๋งˆํฌ ๋“ฑ ๊ธฐ์กด ํ˜ธ์ถœ๋ถ€ ๋ฌด๋ณ€๊ฒฝ) +- `ViewedPoint.offM` ์ถ”๊ฐ€(ํ”„๋ ˆ์ž„๋ณ„ ๋…ธ์„  ์ดํƒˆ ๊ฑฐ๋ฆฌ), `realOffM` memo ์ถ”๊ฐ€ +- ๋ฐฐ์ง€ ํ‘œ๊ธฐ: ๋…ธ์„ ์—์„œ **60m ์ด์ƒ ๋ฒ—์–ด๋‚œ ๊ตฌ๊ฐ„์€ "๋…ธ์„ ๋ฐ– โ—‹โ—‹m"** ๋กœ ํ‘œ์‹œ(๊ฑฐ๋ฆฌ ์‹ค์‹œ๊ฐ„ ๊ฐฑ์‹ ), + 60m ์ด๋‚ด๋ฉด ๊ธฐ์กด ์ฒด์ด๋‹ˆ์ง€(0k000, 0k110, โ€ฆ) ํ‘œ์‹œ + +## ๊ฒ€์ฆ (์‹ค๋ฐ์ดํ„ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜, ์„ธ๊ทธ๋จผํŠธ 1) + +| ์‹œ์  | ๋ฐฐ์ง€ | +|------|------| +| 10s | ๋…ธ์„ ๋ฐ– 708m | +| 54.8s (์Šคํฌ๋ฆฐ์ƒท) | ๋…ธ์„ ๋ฐ– 346m | +| 87s (์ง„์ž…) | 0k000 | +| 100s / 130s / 190s | 0k110 / 0k410 / 1k010 | + +โ†’ ์ง„์ž… ํ›„ ๋ฐฐ์ง€ ์ •์ƒ ์ง„ํ–‰ ํ™•์ธ. tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, ์ƒˆ ๋ฒˆ๋“ค(index-DyILp3c8.js) ์„œ๋น™. + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- OFF_ROUTE_M=60m ๊ณ ์ • โ€” ๋“œ๋ก ์ด ๋…ธ์„  ์˜†์œผ๋กœ ํฌ๊ฒŒ ์ด๊ฒฉ ๋น„ํ–‰ํ•˜๋Š” ๋ฐ์ดํ„ฐ์…‹์ด๋ฉด route.json + ํŒŒ๋ผ๋ฏธํ„ฐํ™” ๊ฒ€ํ†  diff --git a/docs/history/2026-07-09_2021_์นด๋ฉ”๋ผjson-์„ธ๊ทธ๋จผํŠธ๋ผ๋ฒจ-ํŒ์—…์ •๋ ฌ.md b/docs/history/2026-07-09_2021_์นด๋ฉ”๋ผjson-์„ธ๊ทธ๋จผํŠธ๋ผ๋ฒจ-ํŒ์—…์ •๋ ฌ.md new file mode 100644 index 0000000..badddc7 --- /dev/null +++ b/docs/history/2026-07-09_2021_์นด๋ฉ”๋ผjson-์„ธ๊ทธ๋จผํŠธ๋ผ๋ฒจ-ํŒ์—…์ •๋ ฌ.md @@ -0,0 +1,47 @@ +# 2026-07-09 ์นด๋ฉ”๋ผ JSON ์ €์žฅ/์ ์šฉ + ์„ธ๊ทธ๋จผํŠธ ๋ผ๋ฒจ ์บ์‹œ + ํŒ์—… ์ •๋ ฌ + ๋ฐ•์Šค ๋†’์ด + +**์†Œ์š” ์‹œ๊ฐ„**: 20๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 640k / output 10k tokens + +## ์š”์ฒญ 4๊ฑด ์ฒ˜๋ฆฌ + +### 1. ์นด๋ฉ”๋ผ ํ™”๊ฐ ์ •๋ณด๋ฅผ ์˜์ƒ ์ด๋ฆ„์˜ JSON ์œผ๋กœ ์„œ๋ฒ„ ์ €์žฅ โ†’ ํด๋”์— ํ•จ๊ป˜ ์žˆ์œผ๋ฉด ์ž๋™ ์ ์šฉ + +- **์„œ๋ฒ„**: `routes/camera.ts` ์‹ ๊ทœ โ€” `PUT /api/camera/:videoId` ๊ฐ€ ์˜์ƒ ํด๋”(VIDEOS_DIR)์— + `<์˜์ƒ base>.camera.json` ์ €์žฅ(`{ camera: {...}, model, savedAt }`), `GET` ์กฐํšŒ. Path traversal ๋ฐฉ์–ด +- **ํด๋ผ์ด์–ธํŠธ**: + - `parseCameraJson`(geoData): ํด๋”์—์„œ `<์˜์ƒ base>.camera.json`(๋˜๋Š” `.json`) ์ธ์‹ โ€” + ํด๋” ๋‚ด ์–ด๋–ค ๋ถ„ํ• ๋ณธ base ์™€๋„ ๋งค์นญ, ์ˆซ์ž ํ•„๋“œ๋งŒ ํ†ต๊ณผ + - geoStore `cameraJson` ์ ์žฌ โ†’ StationOverlay ์—์„œ **์ตœ์šฐ์„  ์ ์šฉ**(localStorage ๋ณด์ •ยทdjmd ๊ฐ์ง€๊ฐ’๋ณด๋‹ค ์œ„, + effect ์„ ์–ธ ์ˆœ์„œ๋กœ ๋ณด์žฅ) + - ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„ ๐Ÿ“ท ์ค„์— **[์„œ๋ฒ„์— ์ €์žฅ]** ๋ฒ„ํŠผ โ€” ํ˜„์žฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ „์ฒด(focalLen/yaw/pitch/โ€ฆ)๋ฅผ + ํ˜„์žฌ ์žฌ์ƒ ์ค‘์ธ ์˜์ƒ ์ด๋ฆ„์œผ๋กœ ์ €์žฅ. ์ €์žฅ์ค‘โ€ฆ/์ €์žฅ๋จ โœ“/์‹คํŒจ ์ƒํƒœ ํ‘œ์‹œ +- E2E ๊ฒ€์ฆ: PUT โ†’ D: ํด๋”์— ํŒŒ์ผ ์ƒ์„ฑ โ†’ GET ์ผ์น˜ โ†’ ์ •๋ฆฌ ์™„๋ฃŒ + +### 2. ๋‘ ๋ฒˆ์งธ ์˜์ƒ์—์„œ ์‹œ์„ค๋ฌผ ๋ฏธํ‘œ์ถœ + +- ์›์ธ: ๋ผ๋ฒจ ์‚ฌ์ „๊ณ„์‚ฐ(startLabelPrecompute) effect deps ์— `storeFrames` ๋ˆ„๋ฝ โ€” + ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜(loadSegment)์œผ๋กœ frames ๊ฐ€ ๊ต์ฒด๋ผ๋„ ์ด์ „ ์„ธ๊ทธ๋จผํŠธ์˜ ๋ผ๋ฒจ ์บ์‹œ๋ฅผ ๊ณ„์† ์‚ฌ์šฉ โ†’ + ์ƒˆ ์œ„์น˜์—์„œ ์ „๋ถ€ ํ™”๋ฉด ๋ฐ– ํˆฌ์˜ +- ์ˆ˜์ •: ๋‘ precompute effect deps ์— `storeFrames` ์ถ”๊ฐ€ โ†’ ์„ธ๊ทธ๋จผํŠธ ์ „ํ™˜ ์‹œ ์žฌ๊ณ„์‚ฐ + +### 3. ์‹œ์„ค๋ฌผ ๋ผ๋ฒจ ์•„๋ž˜ ๊ฒ€์€ ํŒ์—… ์ •๋ ฌ + +- ์›์ธ: ํŒ์—… x ์•ต์ปค๊ฐ€ ๊ตฌ ๋ผ๋ฒจ ๋ ˆ์ด์•„์›ƒ(์ง€์  ์˜ค๋ฅธ์ชฝ ํ…์ŠคํŠธ) ๊ธฐ์ค€ `sx โˆ’ 10` ๊ณ ์ • โ€” + ๋ผ๋ฒจ์ด ์ค‘์•™์ •๋ ฌ ๋ฐ•์Šค๋กœ ๋ฐ”๋€Œ๋ฉด์„œ ์–ด๊ธ‹๋‚จ +- ์ˆ˜์ •: POI ํŒ์—…์€ `sx โˆ’ ํŒ์—…ํญ/2`(๋ฐ•์Šค ์ค‘์•™ ์•„๋ž˜ ์ •๋ ฌ), ์ธก์  ํŒ์—…์€ ๊ธฐ์กด ์œ ์ง€ + +### 4. ๋ผ๋ฒจ ๋ฐฐ๊ฒฝ ๋ฐ•์Šค ์—ฌ์œ  + +- BOX_H 22 โ†’ **24**(๊ธ€์ž 20px ร— 1.2), PAD_X 6โ†’7, ๋™์ผ์ขŒํ‘œ ๋‹ค์ค‘ ๋ผ๋ฒจ ํ–‰ ๊ฐ„๊ฒฉ ROW_H 24โ†’28 + +## ๊ฒ€์ฆ + +- ํด๋ผ์ด์–ธํŠธ(tsc+vite)ยท์„œ๋ฒ„(tsc) ๋นŒ๋“œ ํ†ต๊ณผ, pm2 ์žฌ์‹œ์ž‘, ์ƒˆ ๋ฒˆ๋“ค(index-BxAoyQvu.js) ์„œ๋น™ +- ์นด๋ฉ”๋ผ API ์™•๋ณต ํ…Œ์ŠคํŠธ ํ†ต๊ณผ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- camera.json ์ €์žฅ์€ ์„œ๋ฒ„ VIDEOS_DIR ๊ธฐ์ค€ โ€” ํด๋” ์„ ํƒ์œผ๋กœ ์—ฌ๋Š” ๋กœ์ปฌ ํด๋”๊ฐ€ VIDEOS_DIR ๊ณผ + ๋‹ค๋ฅธ ๊ฒฝ๋กœ๋ฉด ์ €์žฅ ํŒŒ์ผ์ด ๊ทธ ํด๋”์— ์—†์Œ(ํ˜„ ๊ตฌ์„ฑ์€ ๋™์ผ ํด๋”๋ผ ๋ฌธ์ œ์—†์Œ) +- camera.json ์€ ํด๋” ๊ณต์šฉ(๋ถ„ํ• ๋ณธ ์ „์ฒด ์ ์šฉ). ์„ธ๊ทธ๋จผํŠธ๋ณ„ ๋ณ„๋„ ์นด๋ฉ”๋ผ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ํ™•์žฅ ํ•„์š” diff --git a/docs/history/2026-07-09_2024_์ปค์„œ๋ฐฐ์ง€-์ค„๋ฐ”๊ฟˆ-์ˆ˜์ •.md b/docs/history/2026-07-09_2024_์ปค์„œ๋ฐฐ์ง€-์ค„๋ฐ”๊ฟˆ-์ˆ˜์ •.md new file mode 100644 index 0000000..e1b4c5c --- /dev/null +++ b/docs/history/2026-07-09_2024_์ปค์„œ๋ฐฐ์ง€-์ค„๋ฐ”๊ฟˆ-์ˆ˜์ •.md @@ -0,0 +1,17 @@ +# 2026-07-09 ์Šคํ…Œ์ด์…˜๋ฐ” ์ปค์„œ ๋ฐฐ์ง€ ์ค„๋ฐ”๊ฟˆ ์ˆ˜์ • + +**์†Œ์š” ์‹œ๊ฐ„**: 3๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 680k / output 1k tokens + +## ๋ฌธ์ œ + +์ปค์„œ ๋ฐฐ์ง€์˜ "๋…ธ์„ ๋ฐ– 737m" ํ‘œ๊ธฐ๊ฐ€ ๋‘ ์ค„๋กœ ๊บพ์—ฌ ํ‘œ์‹œ๋จ (๋ฐฐ์ง€๊ฐ€ min-width 112px + 20px ๊ธ€์ž ++ ์ค„๋ฐ”๊ฟˆ ํ—ˆ์šฉ์ด๋ผ ๊ณต๋ฐฑ์—์„œ ๊ฐœํ–‰). + +## ์ˆ˜์ • โ€” TimelineCursor.module.scss + +- ๋ฐฐ์ง€ ๊ธ€์ž 20px โ†’ **17px**, `white-space: nowrap` ์ถ”๊ฐ€ (ํญ์€ ๋‚ด์šฉ์— ๋งž๊ฒŒ ์ž๋™ ํ™•์žฅ) + +## ๊ฒ€์ฆ + +- ๋นŒ๋“œ ํ†ต๊ณผ, ์ƒˆ ๋ฒˆ๋“ค(index-Cf5yoZh9.js) ์„œ๋น™ diff --git a/docs/history/2026-07-09_2030_ํ˜ธ๋ฒ„๊ตฌ๊ฐ„-๋ผ๋ฒจ-๊นœ๋นก์ž„-์ˆ˜์ •.md b/docs/history/2026-07-09_2030_ํ˜ธ๋ฒ„๊ตฌ๊ฐ„-๋ผ๋ฒจ-๊นœ๋นก์ž„-์ˆ˜์ •.md new file mode 100644 index 0000000..716ef57 --- /dev/null +++ b/docs/history/2026-07-09_2030_ํ˜ธ๋ฒ„๊ตฌ๊ฐ„-๋ผ๋ฒจ-๊นœ๋นก์ž„-์ˆ˜์ •.md @@ -0,0 +1,35 @@ +# 2026-07-09 ํ˜ธ๋ฒ„ ๊ตฌ๊ฐ„ ๋ผ๋ฒจ ๊นœ๋นก์ž„ ์ˆ˜์ • (์„ธ๊ทธ๋จผํŠธ 2) + +**์†Œ์š” ์‹œ๊ฐ„**: 6๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 720k / output 3k tokens + +## ๋ฌธ์ œ + +๋‘ ๋ฒˆ์งธ ์˜์ƒ ์žฌ์ƒ ์‹œ ๋ผ๋ฒจ์ด ์‹ฌํ•˜๊ฒŒ ๊นœ๋นก์ž„. + +## ์›์ธ + +์„ธ๊ทธ๋จผํŠธ 2๋Š” **120์ดˆ ํ˜ธ๋ฒ„ ๊ตฌ๊ฐ„**(์ง๋ฒŒ ํšŒ์ „ 0.0ยฐ/s โ€” ์‹ค๋ฐ์ดํ„ฐ ํ™•์ธ, ์™„์ „ ์ •์ง€ ํ”„๋ ˆ์ด๋ฐ): + +1. **ํ™”๋ฉด ๊ฒฝ๊ณ„ ๊ฑธ์นจ ๋ผ๋ฒจ์˜ ํฌํ•จ ๊ฒฝ๊ณ„ ํ”Œ๋ž˜ํ•‘**: ๋ผ๋ฒจ ์‚ฌ์ „๊ณ„์‚ฐ์˜ ํ™”๋ฉด ํฌํ•จ ํŒ์ •์ด + POI ยฑ0.02 / ์ธก์  ยฑ0.05 ๋กœ ํƒ€์ดํŠธ โ€” ํ˜ธ๋ฒ„ ์ค‘ GPS ๋ฏธ์„ธ ์ง€ํ„ฐ๋กœ ๊ฒฝ๊ณ„์— ๊ฑธ์นœ ๋ผ๋ฒจ์ด + ์บ์‹œ ํ”„๋ ˆ์ž„(10Hz)๋งˆ๋‹ค ํฌํ•จโ†”์ œ์™ธ๋ฅผ ๋ฐ˜๋ณต โ†’ ํ‘œ์‹œ/์†Œ๋ฉธ ๊นœ๋นก์ž„. + (์ด๋™ ์ค‘์—๋Š” ๊ฒฝ๊ณ„ ํ†ต๊ณผ๊ฐ€ ์ˆœ๊ฐ„์ด๋ผ ๋ˆˆ์— ์•ˆ ๋„์—ˆ์Œ โ€” ํ˜ธ๋ฒ„์—์„  120์ดˆ ๋‚ด๋‚ด ๋ฐ˜๋ณต) +2. **๊ฒน์นจ ์–ต์ œ ์Šน์ž ํ”Œ๋ž˜ํ•‘**: ๊ฒน์นœ ๋ผ๋ฒจ ์Œ์˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ dist ์ •๋ ฌ โ€” ๊ฑฐ๋ฆฌ๊ฐ€ ๊ฑฐ์˜ ๊ฐ™์€ ์Œ์€ + ์ง€ํ„ฐ๋กœ ์ˆœ์œ„๊ฐ€ ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋’ค๋ฐ”๋€Œ์–ด ๋‘ ๋ผ๋ฒจ์ด ๊ต๋Œ€๋กœ ๊นœ๋นก์ž„. + +## ์ˆ˜์ • โ€” client/src/components/overlay/StationOverlay.tsx + +- ์‚ฌ์ „๊ณ„์‚ฐ ํฌํ•จ ๊ฒฝ๊ณ„๋ฅผ POI/์ธก์  ๋ชจ๋‘ **ยฑ0.15** ๋กœ ํ™•๋Œ€ โ€” ๊ฒฝ๊ณ„ ํŒ์ •์„ ์•ˆ์ •ํ™”ํ•˜๊ณ  + ์‹ค์ œ ํ™”๋ฉด ๋ฐ– ์ปฌ๋ง์€ ๋“œ๋กœ์šฐ ๋‹จ๊ณ„(labelOnScreen)๊ฐ€ ๋‹ด๋‹น (๋งˆ์ปค ์ˆ˜ ์ฆ๊ฐ€๋Š” ๋ฏธ๋ฏธ) +- ๊ฒน์นจ ์–ต์ œ ์ •๋ ฌ์— **๊ฑฐ๋ฆฌ์ฐจ 1m ๋ฏธ๋งŒ = ์ œ๋ชฉ์ˆœ ๊ณ ์ •** ํƒ€์ด๋ธŒ๋ ˆ์ดํฌ ์ถ”๊ฐ€ โ€” ์Šน์ž ๊ณ ์ • + +## ๊ฒ€์ฆ + +- ์„ธ๊ทธ๋จผํŠธ๋ณ„ ์ง๋ฒŒ ๋ฐ์ดํ„ฐ: ์„ธ๊ทธ1 ํ‰๊ท  0.4ยฐ/s(์ด๋™ ์ดฌ์˜) vs ์„ธ๊ทธ2 ํ‰๊ท  0.0ยฐ/s(ํ˜ธ๋ฒ„) โ€” ์›์ธ ๋ถ€ํ•ฉ +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, ์ƒˆ ๋ฒˆ๋“ค(index-pDBh9-ym.js) ์„œ๋น™ + +## ๋‹ค์Œ ๋‹จ๊ณ„ ์ฐธ๊ณ  + +- ๋‚จ์„ ์ˆ˜ ์žˆ๋Š” ์ž”์—ฌ ๊นœ๋นก์ž„: POI ํ‘œ์‹œ๋ฒ”์œ„(maxPoiRange) ๊ฒฝ๊ณ„(๊ธฐ๋ณธ 1000m)์— ์ •ํ™•ํžˆ ๊ฑธ์นœ POI ๊ฐ€ + ํ˜ธ๋ฒ„ ์ค‘ ์ง€ํ„ฐ๋กœ in/out ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒฝ์šฐ โ€” ๋ฐœ์ƒ ์‹œ ๋ฒ”์œ„ ํŒ์ •์— ํžˆ์Šคํ…Œ๋ฆฌ์‹œ์Šค(ยฑ2%) ์ถ”๊ฐ€ ๊ฒ€ํ†  diff --git a/docs/history/2026-07-09_2035_์นด๋ฉ”๋ผํŒจ๋„-๊ฐ’-์ถœ์ฒ˜-ํ™•์ธ.md b/docs/history/2026-07-09_2035_์นด๋ฉ”๋ผํŒจ๋„-๊ฐ’-์ถœ์ฒ˜-ํ™•์ธ.md new file mode 100644 index 0000000..1bfe5b3 --- /dev/null +++ b/docs/history/2026-07-09_2035_์นด๋ฉ”๋ผํŒจ๋„-๊ฐ’-์ถœ์ฒ˜-ํ™•์ธ.md @@ -0,0 +1,23 @@ +# 2026-07-09 ์นด๋ฉ”๋ผ ํŒจ๋„ ๊ฐ’ ์ถœ์ฒ˜ ํ™•์ธ (Q&A) + +**์†Œ์š” ์‹œ๊ฐ„**: 3๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 750k / output 1k tokens + +## ์งˆ๋ฌธ + +ํด๋” ๋กœ๋“œ ํ›„ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„ ๊ฐ’๋“ค์ด ๋“œ๋ก ์— ์ €์žฅ๋œ ์นด๋ฉ”๋ผ ๊ฐ’์ธ์ง€ ํ™•์ธ ์š”์ฒญ. + +## ๋‹ต๋ณ€ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์Œ) + +| ํ•ญ๋ชฉ | ๊ฐ’ | ์ถœ์ฒ˜ | +|------|-----|------| +| f | 29.9mm | **๋“œ๋ก (djmd) ๊ฐ์ง€๊ฐ’ ์ž๋™ ์ ์šฉ** โœ“ | +| ์ƒํƒœ์ค„ yaw/pitch | 13.2ยฐ/โˆ’15ยฐ | **๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ**(ํ˜„์žฌ ํ”„๋ ˆ์ž„ ์ง๋ฒŒ ์ž์„ธ) โœ“ | +| Yaw ยฑ | โˆ’16.5ยฐ | localStorage ๋ณต์›๋œ ์‚ฌ์šฉ์ž ๋ณด์ •(๊ธฐ๋ณธ 0) โ€” f=24 ์‹œ์ ˆ ํŠœ๋‹ ์ž”์žฌ, ์žฌ์กฐ์ • ๊ถŒ์žฅ | +| cyโ‚€ | โˆ’0.105 | localStorage ๋ณต์›๋œ ์‚ฌ์šฉ์ž ๋ณด์ •(๊ธฐ๋ณธ 0) | +| ์ง€์˜ค์ด๋“œ | 25.8 | ์•ฑ ๊ธฐ๋ณธ๊ฐ’(๋Œ€์ „ ๊ธฐ์ค€) โ€” ์ง€๋ฉด๊ณ ๋„ ๋ชจ๋“œ์—์„  ๋ฏธ์‚ฌ์šฉ | +| sen W/H | 36/20.25 | ์•ฑ ๊ธฐ๋ณธ๊ฐ’ โ€” P1์ด ์‹ค์ œ ํ’€ํ”„๋ ˆ์ž„์ด๋ผ ์‚ฌ์‹ค๊ณผ ์ผ์น˜ | + +## ์‚ฌ์šฉ์ž ์•ˆ๋‚ด + +์ดˆ๊ธฐํ™” โ†’ f 29.9 ์ ์šฉ โ†’ Yaw ์žฌ์กฐ์ • โ†’ [์„œ๋ฒ„์— ์ €์žฅ]์œผ๋กœ camera.json ๊ณ ์ • ๊ถŒ์žฅ. diff --git a/docs/history/2026-07-09_2042_djmd-์ž์„ธ-์ฃผ์ -๋ถ€์žฌ-ํ™•์ธ.md b/docs/history/2026-07-09_2042_djmd-์ž์„ธ-์ฃผ์ -๋ถ€์žฌ-ํ™•์ธ.md new file mode 100644 index 0000000..0230d49 --- /dev/null +++ b/docs/history/2026-07-09_2042_djmd-์ž์„ธ-์ฃผ์ -๋ถ€์žฌ-ํ™•์ธ.md @@ -0,0 +1,28 @@ +# 2026-07-09 djmd์— yaw/cyโ‚€ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ (Q&A) + +**์†Œ์š” ์‹œ๊ฐ„**: 5๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 780k / output 2k tokens + +## ์งˆ๋ฌธ + +๋“œ๋ก  ์นด๋ฉ”๋ผ์—์„œ yaw, cyโ‚€ ๊ฐ’๋„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์ง€ ์•Š์€์ง€. + +## ๋ถ„์„ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์Œ) + +์„ธ๊ทธ๋จผํŠธ1 ์ค‘๋ฐ˜(ํ”„๋ ˆ์ž„ 5990, tโ‰ˆ100s, CSV yaw=65.1ยฐ) djmd ๋ ˆ์ฝ”๋“œ ์ „์ˆ˜ ํ•ด๋…: + +- ์กด์žฌ: ์œ„์น˜(6.4/6.5 ์œ„๊ฒฝ๋„ ๋ผ๋””์•ˆ, 6.6 ํƒ€์›์ฒด๊ณ  mm), ๋…ธ์ถœ(7.4 ์…”ํ„ฐ๋ถ„๋ชจ 60~65, 7.5 ISO๊ณ„์—ด, + 7.7~7.10 ์ธก๊ด‘), ๋ Œ์ฆˆ(8.6 ์ดˆ์  299=29.9mm, 8.8 ํฌ์ปค์Šค๊ฑฐ๋ฆฌ), ํ•ด์ƒ๋„/fps +- **๋ถ€์žฌ: ์ง๋ฒŒ ์ž์„ธ(yaw/pitch/roll), ์ฃผ์ (cx/cy) ์บ˜๋ฆฌ๋ธŒ๋ ˆ์ด์…˜** +- 7.4=65 ๋Š” yaw(65.1ยฐ)์™€ ์šฐ์—ฐํžˆ ์œ ์‚ฌํ•˜๋‚˜ ํ”„๋ ˆ์ž„10์—์„œ 61(๋‹น์‹œ yaw 29.7ยฐ)๋กœ ๋ถˆ์ผ์น˜ โ†’ ์…”ํ„ฐ ๋ถ„๋ชจ๋กœ ํŒ์ • + +## ๋‹ต๋ณ€ ์š”์ง€ + +- Yawยฑ๋Š” ๋กœ๊ทธ ๊ณ„ํ†ต ์˜ค์ฐจ(์ž๊ธฐํŽธ๊ฐ ๋“ฑ)์˜ ๋ณด์ • ๋ธํƒ€ โ€” ๋“œ๋ก  ๋ฐ์ดํ„ฐ์— ์กด์žฌ ๋ถˆ๊ฐ€ +- cyโ‚€๋Š” DJI **์‚ฌ์ง„ JPG์˜ XMP DewarpData**์—๋งŒ ์กด์žฌ (fx/fy/cx/cy/์™œ๊ณก๊ณ„์ˆ˜) + +## ์ œ์•ˆ (๋ฏธ๊ตฌํ˜„) + +1. P1 ์‚ฌ์ง„ 1์žฅ ํด๋” ๋™๋ด‰ ์‹œ DewarpData ์ž๋™ ์ ์šฉ ๊ธฐ๋Šฅ +2. GPS ์ง„ํ–‰๋ฐฉ์œ„ vs ์ง๋ฒŒ ํ—ค๋”ฉ์œผ๋กœ yaw ๋ฐ”์ด์–ด์Šค ์ž๋™ ์ถ”์ • +3. ๋ผ๋ฒจ ๋“œ๋ž˜๊ทธ๋กœ yaw ์—ญ์‚ฐ ๋ชจ๋“œ (FOV ๋ณด์ •๊ณผ ๋™์ผ ํŒจํ„ด) โ€” ์‹ค๋ฌด์  ์ตœ์„  diff --git a/docs/history/2026-07-09_2046_yaw๋ณด์ •-๋ฐฉ์•ˆ-์„ค๋ช….md b/docs/history/2026-07-09_2046_yaw๋ณด์ •-๋ฐฉ์•ˆ-์„ค๋ช….md new file mode 100644 index 0000000..0184892 --- /dev/null +++ b/docs/history/2026-07-09_2046_yaw๋ณด์ •-๋ฐฉ์•ˆ-์„ค๋ช….md @@ -0,0 +1,15 @@ +# 2026-07-09 Yaw ๋ณด์ • ๋ฐฉ์•ˆ 2ยท3๋ฒˆ ์ƒ์„ธ ์„ค๋ช… (Q&A) + +**์†Œ์š” ์‹œ๊ฐ„**: 3๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 800k / output 1k tokens + +## ๋‚ด์šฉ (์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์Œ) + +Yaw ๋ณด์ • ์ž๋™ํ™” ๋‘ ๋ฐฉ์•ˆ์„ ์‰ฌ์šด ์–ธ์–ด๋กœ ์žฌ์„ค๋ช…: + +- **2๋ฒˆ(์ž๋™ ์ถ”์ •)**: GPS ์ด๋™ ๋ฐฉ์œ„(์ •ํ™•) vs ์ง๋ฒŒ ํ—ค๋”ฉ(๋‚˜์นจ๋ฐ˜ ์˜ค์ฐจ ํฌํ•จ)์˜ ์ผ์ •ํ•œ ์ฐจ์ด์˜ + ์ค‘์•™๊ฐ’์„ Yawยฑ ์— ์ž๋™ ์ž…๋ ฅ. ๋ฒ„ํŠผ ํ•œ ๋ฒˆ, ๊ทผ์‚ฌ์น˜. ์นด๋ฉ”๋ผ๊ฐ€ ์ง„ํ–‰๋ฐฉํ–ฅ์„ ์•ˆ ๋ณธ ๊ตฌ๊ฐ„์ด ์„ž์ด๋ฉด ์˜ค์ฐจ. +- **3๋ฒˆ(๋“œ๋ž˜๊ทธ ์—ญ์‚ฐ)**: ์ผ์‹œ์ •์ง€ โ†’ ์–ด๊ธ‹๋‚œ ๋ผ๋ฒจ์„ ์˜์ƒ ์† ์‹ค์ œ ์ง€๋ฌผ ์œ„์น˜๋กœ ๋“œ๋ž˜๊ทธ โ†’ + ํ•„์š”ํ•œ Yawยฑ ๋ฅผ ์—ญ์‚ฐํ•ด ์ž๋™ ์„ค์ •. ๊ธฐ์กด FOV ๋ณด์ • ๋ชจ๋“œ(f ์—ญ์‚ฐ)์™€ ๋™์ผ ํŒจํ„ด. ์ •ํ™•ยท์ง๊ด€์ . + +์ถ”์ฒœ: 3๋ฒˆ(์ •ํ™•) ๋˜๋Š” 2๋ฒˆ+3๋ฒˆ ๋ณ‘ํ–‰. ์‚ฌ์šฉ์ž ์„ ํƒ ๋Œ€๊ธฐ. diff --git a/docs/history/2026-07-09_2050_yaw์ž๋™์ถ”์ •-๋“œ๋ž˜๊ทธ์—ญ์‚ฐ-PC์ €์žฅ.md b/docs/history/2026-07-09_2050_yaw์ž๋™์ถ”์ •-๋“œ๋ž˜๊ทธ์—ญ์‚ฐ-PC์ €์žฅ.md new file mode 100644 index 0000000..c86cd4e --- /dev/null +++ b/docs/history/2026-07-09_2050_yaw์ž๋™์ถ”์ •-๋“œ๋ž˜๊ทธ์—ญ์‚ฐ-PC์ €์žฅ.md @@ -0,0 +1,39 @@ +# 2026-07-09 Yaw ์ž๋™์ถ”์ • + ๋“œ๋ž˜๊ทธ ์—ญ์‚ฐ + ์นด๋ฉ”๋ผ๊ฐ’ PC ์ €์žฅ + +**์†Œ์š” ์‹œ๊ฐ„**: 15๋ถ„ +**Context ์‚ฌ์šฉ๋Ÿ‰**: input 850k / output 8k tokens + +## ์ž‘์—… ๋‚ด์šฉ (์š”์ฒญ 3๊ฑด) + +### 1. Yaw ์ž๋™ ์ถ”์ • ๋ฒ„ํŠผ (2๋ฒˆ ๋ฐฉ์•ˆ) + +- ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„ 'Yaw ยฑ' ์•„๋ž˜ **[Yaw ์ž๋™์ถ”์ • (๋น„ํ–‰๊ฒฝ๋กœ ๊ธฐ์ค€)]** ๋ฒ„ํŠผ +- ์•Œ๊ณ ๋ฆฌ์ฆ˜: ์ „์ง„ ๊ตฌ๊ฐ„(์†๋„โ‰ฅ2m/s)์—์„œ GPS ์ง„ํ–‰ ๋ฐฉ์œ„(1์ดˆ ์ฐฝ ํ‰ํ™œ) โˆ’ ์ง๋ฒŒ ํ—ค๋”ฉ ์ฐจ์ด ์ˆ˜์ง‘ + (|์ฐจ|โ‰ฅ60ยฐ ๋Š” ์˜†๋ณด๊ธฐ ์ดฌ์˜์œผ๋กœ ๊ฐ„์ฃผ ์ œ์™ธ) โ†’ **์ค‘์•™๊ฐ’์„ Yawยฑ ๋กœ ์„ค์ •** +- ์‹ค๋ฐ์ดํ„ฐ ๊ฒ€์ฆ: ์„ธ๊ทธ๋จผํŠธ1 ์œ ํšจ ์ƒ˜ํ”Œ 1889๊ฐœ, ์ค‘์•™๊ฐ’ **โˆ’16.3ยฐ** + โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์ด์ „์— ์ˆ˜๋™ ํŠœ๋‹ํ–ˆ๋˜ โˆ’16.5ยฐ ์™€ ๊ฑฐ์˜ ์ผ์น˜ (์•Œ๊ณ ๋ฆฌ์ฆ˜ยท๊ธฐ์กด ํŠœ๋‹ ์ƒํ˜ธ ๊ฒ€์ฆ) + +### 2. Yaw ๋“œ๋ž˜๊ทธ ์—ญ์‚ฐ ๋ชจ๋“œ (3๋ฒˆ ๋ฐฉ์•ˆ) + +- POI ํŽธ์ง‘ ์„น์…˜์— **[๐Ÿงญ Yaw ๋ณด์ • โ€” POI๋ฅผ ์‹ค์ œ ์œ„์น˜(์ขŒ์šฐ)๋กœ ๋“œ๋ž˜๊ทธ]** ๋ชจ๋“œ ์ถ”๊ฐ€ + (ํŽธ์ง‘/์„ธ๋กœํ™”๊ฐ ๋ชจ๋“œ์™€ ์ƒํ˜ธ ๋ฐฐ์ œ, ๊ธฐ์กด fovMode ํŒจํ„ด ์žฌ์‚ฌ์šฉ) +- ์—ญ์‚ฐ: ฮธ0=atan2(Xc,Zc)(ํ˜„์žฌ ํˆฌ์˜๊ฐ) vs ฮธ1=atan((px*โˆ’0.5โˆ’cx0)ยทsW/f)(๋“œ๋กญ ์ง€์  ์š”๊ตฌ๊ฐ) + โ†’ `yawOffset += (ฮธ0โˆ’ฮธ1)`. |ฮ”|>45ยฐ ๋Š” ์˜ค์กฐ์ž‘์œผ๋กœ ๊ฑฐ๋ถ€ +- ๊ฒŒ์ดํŒ…(์ปค์„œ/ํžˆํŠธ/ํŒ์—…/๋“œ๋ž˜๊ทธ ํ”ผ๋“œ๋ฐฑ) 4๊ณณ์— yawMode ์—ฐ๊ฒฐ + +### 3. ์นด๋ฉ”๋ผ๊ฐ’ ์ €์žฅ: ์„œ๋ฒ„ โ†’ **์‚ฌ์šฉ์ž PC ๋‹ค์šด๋กœ๋“œ** + +- ๐Ÿ“ท ์ค„ ๋ฒ„ํŠผ '์„œ๋ฒ„์— ์ €์žฅ' โ†’ **[PC์— ์ €์žฅ]**: ํ˜„์žฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ „์ฒด(Yawยฑ ๋ณด์ • ๊ฒฐ๊ณผ ํฌํ•จ)๋ฅผ + `<์˜์ƒ์ด๋ฆ„>.camera.json` ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ๋‹ค์šด๋กœ๋“œ +- ๋ฐ›์€ ํŒŒ์ผ์„ ์˜์ƒ ํด๋”์— ๋„ฃ์œผ๋ฉด ํด๋” ์„ ํƒ ์‹œ ์ตœ์šฐ์„  ์ž๋™ ์ ์šฉ(๊ธฐ์กด ๋กœ๋” ๊ทธ๋Œ€๋กœ) +- ์„œ๋ฒ„ ๋ผ์šฐํŠธ(/api/camera)๋Š” ์œ ์ง€ํ•˜๋˜ UI ์—์„œ๋Š” ๋ฏธ์‚ฌ์šฉ + +## ๊ฒ€์ฆ + +- tsc+vite ๋นŒ๋“œ ํ†ต๊ณผ, ์ƒˆ ๋ฒˆ๋“ค(index-D5ltlp17.js) ์„œ๋น™ +- Yaw ์ž๋™ ์ถ”์ • ์‹ค๋ฐ์ดํ„ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ โˆ’16.3ยฐ (์ˆ˜๋™๊ฐ’ โˆ’16.5ยฐ ๋ถ€ํ•ฉ) + +## ์‚ฌ์šฉ ํ๋ฆ„ (์‚ฌ์šฉ์ž ์•ˆ๋‚ด) + +1. [Yaw ์ž๋™์ถ”์ •] โ†’ ๋Œ€๋žต ์ •๋ ฌ 2. [๐Ÿงญ Yaw ๋ณด์ •] ๋ชจ๋“œ์—์„œ ๋ผ๋ฒจ ํ•˜๋‚˜๋ฅผ ์‹ค์ œ ์ง€๋ฌผ๋กœ ๋“œ๋ž˜๊ทธ โ†’ ๋ฏธ์„ธ ์กฐ์ • +3. [PC์— ์ €์žฅ] โ†’ ๋ฐ›์€ `<์˜์ƒ์ด๋ฆ„>.camera.json` ์„ ์˜์ƒ ํด๋”๋กœ ์ด๋™ โ†’ ์ดํ›„ ํด๋” ์„ ํƒ๋งŒ์œผ๋กœ ์ ์šฉ diff --git a/ecosystem.config.js b/ecosystem.config.js index 1fef460..d128c09 100755 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,7 +1,7 @@ module.exports = { apps: [ { - name: "ghiVideo", + name: "ghiVideo-v4", // cwd = ์ด ์„ค์ • ํŒŒ์ผ์ด ์žˆ๋Š” ์œ„์น˜(ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ)๋ฅผ ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๊ณ ์ •. // ์ด ํ•œ ๊ณณ๋งŒ ์•ต์ปค๋กœ ๋‘๊ณ , ์•„๋ž˜ ๊ฒฝ๋กœ๋Š” ์ „๋ถ€ cwd ๊ธฐ์ค€ ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ ํ•ด์„๋œ๋‹ค. cwd: __dirname, @@ -10,9 +10,11 @@ module.exports = { // PATH ์˜ node ์‚ฌ์šฉ โ€” pm2 start ์ „์— nvm ์œผ๋กœ Node 20 ์ด ํ™œ์„ฑํ™”๋ผ ์žˆ์–ด์•ผ ํ•จ interpreter: "node", env: { - PORT: 55000, + PORT: 54000, // ์„œ๋ฒ„ config.ts ๊ฐ€ path.resolve(cwd ๊ธฐ์ค€)๋กœ ํ•ด์„ โ†’ ์ƒ๋Œ€๊ฒฝ๋กœ OK - VIDEOS_DIR: "samplevideo", + // ์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„ ๋„๋กœ ๋ฐ์ดํ„ฐ (D: ๋“œ๋ผ์ด๋ธŒ, WSL drvfs ๋งˆ์šดํŠธ ๊ฒฝ์œ ) + VIDEOS_DIR: + "/mnt/d/_project/20260612_abcvideo/import data/v4.0 (์ œ์ฃผ๋„)/v4.0 (์ œ์ฃผ๋„_ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ)/์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„ ๋„๋กœ ๋…ธ์„  ๋™์˜์ƒ ๋น„ํ–‰ ๋กœ๊ทธ/๊ฒฝ๋กœ 1-1", HLS_DIR: "storage/hls", FRAMES_DIR: "storage/frames", THUMBNAILS_DIR: "storage/thumbnails", @@ -20,7 +22,9 @@ module.exports = { // ์ ˆ๋Œ€๊ฒฝ๋กœ ๋Œ€์‹  PATH ์—์„œ ํƒ์ƒ‰ (apt ์„ค์น˜ ์‹œ /usr/bin ์— ์žˆ๊ณ  PATH ํฌํ•จ๋จ) FFMPEG_PATH: "ffmpeg", FFPROBE_PATH: "ffprobe", - GEO_DATA_DIR: "samplevideo", + // ๋น„ํ–‰๋กœ๊ทธ CSV/KML ์œ„์น˜ (๋“œ๋ก  CSV ๋กœ๋”๋Š” ์ œ์ฃผ ํ˜•์‹ ๋ฏธ์ง€์› โ€” ์–ด๋Œ‘ํ„ฐ ์ž‘์—… ํ•„์š”) + GEO_DATA_DIR: + "/mnt/d/_project/20260612_abcvideo/import data/v4.0 (์ œ์ฃผ๋„)/v4.0 (์ œ์ฃผ๋„_ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ)/์ œ์ฃผ ์ค‘์‚ฐ๊ฐ„ ๋„๋กœ ๋…ธ์„  ๋™์˜์ƒ ๋น„ํ–‰ ๋กœ๊ทธ/๊ฒฝ๋กœ 1-1", CENTER_CSV_PATH: "pythonsource/input/center.csv", }, }, diff --git a/server/src/app.ts b/server/src/app.ts index bec89d2..6ad644d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -15,6 +15,7 @@ import annotationsRouter from './routes/annotations'; import geoRouter from './routes/geo'; import elevationRouter from './routes/elevation'; import tileRouter from './routes/tile'; +import cameraRouter from './routes/camera'; const app = express(); @@ -37,6 +38,7 @@ app.use('/api/annotations/:videoId', annotationsRouter); app.use('/api/geo', geoRouter); app.use('/api/elevation', elevationRouter); app.use('/api/tile', tileRouter); +app.use('/api/camera', cameraRouter); app.get('/api/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); diff --git a/server/src/routes/camera.ts b/server/src/routes/camera.ts new file mode 100644 index 0000000..7759856 --- /dev/null +++ b/server/src/routes/camera.ts @@ -0,0 +1,64 @@ +/** + * ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒŒ์ผ ์ €์žฅ/์กฐํšŒ + * + * ํด๋ผ์ด์–ธํŠธ(์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ํŒจ๋„)์˜ '์„œ๋ฒ„์— ์ €์žฅ' ๋ฒ„ํŠผ์ด ํ˜„์žฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ + * ์˜์ƒ๊ณผ ๊ฐ™์€ ํด๋”(VIDEOS_DIR)์— `<์˜์ƒ base>.camera.json` ์œผ๋กœ ์ €์žฅํ•œ๋‹ค. + * ์ดํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ํด๋”๋ฅผ ๋‹ค์‹œ ์„ ํƒํ•˜๋ฉด(์˜์ƒ๊ณผ ํ•จ๊ป˜ ์ด json ์ด ๋”ธ๋ ค ์˜ค๋ฉด) + * ํด๋ผ์ด์–ธํŠธ ๋กœ๋”๊ฐ€ ์ด ํŒŒ์ผ์˜ ์นด๋ฉ”๋ผ ๊ฐ’์„ ์ตœ์šฐ์„ ์œผ๋กœ ์ ์šฉํ•œ๋‹ค. + */ + +import { Router, Request, Response } from 'express'; +import path from 'path'; +import fs from 'fs'; +import { config } from '../config'; + +const router = Router(); + +function cameraFilePath(videoId: string): string | null { + const base = videoId.replace(/\.[^.]+$/, ''); + if (!base) return null; + const filePath = path.resolve(config.videosDir, `${base}.camera.json`); + // Path traversal ๋ฐฉ์–ด โ€” VIDEOS_DIR ๋‚ด๋ถ€๋งŒ ํ—ˆ์šฉ + if (!filePath.startsWith(path.resolve(config.videosDir))) return null; + return filePath; +} + +// PUT /api/camera/:videoId โ€” ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์ €์žฅ (body = { camera: {...}, model?, savedAt? }) +router.put('/:videoId', (req: Request, res: Response) => { + const filePath = cameraFilePath(req.params.videoId); + if (!filePath) { + res.status(400).json({ error: 'Invalid video ID' }); + return; + } + const body = req.body; + if (!body || typeof body !== 'object' || typeof body.camera !== 'object') { + res.status(400).json({ error: 'body.camera ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค' }); + return; + } + try { + fs.writeFileSync(filePath, JSON.stringify(body, null, 2), 'utf-8'); + res.json({ ok: true, file: path.basename(filePath) }); + } catch (e) { + res.status(500).json({ error: String(e) }); + } +}); + +// GET /api/camera/:videoId โ€” ์ €์žฅ๋œ ์นด๋ฉ”๋ผ ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐํšŒ (์—†์œผ๋ฉด 404) +router.get('/:videoId', (req: Request, res: Response) => { + const filePath = cameraFilePath(req.params.videoId); + if (!filePath) { + res.status(400).json({ error: 'Invalid video ID' }); + return; + } + if (!fs.existsSync(filePath)) { + res.status(404).json({ error: 'camera.json ์—†์Œ' }); + return; + } + try { + res.json(JSON.parse(fs.readFileSync(filePath, 'utf-8'))); + } catch (e) { + res.status(500).json({ error: String(e) }); + } +}); + +export default router;