From b9c114eec889bd450c89c1e3a1c9426664a59797 Mon Sep 17 00:00:00 2001 From: Yejiin21 <101397075+Yejiin21@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:50:39 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20presigned=20URL=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-create/api/presignedUrl.ts | 9 +++ .../event-create/hooks/usePresignedUrlHook.ts | 44 +++++++++++ .../event-create/model/presignedUrl.ts | 7 ++ .../event-create/ui/FileUpload.tsx | 79 ++++++++++++++++++- 4 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/features/event-manage/event-create/api/presignedUrl.ts create mode 100644 src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts create mode 100644 src/features/event-manage/event-create/model/presignedUrl.ts diff --git a/src/features/event-manage/event-create/api/presignedUrl.ts b/src/features/event-manage/event-create/api/presignedUrl.ts new file mode 100644 index 00000000..b22a56db --- /dev/null +++ b/src/features/event-manage/event-create/api/presignedUrl.ts @@ -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('/generate-presigned-url', { params: dto }); + return response.data; +}; + +export default presignedUrl; diff --git a/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts new file mode 100644 index 00000000..51c12db3 --- /dev/null +++ b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts @@ -0,0 +1,44 @@ +import { PresignedUrlRequest, PresignedUrlResponse } from '../model/presignedUrl'; +import axios, { AxiosError } from 'axios'; +import { ApiResponse } from '../../../../shared/types/api/apiResponse'; + +const getPresignedUrl = async (dto: PresignedUrlRequest) => { + try { + const { data } = await axios.get>('/generate-presigned-url', { + params: dto, + }); + return data; + } catch (error) { + alert((error as AxiosError>).response?.data.message); + } +}; + +export const putS3Image = async ({ url, file, type }: { url: string; file: File; type: string }) => { + try { + delete axios.defaults.headers.common.Authorization; + await axios.put(url, file, { + headers: { + 'Content-Type': type, + }, + }); + } catch (error) { + alert('이미지 업로드에 실패했습니다.'); + throw new Error('Failed to upload image'); + } +}; + +export const upuploadFile = async (file: File) => { + const { name, type } = file; + const presignedUrlResponse = await getPresignedUrl({ fileName: name }); + + if (!presignedUrlResponse?.data?.result) { + throw new Error('Failed to get presigned url'); + } + + const url = presignedUrlResponse.data.result; + + await putS3Image({ url, file, type }); + + // S3 URL에서 presigned URL 파라미터를 제거하고 기본 URL 반환 + return url.split('?')[0]; +}; diff --git a/src/features/event-manage/event-create/model/presignedUrl.ts b/src/features/event-manage/event-create/model/presignedUrl.ts new file mode 100644 index 00000000..84c71ee9 --- /dev/null +++ b/src/features/event-manage/event-create/model/presignedUrl.ts @@ -0,0 +1,7 @@ +export interface PresignedUrlRequest { + fileName: string; +} + +export interface PresignedUrlResponse { + result: string; +} diff --git a/src/features/event-manage/event-create/ui/FileUpload.tsx b/src/features/event-manage/event-create/ui/FileUpload.tsx index ba03b5d5..35a91afc 100644 --- a/src/features/event-manage/event-create/ui/FileUpload.tsx +++ b/src/features/event-manage/event-create/ui/FileUpload.tsx @@ -1,15 +1,88 @@ import FileUploadImage from '../../../../../public/assets/event-manage/creation/FileUpload.svg'; +import { useRef, useState } from 'react'; +import { upuploadFile } from '../hooks/usePresignedUrlHook'; const FileUpload = () => { + const [isDragging, setIsDragging] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + const fileInputRef = useRef(null); + + const handleFileUpload = async (file: File) => { + if (file.size > 500 * 1024) { + alert('파일 크기는 500KB를 초과할 수 없습니다.'); + return; + } + + if (!['image/jpeg', 'image/png'].includes(file.type)) { + alert('jpg, png 파일만 업로드 가능합니다.'); + return; + } + + try { + const imageUrl = await upuploadFile(file); + setPreviewUrl(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) => { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + }; + return (

배너 사진 첨부

500kB 이하의 jpg, png 파일만 등록할 수 있습니다.

-
- 파일 업로드 - 이미지를 끌어서 올리거나 클릭해서 업로드 하세요. +
+ + {previewUrl ? ( + 업로드된 이미지 + ) : ( + <> + 파일 업로드 + 이미지를 끌어서 올리거나 클릭해서 업로드 하세요. + + )}
); }; + export default FileUpload; From 54866c0cfdd9dfec2122f7116e1f19a3103753cc Mon Sep 17 00:00:00 2001 From: Yejiin21 <101397075+Yejiin21@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:42:50 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20apiResponse=20=EC=A0=9C=EB=84=A4?= =?UTF-8?q?=EB=A6=AD=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/types/api/apiResponse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/types/api/apiResponse.ts b/src/shared/types/api/apiResponse.ts index bf02b54a..4cc173d4 100644 --- a/src/shared/types/api/apiResponse.ts +++ b/src/shared/types/api/apiResponse.ts @@ -1,7 +1,7 @@ export interface ApiResponse { status: number; message: string; - data?: T; + result?: T; } export interface ApiErrorResponse { From fc24fb2c19739a9bd755a9a388fef76bfa2e299d Mon Sep 17 00:00:00 2001 From: Yejiin21 <101397075+Yejiin21@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:45:53 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-create/hooks/usePresignedUrlHook.ts | 32 ++++++++++++------- .../event-create/model/presignedUrl.ts | 2 +- .../event-create/ui/FileUpload.tsx | 4 +-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts index 51c12db3..11074d26 100644 --- a/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts +++ b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts @@ -1,43 +1,51 @@ import { PresignedUrlRequest, PresignedUrlResponse } from '../model/presignedUrl'; -import axios, { AxiosError } from 'axios'; +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 { data } = await axios.get>('/generate-presigned-url', { + const response = await axiosClient.get>('/generate-presigned-url', { params: dto, }); - return data; + console.log('Presigned URL 응답:', response.data.result?.preSignedUrl); + + return response.data.result?.preSignedUrl; } catch (error) { - alert((error as AxiosError>).response?.data.message); + console.error('Presigned URL 요청 실패:', error); + throw error; } }; -export const putS3Image = async ({ url, file, type }: { url: string; file: File; type: string }) => { +export const putS3Image = async ({ url, file }: { url: string; file: File }) => { try { - delete axios.defaults.headers.common.Authorization; + delete axiosClient.defaults.headers.common.Authorization; + console.log('업로드할 URL:', url); await axios.put(url, file, { headers: { - 'Content-Type': type, + 'Content-Type': 'image/jpeg', }, }); } catch (error) { + console.error('S3 업로드 실패:', error); alert('이미지 업로드에 실패했습니다.'); throw new Error('Failed to upload image'); } }; -export const upuploadFile = async (file: File) => { - const { name, type } = file; +export const uploadFile = async (file: File) => { + const { name } = file; const presignedUrlResponse = await getPresignedUrl({ fileName: name }); - if (!presignedUrlResponse?.data?.result) { + if (!presignedUrlResponse) { throw new Error('Failed to get presigned url'); } - const url = presignedUrlResponse.data.result; + const url = presignedUrlResponse; + console.log('Presigned URL:', url); - await putS3Image({ url, file, type }); + await putS3Image({ url, file }); // S3 URL에서 presigned URL 파라미터를 제거하고 기본 URL 반환 return url.split('?')[0]; diff --git a/src/features/event-manage/event-create/model/presignedUrl.ts b/src/features/event-manage/event-create/model/presignedUrl.ts index 84c71ee9..93ca7d12 100644 --- a/src/features/event-manage/event-create/model/presignedUrl.ts +++ b/src/features/event-manage/event-create/model/presignedUrl.ts @@ -3,5 +3,5 @@ export interface PresignedUrlRequest { } export interface PresignedUrlResponse { - result: string; + preSignedUrl: string; } diff --git a/src/features/event-manage/event-create/ui/FileUpload.tsx b/src/features/event-manage/event-create/ui/FileUpload.tsx index 35a91afc..0149b4f9 100644 --- a/src/features/event-manage/event-create/ui/FileUpload.tsx +++ b/src/features/event-manage/event-create/ui/FileUpload.tsx @@ -1,6 +1,6 @@ import FileUploadImage from '../../../../../public/assets/event-manage/creation/FileUpload.svg'; import { useRef, useState } from 'react'; -import { upuploadFile } from '../hooks/usePresignedUrlHook'; +import { uploadFile } from '../hooks/usePresignedUrlHook'; const FileUpload = () => { const [isDragging, setIsDragging] = useState(false); @@ -19,7 +19,7 @@ const FileUpload = () => { } try { - const imageUrl = await upuploadFile(file); + const imageUrl = await uploadFile(file); setPreviewUrl(imageUrl); } catch (error) { console.error('파일 업로드 실패:', error); From e87af4513bc5ddab87f941b467d64bfc2d66ebc4 Mon Sep 17 00:00:00 2001 From: Yejiin21 <101397075+Yejiin21@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:45:22 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EC=82=AC=EC=A7=84=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20presigned=20url=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-create/ui/TextEditor.tsx | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/features/event-manage/event-create/ui/TextEditor.tsx b/src/features/event-manage/event-create/ui/TextEditor.tsx index 3fcb913f..1209b868 100644 --- a/src/features/event-manage/event-create/ui/TextEditor.tsx +++ b/src/features/event-manage/event-create/ui/TextEditor.tsx @@ -1,7 +1,8 @@ // 사진 첨부는 추후에... -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css'; +import { uploadFile } from '../hooks/usePresignedUrlHook'; const formats = [ 'font', @@ -25,23 +26,55 @@ const formats = [ const TextEditor = () => { const [content, setContent] = useState(''); + const quillRef = useRef(null); + + 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; + + try { + const imageUrl = await uploadFile(file); + const range = quillInstance.getSelection(); + if (range) { + quillInstance.insertEmbed(range.index, 'image', imageUrl); + console.log('이미지 첨부 성공:', imageUrl); + } + } catch (error) { + console.error('이미지 업로드 실패:', error); + alert('이미지 업로드에 실패했습니다.'); + } + }; + }; const handleChange = (value: string) => { - const newText = value.replace(/<\/?[^>]+(>|$)/g, ''); // 태그 제거 - setContent(newText); - console.log(newText); + setContent(value); + console.log(value); }; const modules = useMemo(() => { return { - toolbar: [ - [{ header: [1, 2, false] }], - ['bold', 'italic', 'underline', 'strike', 'blockquote'], - [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }], - ['link', 'image'], - [{ align: [] }, { color: [] }, { background: [] }], - ['clean'], - ], + 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, + }, + }, }; }, []); @@ -51,6 +84,7 @@ const TextEditor = () => { Date: Sun, 30 Mar 2025 00:46:38 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/event-manage/event-create/ui/FileUpload.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/features/event-manage/event-create/ui/FileUpload.tsx b/src/features/event-manage/event-create/ui/FileUpload.tsx index 0149b4f9..cf24a1ba 100644 --- a/src/features/event-manage/event-create/ui/FileUpload.tsx +++ b/src/features/event-manage/event-create/ui/FileUpload.tsx @@ -1,11 +1,13 @@ 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(null); const fileInputRef = useRef(null); + const { setEventState } = useFunnelState(); const handleFileUpload = async (file: File) => { if (file.size > 500 * 1024) { @@ -13,14 +15,15 @@ const FileUpload = () => { return; } - if (!['image/jpeg', 'image/png'].includes(file.type)) { - alert('jpg, png 파일만 업로드 가능합니다.'); + 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); } @@ -55,7 +58,7 @@ const FileUpload = () => { return (

배너 사진 첨부

-

500kB 이하의 jpg, png 파일만 등록할 수 있습니다.

+

500kB 이하의 jpeg, png 파일만 등록할 수 있습니다.

Date: Sun, 30 Mar 2025 00:54:43 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=94=94=ED=84=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EA=B0=92=20decription=EC=97=90=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/event-manage/event-create/ui/TextEditor.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/event-manage/event-create/ui/TextEditor.tsx b/src/features/event-manage/event-create/ui/TextEditor.tsx index 1209b868..6cadda9b 100644 --- a/src/features/event-manage/event-create/ui/TextEditor.tsx +++ b/src/features/event-manage/event-create/ui/TextEditor.tsx @@ -3,6 +3,7 @@ import { useMemo, useRef, useState } from 'react'; import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css'; import { uploadFile } from '../hooks/usePresignedUrlHook'; +import { useFunnelState } from '../model/FunnelContext'; const formats = [ 'font', @@ -27,6 +28,7 @@ const formats = [ const TextEditor = () => { const [content, setContent] = useState(''); const quillRef = useRef(null); + const { setEventState } = useFunnelState(); const imageHandler = async () => { if (!quillRef.current) return; @@ -46,7 +48,6 @@ const TextEditor = () => { const range = quillInstance.getSelection(); if (range) { quillInstance.insertEmbed(range.index, 'image', imageUrl); - console.log('이미지 첨부 성공:', imageUrl); } } catch (error) { console.error('이미지 업로드 실패:', error); @@ -57,7 +58,7 @@ const TextEditor = () => { const handleChange = (value: string) => { setContent(value); - console.log(value); + setEventState(prev => ({ ...prev, description: value })); }; const modules = useMemo(() => { From 0346d9244527dd328cea02c665cdd1eac9ea6960 Mon Sep 17 00:00:00 2001 From: Yejiin21 <101397075+Yejiin21@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:00:13 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refact:=20Base64=20=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=9C=A0=EC=A7=80=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event-manage/event-create/hooks/usePresignedUrlHook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts index 11074d26..dcef1b10 100644 --- a/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts +++ b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts @@ -24,7 +24,7 @@ export const putS3Image = async ({ url, file }: { url: string; file: File }) => console.log('업로드할 URL:', url); await axios.put(url, file, { headers: { - 'Content-Type': 'image/jpeg', + 'Content-Type': file.type, }, }); } catch (error) {