WIP: 게시글 첨부파일 작업중
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user