Skip to content

feat: Presigned URL 구현 #83

Merged
Yejiin21 merged 7 commits intodevelopfrom
feat/#78/presigned-url
Apr 1, 2025
Merged

feat: Presigned URL 구현 #83
Yejiin21 merged 7 commits intodevelopfrom
feat/#78/presigned-url

Conversation

@Yejiin21
Copy link
Copy Markdown
Contributor

@Yejiin21 Yejiin21 commented Mar 31, 2025

  • Presigned URL API 연동
  • 텍스트 에디터에 이미지 첨부 기능 추가

Summary by CodeRabbit

  • 새로운 기능
    • 이미지 업로드 경험이 개선되었습니다. 드래그 앤 드롭 및 클릭 선택 방식과 함께, 파일 크기(500KB 이하) 및 형식(JPEG, PNG) 검증 및 미리보기 기능이 추가되었습니다.
    • 텍스트 에디터에 이미지 업로드 기능이 도입되어, 사용자가 선택한 이미지를 에디터 내 원하는 위치에 자동 삽입할 수 있습니다.
    • 파일 업로드 처리 과정에서 오류 발생 시 사용자에게 알림을 제공하여 보다 안전한 파일 처리가 가능해졌습니다.

@Yejiin21 Yejiin21 self-assigned this Mar 31, 2025
@Yejiin21 Yejiin21 linked an issue Mar 31, 2025 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2025

Walkthrough

이 PR은 event-create 모듈 내에서 presigned URL 생성 및 파일/이미지 업로드 기능을 추가하고 개선합니다. 새로운 presigned URL 요청 함수와 관련 훅(getPresignedUrl, putS3Image, uploadFile), 및 그에 따른 타입 (PresignedUrlRequest, PresignedUrlResponse)이 도입되었습니다. UI 측면에서는 FileUpload와 TextEditor 컴포넌트에 드래그 앤 드롭, 이미지 선택 및 에디터 내 이미지 삽입 기능이 추가되었으며, ApiResponse 인터페이스의 응답 프로퍼티명이 data에서 result로 변경되었습니다.

Changes

파일 경로 변경 요약
src/features/event-manage/.../api/presignedUrl.ts, .../hooks/usePresignedUrlHook.ts, .../model/presignedUrl.ts presigned URL 요청 함수 및 관련 훅(getPresignedUrl, putS3Image, uploadFile) 추가, presigned URL 요청/응답 타입(PresignedUrlRequest, PresignedUrlResponse) 정의
src/features/event-manage/.../ui/FileUpload.tsx, .../ui/TextEditor.tsx 파일/이미지 업로드 기능 추가: 드래그 앤 드롭, 파일 선택, 이미지 핸들러, 툴바 업데이트 등 인터페이스 개선
src/shared/types/api/apiResponse.ts ApiResponse 인터페이스 수정: dataresult

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: 업로드 확인 응답
Loading
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: 에디터에 이미지 삽입
Loading

Suggested labels

🔧 Feature

Poem

나는 코드 밭을 뛰어다니는 귀여운 토끼,
새로운 함수와 훅이 내 발걸음을 맞춰주네,
데이터는 이제 'result'에 담겨 춤추고,
드래그하며 업로드하는 기쁨이 피어나는 날,
코딩 정원에서 반짝이는 변화들을 축하해! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

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

@github-actions
Copy link
Copy Markdown

@Yejiin21 Yejiin21 added the 🔧 Feature 기능 구현 label Mar 31, 2025
Copy link
Copy Markdown

@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: 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: 이미지 핸들러 개선 사항이 필요합니다.

이미지 핸들러 함수가 잘 구현되었지만, 몇 가지 개선할 점이 있습니다:

  1. 파일 크기 제한이 없습니다
  2. 업로드 중 로딩 상태를 사용자에게 표시하지 않습니다
  3. 이미지 접근성을 위한 alt 텍스트를 제공하지 않습니다
  4. 에러 처리가 더 구체적이지 않습니다

다음과 같이 개선할 수 있습니다:

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: 파일 업로드 기능이 잘 구현되었으나 보완 제안

파일 업로드 함수가 잘 구현되었지만, 몇 가지 개선사항이 있습니다:

  1. 오류 메시지가 영어로 되어있어 한국어로 통일하는 것이 좋습니다.
  2. 파일 이름에 특수문자나 한글이 포함된 경우 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: 파일 업로드 핸들러 구현이 좋으나 사용자 피드백 개선 필요

파일 유효성 검사와 업로드 로직이 잘 구현되어 있습니다. 다만 몇 가지 개선할 점이 있습니다:

  1. 업로드 중 로딩 상태를 표시하여 사용자에게 피드백을 제공하는 것이 좋습니다.
  2. 오류가 발생했을 때 콘솔에만 로그를 남기고 사용자에게는 알림이 없습니다.
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 구현이 잘 되어 있습니다. 드래그 상태에 따른 시각적 피드백과 이미지 미리보기 기능이 잘 작동합니다. 다만 몇 가지 추가 기능을 제안합니다:

  1. 이미지 업로드 후 삭제 또는 교체 기능 추가
  2. 로딩 상태 표시 추가
{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

📥 Commits

Reviewing files that changed from the base of the PR and between 98b3b8b and 0346d92.

📒 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: 파일 선택 처리가 적절함

파일 입력 요소 클릭 및 파일 선택 처리가 잘 구현되어 있습니다.

Comment on lines +21 to +35
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');
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Member

@hyeeuncho hyeeuncho left a comment

Choose a reason for hiding this comment

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

수고하셨습니다

@Yejiin21 Yejiin21 merged commit c167eef into develop Apr 1, 2025
2 checks passed
@Yejiin21 Yejiin21 deleted the feat/#78/presigned-url branch April 1, 2025 06:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔧 Feature 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Presigned URL 구현

2 participants