-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor: map-image 로딩 처리 및 지도 에러 처리 #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
be33c86
3339da8
bf6bf0c
77e54d5
b825298
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,38 +1,63 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from 'next/router'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Image from 'next/image'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Header, LocationCard, AddressCopy } from '@/shared/components'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/shared/lib'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function LocationPage() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name, imageSrc, description, address } = router.query; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [imgLoaded, setImgLoaded] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultImage = | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'https://geulda-ai-video-bucket.s3.ap-southeast-2.amazonaws.com/default/blank_background.svg'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!router.isReady || !name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <p className="text-center mt-10">불러오는 중...</p>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const src = | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof imageSrc === 'string' ? imageSrc.trim() : defaultImage; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="relative w-full h-[100vh] overflow-auto px-[2.4rem]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Header title={String(name)} onClick={() => router.back()} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <main className="relative pt-[14.3rem] gap-[1.2rem] flex flex-col"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!imgLoaded && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-full h-[436px] bg-gray-200 animate-pulse rounded-[16px]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-[1.2rem] w-full h-[12rem] bg-gray-200 animate-pulse rounded-[20px]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-[1.2rem] w-full h-[4rem] bg-gray-200 animate-pulse rounded-[20px]" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={typeof imageSrc === 'string' ? imageSrc : '/assets/board.svg'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={src} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt={String(name)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={354} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| height={436} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full h-auto object-cover block rounded-[16px]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'w-full h-auto object-cover block rounded-[16px] transition-opacity duration-300', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| imgLoaded ? 'opacity-100' : 'opacity-0' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLoadingComplete={() => setImgLoaded(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
35
to
45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미지 로딩 실패 처리가 누락되었습니다.
다음 diff를 적용하여 에러 핸들링을 추가하세요: <Image
src={src}
alt={String(name)}
width={354}
height={436}
className={cn(
'w-full h-auto object-cover block rounded-[16px] transition-opacity duration-300',
imgLoaded ? 'opacity-100' : 'opacity-0'
)}
onLoadingComplete={() => setImgLoaded(true)}
+ onError={() => setImgLoaded(true)}
/>또는 에러 상태를 별도로 관리하여 fallback 이미지로 교체하는 방법도 고려할 수 있습니다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LocationCard | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| name={String(name)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| address={String(address)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description={String(description)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="mint" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="large" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {imgLoaded && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <LocationCard | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| name={String(name)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| address={String(address)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description={String(description)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="mint" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="large" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddressCopy variant="mint" value={String(address)} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddressCopy variant="mint" value={String(address)} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Header } from '@/shared/components'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter } from 'next/router'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import FullMap from '@/shared/components/map/result/components/FullMap'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MapPage = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -9,6 +10,12 @@ const MapPage = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? router.query.sessionId[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : router.query.sessionId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (router.isReady && !sessionId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.push('/auth'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [router, sessionId]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 리다이렉트 로직이 중복되어 있습니다.
다음 중 하나를 선택하세요: 옵션 1: useEffect만 사용 (즉시 리다이렉트) return (
<main
className="w-full h-[100vh] bg-mint-300 overflow-hidden"
role="main"
aria-label="코스 추천 지도 페이지"
>
<Header
title="코스 추천"
onClick={() => {
if (window.history.length > 1) {
router.back();
} else {
router.push('/map');
}
}}
/>
- {sessionId ? (
- <FullMap
- sessionId={sessionId}
- onError={() => router.push('/auth')}
- />
- ) : (
- <div className="flex h-full items-center justify-center text-gray-600 text-title-sm bg-gray-200">
- 코스 정보가 없습니다
- </div>
- )}
+ {sessionId && (
+ <FullMap
+ sessionId={sessionId}
+ onError={() => router.push('/auth')}
+ />
+ )}
</main>
);옵션 2: 조건부 렌더링만 사용 (메시지 표시) - useEffect(() => {
- if (router.isReady && !sessionId) {
- router.push('/auth');
- }
- }, [router, sessionId]);
-
return (📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <main | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="w-full h-[100vh] bg-mint-300 overflow-hidden" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,7 +34,10 @@ const MapPage = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {sessionId ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FullMap sessionId={sessionId} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FullMap | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessionId={sessionId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onError={() => router.push('/auth')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
인라인 함수로 전달된
+ import { useEffect, useCallback } from 'react';
const MapPage = () => {
const router = useRouter();
const sessionId = Array.isArray(router.query.sessionId)
? router.query.sessionId[0]
: router.query.sessionId;
+ const handleError = useCallback(() => {
+ router.push('/auth');
+ }, [router]);
useEffect(() => {
if (router.isReady && !sessionId) {
router.push('/auth');
}
}, [router, sessionId]);
return (
<main
className="w-full h-[100vh] bg-mint-300 overflow-hidden"
role="main"
aria-label="코스 추천 지도 페이지"
>
<Header
title="코스 추천"
onClick={() => {
if (window.history.length > 1) {
router.back();
} else {
router.push('/map');
}
}}
/>
{sessionId ? (
<FullMap
sessionId={sessionId}
- onError={() => router.push('/auth')}
+ onError={handleError}
/>
) : (
<div className="flex h-full items-center justify-center text-gray-600 text-title-sm bg-gray-200">
코스 정보가 없습니다
</div>
)}
</main>
);
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex h-full items-center justify-center text-gray-600 text-title-sm bg-gray-200"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 코스 정보가 없습니다 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,25 @@ | ||
| import { useRef, useState } from 'react'; | ||
| import { useRef, useState, useEffect } from 'react'; | ||
| import { useCourseSession } from '@/shared/api/course/queries/useCourseSession'; | ||
| import { useKakaoCourseMap } from '@/shared/hooks/kakaoMap/useKakaoCourseMap'; | ||
| import LocationBubble from '@/shared/components/container/LocationBubble'; | ||
| import type { CoursePlace } from '@/shared/api/course/types/courseSession'; | ||
|
|
||
| export default function FullMap({ sessionId }: { sessionId: string }) { | ||
| const mapRef = useRef<HTMLDivElement | null>(null); | ||
| const { data } = useCourseSession(sessionId); | ||
| interface FullMapProps { | ||
| sessionId: string; | ||
| onError?: () => void; | ||
| } | ||
|
|
||
| export default function FullMap({ sessionId, onError }: FullMapProps) { | ||
| const mapRef = useRef<HTMLDivElement | null>(null); | ||
| const { data, isError } = useCourseSession(sessionId); | ||
| const [selectedPlace, setSelectedPlace] = useState<CoursePlace | null>(null); | ||
| const [bubblePos, setBubblePos] = | ||
| useState<{ x: number; y: number } | null>(null); | ||
| const [bubblePos, setBubblePos] = useState<{ x: number; y: number } | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (isError) { | ||
| onError?.(); | ||
| } | ||
| }, [isError, onError]); | ||
|
Comment on lines
+18
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain에러 핸들링 로직에 개선이 필요합니다. 현재 구현에는 다음 문제가 있습니다:
제안 1: Ref를 사용하여 중복 호출 방지 export default function FullMap({ sessionId, onError }: FullMapProps) {
const mapRef = useRef<HTMLDivElement | null>(null);
+ const hasCalledError = useRef(false);
const { data, isError } = useCourseSession(sessionId);
const [selectedPlace, setSelectedPlace] = useState<CoursePlace | null>(null);
const [bubblePos, setBubblePos] = useState<{ x: number; y: number } | null>(null);
useEffect(() => {
- if (isError) {
+ if (isError && !hasCalledError.current) {
+ hasCalledError.current = true;
onError?.();
}
}, [isError, onError]);제안 2: 에러 타입 확인 후 조건부 처리
const { data, isError, error } = useCourseSession(sessionId);
useEffect(() => {
if (isError && error) {
// 401 에러만 인증 페이지로 리다이렉트
if (error.response?.status === 401) {
onError?.();
} else {
// 다른 에러는 사용자에게 메시지 표시
console.error('Course session error:', error);
}
}
}, [isError, error, onError]);다음 스크립트로 🏁 Script executed: #!/bin/bash
# useCourseSession의 에러 처리 및 error 객체 반환 여부 확인
rg -A 10 "export const useCourseSession" --type=tsLength of output: 725 Length of output: 601 🏁 Script executed: #!/bin/bash
# Check getCourseSession API implementation to understand error structure
rg -B 5 -A 15 "export const getCourseSession" --type=tsLength of output: 1072 Length of output: 170 🏁 Script executed: #!/bin/bash
# More specific search for apiAuth definition
rg -B 5 -A 20 "export.*apiAuth" --type=tsLength of output: 1257 Length of output: 1552 🏁 Script executed: #!/bin/bash
# Also check if apiAuth has any error interceptor
rg -A 30 "apiAuth.interceptors" src/shared/api/instance.tsLength of output: 0 중복 호출 방지를 위해 ref 도입이 필수입니다. 현재 구현에서 다음과 같이 ref를 사용하여 수정하세요: export default function FullMap({ sessionId, onError }: FullMapProps) {
const mapRef = useRef<HTMLDivElement | null>(null);
+ const hasErrorHandled = useRef(false);
const { data, isError } = useCourseSession(sessionId);
const [selectedPlace, setSelectedPlace] = useState<CoursePlace | null>(null);
const [bubblePos, setBubblePos] = useState<{ x: number; y: number } | null>(null);
useEffect(() => {
- if (isError) {
+ if (isError && !hasErrorHandled.current) {
+ hasErrorHandled.current = true;
onError?.();
}
}, [isError, onError]);추가 확인 사항:
🤖 Prompt for AI Agents |
||
|
|
||
| useKakaoCourseMap(mapRef, { | ||
| places: data?.places || [], | ||
|
|
@@ -21,7 +30,7 @@ export default function FullMap({ sessionId }: { sessionId: string }) { | |
|
|
||
| const centerX = window.innerWidth / 2; | ||
| const centerY = window.innerHeight / 2; | ||
| const offsetY = 20; | ||
| const offsetY = 20; | ||
|
|
||
| setBubblePos({ | ||
| x: centerX, | ||
|
|
@@ -47,7 +56,7 @@ export default function FullMap({ sessionId }: { sessionId: string }) { | |
| style={{ | ||
| top: bubblePos.y, | ||
| left: bubblePos.x, | ||
| transform: "translate(-50%, -100%)", | ||
| transform: 'translate(-50%, -100%)', | ||
| }} | ||
| onClick={(e) => e.stopPropagation()} | ||
| > | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trim() 후 빈 문자열 처리가 누락되었습니다.
imageSrc.trim()이 빈 문자열을 반환할 경우 이를 처리하지 않아 이미지 로딩이 실패할 수 있습니다.다음 diff를 적용하여 빈 문자열 케이스를 처리하세요:
📝 Committable suggestion
🤖 Prompt for AI Agents