mirror of
https://github.com/15000rpm/Pole-Mapper.git
synced 2026-10-09 10:45:10 +09:00
뭘 수정했더라....;;;
This commit is contained in:
+84
@@ -217,6 +217,90 @@
|
|||||||
flex: 1;
|
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 {
|
.pole-list__filters {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-bottom: 1px solid #e5e7eb;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
|||||||
+127
-48
@@ -49,6 +49,8 @@ function App() {
|
|||||||
const [levelFilter, setLevelFilter] = useState('all');
|
const [levelFilter, setLevelFilter] = useState('all');
|
||||||
const [guFilter, setGuFilter] = useState('용산구');
|
const [guFilter, setGuFilter] = useState('용산구');
|
||||||
const [dongFilter, setDongFilter] = useState('all');
|
const [dongFilter, setDongFilter] = useState('all');
|
||||||
|
const [timeFilterEnabled, setTimeFilterEnabled] = useState(false);
|
||||||
|
const [timeFilterWeek, setTimeFilterWeek] = useState(0);
|
||||||
const polesRef = useRef([]);
|
const polesRef = useRef([]);
|
||||||
const [allDongs, setAllDongs] = useState([]);
|
const [allDongs, setAllDongs] = useState([]);
|
||||||
const popupRef = useRef(null);
|
const popupRef = useRef(null);
|
||||||
@@ -65,13 +67,55 @@ function App() {
|
|||||||
return allDongs.filter((d) => d.gu_name === guFilter);
|
return allDongs.filter((d) => d.gu_name === guFilter);
|
||||||
}, [allDongs, 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(() => {
|
const filteredPoles = useMemo(() => {
|
||||||
return poles.filter(
|
return poles.filter(
|
||||||
(p) => (levelFilter === 'all' || p.level === levelFilter) &&
|
(p) => (levelFilter === 'all' || p.level === levelFilter) &&
|
||||||
(guFilter === 'all' || p.gu === guFilter) &&
|
(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(() => {
|
useEffect(() => {
|
||||||
if (!gpsInfo) return;
|
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(() => {
|
useEffect(() => {
|
||||||
if (mapRef.current) return;
|
if (mapRef.current) return;
|
||||||
|
|
||||||
@@ -267,27 +335,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSuccess = (position) => {
|
const fallback = { lat: 37.5665, lng: 126.978 };
|
||||||
if (aborted) return;
|
lastPositionRef.current = fallback;
|
||||||
const { latitude: lat, longitude: lng } = position.coords;
|
initMap(fallback.lng, fallback.lat);
|
||||||
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) {
|
if (navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(handleSuccess, handleError, {
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
watchIdRef.current = navigator.geolocation.watchPosition(
|
watchIdRef.current = navigator.geolocation.watchPosition(
|
||||||
(position) => {
|
(position) => {
|
||||||
const { latitude: lat, longitude: lng } = position.coords;
|
const { latitude: lat, longitude: lng } = position.coords;
|
||||||
@@ -307,10 +359,10 @@ function App() {
|
|||||||
(err) => {
|
(err) => {
|
||||||
console.error('watchPosition 실패:', err.code, err.message);
|
console.error('watchPosition 실패:', err.code, err.message);
|
||||||
},
|
},
|
||||||
{ enableHighAccuracy: true, maximumAge: 10000 },
|
{ enableHighAccuracy: true, maximumAge: 0 },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
handleError();
|
fallbackUsedRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -362,31 +414,12 @@ function App() {
|
|||||||
console.error('EXIF 읽기 실패:', err);
|
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) {
|
if (lat === undefined) {
|
||||||
const last = lastPositionRef.current;
|
const last = lastPositionRef.current;
|
||||||
if (last) {
|
if (last) {
|
||||||
lat = last.lat;
|
lat = last.lat;
|
||||||
lng = last.lng;
|
lng = last.lng;
|
||||||
source = watchIdRef.current != null ? '실시간 GPS' : '폴백 (초기 위치)';
|
source = '실시간 GPS';
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No GPS available');
|
throw new Error('No GPS available');
|
||||||
}
|
}
|
||||||
@@ -489,8 +522,14 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lat === undefined) {
|
if (lat === undefined) {
|
||||||
skipped++;
|
const last = lastPositionRef.current;
|
||||||
continue;
|
if (last) {
|
||||||
|
lat = last.lat;
|
||||||
|
lng = last.lng;
|
||||||
|
} else {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const level = await requestLevel(file, { lat, lng });
|
const level = await requestLevel(file, { lat, lng });
|
||||||
@@ -602,9 +641,13 @@ function App() {
|
|||||||
ref={folderInputRef}
|
ref={folderInputRef}
|
||||||
onChange={handleFolderUpload}
|
onChange={handleFolderUpload}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="camera-input" className="capture-button">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="capture-button"
|
||||||
|
onClick={handleCameraClick}
|
||||||
|
>
|
||||||
📷 등록
|
📷 등록
|
||||||
</label>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="locate-btn"
|
className="locate-btn"
|
||||||
@@ -674,7 +717,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="gps-info__title">📍 {gpsInfo.source}</div>
|
<div className="gps-info__title">📍 {gpsInfo.source}</div>
|
||||||
<div>{gpsInfo.lat.toFixed(5)}, {gpsInfo.lng.toFixed(5)}</div>
|
{gpsInfo.lat != null && (
|
||||||
|
<div>{gpsInfo.lat.toFixed(5)}, {gpsInfo.lng.toFixed(5)}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -715,6 +760,40 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="pole-list__filters">
|
||||||
{[
|
{[
|
||||||
{ value: 'all', label: '전체', color: '#94a3b8' },
|
{ value: 'all', label: '전체', color: '#94a3b8' },
|
||||||
|
|||||||
Reference in New Issue
Block a user