Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/features/event-manage/event-create/api/presignedUrl.ts
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
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)


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];
};
7 changes: 7 additions & 0 deletions src/features/event-manage/event-create/model/presignedUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface PresignedUrlRequest {
fileName: string;
}

export interface PresignedUrlResponse {
preSignedUrl: string;
}
84 changes: 80 additions & 4 deletions src/features/event-manage/event-create/ui/FileUpload.tsx
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;
59 changes: 47 additions & 12 deletions src/features/event-manage/event-create/ui/TextEditor.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -25,23 +27,55 @@ const formats = [

const TextEditor = () => {
const [content, setContent] = useState('');
const quillRef = useRef<ReactQuill | null>(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,
},
},
};
}, []);

Expand All @@ -51,6 +85,7 @@ const TextEditor = () => {
<ReactQuill
theme="snow"
value={content}
ref={quillRef}
modules={modules}
formats={formats}
onChange={handleChange}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/types/api/apiResponse.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface ApiResponse<T> {
status: number;
message: string;
data?: T;
result?: T;
}

export interface ApiErrorResponse {
Expand Down