WIP: 게시글 첨부파일 작업중

This commit is contained in:
2025-05-02 18:22:40 +09:00
parent cbf582a18a
commit 1bf2919a09
9 changed files with 185 additions and 21 deletions
@@ -1 +1,2 @@
export { Post } from "./ui/post";
export { PostForm } from "./ui/post-form";
@@ -0,0 +1,417 @@
import dayjs from "dayjs";
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 { useLocalStorage } from "@frontend-opensource/use-react-hooks";
import {
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Textarea,
DateRangePicker,
FileUploader,
Button,
} from "@common/components";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import type { Board } from "@/entities/board";
import { handleTusUpload } from "@/entities/tus";
import { AttachmentApi, type Attachment } from "@/entities/attachment";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import { useUserStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
type PostFormProps<T extends PostFormData | PostUpdateFormData> = {
form: UseFormReturn<T>;
handleSave: (data: T) => Promise<Post>;
};
function PostForm<T extends PostFormData | PostUpdateFormData>({
form,
handleSave,
}: PostFormProps<T>) {
const atflCd = form.watch("atflCd" as FieldPath<T>);
const { userInfo } = useUserStore((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 { data: attachmentData } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd as string),
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 saveInterceptors = async (data: T) => {
if (deleteAttachments.length > 0) {
await Promise.all(
deleteAttachments.map(AttachmentApi.deleteAttachmentByAtflId),
);
}
handleSave(data);
};
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 (
<FormWrapper form={form} handleSave={saveInterceptors}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="85%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>제목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"pstTtl" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="제목을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>작성자</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"wrtrNm" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="작성자를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{!userInfo?.userUnqId && (
<TableRow>
<TableHead>비밀번호</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"pstPswd" as FieldPath<T>}
render={({ field }) => (
<FormItem className="flex flex-col">
<FormControl>
<Input
{...field}
type="password"
value={field.value ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="비밀번호를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
<TableRow>
<TableHead>내용</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"pstCn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
onChange={field.onChange}
className="h-[20rem] w-full !text-[1.3rem]"
placeholder="내용을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{bbsInfo?.rfrncUseYn === "Y" && (
<TableRow>
<TableHead>참조 URL</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"rfrncUrl" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="URL을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.popupUseYn === "Y" && (
<TableRow>
<TableHead>팝업여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"popupYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-4">
<RadioGroup
name="popupYn"
value={field.value?.toString() ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="popupY" />
<Label htmlFor="popupY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="popupN" />
<Label htmlFor="popupN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.upndFixUseYn === "Y" && (
<TableRow>
<TableHead>상단고정</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"upndFixYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-4">
<RadioGroup
name="upndFixYn"
value={field.value?.toString() ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="upndFixY" />
<Label htmlFor="upndFixY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="upndFixN" />
<Label htmlFor="upndFixN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.pstgPrdUseYn === "Y" && (
<TableRow>
<TableHead>게시기간</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"prdPstgYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-4">
<RadioGroup
name="prdPstgYn"
value={field.value?.toString() ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="prdPstgY" />
<Label htmlFor="prdPstgY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="prdPstgN" />
<Label htmlFor="prdPstgN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
<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={(e) => {
const files = e.target.files;
if (!files) return;
for (const file of files) {
handleTusUpload(file, {
handleResponse: handleTusResponse,
});
}
}}
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>
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FormWrapper>
);
}
const PeriodSelector = ({ disabled }: { disabled: boolean }) => {
const [periodCode, setPeriodCode] = useState<number | null>(null);
const handlePeriodCodeChange = (value: string) => {
setPeriodCode(parseInt(value));
};
const postingPeriod = (periodCode: number | null) => {
if (!periodCode) return "";
if (periodCode === 99) return "상시";
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
.add(periodCode, "day")
.format("YYYY-MM-DD")}`;
};
return (
<div className="flex items-center gap-4">
<CodeSelect
upCd="PSTGPRDCD"
onValueChange={handlePeriodCodeChange}
disabled={disabled}
placeholder="기간 선택"
/>
{!!periodCode && (
<span className="text-lg text-label">
게시 기간: <u>{postingPeriod(periodCode)}</u>
</span>
)}
{periodCode === 0 && <DateRangePicker />}
</div>
);
};
export { PostForm };