Skip to content

feat: 새 글 수정 API 연동#103

Merged
ljh130334 merged 5 commits intodevelopfrom
feat/api-feeds-modify
Aug 13, 2025
Merged

feat: 새 글 수정 API 연동#103
ljh130334 merged 5 commits intodevelopfrom
feat/api-feeds-modify

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Aug 13, 2025

#️⃣ 연관된 이슈

#94

📝 작업 내용

피드 수정 기능을 구현했습니다. 사용자가 작성한 피드의 내용, 공개/비공개 설정, 태그, 기존 이미지를 수정할 수 있는 기능을 추가했습니다.

🕸️ 주요 구현 내용

1. API 레이어 구현

  • updateFeed.ts: 피드 수정 API 함수 구현
  • useUpdateFeed.ts: 피드 수정 비즈니스 로직을 담은 커스텀 훅 구현

2. 수정 페이지 컴포넌트 구현

  • UpdatePost.tsx: 피드 수정 전용 페이지 컴포넌트 생성
  • 기존 피드 데이터를 불러와 폼에 자동 입력
  • 수정 완료 후 해당 피드 상세 페이지로 자동 이동

3. 기존 컴포넌트 확장

  • PhotoSection.tsx: isEditMode props 추가하여 수정 모드에서 사진 추가 버튼 숨김 처리
  • BookSelectionSection.tsx: readOnly props 추가하여 읽기 전용 모드 지원
  • getFeedDetail.ts: isPublic 필드를 타입 정의에 추가

기능적 제약사항 (요구사항에 따른 설계)

  • 책 정보 수정 불가: 선택된 책 정보는 읽기 전용으로 표시되며 변경할 수 없습니다.
  • 이미지 추가 불가, 삭제만 가능: 수정 모드에서는 새로운 이미지를 추가할 수 없고, 기존 이미지의 삭제만 가능합니다. 이를 위해 PhotoSection 컴포넌트에 isEditMode props를 추가하여 사진 추가 버튼을 완전히 숨겼습니다.
  • 수정 가능한 항목: 글 내용, 공개/비공개 설정, 태그 선택, 기존 이미지 삭제

기술적 해결사항

  • API 요청 형식 최적화: 초기에는 스웨거 명세서에 따라 multipart/form-data 형식으로 구현했으나, 실제 서버에서는 application/json 형식을 기대하고 있어 500 에러가 발생했습니다. 이를 해결하기 위해 JSON 형식을 우선으로 시도하고, 실패 시 FormData로 fallback하는 로직을 구현했습니다.
  • 무한 렌더링 방지: useEffect의 의존성 배열에서 함수 참조로 인한 무한 루프를 방지하기 위해 의존성을 feedId만으로 제한했습니다.

Summary by CodeRabbit

  • New Features
    • 게시글 수정 기능 추가: 피드 상세의 더보기 메뉴에서 수정으로 이동해 내용, 공개/비공개, 태그를 편집할 수 있습니다.
    • 기존 이미지 관리: 수정 화면에서 기존 이미지를 확인하고 개별 삭제할 수 있습니다.
    • 읽기 전용 표시: 선택된 도서 영역은 수정 화면에서 읽기 전용으로 표시됩니다.
    • 태그 제한 안내: 태그는 최대 5개까지, 중복은 방지되며 안내 메시지가 표시됩니다.
  • Improvements
    • 수정 완료 시 해당 피드 상세로 바로 이동하도록 네비게이션 개선.

@ljh130334 ljh130334 added the 📬 API 서버 API 통신 label Aug 13, 2025
@vercel
Copy link

vercel bot commented Aug 13, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Project Deployment Preview Comments Updated (UTC)
thip Ready Preview Comment Aug 13, 2025 6:12am

@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Walkthrough

피드 수정 기능을 추가하고 연동했습니다. 피드 상세 타입 일부를 변경(aliasName→alias, isPublic 추가)했으며, 업데이트 API 클라이언트와 훅(useUpdateFeed), 수정 페이지(UpdatePost), 라우트, 상세 페이지의 수정 이동을 구현했습니다. 게시글 사진/도서 선택 컴포넌트는 읽기 전용·편집 모드와 기존 이미지 처리 로직이 추가되었습니다.

Changes

Cohort / File(s) Summary
Feeds API
src/api/feeds/getFeedDetail.ts
FeedDetailData에서 aliasName→alias로 변경, isPublic: boolean 추가; 사용 예시 갱신
Feeds API (Update)
src/api/feeds/updateFeed.ts
피드 수정 API 클라이언트 추가: JSON PATCH 실패 시 multipart/form-data 폴백; 타입들(UpdateFeedBody/Response 등) 정의
Create Group UI
src/components/creategroup/BookSelectionSection.tsx
읽기 전용 모드 readOnly 추가; 선택/변경 버튼 동작 조건 조정; 표지 플레이스홀더 처리
Create Post UI (Photos)
src/components/createpost/PhotoSection.tsx
기존 이미지(existingImageUrls) 표시/제거, readOnly/isEditMode 도입, 최대 개수 계산 및 UI/동작 분기 추가
Hooks
src/hooks/useUpdateFeed.ts
피드 수정 훅 추가: 태그 사전 검증(중복/최대 5), API 호출, 스낵바 및 성공/실패 핸들링
Routing & Pages
src/pages/index.tsx, src/pages/feed/FeedDetailPage.tsx, src/pages/post/UpdatePost.tsx
수정 페이지 라우트(post/update/:feedId) 추가, 상세 페이지의 수정 액션 네비게이션 구현, UpdatePost 페이지 추가(초기 상세 조회/폼 상태/제출)

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})
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

✨ Feature

Suggested reviewers

  • heeeeyong

Poem

깡총, 깡총 — 코드밭을 달려가며
alias 바꾸고 공개 깃발 펄럭이며
사진 셋 헤아려 딱 맞게 담고,
수정 길 열어 라우터에 길을 냈네.
오늘의 당근은 업데이트 성공! 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-feeds-modify

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: 사용하지 않는 핸들러 함수들을 제거하세요

handlePhotoAddhandlePhotoRemove 함수가 빈 구현으로 남아있습니다. 수정 모드에서는 새 이미지 추가가 불가능하다는 요구사항에 따라 이 함수들은 사용되지 않으므로 제거하는 것이 좋습니다.

-  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: 조건문 로직 개선 가능

readOnlyisEditMode가 동시에 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

📥 Commits

Reviewing files that changed from the base of the PR and between 007a16c and f1c5f03.

📒 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에서의 로딩 종료 처리 모두 적절합니다.

Comment on lines +37 to +54
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;
}
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
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;
}
};

Comment on lines +49 to +51
const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

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

Comment on lines 31 to 34
<SearchBox
hasSelectedBook={!!selectedBook}
onClick={selectedBook ? undefined : onSearchClick}
onClick={selectedBook || readOnly ? undefined : onSearchClick}
>
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
<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' }}
>

Comment on lines +70 to +82
{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>
))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

이미지 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.

Suggested change
{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.

Comment on lines +17 to +43
// ===== 클라이언트 선검증 =====
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);

Copy link

Choose a reason for hiding this comment

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

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

Suggested change
// ===== 클라이언트 선검증 =====
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.

Comment on lines +17 to +23
interface Book {
id: number;
title: string;
author: string;
cover: string;
isbn: string;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
}, [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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 007a16c and f1c5f03.

📒 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;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

속성명 변경이 하위 호환성을 깨뜨릴 수 있습니다

aliasName에서 alias로 속성명이 변경되었습니다. 이 인터페이스를 사용하는 다른 컴포넌트들이 여전히 aliasName을 참조하고 있을 수 있으니 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# aliasName을 참조하는 다른 파일들 확인
rg "aliasName" --type ts --type tsx -A 2

Length of output: 68


아래와 같이 .ts.tsx 파일을 모두 검색하도록 명령을 수정해 다시 한 번 실행해 보시겠어요?


🏁 Script executed:

#!/bin/bash
# aliasName을 참조하는 모든 .ts/.tsx 파일 확인
rg "aliasName" --glob "*.ts" --glob "*.tsx" -n -C2

Length of output: 11834


속성명 변경에 따른 하위 호환성 주의
aliasNamealias로 변경되면서, 아래 위치들에서 여전히 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.

Comment on lines +29 to +32
* - multipart/form-data
* - request: application/json (Blob로 감싸 전송)
* - 이미지 추가는 불가능, 기존 이미지 삭제만 가능
*/
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
* - 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.

Comment on lines +33 to +55
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;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ 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';

Comment on lines +17 to +23
interface Book {
id: number;
title: string;
author: string;
cover: string;
isbn: string;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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]);
Copy link

Choose a reason for hiding this comment

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

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

Suggested change
}, [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.

Comment on lines +121 to +127
const handlePhotoAdd = () => {
return;
};

const handlePhotoRemove = () => {
return;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

사용되지 않는 핸들러 함수들

handlePhotoAddhandlePhotoRemove 함수가 정의되어 있지만 빈 구현이며, 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.

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

@ljh130334 ljh130334 merged commit e2b5426 into develop Aug 13, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-feeds-modify branch August 13, 2025 11:22
@coderabbitai coderabbitai bot mentioned this pull request Aug 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant