-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Presigned URL 구현 #83
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
Changes from all commits
b9c114e
54866c0
fc24fb2
e87af45
1c429ec
be55875
0346d92
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,9 @@ | ||
| import { axiosClient } from '../../../../shared/types/api/http-client'; | ||
| import { PresignedUrlRequest, PresignedUrlResponse } from '../model/presignedUrl'; | ||
|
|
||
| const presignedUrl = async (dto: PresignedUrlRequest) => { | ||
| const response = await axiosClient.get<PresignedUrlResponse>('/generate-presigned-url', { params: dto }); | ||
| return response.data; | ||
| }; | ||
|
|
||
| export default presignedUrl; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PresignedUrlRequest, PresignedUrlResponse } from '../model/presignedUrl'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { axiosClient } from '../../../../shared/types/api/http-client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ApiResponse } from '../../../../shared/types/api/apiResponse'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getPresignedUrl = async (dto: PresignedUrlRequest) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await axiosClient.get<ApiResponse<PresignedUrlResponse>>('/generate-presigned-url', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: dto, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Presigned URL 응답:', response.data.result?.preSignedUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return response.data.result?.preSignedUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Presigned URL 요청 실패:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+35
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 Authorization 헤더 삭제 방식 개선 필요 현재 구현에서 새로운 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
Suggested change
🧰 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) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const uploadFile = async (file: File) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name } = file; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const presignedUrlResponse = await getPresignedUrl({ fileName: name }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!presignedUrlResponse) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Failed to get presigned url'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = presignedUrlResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Presigned URL:', url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await putS3Image({ url, file }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // S3 URL에서 presigned URL 파라미터를 제거하고 기본 URL 반환 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return url.split('?')[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export interface PresignedUrlRequest { | ||
| fileName: string; | ||
| } | ||
|
|
||
| export interface PresignedUrlResponse { | ||
| preSignedUrl: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,91 @@ | ||
| import FileUploadImage from '../../../../../public/assets/event-manage/creation/FileUpload.svg'; | ||
| import { useRef, useState } from 'react'; | ||
| import { uploadFile } from '../hooks/usePresignedUrlHook'; | ||
| import { useFunnelState } from '../model/FunnelContext'; | ||
|
|
||
| const FileUpload = () => { | ||
| const [isDragging, setIsDragging] = useState(false); | ||
| const [previewUrl, setPreviewUrl] = useState<string | null>(null); | ||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
| const { setEventState } = useFunnelState(); | ||
|
|
||
| const handleFileUpload = async (file: File) => { | ||
| if (file.size > 500 * 1024) { | ||
| alert('파일 크기는 500KB를 초과할 수 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| if (!['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)) { | ||
| alert('jpg, jpeg, png 파일만 업로드 가능합니다.'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const imageUrl = await uploadFile(file); | ||
| setPreviewUrl(imageUrl); | ||
| setEventState(prev => ({ ...prev, bannerImageUrl: imageUrl })); | ||
| } catch (error) { | ||
| console.error('파일 업로드 실패:', error); | ||
| } | ||
| }; | ||
|
|
||
| const handleDragOver = (e: React.DragEvent) => { | ||
| e.preventDefault(); | ||
| setIsDragging(true); | ||
| }; | ||
|
|
||
| const handleDragLeave = (e: React.DragEvent) => { | ||
| e.preventDefault(); | ||
| setIsDragging(false); | ||
| }; | ||
|
|
||
| const handleDrop = (e: React.DragEvent) => { | ||
| e.preventDefault(); | ||
| setIsDragging(false); | ||
| const file = e.dataTransfer.files[0]; | ||
| if (file) handleFileUpload(file); | ||
| }; | ||
|
|
||
| const handleClick = () => { | ||
| fileInputRef.current?.click(); | ||
| }; | ||
|
|
||
| const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const file = e.target.files?.[0]; | ||
| if (file) handleFileUpload(file); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col justify-start gap-1"> | ||
| <h1 className="font-bold text-black text-lg">배너 사진 첨부</h1> | ||
| <h2 className="text-placeholderText text-xs md:text-sm">500kB 이하의 jpg, png 파일만 등록할 수 있습니다.</h2> | ||
| <div className="flex flex-col items-center justify-center h-44 border border-dashed border-placeholderText bg-gray3 rounded-[10px] mb-4"> | ||
| <img src={FileUploadImage} alt="파일 업로드" className="w-10 h-10" /> | ||
| <span className="mt-1 text-black text-sm">이미지를 끌어서 올리거나 클릭해서 업로드 하세요.</span> | ||
| <h2 className="text-placeholderText text-xs md:text-sm">500kB 이하의 jpeg, png 파일만 등록할 수 있습니다.</h2> | ||
| <div | ||
| className={`flex flex-col items-center justify-center h-44 border border-dashed ${ | ||
| isDragging ? 'border-main bg-dropdown' : 'border-placeholderText bg-gray3' | ||
| } rounded-[10px] mb-4 cursor-pointer`} | ||
| onDragOver={handleDragOver} | ||
| onDragLeave={handleDragLeave} | ||
| onDrop={handleDrop} | ||
| onClick={handleClick} | ||
| > | ||
| <input | ||
| type="file" | ||
| ref={fileInputRef} | ||
| className="hidden" | ||
| accept="image/jpeg,image/png" | ||
| onChange={handleFileChange} | ||
| /> | ||
| {previewUrl ? ( | ||
| <img src={previewUrl} alt="업로드된 이미지" className="w-full h-full object-cover rounded-[10px]" /> | ||
| ) : ( | ||
| <> | ||
| <img src={FileUploadImage} alt="파일 업로드" className="w-10 h-10" /> | ||
| <span className="mt-1 text-black text-sm">이미지를 끌어서 올리거나 클릭해서 업로드 하세요.</span> | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default FileUpload; |
Uh oh!
There was an error while loading. Please reload this page.