Conversation
…postcard-get-api
…postcard-get-api
fix: 이미지 도메인 추가 및 관련 타입 정의
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough엽서 상세를 API 기반으로 전환하고, 이미지 방향 인식 훅, 액션 컴포넌트, 로컬 스토리지 유틸을 추가했으며 공유·저장 유틸 시그니처와 일부 레이아웃·스타일을 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Page as PostCard Page
participant Storage as localStorage util
participant API as useGetPostCardDetail
participant Hook as useImageOrientation
participant UI as FlipCard / PostCardActions
Note right of Storage `#d5f5e3`: main 페이지에서 로컬 엽서 조회
User->>Page: 접근 (main or /mypage/postcard/[id])
alt main 페이지
Page->>Storage: getPostcard()
Storage-->>Page: PostcardData | null
end
alt id가 있는 경우
Page->>API: getPostCardDetail(postcardId)
API-->>Page: { imageUrl, placeName, placeDescription }
end
Page->>Hook: imageUrl 전달
Hook-->>Page: { orientation, size }
Page->>UI: FlipCard 렌더(front/back, orientation)
User->>UI: 저장 클릭
UI->>UI: PostCardActions.handleSave(imageUrl, placeName)
UI->>Storage: savePostcard(data) (필요 시)
User->>UI: 공유 클릭
UI->>UI: PostCardActions.handleShare(imageUrl, placeName)
UI->>Navigator: navigator.canShare / navigator.share 또는 fallback
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message. |
|
🏷️ Labeler has automatically applied labels based on your PR title, branch name, or commit message. |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (9)
src/shared/utils/postcardActions.ts (1)
2-12: handleShare도 실제 엽서 정보를 전달하도록 개선이 필요합니다.현재는 현재 페이지 URL만 공유하지만, 엽서 제목이나 장소 정보를 함께 공유하면 더 나은 사용자 경험을 제공할 수 있습니다.
-export const handleShare = () => { +export const handleShare = (placeName?: string) => { if (navigator.share) { navigator.share({ title: '엽서 공유', - text: '글다', + text: placeName ? `${placeName}의 엽서 - 글다` : '글다', url: window.location.href, }); } else { alert('이 브라우저에서는 공유 기능을 지원하지 않습니다.'); } };src/shared/hooks/useImageOrientation.ts (1)
4-10: 로딩 상태 추가를 고려해보세요.현재는 이미지 로드 중인지 완료되었는지 구분할 수 없습니다. 로딩 인디케이터 표시가 필요한 경우 isLoading 상태를 추가하면 UX가 개선됩니다.
const [orientation, setOrientation] = useState< 'portrait' | 'landscape' | 'square' >('square'); const [size, setSize] = useState<{ width: number; height: number } | null>( null, ); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (!src) return; + setIsLoading(true); // ... existing code ... img.onload = () => { // ... existing code ... + setIsLoading(false); }; }, [src]); - return { orientation, size }; + return { orientation, size, isLoading };src/pages/main/PostCard.tsx (1)
25-25: className 문자열에 불필요한 공백이 있습니다.조건부 className 끝에 여러 공백이 포함되어 있어 코드 가독성을 떨어뜨립니다.
- orientation === 'portrait' ? 'mb-[5rem] ' : 'mb-[0rem] ', + orientation === 'portrait' ? 'mb-[5rem]' : 'mb-[0rem]',동일한 패턴이 31번, 40번, 55번 라인에도 반복되니 모두 수정하세요.
Also applies to: 31-31, 40-40, 55-55
src/shared/api/member.ts (2)
47-53: staleTime 설정을 추가하여 일관성을 유지하세요.다른 쿼리들(useMyPageQuery, useMyInfoQuery)은 모두 5분의 staleTime을 설정하고 있으나, 새로운 쿼리에는 설정이 누락되었습니다.
return useQuery({ queryKey: ['postcardDetail', postcardId], queryFn: () => getPostCardDetail(postcardId!), enabled: !!postcardId, + staleTime: 1000 * 60 * 5, });
40-45: API 반환 타입의 일관성을 고려하세요.fetchMyPage는
data.data를 반환하는 반면, getPostCardDetail은 전체 ApiResponse를 반환합니다. 일관성을 위해 언래핑된 데이터만 반환하는 것을 고려하세요.export const getPostCardDetail = async (postcardId: number) => { const { data } = await apiWithToken.get<ApiResponse<PostCardDetailResponse>>( `/api/postcards/${postcardId}`, ); - return data; + return data.data; };이렇게 수정하면 사용처에서
data.data.imageUrl대신data.imageUrl로 접근할 수 있어 더 간결합니다.src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
11-11: className 조합에 cn 유틸리티를 사용하세요.문자열 템플릿 대신 프로젝트 전반에서 사용하는 cn 유틸리티를 사용하면 더 일관되고 안전합니다.
+import { cn } from '@/shared/lib'; - <div className={`w-full flex justify-center ${className ?? ''}`}> + <div className={cn('w-full flex justify-center', className)}>이렇게 하면 이중 공백 문제도 자동으로 해결됩니다.
src/pages/mypage/postcard/[id].tsx (3)
17-22: 로딩 상태에 접근성 속성을 추가하세요.스크린 리더 사용자를 위해 로딩 상태에
role="status"와aria-live="polite"속성을 추가하는 것이 좋습니다.if (isLoading) return ( - <div className='flex items-center justify-center h-screen text-gray-500'> + <div + role='status' + aria-live='polite' + className='flex items-center justify-center h-screen text-gray-500' + > 엽서 정보를 불러오는 중입니다... </div> );
24-29: 에러 상태에 접근성 속성을 추가하세요.사용자에게 에러 상태를 명확히 전달하기 위해
role="alert"와aria-live="assertive"속성을 추가하는 것이 좋습니다.if (isError || !data) return ( - <div className='flex items-center justify-center h-screen text-red-500'> + <div + role='alert' + aria-live='assertive' + className='flex items-center justify-center h-screen text-red-500' + > 데이터를 불러오지 못했습니다 😢 </div> );
14-14: 폴백 이미지 경로를 상수로 분리하는 것을 고려하세요.하드코딩된 폴백 이미지 경로(
'/assets/letter_card_backh.png')를 상수로 분리하면 유지보수성이 향상됩니다.파일 상단에 상수를 정의하세요:
const DEFAULT_POSTCARD_IMAGE = '/assets/letter_card_backh.png';그리고 사용:
- const imageUrl = data?.data?.imageUrl ?? '/assets/letter_card_backh.png'; + const imageUrl = data?.data?.imageUrl ?? DEFAULT_POSTCARD_IMAGE;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
public/assets/card2.pngis excluded by!**/*.pngpublic/assets/card3.pngis excluded by!**/*.pngpublic/assets/letter_card_backh.pngis excluded by!**/*.pngpublic/assets/letter_card_backv.pngis excluded by!**/*.png
📒 Files selected for processing (9)
next.config.ts(1 hunks)src/pages/main/PostCard.tsx(1 hunks)src/pages/mypage/components/PostcardContainer.tsx(3 hunks)src/pages/mypage/postcard/[id].tsx(1 hunks)src/shared/api/member.ts(3 hunks)src/shared/components/main/components/PostCardActions/PostCardActions.tsx(1 hunks)src/shared/hooks/useImageOrientation.ts(1 hunks)src/shared/types/membertypes.ts(1 hunks)src/shared/utils/postcardActions.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
src/shared/utils/postcardActions.ts (2)
handleSave(15-21)handleShare(2-12)
src/pages/main/PostCard.tsx (3)
src/shared/hooks/useImageOrientation.ts (1)
useImageOrientation(4-29)src/shared/lib/utils.ts (1)
cn(71-73)src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
PostCardActions(9-29)
src/shared/api/member.ts (3)
src/shared/api/instance.ts (1)
apiWithToken(14-17)src/shared/types/authtypes.ts (1)
ApiResponse(1-6)src/shared/types/membertypes.ts (1)
PostCardDetailResponse(28-33)
src/pages/mypage/postcard/[id].tsx (3)
src/shared/api/member.ts (1)
useGetPostCardDetail(47-53)src/shared/hooks/useImageOrientation.ts (1)
useImageOrientation(4-29)src/shared/lib/utils.ts (1)
cn(71-73)
⏰ 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 (9)
src/shared/types/membertypes.ts (1)
28-33: 타입 정의가 명확하고 적절합니다.PostCardDetailResponse 인터페이스가 엽서 상세 정보를 잘 표현하고 있습니다. 필드명도 직관적이고 API 응답 구조와 잘 매칭됩니다.
src/pages/mypage/components/PostcardContainer.tsx (2)
82-82: 간격 조정으로 레이아웃 일관성이 향상되었습니다.8개 이하의 엽서일 때
gap-[1.2rem]을 추가하여 시각적 균형을 개선했습니다.
93-93: 테마 일관성을 위한 색상 변경이 적절합니다.빈 슬롯의 배경색을
bg-pink-100으로 변경하여 전체 핑크 테마와 통일감을 높였습니다.src/pages/main/PostCard.tsx (3)
11-17: 방향 감지 로직과 동적 props 구성이 잘 구현되었습니다.useImageOrientation 훅을 활용하여 세로/가로 이미지에 따라 적절한 크기와 뒷면 이미지를 선택하는 로직이 명확합니다.
8-9: 하드코딩된 데이터를 API 연동으로 교체 필요.현재 label과 frontSrc가 하드코딩되어 있습니다. PR 설명에 "추후 작업 사항: 메인의 포스트카드 관련 추가 작업 예정"이라고 명시되어 있으므로, 다음 작업 시 API에서 받아온 데이터로 교체해야 합니다.
다음 단계에서는 다음과 같이 API 데이터를 사용하도록 수정이 필요합니다:
const { data: postcardData } = useGetPostCardDetail(postcardId); const label = postcardData?.placeName || '장소명'; const frontSrc = postcardData?.imageUrl || '/assets/card3.png';
16-17: 이미지 파일이 정상적으로 존재합니다.검증 결과, 두 파일 모두 저장소에 존재하는 것으로 확인되었습니다:
public/assets/letter_card_backv.png✓public/assets/letter_card_backh.png✓코드의 이미지 경로가 올바르게 지정되어 있으며, 추가 조치가 필요하지 않습니다.
src/pages/mypage/postcard/[id].tsx (3)
33-36: 이미지 속성 설정 로직이 명확합니다.오리엔테이션에 따라 적절한 이미지 크기와 뒷면 이미지를 설정하는 로직이 잘 구현되어 있습니다.
60-65: FlipCard 컴포넌트 통합이 적절합니다.이미지 오리엔테이션에 따라 동적으로 계산된 props를 FlipCard에 전달하는 방식이 깔끔합니다.
74-74: API 응답에는 주소 필드가 포함되어 있지 않습니다. 이 리뷰는 부정확합니다.검증 결과,
PostCardDetailResponse는postcardId,imageUrl,placeName,placeDescription만 포함하며 주소 필드가 존재하지 않습니다. 따라서 현재address='주소 정보 없음'으로 하드코딩한 것은 불가용한 API 데이터를 대체하는 것이 아니라 API에서 애초에 제공하지 않는 정보에 대한 의도된 폴백입니다. 리뷰의 전제가 API에 주소 정보가 있을 수 있다는 가정을 기반으로 하고 있으나, 이는 타입 정의와 맞지 않습니다.Likely an incorrect or invalid review comment.
|
|
||
| images: { | ||
| domains: ['maps.googleapis.com'], | ||
| domains: ['maps.googleapis.com', 'example.com'], |
There was a problem hiding this comment.
🧩 Analysis chain
'example.com'은 실제 프로덕션 도메인으로 교체해야 합니다.
'example.com'은 RFC 2606에 정의된 예시용 도메인으로, 실제 이미지 로딩이 불가능합니다. 엽서 이미지가 로드될 실제 CDN 또는 API 서버의 도메인으로 교체해야 합니다.
실제 사용할 이미지 도메인을 확인하여 다음과 같이 수정하세요:
images: {
- domains: ['maps.googleapis.com', 'example.com'],
+ domains: ['maps.googleapis.com', 'your-actual-cdn-domain.com'],
},백엔드 API 문서 또는 이미지 URL 응답 예시를 확인하여 실제 도메인을 파악하시기 바랍니다.
🏁 Script executed:
#!/bin/bash
# Examine next.config.ts file content and context
echo "=== next.config.ts content ===" && cat next.config.ts 2>/dev/null || echo "File not found"Length of output: 1059
🏁 Script executed:
#!/bin/bash
# Search for image domain references across codebase
echo "=== Search for image domain references ===" && \
rg -n "domains|example\.com|cdn" --type=ts --type=js --type=json -C3 || echo "No matches found"Length of output: 563
🏁 Script executed:
#!/bin/bash
# Look for environment configuration files
echo "=== Search for env files ===" && \
fd -e "env" -e "env.example" -e "env.local" -e ".env*" || echo "No env files found"Length of output: 28
🏁 Script executed:
#!/bin/bash
# Search for API documentation or backend references
echo "=== Search for backend/API references ===" && \
rg -n "api|API|backend|cdn|CDN" next.config.ts -C5 || echo "No matches found"Length of output: 258
🏁 Script executed:
#!/bin/bash
# Check if there are any comments or documentation about image domains
echo "=== Search for image-related documentation ===" && \
rg -n "image|Image|IMAGE" next.config.ts -C10 || echo "No matches found"Length of output: 529
🏁 Script executed:
#!/bin/bash
# Look for test or example markers in config
echo "=== Search for test/example indicators ===" && \
rg -n "TODO|FIXME|test|example|temporary|placeholder" next.config.ts -i || echo "No markers found"Length of output: 197
🏁 Script executed:
#!/bin/bash
# Search for Image component usage from Next.js
echo "=== Search for Image component usage ===" && \
rg -n "from 'next/image'|import.*Image|<Image" --type=ts --type=tsx --type=js --type=jsx -C2 || echo "No Image component usage found"Length of output: 99
🏁 Script executed:
#!/bin/bash
# Search for image URLs or domain patterns in code
echo "=== Search for image URLs and domain patterns ===" && \
rg -n "example\.com|maps\.googleapis\.com|\.jpg|\.png|\.webp|image.*url|img.*src" --type=ts --type=tsx --type=js --type=jsx -C2 || echo "No matches found"Length of output: 94
🏁 Script executed:
#!/bin/bash
# Search for API calls or fetch patterns that might load images
echo "=== Search for API/fetch patterns ===" && \
rg -n "fetch|axios|api|API|image" --type=ts --type=tsx --type=js --type=jsx -g "!node_modules" -C2 | head -50 || echo "No matches found"Length of output: 65
🏁 Script executed:
#!/bin/bash
# Check if there's a README or documentation explaining image configuration
echo "=== Search for README or documentation ===" && \
fd -e "README*" -e "SETUP*" -e "CONFIG*" -e "docs" | xargs cat 2>/dev/null || echo "No documentation found"Length of output: 42
🏁 Script executed:
#!/bin/bash
# Search for any postcard/image related feature or component
echo "=== Search for postcard/image components ===" && \
rg -n "postcard|Postcard|image.*load|Image.*load" --type=ts --type=tsx --type=js --type=jsx -C3 || echo "No matches found"Length of output: 89
🏁 Script executed:
#!/bin/bash
# Search for Image component without type restriction
echo "=== Search for Image/img elements ===" && \
rg -n "Image|<img" -C2 src/ || echo "No matches found"Length of output: 40778
🏁 Script executed:
#!/bin/bash
# Search for all references to example.com or image domains
echo "=== Search for domain references ===" && \
rg -n "example\.com|maps\.googleapis" -C3 || echo "No matches found"Length of output: 264
🏁 Script executed:
#!/bin/bash
# Check package.json for image-related dependencies
echo "=== Check package.json ===" && \
cat package.json | grep -i "image\|next" || echo "Not found"Length of output: 161
🏁 Script executed:
#!/bin/bash
# Find all source files to understand structure
echo "=== Directory structure ===" && \
find src -type f -name "*.ts" -o -name "*.js" | head -20 || echo "No files found"Length of output: 763
🏁 Script executed:
#!/bin/bash
# Search for any image URL patterns or API calls broadly
echo "=== Search for image loading patterns ===" && \
rg -n "src=|url|URL|http" src/ -C2 | head -40 || echo "No matches found"Length of output: 30352
🏁 Script executed:
#!/bin/bash
# Search for any configuration or documentation about actual image domains/CDN
echo "=== Search for CDN/domain configuration ===" && \
rg -n "CDN|cdn|domain|DOMAIN|imageUrl|imageSrc" --type=ts -A2 -B2 src/shared/api/ || echo "No API config found"Length of output: 63
🏁 Script executed:
#!/bin/bash
# Check if there's any backend integration hints in constants or API files
echo "=== Search for backend/API structure ===" && \
cat src/shared/api/instance.ts 2>/dev/null || echo "Not found"Length of output: 2802
🏁 Script executed:
#!/bin/bash
# Look for any environment or configuration that might specify real image domains
echo "=== Search for .env or configuration examples ===" && \
find . -maxdepth 2 -type f \( -name "*.md" -o -name ".env*" -o -name "*config*" \) ! -path "./node_modules/*" 2>/dev/null | head -20Length of output: 201
🏁 Script executed:
#!/bin/bash
# Check API response types to understand what domains might be returned
echo "=== Check API/type definitions ===" && \
cat src/shared/types/membertypes.ts 2>/dev/null | head -30Length of output: 600
실제 이미지 도메인 확인 필요 - 백엔드 팀과 조율하여 실제 프로덕션 도메인으로 교체하세요.
백엔드 API에서 반환하는 profileImageUrl, eventImageUrl, imageUrl이 Next.js Image 최적화를 제대로 사용하려면 해당 도메인이 next.config.ts의 domains 배열에 등록되어야 합니다. 현재 example.com은 RFC 2606에 정의된 예시용 도메인으로 실제 이미지를 제공할 수 없습니다.
백엔드 API가 제공하는 실제 이미지 도메인(S3, CloudFlare CDN, 또는 별도 호스팅 서버)을 확인하여 다음과 같이 수정하세요:
images: {
- domains: ['maps.googleapis.com', 'example.com'],
+ domains: ['maps.googleapis.com', 'your-actual-image-domain.com'],
},백엔드 팀에 이미지 호스팅 도메인을 문의하거나 백엔드 환경 설정 문서를 확인하세요.
📝 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.
| domains: ['maps.googleapis.com', 'example.com'], | |
| domains: ['maps.googleapis.com', 'your-actual-image-domain.com'], |
🤖 Prompt for AI Agents
In next.config.ts around line 20, the domains array contains the placeholder
"example.com" which cannot serve production images; contact the backend team or
check backend env/docs to get the actual image hostnames used for
profileImageUrl, eventImageUrl, and imageUrl (e.g. S3 bucket domain,
Cloudflare/CDN host, or custom asset domain), then replace "example.com" with
those real domains in the domains array, ensure all image hostnames returned by
the API are listed, save and redeploy Next.js and verify Image optimization
works for the listed URLs.
| @@ -1,72 +1,84 @@ | |||
| import { Header, EventCard } from '@/shared/components'; | |||
| 'use client'; | |||
There was a problem hiding this comment.
Pages Router에서 'use client' 지시문은 불필요합니다.
현재 파일은 src/pages/ 디렉토리에 위치한 Pages Router 컴포넌트입니다. Pages Router에서는 모든 페이지 컴포넌트가 기본적으로 클라이언트 컴포넌트이므로 'use client' 지시문이 불필요합니다. 이 지시문은 App Router(app/ 디렉토리)에서만 필요합니다.
다음과 같이 수정하세요:
-'use client';
import { Header, FlipCard, LocationCard } from '@/shared/components';📝 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.
| 'use client'; | |
| import { Header, FlipCard, LocationCard } from '@/shared/components'; |
🤖 Prompt for AI Agents
In src/pages/mypage/postcard/[id].tsx around lines 1 to 1, remove the
unnecessary 'use client' directive at the top of the file because Pages Router
pages are already client-side; simply delete that line so the file no longer
includes the directive.
| const postcardId = Number(id); | ||
| const { data, isLoading, isError } = useGetPostCardDetail(postcardId); |
There was a problem hiding this comment.
Number(id)가 NaN을 생성할 수 있어 쿼리가 잘못 실행될 수 있습니다.
id가 undefined이거나 배열일 때 Number(id)는 NaN을 반환하는데, NaN은 truthy 값이므로 useGetPostCardDetail 훅의 enabled: !!postcardId 조건을 통과하여 잘못된 API 호출이 발생할 수 있습니다.
다음과 같이 안전하게 처리하세요:
- const postcardId = Number(id);
+ const postcardId = typeof id === 'string' ? Number(id) : undefined;
const { data, isLoading, isError } = useGetPostCardDetail(postcardId);또는 더 명시적으로:
- const postcardId = Number(id);
+ const postcardId =
+ typeof id === 'string' && !isNaN(Number(id)) ? Number(id) : undefined;
const { data, isLoading, isError } = useGetPostCardDetail(postcardId);📝 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.
| const postcardId = Number(id); | |
| const { data, isLoading, isError } = useGetPostCardDetail(postcardId); | |
| const postcardId = typeof id === 'string' ? Number(id) : undefined; | |
| const { data, isLoading, isError } = useGetPostCardDetail(postcardId); |
🤖 Prompt for AI Agents
In src/pages/mypage/postcard/[id].tsx around lines 11-12, the current const
postcardId = Number(id) can produce NaN (e.g., when id is undefined or an array)
which is truthy and can incorrectly enable the query; change parsing to safely
handle undefined/array by first checking Array.isArray(id) or typeof id !==
'string', then parse with parseInt(id, 10) or Number(id) and validate with
Number.isFinite or !Number.isNaN; set postcardId to undefined (or null) when
invalid and pass that into useGetPostCardDetail, and ensure the hook uses
enabled: typeof postcardId === 'number' && Number.isFinite(postcardId) so the
API call only runs for a valid numeric id.
| export const useGetPostCardDetail = (postcardId?: number) => { | ||
| return useQuery({ | ||
| queryKey: ['postcardDetail', postcardId], | ||
| queryFn: () => getPostCardDetail(postcardId!), | ||
| enabled: !!postcardId, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
non-null assertion 연산자 사용이 안전하지 않습니다.
Line 50에서 postcardId!를 사용하고 있지만, enabled 조건만으로는 TypeScript가 postcardId의 non-null을 보장하지 못합니다. 이론상 타입 안전성 문제가 있습니다.
더 안전한 방식으로 수정하세요:
-export const useGetPostCardDetail = (postcardId?: number) => {
+export const useGetPostCardDetail = (postcardId: number | undefined) => {
return useQuery({
queryKey: ['postcardDetail', postcardId],
- queryFn: () => getPostCardDetail(postcardId!),
+ queryFn: () => {
+ if (!postcardId) throw new Error('postcardId is required');
+ return getPostCardDetail(postcardId);
+ },
enabled: !!postcardId,
});
};📝 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.
| export const useGetPostCardDetail = (postcardId?: number) => { | |
| return useQuery({ | |
| queryKey: ['postcardDetail', postcardId], | |
| queryFn: () => getPostCardDetail(postcardId!), | |
| enabled: !!postcardId, | |
| }); | |
| }; | |
| export const useGetPostCardDetail = (postcardId: number | undefined) => { | |
| return useQuery({ | |
| queryKey: ['postcardDetail', postcardId], | |
| queryFn: () => { | |
| if (!postcardId) throw new Error('postcardId is required'); | |
| return getPostCardDetail(postcardId); | |
| }, | |
| enabled: !!postcardId, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/shared/api/member.ts around lines 47 to 53, remove the unsafe non-null
assertion on postcardId; instead make queryFn explicitly guard postcardId (e.g.,
check if postcardId == null and throw or return a rejected promise) before
calling getPostCardDetail so TypeScript knows the value is present and you get
runtime safety; keep enabled: !!postcardId as the opt-in, but perform the null
check inside queryFn to avoid using `!`.
src/shared/components/main/components/PostCardActions/PostCardActions.tsx
Outdated
Show resolved
Hide resolved
| <button | ||
| className='flex items-center justify-center w-[4.8rem] h-[4.8rem]' | ||
| onClick={handleSave} | ||
| aria-label='엽서 이미지 저장' | ||
| > | ||
| <Icon name='Save' color='gray-400' size={28} aria-hidden='true' /> | ||
| </button> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
버튼 타입을 명시하세요.
type 속성이 없는 버튼은 기본적으로 "submit"으로 동작하여, form 내부에서 사용될 경우 의도하지 않은 제출이 발생할 수 있습니다.
<button
+ type='button'
className='flex items-center justify-center w-[4.8rem] h-[4.8rem]'
onClick={handleSave}
aria-label='엽서 이미지 저장'
>두 버튼 모두 type='button'을 추가하세요.
Also applies to: 20-26
🤖 Prompt for AI Agents
In src/shared/components/main/components/PostCardActions/PostCardActions.tsx
around lines 12-18 (and similarly lines 20-26), the <button> elements lack an
explicit type so they default to "submit" and can accidentally submit
surrounding forms; update both buttons to include type='button' (i.e., add the
type attribute to each button element) to prevent unintended form submissions
while preserving existing props and handlers.
| useEffect(() => { | ||
| if (!src) return; | ||
|
|
||
| const img = new Image(); | ||
| img.src = src; | ||
| img.onload = () => { | ||
| const { width, height } = img; | ||
| setSize({ width, height }); | ||
|
|
||
| const ratio = width / height; | ||
| if (ratio > 1) setOrientation('landscape'); | ||
| else if (ratio < 1) setOrientation('portrait'); | ||
| else setOrientation('square'); | ||
| }; | ||
| }, [src]); |
There was a problem hiding this comment.
이미지 로딩 실패 시 에러 핸들링이 누락되었습니다.
img.onerror 핸들러가 없어서 이미지 로드 실패 시 'square' 상태로 남게 되며, 사용자는 실패 여부를 알 수 없습니다.
다음과 같이 에러 처리를 추가하세요:
useEffect(() => {
if (!src) return;
const img = new Image();
img.src = src;
img.onload = () => {
const { width, height } = img;
setSize({ width, height });
const ratio = width / height;
if (ratio > 1) setOrientation('landscape');
else if (ratio < 1) setOrientation('portrait');
else setOrientation('square');
};
+ img.onerror = () => {
+ console.error('Failed to load image:', src);
+ setOrientation('square');
+ setSize(null);
+ };
}, [src]);🤖 Prompt for AI Agents
In src/shared/hooks/useImageOrientation.ts around lines 12 to 26, the Image
object lacks an onerror handler so failed loads leave orientation as 'square'
and provide no failure signal; add an img.onerror handler that sets an explicit
error state (e.g., setOrientation('error') or a separate setError(true)), clears
or resets size (setSize(null) or { width: 0, height: 0 }), and optionally logs
the error; also clean up handlers in the effect cleanup to avoid leaks (nullify
onload/onerror) and ensure the effect still returns that cleanup.
경쟁 조건(race condition) 가능성이 있습니다.
src가 빠르게 변경될 경우, 이전 이미지의 onload가 나중에 실행되어 잘못된 orientation을 설정할 수 있습니다.
cleanup 함수를 추가하여 해결하세요:
useEffect(() => {
if (!src) return;
+ let cancelled = false;
const img = new Image();
img.src = src;
img.onload = () => {
+ if (cancelled) return;
const { width, height } = img;
setSize({ width, height });
const ratio = width / height;
if (ratio > 1) setOrientation('landscape');
else if (ratio < 1) setOrientation('portrait');
else setOrientation('square');
};
+ img.onerror = () => {
+ if (cancelled) return;
+ console.error('Failed to load image:', src);
+ };
+
+ return () => {
+ cancelled = true;
+ };
}, [src]);🤖 Prompt for AI Agents
In src/shared/hooks/useImageOrientation.ts around lines 12 to 26, the effect can
suffer a race condition where a previous Image's onload fires after src has
changed; add a cleanup to prevent stale onload handling by tracking the current
image: create the Image inside the effect, assign img.onload as now, and in the
returned cleanup set img.onload = null (and optionally img.src = ''), or use a
mounted/currentSrc flag to ignore onload if src no longer matches; ensure the
cleanup runs on each src change to prevent setting state from an outdated load
event.
src/shared/utils/postcardActions.ts
Outdated
| export const handleSave = () => { | ||
| const imageUrl = '/assets/Card.svg'; | ||
| const imageUrl = '/assets/Card.png'; | ||
| const link = document.createElement('a'); | ||
| link.href = imageUrl; | ||
| link.download = 'Card.svg'; | ||
| link.download = '글다.png'; | ||
| link.click(); | ||
| }; |
There was a problem hiding this comment.
handleSave 함수가 실제 엽서 이미지를 저장하지 않습니다.
현재 구현은 하드코딩된 '/assets/Card.png'만 저장하므로, 사용자가 획득한 실제 엽서가 아닌 항상 동일한 이미지만 다운로드됩니다.
handleSave와 handleShare는 현재 표시 중인 엽서의 실제 이미지 URL을 매개변수로 받아야 합니다.
다음과 같이 리팩토링을 권장합니다:
-export const handleSave = () => {
- const imageUrl = '/assets/Card.png';
+export const handleSave = (imageUrl: string, fileName?: string) => {
const link = document.createElement('a');
link.href = imageUrl;
- link.download = '글다.png';
+ link.download = fileName || '글다.png';
link.click();
};그리고 PostCardActions 컴포넌트에서 실제 이미지 URL과 파일명을 props로 받아 전달하도록 수정해야 합니다.
🤖 Prompt for AI Agents
In src/shared/utils/postcardActions.ts around lines 15 to 21, handleSave is
hardcoded to '/assets/Card.png' and a fixed filename so it never saves the
currently displayed postcard; change handleSave to accept parameters (imageUrl:
string, filename?: string) and use those values to set link.href and
link.download (fallback filename if none provided), and export the updated
signature so PostCardActions component can pass the actual image URL and desired
filename as props when invoking handleSave/handleShare.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/pages/main/PostCard.tsx (2)
11-17: 이미지 방향 감지 중 로딩 상태 처리를 고려하세요.
useImageOrientation훅은 이미지 로드 전까지 초기값으로'square'를 반환합니다. 이로 인해 이미지 로드 완료 후 레이아웃이 변경되면서 시각적 깜빡임(layout shift)이 발생할 수 있습니다.다음과 같이 로딩 상태를 추가하는 것을 권장합니다:
const { orientation, size } = useImageOrientation(frontSrc); // 이미지 크기가 결정되기 전에는 로딩 표시 if (!size) { return <LoadingSpinner />; } const imageProps = orientation === 'portrait' ? { width: 220, height: 354, backSrc: '/assets/letter_card_backv.png' } : { width: 354, height: 220, backSrc: '/assets/letter_card_backh.png' };
25-26: className 문자열의 불필요한 공백을 제거하세요.여러 className 문자열에 후행 공백이 있습니다. 기능에는 영향을 주지 않지만 코드 품질을 위해 제거하는 것이 좋습니다.
다음과 같이 수정하세요:
- orientation === 'portrait' ? 'mb-[5rem] ' : 'mb-[0rem] ', + orientation === 'portrait' ? 'mb-[5rem]' : 'mb-[0rem]',- orientation === 'portrait' ? 'mt-[6rem] ' : 'mt-[10rem] ', + orientation === 'portrait' ? 'mt-[6rem]' : 'mt-[10rem]',Also applies to: 31-32
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/pages/main/PostCard.tsx(1 hunks)src/shared/components/main/components/PostCardActions/PostCardActions.tsx(1 hunks)src/shared/utils/postcardActions.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/shared/components/main/components/PostCardActions/PostCardActions.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/main/PostCard.tsx (3)
src/shared/hooks/useImageOrientation.ts (1)
useImageOrientation(4-29)src/shared/lib/utils.ts (1)
cn(71-73)src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
PostCardActions(10-33)
⏰ 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
src/pages/main/PostCard.tsx
Outdated
| const label = '가톨릭대'; | ||
| const frontSrc = '/assets/card3.png'; |
There was a problem hiding this comment.
API 통합이 누락되었습니다.
PR 목표는 "postcard GET API 연결"이지만, 현재 코드에서는 모든 데이터가 하드코딩되어 있습니다. label과 frontSrc를 API에서 가져와야 합니다.
다음과 같은 방식으로 API를 통합하는 것을 권장합니다:
const PostCard = () => {
const { data: postcardData, isLoading, error } = useGetPostCardDetail();
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage />;
const label = postcardData?.placeName ?? '';
const frontSrc = postcardData?.imageUrl ?? '';
// ... rest of logic🤖 Prompt for AI Agents
In src/pages/main/PostCard.tsx around lines 8-9 the label and frontSrc are
hardcoded; replace them by calling the postcard GET API via
useGetPostCardDetail(), handle isLoading and error states (return a loading or
error UI), then derive label as postcardData?.placeName ?? '' and frontSrc as
postcardData?.imageUrl ?? '' so the component uses API data instead of static
values.
| export const handleSave = (imageUrl: string, placeName: string) => { | ||
| try { | ||
| const link = document.createElement('a'); | ||
| link.href = imageUrl; | ||
| link.download = `${placeName}.png`; | ||
| link.click(); | ||
| } catch (err) { | ||
| console.error('이미지 저장 실패:', err); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Cross-origin 이미지 처리 및 파일명 검증 필요
현재 구현에는 다음 문제들이 있습니다:
- CORS 이슈:
imageUrl이 cross-origin이고 적절한 CORS 헤더가 없으면, 다운로드가 실패하거나 에러 페이지가 다운로드될 수 있습니다. - 파일명 검증 누락:
placeName에 파일명으로 사용할 수 없는 문자(/,\,:,*,?,",<,>,|)가 포함되어 있을 수 있습니다. - 확장자 하드코딩: 모든 이미지를
.png로 가정하지만, 실제 이미지 형식과 다를 수 있습니다.
다음과 같이 개선하세요:
+// 파일명에서 사용할 수 없는 문자 제거
+const sanitizeFilename = (filename: string): string => {
+ return filename.replace(/[/\\:*?"<>|]/g, '_');
+};
+
+// 이미지 URL에서 확장자 추출
+const getImageExtension = (url: string): string => {
+ const match = url.match(/\.(png|jpg|jpeg|gif|webp)$/i);
+ return match ? match[1] : 'png';
+};
+
export const handleSave = (imageUrl: string, placeName: string) => {
try {
+ const sanitizedName = sanitizeFilename(placeName);
+ const extension = getImageExtension(imageUrl);
const link = document.createElement('a');
link.href = imageUrl;
- link.download = `${placeName}.png`;
+ link.download = `${sanitizedName}.${extension}`;
link.click();
} catch (err) {
console.error('이미지 저장 실패:', err);
}
};추가 고려사항: Cross-origin 이미지의 경우, fetch API로 blob을 가져와서 blob URL로 다운로드하는 방식이 더 안정적입니다:
export const handleSave = async (imageUrl: string, placeName: string) => {
try {
const sanitizedName = sanitizeFilename(placeName);
const extension = getImageExtension(imageUrl);
const response = await fetch(imageUrl);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = `${sanitizedName}.${extension}`;
link.click();
// 메모리 정리
URL.revokeObjectURL(blobUrl);
} catch (err) {
console.error('이미지 저장 실패:', err);
alert('이미지를 저장할 수 없습니다. CORS 정책을 확인해주세요.');
}
};🤖 Prompt for AI Agents
In src/shared/utils/postcardActions.ts around lines 15 to 24, the current
handleSave implementation risks CORS failures, uses a hardcoded .png extension,
and doesn't sanitize placeName; modify handleSave to be async, fetch the image
as a blob (so cross-origin images that allow fetch succeed and you avoid
downloading error pages), derive the extension from the response Content-Type
(fall back to parsing the URL or a default like png), sanitize placeName by
removing/replacing invalid filename characters (\/:*?"<>| and trimming), create
an object URL from the blob, set link.href to that blob URL and link.download to
`${sanitizedName}.${extension}`, trigger link.click(), revoke the object URL
after use, and in the catch block log the error and surface a user-friendly
message about possible CORS issues.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/shared/utils/postcardActions.ts (1)
21-30: Cross-origin 이미지 처리 및 파일명 검증이 필요합니다.이전 리뷰에서 지적된 다음 문제들이 여전히 존재합니다:
- CORS 이슈: cross-origin 이미지에서 적절한 CORS 헤더가 없으면 다운로드가 실패하거나 에러 페이지가 다운로드될 수 있습니다.
- 파일명 검증 누락:
placeName에 파일명으로 사용할 수 없는 문자(/,\,:,*,?,",<,>,|)가 포함되어 있을 수 있습니다.- 확장자 하드코딩: 모든 이미지를
.png로 가정하지만, 실제 이미지 형식과 다를 수 있습니다.다음과 같이 개선하세요:
+// 파일명에서 사용할 수 없는 문자 제거 +const sanitizeFilename = (filename: string): string => { + return filename.replace(/[/\\:*?"<>|]/g, '_').trim(); +}; + +// 이미지 URL에서 확장자 추출 +const getImageExtension = (url: string): string => { + const match = url.match(/\.(png|jpg|jpeg|gif|webp)$/i); + return match ? match[1] : 'png'; +}; + -export const handleSave = (imageUrl: string, placeName: string) => { +export const handleSave = async (imageUrl: string, placeName: string) => { try { + const sanitizedName = sanitizeFilename(placeName); + const extension = getImageExtension(imageUrl); + + const response = await fetch(imageUrl); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); - link.href = imageUrl; - link.download = `${placeName}.png`; + link.href = blobUrl; + link.download = `${sanitizedName}.${extension}`; link.click(); + + // 메모리 정리 + URL.revokeObjectURL(blobUrl); } catch (err) { console.error('이미지 저장 실패:', err); + alert('이미지를 저장할 수 없습니다. CORS 정책을 확인해주세요.'); } };
🧹 Nitpick comments (1)
src/pages/main/PostCard.tsx (1)
31-37: 로딩 메시지가 부정확합니다.현재 메시지는 "엽서 데이터를 불러오는 중..."이지만,
getPostcard()는 localStorage에서 동기적으로 데이터를 가져오므로 네트워크 로딩이 아닙니다. 더 정확한 메시지로 변경하는 것이 좋습니다.다음과 같이 수정하세요:
if (!postcard) { return ( <div className='flex items-center justify-center h-screen'> - <p className='text-gray-500 text-lg'>엽서 데이터를 불러오는 중...</p> + <p className='text-gray-500 text-lg'>엽서 데이터가 없습니다.</p> </div> ); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/pages/main/PostCard.tsx(1 hunks)src/pages/main/node/[placeId].tsx(2 hunks)src/shared/utils/postcardActions.ts(1 hunks)src/shared/utils/storage.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/shared/utils/storage.ts (2)
src/shared/utils/token.ts (4)
isBrowser(17-21)accessToken(11-15)isBrowser(6-7)isBrowser(8-9)src/shared/hooks/useRecentLogin.ts (2)
useRecentLogin(6-21)platform(15-18)
src/pages/main/PostCard.tsx (7)
src/shared/api/main/node/types/stampAcquireTypes.ts (1)
PostcardData(6-12)src/shared/utils/storage.ts (1)
getPostcard(20-29)src/shared/hooks/useImageOrientation.ts (1)
useImageOrientation(4-29)src/shared/lib/utils.ts (1)
cn(71-73)src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
PostCardActions(10-33)src/shared/components/flipCard/FlipCard.tsx (2)
FlipCardProps(12-62)FlipCardProps(5-10)src/shared/components/container/LocationCard.tsx (1)
LocationCardProps(16-119)
src/pages/main/node/[placeId].tsx (2)
src/shared/api/main/node/queries/useStampAcquire.ts (1)
useStampAcquire(5-9)src/shared/utils/storage.ts (1)
savePostcard(10-17)
🪛 Biome (2.1.2)
src/pages/main/PostCard.tsx
[error] 45-45: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.
Hooks should not be called after an early return.
For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
(lint/correctness/useHookAtTopLevel)
🔇 Additional comments (2)
src/pages/main/node/[placeId].tsx (2)
16-17: LGTM! 스탬프 획득 후 데이터 영속화 로직이 잘 구현되었습니다.import 구조 정리와
savePostcard추가로 획득한 엽서 데이터를 로컬스토리지에 저장하는 흐름이 명확합니다.
63-70: LGTM! 엽서 데이터 저장 및 네비게이션 로직이 적절합니다.스탬프 획득 성공 시 엽서 데이터를 저장하고, 필요한 데이터만 쿼리 파라미터로 전달하는 방식이 깔끔합니다.
| export const handleShare = async (imageUrl: string, title: string) => { | ||
| if (navigator.canShare && navigator.canShare({ files: [] })) { | ||
| const response = await fetch(imageUrl); | ||
| const blob = await response.blob(); | ||
| const file = new File([blob], `${title}.png`, { type: blob.type }); | ||
|
|
||
| try { | ||
| await navigator.share({ | ||
| title: `${title} 엽서`, | ||
| text: '내가 획득한 엽서를 공유합니다!', | ||
| files: [file], | ||
| }); | ||
| } catch (err) { | ||
| console.error('공유 실패:', err); | ||
| } | ||
| } else { | ||
| alert('이 브라우저에서는 공유 기능을 지원하지 않습니다.'); | ||
| alert('이 브라우저에서는 이미지 공유를 지원하지 않습니다.'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
fetch 에러 처리가 누락되었습니다.
현재 fetch와 blob 생성 로직이 try/catch 블록 밖에 있어 네트워크 에러나 CORS 에러 시 처리되지 않습니다.
다음과 같이 전체 로직을 try/catch로 감싸세요:
export const handleShare = async (imageUrl: string, title: string) => {
+ try {
if (navigator.canShare && navigator.canShare({ files: [] })) {
const response = await fetch(imageUrl);
const blob = await response.blob();
const file = new File([blob], `${title}.png`, { type: blob.type });
- try {
- await navigator.share({
- title: `${title} 엽서`,
- text: '내가 획득한 엽서를 공유합니다!',
- files: [file],
- });
- } catch (err) {
- console.error('공유 실패:', err);
- }
+ await navigator.share({
+ title: `${title} 엽서`,
+ text: '내가 획득한 엽서를 공유합니다!',
+ files: [file],
+ });
} else {
alert('이 브라우저에서는 이미지 공유를 지원하지 않습니다.');
}
+ } catch (err) {
+ console.error('이미지 공유 실패:', err);
+ alert('이미지를 공유할 수 없습니다. 네트워크 연결이나 CORS 정책을 확인해주세요.');
+ }
};📝 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.
| export const handleShare = async (imageUrl: string, title: string) => { | |
| if (navigator.canShare && navigator.canShare({ files: [] })) { | |
| const response = await fetch(imageUrl); | |
| const blob = await response.blob(); | |
| const file = new File([blob], `${title}.png`, { type: blob.type }); | |
| try { | |
| await navigator.share({ | |
| title: `${title} 엽서`, | |
| text: '내가 획득한 엽서를 공유합니다!', | |
| files: [file], | |
| }); | |
| } catch (err) { | |
| console.error('공유 실패:', err); | |
| } | |
| } else { | |
| alert('이 브라우저에서는 공유 기능을 지원하지 않습니다.'); | |
| alert('이 브라우저에서는 이미지 공유를 지원하지 않습니다.'); | |
| } | |
| }; | |
| export const handleShare = async (imageUrl: string, title: string) => { | |
| try { | |
| if (navigator.canShare && navigator.canShare({ files: [] })) { | |
| const response = await fetch(imageUrl); | |
| const blob = await response.blob(); | |
| const file = new File([blob], `${title}.png`, { type: blob.type }); | |
| await navigator.share({ | |
| title: `${title} 엽서`, | |
| text: '내가 획득한 엽서를 공유합니다!', | |
| files: [file], | |
| }); | |
| } else { | |
| alert('이 브라우저에서는 이미지 공유를 지원하지 않습니다.'); | |
| } | |
| } catch (err) { | |
| console.error('이미지 공유 실패:', err); | |
| alert('이미지를 공유할 수 없습니다. 네트워크 연결이나 CORS 정책을 확인해주세요.'); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/shared/utils/postcardActions.ts around lines 1 to 19, the fetch/blob
creation is not inside a try/catch so network/CORS or non-OK responses will
throw unhandled; wrap the async flow (fetch, response.ok check, response.blob,
File creation and navigator.share) in a try/catch, check response.ok and throw
or handle non-2xx responses before calling blob(), and in the catch log the
error and show a user-facing message (alert or toast) instead of letting the
exception bubble; keep the existing fallback for unsupported browsers.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/shared/components/container/LocationCard.tsx (1)
24-28: 사용되지 않는liked상태와 주석 처리된 핸들러 정리 제안지금은
liked상태가 어디에서도 사용되지 않고,handleLikeClick만 주석으로 남아 있습니다.
좋아요 기능을 당분간 노출하지 않는 것이 의도라면, 상태와 관련 주석을 함께 제거하거나TODO코멘트로 의도를 남겨 두면 코드가 더 깔끔해질 것 같습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
public/assets/bannerMain.svgis excluded by!**/*.svgpublic/assets/bannerMap.svgis excluded by!**/*.svg
📒 Files selected for processing (4)
src/pages/main/PostCard.tsx(1 hunks)src/shared/components/button/AddressCopy.tsx(1 hunks)src/shared/components/container/LocationCard.tsx(4 hunks)src/shared/utils/storage.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/shared/utils/storage.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/pages/main/PostCard.tsx (5)
src/shared/api/main/node/types/stampAcquireTypes.ts (1)
PostcardData(6-12)src/shared/utils/storage.ts (1)
getPostcard(16-25)src/shared/hooks/useImageOrientation.ts (1)
useImageOrientation(4-29)src/shared/lib/utils.ts (1)
cn(71-73)src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
PostCardActions(10-33)
src/shared/components/button/AddressCopy.tsx (1)
src/shared/lib/utils.ts (1)
cn(71-73)
⏰ 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/components/button/AddressCopy.tsx (1)
65-71: 민트 variant 색상 토큰 변경 잘 반영되었습니다.아이콘/텍스트를
mint-600으로 올리면서 다른 컴포넌트의 민트 계열(예: LocationCard)과 톤이 맞고 대비도 더 좋아 보입니다. 로직 변경은 없어서 동작 측면 문제는 없어 보입니다.src/shared/components/container/LocationCard.tsx (1)
60-63: mint variant 색상 계열 통일 👍주소/설명 텍스트와 MapPin 아이콘을
mint-600계열로 맞추면서, 다른 컴포넌트(예: AddressCopy)의 민트 스타일과 일관성이 생겼고 가독성도 좋아 보입니다. 기능 변경 없이 시각적 토큰만 조정된 부분이라 동작 측면 이슈는 없어 보입니다.Also applies to: 73-94
src/pages/main/PostCard.tsx (1)
18-21: 이미지 방향 기반 레이아웃 및 액션/LocationCard 연동 구조가 깔끔합니다
orientation으로 카드 사이즈와 여백을 일관되게 분기하고,FlipCard/PostCardActions/LocationCard에postcard데이터만 흘려보내는 구조가 명확해서 이후 확장/수정 시에도 이해하기 쉬울 것 같습니다. 훅 호출 위치도 모두 컴포넌트 최상단에 배치되어 현재 구조에서는 특별한 동작 이슈는 없어 보입니다.Also applies to: 30-81, 84-91
| useEffect(() => { | ||
| const stored = getPostcard(); | ||
| if (stored) setPostcard(stored); | ||
| }, []); | ||
|
|
||
| const { orientation } = useImageOrientation( | ||
| postcard?.imageUrl || '/assets/card_placeholder.png', | ||
| ); | ||
|
|
||
| if (!postcard) { | ||
| return ( | ||
| <div className='flex items-center justify-center h-screen'> | ||
| <p>엽서 데이터를 불러오는 중...</p> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
로컬스토리지에 엽서가 없을 때 무한 로딩 상태가 됩니다
useEffect에서 getPostcard()가 null을 반환하는 경우 setPostcard가 한 번도 호출되지 않아 postcard가 계속 null로 남고, 그 결과 이 컴포넌트는 영구히 "엽서 데이터를 불러오는 중..." 화면만 보여주게 됩니다.
사용자가 직접 URL로 진입하거나, 로컬스토리지가 초기화된 상태에서는 엽서를 볼 수 있는 방법이 없어져 UX 상 문제가 될 수 있습니다.
isLoading/hasLoaded 같은 플래그를 추가해 “로딩 중”과 “데이터 없음”을 분리하거나, 초기 로딩 이후에도 postcard === null인 경우에는 별도의 빈 상태(예: “엽서 데이터를 찾을 수 없습니다”)를 보여주는 분기 처리를 추가하는 것을 권장드립니다.
🤖 Prompt for AI Agents
In src/pages/main/PostCard.tsx around lines 13-28, the effect only sets postcard
when getPostcard() returns a value so postcard stays null and the component
shows a perpetual "loading" state; add an explicit loading flag (e.g. isLoading)
that starts true, set it to false after getPostcard() completes regardless of
result, and update the render to branch: while isLoading show the existing
loading UI, when not loading and postcard is null show a clear empty state
message (e.g. "엽서 데이터를 찾을 수 없습니다") with an action or link to create/return,
otherwise render the postcard; ensure useImageOrientation receives a fallback
only after confirming postcard exists or guard its call behind the
loaded/postcard check.
KongMezu
left a comment
There was a problem hiding this comment.
큰 수정사항 없어서 s3 연결만 하면 될 거 같습니다.. 수고하셨습니다.
마이페이지 감사합니다..
src/pages/main/PostCard.tsx
Outdated
| const PostCard = () => { | ||
| const label = '가톨릭대'; // 임시 | ||
| const label = '가톨릭대'; | ||
| const frontSrc = '/assets/card3.png'; |
There was a problem hiding this comment.
여기 하드 코딩 의도 하신건가요..?
(정말 포스트 카드 쪽은 거의 안봐서 물어봅니다.)
🔥 작업 내용
🤔 추후 작업 사항
🔗 이슈
PR Point (To Reviewer)
📸 피그마 스크린샷 or 기능 GIF
(작업 내역 스크린샷)
Summary by CodeRabbit
New Features
Refactor
Style
Chores
Infrastructure