Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Walkthrough피드 수정 기능을 추가하고 연동했습니다. 피드 상세 타입 일부를 변경(aliasName→alias, isPublic 추가)했으며, 업데이트 API 클라이언트와 훅(useUpdateFeed), 수정 페이지(UpdatePost), 라우트, 상세 페이지의 수정 이동을 구현했습니다. 게시글 사진/도서 선택 컴포넌트는 읽기 전용·편집 모드와 기존 이미지 처리 로직이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant FeedDetailPage
participant Router
participant UpdatePost
participant API as getFeedDetail/updateFeed
participant Server
User->>FeedDetailPage: 더보기 > 수정
FeedDetailPage->>Router: navigate(/post/update/:feedId)
Router->>UpdatePost: mount
UpdatePost->>API: getFeedDetail(feedId)
API->>Server: GET /feeds/{feedId}
Server-->>API: FeedDetailData (alias, isPublic, images, ...)
API-->>UpdatePost: 데이터 반환
UpdatePost->>User: 수정 UI 표시
User->>UpdatePost: 완료 버튼 클릭
UpdatePost->>API: updateFeed(feedId, body)
API->>Server: PATCH /feeds/{feedId} (JSON)
alt JSON 실패
API->>Server: PATCH /feeds/{feedId} (multipart/form-data)
end
Server-->>API: UpdateFeedResponse
API-->>UpdatePost: 결과 반환
UpdatePost->>Router: navigate(/feed/{feedId})
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ 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: 7
🔭 Outside diff range comments (1)
src/api/feeds/getFeedDetail.ts (1)
9-22: aliasName → alias 네이밍 및 공개 여부 필드 일관성 점검 필요aliasName 잔여 사용처가 다수 발견되어, getFeedDetail.ts에서 도입한 alias로 완전 교체가 필요합니다. 또한 API·타입은 isPublic을 사용하지만, UI 토글(state)·mock·일부 컴포넌트에서는 isPrivate을 직접 쓰고 있어 혼용되고 있습니다. 변환 로직을 명확히 하거나, 네이밍을 통일해 주세요.
수정이 필요한 주요 위치:
- 타입 정의
• src/types/user.ts, profile.ts, follow.ts
• src/api/users/** (postSignup.ts, patchProfile.ts, getUsers.ts, getMyProfile.ts, getAlias.ts)
• src/api/comments/getComments.ts- 페이지
• src/pages/signup/SignupDone.tsx, SignupGenre.tsx
• src/pages/mypage/EditPage.tsx, Mypage.tsx
• src/pages/feed/FollowerListPage.tsx- mock·데이터
• src/data/userData.ts
• src/mocks/searchBook.mock.ts, src/data/postData.ts, src/mocks/groupDetail.mock.ts- 컴포넌트
• src/components/feed/** (UserProfileItem.tsx, Profile.tsx, MyFeed.tsx, OtherFeed.tsx)
• src/components/common/Post/** (PostHeader.tsx, Reply.tsx, SubReply.tsx, PostFooter.tsx)- 공개 여부 필드
• API/타입: isPublic (통일)
• UI 토글·mock: isPrivate 사용 → isPublic의 반대 개념이라면, 변수명·props명 통일 또는 매핑 로직 주석·함수로 분리위 목록을 참고하여 아래 스크립트로 남은 aliasName 및 isPrivate 검색 후 모두 교체·검증해 주세요.
rg -n '\baliasName\b' rg -n '\bisPrivate\b'
🧹 Nitpick comments (9)
src/components/creategroup/BookSelectionSection.tsx (3)
39-57: 표지 플레이스홀더 인라인 스타일 제거 권장플레이스홀더 div에 인라인 스타일이 많아 스타일 일관성과 재사용성이 떨어집니다. .styled.ts로 이동해 컴포넌트화하면 유지보수성이 개선됩니다.
예시: BookSelectionSection.styled.ts에 추가
export const PlaceholderCover = styled.div` width: 60px; height: 80px; background-color: ${semanticColors.background.cardDark}; display: flex; align-items: center; justify-content: center; font-size: 10px; color: ${semanticColors.text.tertiary}; border-radius: 4px; `;그리고 사용부를 다음과 같이 단순화:
- {selectedBook.cover && selectedBook.cover.trim() !== '' ? ( - <img src={selectedBook.cover} alt={selectedBook.title} /> - ) : ( - <div style={...}>책표지</div> - )} + {selectedBook.cover && selectedBook.cover.trim() !== '' ? ( + <img src={selectedBook.cover} alt={`${selectedBook.title} 표지`} /> + ) : ( + <PlaceholderCover>책표지</PlaceholderCover> + )}
40-41: 대체 텍스트(alt) 더 구체화 제안접근성 개선을 위해 alt에 '표지' 등 맥락을 포함해 주세요.
- <img src={selectedBook.cover} alt={selectedBook.title} /> + <img src={selectedBook.cover} alt={`${selectedBook.title} 표지`} />
71-73: 텍스트 색상 인라인 스타일 제거 제안span의 색상 스타일도 .styled.ts로 이동하면 스타일 일관성이 향상됩니다. SearchBox 내부 텍스트 공통 스타일 컴포넌트를 고려해 주세요.
src/pages/index.tsx (1)
14-14: 초기 번들 크기 축소를 위한 코드 스플리팅 제안UpdatePost 페이지는 사용 빈도에 따라 지연 로딩이 적합합니다. React.lazy + Suspense로 코드 스플리팅을 고려해 주세요.
예시:
-import UpdatePost from './post/UpdatePost'; +import { lazy, Suspense } from 'react'; +const UpdatePost = lazy(() => import('./post/UpdatePost'));라우트 사용부:
-<Route path="post/update/:feedId" element={<UpdatePost />} /> +<Route + path="post/update/:feedId" + element={ + <Suspense fallback={null}> + <UpdatePost /> + </Suspense> + } +/>Also applies to: 55-56
src/hooks/useUpdateFeed.ts (2)
13-16: 연속 클릭으로 인한 중복 요청 방지이미 로딩 중일 때는 즉시 반환하여 중복 PATCH를 예방하세요.
- const updateExistingFeed = async (feedId: number, body: UpdateFeedBody) => { + const updateExistingFeed = async (feedId: number, body: UpdateFeedBody) => { + if (loading) { + return { success: false as const, error: '요청이 이미 진행 중입니다.' }; + } try { setLoading(true);
13-13: 반환 타입을 명시해 호출부 가독성과 타입 안전성 향상명시적 반환 타입을 선언하면 사용하는 곳에서 분기 처리가 명확해집니다.
- const updateExistingFeed = async (feedId: number, body: UpdateFeedBody) => { + const updateExistingFeed = async ( + feedId: number, + body: UpdateFeedBody, + ): Promise<{ success: true; feedId: number } | { success: false; error?: string }> => {src/pages/post/UpdatePost.tsx (2)
121-127: 사용하지 않는 핸들러 함수들을 제거하세요
handlePhotoAdd와handlePhotoRemove함수가 빈 구현으로 남아있습니다. 수정 모드에서는 새 이미지 추가가 불가능하다는 요구사항에 따라 이 함수들은 사용되지 않으므로 제거하는 것이 좋습니다.- const handlePhotoAdd = () => { - return; - }; - - const handlePhotoRemove = () => { - return; - };그리고 Line 176-177의 PhotoSection props도 수정:
<PhotoSection photos={selectedPhotos} - onPhotoAdd={handlePhotoAdd} - onPhotoRemove={handlePhotoRemove} + onPhotoAdd={() => {}} + onPhotoRemove={() => {}} existingImageUrls={remainImageUrls} onExistingImageRemove={handleExistingImageRemove} isEditMode={true} />
110-111: 빈 배열에 대한 불필요한 spread 연산자 사용
remainImageUrls가 빈 배열일 때도 객체에 포함시키고 있습니다. 서버 API가 빈 배열을 처리하는 방식에 따라 의도하지 않은 동작이 발생할 수 있습니다.- ...(remainImageUrls.length ? { remainImageUrls } : {}), + ...(remainImageUrls.length > 0 ? { remainImageUrls } : {}),src/components/createpost/PhotoSection.tsx (1)
37-39: 조건문 로직 개선 가능
readOnly와isEditMode가 동시에 true인 경우는 논리적으로 모순됩니다. 더 명확한 조건 처리가 필요합니다.const handleFileInputClick = () => { - if (readOnly || isEditMode) return; + // 수정 모드나 읽기 전용 모드에서는 새 사진 추가 불가 + if (readOnly || isEditMode) { + console.warn('수정 모드 또는 읽기 전용 모드에서는 새 사진을 추가할 수 없습니다.'); + return; + } fileInputRef.current?.click(); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/api/feeds/getFeedDetail.ts(3 hunks)src/api/feeds/updateFeed.ts(1 hunks)src/components/creategroup/BookSelectionSection.tsx(1 hunks)src/components/createpost/PhotoSection.tsx(2 hunks)src/hooks/useUpdateFeed.ts(1 hunks)src/pages/feed/FeedDetailPage.tsx(1 hunks)src/pages/index.tsx(2 hunks)src/pages/post/UpdatePost.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/pages/post/UpdatePost.tsx (5)
src/types/book.ts (1)
Book(1-7)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/hooks/useUpdateFeed.ts (1)
useUpdateFeed(9-78)src/api/feeds/getFeedDetail.ts (1)
getFeedDetail(34-37)src/api/feeds/updateFeed.ts (1)
UpdateFeedBody(4-9)
src/components/creategroup/BookSelectionSection.tsx (2)
src/components/creategroup/BookSelectionSection.styled.ts (8)
SearchBox(4-20)SelectedBookContainer(33-38)SelectedBookCover(40-52)SelectedBookInfo(54-59)SelectedBookTitle(61-66)SelectedBookAuthor(68-72)ChangeButton(74-84)SearchIcon(22-31)src/styles/global/global.ts (1)
semanticColors(80-153)
src/api/feeds/updateFeed.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/hooks/useUpdateFeed.ts (2)
src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/feeds/updateFeed.ts (3)
UpdateFeedBody(4-9)UpdateFeedResponse(25-25)updateFeed(33-55)
src/components/createpost/PhotoSection.tsx (1)
src/components/createpost/PhotoSection.styled.ts (6)
PhotoContainer(4-8)PhotoGrid(10-14)AddPhotoButton(16-41)PhotoImage(49-54)RemoveButton(56-78)PhotoCount(80-85)
🔇 Additional comments (3)
src/pages/index.tsx (1)
14-14: 업데이트 라우트 추가 적절함UpdatePost 라우트와 navigate 경로가 일치하고, 기존 라우트에 영향 없습니다. 변경 사항 문제 없어 보입니다.
Also applies to: 55-56
src/pages/feed/FeedDetailPage.tsx (1)
90-93: feedId 가드 추가로 잠재적 잘못된 네비게이션 방지경로 파라미터가 보장되지 않는 상황(비정상 진입, 테스트 렌더 등)에서 /post/update/undefined로 이동할 수 있습니다. 가드를 추가하세요.
- onEdit: () => { - closePopup(); - navigate(`/post/update/${feedId}`); - }, + onEdit: () => { + if (!feedId) { + console.warn('feedId가 없어 수정 페이지로 이동할 수 없습니다.'); + closePopup(); + return; + } + closePopup(); + navigate(`/post/update/${feedId}`); + },src/hooks/useUpdateFeed.ts (1)
44-78: 성공/실패 핸들링 및 스낵바 사용 패턴 적절성공/실패 분기와 스낵바 피드백, finally에서의 로딩 종료 처리 모두 적절합니다.
| try { | ||
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
|
|
||
| return data; | ||
| } catch (error) { | ||
| console.error('수정 API 에러:', error); | ||
|
|
||
| const form = new FormData(); | ||
| form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' })); | ||
|
|
||
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| }); | ||
|
|
||
| return data; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
오류 처리 로직이 비직관적입니다
JSON 요청 실패 시 무조건 FormData로 재시도하는 현재 구조는 실제 네트워크 오류나 다른 문제를 숨길 수 있습니다. 서버가 500 에러를 반환하는 특정 상황에서만 FormData로 폴백하도록 개선이 필요합니다.
export const updateFeed = async (
feedId: number,
body: UpdateFeedBody,
): Promise<UpdateFeedResponse> => {
try {
const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, {
headers: { 'Content-Type': 'application/json' },
});
return data;
} catch (error) {
- console.error('수정 API 에러:', error);
+ // axios 에러이고 500 에러인 경우에만 FormData로 재시도
+ if (axios.isAxiosError(error) && error.response?.status === 500) {
+ console.log('500 에러 발생, FormData로 재시도');
- const form = new FormData();
- form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));
+ const form = new FormData();
+ form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));
- const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
+ const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
- return data;
+ return data;
+ }
+
+ // 다른 에러는 그대로 전파
+ throw error;
}
};axios import도 추가해야 합니다:
+import axios from 'axios';
import { apiClient } from '../index';📝 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.
| try { | |
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| return data; | |
| } catch (error) { | |
| console.error('수정 API 에러:', error); | |
| const form = new FormData(); | |
| form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' })); | |
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | |
| headers: { 'Content-Type': 'multipart/form-data' }, | |
| }); | |
| return data; | |
| } | |
| +import axios from 'axios'; | |
| import { apiClient } from '../index'; | |
| export const updateFeed = async ( | |
| feedId: number, | |
| body: UpdateFeedBody, | |
| ): Promise<UpdateFeedResponse> => { | |
| try { | |
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| }); | |
| return data; | |
| } catch (error) { | |
| - console.error('수정 API 에러:', error); | |
| + // axios 에러이고 500 에러인 경우에만 FormData로 재시도 | |
| + if (axios.isAxiosError(error) && error.response?.status === 500) { | |
| + console.log('500 에러 발생, FormData로 재시도'); | |
| + | |
| + const form = new FormData(); | |
| + form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' })); | |
| + | |
| + const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | |
| + headers: { 'Content-Type': 'multipart/form-data' }, | |
| + }); | |
| + | |
| + return data; | |
| + } | |
| + | |
| + // 다른 에러는 그대로 전파 | |
| + throw error; | |
| } | |
| }; |
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| }); |
There was a problem hiding this comment.
Content-Type 헤더를 명시적으로 설정하지 마세요
FormData를 전송할 때 Content-Type: multipart/form-data를 명시적으로 설정하면 boundary 파라미터가 누락됩니다. axios가 자동으로 올바른 헤더를 설정하도록 해야 합니다.
const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
- headers: { 'Content-Type': 'multipart/form-data' },
+ // Content-Type 헤더를 제거하여 axios가 자동으로 설정하도록 함
});📝 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.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | |
| headers: { 'Content-Type': 'multipart/form-data' }, | |
| }); | |
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | |
| // Content-Type 헤더를 제거하여 axios가 자동으로 설정하도록 함 | |
| }); |
🤖 Prompt for AI Agents
In src/api/feeds/updateFeed.ts around lines 49 to 51, the request explicitly
sets 'Content-Type: multipart/form-data', which omits the required boundary and
breaks FormData uploads; remove the explicit headers option (or omit the
Content-Type entry) so axios can automatically set the correct Content-Type
including the boundary when sending FormData.
| <SearchBox | ||
| hasSelectedBook={!!selectedBook} | ||
| onClick={selectedBook ? undefined : onSearchClick} | ||
| onClick={selectedBook || readOnly ? undefined : onSearchClick} | ||
| > |
There was a problem hiding this comment.
🛠️ Refactor suggestion
읽기 전용일 때 커서/상호작용 불일치 수정 제안
readOnly이거나 이미 선택된 상태면 onClick을 제거하셨지만, SearchBox는 여전히 pointer 커서를 표시합니다. 사용성/접근성 측면에서 혼란을 줄 수 있으니 role/aria-disabled와 커서 스타일을 조건부로 맞춰주세요.
다음과 같이 보완을 제안드립니다.
- <SearchBox
- hasSelectedBook={!!selectedBook}
- onClick={selectedBook || readOnly ? undefined : onSearchClick}
- >
+ <SearchBox
+ hasSelectedBook={!!selectedBook}
+ onClick={selectedBook || readOnly ? undefined : onSearchClick}
+ role={!selectedBook && !readOnly ? 'button' : undefined}
+ aria-disabled={Boolean(selectedBook || readOnly)}
+ style={{ cursor: selectedBook || readOnly ? 'default' : 'pointer' }}
+ >📝 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.
| <SearchBox | |
| hasSelectedBook={!!selectedBook} | |
| onClick={selectedBook ? undefined : onSearchClick} | |
| onClick={selectedBook || readOnly ? undefined : onSearchClick} | |
| > | |
| <SearchBox | |
| hasSelectedBook={!!selectedBook} | |
| onClick={selectedBook || readOnly ? undefined : onSearchClick} | |
| role={!selectedBook && !readOnly ? 'button' : undefined} | |
| aria-disabled={Boolean(selectedBook || readOnly)} | |
| style={{ cursor: selectedBook || readOnly ? 'default' : 'pointer' }} | |
| > |
| {existingImageUrls.map((imageUrl, index) => ( | ||
| <div | ||
| key={`existing-${index}`} | ||
| style={{ position: 'relative', width: '80px', height: '80px' }} | ||
| > | ||
| <PhotoImage src={imageUrl} alt={`기존 이미지 ${index + 1}`} /> | ||
| {!readOnly && onExistingImageRemove && ( | ||
| <RemoveButton onClick={() => onExistingImageRemove(imageUrl)}> | ||
| <img src={closeIcon} alt="삭제" /> | ||
| </RemoveButton> | ||
| )} | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
이미지 URL 메모리 누수 가능성
createImageUrl 함수로 생성된 객체 URL이 해제되지 않고 있습니다. 컴포넌트가 언마운트되거나 이미지가 제거될 때 URL.revokeObjectURL()을 호출해야 메모리 누수를 방지할 수 있습니다.
useEffect를 추가하여 cleanup 처리:
+import { useRef, useEffect } from 'react';
const PhotoSection = ({
// ... props
}: PhotoSectionProps) => {
const fileInputRef = useRef<HTMLInputElement>(null);
+ const objectUrlsRef = useRef<string[]>([]);
const createImageUrl = (file: File) => {
- return URL.createObjectURL(file);
+ const url = URL.createObjectURL(file);
+ objectUrlsRef.current.push(url);
+ return url;
};
+ useEffect(() => {
+ return () => {
+ // 컴포넌트 언마운트 시 모든 객체 URL 해제
+ objectUrlsRef.current.forEach(url => URL.revokeObjectURL(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.
| {existingImageUrls.map((imageUrl, index) => ( | |
| <div | |
| key={`existing-${index}`} | |
| style={{ position: 'relative', width: '80px', height: '80px' }} | |
| > | |
| <PhotoImage src={imageUrl} alt={`기존 이미지 ${index + 1}`} /> | |
| {!readOnly && onExistingImageRemove && ( | |
| <RemoveButton onClick={() => onExistingImageRemove(imageUrl)}> | |
| <img src={closeIcon} alt="삭제" /> | |
| </RemoveButton> | |
| )} | |
| </div> | |
| ))} | |
| // At the top of src/components/createpost/PhotoSection.tsx | |
| +import { useRef, useEffect } from 'react'; | |
| import React from 'react'; | |
| // …other imports | |
| const PhotoSection = ({ | |
| // … props | |
| }: PhotoSectionProps) => { | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| + const objectUrlsRef = useRef<string[]>([]); | |
| const createImageUrl = (file: File) => { | |
| - return URL.createObjectURL(file); | |
| + const url = URL.createObjectURL(file); | |
| + objectUrlsRef.current.push(url); | |
| + return url; | |
| }; | |
| + useEffect(() => { | |
| + return () => { | |
| + // 컴포넌트 언마운트 시 모든 객체 URL 해제 | |
| + objectUrlsRef.current.forEach(url => URL.revokeObjectURL(url)); | |
| + }; | |
| + }, []); | |
| // …rest of component (rendering existingImageUrls, handling file input, etc.) | |
| }; |
🤖 Prompt for AI Agents
In src/components/createpost/PhotoSection.tsx around lines 70-82, existing image
object URLs created by createImageUrl are never revoked; add logic to revoke
them when an image is removed and when the component unmounts to prevent memory
leaks. Track object URLs created or passed in (e.g., with a Set or ref), call
URL.revokeObjectURL(url) when onExistingImageRemove is invoked for a blob/object
URL (skip external http(s) URLs), remove the URL from the tracker, and add a
useEffect cleanup that iterates remaining tracked URLs and revokes them on
unmount.
| // ===== 클라이언트 선검증 ===== | ||
| if (body.tagList) { | ||
| // 최대 5개 | ||
| if (body.tagList.length > 5) { | ||
| openSnackbar({ | ||
| message: '태그는 최대 5개까지 입력할 수 있어요.', | ||
| variant: 'top', | ||
| onClose: closePopup, | ||
| }); | ||
| return { success: false as const }; | ||
| } | ||
| // 중복 제거 체크 | ||
| const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); | ||
| const uniq = new Set(trimmed); | ||
| if (uniq.size !== trimmed.length) { | ||
| openSnackbar({ | ||
| message: '태그는 중복될 수 없어요.', | ||
| variant: 'top', | ||
| onClose: closePopup, | ||
| }); | ||
| return { success: false as const }; | ||
| } | ||
| } | ||
| // ===== 선검증 끝 ===== | ||
|
|
||
| const res: UpdateFeedResponse = await updateFeed(feedId, body); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
태그 전처리를 실제 요청 본문에 반영하세요(공백 제거/중복 제거).
현재는 트리밍/중복 검사를 하지만, 정제된 결과를 body에 반영하지 않아 서버로 공백 포함 태그가 전송될 수 있습니다. 또한 최대 5개 검사는 dedupe 후의 개수 기준으로 하는 것이 안전합니다.
- // ===== 클라이언트 선검증 =====
- if (body.tagList) {
- // 최대 5개
- if (body.tagList.length > 5) {
+ // ===== 클라이언트 선검증 및 정규화 =====
+ let normalizedBody: UpdateFeedBody = body;
+ if (body.tagList) {
+ const trimmed = body.tagList.map(t => t.trim()).filter(Boolean);
+ const deduped = Array.from(new Set(trimmed));
+ // 최대 5개 (정규화 이후 기준)
+ if (deduped.length > 5) {
openSnackbar({
message: '태그는 최대 5개까지 입력할 수 있어요.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
- // 중복 제거 체크
- const trimmed = body.tagList.map(t => t.trim()).filter(Boolean);
- const uniq = new Set(trimmed);
- if (uniq.size !== trimmed.length) {
- openSnackbar({
- message: '태그는 중복될 수 없어요.',
- variant: 'top',
- onClose: closePopup,
- });
- return { success: false as const };
- }
+ // 공백 제거 및 중복 제거 반영
+ normalizedBody = { ...body, tagList: deduped };
}
- // ===== 선검증 끝 =====
+ // ===== 선검증 끝 =====
- const res: UpdateFeedResponse = await updateFeed(feedId, body);
+ const res: UpdateFeedResponse = await updateFeed(feedId, normalizedBody);참고:
- UX 측면에서 중복 발견 시 하드 실패 대신 자동 정규화(공백 제거 및 dedupe) 후 전송을 추천합니다. 필요 시 "중복 태그를 자동으로 정리했어요." 같은 토스트 안내를 추가할 수 있습니다.
📝 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.
| // ===== 클라이언트 선검증 ===== | |
| if (body.tagList) { | |
| // 최대 5개 | |
| if (body.tagList.length > 5) { | |
| openSnackbar({ | |
| message: '태그는 최대 5개까지 입력할 수 있어요.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } | |
| // 중복 제거 체크 | |
| const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); | |
| const uniq = new Set(trimmed); | |
| if (uniq.size !== trimmed.length) { | |
| openSnackbar({ | |
| message: '태그는 중복될 수 없어요.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } | |
| } | |
| // ===== 선검증 끝 ===== | |
| const res: UpdateFeedResponse = await updateFeed(feedId, body); | |
| // ===== 클라이언트 선검증 및 정규화 ===== | |
| let normalizedBody: UpdateFeedBody = body; | |
| if (body.tagList) { | |
| const trimmed = body.tagList.map(t => t.trim()).filter(Boolean); | |
| const deduped = Array.from(new Set(trimmed)); | |
| // 최대 5개 (정규화 이후 기준) | |
| if (deduped.length > 5) { | |
| openSnackbar({ | |
| message: '태그는 최대 5개까지 입력할 수 있어요.', | |
| variant: 'top', | |
| onClose: closePopup, | |
| }); | |
| return { success: false as const }; | |
| } | |
| // 공백 제거 및 중복 제거 반영 | |
| normalizedBody = { ...body, tagList: deduped }; | |
| } | |
| // ===== 선검증 끝 ===== | |
| const res: UpdateFeedResponse = await updateFeed(feedId, normalizedBody); |
🤖 Prompt for AI Agents
In src/hooks/useUpdateFeed.ts around lines 17 to 43, the code trims and checks
tags but never updates body.tagList so the unclean tags are sent and the max-5
check runs before dedupe; modify the logic to first compute a normalized array
(trim each tag, filter out empty strings, dedupe while preserving order), then
set body.tagList = normalized; enforce the 5-tag limit after normalization
(either slice to first 5 or return failure—prefer slicing for auto-normalization
UX), and show an informational snackbar when tags were changed (e.g., "중복 태그를
자동으로 정리했어요.") before calling updateFeed.
| interface Book { | ||
| id: number; | ||
| title: string; | ||
| author: string; | ||
| cover: string; | ||
| isbn: string; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Book 타입을 별도로 정의하지 말고 기존 타입을 재사용하세요
프로젝트에 이미 src/types/book.ts에 Book 타입이 정의되어 있습니다. 로컬 Book 인터페이스를 삭제하고 기존 타입을 import하여 사용하세요.
-interface Book {
- id: number;
- title: string;
- author: string;
- cover: string;
- isbn: string;
-}
+import type { Book } from '@/types/book';그리고 Line 63-69에서 Book 객체를 생성할 때:
setSelectedBook({
- id: 0,
- title: data.bookTitle,
- author: data.bookAuthor,
- cover: '',
- isbn: data.isbn,
+ isbn: data.isbn,
+ title: data.bookTitle,
+ author: data.bookAuthor,
+ coverUrl: '', // cover 대신 coverUrl 사용
+ publisher: '', // publisher 필드 추가
});Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/pages/post/UpdatePost.tsx around lines 17-23 and 63-69, remove the local
Book interface and import the existing Book type from src/types/book.ts; replace
all references to the local interface with the imported type. When creating the
Book object at lines 63-69, type the new object with the imported Book (or use
Partial<Book>/Omit<Book, 'id'> if not all fields are present) and ensure the
object contains the required properties (id, title, author, cover, isbn) or is
correctly typed to reflect missing fields so TypeScript types align with the
shared type.
| }; | ||
|
|
||
| loadFeedDetail(); | ||
| }, [feedId]); |
There was a problem hiding this comment.
useEffect 의존성 배열에 필요한 의존성이 누락되었습니다
useEffect 내부에서 openSnackbar, closePopup, navigate 함수를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다. React의 exhaustive-deps 규칙을 위반하고 있습니다.
- }, [feedId]);
+ }, [feedId, openSnackbar, closePopup, navigate]);📝 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.
| }, [feedId]); | |
| }, [feedId, openSnackbar, closePopup, navigate]); |
🤖 Prompt for AI Agents
In src/pages/post/UpdatePost.tsx around line 89, the useEffect uses
openSnackbar, closePopup, and navigate inside its body but only lists feedId in
the dependency array; update the dependency array to include these functions (or
make them stable via useCallback/useRef) so exhaustive-deps is satisfied —
either add openSnackbar, closePopup, and navigate to the array, or memoize those
functions where they are defined and then include the stable references in the
array.
There was a problem hiding this comment.
Actionable comments posted: 6
🔭 Outside diff range comments (1)
src/components/createpost/PhotoSection.tsx (1)
52-54: 메모리 누수 가능성
createImageUrl함수에서 생성한 ObjectURL이 해제되지 않아 메모리 누수가 발생할 수 있습니다. 컴포넌트가 언마운트되거나 이미지가 제거될 때 URL을 해제해야 합니다.useEffect를 사용하여 cleanup 함수에서 ObjectURL을 해제:
+import { useRef, useEffect } from 'react'; const PhotoSection = ({ photos, onPhotoAdd, onPhotoRemove, existingImageUrls = [], onExistingImageRemove, readOnly = false, isEditMode = false, }: PhotoSectionProps) => { const fileInputRef = useRef<HTMLInputElement>(null); + const objectUrlsRef = useRef<string[]>([]); + + useEffect(() => { + // cleanup function to revoke object URLs + return () => { + objectUrlsRef.current.forEach(url => URL.revokeObjectURL(url)); + }; + }, []); const createImageUrl = (file: File) => { - return URL.createObjectURL(file); + const url = URL.createObjectURL(file); + objectUrlsRef.current.push(url); + return url; };또한 이미지가 제거될 때도 해당 URL을 해제하도록 수정:
{photos.map((photo, index) => ( <div key={`new-${index}`} style={{ position: 'relative', width: '80px', height: '80px' }} > <PhotoImage src={createImageUrl(photo)} alt={`새 이미지 ${index + 1}`} /> {!readOnly && ( - <RemoveButton onClick={() => onPhotoRemove(index)}> + <RemoveButton onClick={() => { + const url = objectUrlsRef.current[index]; + if (url) URL.revokeObjectURL(url); + objectUrlsRef.current.splice(index, 1); + onPhotoRemove(index); + }}> <img src={closeIcon} alt="삭제" /> </RemoveButton> )} </div> ))}
🧹 Nitpick comments (5)
src/components/creategroup/BookSelectionSection.tsx (1)
39-57: 인라인 스타일 대신 styled-component 사용 권장책 표지가 없을 때 렌더링되는 플레이스홀더 div에 인라인 스타일이 직접 적용되어 있습니다. 일관성과 유지보수성을 위해 styled-component로 분리하는 것이 좋습니다.
BookSelectionSection.styled.ts파일에 새 컴포넌트 추가:export const PlaceholderCover = styled.div` width: 60px; height: 80px; background-color: #333; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #999; border-radius: 4px; `;그 후 컴포넌트에서 사용:
- ) : ( - <div - style={{ - width: '60px', - height: '80px', - backgroundColor: '#333', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '10px', - color: '#999', - borderRadius: '4px', - }} - > - 책표지 - </div> - )} + ) : ( + <PlaceholderCover> + 책표지 + </PlaceholderCover> + )}src/components/createpost/PhotoSection.tsx (1)
71-82: 접근성 개선 필요기존 이미지를 보여주는 div 엘리먼트에 인라인 스타일이 사용되고 있으며, 이미지 삭제 버튼의 접근성 속성이 부족합니다.
styled-component로 분리하고 ARIA 속성 추가:
- {existingImageUrls.map((imageUrl, index) => ( - <div - key={`existing-${index}`} - style={{ position: 'relative', width: '80px', height: '80px' }} - > - <PhotoImage src={imageUrl} alt={`기존 이미지 ${index + 1}`} /> - {!readOnly && onExistingImageRemove && ( - <RemoveButton onClick={() => onExistingImageRemove(imageUrl)}> - <img src={closeIcon} alt="삭제" /> - </RemoveButton> - )} - </div> - ))} + {existingImageUrls.map((imageUrl, index) => ( + <PhotoWrapper key={`existing-${index}`}> + <PhotoImage src={imageUrl} alt={`기존 이미지 ${index + 1}`} /> + {!readOnly && onExistingImageRemove && ( + <RemoveButton + onClick={() => onExistingImageRemove(imageUrl)} + aria-label={`기존 이미지 ${index + 1} 삭제`} + > + <img src={closeIcon} alt="" /> + </RemoveButton> + )} + </PhotoWrapper> + ))}
PhotoSection.styled.ts에 추가:export const PhotoWrapper = styled.div` position: relative; width: 80px; height: 80px; `;src/hooks/useUpdateFeed.ts (1)
26-26: 함수 반환 타입 불일치
success: false as const는 TypeScript의 const assertion을 사용하지만, 반환 타입이 명시되지 않아 타입 추론에 의존합니다. 일관성을 위해 함수의 반환 타입을 명시적으로 선언하는 것이 좋습니다.- const updateExistingFeed = async (feedId: number, body: UpdateFeedBody) => { + const updateExistingFeed = async ( + feedId: number, + body: UpdateFeedBody + ): Promise<{ success: true; feedId: number } | { success: false; error?: string }> => {Also applies to: 37-37
src/pages/post/UpdatePost.tsx (2)
53-53: navigate(-1) 사용 시 주의사항
navigate(-1)은 브라우저 히스토리를 한 단계 뒤로 이동시킵니다. 하지만 사용자가 직접 URL로 접근한 경우 예상치 못한 페이지로 이동할 수 있습니다. 명시적인 경로로 리다이렉트하는 것이 더 안전합니다.- navigate(-1); + navigate('/feed'); // 또는 적절한 기본 페이지
111-111: 빈 배열도 remainImageUrls 필드로 전송됨remainImageUrls가 빈 배열일 때도 API 요청에 포함됩니다. 서버에서 빈 배열과 undefined를 다르게 처리할 수 있으므로, 빈 배열인 경우 필드를 제외하는 것이 더 명확할 수 있습니다.
현재 로직도 작동하지만, 더 명확한 의도 전달을 위해 다음과 같이 수정할 수 있습니다:
- ...(remainImageUrls.length ? { remainImageUrls } : {}), + ...(remainImageUrls.length > 0 ? { remainImageUrls } : {}),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/api/feeds/getFeedDetail.ts(3 hunks)src/api/feeds/updateFeed.ts(1 hunks)src/components/creategroup/BookSelectionSection.tsx(1 hunks)src/components/createpost/PhotoSection.tsx(2 hunks)src/hooks/useUpdateFeed.ts(1 hunks)src/pages/feed/FeedDetailPage.tsx(1 hunks)src/pages/index.tsx(2 hunks)src/pages/post/UpdatePost.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/api/feeds/updateFeed.ts (1)
src/api/index.ts (1)
apiClient(27-33)
src/pages/post/UpdatePost.tsx (5)
src/types/book.ts (1)
Book(1-7)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/hooks/useUpdateFeed.ts (1)
useUpdateFeed(9-78)src/api/feeds/getFeedDetail.ts (1)
getFeedDetail(34-37)src/api/feeds/updateFeed.ts (1)
UpdateFeedBody(4-9)
src/hooks/useUpdateFeed.ts (2)
src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/feeds/updateFeed.ts (3)
UpdateFeedBody(4-9)UpdateFeedResponse(25-25)updateFeed(33-55)
src/components/createpost/PhotoSection.tsx (1)
src/components/createpost/PhotoSection.styled.ts (6)
PhotoContainer(4-8)PhotoGrid(10-14)AddPhotoButton(16-41)PhotoImage(49-54)RemoveButton(56-78)PhotoCount(80-85)
🔇 Additional comments (5)
src/pages/index.tsx (1)
55-55: 라우트 경로 일관성 확인새로 추가된 수정 라우트
/post/update/:feedId가 기존 생성 라우트/post/create와 일관된 패턴을 따르고 있어 좋습니다.src/pages/feed/FeedDetailPage.tsx (1)
90-93: 수정 페이지 이동 구현 적절함피드 수정 버튼 클릭 시 팝업을 닫고 UpdatePost 페이지로 네비게이션하는 로직이 깔끔하게 구현되었습니다.
src/api/feeds/getFeedDetail.ts (1)
21-21: 새로운 필수 속성 추가 확인
isPublic속성이 필수 속성으로 추가되었습니다. 백엔드 API가 실제로 이 필드를 반환하는지 확인이 필요합니다.src/hooks/useUpdateFeed.ts (2)
73-74: finally 블록에서 상태 업데이트finally 블록에서
setLoading(false)를 호출하는 것은 좋은 패턴입니다. 하지만 컴포넌트가 언마운트된 후에도 실행될 수 있으므로 메모리 누수 방지를 위한 cleanup이 필요할 수 있습니다.
29-31: 태그 유효성 검증 로직 개선 필요현재 로직은 공백 문자만 있는 태그를 필터링하지만, 중복 검사 시 원본 배열과 Set의 크기를 비교합니다. 이로 인해 공백 태그가 제거되면서 크기가 달라져 잘못된 중복 오류가 발생할 수 있습니다.
- 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) { + openSnackbar({ + message: '태그는 중복될 수 없어요.', + variant: 'top', + onClose: closePopup, + }); + return { success: false as const }; + } + // 실제 전송할 태그 리스트 업데이트 + body.tagList = Array.from(uniq);Likely an incorrect or invalid review comment.
| creatorNickname: string; | ||
| creatorProfileImageUrl: string; | ||
| aliasName: string; | ||
| alias: string; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
속성명 변경이 하위 호환성을 깨뜨릴 수 있습니다
aliasName에서 alias로 속성명이 변경되었습니다. 이 인터페이스를 사용하는 다른 컴포넌트들이 여전히 aliasName을 참조하고 있을 수 있으니 확인이 필요합니다.
🏁 Script executed:
#!/bin/bash
# aliasName을 참조하는 다른 파일들 확인
rg "aliasName" --type ts --type tsx -A 2Length of output: 68
아래와 같이 .ts 및 .tsx 파일을 모두 검색하도록 명령을 수정해 다시 한 번 실행해 보시겠어요?
🏁 Script executed:
#!/bin/bash
# aliasName을 참조하는 모든 .ts/.tsx 파일 확인
rg "aliasName" --glob "*.ts" --glob "*.tsx" -n -C2Length of output: 11834
속성명 변경에 따른 하위 호환성 주의
aliasName이 alias로 변경되면서, 아래 위치들에서 여전히 aliasName을 참조하고 있어 런타임 오류가 발생할 수 있습니다. 모두 점검 후 alias로 이름을 통일하거나, 기존 속성을 매핑해 주세요.
– 타입 정의
• src/types/user.ts
• src/types/profile.ts
• src/types/follow.ts
– 샘플 데이터
• src/data/userData.ts
– API 인터페이스
• src/api/users/postSignup.ts
• src/api/users/patchProfile.ts
• src/api/users/getUsers.ts
• src/api/users/getMyProfile.ts
• src/api/users/getAlias.ts
• src/api/comments/getComments.ts (2곳)
– 컴포넌트
• src/components/feed/MyFeed.tsx
• src/components/feed/UserProfileItem.tsx
• src/components/feed/Profile.tsx
• src/components/feed/OtherFeed.tsx
• src/components/common/Post/SubReply.tsx
• src/components/common/Post/Reply.tsx
• src/components/common/Post/PostHeader.tsx
– 페이지
• src/pages/signup/SignupGenre.tsx
• src/pages/signup/SignupDone.tsx
• src/pages/mypage/Mypage.tsx
• src/pages/mypage/EditPage.tsx
• src/pages/feed/FollowerListPage.tsx
위치가 많으니 rg "aliasName" --glob "*.{ts,tsx}"로 한 번 더 전체 검색 후 일괄 리팩토링을 권장드립니다.
🤖 Prompt for AI Agents
In src/api/feeds/getFeedDetail.ts around line 9, the property name was changed
from aliasName to alias but many files still reference aliasName causing runtime
errors; search the repo for "aliasName" (e.g. rg "aliasName" --glob
"*.{ts,tsx}") and either rename those references to alias or add a compatibility
mapping (e.g. when reading objects, set alias = alias ?? aliasName) so both
shapes work; update the listed type definitions (src/types/user.ts,
src/types/profile.ts, src/types/follow.ts), sample data (src/data/userData.ts),
API handlers (src/api/users/* and src/api/comments/getComments.ts), components
(all listed feed and post components), and pages to use the unified alias
property.
| * - multipart/form-data | ||
| * - request: application/json (Blob로 감싸 전송) | ||
| * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능 | ||
| */ |
There was a problem hiding this comment.
JSDoc 주석과 실제 구현의 불일치
JSDoc 주석에서는 multipart/form-data를 사용한다고 명시되어 있지만, 실제로는 application/json을 먼저 시도하고 실패 시에만 multipart/form-data로 폴백합니다. 주석을 정확하게 수정해야 합니다.
/**
* 피드 수정 API
- * - multipart/form-data
- * - request: application/json (Blob로 감싸 전송)
- * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능
+ * - 먼저 application/json으로 요청 시도
+ * - 실패 시 multipart/form-data로 폴백 (request 필드에 JSON Blob 포함)
+ * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능
*/📝 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.
| * - multipart/form-data | |
| * - request: application/json (Blob로 감싸 전송) | |
| * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능 | |
| */ | |
| /** | |
| * 피드 수정 API | |
| * - 먼저 application/json으로 요청 시도 | |
| * - 실패 시 multipart/form-data로 폴백 (request 필드에 JSON Blob 포함) | |
| * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능 | |
| */ |
🤖 Prompt for AI Agents
In src/api/feeds/updateFeed.ts around lines 29 to 32, the JSDoc incorrectly
states that the endpoint uses multipart/form-data while the implementation
actually tries application/json first (sending JSON wrapped in a Blob) and only
falls back to multipart/form-data; update the JSDoc to reflect the real behavior
by describing the actual request flow (primary: application/json via Blob,
fallback: multipart/form-data), and clarify that adding images is not
supported—only existing image deletion is allowed.
| export const updateFeed = async ( | ||
| feedId: number, | ||
| body: UpdateFeedBody, | ||
| ): Promise<UpdateFeedResponse> => { | ||
| try { | ||
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }); | ||
|
|
||
| return data; | ||
| } catch (error) { | ||
| console.error('수정 API 에러:', error); | ||
|
|
||
| const form = new FormData(); | ||
| form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' })); | ||
|
|
||
| const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, { | ||
| headers: { 'Content-Type': 'multipart/form-data' }, | ||
| }); | ||
|
|
||
| return data; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
에러 처리 로직 개선 필요
현재 구현은 첫 번째 요청이 실패하면 무조건 FormData로 재시도하는데, 이는 네트워크 오류나 권한 문제 등 다른 이유로 실패한 경우에도 불필요한 재시도를 하게 됩니다. 서버가 500 에러를 반환하는 특정 케이스에서만 FormData로 폴백하도록 개선이 필요합니다.
export const updateFeed = async (
feedId: number,
body: UpdateFeedBody,
): Promise<UpdateFeedResponse> => {
try {
const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, body, {
headers: { 'Content-Type': 'application/json' },
});
return data;
} catch (error) {
- console.error('수정 API 에러:', error);
+ // axios 에러 타입 체크 및 500 에러 확인
+ if (axios.isAxiosError(error) && error.response?.status === 500) {
+ console.warn('JSON 요청 실패 (500), FormData로 재시도:', error);
- const form = new FormData();
- form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));
+ const form = new FormData();
+ form.append('request', new Blob([JSON.stringify(body)], { type: 'application/json' }));
- const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
+ const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
- return data;
+ return data;
+ }
+
+ // 다른 에러는 그대로 전파
+ throw error;
}
};또한 파일 상단에 axios import 추가:
import axios from 'axios';| interface Book { | ||
| id: number; | ||
| title: string; | ||
| author: string; | ||
| cover: string; | ||
| isbn: string; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Book 타입 중복 정의
Book 인터페이스가 로컬에서 정의되었지만 src/types/book.ts에 이미 유사한 타입이 존재합니다. 타입 일관성을 위해 기존 타입을 재사용하거나 확장하는 것이 좋습니다.
-interface Book {
- id: number;
- title: string;
- author: string;
- cover: string;
- isbn: string;
-}
+import type { Book } from '@/types/book';
+
+interface LocalBook extends Book {
+ id: number;
+}🤖 Prompt for AI Agents
In src/pages/post/UpdatePost.tsx around lines 17 to 23, a local Book interface
is duplicated but an equivalent type exists in src/types/book.ts; remove the
local Book definition and import the canonical Book type (or import and extend
it if this file needs additional fields), update any references to use the
imported type, and ensure the import path is correct and exported type names
match.
| }; | ||
|
|
||
| loadFeedDetail(); | ||
| }, [feedId]); |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
useEffect 의존성 배열 불완전
useEffect 훅에서 openSnackbar, closePopup, navigate 함수들을 사용하지만 의존성 배열에 포함되지 않았습니다. ESLint의 exhaustive-deps 규칙이 비활성화되어 있는지 확인이 필요합니다.
🏁 Script executed:
#!/bin/bash
# ESLint 설정에서 exhaustive-deps 규칙 확인
fd -e json -e js "eslint" --exec grep -l "exhaustive-deps" {} \;Length of output: 65
useEffect 의존성과 ESLint 설정을 함께 점검하기 위해 아래 스크립트를 실행해주세요.
특히 src/pages/post/UpdatePost.tsx의 1~200라인을 확인하고, 프로젝트 전반의 exhaustive-deps 설정 유무를 검색합니다.
🏁 Script executed:
#!/bin/bash
# 1) UpdatePost.tsx 파일 경로 및 내용 확인
file=$(fd "UpdatePost.tsx" -t f | head -n1)
echo "=== 파일: $file ==="
sed -n '1,200p' "$file"
# 2) 프로젝트 전체에서 exhaustive-deps 룰 설정 검색
echo "=== ESLint 설정에서 exhaustive-deps 검색 ==="
grep -R "exhaustive-deps" -n .Length of output: 9660
UpdatePost.tsx useEffect 의존성 배열 보완 필요
useEffect 훅에서 openSnackbar, closePopup, navigate 함수를 사용하지만 의존성 배열에 포함되지 않아 ESLint 경고가 발생하고, 향후 훅 재실행 시 의도치 않은 동작이 일어날 수 있습니다. 아래 중 한 가지 방법으로 수정해주세요.
— 적용 위치
• 파일: src/pages/post/UpdatePost.tsx
• 약 62–94행
— 제안 해결책
• 누락된 의존성 추가
- }, [feedId]);
+ }, [feedId, openSnackbar, closePopup, navigate]);• 또는 명시적으로 의존성 고정을 원할 경우 eslint-disable 주석 추가
- useEffect(() => {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(() => {📝 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.
| }, [feedId]); | |
| }, [feedId, openSnackbar, closePopup, navigate]); |
🤖 Prompt for AI Agents
In src/pages/post/UpdatePost.tsx around lines 62 to 94, the useEffect ending at
line 89 references openSnackbar, closePopup, and navigate but only lists feedId
in its dependency array; update the dependency array to include openSnackbar,
closePopup, and navigate (or, if you intentionally want to lock dependencies,
add a targeted eslint-disable-next-line react-hooks/exhaustive-deps comment
above the useEffect with a brief justification). Ensure you choose one approach
(add missing deps or add the eslint-disable with justification) and keep the
dependency list accurate to prevent stale closures.
| const handlePhotoAdd = () => { | ||
| return; | ||
| }; | ||
|
|
||
| const handlePhotoRemove = () => { | ||
| return; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
사용되지 않는 핸들러 함수들
handlePhotoAdd와 handlePhotoRemove 함수가 정의되어 있지만 빈 구현이며, PhotoSection에 props로 전달되고 있습니다. 편집 모드에서 사진 추가가 불가능하다면 이 핸들러들을 제거하거나 조건부로 전달하는 것이 좋습니다.
- const handlePhotoAdd = () => {
- return;
- };
-
- const handlePhotoRemove = () => {
- return;
- };
<PhotoSection
photos={selectedPhotos}
- onPhotoAdd={handlePhotoAdd}
- onPhotoRemove={handlePhotoRemove}
+ onPhotoAdd={undefined}
+ onPhotoRemove={undefined}
existingImageUrls={remainImageUrls}
onExistingImageRemove={handleExistingImageRemove}
isEditMode={true}
/>📝 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 handlePhotoAdd = () => { | |
| return; | |
| }; | |
| const handlePhotoRemove = () => { | |
| return; | |
| }; | |
| <PhotoSection | |
| photos={selectedPhotos} | |
| onPhotoAdd={undefined} | |
| onPhotoRemove={undefined} | |
| existingImageUrls={remainImageUrls} | |
| onExistingImageRemove={handleExistingImageRemove} | |
| isEditMode={true} | |
| /> |
🤖 Prompt for AI Agents
In src/pages/post/UpdatePost.tsx around lines 121–127, the handlers
handlePhotoAdd and handlePhotoRemove are empty yet passed into PhotoSection;
remove these no-op functions or stop passing them when in edit mode. If photo
changes should be disabled in edit mode, conditionally omit the props (pass
undefined or omit keys) so PhotoSection doesn't receive no-op callbacks;
otherwise implement the handlers to perform the actual add/remove logic (open
uploader, update state, call API) and wire them into PhotoSection. Ensure prop
types remain correct after removal or conditional passing.
#️⃣ 연관된 이슈
#94
📝 작업 내용
피드 수정 기능을 구현했습니다. 사용자가 작성한 피드의 내용, 공개/비공개 설정, 태그, 기존 이미지를 수정할 수 있는 기능을 추가했습니다.
🕸️ 주요 구현 내용
1. API 레이어 구현
updateFeed.ts: 피드 수정 API 함수 구현useUpdateFeed.ts: 피드 수정 비즈니스 로직을 담은 커스텀 훅 구현2. 수정 페이지 컴포넌트 구현
UpdatePost.tsx: 피드 수정 전용 페이지 컴포넌트 생성3. 기존 컴포넌트 확장
PhotoSection.tsx:isEditModeprops 추가하여 수정 모드에서 사진 추가 버튼 숨김 처리BookSelectionSection.tsx: readOnly props 추가하여 읽기 전용 모드 지원getFeedDetail.ts: isPublic 필드를 타입 정의에 추가기능적 제약사항 (요구사항에 따른 설계)
기술적 해결사항
multipart/form-data형식으로 구현했으나, 실제 서버에서는application/json형식을 기대하고 있어 500 에러가 발생했습니다. 이를 해결하기 위해 JSON 형식을 우선으로 시도하고, 실패 시 FormData로 fallback하는 로직을 구현했습니다.useEffect의 의존성 배열에서 함수 참조로 인한 무한 루프를 방지하기 위해 의존성을 feedId만으로 제한했습니다.Summary by CodeRabbit