Conversation
Walkthrough이 PR은 event-create 모듈 내에서 presigned URL 생성 및 파일/이미지 업로드 기능을 추가하고 개선합니다. 새로운 presigned URL 요청 함수와 관련 훅( Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API
participant S3
Client->>API: GET /generate-presigned-url (dto)
API-->>Client: presignedUrl 응답
Client->>S3: PUT 파일 업로드 (presignedUrl)
S3-->>Client: 업로드 확인 응답
sequenceDiagram
participant User
participant TextEditor
participant FileInput
participant UploadHook
User->>TextEditor: 이미지 버튼 클릭
TextEditor->>FileInput: 파일 다이얼로그 열기
FileInput-->>TextEditor: 파일 선택
TextEditor->>UploadHook: uploadFile 호출
UploadHook->>API: GET /generate-presigned-url (dto)
API-->>UploadHook: presignedUrl 응답
UploadHook->>S3: PUT 파일 업로드
S3-->>UploadHook: 업로드 성공 응답
UploadHook-->>TextEditor: 파일 URL 반환
TextEditor->>Editor: 에디터에 이미지 삽입
Suggested labels
Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (8)
src/features/event-manage/event-create/model/presignedUrl.ts (1)
1-7: 명명 규칙 통일이 필요합니다.인터페이스 속성 이름을 살펴보면,
fileName은 camelCase를 사용하는 반면preSignedUrl은 혼합 사용(PascalCase 형태 포함)하고 있습니다. 일관성을 위해 모든 속성 이름에 camelCase 규칙을 적용하는 것이 좋습니다.아래와 같이 수정하는 것을 고려해보세요:
export interface PresignedUrlResponse { - preSignedUrl: string; + presignedUrl: string; }src/features/event-manage/event-create/ui/TextEditor.tsx (3)
1-1: 주석 업데이트가 필요합니다."사진 첨부는 추후에..." 주석은 이미 기능이 구현되었으므로 제거하거나 업데이트해야 합니다.
-// 사진 첨부는 추후에... +// 이미지 첨부 기능 구현 완료
33-57: 이미지 핸들러 개선 사항이 필요합니다.이미지 핸들러 함수가 잘 구현되었지만, 몇 가지 개선할 점이 있습니다:
- 파일 크기 제한이 없습니다
- 업로드 중 로딩 상태를 사용자에게 표시하지 않습니다
- 이미지 접근성을 위한 alt 텍스트를 제공하지 않습니다
- 에러 처리가 더 구체적이지 않습니다
다음과 같이 개선할 수 있습니다:
const imageHandler = async () => { if (!quillRef.current) return; const quillInstance = quillRef.current.getEditor(); const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.click(); input.onchange = async () => { const file = input.files?.[0]; if (!file) return; + // 파일 크기 제한 (5MB) + const MAX_FILE_SIZE = 5 * 1024 * 1024; + if (file.size > MAX_FILE_SIZE) { + alert('이미지 크기는 5MB 이하여야 합니다.'); + return; + } + // 로딩 상태 설정 + setEventState(prev => ({ ...prev, isUploading: true })); try { const imageUrl = await uploadFile(file); const range = quillInstance.getSelection(); if (range) { - quillInstance.insertEmbed(range.index, 'image', imageUrl); + // alt 텍스트 추가를 위한 사용자 입력 + const altText = prompt('이미지 설명(대체 텍스트)을 입력하세요:', file.name) || '이미지'; + quillInstance.insertEmbed(range.index, 'image', imageUrl); + // alt 속성 설정하기 (Quill 에디터가 지원하는 경우) + // DOM 조작을 통해 방금 삽입된 이미지의 alt 속성을 설정할 수도 있습니다 } } catch (error) { console.error('이미지 업로드 실패:', error); - alert('이미지 업로드에 실패했습니다.'); + alert(`이미지 업로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`); } finally { + // 로딩 상태 해제 + setEventState(prev => ({ ...prev, isUploading: false })); } }; };
64-80: 도구 모음 구성이 잘 개선되었습니다.헤더 레벨을 추가하고 이미지 핸들러를 연결한 것은 좋은 개선입니다. 하지만
useMemo의 의존성 배열에imageHandler를 추가하는 것이 좋습니다.const modules = useMemo(() => { return { toolbar: { container: [ [{ header: [1, 2, 3, 4, false] }], ['bold', 'italic', 'underline', 'strike', 'blockquote'], [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], ['link', 'image'], [{ align: [] }, { color: [] }, { background: [] }], ['clean'], ], handlers: { image: imageHandler, }, }, }; -}, []); +}, [imageHandler]);src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts (2)
7-19: 함수 구현이 잘 되었으나 반환 값 처리 개선 필요Presigned URL을 가져오는 기능이 잘 구현되었습니다. 다만, 반환 값에 옵셔널 체이닝(
?.)을 사용하고 있어 TypeScript 타입 시스템에서는 반환 값이string | undefined로 추론됩니다. 이로 인해 업로드 함수에서 추가 검증이 필요합니다.- return response.data.result?.preSignedUrl; + if (!response.data.result?.preSignedUrl) { + throw new Error('Presigned URL이 반환되지 않았습니다.'); + } + return response.data.result.preSignedUrl;
37-52: 파일 업로드 기능이 잘 구현되었으나 보완 제안파일 업로드 함수가 잘 구현되었지만, 몇 가지 개선사항이 있습니다:
- 오류 메시지가 영어로 되어있어 한국어로 통일하는 것이 좋습니다.
- 파일 이름에 특수문자나 한글이 포함된 경우 URL 인코딩이 필요할 수 있습니다.
export const uploadFile = async (file: File) => { - const { name } = file; + // 파일 이름에 특수문자나 한글이 있을 수 있으므로 인코딩 처리 + const fileName = encodeURIComponent(file.name); - const presignedUrlResponse = await getPresignedUrl({ fileName: name }); + const presignedUrlResponse = await getPresignedUrl({ fileName }); if (!presignedUrlResponse) { - throw new Error('Failed to get presigned url'); + throw new Error('Presigned URL을 가져오는데 실패했습니다'); } const url = presignedUrlResponse; console.log('Presigned URL:', url); await putS3Image({ url, file }); // S3 URL에서 presigned URL 파라미터를 제거하고 기본 URL 반환 return url.split('?')[0]; };src/features/event-manage/event-create/ui/FileUpload.tsx (2)
12-30: 파일 업로드 핸들러 구현이 좋으나 사용자 피드백 개선 필요파일 유효성 검사와 업로드 로직이 잘 구현되어 있습니다. 다만 몇 가지 개선할 점이 있습니다:
- 업로드 중 로딩 상태를 표시하여 사용자에게 피드백을 제공하는 것이 좋습니다.
- 오류가 발생했을 때 콘솔에만 로그를 남기고 사용자에게는 알림이 없습니다.
const handleFileUpload = async (file: File) => { + const [isLoading, setIsLoading] = useState(false); if (file.size > 500 * 1024) { alert('파일 크기는 500KB를 초과할 수 없습니다.'); return; } if (!['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)) { alert('jpg, jpeg, png 파일만 업로드 가능합니다.'); return; } try { + setIsLoading(true); const imageUrl = await uploadFile(file); setPreviewUrl(imageUrl); setEventState(prev => ({ ...prev, bannerImageUrl: imageUrl })); + setIsLoading(false); } catch (error) { console.error('파일 업로드 실패:', error); + alert('파일 업로드에 실패했습니다. 다시 시도해주세요.'); + setIsLoading(false); } };그리고 UI 부분에서 로딩 상태를 표시하는 부분도 추가해야 합니다.
58-86: UI 구현이 좋으나 추가 기능 제안UI 구현이 잘 되어 있습니다. 드래그 상태에 따른 시각적 피드백과 이미지 미리보기 기능이 잘 작동합니다. 다만 몇 가지 추가 기능을 제안합니다:
- 이미지 업로드 후 삭제 또는 교체 기능 추가
- 로딩 상태 표시 추가
{previewUrl ? ( - <img src={previewUrl} alt="업로드된 이미지" className="w-full h-full object-cover rounded-[10px]" /> + <div className="relative w-full h-full"> + <img src={previewUrl} alt="업로드된 이미지" className="w-full h-full object-cover rounded-[10px]" /> + <button + className="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" + onClick={(e) => { + e.stopPropagation(); + setPreviewUrl(null); + setEventState(prev => ({ ...prev, bannerImageUrl: null })); + }} + > + <span className="text-red-500 font-bold">×</span> + </button> + </div> ) : ( <> <img src={FileUploadImage} alt="파일 업로드" className="w-10 h-10" /> <span className="mt-1 text-black text-sm">이미지를 끌어서 올리거나 클릭해서 업로드 하세요.</span> + {isLoading && <p className="mt-2 text-main">업로드 중...</p>} </> )}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/features/event-manage/event-create/api/presignedUrl.ts(1 hunks)src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts(1 hunks)src/features/event-manage/event-create/model/presignedUrl.ts(1 hunks)src/features/event-manage/event-create/ui/FileUpload.tsx(1 hunks)src/features/event-manage/event-create/ui/TextEditor.tsx(3 hunks)src/shared/types/api/apiResponse.ts(1 hunks)
🧰 Additional context used
🧬 Code Definitions (4)
src/features/event-manage/event-create/api/presignedUrl.ts (1)
src/features/event-manage/event-create/model/presignedUrl.ts (2)
PresignedUrlRequest(1-3)PresignedUrlResponse(5-7)
src/features/event-manage/event-create/ui/TextEditor.tsx (1)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
useFunnelState(53-59)
src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts (1)
src/features/event-manage/event-create/model/presignedUrl.ts (2)
PresignedUrlRequest(1-3)PresignedUrlResponse(5-7)
src/features/event-manage/event-create/ui/FileUpload.tsx (2)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
useFunnelState(53-59)src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts (1)
uploadFile(37-52)
🪛 Biome (1.9.4)
src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts
[error] 23-24: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
🔇 Additional comments (5)
src/features/event-manage/event-create/ui/TextEditor.tsx (2)
59-62: 코드가 간결해졌습니다.이전에 사용하던 텍스트 정제 로직을 제거하고 간결하게 값을 직접 설정하는 방식으로 변경한 것은 좋은 개선입니다.
85-93: ref 속성이 추가되었습니다.Quill 에디터 인스턴스에 접근하기 위한 ref 연결이 잘 구현되었습니다.
src/features/event-manage/event-create/ui/FileUpload.tsx (3)
7-10: 상태 관리 구현이 적절함드래그 상태와, 미리보기 URL을 관리하기 위한 상태 설정이 잘 되어 있습니다.
useRef를 사용하여 파일 입력 요소에 접근하는 방법도 적절합니다.
32-47: 드래그 앤 드롭 구현이 훌륭함드래그 앤 드롭 기능이 잘 구현되어 있습니다. 이벤트 처리와 상태 관리가 적절히 이루어지고 있습니다.
49-56: 파일 선택 처리가 적절함파일 입력 요소 클릭 및 파일 선택 처리가 잘 구현되어 있습니다.
| export const putS3Image = async ({ url, file }: { url: string; file: File }) => { | ||
| try { | ||
| delete axiosClient.defaults.headers.common.Authorization; | ||
| console.log('업로드할 URL:', url); | ||
| await axios.put(url, file, { | ||
| headers: { | ||
| 'Content-Type': file.type, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| console.error('S3 업로드 실패:', error); | ||
| alert('이미지 업로드에 실패했습니다.'); | ||
| throw new Error('Failed to upload image'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Authorization 헤더 삭제 방식 개선 필요
현재 구현에서 Authorization 헤더를 삭제하는 방식은 전역 axios 인스턴스에 영향을 미칠 수 있어 문제가 될 수 있습니다. 또한 static 분석 도구에서도 delete 연산자 사용이 성능 문제를 일으킬 수 있다고 지적하고 있습니다.
새로운 axios 인스턴스를 생성하거나 기존 인스턴스를 복제하여 헤더를 수정하는 것이 더 안전합니다:
- try {
- delete axiosClient.defaults.headers.common.Authorization;
- console.log('업로드할 URL:', url);
- await axios.put(url, file, {
- headers: {
- 'Content-Type': file.type,
- },
- });
+ try {
+ console.log('업로드할 URL:', url);
+ // axios 인스턴스 직접 사용하여 Authorization 헤더 없이 요청
+ await axios.put(url, file, {
+ headers: {
+ 'Content-Type': file.type,
+ },
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const putS3Image = async ({ url, file }: { url: string; file: File }) => { | |
| try { | |
| delete axiosClient.defaults.headers.common.Authorization; | |
| console.log('업로드할 URL:', url); | |
| await axios.put(url, file, { | |
| headers: { | |
| 'Content-Type': file.type, | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error('S3 업로드 실패:', error); | |
| alert('이미지 업로드에 실패했습니다.'); | |
| throw new Error('Failed to upload image'); | |
| } | |
| }; | |
| export const putS3Image = async ({ url, file }: { url: string; file: File }) => { | |
| try { | |
| console.log('업로드할 URL:', url); | |
| // axios 인스턴스 직접 사용하여 Authorization 헤더 없이 요청 | |
| await axios.put(url, file, { | |
| headers: { | |
| 'Content-Type': file.type, | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error('S3 업로드 실패:', error); | |
| alert('이미지 업로드에 실패했습니다.'); | |
| throw new Error('Failed to upload image'); | |
| } | |
| }; |
🧰 Tools
🪛 Biome (1.9.4)
[error] 23-24: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
Summary by CodeRabbit