Skip to content

Refactor: 디자인 수정, 이미지 다운로드 etc..#139

Merged
jjangminii merged 11 commits intodevelopfrom
refactor/#137/photo-downlod
Nov 15, 2025
Merged

Refactor: 디자인 수정, 이미지 다운로드 etc..#139
jjangminii merged 11 commits intodevelopfrom
refactor/#137/photo-downlod

Conversation

@jjangminii
Copy link
Copy Markdown
Contributor

@jjangminii jjangminii commented Nov 15, 2025

🔥 작업 내용

  • ex) 로그인 페이지 UI 및 반응형 구현

🤔 추후 작업 사항

  • ex) 소셜 로그인 연동 (카카오, 구글)

🔗 이슈

PR Point (To Reviewer)

  • ex) 로그인 입력 검증 로직 적절한지 확인 부탁드립니다.

📸 피그마 스크린샷 or 기능 GIF

(작업 내역 스크린샷)

Summary by CodeRabbit

  • Style

    • 여러 페이지 레이아웃 간격·여백·이미지 컨테이너 높이 조정, 기본 플레이스홀더·배경 이미지 교체 및 아이콘 색상 옵션 축소로 디자인 일관성 강화
    • 일부 섹션의 쌓임순서(z-index) 조정
  • New Features

    • 포스트카드 저장 동작이 서버 기반 다운로드 흐름으로 통합되어 이미지 즉시 다운로드 가능
  • Bug Fixes

    • 이미지 URL 처리와 로딩 투명도 전환 개선으로 이미지 렌더링 안정성 향상
  • Style

    • 여러 페이지 레이아웃 간격·여백·이미지 컨테이너 높이 조정, 기본 플레이스홀더·배경 이미지 교체 및 아이콘 색상 옵션 축소로 디자인 일관성 강화
    • 일부 섹션의 쌓임순서(z-index) 조정
  • New Features

    • 포스트카드 저장 동작이 서버 기반 다운로드 흐름으로 통합되어 이미지 즉시 다운로드 가능
  • Bug Fixes

    • 이미지 URL 처리와 로딩 투명도 전환 개선로 이미지 렌더링 안정성 향상

@jjangminii jjangminii self-assigned this Nov 15, 2025
@jjangminii jjangminii linked an issue Nov 15, 2025 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 15, 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 15, 2025 6:11pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Nov 15, 2025

Walkthrough

엽서 이미지 URL을 정리한 cleanedImageUrl로 통일하고 기본 폴백 이미지를 변경, PostCardActions에서 공유 버튼 제거 및 서버 경유 파일 다운로드 흐름 추가, 지도·장소 페이지 이미지 레이아웃·스타일 및 아이콘 색상 타입 축소, Boardgame UI 자산/색상 및 몇몇 레이아웃/스타일 조정이 포함됩니다.

Changes

Cohort / File(s) Summary
PostCard UI & image handling
src/pages/main/PostCard.tsx, src/pages/mypage/postcard/[id].tsx
postcard.imageUrl을 정리한 cleanedImageUrl 사용으로 이미지 참조와 props를 통일; 기본 폴백 이미지를 /assets/letter_card_backv.png로 변경; useImageOrientation 호출 위치 조정
PostCard actions & download util
src/shared/components/main/components/PostCardActions/PostCardActions.tsx, src/shared/utils/postcardActions.ts
공유 버튼 제거; 저장 버튼이 downloadFromServer(imageUrl, fileName) 호출로 변경; /api/file을 경유해 서버에서 파일을 받아 클라이언트에서 다운로드하는 유틸 추가
Server API for file proxy
src/pages/api/file.ts
신규 Next.js API 라우트 추가: 쿼리 url 검증 후 외부 리소스 fetch → blob → ArrayBuffer로 변환해 { type, arrayBuffer } 반환; FileResponseErrorResponse 인터페이스 추가
Map / Location image layout & styling
src/pages/main/node/[placeId].tsx, src/pages/map/index.tsx, src/pages/map/location/[placeId].tsx
히어로 이미지 컨테이너 높이 확장(256px → 43rem), 장소 이미지에 section[aria-label="장소 대표 이미지"] 래퍼 추가 및 로딩/opacity 전환 도입; JSX 따옴표·포맷·패딩 등 스타일/마크업 정리
Boardgame UI asset & color
src/shared/components/main/components/board/Boardgame.tsx
배경 에셋 경로 변경(/assets/background_.svg/assets/background.png) 및 스탬프 아이콘 색상 토큰을 whitegray-50으로 변경
Icon color type change
src/shared/icons/components/icon.tsx
IconColor 유니온 타입에서 'white' 옵션 제거 (타입 축소)
MyPage layering tweak
src/pages/mypage/index.tsx
"저장한 엽서" 섹션 z-index 값을 100 → 10으로 낮춤

Sequence Diagram(s)

sequenceDiagram
    participant Client as 브라우저 (PostCardActions)
    participant Server as Next.js API (/api/file)
    participant Remote as 외부 파일 서버

    rect rgb(220,245,255)
    Client->>Server: GET /api/file?url=<fileUrl>
    note right of Server: 서버에서 외부 리소스 fetch 및 변환
    Server->>Remote: fetch(<fileUrl>)
    Remote-->>Server: Response (binary, content-type)
    Server->>Server: response.blob() → arrayBuffer → number[]
    Server-->>Client: 200 { type, arrayBuffer }
    end

    rect rgb(235,255,230)
    Client->>Client: JSON → Blob 생성\nObjectURL 생성 → anchor.click() → 다운로드 시작
    Client->>Client: URL.revokeObjectURL()
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 깊게 검토할 항목:
    • src/pages/api/file.ts: 외부 URL 검증, 에러 응답 일관성, ArrayBuffer 직렬화 안전성
    • src/shared/utils/postcardActions.tsPostCardActions: MIME 타입 처리, 파일명 인코딩/인젝션 방지, 사용자 흐름(로딩/오류) 고려
    • src/shared/icons/components/icon.tsx: 'white' 제거로 인한 사용처 영향(스타일 깨짐 여부)
    • 이미지 레이아웃 변경 파일(src/pages/*/[placeId].tsx): 접근성(aria-label) 및 반응형/스타일 영향

Possibly related PRs

Suggested labels

refactor, api

Suggested reviewers

  • KongMezu
  • skyblue1232

🐰 엽서 한 장 다듬어 들고,
뒤표지 바꿔 끼우며 깡충댄다.
공유는 줄고 서버로 건너가,
한 번의 클릭에 그림이 내려오네.
토끼가 박수, 당근으로 축하해요 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿 구조만 유지하고 실제 내용이 채워져 있지 않습니다. 모든 섹션에 예시 텍스트만 포함되어 있고 실제 작업 내용, 검토 포인트, 이슈 번호가 기재되지 않았습니다. 실제 작업 내용을 작성하세요. '이미지 URL 정규화', '파일 다운로드 API 추가', '아이콘 색상 토큰 제거' 등 변경사항을 설명하고, 검토 포인트와 관련 이슈 번호를 추가해주세요.
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.
Title check ❓ Inconclusive PR 제목이 실제 변경사항을 제대로 반영하지 못합니다. 제목은 '디자인 수정'이지만 실제로는 이미지 다운로드 기능 추가, API 라우트 생성, 아이콘 색상 변경 등 구체적인 기능 변경이 포함되어 있습니다. 더 구체적인 PR 제목으로 변경하세요. 예: 'Refactor: 이미지 다운로드 기능 추가 및 UI 스타일 개선' 또는 'Refactor: PostCard 이미지 처리 및 다운로드 기능 구현'
✨ 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 refactor/#137/photo-downlod

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
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.

@github-actions github-actions bot added the comment 필요한 주석 추가 및 변경 label Nov 15, 2025
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: 2

Caution

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

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

1-1: Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있습니다. 'use client' 지시어는 App Router 전용이므로 제거해야 합니다.

Based on learnings

다음 diff를 적용하세요:

-'use client';
 import { useState, useEffect } from 'react';
src/shared/components/main/components/board/Boardgame.tsx (1)

1-1: Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있으므로 'use client' 지시어를 제거해야 합니다.

Based on learnings

-'use client';
 import Image from 'next/image';
src/pages/mypage/index.tsx (1)

1-1: Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있으므로 'use client' 지시어를 제거해야 합니다.

Based on learnings

-'use client';
-
 import React, { useEffect } from 'react';
src/pages/main/PostCard.tsx (1)

1-1: Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있으므로 'use client' 지시어를 제거해야 합니다.

Based on learnings

-'use client';
 import { useEffect, useState } from 'react';
src/pages/mypage/postcard/[id].tsx (1)

1-1: Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있으므로 'use client' 지시어를 제거해야 합니다.

Based on learnings

-'use client';
 import { Header, FlipCard, LocationCard } from '@/shared/components';
🧹 Nitpick comments (1)
src/pages/main/node/[placeId].tsx (1)

129-139: 주요 이미지에서 priority 속성 제거 재고 필요

히어로 이미지에서 priority 속성을 제거하면 LCP(Largest Contentful Paint) 성능에 부정적인 영향을 줄 수 있습니다. 이 이미지가 above-the-fold 콘텐츠인 경우 우선순위 로딩이 권장됩니다.

성능 최적화를 위해 priority 속성을 다시 추가하는 것을 고려하세요:

 <Image
   src={imageUrl || '/assets/board.svg'}
   alt={placeName}
   fill
+  priority
   onLoadingComplete={() => setImageLoaded(true)}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2880f95 and 4600471.

⛔ Files ignored due to path filters (2)
  • public/assets/background.png is excluded by !**/*.png
  • src/shared/icons/source/Stamp.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • src/pages/main/PostCard.tsx (2 hunks)
  • src/pages/main/node/[placeId].tsx (1 hunks)
  • src/pages/map/index.tsx (2 hunks)
  • src/pages/map/location/[placeId].tsx (1 hunks)
  • src/pages/mypage/index.tsx (1 hunks)
  • src/pages/mypage/postcard/[id].tsx (2 hunks)
  • src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1 hunks)
  • src/shared/components/main/components/board/Boardgame.tsx (2 hunks)
  • src/shared/icons/components/icon.tsx (0 hunks)
💤 Files with no reviewable changes (1)
  • src/shared/icons/components/icon.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-31T17:34:04.352Z
Learnt from: jjangminii
Repo: geulDa/FE PR: 64
File: src/pages/map/result/components/ResultMap.tsx:3-3
Timestamp: 2025-10-31T17:34:04.352Z
Learning: The geulDa/FE project uses Next.js Pages Router (not App Router), so components should import useRouter from 'next/router' and should not use the 'use client' directive.

Applied to files:

  • src/pages/map/index.tsx
🧬 Code graph analysis (4)
src/shared/components/main/components/board/Boardgame.tsx (2)
src/shared/components/main/components/stampBoard/Stamp.tsx (1)
  • StampProps (10-35)
src/pages/main/index.tsx (1)
  • MainPage (8-68)
src/pages/mypage/postcard/[id].tsx (2)
src/shared/api/member.ts (1)
  • useGetPostCardDetail (45-51)
src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
  • PostCardActions (10-25)
src/pages/map/index.tsx (2)
src/shared/hooks/useCourseSelection.ts (1)
  • useCourseSelection (3-16)
src/shared/components/header/ControlBar.tsx (1)
  • ControlBar (27-75)
src/pages/map/location/[placeId].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 (8)
src/pages/main/node/[placeId].tsx (1)

123-123: 히어로 이미지 높이 대폭 증가 검토 필요

이미지 컨테이너 높이가 256px에서 43rem(약 688px)으로 크게 증가했습니다. 모바일 뷰포트에서 이미지가 화면을 과도하게 차지하여 주요 콘텐츠가 첫 화면에서 밀려날 수 있습니다.

다양한 기기에서 레이아웃을 테스트하여 사용자 경험이 적절한지 확인해 주세요.

src/shared/components/main/components/board/Boardgame.tsx (2)

27-27: 배경 이미지 형식 변경으로 인한 파일 크기 증가 확인 필요

배경 이미지가 SVG에서 PNG로 변경되었습니다. PNG 파일이 일반적으로 SVG보다 크므로 번들 크기와 로딩 성능에 영향을 줄 수 있습니다.

PNG 파일 크기를 확인하고 성능 영향을 검토해 주세요. 가능하다면 최적화된 WebP 형식도 고려할 수 있습니다.


65-65: 아이콘 색상 변경 확인

Stamp 아이콘 색상이 'white'에서 'gray-50'으로 변경되었습니다. 이는 IconColor 타입에서 'white' 옵션이 제거된 것과 일치합니다.

src/pages/map/index.tsx (1)

16-17: 일관된 코드 스타일 적용 확인

따옴표 스타일을 작은따옴표로 통일하고 패딩 값을 조정한 포맷팅 변경사항입니다. 코드베이스 전반의 일관성을 향상시킵니다.

Also applies to: 50-79

src/pages/map/location/[placeId].tsx (1)

34-51: 접근성 및 사용자 경험 개선

이미지 섹션에 aria-label을 추가하고 불투명도 전환 효과를 구현하여 접근성과 사용자 경험이 개선되었습니다.

src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)

3-23: 공유 기능 제거 확인

PostCardActions 컴포넌트에서 공유(Export) 버튼이 제거되고 저장(Save) 기능만 남았습니다. 이것이 의도된 디자인 변경인지 확인해 주세요.

이 변경으로 인해 사용자가 엽서를 공유할 수 있는 다른 방법이 제공되는지 확인해 주세요.

src/pages/main/PostCard.tsx (1)

19-19: 기본 이미지 및 주석 업데이트 확인

기본 이미지 경로가 일관되게 업데이트되었고, 주석이 변경된 기능(공유 버튼 제거)을 정확하게 반영합니다.

Also applies to: 71-71

src/pages/mypage/postcard/[id].tsx (1)

7-7: PostCardActions 통합 및 기본 이미지 업데이트 확인

PostCardActions 컴포넌트가 올바르게 통합되었고, 기본 이미지가 다른 엽서 관련 페이지와 일관되게 업데이트되었습니다.

Also applies to: 15-15, 68-68

<section
aria-label='저장한 엽서'
className='w-full mt-[1.8rem] relative z-[100]'
className='w-full mt-[1.8rem] relative z-[10]'
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

🧩 Analysis chain

z-index 감소로 인한 레이어링 충돌 가능성 확인 필요

z-index가 100에서 10으로 크게 감소했습니다. 다른 UI 요소(예: 팝업, 모달, BottomNav 등)가 z-index 10 이상을 사용하는 경우 저장한 엽서 섹션이 가려질 수 있습니다.

다음 스크립트로 z-index 사용 현황을 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# z-index 값이 10 이상인 요소들을 검색하여 충돌 가능성 확인
rg -n "z-\[([1-9]\d+|[2-9]\d)\]" --type tsx --type ts -C 2

Length of output: 27


🏁 Script executed:

#!/bin/bash
# 검색: tsx/ts/css 파일에서 z-index 사용 현황 확인
# Tailwind z-index 클래스 및 스타일 속성 검색

echo "=== Tailwind z-index 클래스 검색 (z-[숫자] 형식) ==="
rg -n "z-\[[0-9]+\]" -g "*.tsx" -g "*.ts" -g "*.css" -C 1

echo ""
echo "=== z-index CSS 속성 및 스타일 검색 ==="
rg -n "z-index\s*[:=]" -g "*.tsx" -g "*.ts" -g "*.css" -C 1

echo ""
echo "=== 특히 z-index가 10 이상인 값 검색 ==="
rg -n "z-\[([1-9][0-9]|[0-9]{3,})\]" -g "*.tsx" -g "*.ts" -C 1

Length of output: 3939


z-index 감소로 인한 레이어링 충돌 위험 확인됨

z-index를 100에서 10으로 감소시킨 것이 실제로 문제를 야기할 수 있습니다. 코드베이스 전체에서 다음 UI 요소들이 더 높은 z-index를 사용하고 있습니다:

  • PopupSet (z-[100], fixed)
  • Header (z-[100], fixed)
  • Overlay (z-[99], fixed)
  • LocationBubbleSet (z-[50], fixed)

저장한 엽서 섹션이 relative z-[10]으로 설정되어 있으므로, 이들 fixed 요소(특히 z-[100]인 Popup과 Header)가 표시될 때 가려질 수 있습니다. z-index를 적절히 조정하거나 레이어링 구조를 재검토하는 것을 권장합니다.

🤖 Prompt for AI Agents
In src/pages/mypage/index.tsx around line 131, the saved-postcard section is set
to className='w-full mt-[1.8rem] relative z-[10]' which is likely to be visually
obscured by fixed elements using higher z-indexes (PopupSet z-[100], Header
z-[100], Overlay z-[99], LocationBubbleSet z-[50]); update the z-index to sit
above those fixed elements or adjust stacking context: either raise the z value
to >=101 (e.g., z-[101]) if this section must overlay fixed elements, or
refactor so the fixed elements are behind (lower their z) or move the
saved-postcard out of a competing stacking context (avoid creating unwanted
stacking contexts on parents); after change, verify visually that PopupSet,
Header, Overlay, and LocationBubbleSet still behave as intended across
breakpoints.

@@ -1,6 +1,6 @@
'use client';
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

Pages Router에서 'use client' 지시어 제거 필요

이 프로젝트는 Next.js Pages Router를 사용하고 있으므로 'use client' 지시어를 제거해야 합니다.

Based on learnings

-'use client';
 import { Icon } from '@/shared/icons';
📝 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
'use client';
import { Icon } from '@/shared/icons';
🤖 Prompt for AI Agents
In src/shared/components/main/components/PostCardActions/PostCardActions.tsx
lines 1-1, remove the top-level 'use client' directive; ensure the file does not
rely on client-only hooks or browser APIs (useState, useEffect, window,
document). If it currently uses client features, move those bits into a separate
client-only child component or lift state/handlers to a parent client component,
then export PostCardActions as a server component without the directive; run the
app/build to verify no client-only usage errors remain.

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: 4

🧹 Nitpick comments (1)
src/pages/api/file.ts (1)

9-11: 응답 타입에서 any 사용을 제거하세요.

NextApiResponse<FileResponse | any>에서 any를 사용하면 타입 안전성이 저하됩니다. 오류 응답에 대한 명시적 타입을 정의하세요.

다음 diff를 적용하세요:

+interface ErrorResponse {
+  error: string;
+}
+
 export default async function handler(
   req: NextApiRequest,
-  res: NextApiResponse<FileResponse | any>,
+  res: NextApiResponse<FileResponse | ErrorResponse>,
 ) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4600471 and 5b8b479.

📒 Files selected for processing (3)
  • src/pages/api/file.ts (1 hunks)
  • src/shared/components/main/components/PostCardActions/PostCardActions.tsx (2 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
⏰ 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 (1)
src/shared/utils/postcardActions.ts (1)

32-52: 대용량 파일 처리 시 성능 및 메모리 문제를 검토하세요.

현재 구현은 전체 파일을 JSON 배열로 직렬화/역직렬화하므로 대용량 파일(예: 수 MB 이상)의 경우 메모리 사용량이 크고 성능이 저하될 수 있습니다. /api/file 엔드포인트가 바이너리 스트림을 직접 프록시하도록 변경하는 것을 고려해보세요.

다운로드 대상 파일의 예상 크기를 확인하고, 대용량 파일(>5MB)을 처리해야 하는 경우 더 효율적인 스트리밍 방식으로 리팩토링을 권장합니다.

Comment on lines +27 to +33
const blob = await response.blob();
const buffer = await blob.arrayBuffer();

res.status(200).send({
type: blob.type,
arrayBuffer: Array.from(new Uint8Array(buffer)),
});
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

메모리 고갈 및 성능 저하 위험이 있습니다.

전체 파일을 메모리에 로드하고 JSON 배열로 직렬화하므로 대용량 파일 다운로드 시 서버 메모리가 고갈되고 응답 시간이 크게 증가합니다. 또한 Content-Length 제한이 없어 악의적인 요청으로 서버가 다운될 수 있습니다.

다음과 같이 스트리밍 방식으로 리팩토링하는 것을 권장합니다:

// 파일 크기 제한 (예: 10MB)
const MAX_FILE_SIZE = 10 * 1024 * 1024;

const response = await fetch(urlObj.toString());

if (!response.ok) {
  return res.status(500).json({ error: 'Failed to fetch file' });
}

const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength) > MAX_FILE_SIZE) {
  return res.status(413).json({ error: 'File too large' });
}

// 바이너리 스트림으로 직접 전달
res.setHeader('Content-Type', response.headers.get('content-type') || 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment');

const buffer = await response.arrayBuffer();
res.status(200).send(Buffer.from(buffer));

그리고 클라이언트 측 downloadFromServer 함수도 바이너리 응답을 처리하도록 수정:

export async function downloadFromServer(url: string, fileName: string) {
  try {
    const resp = await fetch(`/api/file?url=${encodeURIComponent(url)}`);
    if (!resp.ok) {
      throw new Error(`파일 다운로드 실패: ${resp.status}`);
    }
    
    const blob = await resp.blob();
    const blobUrl = window.URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.href = blobUrl;
    a.download = fileName;
    a.click();
    
    setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
  } catch (err) {
    console.error('다운로드 실패:', err);
  }
}
🤖 Prompt for AI Agents
In src/pages/api/file.ts around lines 27 to 33, the handler loads the entire
fetched file into memory and serializes it as a JSON array (Uint8Array ->
Array.from), risking memory exhaustion and performance degradation for large or
malicious files; change to a streaming/binary response: enforce a max file size
by checking Content-Length header and return 413 if exceeded, validate
response.ok and return 500 on fetch failure, forward content-type and
content-disposition headers, and pipe or send the binary body directly (as a
Buffer/stream) instead of converting to JSON so the server does not buffer the
whole file in memory and the client can receive a proper Blob for download.

Comment on lines +34 to +35
const resp = await fetch(`/api/file?url=${encodeURIComponent(url)}`);
const json = await resp.json();
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

응답 상태 검증이 누락되었습니다.

resp.json()을 호출하기 전에 응답 상태를 확인해야 합니다. API 요청이 실패한 경우 JSON 파싱 오류가 발생할 수 있습니다.

다음 diff를 적용하여 응답 상태를 검증하세요:

   const resp = await fetch(`/api/file?url=${encodeURIComponent(url)}`);
+  if (!resp.ok) {
+    throw new Error(`파일 다운로드 실패: ${resp.status}`);
+  }
   const json = await resp.json();
📝 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 resp = await fetch(`/api/file?url=${encodeURIComponent(url)}`);
const json = await resp.json();
const resp = await fetch(`/api/file?url=${encodeURIComponent(url)}`);
if (!resp.ok) {
throw new Error(`파일 다운로드 실패: ${resp.status}`);
}
const json = await resp.json();
🤖 Prompt for AI Agents
In src/shared/utils/postcardActions.ts around lines 34-35, the code calls
resp.json() without verifying the HTTP response status; add a response status
check (e.g., if (!resp.ok) { const errText = await resp.text(); throw new
Error(`Fetch /api/file failed: ${resp.status} ${resp.statusText} - ${errText}`);
}) before calling resp.json() so failed responses are handled and do not cause
JSON parse errors.

Comment on lines +41 to +48
const blobUrl = window.URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
a.click();

URL.revokeObjectURL(blobUrl);
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

URL 해제 타이밍 문제 가능성이 있습니다.

a.click() 직후 즉시 URL.revokeObjectURL()을 호출하면, 브라우저가 다운로드를 시작하기 전에 URL이 해제되어 다운로드가 실패할 수 있습니다. 특히 대용량 파일의 경우 이 문제가 발생할 가능성이 높습니다.

다음 diff를 적용하여 약간의 지연 후 URL을 해제하세요:

   a.click();

-  URL.revokeObjectURL(blobUrl);
+  setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
📝 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 blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
a.click();
URL.revokeObjectURL(blobUrl);
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
a.click();
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
🤖 Prompt for AI Agents
In src/shared/utils/postcardActions.ts around lines 41 to 48, revokeObjectURL is
called immediately after a.click(), which can cancel the download before the
browser starts fetching the blob; add a short delay before calling
URL.revokeObjectURL (for example wrap revokeObjectURL in setTimeout with a small
timeout like 500–1000ms) or revoke the URL in a safe callback (e.g., after
navigation/focus or using a.download link click completion handler) so the
download has time to start before the blob URL is released.

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: 1

🧹 Nitpick comments (1)
src/pages/main/PostCard.tsx (1)

68-82: 이미지 표시와 다운로드에 동일한 cleanedImageUrl을 사용하는 점이 좋습니다

FlipCardfrontSrcPostCardActionsimageUrl 모두 cleanedImageUrl를 공유하도록 정리한 부분은, 화면에 보이는 이미지와 실제 저장되는 파일이 일치하도록 해줘서 일관성이 좋습니다.

위에서 제안한 cleanedImageUrl 보정만 반영되면, 표시/다운로드 경로 모두 한 번에 안정성이 올라갈 것 같습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b8b479 and d980b51.

📒 Files selected for processing (2)
  • src/pages/main/PostCard.tsx (4 hunks)
  • src/pages/mypage/postcard/[id].tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/mypage/postcard/[id].tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/main/PostCard.tsx (2)
src/shared/hooks/useImageOrientation.ts (1)
  • useImageOrientation (4-29)
src/shared/components/main/components/PostCardActions/PostCardActions.tsx (1)
  • PostCardActions (10-25)
⏰ 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 (1)
src/pages/main/PostCard.tsx (1)

34-34: null 가드 이후 구조 분해 패턴이 안전합니다

if (!postcard) return으로 null 가드를 한 뒤 const { placeName, description, address } = postcard;를 수행하는 패턴은 타입·런타임 모두 안전하고, 읽기도 좋습니다. 현재 형태 그대로 유지해도 괜찮아 보입니다.

Comment on lines +18 to 25
// 🔥 postcard가 null이어도 안전하게 처리
const cleanedImageUrl = (
postcard?.imageUrl || '/assets/letter_card_backv.png'
).trim();

// 🔥 훅은 조건문 밖에서 항상 호출!
const { orientation } = useImageOrientation(cleanedImageUrl);

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

공백만 있는 imageUrl에서 폴백이 적용되지 않을 수 있습니다

(postcard?.imageUrl || '/assets/letter_card_backv.png').trim() 패턴에서는 imageUrl' ' 같은 공백 문자열일 때 문제가 생깁니다.

  • ' '는 truthy라서 ||에서 그대로 선택되고,
  • trim() 이후에는 ''가 되어
    • useImageOrientation('') → 훅은 아무 것도 하지 않고 orientation은 기본값 square 유지
    • FlipCardfrontSrc='' → 깨진 이미지
    • PostCardActionsdownloadFromServer('', ...) → 다운로드 실패 가능

공백·빈 문자열 모두에 대해 폴백이 동작하도록, 먼저 trim()으로 정리한 뒤 폴백을 적용하는 쪽이 안전합니다.

-  const cleanedImageUrl = (
-    postcard?.imageUrl || '/assets/letter_card_backv.png'
-  ).trim();
+  const rawImageUrl = postcard?.imageUrl?.trim();
+  const cleanedImageUrl =
+    rawImageUrl && rawImageUrl.length > 0
+      ? rawImageUrl
+      : '/assets/letter_card_backv.png';

이렇게 하면 실제 URL이 공백/빈 문자열인 경우에도 항상 폴백 이미지를 사용하게 되어, 보기/저장 둘 다 안정적으로 동작합니다.

📝 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
// 🔥 postcard가 null이어도 안전하게 처리
const cleanedImageUrl = (
postcard?.imageUrl || '/assets/letter_card_backv.png'
).trim();
// 🔥 훅은 조건문 밖에서 항상 호출!
const { orientation } = useImageOrientation(cleanedImageUrl);
// 🔥 postcard가 null이어도 안전하게 처리
const rawImageUrl = postcard?.imageUrl?.trim();
const cleanedImageUrl =
rawImageUrl && rawImageUrl.length > 0
? rawImageUrl
: '/assets/letter_card_backv.png';
// 🔥 훅은 조건문 밖에서 항상 호출!
const { orientation } = useImageOrientation(cleanedImageUrl);
🤖 Prompt for AI Agents
In src/pages/main/PostCard.tsx around lines 18 to 25, the current expression
(postcard?.imageUrl || '/assets/letter_card_backv.png').trim() fails to fall
back when imageUrl is whitespace-only; instead, first normalize by trimming the
possibly undefined value and then apply the fallback so whitespace becomes empty
and triggers the default. Change to trim the source (e.g. (postcard?.imageUrl ??
'').trim()) and then use || '/assets/letter_card_backv.png' to produce
cleanedImageUrl, keeping the useImageOrientation(cleanedImageUrl) hook call
outside any conditional.

@jjangminii jjangminii changed the title Refactor: 디자인 수정 Refactor: 디자인 수정, 이미지 다운로드 etc.. Nov 15, 2025
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: 2

♻️ Duplicate comments (2)
src/pages/api/file.ts (2)

18-24: 중요: SSRF 취약점이 여전히 존재합니다.

이전 리뷰에서 지적된 SSRF(Server-Side Request Forgery) 취약점이 수정되지 않았습니다. 클라이언트가 제공한 임의의 URL로 서버가 요청을 수행하므로, 공격자가 내부 네트워크 리소스나 메타데이터 서비스에 접근할 수 있습니다.

허용된 도메인 화이트리스트를 적용하여 신뢰할 수 있는 호스트(예: S3 버킷)만 접근 가능하도록 제한해야 합니다. 이전 리뷰 코멘트를 참고하세요.


30-36: 중요: 메모리 고갈 위험이 여전히 존재합니다.

이전 리뷰에서 지적된 메모리 문제가 수정되지 않았습니다. 전체 파일을 메모리에 로드하고 JSON 배열로 변환하므로 대용량 파일 다운로드 시 서버 메모리가 고갈될 수 있습니다.

스트리밍 방식으로 리팩토링하고 파일 크기 제한을 추가해야 합니다. 이전 리뷰 코멘트의 제안된 구현을 참고하세요.

🧹 Nitpick comments (1)
src/pages/api/file.ts (1)

13-41: 인증 및 요청 제한 추가를 고려하세요.

현재 API 엔드포인트는 인증 없이 누구나 접근할 수 있습니다. 프록시로 악용되거나 과도한 요청으로 서버 리소스가 소진될 수 있습니다.

다음 개선 사항을 고려하세요:

  1. 인증 추가: 세션 또는 API 키 검증
  2. Rate Limiting: Next.js middleware 또는 라이브러리(예: @upstash/ratelimit) 사용
  3. 모니터링: 접근하는 URL 로깅으로 의심스러운 활동 추적

예시 (rate limiting with middleware):

// middleware.ts 또는 API 핸들러 내부
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 분당 10회
});

// handler 내부에서
const identifier = req.headers['x-forwarded-for'] || 'anonymous';
const { success } = await ratelimit.limit(identifier);

if (!success) {
  return res.status(429).json({ error: 'Too many requests' });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d980b51 and 17c2e02.

📒 Files selected for processing (1)
  • src/pages/api/file.ts (1 hunks)
⏰ 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

Comment on lines +13 to +16
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<FileResponse | ErrorResponse>,
) {
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

HTTP 메서드 제한을 추가하세요.

현재 핸들러는 모든 HTTP 메서드(GET, POST, PUT 등)를 허용합니다. 읽기 전용 프록시 엔드포인트이므로 GET 메서드만 허용해야 합니다.

다음 diff를 적용하여 GET 메서드만 허용하도록 제한하세요:

 export default async function handler(
   req: NextApiRequest,
   res: NextApiResponse<FileResponse | ErrorResponse>,
 ) {
   try {
+    if (req.method !== 'GET') {
+      return res.status(405).json({ error: 'Method not allowed' });
+    }
+
     const { url } = req.query;
🤖 Prompt for AI Agents
In src/pages/api/file.ts around lines 13 to 16, the API handler currently
accepts all HTTP methods; restrict it to only allow GET by checking req.method
at the start of the handler and returning a 405 Method Not Allowed (with an
Allow: GET header) for any non-GET requests, then continue existing logic only
for GET; implement the method check as the first operation inside the exported
async handler so the endpoint behaves as a read-only proxy.

return res.status(400).json({ error: 'Missing url' });
}

const response = await fetch(url.trim());
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

fetch 요청에 타임아웃을 추가하세요.

원격 서버가 응답하지 않을 경우 요청이 무한정 대기할 수 있습니다. 이는 서버 리소스 고갈로 이어질 수 있습니다.

다음 diff를 적용하여 타임아웃을 추가하세요:

+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000); // 10초 타임아웃
+
-    const response = await fetch(url.trim());
+    try {
+      const response = await fetch(url.trim(), { signal: controller.signal });
+      clearTimeout(timeoutId);
+    } catch (err) {
+      clearTimeout(timeoutId);
+      if (err instanceof Error && err.name === 'AbortError') {
+        return res.status(504).json({ error: 'Request timeout' });
+      }
+      throw err;
+    }

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

🤖 Prompt for AI Agents
In src/pages/api/file.ts around line 24, the fetch call uses await
fetch(url.trim()) without a timeout, which can hang indefinitely; add an
AbortController, start a timer (e.g., setTimeout) that calls controller.abort()
after a reasonable timeout (e.g., 5-30s), pass { signal: controller.signal }
into fetch, clear the timeout when fetch resolves, and handle AbortError to
return a proper timeout response or error; ensure URL is still trimmed and
errors are propagated/logged appropriately.

Copy link
Copy Markdown
Contributor

@KongMezu KongMezu left a comment

Choose a reason for hiding this comment

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

야심한 시각까지..수고하셨습니다..

@jjangminii jjangminii merged commit 3372cec into develop Nov 15, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comment 필요한 주석 추가 및 변경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] 다운로드.....

2 participants