뭘 수정했더라....;;;

This commit is contained in:
2026-08-22 14:59:52 +09:00
parent 3c458de80e
commit a5a1b2a2a2
2 changed files with 211 additions and 48 deletions
+84
View File
@@ -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;
+121 -42
View File
@@ -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);
};
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,9 +522,15 @@ function App() {
}
if (lat === undefined) {
const last = lastPositionRef.current;
if (last) {
lat = last.lat;
lng = last.lng;
} else {
skipped++;
continue;
}
}
const level = await requestLevel(file, { lat, lng });
if (!level) {
@@ -602,9 +641,13 @@ function App() {
ref={folderInputRef}
onChange={handleFolderUpload}
/>
<label htmlFor="camera-input" className="capture-button">
<button
type="button"
className="capture-button"
onClick={handleCameraClick}
>
📷 등록
</label>
</button>
<button
type="button"
className="locate-btn"
@@ -674,7 +717,9 @@ function App() {
}
>
<div className="gps-info__title">📍 {gpsInfo.source}</div>
{gpsInfo.lat != null && (
<div>{gpsInfo.lat.toFixed(5)}, {gpsInfo.lng.toFixed(5)}</div>
)}
</div>
)}
@@ -715,6 +760,40 @@ function App() {
</div>
</div>
{poles.length > 0 && (
<div className="pole-list__time-filter">
<div className="time-filter__header">
<span className="time-filter__label">⏱ 촬영시간</span>
<button
type="button"
className={`time-filter__toggle ${timeFilterEnabled ? 'time-filter__toggle--on' : ''}`}
onClick={() => {
setTimeFilterEnabled((v) => !v);
if (!timeFilterEnabled) setTimeFilterWeek(maxWeek);
}}
>
{timeFilterEnabled ? 'ON' : 'OFF'}
</button>
</div>
{timeFilterEnabled && (
<>
<input
type="range"
className="time-filter__slider"
min={minWeek}
max={maxWeek}
value={timeFilterWeek}
onChange={(e) => setTimeFilterWeek(Number(e.target.value))}
/>
<div className="time-filter__info">
<span>{timeFilterLabel}</span>
<span className="time-filter__count">{filteredPoles.length}기</span>
</div>
</>
)}
</div>
)}
<div className="pole-list__filters">
{[
{ value: 'all', label: '전체', color: '#94a3b8' },