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..dcef1b10 --- /dev/null +++ b/src/features/event-manage/event-create/hooks/usePresignedUrlHook.ts @@ -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>('/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'); + } +}; + +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]; +}; 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..93ca7d12 --- /dev/null +++ b/src/features/event-manage/event-create/model/presignedUrl.ts @@ -0,0 +1,7 @@ +export interface PresignedUrlRequest { + fileName: string; +} + +export interface PresignedUrlResponse { + 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 ba03b5d5..cf24a1ba 100644 --- a/src/features/event-manage/event-create/ui/FileUpload.tsx +++ b/src/features/event-manage/event-create/ui/FileUpload.tsx @@ -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(null); + const fileInputRef = useRef(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) => { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file); + }; + return (

배너 사진 첨부

-

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

-
- 파일 업로드 - 이미지를 끌어서 올리거나 클릭해서 업로드 하세요. +

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

+
+ + {previewUrl ? ( + 업로드된 이미지 + ) : ( + <> + 파일 업로드 + 이미지를 끌어서 올리거나 클릭해서 업로드 하세요. + + )}
); }; + export default FileUpload; diff --git a/src/features/event-manage/event-create/ui/TextEditor.tsx b/src/features/event-manage/event-create/ui/TextEditor.tsx index 3fcb913f..6cadda9b 100644 --- a/src/features/event-manage/event-create/ui/TextEditor.tsx +++ b/src/features/event-manage/event-create/ui/TextEditor.tsx @@ -1,7 +1,9 @@ // 사진 첨부는 추후에... -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'; +import { useFunnelState } from '../model/FunnelContext'; const formats = [ 'font', @@ -25,23 +27,55 @@ const formats = [ const TextEditor = () => { const [content, setContent] = useState(''); + const quillRef = useRef(null); + const { setEventState } = useFunnelState(); + + 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); + } + } catch (error) { + console.error('이미지 업로드 실패:', error); + alert('이미지 업로드에 실패했습니다.'); + } + }; + }; const handleChange = (value: string) => { - const newText = value.replace(/<\/?[^>]+(>|$)/g, ''); // 태그 제거 - setContent(newText); - console.log(newText); + setContent(value); + setEventState(prev => ({ ...prev, description: 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 +85,7 @@ const TextEditor = () => { { status: number; message: string; - data?: T; + result?: T; } export interface ApiErrorResponse {