Fix: 이미지 관련 문의사항 해결#204
Conversation
commit c07799a Merge: 0b2fa2b 6d6f73e Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:58:43 2025 +0900 Merge branch 'develop' of https://github.com/YAPP-Github/26th-Web-Team-3-FE into qa/additional/#199 commit 0b2fa2b Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:43:48 2025 +0900 style: grid-layout 모바일 view 수정 commit 278afe5 Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:24:26 2025 +0900 fix: 편지 담기 reset 추가 commit 6d6f73e Author: beom <74394824+seung365@users.noreply.github.com> Date: Sat Aug 23 00:05:59 2025 +0900 fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 (#198)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded@seung365 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 11 minutes and 39 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
Walkthrough이미지 업로드 훅을 파일 선택/미리보기 중심 API로 전면 개편했고, 작성 모달이 이에 맞게 제출 플로우를 수정했습니다. 파일 업로드 뮤테이션의 기본 성공/실패 콜백을 제거했으며, 편지 작성 타입의 objectKey를 선택적으로 변경했습니다. 확인 팝업은 로딩 상태를 지원합니다. 숨김 파일 입력용 스타일이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor U as User
participant WM as WriteModal
participant IU as useImageUpload
participant FU as useFileUpload (mutation)
participant API as File API
rect rgb(240,248,255)
note right of U: 이미지 선택
U->>WM: 파일 선택
WM->>IU: handleFileChange(File)
IU-->>WM: previewUrl 설정
end
rect rgb(245,255,250)
note right of U: 제출(confirm)
U->>WM: 편지 담기
WM->>WM: isPending/isUploading 가드
alt 파일 있음
WM->>IU: uploadFile()
IU->>FU: mutateAsync(file)
FU->>API: GET presigned URL
FU->>API: PUT file
API-->>FU: 200 OK + objectKey
FU-->>IU: objectKey
IU-->>WM: objectKey
else 파일 없음
WM->>WM: objectKey undefined
end
WM->>WM: submit(payload)
WM-->>U: 성공 후 리셋/미리보기 제거
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
🚀 Storybook 배포📖 Storybook: https://683d91ab23651aa0b399e435-xgdtokadlx.chromatic.com/ |
|
This pull request (commit
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (13)
app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts (1)
178-181: display: none 대신 시각적 숨김(SR-only) 패턴 사용 제안파일 입력을
display: none으로 숨기면 스크린 리더 포커스 트리에서 완전히 제외됩니다. 라벨 클릭으로 동작은 하지만 접근성 관점에서는 시각적 숨김 패턴을 권장합니다.아래처럼 교체하면 키보드/보조공학 기기 호환성이 좋아집니다. 라벨의 htmlFor/id 연결도 함께 확인해주세요.
-export const imageInput = style({ - display: "none", -}); +export const imageInput = style({ + position: "absolute", + width: "1px", + height: "1px", + padding: 0, + margin: "-1px", + overflow: "hidden", + clip: "rect(0 0 0 0)", + clipPath: "inset(50%)", + whiteSpace: "nowrap", + border: 0, +});shared/ui/popup/popup-confirm-letter/index.tsx (3)
5-5: 로더 색상 themeVars 사용 및 a11y 속성 추가디자인 토큰 일관성(다크/라이트 테마 대응)과 접근성 향상을 위해 로더 색상을 themeVars로, 로더 컨테이너에 상태 전달 속성을 추가하는 것을 권장합니다.
import Image from "next/image"; -import { PulseLoader } from "react-spinners"; +import { PulseLoader } from "react-spinners"; +import { themeVars } from "@/shared/styles/base/theme.css"; @@ - {isLoading ? ( - <PulseLoader color="white" size={10} /> + {isLoading ? ( + <div role="status" aria-live="polite"> + <PulseLoader color={themeVars.color.white[100]} size={10} /> + </div> ) : (Also applies to: 34-36
8-14: isLoading 기본값 제공으로 이전 호출부 안전성 확보타 호출부가 아직 업데이트되지 않았을 가능성에 대비해 기본값을 두면 안전합니다. 타입은 그대로 required여도 런타임 안전성이 올라갑니다.
-const PopupConfirmLetter = ({ - isLoading, +const PopupConfirmLetter = ({ + isLoading = false, openDate, isOpen, close, onConfirm, }: PopupConfirmLetterProps) => {
34-45: 로딩 시 버튼 언마운트 대신 disabled 처리로 포커스/레이아웃 안정성 개선로딩 중 버튼을 제거하면 포커스 손실/레이아웃 점프가 발생할 수 있습니다. 동일한 영역을 유지하며 버튼을 비활성화하는 패턴을 권장합니다. 필요 시 스피너는 버튼 옆에 배치하세요.
- {isLoading ? ( - <div role="status" aria-live="polite"> - <PulseLoader color={themeVars.color.white[100]} size={10} /> - </div> - ) : ( - <> - <Popup.Button onClick={close} className={styles.content}> - 계속 쓰기 - </Popup.Button> - <Popup.Button className={styles.putButton} onClick={onConfirm}> - 편지 담기 - </Popup.Button> - </> - )} + <> + {isLoading && ( + <div role="status" aria-live="polite" style={{ display: "flex", justifyContent: "center", width: "100%" }}> + <PulseLoader color={themeVars.color.white[100]} size={10} /> + </div> + )} + <Popup.Button + onClick={close} + className={styles.content} + disabled={isLoading} + aria-disabled={isLoading} + > + 계속 쓰기 + </Popup.Button> + <Popup.Button + className={styles.putButton} + onClick={onConfirm} + disabled={isLoading} + aria-disabled={isLoading} + > + 편지 담기 + </Popup.Button> + </>shared/ui/popup/popup.stories.tsx (1)
216-226: ConfirmLetter 스토리에 로딩 상태 예시 추가 제안isLoading={false}만 있으면 스피너 UI 회귀 테스트가 어렵습니다. 로딩 전용 스토리를 하나 더 두면 시각/동작 확인이 쉬워집니다.
export const ConfirmLetterLoading: Story = { render: () => ( <button style={{ padding: "10px 20px", borderRadius: "12px" }} onClick={() => { overlay.open(({ isOpen, close }) => ( <PopupConfirmLetter isLoading={true} openDate="2025. 06. 25" isOpen={isOpen} close={close} onConfirm={() => {}} /> )); }} > Open Popup (Loading) </button> ), };app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (3)
23-37: 파일 확장자/형식 검증과 입력 초기화
- alert는 훅 레벨에서 지양(테스트/UX 방해). 로그 또는 상위 컴포넌트로 에러 전달 권장.
- 확장자는 소문자로 정규화.
- 동일 파일 재선택 시 change 이벤트가 발생하도록 input 값을 초기화.
- (선택) 허용 확장자 화이트리스트로 조기 차단.
- const extension = file.name.split(".").pop(); - if (!extension) { - alert("파일 확장자를 찾을 수 없습니다."); - return; - } + const extension = file.name.split(".").pop()?.toLowerCase(); + if (!extension) { + console.warn("파일 확장자를 찾을 수 없습니다."); + return; + } + const allowed = new Set(["jpg", "jpeg", "png", "webp", "heic", "heif", "gif"]); + if (!allowed.has(extension)) { + console.warn(`허용되지 않은 파일 형식: ${extension}`); + return; + } @@ - setSelectedFile(file); - setPreviewUrl(objectUrl); + setSelectedFile(file); + setPreviewUrl(objectUrl); + // 동일 파일 재선택 허용 + e.currentTarget.value = "";
39-58: 업로드 전 용량 제한 및 확장자 검증을 통일대용량 이미지(특히 모바일 촬영 원본)는 업로드/메모리 비용이 큽니다. 업로드 전에 크기/형식 검사를 통일해 조기 실패시키면 UX/리소스 절약에 유리합니다.
- const uploadFile = async (): Promise<string> => { + const uploadFile = async (): Promise<string> => { if (!selectedFile) { throw new Error("선택된 파일이 없습니다."); } const fileName = "LETTER"; - const extension = selectedFile.name.split(".").pop(); - - if (!extension) { - throw new Error("파일 확장자를 찾을 수 없습니다."); - } + const extension = selectedFile.name.split(".").pop()?.toLowerCase(); + if (!extension) throw new Error("파일 확장자를 찾을 수 없습니다."); + const allowed = new Set(["jpg", "jpeg", "png", "webp", "heic", "heif", "gif"]); + if (!allowed.has(extension)) throw new Error(`허용되지 않은 파일 형식: ${extension}`); + const MAX_MB = 10; + if (selectedFile.size > MAX_MB * 1024 * 1024) { + throw new Error(`파일 최대 크기 ${MAX_MB}MB를 초과했습니다.`); + }(선택) allowed/최대 용량 상수는 훅 상단의 모듈 상수로 올려 DRY하게 관리하면 더 좋습니다.
9-16: 미세 최적화: revoke 시점 단일화현재
useEffectcleanup과removeImage양쪽에서URL.revokeObjectURL이 실행될 수 있습니다(중복 호출은 무해하지만 중복입니다). cleanup 하나로 통일하면 간결해집니다.useEffect(() => { return () => { if (previewUrl) { URL.revokeObjectURL(previewUrl); } }; }, [previewUrl]);
removeImage에서는 state만 초기화하도록 단순화:- const removeImage = () => { - if (previewUrl) { - URL.revokeObjectURL(previewUrl); - } - setPreviewUrl(null); - setSelectedFile(null); - }; + const removeImage = () => { + setPreviewUrl(null); + setSelectedFile(null); + };app/(sub)/capsule-detail/_components/write-modal/index.tsx (5)
74-116: 현재는 순차 처리(업로드 완료 후 편지 생성). PR 목표의 ‘동시 PUT’과는 다를 수 있음handleConfirm에서
await uploadFile()후에writeLetterMutate를 호출하므로 실질적으로 직렬 흐름입니다. 백엔드가 objectKey를 사전 발급하고 PUT 완료를 동기화할 필요가 없다면, 다음 접근으로 체감 지연을 줄일 수 있습니다:
- 업로드 시작 시점에 objectKey를 먼저 확보하고(훅에서 presign 응답 즉시 반환), PUT은 백그라운드로 진행.
useWriteLetter는mutateAsync를 사용해 Promise 기반으로 처리하고, 업로드 PUT과 편지 생성 API를Promise.all로 동기화(둘 중 하나 실패 시 일관된 에러 핸들링).- 실패 시 orphan object 정리 전략(필요 시) 정의.
백엔드 계약이 “편지 생성 시 objectKey 필요, PUT 완료는 비동기 허용”인지 확인 부탁드립니다. 허용된다면, 훅을
startUpload(): { objectKey, putPromise }형태로 분리하는 리팩터가 적합합니다.
194-211: blob URL 미리보기는 Next/Image 최적화 우회가 필요할 수 있음 + 접근성 소폭 보완일부 Next.js 버전/설정에서
blob:URL은 이미지 최적화 대상이 아니어서 경고가 발생하거나 렌더링이 실패할 수 있습니다. 안전하게unoptimized를 추가하는 것을 권장합니다. 또한 삭제 버튼에 스크린리더용 라벨을 추가하면 접근성이 개선됩니다.적용 제안:
- <button + <button type="button" onClick={removeImage} className={styles.removeImageButton} + aria-label="이미지 삭제" > ... - <Image + <Image src={previewUrl} - alt="업로드된 이미지" + alt="선택한 이미지 미리보기" width={80} height={80} className={styles.imagePreview} + unoptimized />iOS Safari(특히 저사양 기기)에서 blob 미리보기/스크롤 성능도 한 번만 실기기로 확인 부탁드립니다.
213-221: 숨김 파일 입력 스타일 class 사용 및 비활성 조건 확장 제안CSS 모듈로
imageInput스타일이 제공된다면 inline style 대신 class 사용이 일관성/오버라이드 측면에서 안전합니다. 또한 제출 중(isPending)에도 파일 선택을 막아 레이스를 줄이는 것이 좋습니다.- <input + <input type="file" accept="image/*" onChange={handleFileChange} - disabled={isUploading} - style={{ display: "none" }} + disabled={isUploading || isPending} + className={styles.imageInput} + capture="environment" // 모바일 카메라 우선(옵션) />
255-255: isPending || isUploading 반복 사용을 상수로 통일파일 전역에서 자주 쓰이므로
const isLoading = isPending || isUploading;로 통일하면 가독성과 변경 용이성이 올라갑니다(제출 버튼 disabled/로더, Confirm 모달 isLoading에 공통 사용).
282-286: reset 시 objectKey는 폼 필드가 아니므로 생략 가능 + 초기화 로직 DRYobjectKey는 RHF로 관리되는 필드가 아니므로
reset인자에서 제외해도 됩니다. 또한 성공/뒤로가기에서 동일한 초기화(removeImage 포함)를 반복하니 헬퍼로 추출하면 유지보수가 쉬워집니다.간단 수정:
reset({ capsuleId: capsuleData.result.id.toString(), content: "", from: "", - objectKey: undefined, });그리고 다음과 같은 헬퍼로 중복 제거(예시):
const resetFormAndImage = () => { reset({ capsuleId: capsuleData.result.id.toString(), content: "", from: "" }); if (previewUrl) removeImage(); };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts(1 hunks)app/(sub)/capsule-detail/_components/write-modal/index.tsx(8 hunks)app/(sub)/capsule-detail/_components/write-modal/write-modal.css.ts(1 hunks)shared/api/mutations/file.ts(0 hunks)shared/types/api/letter.ts(1 hunks)shared/ui/popup/popup-confirm-letter/index.tsx(2 hunks)shared/ui/popup/popup.stories.tsx(2 hunks)
💤 Files with no reviewable changes (1)
- shared/api/mutations/file.ts
🧰 Additional context used
🧬 Code graph analysis (2)
app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (1)
shared/api/mutations/file.ts (1)
useFileUpload(5-34)
app/(sub)/capsule-detail/_components/write-modal/index.tsx (2)
shared/types/api/letter.ts (1)
WriteLetterReq(32-37)app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (1)
useImageUpload(4-76)
⏰ 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: deploy
🔇 Additional comments (6)
shared/types/api/letter.ts (1)
35-36: objectKey optional 적용 검증 및 서버 처리 확인 필요
- 클라이언트에서 이미지가 없을 때
objectKey를 완전히 생략하지 않고 명시적으로undefined로 전송하고 있습니다
• app/(sub)/capsule-detail/_components/write-modal/index.tsx:94
• app/(sub)/capsule-detail/_components/write-modal/index.tsx:282- 서버 측 DTO/Bean Validation/Zod/OpenAPI 스키마 등이
objectKey가undefined혹은 누락된 경우에도 동일하게 처리되도록 보장되어야 합니다- 직렬화 과정에서
objectKey가 빈 문자열("")로 전송되지 않도록(기본 제외되더라도) 설정을 재확인해 주세요백엔드 처리 로직과 클라이언트 직렬화 설정을 한 번 더 점검해 주시면 좋겠습니다.
shared/ui/popup/popup.stories.tsx (1)
134-138: 형식 정리 OK경고 팝업 스토리의 prop 개행 정리는 가독성에 도움 됩니다.
app/(sub)/capsule-detail/_components/write-modal/_hooks/use-image-upload.ts (1)
68-75: API 사용은 적절합니다
mutateAsync로 objectKey를 직접 반환받는 구조가 이번 플로우(이미지 있을 때만 업로드)에 잘 맞습니다.hasFile/isUploading제공으로 상위 컴포넌트 제어가 명확합니다.app/(sub)/capsule-detail/_components/write-modal/index.tsx (3)
39-45: react-hook-form 기본값/타입 정합성 OKobjectKey를 기본값에서 제거하고 타입(WriteLetterReq의 optional)과 정합성을 맞춘 점 좋습니다.
57-64: 새 useImageUpload 훅 연동 적절미리보기/업로드 상태(hasFile, isUploading)와 액션(handleFileChange, removeImage, uploadFile)만 노출하는 인터페이스로 깔끔하게 분리되어 있어 모달 측 복잡도가 낮아졌습니다.
122-122: 작성 중 경고 조건 정의 적절내용/보낸이/이미지 미리보기 중 하나라도 있으면 경고 노출하는 로직이 기대 동작과 부합합니다.
|
This pull request (commit
|
commit 37dca81 Author: beom <74394824+seung365@users.noreply.github.com> Date: Mon Aug 25 12:48:52 2025 +0900 Fix: 이미지 관련 문의사항 해결 (#204) * fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 * Squashed commit of the following: commit c07799a Merge: 0b2fa2b 6d6f73e Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:58:43 2025 +0900 Merge branch 'develop' of https://github.com/YAPP-Github/26th-Web-Team-3-FE into qa/additional/#199 commit 0b2fa2b Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:43:48 2025 +0900 style: grid-layout 모바일 view 수정 commit 278afe5 Author: 백승범 <bdh3659@naver.com> Date: Sat Aug 23 00:24:26 2025 +0900 fix: 편지 담기 reset 추가 commit 6d6f73e Author: beom <74394824+seung365@users.noreply.github.com> Date: Sat Aug 23 00:05:59 2025 +0900 fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 (#198) * fix: 모바일에서 간헐적으로 이미지 업로드 안되는 현상 수정 * feat: objectKey를 선택적 속성으로 변경 * feat: 사진 업로드 로직 변경 * chore: 줄간격 추가 * chore: type 충돌 해결 * chore: 코드 리뷰 반영 commit 5c19df6 Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Mon Aug 25 00:53:03 2025 +0000 Update dev dependencies (#203) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit a86f47b Author: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Date: Sun Aug 24 22:01:52 2025 +0000 Update all non-major dependencies (#202) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> commit b23c714 Author: beom <74394824+seung365@users.noreply.github.com> Date: Sat Aug 23 01:05:36 2025 +0900 QA: 추가 QA 반영 (#200) * fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 * fix: 편지 담기 reset 추가 * style: grid-layout 모바일 view 수정 commit 6d6f73e Author: beom <74394824+seung365@users.noreply.github.com> Date: Sat Aug 23 00:05:59 2025 +0900 fix: 모달 닫힘 경고 팝업 뜨도록 수정 및 esc props 전달 (#198)
📌 Summary
📚 Tasks
👀 To Reviewer
디스코드 내에서 논의된 백엔드 측 요구사항 및 오류 제보 사항 반영건입니다.
Summary by CodeRabbit
신기능
개선
스타일
잡무