refactor: 게시글 폼 로직 수정

This commit is contained in:
2025-05-12 15:14:31 +09:00
parent 8b265a7aae
commit aa56483f8d
5 changed files with 430 additions and 379 deletions
@@ -50,7 +50,8 @@ export class CommentApiService implements CommentService {
cmntPswd: string,
): Promise<Comment> {
const response = await client.delete<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}?cmntPswd=${cmntPswd}`,
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
{ params: { cmntPswd } },
);
return response.data;
}
@@ -1,5 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
@@ -13,6 +14,7 @@ import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const { userInfo } = useUserStore((state) => state);
@@ -25,7 +27,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(bbsId, pstNo, data),
onSuccess: () => {
alert("수정되었습니다.");
alert("수정되었습니다.", router.back);
},
});
@@ -1,5 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
@@ -13,6 +14,7 @@ import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const { userInfo } = useUserStore((state) => state);
@@ -25,7 +27,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(bbsId, pstNo, data),
onSuccess: () => {
alert("수정되었습니다.");
alert("수정되었습니다.", router.back);
},
});
@@ -1,9 +1,13 @@
import dayjs from "dayjs";
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 { useLocalStorage } from "@frontend-opensource/use-react-hooks";
import {
useConfirm,
useLocalStorage,
} from "@frontend-opensource/use-react-hooks";
import {
Input,
Label,
@@ -26,8 +30,7 @@ import { AttachmentApi, type Attachment } from "@/entities/attachment";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useUserStore, useAlertStore, setCookie } from "@/shared/lib";
import { FormWrapper } from "@/shared/ui/form-wrapper";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
type PostFormProps<T extends PostFormData | PostUpdateFormData> = {
form: UseFormReturn<T>;
@@ -40,6 +43,9 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
}: PostFormProps<T>) {
const atflCd = form.getValues("atflCd" as FieldPath<T>);
const router = useRouter();
const { confirm } = useConfirm();
const { userInfo } = useUserStore((state) => state);
const { setMessage: alert } = useAlertStore((state) => state);
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
@@ -81,6 +87,17 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
}
};
const onSubmit = async (data: T) => {
if (await confirm("저장하시겠습니까?")) {
if (deleteAttachments.length > 0) {
await Promise.all(
deleteAttachments.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);
@@ -104,15 +121,6 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
}
};
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(
@@ -134,109 +142,21 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
}, []);
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 as string) ?? ""}
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 as string) ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="작성자를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{!userInfo?.userUnqId && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="85%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>비밀번호</TableHead>
<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 as string) ?? ""}
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 as string) ?? ""}
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>}
name={"pstTtl" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
@@ -244,7 +164,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
{...field}
value={(field.value as string) ?? ""}
className="!text-[1.3rem]"
placeholder="URL을 입력해주세요"
placeholder="제목을 입력해주세요"
/>
</FormControl>
</FormItem>
@@ -252,256 +172,378 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.popupUseYn === "Y" && (
<TableRow>
<TableHead>팝업여부</TableHead>
<TableHead>작성자</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"popupYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"popupYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues = form.getValues().popupYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues("popupYn" as FieldPath<T>) ===
"N"
}
handlePeriodValueChange={(periodCode, date) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.팝업게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([{ ...fieldValues, ...date }]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.upndFixUseYn === "Y" && (
<TableRow>
<TableHead>상단고정</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"upndFixYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"upndFixYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues = form.getValues().upndFixYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues(
"upndFixYn" as FieldPath<T>,
) === "N"
}
handlePeriodValueChange={(periodCode, date) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.상단게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([{ ...fieldValues, ...date }]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.pstgPrdUseYn === "Y" && (
<TableRow>
<TableHead>게시기간</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"prdPstgYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"prdPstgYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues = form.getValues().prdPstgYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues(
"prdPstgYn" as FieldPath<T>,
) === "N"
}
handlePeriodValueChange={(periodCode, date) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.기간게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([{ ...fieldValues, ...date }]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.atflUseYn === "Y" && (
<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>
}
<FormField
control={form.control}
name={"wrtrNm" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={(field.value as string) ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="작성자를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</FormWrapper>
{!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 as string) ?? ""}
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 as string) ?? ""}
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 as string) ?? ""}
className="!text-[1.3rem]"
placeholder="URL을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.popupUseYn === "Y" && (
<TableRow>
<TableHead>팝업여부</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"popupYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"popupYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues = form.getValues().popupYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues(
"popupYn" as FieldPath<T>,
) === "N"
}
handlePeriodValueChange={(
periodCode,
date,
) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.팝업게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{ ...fieldValues, ...date },
]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.upndFixUseYn === "Y" && (
<TableRow>
<TableHead>상단고정</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"upndFixYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"upndFixYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues =
form.getValues().upndFixYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues(
"upndFixYn" as FieldPath<T>,
) === "N"
}
handlePeriodValueChange={(
periodCode,
date,
) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.상단게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{ ...fieldValues, ...date },
]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.pstgPrdUseYn === "Y" && (
<TableRow>
<TableHead>게시기간</TableHead>
<TableCell className="border">
<div className="flex items-center gap-4">
<FormField
control={form.control}
name={"prdPstgYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<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>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"prdPstgYnEtc" as FieldPath<T>}
render={({ field }) => {
const fieldValues =
form.getValues().prdPstgYnEtc?.[0];
return (
<FormItem>
<FormControl>
<PeriodSelector
defaultValues={{ ...fieldValues }}
disabled={
form.getValues(
"prdPstgYn" as FieldPath<T>,
) === "N"
}
handlePeriodValueChange={(
periodCode,
date,
) => {
field.onChange([
{
...fieldValues,
...date,
bbsPstgTypeCd:
BOARD_POSTING_CODE.기간게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{ ...fieldValues, ...date },
]);
}}
/>
</FormControl>
</FormItem>
);
}}
/>
</div>
</TableCell>
</TableRow>
)}
{bbsInfo?.atflUseYn === "Y" && (
<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>
}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
취소
</Button>
<Button size="lg" type="submit">
저장
</Button>
</div>
</form>
</Form>
);
}
@@ -110,25 +110,29 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
className="min-h-[40rem] p-8"
dangerouslySetInnerHTML={{ __html: postData?.pstCn ?? "" }}
/>
<hr className="mt-8" />
<div className="flex items-center gap-4 p-8">
<span className="font-bold">첨부파일</span>
{isFetching ? (
<Skeleton className="h-[2.5rem] w-[20rem]" />
) : (
filename && (
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
)
)}
</div>
{isFetching ? (
<div className="p-8">
<Skeleton className="h-[2.5rem] w-[32rem]" />
</div>
) : (
filename && (
<>
<hr />
<div className="flex items-center gap-4 p-8">
<span className="font-bold">첨부파일</span>
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
</div>
</>
)
)}
</div>
</PostAction>
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />