Skip to content

Api: 스탬프 획득 api 연동 및 페이지 분기 처리#122

Merged
KongMezu merged 2 commits intodevelopfrom
api/#112/ai-video-api
Nov 13, 2025
Merged

Api: 스탬프 획득 api 연동 및 페이지 분기 처리#122
KongMezu merged 2 commits intodevelopfrom
api/#112/ai-video-api

Conversation

@skyblue1232
Copy link
Copy Markdown
Contributor

@skyblue1232 skyblue1232 commented Nov 13, 2025

🔥 작업 내용

  • Node([placeId].tsx) 페이지 수정
    • useGetPlaceDetail로 명소 상세 조회
    • useStampAcquire mutation 연결 (/api/stamps/{placeId}/acquire)
    • JWT 기반 사용자 인증(useUserStatus) 반영
    • getLocation()으로 실시간 위치 정보 획득 및 API 호출
    • 100m 반경 밖일 경우 PopupSet 모달 표시 ("해당 위치를 다시 확인해 주세요.")
    • hidden 값(true/false)에 따라
      /main/videoPlay?placeName=...&hidden=true 또는 /main/videoPlay?placeName=... 으로 분기 이동
  • VideoPlayPage 리팩토링
    • placeName 쿼리 기반으로 VIDEO_LOCATIONS에서 영상 매칭
    • 영상 재생 완료 시 hidden 값에 따라
      HiddenReward 또는 PostCard 페이지로 자동 이동
  • VIDEO_LOCATIONS 상수 구성
    • 총 8개 명소의 실제 영상 URL 및 설명 등록
    • id 제거 후 label 기반 매칭 구조로 단순화
  • Next.js Tooling 개선
    • 빌드 에러(jwt-decode 미설치) 수정
    • Axios 인스턴스 및 React Query 토큰 처리 유지
    • 400 에러 발생 시 Dev Overlay 대신 사용자 모달로 에러 처리

🤔 추후 작업 사항

  • getLocation() 좌표 테스트 및 반경 조건(100m) 세부 검증
  • 명소 상세 API 응답 구조에 맞춘 데이터 매핑
  • HiddenReward → PostCard 간 데이터 전달 (router.query) 개선
  • 영상 재생 중 로딩 상태 및 에러 처리 추가

🔗 이슈

  • close #

💬 PR Point (To Reviewer)

  • 위치 오차로 인한 400 응답 시 PopupSet 모달 표시 로직이 자연스러운지 확인 부탁드립니다.
  • VideoPlayPage에서 placeName 기준 매칭 구조가 정상적으로 작동하는지 검토 부탁드립니다.
  • 향후 placeId 기반 전환이 필요할지 의견 부탁드립니다.

📸 기능 시연

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 스탬프 획득 기능 추가: 위치 감지를 통한 자동 스탬프 수집 흐름
    • 비로그인 사용자 대상 로그인 팝업 표시
    • 스탬프 획득 실패 시 오류 팝업으로 사용자 안내
    • 숨겨진 엽서 지원 추가
  • 개선사항

    • 비디오 위치 조회 로직 최적화
    • 비디오 재생 종료 후 다음 화면 이동 경로 개선
    • 위치 데이터 구조 정리 및 외부 비디오 소스 통합

@skyblue1232 skyblue1232 self-assigned this Nov 13, 2025
@skyblue1232 skyblue1232 added the api api 연결 label Nov 13, 2025
@skyblue1232 skyblue1232 added the fix 버그 및 자잘한 오류 수정 label Nov 13, 2025
@skyblue1232 skyblue1232 linked an issue Nov 13, 2025 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
af-fe Ready Ready Preview Comment Nov 13, 2025 7:02pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 13, 2025

워크스루

스탬프 획득 기능을 구현합니다. 사용자가 스탬프 버튼을 클릭하면 로그인 확인 후 현재 위치를 수집하여 API로 전송하고, 응답 데이터와 함께 비디오 재생 페이지로 이동합니다. 실패 시 에러 팝업을 표시하고, 비디오 재생 페이지는 라벨 기반 위치 검색으로 변경됩니다.

변경 사항

응집 / 파일 변경 요약
스탬프 획득 API 계층
src/shared/api/main/node/api/stampAcquire.ts, src/shared/api/main/node/queries/useStampAcquire.ts, src/shared/api/main/node/types/stampAcquireTypes.ts
스탬프 획득을 위한 API 헬퍼, React Query 뮤테이션 훅, 및 요청/응답 타입 인터페이스 추가
스탬프 획득 UI 통합
src/pages/main/node/[placeId].tsx
스탬프 버튼에 클릭 핸들러를 추가하여 로그인 확인, 위치 수집, API 호출, 비디오 재생 페이지 네비게이션 및 에러 팝업 처리 구현
비디오 위치 타입 정의 변경
src/shared/types/main/videoLocation.ts
VideoLocation 인터페이스에서 id 속성 제거
비디오 위치 데이터 및 조회 로직 업데이트
src/shared/constants/main/videoLocations.ts, src/pages/main/videoPlay/index.tsx
VIDEO_LOCATIONS 데이터셋 갱신(id 제거, S3 URL로 전환), 비디오 재생 페이지에서 id 기반 조회를 라벨 기반 조회로 변경, 숨겨진 보상 플래그 처리 추가

시퀀스 다이어그램

sequenceDiagram
    participant User
    participant PlaceNode as [placeId].tsx
    participant LocationAPI as Location API
    participant StampAPI as stampAcquire API
    participant Router as Router
    participant VideoPlay as videoPlay Page

    User->>PlaceNode: Click stamp button
    
    alt Not logged in
        PlaceNode->>PlaceNode: Show login popup
        User->>Router: Close login popup
        Router->>Router: Redirect to auth
    else Already completed
        PlaceNode->>PlaceNode: No action
    else Proceed to acquire
        PlaceNode->>LocationAPI: Get current location
        
        alt Location success
            LocationAPI-->>PlaceNode: latitude, longitude
            PlaceNode->>StampAPI: POST with placeId & location
            
            alt Stamp acquired
                StampAPI-->>PlaceNode: StampAcquireResponse
                PlaceNode->>Router: Navigate to /main/videoPlay
                Router->>VideoPlay: Load with postcard data
            else Acquisition failed
                StampAPI-->>PlaceNode: Error
                PlaceNode->>PlaceNode: Show error popup
            end
        else Location failed
            LocationAPI-->>PlaceNode: Error
            PlaceNode->>PlaceNode: Log error (no crash)
        end
    end
Loading

예상 코드 리뷰 난이도

🎯 3 (보통) | ⏱️ ~20분

  • 추가 주의 필요 영역:
    • src/pages/main/node/[placeId].tsx에서 에러 처리 흐름 및 팝업 상태 관리 로직 검증
    • src/pages/main/videoPlay/index.tsx에서 라벨 기반 조회 시 undefined 경우의 폴백 처리 확인
    • VIDEO_LOCATIONS 상수 변경에 따른 기존 참조 코드의 영향도 검토
    • API 타입 정의가 백엔드 응답과 일치하는지 확인

관련 PR

제안 라벨

feat

제안 검수자

  • KongMezu

🐰 스탬프를 모아 모아, 위치에서 위치로
영상 재생하며 숨겨진 보상을 찾네
API 호출 성공, 에러도 처리하고
라벨 기반으로 재구성하니
여행 기록은 더욱 풍성해지는구나! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿 구조를 따르며 작업 내용, 추후 작업, PR Point를 충실히 작성함. 다만 이슈 번호('close #')가 미기재되어 있음.
Title check ✅ Passed 풀 리퀘스트 제목은 '스탬프 획득 API 연동 및 페이지 분기 처리'로, 변경사항의 주요 내용인 스탬프 획득 API 연동 및 페이지 라우팅 로직을 명확하게 요약하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch api/#112/ai-video-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the comment 필요한 주석 추가 및 변경 label Nov 13, 2025
@github-actions
Copy link
Copy Markdown

🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message.
Please verify that they are correct before merging.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/main/node/[placeId].tsx (1)

111-125: 뮤테이션 진행 중 버튼을 비활성화하세요.

로딩 상태 동안 버튼의 시각적 피드백과 비활성화가 필요합니다.

           <button
             aria-label={isCompleted ? '스탬프 획득 완료' : '스탬프 찍기'}
             className={cn(
               'absolute bottom-0 right-0',
               isCompleted && 'p-[2.5rem]',
+              isAcquiring && 'opacity-50 cursor-not-allowed',
             )}
             onClick={handleStampClick}
+            disabled={isAcquiring}
           >
             <Icon
               name={isCompleted ? 'Stamp' : 'PressStamp'}
               color={isCompleted ? 'pink-400' : 'gray-50'}
               size={isCompleted ? 100 : 160}
               aria-hidden='true'
             />
           </button>
🧹 Nitpick comments (2)
src/shared/api/main/node/queries/useStampAcquire.ts (1)

5-9: React Query 뮤테이션 훅이 올바르게 구현되었습니다.

타입 정의와 뮤테이션 함수 위임이 적절합니다.

디버깅과 개발자 도구 사용성 향상을 위해 mutationKey를 추가하는 것을 고려하세요:

 export const useStampAcquire = () => {
   return useMutation<StampAcquireResponse, Error, { placeId: number; body: StampAcquireRequest }>({
+    mutationKey: ['stampAcquire'],
     mutationFn: ({ placeId, body }) => postStampAcquire(placeId, body),
   });
 };
src/pages/main/node/[placeId].tsx (1)

57-57: 프로덕션 코드에서 console.log를 제거하세요.

디버깅용 로그는 프로덕션 환경에서 제거되어야 합니다.

         const placeIdNum = Number(placeId);

-        console.log('📍 현재 위치:', body);

         acquireStamp(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6773c93 and b334985.

📒 Files selected for processing (7)
  • src/pages/main/node/[placeId].tsx (4 hunks)
  • src/pages/main/videoPlay/index.tsx (1 hunks)
  • src/shared/api/main/node/api/stampAcquire.ts (1 hunks)
  • src/shared/api/main/node/queries/useStampAcquire.ts (1 hunks)
  • src/shared/api/main/node/types/stampAcquireTypes.ts (1 hunks)
  • src/shared/constants/main/videoLocations.ts (1 hunks)
  • src/shared/types/main/videoLocation.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/shared/types/main/videoLocation.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/shared/api/main/node/api/stampAcquire.ts (2)
src/shared/api/main/node/types/stampAcquireTypes.ts (2)
  • StampAcquireRequest (1-4)
  • StampAcquireResponse (14-26)
src/shared/api/instance.ts (1)
  • apiWithToken (14-17)
src/shared/api/main/node/queries/useStampAcquire.ts (2)
src/shared/api/main/node/types/stampAcquireTypes.ts (2)
  • StampAcquireResponse (14-26)
  • StampAcquireRequest (1-4)
src/shared/api/main/node/api/stampAcquire.ts (1)
  • postStampAcquire (4-13)
src/pages/main/node/[placeId].tsx (4)
src/shared/hooks/useUserStatus.ts (1)
  • useUserStatus (7-66)
src/shared/api/main/node/queries/useStampAcquire.ts (1)
  • useStampAcquire (5-9)
src/shared/utils/handleGetLocation.ts (1)
  • getLocation (1-15)
src/shared/components/set/PopupSet.tsx (1)
  • PopupSet (11-35)
src/pages/main/videoPlay/index.tsx (2)
src/shared/constants/main/videoLocations.ts (1)
  • VIDEO_LOCATIONS (3-44)
src/shared/components/main/components/video/VideoPlayer.tsx (1)
  • VideoPlayer (9-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-deploy
🔇 Additional comments (3)
src/shared/api/main/node/types/stampAcquireTypes.ts (1)

1-26: 타입 정의가 명확하고 올바릅니다.

API 계약과 일치하는 타입 정의입니다. video 필드가 옵셔널로 처리되어 있어 안전합니다.

src/shared/api/main/node/api/stampAcquire.ts (1)

4-13: API 헬퍼 구현이 올바릅니다.

apiWithToken을 사용한 인증된 요청과 타입 안전한 응답 처리가 적절합니다. placeIdnumber로 타입이 지정되어 있어 안전합니다.

src/shared/constants/main/videoLocations.ts (1)

4-44: 백엔드 placeName 값과 라벨의 데이터 정합성을 확인하세요.

검증 결과, loc.label === placeName로 정확한 문자열 매칭을 하고 있습니다. 백엔드 API에서 반환하는 postcard.placeName 값이 VIDEO_LOCATIONS의 라벨과 완벽히 일치하지 않으면 비디오가 재생되지 않습니다.

현재 코드는 일치하는 위치를 찾지 못해도 VideoPlayer가 placeholder UI를 표시하므로 크래시는 발생하지 않지만, 사용자는 비디오 대신 "Video play" 텍스트를 보게 됩니다. 다음 사항을 확인하세요:

  • 백엔드 API가 반환하는 placeName 값이 정확히 "부천아트벙커", "다솔관", "부천자유시장" 등과 일치하는지 확인
  • 공백, 대소문자, 문장 부호의 차이가 없는지 검증
  • 필요시 정규화(trim, 소문자 변환) 추가 고려

S3 URL 접근성은 정상 확인됨 (HTTP 200 OK).

Comment on lines 40 to +46
const handleStampClick = () => {
if (!isLoggedIn) {
setShowLoginPopup(true);
return;
}

if (isCompleted) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로그인 체크 전에 완료 상태를 확인하세요.

현재 로직에서는 사용자가 로그인하지 않으면 이미 완료된 스탬프인지 확인할 수 없습니다. UX 관점에서 완료 상태 확인을 먼저 수행하는 것이 더 적절합니다.

다음과 같이 순서를 변경하세요:

   const handleStampClick = () => {
+    if (isCompleted) return;
+
     if (!isLoggedIn) {
       setShowLoginPopup(true);
       return;
     }

-    if (isCompleted) return;
-
     // 위치 가져와서 API 호출
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleStampClick = () => {
if (!isLoggedIn) {
setShowLoginPopup(true);
return;
}
if (isCompleted) return;
const handleStampClick = () => {
if (isCompleted) return;
if (!isLoggedIn) {
setShowLoginPopup(true);
return;
}
// 위치 가져와서 API 호출
🤖 Prompt for AI Agents
In src/pages/main/node/[placeId].tsx around lines 40 to 46, the current
handleStampClick checks login before completion so unauthenticated users can't
see that a stamp is already completed; change the order so you first check if
(isCompleted) and return early, then check if (!isLoggedIn) to show the login
popup — keep the existing returns and side effects otherwise.

Comment on lines 49 to 87
getLocation(
(pos) => console.log('📍 현재 위치:', pos.coords),
(err) => console.error('⚠️ 위치 에러:', err.message),
(pos) => {
const body = {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
};
const placeIdNum = Number(placeId);

console.log('📍 현재 위치:', body);

acquireStamp(
{ placeId: placeIdNum, body },
{
onSuccess: (res) => {
console.log('스탬프 획득 성공:', res.data);

const { postcard } = res.data;
const { hidden } = postcard;

// 항상 videoPlay로 이동하되, hidden이 true면 쿼리로 전달
router.push({
pathname: `/main/videoPlay`,
query: {
placeName: postcard.placeName,
...(hidden ? { hidden: 'true' } : {}),
},
});
},
onError: (err) => {
console.error('스탬프 획득 실패:', err);
setShowErrorPopup(true);
},
},
);
},
(err) => {
console.error('위치 정보를 가져올 수 없습니다:', err.message);
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

에러 타입별 처리와 로딩 상태를 추가하세요.

현재 구현의 문제점:

  1. 모든 API 에러에 대해 동일한 메시지("해당 위치를 다시 확인해 주세요")를 표시합니다. 400(위치 오류), 401(인증), 500(서버) 등을 구분해야 합니다.
  2. 위치 정보 가져오기 실패 시 사용자 피드백이 없습니다.
  3. 뮤테이션 진행 중 로딩 상태가 표시되지 않아 중복 요청 가능성이 있습니다.

다음 diff를 적용하세요:

+  const [isAcquiring, setIsAcquiring] = useState(false);
+  const [errorMessage, setErrorMessage] = useState('');
   const { mutate: acquireStamp } = useStampAcquire();

   // ... 

   const handleStampClick = () => {
     if (isCompleted) return;
     
     if (!isLoggedIn) {
       setShowLoginPopup(true);
       return;
     }

+    if (isAcquiring) return; // 중복 요청 방지
+    setIsAcquiring(true);
+
     // 위치 가져와서 API 호출
     getLocation(
       (pos) => {
         const body = {
           latitude: pos.coords.latitude,
           longitude: pos.coords.longitude,
         };
         const placeIdNum = Number(placeId);

-        console.log('📍 현재 위치:', body);

         acquireStamp(
           { placeId: placeIdNum, body },
           {
             onSuccess: (res) => {
-              console.log('스탬프 획득 성공:', res.data);
+              setIsAcquiring(false);

               const { postcard } = res.data;
               const { hidden } = postcard;

               router.push({
                 pathname: `/main/videoPlay`,
                 query: {
                   placeName: postcard.placeName,
                   ...(hidden ? { hidden: 'true' } : {}),
                 },
               });
             },
             onError: (err) => {
-              console.error('스탬프 획득 실패:', err);
-              setShowErrorPopup(true);
+              setIsAcquiring(false);
+              
+              // 에러 타입별 처리
+              if (err instanceof Error) {
+                const axiosError = err as any;
+                if (axiosError.response?.status === 400) {
+                  setErrorMessage('해당 위치를 다시 확인해 주세요.');
+                } else if (axiosError.response?.status === 401) {
+                  setErrorMessage('인증이 만료되었습니다. 다시 로그인해주세요.');
+                } else {
+                  setErrorMessage('스탬프 획득에 실패했습니다. 다시 시도해주세요.');
+                }
+              } else {
+                setErrorMessage('알 수 없는 오류가 발생했습니다.');
+              }
+              setShowErrorPopup(true);
             },
           },
         );
       },
       (err) => {
-        console.error('위치 정보를 가져올 수 없습니다:', err.message);
+        setIsAcquiring(false);
+        setErrorMessage('위치 정보를 가져올 수 없습니다. 위치 권한을 확인해주세요.');
+        setShowErrorPopup(true);
       },
     );
   };

그리고 에러 팝업도 수정하세요:

       {/* 위치 에러 팝업 */}
       {showErrorPopup && (
         <PopupSet
-          text='해당 위치를 다시 확인해 주세요.'
+          text={errorMessage}
           onClose={() => setShowErrorPopup(false)}
         />
       )}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +62 to +76
onSuccess: (res) => {
console.log('스탬프 획득 성공:', res.data);

const { postcard } = res.data;
const { hidden } = postcard;

// 항상 videoPlay로 이동하되, hidden이 true면 쿼리로 전달
router.push({
pathname: `/main/videoPlay`,
query: {
placeName: postcard.placeName,
...(hidden ? { hidden: 'true' } : {}),
},
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

응답 데이터 구조를 검증하세요.

res.data.postcard 구조를 검증 없이 바로 접근하면 API 응답이 예상과 다를 때 런타임 에러가 발생할 수 있습니다.

다음과 같이 방어적 코딩을 추가하세요:

             onSuccess: (res) => {
               setIsAcquiring(false);

               const { postcard } = res.data;
+              
+              if (!postcard || !postcard.placeName) {
+                setErrorMessage('올바르지 않은 응답 형식입니다.');
+                setShowErrorPopup(true);
+                return;
+              }
+
               const { hidden } = postcard;

               router.push({
                 pathname: `/main/videoPlay`,
                 query: {
                   placeName: postcard.placeName,
                   ...(hidden ? { hidden: 'true' } : {}),
                 },
               });
             },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/main/node/[placeId].tsx around lines 62 to 76, the handler accesses
res.data.postcard and postcard.hidden without validating the API response; add
defensive checks before using these fields by verifying res and res.data are
defined and that res.data.postcard is an object (or use optional chaining and a
safe fallback), log a useful error or return early if the structure is missing,
and only call router.push when postcard data (placeName) is present; keep the
existing behavior of adding hidden: 'true' to query only when postcard.hidden is
true.

Comment on lines +8 to +12
const { placeName, hidden } = router.query;

const handleVideoEnd = () => {
const isHiddenReward = Math.random() < 0.5;
const location = VIDEO_LOCATIONS.find(
(loc) => loc.label === placeName
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

라우터 쿼리 파라미터의 타입 안전성을 강화하세요.

router.query의 값은 string | string[] | undefined 타입입니다. 현재 코드는 타입 검증 없이 placeName을 직접 사용하여 런타임 에러나 예기치 않은 동작이 발생할 수 있습니다.

다음 diff를 적용하여 타입 가드를 추가하세요:

 export default function VideoPlayPage() {
   const router = useRouter();
-  const { placeName, hidden } = router.query;
+  const placeName = typeof router.query.placeName === 'string' ? router.query.placeName : undefined;
+  const hidden = typeof router.query.hidden === 'string' ? router.query.hidden : undefined;

+  if (!placeName) {
+    return (
+      <div className="flex items-center justify-center h-screen">
+        <p>잘못된 접근입니다.</p>
+      </div>
+    );
+  }
+
   const location = VIDEO_LOCATIONS.find(
     (loc) => loc.label === placeName
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { placeName, hidden } = router.query;
const handleVideoEnd = () => {
const isHiddenReward = Math.random() < 0.5;
const location = VIDEO_LOCATIONS.find(
(loc) => loc.label === placeName
);
const placeName = typeof router.query.placeName === 'string' ? router.query.placeName : undefined;
const hidden = typeof router.query.hidden === 'string' ? router.query.hidden : undefined;
if (!placeName) {
return (
<div className="flex items-center justify-center h-screen">
<p>잘못된 접근입니다.</p>
</div>
);
}
const location = VIDEO_LOCATIONS.find(
(loc) => loc.label === placeName
);
🤖 Prompt for AI Agents
In src/pages/main/videoPlay/index.tsx around lines 8 to 12,
router.query.placeName is typed as string | string[] | undefined but is used
directly; add a type guard that ensures placeName is a single string before
using it (e.g., check typeof placeName === 'string' and handle the
array/undefined cases), then pass that validated string to VIDEO_LOCATIONS.find;
if placeName is missing or an array, handle gracefully (fallback value, early
return, or error state) so runtime errors are prevented.

Comment on lines +14 to 24
const handleVideoEnd = () => {
if (hidden === 'true') {
router.push(
`/main/HiddenReward?place=${encodeURIComponent(placeName as string)}`
);
} else {
router.push(
`/main/PostCard?place=${encodeURIComponent(placeName as string)}`
);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

비디오 종료 핸들러에서 타입 캐스팅을 제거하세요.

placeName as string 캐스팅은 이전에 제안한 타입 가드를 적용하면 불필요해집니다. 타입 가드 적용 후 이 코드는 안전하게 동작합니다.

타입 가드 적용 후 다음과 같이 수정하세요:

   const handleVideoEnd = () => {
     if (hidden === 'true') {
       router.push(
-        `/main/HiddenReward?place=${encodeURIComponent(placeName as string)}`
+        `/main/HiddenReward?place=${encodeURIComponent(placeName)}`
       );
     } else {
       router.push(
-        `/main/PostCard?place=${encodeURIComponent(placeName as string)}`
+        `/main/PostCard?place=${encodeURIComponent(placeName)}`
       );
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleVideoEnd = () => {
if (hidden === 'true') {
router.push(
`/main/HiddenReward?place=${encodeURIComponent(placeName as string)}`
);
} else {
router.push(
`/main/PostCard?place=${encodeURIComponent(placeName as string)}`
);
}
};
const handleVideoEnd = () => {
if (hidden === 'true') {
router.push(
`/main/HiddenReward?place=${encodeURIComponent(placeName)}`
);
} else {
router.push(
`/main/PostCard?place=${encodeURIComponent(placeName)}`
);
}
};
🤖 Prompt for AI Agents
In src/pages/main/videoPlay/index.tsx around lines 14 to 24, remove the
unnecessary "as string" casts in the router.push calls and use the already
type-guarded placeName directly (e.g., encodeURIComponent(placeName)) since the
earlier type guard ensures placeName is a string; update both
`/main/HiddenReward?place=...` and `/main/PostCard?place=...` calls to pass
encodeURIComponent(placeName) without casting and, if helpful for clarity,
compute a single encodedPlace const above the conditional and use it in both
branches.

Comment on lines 36 to 41
<VideoPlayer
src={location?.videoSrc}
label={location?.label}
label={location?.label || (placeName as string)}
onEnd={handleVideoEnd}
/>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

location을 찾지 못한 경우 에러 처리를 추가하세요.

VIDEO_LOCATIONS에서 일치하는 label을 찾지 못하면 locationundefined가 되어 비디오가 재생되지 않습니다. 사용자에게 명확한 피드백을 제공해야 합니다.

다음 diff를 적용하여 에러 상태를 처리하세요:

   const location = VIDEO_LOCATIONS.find(
     (loc) => loc.label === placeName
   );
+
+  if (!location) {
+    return (
+      <div className="flex flex-col items-center justify-center h-screen gap-4">
+        <p className="text-body-lg">해당 장소의 영상을 찾을 수 없습니다.</p>
+        <button
+          onClick={() => router.back()}
+          className="px-4 py-2 bg-mint-500 text-white rounded"
+        >
+          돌아가기
+        </button>
+      </div>
+    );
+  }

   const handleVideoEnd = () => {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/main/videoPlay/index.tsx around lines 36 to 41, handle the case
where location is undefined before rendering VideoPlayer: check whether location
exists and if not set an error state or render a clear fallback UI (e.g., an
error message, a toast, or a redirect) that informs the user the video could not
be found; ensure you do not pass undefined to VideoPlayer by guarding the
component render (or using a conditional early return) and include any relevant
logging for debugging.

<VideoPlayer
src={location?.videoSrc}
label={location?.label}
label={location?.label || (placeName as string)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

fallback 라벨에서 타입 캐스팅을 제거하세요.

이전에 제안한 타입 가드와 location 검증을 적용하면 이 캐스팅도 불필요합니다.

         <VideoPlayer
           src={location?.videoSrc}
-          label={location?.label || (placeName as string)}
+          label={location.label}
           onEnd={handleVideoEnd}
         />

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/main/videoPlay/index.tsx around line 38, remove the explicit cast
"(placeName as string)" from the fallback label and instead rely on the earlier
type guard/validation for location/placeName; replace the expression with a safe
fallback such as location?.label ?? placeName (or String(placeName) only if
placeName can be non-string and you intentionally want coercion), and ensure
placeName is narrowed to string earlier (or validated) so no cast is needed.

@skyblue1232 skyblue1232 changed the title api/stamp-acquire Api: 스탬프 획득 api 연동 및 페이지 분기 처리 Nov 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api api 연결 comment 필요한 주석 추가 및 변경 fix 버그 및 자잘한 오류 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[api&feat] AI 비디오 S3 저장 및 video api 연동

2 participants