Skip to content

feat : 기록 화면 지도 개선 및 포토 리포트 오버레이 컴포넌트화#130

Merged
peisonger merged 6 commits into
mainfrom
fix/record-report
Jun 5, 2026
Merged

feat : 기록 화면 지도 개선 및 포토 리포트 오버레이 컴포넌트화#130
peisonger merged 6 commits into
mainfrom
fix/record-report

Conversation

@peisonger

@peisonger peisonger commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

지도

  • GPS 트랙 없을 때 → 코스 폴리라인 + 시작(파란 점)/끝(빨간 점) 마커로 폴백
  • GPS 트랙 있을 때 → 실제 GPS 경로 + 시작/끝 마커
  • GPS 트랙 있고 사진 있을 때 → GPS 경로 + 시작/끝 마커 + 원형 사진 마커(24×24)
  • 정적 이미지 폴백 제거, NaverMapView 항상 렌더링
  • 경로 크기에 맞게 zoom 자동 계산 (course-detail-info 동일 로직)
  • 폴리라인 굵기 코스 상세 화면과 통일 (width 4)
  • 날짜 배지 디자인 피그마 스펙 반영 (날짜/시간 텍스트 분리)
  • bottom-sheet.tsx 네비게이션에 courseId 파라미터 추가

포토 리포트

  • PNG 이미지 오버레이 → SVG/React 컴포넌트 방식으로 교체 (overlay-stats.tsx)
  • 템플릿 3종 (Template1~3Overlay) 구현: 그라디언트 배경, 산 실루엣, 통계 스탯, 세모산 로고
  • HikingRecordDetail 타입에 track(GeoJSON), photos 필드 추가

Test plan

  • GPS 트랙 없는 기록 → 코스 폴리라인 지도 표시 확인
  • GPS 트랙 있는 기록 → 노란 경로 + 파란/빨간 점 마커 확인
  • 사진 찍은 기록 → 원형 사진 마커 GPS 좌표에 표시 확인
  • 포토 리포트 템플릿 3종 렌더링 확인
  • 날짜 배지 정상 표시 확인

peisonger added 4 commits June 4, 2026 10:56
- 하드코딩 SVG 지도 → NaverMapView + GPS 경로(노란 선) + 사진·출발·도착 마커로 교체
- HikingRecordDetail 타입에 track(GeoJSON), photos 필드 추가
- 날짜 배지 디자인 피그마 스펙 반영 (배경색, 날짜/시간 텍스트 분리)
- CourseBottomSheet → hikingRecordId 올바르게 전달하도록 버그 수정
- 포토 리포트 오버레이 3열 우측 여백 부족 수정
- GPS 트랙 없음 → 코스 폴리라인 + 시작/끝 마커
- GPS 트랙 있음, 사진 없음 → GPS 경로 + 시작/끝 마커
- GPS 트랙 있음, 사진 있음 → GPS 경로 + 시작/끝 마커 + 원형 사진 마커
- 정적 이미지 폴백(mapbasic.svg) 제거, NaverMapView 항상 렌더링
- bottom-sheet.tsx 네비게이션에 courseId 파라미터 추가

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a detailed hiking record view and an interactive photo report editor, integrating Naver Map overlays to display routes and milestone photos, alongside customizable stats overlay templates. The review feedback highlights several critical improvements to ensure robustness: validating the session ID before restoring state to prevent cross-session leaks, merging loaded tracking photos with saved ones to avoid UI lockouts, cleaning up map camera-fitting timers to prevent memory leaks, and adding defensive checks for invalid dates and empty coordinates to avoid rendering errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 86 to 91
useEffect(() => {
if (trackingPhotos.length > 0 && photos.length === 0) {
setPhotos(trackingPhotos.map((uri) => ({ key: uri, source: { uri } })));
setSelectedPhoto(0);
}
}, [trackingPhotos]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

photos 배열이 이전 화면의 저장된 상태(길이 1)로 초기화된 경우, photos.length === 0 조건이 거짓이 되어 trackingPhotos를 로드하는 useEffect가 실행되지 않습니다. 이로 인해 사용자는 해당 기록에서 촬영한 다른 사진들을 썸네일 목록에서 볼 수 없고 오직 저장된 사진 한 장만 보게 되는 심각한 사용성 문제가 발생합니다. 기존 사진 목록에 trackingPhotos를 중복 없이 병합하도록 개선해야 합니다.

  useEffect(() => {
    if (trackingPhotos.length > 0) {
      setPhotos((prev) => {
        const existingKeys = new Set(prev.map((p) => p.key));
        const newTrackingPhotos = trackingPhotos
          .filter((uri) => !existingKeys.has(uri))
          .map((uri) => ({ key: uri, source: { uri } }));
        const merged = [...prev, ...newTrackingPhotos];
        if (selectedPhoto === null && merged.length > 0) {
          setSelectedPhoto(0);
        }
        return merged;
      });
    }
  }, [trackingPhotos]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정함

Comment thread app/record/[id].tsx
const photoReportShotRef = useRef<ViewShot | null>(null);
const mapRef = useRef<NaverMapViewRef>(null);
const activeTabPublic = isPublicByTab[activeTab];
const trackCoords = parseTrack(recordDetail?.track);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이미 @/features/tracking/utils/parse-course-polyline에 동일한 GeoJSON LineString 파싱 로직을 수행하는 parseCoursePolyline 함수가 정의되어 있습니다. 중복 코드를 방지하기 위해 parseTrack 함수를 제거하고 parseCoursePolyline을 직접 재사용하는 것을 권장합니다.

Suggested change
const trackCoords = parseTrack(recordDetail?.track);
const trackCoords = parseCoursePolyline(recordDetail?.track);

Comment thread app/record/[id].tsx
Comment on lines +75 to +87
function formatMapDateParts(startedAt?: string, endedAt?: string): { date: string; time: string } {
if (!startedAt) return { date: "", time: "" };
const d = new Date(startedAt);
const yy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
const day = DAY_KO[d.getDay()];
const startTime = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
if (!endedAt) return { date: `${yy}.${mm}.${dd} ${day}`, time: startTime };
const e = new Date(endedAt);
const endTime = `${String(e.getHours()).padStart(2, "0")}:${String(e.getMinutes()).padStart(2, "0")}`;
return { date: `${yy}.${mm}.${dd} ${day}`, time: `${startTime} - ${endTime}` };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

startedAt 또는 endedAt에 유효하지 않은 날짜 문자열이 전달될 경우, new Date()Invalid Date를 반환하며 이후 getFullYear(), getMonth() 등의 메서드가 NaN을 반환하여 화면에 "NaN.NaN.NaN undefined"와 같이 비정상적인 텍스트가 표시될 수 있습니다. 날짜가 유효한지 확인하는 방어 코드를 추가하는 것이 안전합니다.

function formatMapDateParts(startedAt?: string, endedAt?: string): { date: string; time: string } {
  if (!startedAt) return { date: "", time: "" };
  const d = new Date(startedAt);
  if (isNaN(d.getTime())) return { date: "", time: "" };
  const yy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  const day = DAY_KO[d.getDay()];
  const startTime = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
  if (!endedAt) return { date: `${yy}.${mm}.${dd} ${day}`, time: startTime };
  const e = new Date(endedAt);
  if (isNaN(e.getTime())) return { date: `${yy}.${mm}.${dd} ${day}`, time: startTime };
  const endTime = `${String(e.getHours()).padStart(2, "0")}:${String(e.getMinutes()).padStart(2, "0")}`;
  return { date: `${yy}.${mm}.${dd} ${day}`, time: `${startTime} - ${endTime}` };
}

Comment thread app/record/[id].tsx
Comment on lines +226 to +242
useEffect(() => {
if (trackCoords.length < 2) return;
const lats = trackCoords.map((c) => c.latitude);
const lngs = trackCoords.map((c) => c.longitude);
const minLat = Math.min(...lats);
const maxLat = Math.max(...lats);
const minLng = Math.min(...lngs);
const maxLng = Math.max(...lngs);
const padding = 0.15;
setTimeout(() => {
mapRef.current?.animateCameraWithTwoCoords({
coord1: { latitude: minLat - (maxLat - minLat) * padding, longitude: minLng - (maxLng - minLng) * padding },
coord2: { latitude: maxLat + (maxLat - minLat) * padding, longitude: maxLng + (maxLng - minLng) * padding },
duration: 300,
});
}, 300);
}, [trackCoords.length]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useEffect 내에서 setTimeout을 사용하여 카메라를 이동시키고 있습니다. 컴포넌트가 300ms 이내에 언마운트되거나 trackCoords.length가 변경되어 이펙트가 재실행될 때 타이머가 정리(cleanup)되지 않으면, 메모리 누수가 발생하거나 이미 언마운트된 컴포넌트의 ref에 접근하려는 경고가 발생할 수 있습니다. 반환 함수(cleanup function)에서 타이머를 클리어해 주어야 합니다.

Suggested change
useEffect(() => {
if (trackCoords.length < 2) return;
const lats = trackCoords.map((c) => c.latitude);
const lngs = trackCoords.map((c) => c.longitude);
const minLat = Math.min(...lats);
const maxLat = Math.max(...lats);
const minLng = Math.min(...lngs);
const maxLng = Math.max(...lngs);
const padding = 0.15;
setTimeout(() => {
mapRef.current?.animateCameraWithTwoCoords({
coord1: { latitude: minLat - (maxLat - minLat) * padding, longitude: minLng - (maxLng - minLng) * padding },
coord2: { latitude: maxLat + (maxLat - minLat) * padding, longitude: maxLng + (maxLng - minLng) * padding },
duration: 300,
});
}, 300);
}, [trackCoords.length]);
useEffect(() => {
if (trackCoords.length < 2) return;
const lats = trackCoords.map((c) => c.latitude);
const lngs = trackCoords.map((c) => c.longitude);
const minLat = Math.min(...lats);
const maxLat = Math.max(...lats);
const minLng = Math.min(...lngs);
const maxLng = Math.max(...lngs);
const padding = 0.15;
const timer = setTimeout(() => {
mapRef.current?.animateCameraWithTwoCoords({
coord1: { latitude: minLat - (maxLat - minLat) * padding, longitude: minLng - (maxLng - minLng) * padding },
coord2: { latitude: maxLat + (maxLat - minLat) * padding, longitude: maxLng + (maxLng - minLng) * padding },
duration: 300,
});
}, 300);
return () => clearTimeout(timer);
}, [trackCoords.length]);

Comment thread app/record/[id].tsx
Comment on lines +372 to +379
const lats = mapCoords.map((c) => c.latitude);
const lngs = mapCoords.map((c) => c.longitude);
const latKm = (Math.max(...lats) - Math.min(...lats)) * 111;
const lngKm = (Math.max(...lngs) - Math.min(...lngs)) * 89;
const dominantRatio = Math.max(lngKm / 12, latKm / 7);
const zoom = mapCoords.length > 1
? Math.min(Math.max(Math.round(11 + Math.log2(0.6 / dominantRatio)), 6), 18)
: 13;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

mapCoords가 비어있을 때(예: 데이터 로딩 중이거나 경로 정보가 없을 때), Math.max(...lats)Math.min(...lats)는 각각 -InfinityInfinity를 반환합니다. 이로 인해 latKmlngKm-Infinity가 되고, dominantRatio 또한 -Infinity가 되어 Math.log2 계산 시 비정상적인 값이 발생합니다. 비록 삼항 연산자(mapCoords.length > 1)로 실제 zoom 값은 13으로 대체되지만, 불필요하고 위험한 수학적 연산이 무조건 실행되므로 해당 계산을 조건문 내부로 격리하는 것이 안전합니다.

Suggested change
const lats = mapCoords.map((c) => c.latitude);
const lngs = mapCoords.map((c) => c.longitude);
const latKm = (Math.max(...lats) - Math.min(...lats)) * 111;
const lngKm = (Math.max(...lngs) - Math.min(...lngs)) * 89;
const dominantRatio = Math.max(lngKm / 12, latKm / 7);
const zoom = mapCoords.length > 1
? Math.min(Math.max(Math.round(11 + Math.log2(0.6 / dominantRatio)), 6), 18)
: 13;
let zoom = 13;
if (mapCoords.length > 1) {
const lats = mapCoords.map((c) => c.latitude);
const lngs = mapCoords.map((c) => c.longitude);
const latKm = (Math.max(...lats) - Math.min(...lats)) * 111;
const lngKm = (Math.max(...lngs) - Math.min(...lngs)) * 89;
const dominantRatio = Math.max(lngKm / 12, latKm / 7);
zoom = Math.min(Math.max(Math.round(11 + Math.log2(0.6 / dominantRatio)), 6), 18);
}

Comment on lines +36 to +43
export function fmtDate(iso?: string): string {
if (!iso) return "--";
const d = new Date(iso);
const yy = String(d.getFullYear()).slice(2);
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}.${mm}.${dd}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

iso 매개변수에 유효하지 않은 날짜 문자열이 전달될 경우, new Date(iso)Invalid Date를 반환하며 getFullYear() 등의 메서드가 NaN을 반환하여 결과적으로 비정상적인 문자열이 렌더링될 수 있습니다. 날짜 유효성 검사를 추가하여 안전하게 처리해야 합니다.

Suggested change
export function fmtDate(iso?: string): string {
if (!iso) return "--";
const d = new Date(iso);
const yy = String(d.getFullYear()).slice(2);
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}.${mm}.${dd}`;
}
export function fmtDate(iso?: string): string {
if (!iso) return "--";
const d = new Date(iso);
if (isNaN(d.getTime())) return "--";
const yy = String(d.getFullYear()).slice(2);
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
return `${yy}.${mm}.${dd}`;
}

@SEMOSAN SEMOSAN deleted a comment from gemini-code-assist Bot Jun 4, 2026
@peisonger peisonger merged commit dde356e into main Jun 5, 2026
2 checks passed
@peisonger peisonger deleted the fix/record-report branch June 5, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants