refactor: GeneralFileUploader 컴포넌트 분리

This commit is contained in:
2025-05-12 17:02:25 +09:00
parent 528c281859
commit b202b4a99d
2 changed files with 140 additions and 107 deletions
@@ -0,0 +1,129 @@
import { useEffect, useState } from "react";
import type { FieldPath, UseFormReturn } from "react-hook-form";
import type { HttpResponse, Upload } from "tus-js-client";
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
import { Button, FileUploader } from "@common/components";
import type { Board } from "@/entities/board";
import { handleTusUpload } from "@/entities/tus";
import { type Attachment } from "@/entities/attachment";
import type { PostFormData, PostUpdateFormData } from "@/entities/post";
import { setCookie, useAlertStore } from "@/shared/lib";
function GeneralFileUploader<T extends PostFormData | PostUpdateFormData>({
form,
attachmentData,
deleteAttachmentIds,
setDeleteAttachmentIds,
}: {
form: UseFormReturn<T>;
attachmentData?: Attachment[];
deleteAttachmentIds: string[];
setDeleteAttachmentIds: (deleteAttachmentIds: string[]) => void;
}) {
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
const { setMessage: alert } = useAlertStore((state) => state);
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
const [attachments, setAttachments] = useState<Record<string, Attachment>>(
{},
);
const handleTusResponse = (res: HttpResponse, upload: Upload) => {
const fileInfo = res.getHeader("X-File-Info");
if (
typeof fileInfo !== "string" ||
fileInfo === undefined ||
fileInfo === null
)
return;
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
if (atflCd) form.setValue("atflCd" as FieldPath<T>, atflCd);
if (atflId) {
setAttachments((prev) => ({
...prev,
[atflId]: {
atflCd,
atflId,
orgnlFileNm: (upload.file as File).name,
atflFlieSz: (upload.file as File).size,
atflType: (upload.file as File).type,
},
}));
}
};
const onUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const atflCd = form.getValues("atflCd" as FieldPath<T>);
if (atflCd) setCookie("atfl-cd-key", atflCd as string);
const files = e.target.files;
if (!files) return;
if (bbsInfo?.atflMaxCnt && files.length > bbsInfo.atflMaxCnt) {
alert("첨부파일 최대 개수를 초과했습니다.");
return;
}
for (const file of files) {
const fileSize = Math.round(file.size / 1024); // Byte -> KB
if (bbsInfo?.atflMaxFileSz && fileSize > bbsInfo.atflMaxFileSz) {
alert("첨부파일 최대 크기를 초과했습니다.");
return;
}
handleTusUpload(file, {
handleResponse: handleTusResponse,
});
}
};
useEffect(() => {
const mapper = (data: Attachment[]) =>
data.reduce(
(acc, curr) => {
acc[curr.atflId] = curr;
return acc;
},
{} as Record<string, Attachment>,
);
if (attachmentData) setAttachments(mapper(attachmentData));
}, [attachmentData]);
useEffect(() => {
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
}, [storedBbsInfo]);
return (
<FileUploader
id="atflCd"
multiple
className="flex !h-auto max-w-full flex-row-reverse gap-4 [&>label]:h-[3.2rem]"
onUpload={onUpload}
customPreview={
<div className="flex w-full gap-4">
{Object.entries(attachments).map(([atflId, attachment]) => {
if (deleteAttachmentIds.includes(atflId)) return null;
return (
<div
key={atflId}
className="flex h-[3.2rem] items-center gap-2 rounded-sm border border-border bg-background px-3 py-2 text-foreground"
>
<span>{attachment.orgnlFileNm}</span>
<Button
color="lightGray"
className="flex size-7 items-center justify-center rounded-full p-0"
onClick={() =>
setDeleteAttachmentIds([...deleteAttachmentIds, atflId])
}
>
<i className="diveicon di-x text-xs" />
</Button>
</div>
);
})}
</div>
}
/>
);
}
export { GeneralFileUploader };
@@ -1,7 +1,6 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import type { HttpResponse, Upload } from "tus-js-client";
import type { UseFormReturn, FieldPath } from "react-hook-form";
import {
useConfirm,
@@ -18,17 +17,16 @@ import {
TableHead,
TableRow,
Textarea,
FileUploader,
Button,
} from "@common/components";
import { handleTusUpload } from "@/entities/tus";
import { BOARD_POSTING_CODE, type Board } from "@/entities/board";
import { AttachmentApi, type Attachment } from "@/entities/attachment";
import { AttachmentApi } from "@/entities/attachment";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useUserStore, useAlertStore, setCookie } from "@/shared/lib";
import { useUserStore, setCookie } from "@/shared/lib";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { PostingPeriodSelector } from "./posting-period-selector";
import { GeneralFileUploader } from "./general-file-uploader";
type PostFormProps<T extends PostFormData | PostUpdateFormData> = {
form: UseFormReturn<T>;
@@ -45,14 +43,10 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
const { confirm } = useConfirm();
const { userInfo } = useUserStore((state) => state);
const { setMessage: alert } = useAlertStore((state) => state);
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
const [attachments, setAttachments] = useState<Record<string, Attachment>>(
{},
);
const [deleteAttachments, setDeleteAttachments] = useState<string[]>([]);
const [deleteAttachmentIds, setDeleteAttachmentIds] = useState<string[]>([]);
const { data: attachmentData } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
@@ -60,77 +54,17 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
enabled: !!atflCd,
});
const handleTusResponse = (res: HttpResponse, upload: Upload) => {
const fileInfo = res.getHeader("X-File-Info");
if (
typeof fileInfo !== "string" ||
fileInfo === undefined ||
fileInfo === null
)
return;
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
if (atflCd) form.setValue("atflCd" as FieldPath<T>, atflCd);
if (atflId) {
setAttachments((prev) => ({
...prev,
[atflId]: {
atflCd,
atflId,
orgnlFileNm: (upload.file as File).name,
atflFlieSz: (upload.file as File).size,
atflType: (upload.file as File).type,
},
}));
}
};
const onSubmit = async (data: T) => {
if (await confirm("저장하시겠습니까?")) {
if (deleteAttachments.length > 0) {
if (deleteAttachmentIds.length > 0) {
await Promise.all(
deleteAttachments.map(AttachmentApi.deleteAttachmentByAtflId),
deleteAttachmentIds.map(AttachmentApi.deleteAttachmentByAtflId),
);
}
await handleSave(data);
}
};
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const atflCd = form.getValues("atflCd" as FieldPath<T>);
if (atflCd) setCookie("atfl-cd-key", atflCd as string);
const files = e.target.files;
if (!files) return;
if (bbsInfo?.atflMaxCnt && files.length > bbsInfo.atflMaxCnt) {
alert("첨부파일 최대 개수를 초과했습니다.");
return;
}
for (const file of files) {
const fileSize = Math.round(file.size / 1024); // Byte -> KB
if (bbsInfo?.atflMaxFileSz && fileSize > bbsInfo.atflMaxFileSz) {
alert("첨부파일 최대 크기를 초과했습니다.");
return;
}
handleTusUpload(file, {
handleResponse: handleTusResponse,
});
}
};
useEffect(() => {
const mapper = (data: Attachment[]) =>
data.reduce(
(acc, curr) => {
acc[curr.atflId] = curr;
return acc;
},
{} as Record<string, Attachment>,
);
if (attachmentData) setAttachments(mapper(attachmentData));
}, [attachmentData]);
useEffect(() => {
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
}, [storedBbsInfo]);
@@ -485,41 +419,11 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell className="border">
<FileUploader
id="atflCd"
multiple
className="flex !h-auto max-w-full flex-row-reverse gap-4 [&>label]:h-[3.2rem]"
onUpload={handleUpload}
customPreview={
<div className="flex w-full gap-4">
{Object.entries(attachments).map(
([atflId, attachment]) => {
if (deleteAttachments.includes(atflId))
return null;
return (
<div
key={atflId}
className="flex h-[3.2rem] items-center gap-2 rounded-sm border border-border bg-background px-3 py-2 text-foreground"
>
<span>{attachment.orgnlFileNm}</span>
<Button
color="lightGray"
className="flex size-7 items-center justify-center rounded-full p-0"
onClick={() =>
setDeleteAttachments((prev) => [
...prev,
atflId,
])
}
>
<i className="diveicon di-x text-xs" />
</Button>
</div>
);
},
)}
</div>
}
<GeneralFileUploader
form={form}
attachmentData={attachmentData}
deleteAttachmentIds={deleteAttachmentIds}
setDeleteAttachmentIds={setDeleteAttachmentIds}
/>
</TableCell>
</TableRow>