From a5a1b2a2a282fa3d9985796ae37cd06dd6360d83 Mon Sep 17 00:00:00 2001 From: gerd Date: Sat, 22 Aug 2026 14:59:52 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AD=98=20=EC=88=98=EC=A0=95=ED=96=88?= =?UTF-8?q?=EB=8D=94=EB=9D=BC....;;;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 84 +++++++++++++++++++++++++ src/App.jsx | 175 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 211 insertions(+), 48 deletions(-) diff --git a/src/App.css b/src/App.css index 99010a4..f5afb63 100644 --- a/src/App.css +++ b/src/App.css @@ -217,6 +217,90 @@ flex: 1; } +.pole-list__time-filter { + padding: 10px 12px; + border-bottom: 1px solid #e5e7eb; + flex-shrink: 0; +} + +.time-filter__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.time-filter__label { + font-size: 12px; + font-weight: 600; + color: #374151; +} + +.time-filter__toggle { + padding: 2px 10px; + border-radius: 10px; + border: 1px solid #d1d5db; + background: white; + font-size: 11px; + font-weight: 600; + cursor: pointer; + color: #6b7280; + transition: background-color 0.15s, color 0.15s; +} + +.time-filter__toggle--on { + background-color: #3b82f6; + color: white; + border-color: #3b82f6; +} + +.time-filter__slider { + width: 100%; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: #e5e7eb; + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.time-filter__slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: #3b82f6; + border: 2px solid white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +.time-filter__slider::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: #3b82f6; + border: 2px solid white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; +} + +.time-filter__info { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 4px; + font-size: 11px; + color: #6b7280; +} + +.time-filter__count { + font-weight: 600; + color: #3b82f6; +} + .pole-list__filters { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; diff --git a/src/App.jsx b/src/App.jsx index feb16d3..72760e2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -49,6 +49,8 @@ function App() { const [levelFilter, setLevelFilter] = useState('all'); const [guFilter, setGuFilter] = useState('용산구'); const [dongFilter, setDongFilter] = useState('all'); + const [timeFilterEnabled, setTimeFilterEnabled] = useState(false); + const [timeFilterWeek, setTimeFilterWeek] = useState(0); const polesRef = useRef([]); const [allDongs, setAllDongs] = useState([]); const popupRef = useRef(null); @@ -65,13 +67,55 @@ function App() { return allDongs.filter((d) => d.gu_name === guFilter); }, [allDongs, guFilter]); + const { minWeek, maxWeek } = useMemo(() => { + if (poles.length === 0) return { minWeek: 0, maxWeek: 0 }; + let minTs = Infinity; + let maxTs = -Infinity; + for (const p of poles) { + const t = Date.parse(p.timestamp); + if (!Number.isNaN(t)) { + if (t < minTs) minTs = t; + if (t > maxTs) maxTs = t; + } + } + if (!Number.isFinite(minTs)) return { minWeek: 0, maxWeek: 0 }; + const toWeek = (ts) => Math.floor((ts - minTs) / (7 * 86400000)); + return { minWeek: 0, maxWeek: Math.max(toWeek(maxTs), 0) }; + }, [poles]); + + const timeFilterCutoff = useMemo(() => { + if (!timeFilterEnabled || poles.length === 0) return null; + let minTs = Infinity; + for (const p of poles) { + const t = Date.parse(p.timestamp); + if (!Number.isNaN(t) && t < minTs) minTs = t; + } + if (!Number.isFinite(minTs)) return null; + const cutoffTs = minTs + (timeFilterWeek + 1) * 7 * 86400000; + return new Date(cutoffTs).toISOString(); + }, [timeFilterEnabled, timeFilterWeek, poles]); + + const timeFilterLabel = useMemo(() => { + if (!timeFilterEnabled || poles.length === 0) return ''; + let minTs = Infinity; + for (const p of poles) { + const t = Date.parse(p.timestamp); + if (!Number.isNaN(t) && t < minTs) minTs = t; + } + if (!Number.isFinite(minTs)) return ''; + const cutoffTs = minTs + (timeFilterWeek + 1) * 7 * 86400000; + const d = new Date(cutoffTs); + return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${Math.ceil(d.getDate() / 7)}주차`; + }, [timeFilterEnabled, timeFilterWeek, poles]); + const filteredPoles = useMemo(() => { return poles.filter( (p) => (levelFilter === 'all' || p.level === levelFilter) && (guFilter === 'all' || p.gu === guFilter) && - (dongFilter === 'all' || p.dong === dongFilter) + (dongFilter === 'all' || p.dong === dongFilter) && + (!timeFilterEnabled || p.timestamp <= timeFilterCutoff) ); - }, [poles, levelFilter, guFilter, dongFilter]); + }, [poles, levelFilter, guFilter, dongFilter, timeFilterEnabled, timeFilterCutoff]); useEffect(() => { if (!gpsInfo) return; @@ -173,6 +217,30 @@ function App() { } }, []); + const refreshGps = useCallback(() => { + return new Promise((resolve) => { + if (!navigator.geolocation) { + resolve(lastPositionRef.current); + return; + } + navigator.geolocation.getCurrentPosition( + (pos) => { + const position = { lat: pos.coords.latitude, lng: pos.coords.longitude }; + lastPositionRef.current = position; + resolve(position); + }, + () => resolve(lastPositionRef.current), + { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }, + ); + }); + }, []); + + const handleCameraClick = () => { + if (processing) return; + refreshGps(); + document.getElementById('camera-input').click(); + }; + useEffect(() => { if (mapRef.current) return; @@ -267,27 +335,11 @@ function App() { } }; - const handleSuccess = (position) => { - if (aborted) return; - const { latitude: lat, longitude: lng } = position.coords; - lastPositionRef.current = { lat, lng }; - initMap(lng, lat); - }; - - const handleError = (err) => { - if (aborted) return; - console.error('getCurrentPosition 실패:', err?.code, err?.message); - const fallback = { lat: 37.5665, lng: 126.978 }; - lastPositionRef.current = fallback; - fallbackUsedRef.current = true; - initMap(fallback.lng, fallback.lat); - }; + const fallback = { lat: 37.5665, lng: 126.978 }; + lastPositionRef.current = fallback; + initMap(fallback.lng, fallback.lat); if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(handleSuccess, handleError, { - enableHighAccuracy: true, - }); - watchIdRef.current = navigator.geolocation.watchPosition( (position) => { const { latitude: lat, longitude: lng } = position.coords; @@ -307,10 +359,10 @@ function App() { (err) => { console.error('watchPosition 실패:', err.code, err.message); }, - { enableHighAccuracy: true, maximumAge: 10000 }, + { enableHighAccuracy: true, maximumAge: 0 }, ); } else { - handleError(); + fallbackUsedRef.current = true; } return () => { @@ -362,31 +414,12 @@ function App() { console.error('EXIF 읽기 실패:', err); } - if (lat === undefined && navigator.geolocation) { - try { - console.log('브라우저 GPS 요청 중...'); - const pos = await new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, { - enableHighAccuracy: true, - timeout: 15000, - maximumAge: 30000, - }); - }); - lat = pos.coords.latitude; - lng = pos.coords.longitude; - source = '브라우저 GPS'; - lastPositionRef.current = { lat, lng }; - } catch (err) { - console.error('브라우저 GPS 실패:', err); - } - } - if (lat === undefined) { const last = lastPositionRef.current; if (last) { lat = last.lat; lng = last.lng; - source = watchIdRef.current != null ? '실시간 GPS' : '폴백 (초기 위치)'; + source = '실시간 GPS'; } else { throw new Error('No GPS available'); } @@ -489,8 +522,14 @@ function App() { } if (lat === undefined) { - skipped++; - continue; + const last = lastPositionRef.current; + if (last) { + lat = last.lat; + lng = last.lng; + } else { + skipped++; + continue; + } } const level = await requestLevel(file, { lat, lng }); @@ -602,9 +641,13 @@ function App() { ref={folderInputRef} onChange={handleFolderUpload} /> -