feat: 새 글 작성 API 연동#102
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
✨ 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
CodeRabbit Configuration File (
|
✨ 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
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 8
🔭 Outside diff range comments (1)
src/mocks/searchBook.mock.ts (1)
1-76: 레거시 필드contentUrls및success플래그 전역 정비 필요API 스펙과 모의 데이터의 필드명이 일치하지 않아, 아래 위치에 남아 있는
contentUrls와success플래그를 검토하고 통일 또는 불필요한 필드는 제거해 주세요.
contentUrls
- src/types/post.ts (라인 12, 37)
- src/mocks/searchBook.mock.ts (라인 49, 68)
- src/data/postData.ts (라인 17, 36, 55, 77)
- src/api/feeds/getFeedDetail.ts (라인 16)
- src/api/feeds/getOtherFeed.ts (라인 11)
- src/components/feed/FeedDetailPostBody.tsx (라인 84, 90, 109, 129)
- src/components/common/Post/PostBody.tsx (라인 53, 56, 71)
success
- src/hooks/useCreateFeed.ts (라인 26–102 전반)
- src/api/users/postSignup.ts (라인 9)
- src/api/users/getAlias.ts (라인 11)
위 파일들을 스펙에 맞춰 필드명을 일관되게 수정하거나, 더 이상 사용하지 않는 레거시 키·플래그는 제거해 주시기 바랍니다.
🧹 Nitpick comments (14)
src/api/feeds/getOtherFeed.ts (2)
11-15: contentsUrl → contentUrls로의 리네임: 소비자 컴포넌트/타입 일관성 재확인
- 서버가 실제로 복수형
contentUrls(배열)를 반환하는지 확인 바랍니다. 이전 서버-클라이언트 불일치의 근본 원인이었던 필드명이라 회귀 가능성이 있습니다.- 본 타입은 PostData와 거의 동일한 필드 집합을 가집니다. 중복 정의를 줄이기 위해 PostData 기반의 Pick/확장을 고려해 주세요. 유지보수성이 좋아집니다.
예시:
import type { PostData } from '@/types/post'; // 필요시 차이점만 더하거나 제외 export type OtherFeedItem = Pick< PostData, | 'feedId' | 'postDate' | 'isbn' | 'bookTitle' | 'bookAuthor' | 'contentBody' | 'contentUrls' | 'likeCount' | 'commentCount' | 'isSaved' >;위 스크립트(다른 코멘트에 첨부)로
contentsUrl잔존 사용처를 함께 점검해 주세요.
24-28: 응답 필드 success → isSuccess 리네임: 공용 처리 로직과의 호환성 확인
- 공용 API 응답 처리(예: 인터셉터, 토스트 트리거, 에러 핸들링)가 기존
success를 기준으로 동작한다면, 본 리네임 이후 실패 케이스 분기/UX가 깨질 수 있습니다.- 서버가
isSuccess를 반환하는지도 반드시 확인 바랍니다.위 레포 점검 스크립트의
.success탐지 섹션으로 사용처 확인을 권장합니다.가능하면 응답 베이스 타입(ApiResponse)로 중복 제거를 추천드립니다.
src/api/feeds/getFeedDetail.ts (1)
26-26: success → isSuccess 리네임: 인터셉터/에러 핸들링 경로 점검 권장
- 타입 통일은 적절합니다. 다만 인터셉터나 호출부에서
.success를 기준으로 분기했다면 런타임 동작이 바뀔 수 있습니다.중복을 줄이기 위해 공용
ApiResponse<T>타입 도입을 고려해 주세요. 예:export type FeedDetailResponse = ApiResponse<FeedDetailData>;레포 점검 스크립트의
.success탐지 섹션으로 레거시 사용처를 확인해 주세요.src/types/post.ts (1)
5-5: nullable 일관성 검토: creatorProfileImageUrl는 null 대응 필요ReplyDataProps에서는 creatorProfileImageUrl가 string | null인데, 여기서는 optional string으로 선언되어 있습니다. 서버가 null을 반환할 수 있다면 PostData도 string | null로 명시하는 편이 타입 안정성이 높습니다.
적용 예:
- creatorProfileImageUrl?: string; + creatorProfileImageUrl?: string | null;src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (3)
29-30: Book 인터페이스에 isbn 추가는 타 기능 연동에 유용 — 검색/표시 활용을 함께 고려isbn 추가로 선택/전달 경로가 마련되었습니다. 사용자 검색 UX를 위해 ISBN으로도 필터링 가능하도록 확장하는 것을 권장합니다(하이픈/공백 제거 포함).
아래와 같이 검색에 ISBN을 포함할 수 있습니다(추가 import 포함).
+import { normalizeIsbn } from '../../../utils/isbn';- const filtered = currentTabBooks.filter( - book => - book.title.toLowerCase().includes(searchQuery.toLowerCase()) || - book.author.toLowerCase().includes(searchQuery.toLowerCase()), - ); + const q = searchQuery.toLowerCase().trim(); + const qDigits = normalizeIsbn(searchQuery); + const filtered = currentTabBooks.filter(book => { + const byText = + book.title.toLowerCase().includes(q) || + book.author.toLowerCase().includes(q); + const byIsbn = qDigits.length > 0 && normalizeIsbn(book.isbn).includes(qDigits); + return byText || byIsbn; + });
99-111: 탭 전환 시에도 ISBN 검색 조건 유지 반영 제안탭 변경 시에도 동일한 ISBN 검색 조건을 유지하려면 위와 동일한 로직을 재사용하는 것이 좋습니다(중복 제거를 위해 헬퍼 함수로 분리 권장).
예시:
const filterBooks = (books: Book[], query: string) => { const q = query.toLowerCase().trim(); const qDigits = normalizeIsbn(query); if (!q && !qDigits) return books; return books.filter(b => { const byText = b.title.toLowerCase().includes(q) || b.author.toLowerCase().includes(q); const byIsbn = qDigits.length > 0 && normalizeIsbn(b.isbn).includes(qDigits); return byText || byIsbn; }); };그리고 setFilteredBooks(filterBooks(newTabBooks, searchQuery)) 형태로 사용.
Also applies to: 165-170
183-188: onKeyPress 대신 onKeyDown 사용 권장React 18 환경에서는 onKeyPress가 비권장입니다. Enter 처리만 필요하다면 onKeyDown으로 교체하세요.
- onKeyPress={handleKeyPress} + onKeyDown={handleKeyPress}또는 handleKeyPress 핸들러명을 handleKeyDown으로 정리.
src/mocks/searchBook.mock.ts (1)
45-46: 모의 데이터의 ISBN 값을 유효한 형태로 교체 권장현재 'isbn': '3'은 실제 유효 ISBN 형식이 아니라 추후 체크디지트 검증 로직 도입 시 테스트를 방해할 수 있습니다. 유효한 ISBN-13으로 교체를 권장합니다.
예시 수정:
- isbn: '3', + isbn: '9780374500016',- isbn: '3', + isbn: '9780306406157',Also applies to: 63-64
src/api/images/uploadImage.ts (2)
71-73: 병렬 업로드는 현재 개수 제약하에 적절 — 확장성 대비하여 동시성 제한 고려(옵션)MAX_IMAGES=3이면 문제 없지만, 향후 확장 시 p-limit 유사 제한을 적용하면 네트워크 안정성이 좋아집니다.
17-19: 지원 확장자 스펙 서버와 합의 필요(webp/heic 등)클라이언트 확장자 제한은 서버 수용 범위와 반드시 일치해야 합니다. webp, heic 지원 여부를 확인해 주세요. 필요 시 REGEX/메시지를 동기화합니다.
src/utils/isbn.ts (1)
17-23: ensureIsbn13에 체크디지트 검증 반영 확인위 검증 강화 후에도 ensureIsbn13의 흐름은 그대로 유효합니다. 다만 모의 데이터의 ISBN이 유효하지 않다면 null을 반환할 수 있으므로, 모의 데이터 정합성도 함께 보정해 주세요.
src/components/common/Post/PostBody.tsx (1)
71-73: 이미지 렌더링 시 alt 속성 추가 권장접근성 향상을 위해 이미지에 alt 속성을 추가하는 것이 좋습니다.
- {contentUrls.map((src: string, i: number) => ( - <img key={i} src={src} /> + {contentUrls.map((src: string, i: number) => ( + <img key={i} src={src} alt={`이미지 ${i + 1}`} />src/hooks/useCreateFeed.ts (1)
61-61: 정규식 패턴 개선 권장현재 정규식이 .jpeg를 .jpe로도 매칭할 수 있습니다. 더 정확한 패턴을 사용하는 것이 좋습니다.
- const extOk = (name: string) => /\.(jpe?g|png|gif)$/i.test(name); + const extOk = (name: string) => /\.(jpeg|jpg|png|gif)$/i.test(name);src/pages/post/CreatePost.tsx (1)
127-127: 이미지 개수 제한 로직 개선slice를 사용한 3장 제한이 작동하지만, 사용자에게 제한 초과를 알리는 것이 더 좋습니다.
const handlePhotoAdd = (files: File[]) => { - setSelectedPhotos(prev => [...prev, ...files].slice(0, 3)); + setSelectedPhotos(prev => { + const combined = [...prev, ...files]; + if (combined.length > 3) { + openSnackbar({ + message: '사진은 최대 3장까지 업로드 가능합니다.', + variant: 'top', + onClose: closePopup, + }); + return combined.slice(0, 3); + } + return combined; + });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/api/feeds/createFeed.ts(1 hunks)src/api/feeds/getFeedDetail.ts(2 hunks)src/api/feeds/getMyFeed.ts(1 hunks)src/api/feeds/getOtherFeed.ts(2 hunks)src/api/feeds/getTotalFeed.ts(1 hunks)src/api/images/uploadImage.ts(1 hunks)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(3 hunks)src/components/common/Post/PostBody.tsx(2 hunks)src/components/feed/FeedDetailPostBody.tsx(3 hunks)src/data/postData.ts(4 hunks)src/hooks/useCreateFeed.ts(1 hunks)src/mocks/searchBook.mock.ts(2 hunks)src/pages/post/CreatePost.tsx(2 hunks)src/types/post.ts(2 hunks)src/utils/isbn.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/components/common/Post/PostBody.tsx (1)
src/types/post.ts (1)
PostBodyProps(35-38)
src/api/feeds/createFeed.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/api/images/uploadImage.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/hooks/useCreateFeed.ts (2)
src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/feeds/createFeed.ts (3)
CreateFeedBody(4-9)CreateFeedResponse(28-28)createFeed(36-55)
src/pages/post/CreatePost.tsx (5)
src/utils/isbn.ts (3)
normalizeIsbn(1-1)isIsbn10(3-3)ensureIsbn13(18-23)src/types/book.ts (1)
Book(1-7)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/hooks/useCreateFeed.ts (1)
useCreateFeed(9-109)src/api/feeds/createFeed.ts (1)
CreateFeedBody(4-9)
🔇 Additional comments (15)
src/api/feeds/getMyFeed.ts (1)
13-13: isSuccess 리네임 반영은 타 파일과 일관적이나, 서버 스펙/공용 핸들러 확인 필요
- getTotalFeed/getFeedDetail/getOtherFeed와 동일하게
isSuccess로 통일된 점은 좋습니다.- 다만 서버 응답 필드가 실제로
isSuccess인지, 그리고 공용 핸들러가.success를 참조하지 않는지 확인해 주세요.레포 점검 스크립트로
.success접근/정의 잔존 여부를 확인해 주세요.src/data/postData.ts (1)
17-18: Mock 데이터의 contentsUrl → contentUrls 리네임 반영 OK. 잔존 사용처 점검만 병행해 주세요
- 변경된 타입(PostData, FeedPostProps)와 일치합니다. import된 에셋(test, test2)은 번들러에서 string으로 해석되므로 string[]에도 적합합니다.
- 과거
contentsUrl를 참조하던 컴포넌트/유틸이 남아있으면 이미지가 렌더링되지 않습니다. 전역 검색으로 잔존 사용처를 제거해 주세요.동일 레포 점검 스크립트의
contentsUrl/contentUrl탐지 섹션을 실행해 확인해 주세요.Also applies to: 36-37, 55-56, 77-77
src/api/feeds/getFeedDetail.ts (1)
16-16: 이미지 필드명 contentsUrl → contentUrls: 서버 응답과의 최종 정합성 확인 요망
- 상세 API에서도 복수형
contentUrls로 통일된 점은 좋습니다. 서버 스펙도 동일한지 반드시 확인해 주세요. 과거 mismatch가 있던 필드라 회귀 테스트가 중요합니다.레포 점검 스크립트로
contentsUrl잔존 참조를 확인하고, 실제 API 응답 예시 스냅샷(또는 MSW/mock)도contentUrls인지 검증해 주세요.src/types/post.ts (2)
37-38: PostBodyProps Pick 업데이트 일관성 OKPostBodyProps에서 contentUrls 반영이 정확합니다. 해당 타입을 소비하는 컴포넌트들의 prop도 함께 업데이트된 것으로 보입니다.
12-12: success 필드 isSuccess로 리네임 누락 수정 필요
contentUrls레거시 참조는 발견되지 않았으나,success→isSuccess필드명 전환이 아직 반영되지 않은 부분이 확인되었습니다. 아래 위치에서 모두isSuccess로 변경해 주세요.
- src/hooks/useCreateFeed.ts
• 줄 26, 37, 49, 58, 68, 86, 93, 102- src/api/users/postSignup.ts
• 줄 9- src/api/users/getAlias.ts
• 줄 11예시 변경(diff):
- return { success: false as const }; + return { isSuccess: false as const }; - return { success: true as const, feedId: res.data.feedId }; + return { isSuccess: true as const, feedId: res.data.feedId }; - success: boolean; + isSuccess: boolean;Likely an incorrect or invalid review comment.
src/mocks/searchBook.mock.ts (1)
49-49: contentUrls로의 리네임 일관성 확인모의 데이터에서 contentUrls로 필드가 일괄 치환되어 타입/렌더링과 일치합니다.
Also applies to: 68-69
src/api/images/uploadImage.ts (1)
78-95: 실패 상세 집계/메시징 좋습니다부분 실패를 상세하게 수집해 사용자 메시지에 녹여내는 방식이 실사용성에 유익합니다.
src/utils/isbn.ts (1)
7-15: ISBN-10 → 13 변환 로직 정확prefix 978 및 체크디지트 재계산 로직이 올바릅니다.
src/components/common/Post/PostBody.tsx (1)
53-56: Props 이름 변경 확인 완료
contentsUrl에서contentUrls로의 변경이 올바르게 적용되었습니다. 서버 응답과 클라이언트 파싱 간의 불일치를 해결하는 중요한 수정입니다.src/components/feed/FeedDetailPostBody.tsx (2)
84-90: Props 이름 변경 및 이미지 확인 로직 개선
contentUrls로의 변경이 올바르게 적용되었습니다. 배열 길이를 통한 이미지 존재 여부 확인도 적절합니다.
109-111: 이미지 렌더링 개선 확인alt 속성이 이미 적절하게 추가되어 있어 접근성이 보장됩니다.
src/api/feeds/createFeed.ts (1)
43-43: JSON Blob 생성 방식 확인FormData에 JSON을 Blob으로 감싸서 전송하는 방식이 서버 API 스펙과 일치하는지 확인이 필요합니다. 일반적으로는 개별 필드로 전송하는 경우가 많습니다.
서버 API 문서를 확인하여
request필드를 JSON Blob으로 받는 것이 올바른 스펙인지 검증해주세요.src/hooks/useCreateFeed.ts (1)
73-94: API 응답 처리 로직 적절함성공/실패 케이스에 대한 처리와 사용자 피드백이 잘 구현되어 있습니다.
src/pages/post/CreatePost.tsx (2)
81-106: ISBN 재시도 로직 구현 확인다양한 ISBN 형식으로 재시도하는 로직이 잘 구현되어 있습니다. 서버가 ISBN-13, ISBN-10, 정규화된 값을 순차적으로 시도하여 호환성을 높였습니다.
147-150: 로딩 상태 처리 적절함로딩 중 버튼 텍스트 변경과 중복 제출 방지가 잘 구현되어 있습니다.
| const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| }); |
There was a problem hiding this comment.
Content-Type 헤더 설정 제거 필요
multipart/form-data를 사용할 때는 브라우저가 자동으로 boundary를 포함한 Content-Type을 설정합니다. 명시적으로 설정하면 boundary가 누락되어 서버에서 파싱 오류가 발생할 수 있습니다.
- const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
+ const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form);📝 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 { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | |
| headers: { 'Content-Type': 'multipart/form-data' }, | |
| }); | |
| const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form); |
🤖 Prompt for AI Agents
In src/api/feeds/createFeed.ts around lines 50 to 52, the request explicitly
sets 'Content-Type': 'multipart/form-data', which prevents the browser (or
FormData lib) from adding the required boundary and can cause server parse
errors; remove the manual Content-Type header so the client sets it
automatically, or if this is running in Node with form-data, replace the static
header with the form's generated headers (e.g., use form.getHeaders()) so the
boundary is included.
| // API 응답 타입 | ||
| export interface TotalFeedResponse { | ||
| success: boolean; | ||
| isSuccess: boolean; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
응답 필드 success → isSuccess로의 리네임: 전역 일관성 및 서버 스펙 확인 필요
- 클라이언트 전반에서
.success접근/구조분해가 남아있으면 런타임 오류가 납니다. 공용 인터셉터나 헬퍼가.success를 참조하지 않는지 확인해 주세요. - 서버가 실제로
isSuccess를 반환하는지도 반드시 확인해야 합니다. 서버가success를 그대로 돌려주면 타입만 바뀌고 런타임은 깨집니다.
다음 스크립트로 레포 내 남아있는 사용처를 점검할 수 있습니다.
추가로, 응답 스키마 중복을 줄이기 위해 공용 제네릭 타입 도입을 권장드립니다. 예:
// src/types/api.ts
export interface ApiResponse<T> {
isSuccess: boolean;
code: number;
message: string;
data: T;
}이후 본 파일에서는:
export type TotalFeedResponse = ApiResponse<TotalFeedData>;🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[검색] contentsUrl, contentUrl, contentsUrls 잔존 레거시 키 탐지"
rg -n --hidden -S '\bcontentsUrl\b|\bcontentUrl\b|\bcontentsUrls\b' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] .success 프로퍼티 접근 탐지"
rg -n --hidden -S '\.success\b' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] 구조분해 { success } 탐지"
rg -n --hidden -S '\{\s*success\s*(,|\})' -g '!node_modules' -g '!dist' -g '!build' -g '!.next' || true
echo
echo "[검색] 타입/인터페이스에서 success: 키 정의 탐지"
rg -n --hidden -S '\bsuccess\s*:' --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!.next' || true
echo
echo "[점검] PostData가 contentUrls를 포함하는지 확인"
rg -n --hidden -A 20 'interface\s+PostData' src || true
echo
echo "[점검] apiClient 인터셉터에서 success 참조 여부 확인"
fd index.ts src/api | xargs -I{} rg -n --hidden -A 3 -B 3 '\bsuccess\b' {} || trueLength of output: 2887
success → isSuccess 리네임: 남은 참조/타입 일괄 수정 필요
아래 위치들에서 여전히 success 필드를 사용하고 있어 런타임 오류가 발생합니다. 모두 isSuccess로 변경해주세요.
- src/pages/signup/SignupGenre.tsx:94 (
if (result.success)) - src/pages/post/CreatePost.tsx:92 (
if (result?.success)) - src/hooks/useCreateFeed.ts
- 반환 객체의
success: …→isSuccess: …(26, 37, 49, 58, 68, 86, 93, 102번째 줄)
- 반환 객체의
추가로:
- 서버가 실제로
isSuccess를 반환하는지 백엔드 스펙을 확인해 주세요. - API 응답 타입 중복을 줄이기 위해 제네릭
ApiResponse<T>타입 도입을 검토해 보시길 권장합니다.
🤖 Prompt for AI Agents
In src/api/feeds/getTotalFeed.ts around line 13 and the codebase, the response
field was renamed from success to isSuccess but several call sites and return
shapes still use success; update all usages to isSuccess: change
src/pages/signup/SignupGenre.tsx line ~94 (if (result.success) → if
(result.isSuccess)), src/pages/post/CreatePost.tsx line ~92 (if
(result?.success) → if (result?.isSuccess)), and update
src/hooks/useCreateFeed.ts where the hook currently returns success in multiple
places (lines ~26, 37, 49, 58, 68, 86, 93, 102) to return isSuccess instead;
additionally verify the backend actually returns isSuccess in the API spec and
consider introducing a generic ApiResponse<T> type to centralize the response
shape to avoid future mismatches.
| function validateFile(file: File) { | ||
| if (!file || file.size === 0) { | ||
| // 서버 코드 170001과 의미 일치 | ||
| throw new Error('업로드하려는 이미지가 비어있습니다.'); | ||
| } | ||
| if (!IMAGE_EXT_REGEX.test(file.name)) { | ||
| // 서버 코드 170003과 의미 일치 | ||
| throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.'); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
확장자만으로는 불충분 — MIME 타입 기반 검사 추가
캡처 이미지처럼 확장자가 없는 파일명 케이스가 존재합니다. file.type('image/*')로 1차 검증하고, 불명확할 때만 확장자 regex를 보조로 쓰는 게 안전합니다.
function validateFile(file: File) {
if (!file || file.size === 0) {
// 서버 코드 170001과 의미 일치
throw new Error('업로드하려는 이미지가 비어있습니다.');
}
- if (!IMAGE_EXT_REGEX.test(file.name)) {
+ const isImageByMime = !!file.type && file.type.startsWith('image/');
+ const isImageByName = IMAGE_EXT_REGEX.test(file.name || '');
+ if (!isImageByMime && !isImageByName) {
// 서버 코드 170003과 의미 일치
- throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.');
+ throw new Error('이미지 파일만 업로드할 수 있습니다. (jpg, jpeg, png, gif)');
}
}📝 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.
| function validateFile(file: File) { | |
| if (!file || file.size === 0) { | |
| // 서버 코드 170001과 의미 일치 | |
| throw new Error('업로드하려는 이미지가 비어있습니다.'); | |
| } | |
| if (!IMAGE_EXT_REGEX.test(file.name)) { | |
| // 서버 코드 170003과 의미 일치 | |
| throw new Error('파일 형식은 jpg, jpeg, png, gif만 가능합니다.'); | |
| } | |
| } | |
| function validateFile(file: File) { | |
| if (!file || file.size === 0) { | |
| // 서버 코드 170001과 의미 일치 | |
| throw new Error('업로드하려는 이미지가 비어있습니다.'); | |
| } | |
| const isImageByMime = !!file.type && file.type.startsWith('image/'); | |
| const isImageByName = IMAGE_EXT_REGEX.test(file.name || ''); | |
| if (!isImageByMime && !isImageByName) { | |
| // 서버 코드 170003과 의미 일치 | |
| throw new Error('이미지 파일만 업로드할 수 있습니다. (jpg, jpeg, png, gif)'); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/api/images/uploadImage.ts around lines 22-31, the current validateFile
only checks filename extension; change it to first verify MIME by checking
file.type and accept when file.type && file.type.startsWith('image/'), otherwise
fall back to the existing IMAGE_EXT_REGEX check (use a lowercased file.name when
testing) and keep the same error throws for empty files and invalid images;
ensure the size check remains first and that the logic treats missing/empty
file.type as requiring the extension check.
| const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| signal: options?.signal, | ||
| }); |
There was a problem hiding this comment.
FormData 전송 시 Content-Type 수동 지정 금지(브라우저가 boundary 자동 설정)
Axios 브라우저 환경에서 FormData를 보낼 때 'Content-Type'을 직접 설정하면 boundary가 빠져 서버에서 파싱 실패할 수 있습니다. 명시 헤더를 제거하세요.
- const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- signal: options?.signal,
- });
+ const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, {
+ signal: options?.signal,
+ });📝 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 { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { | |
| headers: { 'Content-Type': 'multipart/form-data' }, | |
| signal: options?.signal, | |
| }); | |
| const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { | |
| signal: options?.signal, | |
| }); |
🤖 Prompt for AI Agents
In src/api/images/uploadImage.ts around lines 45 to 48, the code sets
'Content-Type': 'multipart/form-data' manually which prevents the browser from
adding the required boundary; remove the explicit Content-Type header (omit the
headers object or at least the Content-Type entry) so the browser/axios can set
the correct multipart boundary automatically, keeping the signal option
unchanged.
| // 최대 5개 | ||
| if (body.tagList.length > 5) { | ||
| openSnackbar({ | ||
| message: '태그는 최대 5개까지 입력할 수 있어요.', | ||
| variant: 'top', | ||
| onClose: closePopup, | ||
| }); | ||
| return { success: false as const }; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
태그 검증 로직 개선 필요
최대 5개 제한은 적절하나, 빈 문자열 처리 로직에 문제가 있을 수 있습니다.
- if (body.tagList.length > 5) {
+ const trimmed = body.tagList.map(t => t.trim()).filter(Boolean);
+ if (trimmed.length > 5) {
openSnackbar({
message: '태그는 최대 5개까지 입력할 수 있어요.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
- // 중복 제거 체크
- const trimmed = body.tagList.map(t => t.trim()).filter(Boolean);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/hooks/useCreateFeed.ts around lines 19 to 27, the current tag-count check
counts raw entries and can allow blank or whitespace-only tags to bypass
validation; trim each tag and filter out empty strings first, then perform the
max-5 check on the cleaned list, update body.tagList (or validate against the
cleaned list) and show the same snackbar and return on failure; also ensure
individual tags aren't empty by rejecting if any trimmed tag === '' with the
existing snackbar flow.
| // 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지 | ||
| const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase(); | ||
| const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | ||
|
|
There was a problem hiding this comment.
중복된 ISBN 유틸리티 함수
src/utils/isbn.ts에 이미 동일한 함수들이 있는데 여기서 재정의하고 있습니다. 중복 코드를 제거하고 import를 사용해야 합니다.
-// 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지
-const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase();
-const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn);
+import { normalizeIsbn, isIsbn10 } from '@/utils/isbn';📝 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.
| // 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지 | |
| const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase(); | |
| const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | |
| import { normalizeIsbn, isIsbn10 } from '@/utils/isbn'; |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around lines 18 to 21, the normalizeIsbn and
isIsbn10 helpers are duplicates of functions in src/utils/isbn.ts; remove these
local definitions and import the existing utilities from src/utils/isbn.ts (e.g.
import { normalizeIsbn, isIsbn10 } from 'src/utils/isbn'), update any local
references to use the imported names, and ensure the utils file exports those
functions with compatible types so the component builds without type errors.
| const handleBookSearchOpen = () => { | ||
| setIsBookSearchOpen(true); | ||
| }; | ||
| const candidates = makeIsbnCandidates(selectedBook!.isbn); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
selectedBook null 체크 필요
isFormValid 검증 후에도 TypeScript 컴파일러는 selectedBook이 null일 수 있다고 판단합니다. 타입 가드를 추가하는 것이 안전합니다.
- const candidates = makeIsbnCandidates(selectedBook!.isbn);
+ if (!selectedBook) return;
+ const candidates = makeIsbnCandidates(selectedBook.isbn);📝 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 candidates = makeIsbnCandidates(selectedBook!.isbn); | |
| if (!selectedBook) return; | |
| const candidates = makeIsbnCandidates(selectedBook.isbn); |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around line 75, calling
makeIsbnCandidates(selectedBook!.isbn) assumes selectedBook is non-null but
TypeScript still warns; add an explicit null/type guard before using
selectedBook (e.g., check if selectedBook is falsy and return or skip candidate
generation) or narrow the type with an if (selectedBook) block so
makeIsbnCandidates is only called when selectedBook is defined.
| export const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | ||
| export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn); |
There was a problem hiding this comment.
isIsbn10/isIsbn13이 형식만 검사 — 체크디지트 검증이 PR 요건과 불일치
현재 구현은 패턴 검증만 수행합니다. PR 요약에 명시된 “체크디지트 검증”을 충족하도록 실제 체크디지트 검증 로직을 포함시키는 것이 맞습니다.
아래와 같이 개선을 제안합니다.
-export const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn);
-export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn);
+export const isIsbn10 = (isbn: string) => {
+ if (!/^[0-9]{9}[0-9X]$/.test(isbn)) return false;
+ // 가중치 10..1, X는 10
+ let sum = 0;
+ for (let i = 0; i < 10; i++) {
+ const c = isbn[i];
+ const v = c === 'X' ? 10 : Number(c);
+ if (Number.isNaN(v)) return false;
+ sum += v * (10 - i);
+ }
+ return sum % 11 === 0;
+};
+
+export const isIsbn13 = (isbn: string) => {
+ if (!/^[0-9]{13}$/.test(isbn)) return false;
+ const digits = isbn.split('').map(Number);
+ if (digits.some(d => Number.isNaN(d))) return false;
+ const sum = digits
+ .slice(0, 12)
+ .reduce((acc, n, i) => acc + n * (i % 2 === 0 ? 1 : 3), 0);
+ const check = (10 - (sum % 10)) % 10;
+ return digits[12] === check;
+};📝 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 isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | |
| export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn); | |
| export const isIsbn10 = (isbn: string) => { | |
| if (!/^[0-9]{9}[0-9X]$/.test(isbn)) return false; | |
| // 가중치 10..1, X는 10 | |
| let sum = 0; | |
| for (let i = 0; i < 10; i++) { | |
| const c = isbn[i]; | |
| const v = c === 'X' ? 10 : Number(c); | |
| if (Number.isNaN(v)) return false; | |
| sum += v * (10 - i); | |
| } | |
| return sum % 11 === 0; | |
| }; | |
| export const isIsbn13 = (isbn: string) => { | |
| if (!/^[0-9]{13}$/.test(isbn)) return false; | |
| const digits = isbn.split('').map(Number); | |
| if (digits.some(d => Number.isNaN(d))) return false; | |
| const sum = digits | |
| .slice(0, 12) | |
| .reduce((acc, n, i) => acc + n * (i % 2 === 0 ? 1 : 3), 0); | |
| const check = (10 - (sum % 10)) % 10; | |
| return digits[12] === check; | |
| }; |
🤖 Prompt for AI Agents
In src/utils/isbn.ts around lines 3 to 4, the current functions only validate
format but must also validate the checksum; update isIsbn10 to strip
non-digits/X, ensure length 10, compute the ISBN-10 checksum by multiplying the
first 9 digits by weights 10 down to 2, add the check digit (treat 'X' as 10),
sum and verify sum % 11 === 0; update isIsbn13 to strip non-digits, ensure
length 13, compute the ISBN-13 checksum by summing digits with weights 1 and 3
alternating for the first 12 digits, compute expected check digit as (10 - (sum
% 10)) % 10 and compare to the 13th digit; keep initial regex/format check or
perform sanitation first, handle uppercase 'X', and return boolean results.
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
src/api/feeds/getMyFeed.ts (1)
11-17: API 공통 응답 스키마 제네릭으로 중복 제거 제안동일한 응답 프레임(즉, isSuccess/code/message/data)이 여러 파일에 반복됩니다. 공통
ApiResponse<T>제네릭으로 정리하면 유지보수가 용이합니다.아래와 같이 변경(예시):
- 공통 타입 추가(예: src/types/api.ts)
export interface ApiResponse<T> { isSuccess: boolean; code: number; message: string; data: T; }
- 본 파일의 응답 타입 간소화
-export interface MyFeedResponse { - isSuccess: boolean; - code: number; - message: string; - data: MyFeedData; -} +export type MyFeedResponse = ApiResponse<MyFeedData>;src/utils/isbn.ts (1)
6-15: 후속 기능(선택): 978-prefixed ISBN-13 → ISBN-10 변환과 후보 생성기 제공검색 다중 후보 전략(13 → 10 → 정규화)에 맞춰 13→10 변환과 후보 리스트 함수가 있으면 재사용성이 좋아집니다.
예시 구현:
export const isbn13to10 = (isbn13: string): string | null => { const n = normalizeIsbn(isbn13); if (!isIsbn13(n) || !n.startsWith('978')) return null; const core = n.slice(3, 12); // 9자리 const sum = core.split('') .map(Number) .reduce((acc, d, idx) => acc + d * (10 - (idx + 1)), 0); // 가중치 10..2 const checkVal = (11 - (sum % 11)) % 11; const checkChar = checkVal === 10 ? 'X' : String(checkVal); return core + checkChar; }; export const toIsbnCandidates = (raw: string): string[] => { const set = new Set<string>(); const n13 = ensureIsbn13(raw); if (n13) { set.add(n13); const n10 = isbn13to10(n13); if (n10) set.add(n10); } const n = normalizeIsbn(raw); if (isIsbn10(n)) set.add(n); return Array.from(set); };src/data/postData.ts (1)
31-31: Mock ISBN 값의 길이/체크디지트 유효성 확인 권장PR에서 ISBN 체크디지트 검증을 도입하면, 12자리 등 유효하지 않은 샘플은 유틸/폼 검증에 걸릴 수 있습니다. 실제 유효한 ISBN으로 교체하면 회귀를 줄일 수 있습니다.
예시(검증된 값 사용 권장):
- isbn: '111152544099', + isbn: '9780306406157', // 예: 널리 알려진 유효한 ISBN-13- isbn: '111155541766', + isbn: '0306406152', // 예: 위 13자리의 대응 ISBN-10필요 시, 제가 전체 mock의 ISBN을 일괄 유효값으로 교체하는 커밋을 제안드릴 수 있습니다.
Also applies to: 50-50, 72-72
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (2)
46-46: 모든 mock 항목에 isbn 추가: 데이터 일관성 확보. 유효성(체크디지트)만 한번 확인 부탁목표와 맞고 일관성 좋습니다. 다만 일부 값은 ISBN-13 체크디지트를 통과하지 않을 수 있습니다. 이후 ISBN 정규화/검증 로직(ensureIsbn13 등)을 mock에도 적용한다면 예기치 않은 실패를 줄일 수 있습니다.
필요하시면 utils/isbn.ts의 ensureIsbn13 혹은 정규식 기반 간단한 포맷터를 적용해 mock 값도 13자리 유효 포맷으로 일괄 업데이트하는 스크립트/PR 보조 드리겠습니다.
Also applies to: 53-53, 60-60, 70-70, 77-77, 85-85
104-108: 검색 필터에 ISBN 포함 제안 (하이픈 무시 매칭)사용자가 ISBN으로도 검색할 수 있도록 필터 조건에 isbn을 추가하면 UX가 좋아집니다. 하이픈 포함/미포함 모두 매칭되도록 처리 제안드립니다.
다음 diff 적용을 검토해주세요.
- book.title.toLowerCase().includes(searchQuery.toLowerCase()) || - book.author.toLowerCase().includes(searchQuery.toLowerCase()), + book.title.toLowerCase().includes(searchQuery.toLowerCase()) || + book.author.toLowerCase().includes(searchQuery.toLowerCase()) || + book.isbn.replace(/-/g, '').includes(searchQuery.replace(/-/g, '')),동일 로직을 탭 변경 시 재검색 부분에도 적용:
- book.title.toLowerCase().includes(searchQuery.toLowerCase()) || - book.author.toLowerCase().includes(searchQuery.toLowerCase()), + book.title.toLowerCase().includes(searchQuery.toLowerCase()) || + book.author.toLowerCase().includes(searchQuery.toLowerCase()) || + book.isbn.replace(/-/g, '').includes(searchQuery.replace(/-/g, '')),Also applies to: 165-168
src/api/feeds/getFeedDetail.ts (1)
34-36: 구버전 서버 응답 호환 어댑터 제안 (contentsUrl → contentUrls 매핑)BE가 환경별로 아직 혼재되어 있을 가능성에 대비해 가벼운 어댑터를 넣어두면 회귀 위험이 줄어듭니다. 타입 안전성은 any 캐스팅으로 최소 침범만 합니다.
다음과 같이 보완을 고려해주세요.
export const getFeedDetail = async (feedId: number) => { const response = await apiClient.get<FeedDetailResponse>(`/feeds/${feedId}`); - return response.data; + const res = response.data; + // 임시 호환: 서버가 구키 `contentsUrl`을 반환하는 경우에도 동작하도록 매핑 + if ( + res?.data && + !(res.data as any).contentUrls && + Array.isArray((res.data as any).contentsUrl) + ) { + (res.data as any).contentUrls = (res.data as any).contentsUrl; + } + return res; };src/components/common/Post/PostBody.tsx (1)
71-73: 이미지 렌더링 접근성/안정성 개선 제안: alt/키/지연로딩
- key로 index 대신 src 사용 권장(재정렬/삭제 시 불필요한 재마운트 방지)
- alt 텍스트 추가로 접근성 개선
- loading="lazy"로 성능 최적화
다음과 같이 개선을 제안드립니다.
- {contentUrls.map((src: string, i: number) => ( - <img key={i} src={src} /> - ))} + {contentUrls.map((src, i) => ( + <img + key={src || i} + src={src} + alt={`${bookTitle} 이미지 ${i + 1}`} + loading="lazy" + /> + ))}src/hooks/useCreateFeed.ts (1)
61-62: 이미지 확장자 검증 함수명 개선 제안
extOk라는 함수명이 너무 간략합니다. 더 명확한 이름을 사용하는 것이 좋겠습니다.-const extOk = (name: string) => /\.(jpe?g|png|gif)$/i.test(name); -if (images.some(f => !extOk(f.name))) { +const isValidImageExtension = (name: string) => /\.(jpe?g|png|gif)$/i.test(name); +if (images.some(f => !isValidImageExtension(f.name))) {src/api/images/uploadImage.ts (1)
45-48: Content-Type 헤더 설정 재검토 필요
createFeed.ts와 마찬가지로, axios는 FormData 전송 시 자동으로 적절한 Content-Type과 boundary를 설정합니다.-const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - signal: options?.signal, -}); +const { data } = await apiClient.post<UploadImageResponse>('/images/upload', formData, { + signal: options?.signal, +});
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/api/feeds/createFeed.ts(1 hunks)src/api/feeds/getFeedDetail.ts(2 hunks)src/api/feeds/getMyFeed.ts(1 hunks)src/api/feeds/getOtherFeed.ts(2 hunks)src/api/feeds/getTotalFeed.ts(1 hunks)src/api/images/uploadImage.ts(1 hunks)src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx(3 hunks)src/components/common/Post/PostBody.tsx(2 hunks)src/components/feed/FeedDetailPostBody.tsx(3 hunks)src/data/postData.ts(4 hunks)src/hooks/useCreateFeed.ts(1 hunks)src/mocks/searchBook.mock.ts(2 hunks)src/pages/post/CreatePost.tsx(2 hunks)src/types/post.ts(2 hunks)src/utils/isbn.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/api/feeds/createFeed.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/components/common/Post/PostBody.tsx (1)
src/types/post.ts (1)
PostBodyProps(35-38)
src/api/images/uploadImage.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/hooks/useCreateFeed.ts (2)
src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/feeds/createFeed.ts (3)
CreateFeedBody(4-9)CreateFeedResponse(28-28)createFeed(36-55)
src/pages/post/CreatePost.tsx (5)
src/utils/isbn.ts (3)
normalizeIsbn(1-1)isIsbn10(3-3)ensureIsbn13(18-23)src/types/book.ts (1)
Book(1-7)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/hooks/useCreateFeed.ts (1)
useCreateFeed(9-109)src/api/feeds/createFeed.ts (1)
CreateFeedBody(4-9)
🔇 Additional comments (21)
src/api/feeds/getMyFeed.ts (1)
13-17: getMyFeeds 응답의success→isSuccess변경, 호출부 영향 없음검증 결과
getMyFeeds(이전getMyFeed) 호출부에서는 더 이상.success에 접근하고 있지 않습니다.
전역적으로.success속성 접근(SignupGenre.tsx, CreatePost.tsx 등)이 존재하지만, 이는 다른 API(useCreateFeed, postSignup, getAlias 등)의 응답 처리부로,getMyFeeds와 독립적입니다.따라서
getMyFeed.ts내 타입 리네이밍은 안전하며, 호출부에 추가 수정은 필요 없습니다.src/api/feeds/getTotalFeed.ts (1)
13-17: success → isSuccess 리네이밍 일관성 OK타입 정의와 제너릭 적용이 일관적입니다. 상위 호출부에서
.success접근이 남아있지 않은지만 추가로 확인하면 좋겠습니다. 위 파일에 첨부한 스크립트로 전역 점검 가능합니다.src/api/feeds/getOtherFeed.ts (2)
11-11: contentsUrl → contentUrls 리네이밍 적절서버 응답 키와 클라이언트 타입이 정합성을 갖게 되어 이미지 렌더링 이슈 예방에 도움이 됩니다. 소비 측 컴포넌트(Feed/PostBody 등)도 동일하게 반영되었는지 전역 점검 권장합니다.
24-28: success → isSuccess 리네이밍 적절응답 스키마 표준화 방향에 부합합니다. 동일 변경이 전체 피드 API들에 반영되어 있음을 확인했습니다.
src/utils/isbn.ts (2)
7-15: ISBN-10 → 13 변환 로직 정확978 접두 + (1,3) 가중치 합으로 체크디지트 계산이 정확합니다.
1-1: 입력 정규화(normalizeIsbn) 적절하이픈/공백 제거 및 X 대문자 통일 처리 모두 적절합니다.
src/data/postData.ts (1)
17-17: contentsUrl → contentUrls 리네이밍 적용 OK모든 변경 지점이 일관되며, 빈 배열 처리([])도 클라이언트 렌더링을 단순화합니다.
Also applies to: 36-36, 55-55, 77-77
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.tsx (1)
29-29: Book 타입에 ISBN 필드 추가: 일관성 확보 LGTMCreate Post 플로우와 ISBN 연동 목적에 부합하며, 선택 콜백(onSelectBook)을 통해 상위로 전달되는 데이터에도 포함되어 재사용성이 좋아졌습니다.
src/mocks/searchBook.mock.ts (1)
49-49: mock posts의 contentsUrl → contentUrls rename: 일관성 확보 LGTM.모든 UI/타입/응답에 맞춰 통일된 명칭 사용이 확인됩니다. 다만 같은 블록 내 isbn 값이 매우 짧은 케이스(예: "3")가 있어, 실제 ISBN 기반 기능(정규화/검증/검색)이 들어오면 쉽게 실패할 수 있습니다. 테스트 범위에 따라 필요 시 유효한 13자리 값으로 보정하는 것을 권장합니다.
isbn mock 값이 실제 정규화/검증 경로로 흘러가는지 확인 부탁드립니다. 필요 시 보정 작업 도와드릴 수 있습니다.
Also applies to: 68-68
src/types/post.ts (1)
12-12: PostData 및 PostBodyProps의 contentsUrl → contentUrls: 타입 일관성 LGTMUI/Mock/API 타입 전반의 정합성이 맞춰졌습니다. create/update/read 경로에서 동일한 명칭을 사용하므로 유지보수성도 좋아졌습니다.
Also applies to: 37-37
src/components/common/Post/PostBody.tsx (1)
53-57: prop rename 반영(contentUrls) 및 hasImage 계산: LGTM기존 로직 유지하면서 명칭만 통일되었습니다. 기본값 디폴팅도 안전합니다.
src/components/feed/FeedDetailPostBody.tsx (3)
84-90: API 응답 필드명 변경이 올바르게 적용되었습니다.
contentsUrl에서contentUrls로의 필드명 변경이 일관되게 적용되었습니다. props 정의, 배열 체크, 이미지 렌더링 로직이 모두 새로운 필드명을 사용하도록 업데이트되었습니다.
109-111: 이미지 렌더링 로직이 적절히 수정되었습니다.
contentUrls배열을 순회하며 이미지를 렌더링하는 로직이 올바르게 구현되었습니다.
128-129: ImageViewer 컴포넌트의 props도 일관되게 업데이트되었습니다.
ImageViewer컴포넌트에 전달되는 이미지 배열도contentUrls로 변경되어 일관성이 유지됩니다.src/hooks/useCreateFeed.ts (3)
41-70: 이미지 검증 로직이 잘 구현되었습니다.이미지 개수 제한(3장), 빈 파일 검증, 확장자 검증이 적절히 구현되었습니다. 사용자 친화적인 에러 메시지도 제공됩니다.
75-94: API 응답 처리가 적절히 구현되었습니다.
isSuccess필드를 사용한 성공/실패 분기 처리와 에러 메시지 표시가 잘 구현되었습니다.
29-38: 태그 중복 검증 로직 개선 필요현재 로직은 trim된 태그와 원본 태그의 개수를 비교하여 중복을 검사하지만, 실제로는 다른 문제를 감지할 수 있습니다. 예를 들어
["태그1", " 태그2 "]는 중복이 없지만 현재 로직에서는 중복으로 판단됩니다.다음과 같이 수정하는 것을 제안합니다:
-// 중복 제거 체크 -const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); -const uniq = new Set(trimmed); -if (uniq.size !== trimmed.length) { +// 공백 제거 및 빈 태그 필터링 +const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); +// 중복 체크 +const uniq = new Set(trimmed); +if (uniq.size !== trimmed.length) {또한 body.tagList를 trimmed로 교체하는 것도 고려해보세요:
+// 정제된 태그 리스트로 교체 +body.tagList = Array.from(uniq);Likely an incorrect or invalid review comment.
src/api/feeds/createFeed.ts (1)
36-55: multipart/form-data API 구현이 적절합니다.FormData를 사용한 multipart/form-data 구성과 JSON 페이로드를 Blob으로 감싸는 처리가 올바르게 구현되었습니다. 이미지 파일 첨부 로직도 적절합니다.
src/api/images/uploadImage.ts (2)
17-19: 상수 정의가 명확합니다.이미지 확장자 정규식과 최대 업로드 개수가 적절히 정의되었습니다.
78-95: 병렬 업로드 에러 처리가 잘 구현되었습니다.
Promise.allSettled를 사용한 병렬 처리와 개별 실패 사유를 수집하여 상세한 에러 메시지를 제공하는 로직이 훌륭합니다.src/pages/post/CreatePost.tsx (1)
23-34: ISBN 후보군 생성 로직이 견고합니다.ISBN-13 우선, ISBN-10 폴백, 정규화된 원본 순서로 후보를 생성하는 로직이 잘 구현되었습니다. 중복 제거도 적절합니다.
| const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| }); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Content-Type 헤더 설정 확인 필요
axios는 FormData를 전송할 때 자동으로 적절한 Content-Type과 boundary를 설정합니다. 명시적으로 'multipart/form-data'를 설정하면 boundary가 누락될 수 있습니다.
헤더를 제거하거나 axios가 자동으로 설정하도록 하는 것을 고려해보세요:
-const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, {
- headers: { 'Content-Type': 'multipart/form-data' },
-});
+const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form);🏁 Script executed:
#!/bin/bash
# apiClient의 기본 헤더 설정 확인
rg -A 5 "apiClient.*create|axios.create" src/api/Length of output: 294
FormData 전송 시 Content-Type 자동 설정 검토 필요
현재 apiClient 인스턴스의 기본 헤더로 'Content-Type': 'application/json'이 설정되어 있어, FormData 전송 시에도 이 헤더가 적용됩니다. axios는 FormData를 전송할 때 boundary를 포함한 올바른 Content-Type을 자동으로 설정하지만, 기본 헤더나 명시적 헤더가 있으면 자동 설정이 무시됩니다. 다음 중 하나의 방식을 적용해 주세요:
-
src/api/feeds/createFeed.ts (라인 50–52)
요청할 때 defaultContent-Type헤더를 제거하도록 override- const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); + const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { + headers: { 'Content-Type': undefined }, + });
-
src/api/index.ts
apiClient기본 헤더에서Content-Type설정 제거 (JSON 전송 시에만 별도 명시)- export const apiClient = axios.create({ - baseURL: API_BASE_URL, - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, - }); + export const apiClient = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, + });
위 수정 후, FormData 전송 시 브라우저(또는 axios)가 자동으로 설정하는 Content-Type과 boundary가 올바르게 포함되는지 확인해 주세요.
📝 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 { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | |
| headers: { 'Content-Type': 'multipart/form-data' }, | |
| }); | |
| const { data } = await apiClient.post<CreateFeedResponse>('/feeds', form, { | |
| headers: { 'Content-Type': undefined }, | |
| }); |
🤖 Prompt for AI Agents
In src/api/feeds/createFeed.ts around lines 50–52, the request explicitly sets
'Content-Type': 'multipart/form-data' which conflicts with axios/FormData's
automatic header (including the boundary); remove the Content-Type header
override for this POST so axios can set the correct multipart boundary
automatically (alternatively, remove the default 'Content-Type' from apiClient
in src/api/index.ts and only set 'application/json' where needed), then verify
FormData requests include the correct Content-Type with boundary.
| bookAuthor: string; | ||
| contentBody: string; | ||
| contentsUrl: string[]; | ||
| contentUrls: string[]; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
API 응답 키 rename(contentUrls, isSuccess): 방향성 및 일관성 LGTM. 잔존 사용처 점검 권장
서버-클라이언트 계약 불일치로 발생하던 이미지 미노출 문제를 해결하는 바람직한 변경입니다. 동일 패턴(Other/My/Total Feed 등)으로 전역 적용도 잘 맞습니다. 남아있는 legacy 키 사용을 한번 더 스캔해 주세요.
다음 스크립트로 레거시 키 사용 여부를 전역 점검할 수 있습니다. 기대 결과는 빈 출력(= 더 이상 사용 없음)입니다.
Also applies to: 26-26
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) 레거시 'contentsUrl' 사용처 검색 (없어야 정상)"
rg -n -S '\bcontentsUrl\b' --glob '!node_modules'
echo
echo "2) 레거시 'success' 키 사용처 검색 (isSuccess 제외)"
rg -n -S -P '(?<!is)success\s*[:=]' --glob '!node_modules' | rg -v 'isSuccess' || true
echo
echo "3) 혹시 모를 snake_case 'contents_url' 검색"
rg -n -S '\bcontents_url\b' --glob '!node_modules' || true
echo
echo "4) 프로퍼티 접근 형태 '.success' 검색 ('.isSuccess'는 제외)"
rg -n -S -P '\.success\b' --glob '!node_modules' || trueLength of output: 160
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "1) 레거시 'contentsUrl' 사용처 검색 (없어야 정상)"
rg -n -S '\bcontentsUrl\b' --glob '!node_modules' || true
echo
echo "2) 레거시 'success' 키 사용처 검색 (isSuccess 제외)"
rg -n -S -P '(?<!is)success\s*[:=]' --glob '!node_modules' | rg -v 'isSuccess' || true
echo
echo "3) 혹시 모를 snake_case 'contents_url' 검색"
rg -n -S '\bcontents_url\b' --glob '!node_modules' || true
echo
echo "4) 프로퍼티 접근 형태 '.success' 검색 ('.isSuccess'는 제외)"
rg -n -S -P '\.success\b' --glob '!node_modules' | rg -v '\.isSuccess\b' || trueLength of output: 1740
남아있는 success 프로퍼티 사용처 점검 및 갱신 필요
API 응답 키를 success에서 isSuccess로 변경하셨다면, 내부 매핑과 소비 측 코드에서도 동일하게 반영해야 합니다. 아래 위치들을 확인 후 적절히 수정해 주세요.
-
src/hooks/useCreateFeed.ts
• 26, 37, 49, 58, 68, 86, 93, 102행:return { success: /*…*/ }
→
isSuccess를 매핑하거나 리턴 프로퍼티명을isSuccess로 변경 -
src/pages/post/CreatePost.tsx
• 55행의onSuccess:콜백명(onSuccess)은 그대로 두셔도 무방
• 92행:if (result?.success) { … }
→
result?.isSuccess또는 내부 매핑 확인 -
src/pages/signup/SignupGenre.tsx
• 94행:if (result.success) { … }
→
result.isSuccess로 교체 검토 -
(선택) feed 외 엔드포인트
• src/api/users/getAlias.ts, src/api/users/postSignup.ts 등
→ 해당 API들은 아직success를 쓰는 것이 의도된 것인지 확인
위 항목들을 반영하여 잔여 legacy 키(success)를 제거 및 통일해 주세요.
| // 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지 | ||
| const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase(); | ||
| const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | ||
|
|
||
| // ISBN 후보군 생성: 13자리(우선) → 원본정규화 → (가능하면) 10자리 | ||
| const makeIsbnCandidates = (raw: string) => { | ||
| const candidates: string[] = []; | ||
| const normalized = normalizeIsbn(raw); | ||
| const isbn13 = ensureIsbn13(raw); // 13으로 변환 성공 시 | ||
| if (isbn13) candidates.push(isbn13); | ||
| // 혹시 서버가 10자리로만 붙는 경우 대비(일부 API 환경에서 존재) | ||
| if (isIsbn10(normalized)) candidates.push(normalized); | ||
| // 마지막으로 raw 정규화 값(13도 10도 아니면 그래도 시도) | ||
| if (!candidates.includes(normalized)) candidates.push(normalized); | ||
| // 중복 제거 | ||
| return Array.from(new Set(candidates)); | ||
| }; |
There was a problem hiding this comment.
ISBN 유틸리티 중복 정의
normalizeIsbn과 isIsbn10 함수가 이미 src/utils/isbn.ts에 정의되어 있는 것으로 보입니다. 중복 정의를 제거하고 import를 사용하세요.
-import { ensureIsbn13 } from '@/utils/isbn';
-
-// 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지
-const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase();
-const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn);
+import { ensureIsbn13, normalizeIsbn, isIsbn10 } from '@/utils/isbn';📝 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.
| // 🔧 보조 유틸: 하이픈/공백 제거 + 대문자 X 유지 | |
| const normalizeIsbn = (raw: string) => raw.replace(/[^0-9Xx]/g, '').toUpperCase(); | |
| const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | |
| // ISBN 후보군 생성: 13자리(우선) → 원본정규화 → (가능하면) 10자리 | |
| const makeIsbnCandidates = (raw: string) => { | |
| const candidates: string[] = []; | |
| const normalized = normalizeIsbn(raw); | |
| const isbn13 = ensureIsbn13(raw); // 13으로 변환 성공 시 | |
| if (isbn13) candidates.push(isbn13); | |
| // 혹시 서버가 10자리로만 붙는 경우 대비(일부 API 환경에서 존재) | |
| if (isIsbn10(normalized)) candidates.push(normalized); | |
| // 마지막으로 raw 정규화 값(13도 10도 아니면 그래도 시도) | |
| if (!candidates.includes(normalized)) candidates.push(normalized); | |
| // 중복 제거 | |
| return Array.from(new Set(candidates)); | |
| }; | |
| import { ensureIsbn13, normalizeIsbn, isIsbn10 } from '@/utils/isbn'; | |
| // ISBN 후보군 생성: 13자리(우선) → 원본정규화 → (가능하면) 10자리 | |
| const makeIsbnCandidates = (raw: string) => { | |
| const candidates: string[] = []; | |
| const normalized = normalizeIsbn(raw); | |
| const isbn13 = ensureIsbn13(raw); // 13으로 변환 성공 시 | |
| if (isbn13) candidates.push(isbn13); | |
| // 혹시 서버가 10자리로만 붙는 경우 대비(일부 API 환경에서 존재) | |
| if (isIsbn10(normalized)) candidates.push(normalized); | |
| // 마지막으로 raw 정규화 값(13도 10도 아니면 그래도 시도) | |
| if (!candidates.includes(normalized)) candidates.push(normalized); | |
| // 중복 제거 | |
| return Array.from(new Set(candidates)); | |
| }; |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around lines 18 to 34, the local definitions of
normalizeIsbn and isIsbn10 duplicate implementations in src/utils/isbn.ts;
remove these local functions and import normalizeIsbn and isIsbn10 from
src/utils/isbn.ts instead, update the top-of-file imports accordingly, and
ensure makeIsbnCandidates uses the imported functions (also check if
ensureIsbn13 lives in the same utils file and import it too if needed).
| const candidates = makeIsbnCandidates(selectedBook!.isbn); | ||
|
|
||
| // images: 선택값 (없으면 undefined 전달 → FormData에 미첨부) | ||
| const filesOrUndefined = selectedPhotos.length ? selectedPhotos : undefined; | ||
|
|
||
| // 최대 2회까지(총 3회) 재시도: 13 → (10) → (정규화원본) | ||
| for (let i = 0; i < Math.min(candidates.length, 3); i++) { | ||
| const isbnToSend = candidates[i]; | ||
| const body: CreateFeedBody = { | ||
| isbn: isbnToSend, | ||
| contentBody: postContent.trim(), | ||
| isPublic: !isPrivate, | ||
| ...(selectedTags.length ? { tagList: selectedTags } : {}), | ||
| }; | ||
|
|
||
| try { | ||
| const result = await createNewFeed(body, filesOrUndefined); | ||
| if (result?.success) { | ||
| // onSuccess에서 이동 처리됨 | ||
| return; | ||
| } else { | ||
| // useCreateFeed에서 서버 메시지를 스낵바로 띄움 | ||
| // 80009면 다음 후보로 자동 재시도, 그 외면 바로 중단 | ||
| // (result.errorCode를 반환하도록 훅을 확장했다면 여기서 체크) | ||
| // 현재 훅은 errorCode를 안 주니, 다음 후보가 있으면 조용히 다음 루프 진행 | ||
| } | ||
| } catch (error) { | ||
| console.error(`[CreatePost] Try #${i + 1} failed:`, error); | ||
| // 네트워크/타임아웃 등은 바로 중단 | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
재시도 로직 개선 제안
현재 구현은 모든 실패에 대해 다음 후보로 진행하지만, ISBN 관련 오류(80009)와 다른 오류를 구분하면 더 효율적일 것입니다.
useCreateFeed 훅이 error code를 반환하도록 수정하거나, 특정 에러 코드에서만 재시도하도록 로직을 개선하는 것을 고려해보세요:
const result = await createNewFeed(body, filesOrUndefined);
if (result?.success) {
return;
} else {
- // useCreateFeed에서 서버 메시지를 스낵바로 띄움
- // 80009면 다음 후보로 자동 재시도, 그 외면 바로 중단
- // (result.errorCode를 반환하도록 훅을 확장했다면 여기서 체크)
- // 현재 훅은 errorCode를 안 주니, 다음 후보가 있으면 조용히 다음 루프 진행
+ // ISBN 관련 오류(예: 80009)가 아닌 경우 중단
+ if (result.errorCode && result.errorCode !== 80009) {
+ break;
+ }
+ // ISBN 오류인 경우 다음 후보로 계속 진행
}📝 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 candidates = makeIsbnCandidates(selectedBook!.isbn); | |
| // images: 선택값 (없으면 undefined 전달 → FormData에 미첨부) | |
| const filesOrUndefined = selectedPhotos.length ? selectedPhotos : undefined; | |
| // 최대 2회까지(총 3회) 재시도: 13 → (10) → (정규화원본) | |
| for (let i = 0; i < Math.min(candidates.length, 3); i++) { | |
| const isbnToSend = candidates[i]; | |
| const body: CreateFeedBody = { | |
| isbn: isbnToSend, | |
| contentBody: postContent.trim(), | |
| isPublic: !isPrivate, | |
| ...(selectedTags.length ? { tagList: selectedTags } : {}), | |
| }; | |
| try { | |
| const result = await createNewFeed(body, filesOrUndefined); | |
| if (result?.success) { | |
| // onSuccess에서 이동 처리됨 | |
| return; | |
| } else { | |
| // useCreateFeed에서 서버 메시지를 스낵바로 띄움 | |
| // 80009면 다음 후보로 자동 재시도, 그 외면 바로 중단 | |
| // (result.errorCode를 반환하도록 훅을 확장했다면 여기서 체크) | |
| // 현재 훅은 errorCode를 안 주니, 다음 후보가 있으면 조용히 다음 루프 진행 | |
| } | |
| } catch (error) { | |
| console.error(`[CreatePost] Try #${i + 1} failed:`, error); | |
| // 네트워크/타임아웃 등은 바로 중단 | |
| break; | |
| } | |
| } | |
| for (let i = 0; i < Math.min(candidates.length, 3); i++) { | |
| const isbnToSend = candidates[i]; | |
| const body: CreateFeedBody = { | |
| isbn: isbnToSend, | |
| contentBody: postContent.trim(), | |
| isPublic: !isPrivate, | |
| ...(selectedTags.length ? { tagList: selectedTags } : {}), | |
| }; | |
| try { | |
| const result = await createNewFeed(body, filesOrUndefined); | |
| if (result?.success) { | |
| // onSuccess에서 이동 처리됨 | |
| return; | |
| } else { | |
| // ISBN 관련 오류(예: 80009)가 아닌 경우 중단 | |
| if (result.errorCode && result.errorCode !== 80009) { | |
| break; | |
| } | |
| // ISBN 오류인 경우 다음 후보로 계속 진행 | |
| } | |
| } catch (error) { | |
| console.error(`[CreatePost] Try #${i + 1} failed:`, error); | |
| // 네트워크/타임아웃 등은 바로 중단 | |
| break; | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around lines 75 to 106, the retry loop blindly
proceeds to the next ISBN candidate on any non-success result which masks
whether the failure is an ISBN-specific error (code 80009) or a different
server/client error; update the flow so the createNewFeed/useCreateFeed hook
returns a typed errorCode (extend CreateFeed result type) and change the loop to
inspect result.errorCode: if result.success break/return; if errorCode === 80009
continue to the next candidate; otherwise break and surface the error (or show
snackbar) so non-ISBN problems don’t trigger retries; keep the catch behavior to
break on network/timeout exceptions and log appropriately.
| setSelectedPhotos(prev => [...prev, ...files].slice(0, 3)); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
사진 개수 제한 로직 개선 필요
현재 로직은 3장을 초과하는 사진을 자동으로 잘라내지만, 사용자에게 알림 없이 진행됩니다.
사용자에게 제한을 알리는 것이 좋습니다:
const handlePhotoAdd = (files: File[]) => {
- setSelectedPhotos(prev => [...prev, ...files].slice(0, 3));
+ setSelectedPhotos(prev => {
+ const newPhotos = [...prev, ...files];
+ if (newPhotos.length > 3) {
+ openSnackbar({
+ message: '사진은 최대 3장까지 선택할 수 있습니다.',
+ variant: 'top',
+ onClose: closePopup,
+ });
+ return newPhotos.slice(0, 3);
+ }
+ return newPhotos;
+ });
};📝 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.
| setSelectedPhotos(prev => [...prev, ...files].slice(0, 3)); | |
| }; | |
| const handlePhotoAdd = (files: File[]) => { | |
| setSelectedPhotos(prev => { | |
| const newPhotos = [...prev, ...files]; | |
| if (newPhotos.length > 3) { | |
| openSnackbar({ | |
| message: '사진은 최대 3장까지 선택할 수 있습니다.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return newPhotos.slice(0, 3); | |
| } | |
| return newPhotos; | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around lines 127-128, the current
setSelectedPhotos call silently slices photos to 3 items; change this so when
the combined existing + new files exceed 3 you only add up to the limit and show
a user-facing notification (toast/alert) informing them the 3-photo limit was
reached. Implement a check before updating state: compute allowedCount = max(0,
3 - prev.length), if allowedCount is 0 show the notification and do not change
state; otherwise add files.slice(0, allowedCount) and show a notification if
files.length > allowedCount. Ensure the UX prevents silent truncation and
provides clear feedback.
| const handleTagToggle = (tag: string) => { | ||
| setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
태그 개수 제한 확인 필요
태그를 5개까지만 허용한다고 하셨는데, 현재 토글 로직에는 제한이 없습니다.
const handleTagToggle = (tag: string) => {
- setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]));
+ setSelectedTags(prev => {
+ if (prev.includes(tag)) {
+ return prev.filter(t => t !== tag);
+ }
+ if (prev.length >= 5) {
+ openSnackbar({
+ message: '태그는 최대 5개까지 선택할 수 있습니다.',
+ variant: 'top',
+ onClose: closePopup,
+ });
+ return prev;
+ }
+ return [...prev, tag];
+ });
};📝 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 handleTagToggle = (tag: string) => { | |
| setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag])); | |
| }; | |
| const handleTagToggle = (tag: string) => { | |
| setSelectedTags(prev => { | |
| if (prev.includes(tag)) { | |
| return prev.filter(t => t !== tag); | |
| } | |
| if (prev.length >= 5) { | |
| openSnackbar({ | |
| message: '태그는 최대 5개까지 선택할 수 있습니다.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return prev; | |
| } | |
| return [...prev, tag]; | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In src/pages/post/CreatePost.tsx around lines 136-138, the tag toggle handler
currently allows unlimited tags; modify handleTagToggle so when toggling ON it
first checks selectedTags length and prevents adding more than 5 (either return
early or trigger the existing UI validation/notification), while still allowing
toggling OFF to remove tags; implement the check inside the setSelectedTags
updater (or before calling it) to ensure no race conditions and keep toggle
behavior idempotent.
| export const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | ||
| export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn); | ||
|
|
There was a problem hiding this comment.
isIsbn10/isIsbn13가 형식만 검사함 — 체크디지트 검증 누락
PR 요약에 명시된 “정규화 및 체크디지트 검증”과 달리 현재 함수는 정규표현식 기반 형식만 확인합니다. 실제 체크디지트를 검증하도록 수정해야 잘못된 ISBN을 걸러낼 수 있습니다.
아래처럼 체크디지트 검증을 포함하도록 교체 제안:
-export const isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn);
-export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn);
+export const isIsbn10 = (isbn: string) => {
+ const n = normalizeIsbn(isbn);
+ if (!/^[0-9]{9}[0-9X]$/.test(n)) return false;
+ const digits = n.split('').map((ch) => (ch === 'X' ? 10 : Number(ch)));
+ const sum = digits.reduce((acc, d, idx) => acc + d * (10 - idx), 0);
+ return sum % 11 === 0;
+};
+
+export const isIsbn13 = (isbn: string) => {
+ const n = normalizeIsbn(isbn);
+ if (!/^[0-9]{13}$/.test(n)) return false;
+ const sum = n
+ .slice(0, 12)
+ .split('')
+ .map(Number)
+ .reduce((acc, d, idx) => acc + d * (idx % 2 === 0 ? 1 : 3), 0);
+ const check = (10 - (sum % 10)) % 10;
+ return check === Number(n[12]);
+};📝 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 isIsbn10 = (isbn: string) => /^[0-9]{9}[0-9X]$/.test(isbn); | |
| export const isIsbn13 = (isbn: string) => /^[0-9]{13}$/.test(isbn); | |
| export const isIsbn10 = (isbn: string) => { | |
| const n = normalizeIsbn(isbn); | |
| if (!/^[0-9]{9}[0-9X]$/.test(n)) return false; | |
| const digits = n.split('').map((ch) => (ch === 'X' ? 10 : Number(ch))); | |
| const sum = digits.reduce((acc, d, idx) => acc + d * (10 - idx), 0); | |
| return sum % 11 === 0; | |
| }; | |
| export const isIsbn13 = (isbn: string) => { | |
| const n = normalizeIsbn(isbn); | |
| if (!/^[0-9]{13}$/.test(n)) return false; | |
| const sum = n | |
| .slice(0, 12) | |
| .split('') | |
| .map(Number) | |
| .reduce((acc, d, idx) => acc + d * (idx % 2 === 0 ? 1 : 3), 0); | |
| const check = (10 - (sum % 10)) % 10; | |
| return check === Number(n[12]); | |
| }; |
🤖 Prompt for AI Agents
In src/utils/isbn.ts around lines 3 to 5, the isIsbn10/isIsbn13 functions only
check format via regex but must also validate the checksum; replace them with
implementations that first normalize input (remove hyphens and spaces,
uppercase), then for ISBN-10 verify length 10 and compute sum of (digit *
weight) where weights 10..1 and accept 'X' as digit 10 for the check digit with
total % 11 === 0, and for ISBN-13 verify length 13 and compute weighted sum
using weights 1 and 3 alternately for the first 12 digits and ensure the
calculated check digit (10 - (sum % 10)) % 10 equals the 13th digit; return true
only if both format and checksum pass.
| /** 하이픈/공백 제거 → 10이면 13으로 변환 → 최종 13자리 숫자 반환 */ | ||
| export const ensureIsbn13 = (raw: string): string | null => { | ||
| const n = normalizeIsbn(raw); | ||
| if (isIsbn13(n)) return n; | ||
| if (isIsbn10(n)) return isbn10to13(n); | ||
| return null; | ||
| }; |
There was a problem hiding this comment.
ensureIsbn13가 형식만 통과시키는 문제 — 유효성 검증 반영 필요
위의 수정과 연계하여, ensureIsbn13도 “유효한” 13자리거나 “유효한” 10자리에서 변환된 경우에만 값을 반환하도록 유지되어야 합니다. isIsbn10/isIsbn13를 체크디지트 검증으로 바꾸면 본 함수는 아래 diff만으로 목적에 부합합니다.
export const ensureIsbn13 = (raw: string): string | null => {
const n = normalizeIsbn(raw);
- if (isIsbn13(n)) return n;
- if (isIsbn10(n)) return isbn10to13(n);
+ if (isIsbn13(n)) return n; // 13자리 '유효'
+ if (isIsbn10(n)) return isbn10to13(n); // 10자리 '유효' → 13으로 변환
return null;
};📝 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.
| /** 하이픈/공백 제거 → 10이면 13으로 변환 → 최종 13자리 숫자 반환 */ | |
| export const ensureIsbn13 = (raw: string): string | null => { | |
| const n = normalizeIsbn(raw); | |
| if (isIsbn13(n)) return n; | |
| if (isIsbn10(n)) return isbn10to13(n); | |
| return null; | |
| }; | |
| /** 하이픈/공백 제거 → 10이면 13으로 변환 → 최종 13자리 숫자 반환 */ | |
| export const ensureIsbn13 = (raw: string): string | null => { | |
| const n = normalizeIsbn(raw); | |
| if (isIsbn13(n)) return n; // 13자리 '유효' | |
| if (isIsbn10(n)) return isbn10to13(n); // 10자리 '유효' → 13으로 변환 | |
| return null; | |
| }; |
#️⃣연관된 이슈
#94
📝작업 내용
새 글 작성 기능의 완전한 구현과 피드에서 이미지가 표시되지 않는 문제를 해결했습니다.
주요 해결 과정:
1. 새 글 작성 API 연동 구현
2. 피드 이미지 렌더링 문제 해결
3. 사용자 경험 개선
기술적 세부사항:
스크린샷 (선택)
💬리뷰 요구사항(선택)
타입 정의 일관성: contentUrls vs contentsUrl 처럼 서버 응답과 클라이언트 타입 정의가 불일치하는 다른 필드들이 있는지 확인해주시면 좋겠습니다.
Summary by CodeRabbit
New Features
Refactor