-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 새 글 수정 API 연동 #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 새 글 수정 API 연동 #103
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { apiClient } from '../index'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 피드 수정 요청 바디 타입 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface UpdateFeedBody { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentBody: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPublic: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagList?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remainImageUrls?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 성공 응답 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface UpdateFeedSuccess { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSuccess: true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** 실패 응답 */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface UpdateFeedFail { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSuccess: false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type UpdateFeedResponse = UpdateFeedSuccess | UpdateFeedFail; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 피드 수정 API | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - multipart/form-data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - request: application/json (Blob로 감싸 전송) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 이미지 추가는 불가능, 기존 이미지 삭제만 가능 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Content-Type 헤더를 명시적으로 설정하지 마세요 FormData를 전송할 때 const { data } = await apiClient.patch<UpdateFeedResponse>(`/feeds/${feedId}`, form, {
- headers: { 'Content-Type': 'multipart/form-data' },
+ // Content-Type 헤더를 제거하여 axios가 자동으로 설정하도록 함
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 사용 예시: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 기존 이미지 일부 유지 (새 이미지 추가는 불가) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updateBody: UpdateFeedBody = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentBody: "수정된 글 내용입니다!", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPublic: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagList: ["한국소설", "책추천", "역사"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remainImageUrls: ["https://img.domain.com/1.jpg"] // 기존 이미지 중 유지할 것들 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await updateFeed(123, updateBody); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.isSuccess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('피드 수정 성공:', result.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('피드 수정 실패:', result.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('네트워크 오류:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 모든 이미지 삭제 후 텍스트만 수정 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const textOnlyUpdate: UpdateFeedBody = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentBody: "텍스트만 수정", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPublic: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagList: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remainImageUrls: [] // 모든 기존 이미지 삭제 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await updateFeed(123, textOnlyUpdate); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,39 +16,61 @@ interface BookSelectionSectionProps { | |||||||||||||||||||||||||
| selectedBook: { cover: string; title: string; author: string } | null; | ||||||||||||||||||||||||||
| onSearchClick: () => void; | ||||||||||||||||||||||||||
| onChangeClick: () => void; | ||||||||||||||||||||||||||
| readOnly?: boolean; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const BookSelectionSection = ({ | ||||||||||||||||||||||||||
| selectedBook, | ||||||||||||||||||||||||||
| onSearchClick, | ||||||||||||||||||||||||||
| onChangeClick, | ||||||||||||||||||||||||||
| readOnly = false, | ||||||||||||||||||||||||||
| }: BookSelectionSectionProps) => { | ||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <Section> | ||||||||||||||||||||||||||
| <SectionTitle>책 선택</SectionTitle> | ||||||||||||||||||||||||||
| <SearchBox | ||||||||||||||||||||||||||
| hasSelectedBook={!!selectedBook} | ||||||||||||||||||||||||||
| onClick={selectedBook ? undefined : onSearchClick} | ||||||||||||||||||||||||||
| onClick={selectedBook || readOnly ? undefined : onSearchClick} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
|
Comment on lines
31
to
34
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||
| {selectedBook ? ( | ||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||
| <SelectedBookContainer> | ||||||||||||||||||||||||||
| <SelectedBookCover> | ||||||||||||||||||||||||||
| <img src={selectedBook.cover} alt={selectedBook.title} /> | ||||||||||||||||||||||||||
| {selectedBook.cover && selectedBook.cover.trim() !== '' ? ( | ||||||||||||||||||||||||||
| <img src={selectedBook.cover} alt={selectedBook.title} /> | ||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||
| width: '60px', | ||||||||||||||||||||||||||
| height: '80px', | ||||||||||||||||||||||||||
| backgroundColor: '#333', | ||||||||||||||||||||||||||
| display: 'flex', | ||||||||||||||||||||||||||
| alignItems: 'center', | ||||||||||||||||||||||||||
| justifyContent: 'center', | ||||||||||||||||||||||||||
| fontSize: '10px', | ||||||||||||||||||||||||||
| color: '#999', | ||||||||||||||||||||||||||
| borderRadius: '4px', | ||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| 책표지 | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </SelectedBookCover> | ||||||||||||||||||||||||||
| <SelectedBookInfo> | ||||||||||||||||||||||||||
| <SelectedBookTitle>{selectedBook.title}</SelectedBookTitle> | ||||||||||||||||||||||||||
| <SelectedBookAuthor>{selectedBook.author} 저</SelectedBookAuthor> | ||||||||||||||||||||||||||
| </SelectedBookInfo> | ||||||||||||||||||||||||||
| </SelectedBookContainer> | ||||||||||||||||||||||||||
| <ChangeButton onClick={onChangeClick}>변경</ChangeButton> | ||||||||||||||||||||||||||
| {!readOnly && <ChangeButton onClick={onChangeClick}>변경</ChangeButton>} | ||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||
| <SearchIcon> | ||||||||||||||||||||||||||
| <img src={searchIcon} alt="검색" /> | ||||||||||||||||||||||||||
| </SearchIcon> | ||||||||||||||||||||||||||
| <span style={{ color: semanticColors.text.secondary }}>검색해서 찾기</span> | ||||||||||||||||||||||||||
| <span style={{ color: semanticColors.text.secondary }}> | ||||||||||||||||||||||||||
| {readOnly ? '책 정보' : '검색해서 찾기'} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </SearchBox> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,16 +16,31 @@ interface PhotoSectionProps { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photos: File[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPhotoAdd: (files: File[]) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPhotoRemove: (index: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existingImageUrls?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onExistingImageRemove?: (imageUrl: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readOnly?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isEditMode?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PhotoSection = ({ photos, onPhotoAdd, onPhotoRemove }: PhotoSectionProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PhotoSection = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photos, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPhotoAdd, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPhotoRemove, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existingImageUrls = [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onExistingImageRemove, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readOnly = false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isEditMode = false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: PhotoSectionProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleFileInputClick = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (readOnly || isEditMode) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileInputRef.current?.click(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (readOnly || isEditMode) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const files = Array.from(e.target.files || []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (files.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onPhotoAdd(files); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -38,34 +53,61 @@ const PhotoSection = ({ photos, onPhotoAdd, onPhotoRemove }: PhotoSectionProps) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return URL.createObjectURL(file); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDisabled = photos.length >= 3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalImageCount = existingImageUrls.length + photos.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isDisabled = totalImageCount >= 3 || readOnly || isEditMode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SectionTitle>사진 추가</SectionTitle> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoGrid> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddPhotoButton onClick={handleFileInputClick} disabled={isDisabled}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={isDisabled ? plusDisabledIcon : plusIcon} alt="사진 추가" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </AddPhotoButton> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!readOnly && !isEditMode && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <AddPhotoButton onClick={handleFileInputClick} disabled={isDisabled}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={isDisabled ? plusDisabledIcon : plusIcon} alt="사진 추가" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </AddPhotoButton> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {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> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미지 URL 메모리 누수 가능성
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {photos.map((photo, index) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={index} style={{ position: 'relative', width: '80px', height: '80px' }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoImage src={createImageUrl(photo)} alt={`선택된 사진 ${index + 1}`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <RemoveButton onClick={() => onPhotoRemove(index)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={closeIcon} alt="삭제" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </RemoveButton> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={`new-${index}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ position: 'relative', width: '80px', height: '80px' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoImage src={createImageUrl(photo)} alt={`새 이미지 ${index + 1}`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!readOnly && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <RemoveButton onClick={() => onPhotoRemove(index)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img src={closeIcon} alt="삭제" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </RemoveButton> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </PhotoGrid> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoCount>{photos.length}/3개</PhotoCount> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="file" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accept="image/*" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multiple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ display: 'none' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleFileChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PhotoCount>{totalImageCount}/3개</PhotoCount> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!readOnly && !isEditMode && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={fileInputRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="file" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accept="image/*" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| multiple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ display: 'none' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleFileChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </PhotoContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { updateFeed, type UpdateFeedBody, type UpdateFeedResponse } from '@/api/feeds/updateFeed'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { usePopupActions } from './usePopupActions'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface UseUpdateFeedProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess?: (feedId: number) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useUpdateFeed = (options?: UseUpdateFeedProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { openSnackbar, closePopup } = usePopupActions(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updateExistingFeed = async (feedId: number, body: UpdateFeedBody) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ===== 클라이언트 선검증 ===== | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+17
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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);참고:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (res.isSuccess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openSnackbar({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: '피드가 수정되었습니다.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant: 'top', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: closePopup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options?.onSuccess) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.onSuccess(feedId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true as const, feedId }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openSnackbar({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: res.message || '피드 수정에 실패했습니다.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant: 'top', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: closePopup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false as const, error: res.message }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('피드 수정 실패:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openSnackbar({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: '피드 수정 중 오류가 발생했습니다.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant: 'top', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: closePopup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: false as const, error: '피드 수정 중 오류가 발생했습니다.' }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { updateExistingFeed, loading }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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:
Length of output: 68
아래와 같이
.ts및.tsx파일을 모두 검색하도록 명령을 수정해 다시 한 번 실행해 보시겠어요?🏁 Script executed:
Length 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