코드만 존재하며 설명은 추후 설명예정입니다.
Component.tsx
"use client";
import { ChangeEvent, MouseEvent, useEffect, useRef, useState } from "react";
import classes from "./fileUploader.module.scss";
import Image from "next/image";
import addBtn from "/public/pages/reservation/add.svg";
import EtcWrapper from "./_section/etcWrapper";
import { v4 as uuidv4 } from "uuid";
export interface FileCallBackType {
fileName: string;
filePath: string;
}
interface FileUploaderTypes {
onChangeFile: (urls: Array<FileCallBackType>) => void;
onChangeDeleteFile?: (urls: Array<FileCallBackType>) => void;
bucketName: string;
s3Path: string;
maxFileCount?: number;
maxFileSize?: number;
isImage?: boolean;
value?: Array<FileCallBackType>;
}
interface UploadFileDataType {
url: string;
fileName: string;
}
const FileUploader = ({
onChangeFile,
onChangeDeleteFile,
bucketName,
maxFileCount = 5,
maxFileSize = 10,
isImage = false,
s3Path,
value,
}: FileUploaderTypes) => {
const MAX_FILES = maxFileCount;
const MAX_FILE_SIZE_MB = maxFileSize;
const fileUploadRef = useRef<HTMLInputElement>(null);
const [fileNames, setFileNames] = useState<Array<string>>([]);
const [fileUrls, setFileUrls] = useState<Array<string>>([]);
const [sendFiles, setSendFiles] = useState<Array<any>>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const onClickFileSearch = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (fileUploadRef.current) {
fileUploadRef.current.click();
}
};
const uploadFile = async (file: File): Promise<UploadFileDataType | null> => {
const formData = new FormData();
const uniqueFileName = `${uuidv4()}___${file.name}`;
formData.append("file", file);
formData.append("fileName", uniqueFileName);
formData.append("fileType", file.type);
formData.append("bucketName", bucketName);
formData.append("s3Path", s3Path);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
console.error("Failed to upload file");
return null;
}
const data = await response.json();
return { url: data.url, fileName: uniqueFileName };
};
const onChangeFileItem = async (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) return;
if (files.length > MAX_FILES) {
setErrorMessage(`You can only upload up to ${MAX_FILES} files.`);
return;
}
const validFiles = Array.from(files).filter((file) => {
if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {
setErrorMessage(`Each file must be under ${MAX_FILE_SIZE_MB}MB.`);
return false;
}
if (isImage && !file.type.match(/^image\/(jpeg|png|gif|bmp|webp)$/)) {
setErrorMessage("Only image files are allowed.");
return false;
}
return true;
});
if (validFiles.length !== files.length) {
return;
}
const urlsArray: Array<string> = [...fileUrls];
const fileNameArray: Array<string> = [...fileNames];
const sendData = [];
for (const file of validFiles) {
const { url, fileName }: any = await uploadFile(file);
sendData.push({ fileName: file.name, filePath: url });
if (url) {
urlsArray.push(url);
}
if (fileName) {
fileNameArray.push(fileName);
}
}
onChangeFile([...sendFiles, ...sendData]);
setFileUrls([...fileUrls, ...urlsArray]);
setFileNames([...fileNames, ...fileNameArray]);
setErrorMessage(null);
};
useEffect(() => {
if (!value || Array.isArray(value) === false) return;
if (Array.isArray(value)) {
const names = value.map((v) => v.fileName);
setFileNames(names);
const urls = value.map((v) => v.filePath);
setFileUrls(urls);
setSendFiles(value);
}
}, [value]);
useEffect(() => {}, []);
return (
<div className={classes.wrapper}>
<div className={classes.uploadWrapper}>
<input
className={classes.titleInput}
type="text"
value={fileNames
.map((name) => name.split("___").reverse()[0])
.join(", ")}
readOnly
/>
<button className={classes.whiteButton} onClick={onClickFileSearch}>
<Image src={addBtn} alt="" />
</button>
<input
className={classes.hiddenFileInput}
type="file"
name="imageUpload"
onChange={onChangeFileItem}
ref={fileUploadRef}
multiple
/>
</div>
{errorMessage && <p className={classes.error}>{errorMessage}</p>}
<EtcWrapper
fileUrls={fileUrls}
fileNames={fileNames}
sendFiles={sendFiles}
s3Path={s3Path}
bucketName={bucketName}
setFileUrls={setFileUrls}
setFileNames={setFileNames}
setSendFiles={setSendFiles}
onChangeDeleteFile={onChangeDeleteFile}
/>
</div>
);
};
export default FileUploader;
Etc wrapper
import { Dispatch, SetStateAction } from "react";
import classes from "../fileUploader.module.scss";
import { FiFileText, FiMinus } from "react-icons/fi";
import { useSetAtom } from "jotai";
import { addPopupAtom } from "@/atom/popup";
import ImagePrevModal from "./imagePrev";
import Image from "next/image";
import { FileCallBackType } from "../fileUploader";
interface EtcWrapperTypes {
s3Path: string;
bucketName: string;
fileUrls: Array<string>;
fileNames: Array<string>;
sendFiles: Array<FileCallBackType>;
setFileUrls: Dispatch<SetStateAction<Array<string>>>;
setFileNames: Dispatch<SetStateAction<Array<string>>>;
setSendFiles: Dispatch<SetStateAction<Array<FileCallBackType>>>;
onChangeDeleteFile?: (urls: Array<FileCallBackType>) => void;
}
const EtcWrapper = ({
fileUrls,
fileNames,
sendFiles,
s3Path,
bucketName,
setFileUrls,
setFileNames,
setSendFiles,
onChangeDeleteFile,
}: EtcWrapperTypes) => {
const addPopup = useSetAtom(addPopupAtom);
const onClickFile = (url: string, alt: string) => {
return addPopup({
type: "popup",
content: <ImagePrevModal url={url} alt={alt} />,
});
};
const onClickDelete = async (index: number) => {
const key = `uploads/${s3Path}/${fileNames[index]}`;
const response = await fetch("/api/upload/delete", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ bucketName, key }),
});
if (response.ok) {
console.log("File deleted successfully");
setFileUrls((prevUrls) => prevUrls.filter((_, i) => i !== index));
setFileNames((prevNames) => prevNames.filter((_, i) => i !== index));
setSendFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
if (onChangeDeleteFile) {
onChangeDeleteFile([...sendFiles.filter((_, i) => i !== index)]);
}
} else {
console.error("Failed to delete file");
}
};
return (
<div className={classes.etcWrapper}>
{}
{}
{}
<div className={classes.previewList}>
{fileUrls.map((url, index) => (
<div key={index} className={classes.previewItem}>
<div
onClick={() => onClickDelete(index)}
className={classes.deleteIconWrapper}
>
<FiMinus />
</div>
{Array.isArray(fileNames) &&
fileNames.length > 0 &&
fileNames[index].match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) ? (
<div onClick={() => onClickFile(url, fileNames[index])}>
<img
src={url}
alt={fileNames[index]}
className={classes.previewImage}
/>
{}
</div>
) : (
<div>
<FiFileText size={100} color="#cccccc" />
</div>
)}
</div>
))}
</div>
</div>
);
};
export default EtcWrapper;
사용
const onChangeUploadFile = (urls: Array<FileCallBackType>) => {
setState({ name: "fileData", value: urls });
};
return(
<FileUploader
onChangeFile={onChangeUploadFile}
onChangeDeleteFile={onChangeUploadFile}
bucketName={"xplat-s3-bucket"}
s3Path={"admin/consulting"}
value={orderFormData.fileData}
/>
)
route.ts (Upload)
import { NextResponse } from "next/server";
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
const s3Client = new S3Client({
region: "ap-northeast-2",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
async function streamToBuffer(
stream: ReadableStream<Uint8Array>
): Promise<Buffer> {
const reader = stream.getReader();
const chunks = [];
let done, value;
while (!done) {
({ done, value } = await reader.read());
if (value) chunks.push(value);
}
return Buffer.concat(chunks);
}
export async function POST(req: Request) {
try {
const contentType = req.headers.get("content-type") || "";
if (!contentType.startsWith("multipart/form-data")) {
return NextResponse.json(
{ error: "Invalid content type" },
{ status: 400 }
);
}
const formData = await req.formData();
const file = formData.get("file") as Blob | null;
const fileName = formData.get("fileName") as string;
const fileType = formData.get("fileType") as string;
const bucketName = formData.get("bucketName") as string;
const s3Path = formData.get("s3Path") as string;
if (!file || !fileName || !fileType || !bucketName) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}
const arrayBuffer = await file.arrayBuffer();
const fileBuffer = Buffer.from(arrayBuffer);
const key = `uploads/${s3Path}/${fileName}`;
const upload = new Upload({
client: s3Client,
params: {
Bucket: bucketName,
Key: key,
Body: fileBuffer,
ContentType: fileType,
ContentDisposition: `attachment; filename="${encodeURIComponent(
fileName
)}"`,
},
});
console.log("Starting upload...");
await upload.done();
console.log("File uploaded successfully to S3.");
const downloadUrl = `https://${bucketName}.s3.amazonaws.com/${key}`;
console.log("Download URL:", downloadUrl);
return NextResponse.json({ url: downloadUrl });
} catch (error) {
console.error("Error uploading file:", error);
return NextResponse.json(
{ error: "Failed to upload file" },
{ status: 500 }
);
}
}
route.ts(delete)
import { NextResponse } from "next/server";
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: "ap-northeast-2",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function DELETE(req: Request) {
try {
const { bucketName, key } = await req.json();
if (!bucketName || !key) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 }
);
}
const deleteParams = {
Bucket: bucketName,
Key: key,
};
const command = new DeleteObjectCommand(deleteParams);
await s3Client.send(command);
return NextResponse.json({ message: "File deleted successfully" });
} catch (error) {
console.error("Error deleting file:", error);
return NextResponse.json(
{ error: "Failed to delete file" },
{ status: 500 }
);
}
}