WIP: 게시글 등록, 수정 작업중

This commit is contained in:
2025-05-02 14:40:35 +09:00
parent 0b0d567072
commit 9b20dc4847
12 changed files with 290 additions and 186 deletions
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/notice";
export default CreatePage;
@@ -2,6 +2,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
@@ -17,9 +18,11 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
<QueryClientProvider client={queryClient}>
<StoreProvider>
<ConfirmProvider>
<MenuWrapper>{children}</MenuWrapper>
<AlertDialog />
<ConfirmDialog />
<AuthWrapper>
<MenuWrapper>{children}</MenuWrapper>
<AlertDialog />
<ConfirmDialog />
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
</QueryClientProvider>
@@ -1 +1,2 @@
export { LoginApi } from "./api/login-service";
export { AuthWrapper } from "./lib/auth-wrapper";
@@ -0,0 +1,40 @@
"use client";
import { useEffect } from "react";
import { client } from "@/shared/api";
import type { IUserInfo } from "@/shared/store";
import { getCookie, useUserStore } from "@/shared/lib";
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
import { LoginApi } from "../api/login-service";
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
throw new Error("Missing required authentication tokens");
}
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
);
if (!userInfo) setUserInfo(response.data);
}
useEffect(() => {
(async () => {
try {
await authCheck();
} catch (error) {
console.error(error);
await LoginApi.silentRefresh();
await authCheck();
}
})();
}, []);
return <>{children}</>;
};
export { AuthWrapper };
@@ -11,9 +11,15 @@ import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
placeholder?: string;
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
function CodeSelect({
upCd,
defaultValue,
placeholder = "선택",
...props
}: CodeSelectProps) {
const { data: code } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
queryFn: () => CodeApi.getCode(upCd),
@@ -21,7 +27,7 @@ function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
});
const defaultValueText =
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택";
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
return (
<Select {...props}>
@@ -96,11 +96,13 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const isAllSelected =
siteList && selectedRows.length === siteList.content.length;
const postingPeriod = (siteId: string) => {
const periodCode = Number(rememberPeriodCode[siteId]);
const postingPeriodFormat = (siteId: string) => {
const periodCode = parseInt(rememberPeriodCode[siteId] ?? "");
return postingPeriod(periodCode);
};
const postingPeriod = (periodCode: number) => {
if (periodCode === 99) return "상시";
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
.add(periodCode, "day")
.format("YYYY-MM-DD")}`;
@@ -175,7 +177,9 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
const from = dayjs().format("YYYY-MM-DD 00:00:00");
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
const to = dayjs()
.add(parseInt(value), "day")
.format("YYYY-MM-DD 23:59:59");
setRememberPeriodCode((prev) => ({
...prev,
@@ -272,17 +276,18 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
onValueChange={handlePeriodCodeChange(site.siteId)}
disabled={!selectedRows.includes(i)}
/>
{!!Number(rememberPeriodCode[site.siteId]) && (
{!!parseInt(rememberPeriodCode[site.siteId] ?? "") && (
<span
className={cn(
"text-lg text-label",
!selectedRows.includes(i) && "text-muted",
)}
>
게시 기간: <u>{postingPeriod(site.siteId)}</u>
게시 기간: <u>{postingPeriodFormat(site.siteId)}</u>
</span>
)}
{Number(rememberPeriodCode[site.siteId]) === 0 && (
{parseInt(rememberPeriodCode[site.siteId] ?? "") ===
0 && (
<DateRangePicker
handleDateChange={(date) =>
handlePeriodDateChange(site.siteId)(
@@ -1,3 +1,4 @@
export { NoticePage } from "./ui/notice-page";
export { NoticePost } from "./ui/notice-post";
export { ModifyPage } from "./ui/modify-page";
export { CreatePage } from "./ui/create-page";
@@ -4,14 +4,15 @@ import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { postFormSchema, type PostFormData } from "@/entities/post";
import { PostApi } from "@/entities/post";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { NoticeForm } from "./notice-form";
import { NoticeLayout } from "./notice-layout";
function CreatePage() {
const router = useRouter();
const { userInfo } = useUserStore((state) => state);
const { setMessage: alert } = useAlertStore((state) => state);
const { mutateAsync: createPost } = useMutation({
@@ -23,10 +24,14 @@ function CreatePage() {
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
defaultValues: {
values: {
bbsId: 0,
pstTtl: "",
pstPswd: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
popupYn: "N",
upndFixYn: "N",
prdPstgYn: "N",
},
});
@@ -1,5 +1,7 @@
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import type { UseFormReturn, FieldPath } from "react-hook-form";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
import {
Input,
Label,
@@ -11,9 +13,14 @@ import {
TableHead,
TableRow,
Textarea,
Button,
DateRangePicker,
} from "@common/components";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import type { Board } from "@/entities/board";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import { useUserStore } from "@/shared/lib";
type NoticeFormProps<T extends PostFormData | PostUpdateFormData> = {
form: UseFormReturn<T>;
@@ -24,63 +31,68 @@ function NoticeForm<T extends PostFormData | PostUpdateFormData>({
form,
handleSave,
}: NoticeFormProps<T>) {
const onSubmit = async (data: T) => {
await handleSave(data);
};
const { userInfo } = useUserStore((state) => state);
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
useEffect(() => {
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
}, [storedBbsInfo]);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="35%" />
<col width="15%" />
<col width="35%" />
</colgroup>
<TableBody>
<FormWrapper form={form} handleSave={handleSave}>
<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" colSpan={3}>
<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="!text-[1.3rem]"
placeholder="작성자를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
<TableHead>비밀번호</TableHead>
<TableCell className="border">
<FormField
@@ -93,7 +105,7 @@ function NoticeForm<T extends PostFormData | PostUpdateFormData>({
{...field}
type="password"
value={field.value ?? ""}
className="!text-[1.3rem]"
className="w-[20rem] !text-[1.3rem]"
placeholder="비밀번호를 입력해주세요"
/>
</FormControl>
@@ -102,31 +114,33 @@ function NoticeForm<T extends PostFormData | PostUpdateFormData>({
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>내용</TableHead>
<TableCell className="border" colSpan={3}>
<FormField
control={form.control}
name={"pstCn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
onChange={field.onChange}
className="w-full !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" colSpan={3}>
<TableCell className="border">
<FormField
control={form.control}
name={"rfrncUrl" as FieldPath<T>}
@@ -145,118 +159,160 @@ function NoticeForm<T extends PostFormData | PostUpdateFormData>({
/>
</TableCell>
</TableRow>
)}
{bbsInfo?.popupUseYn === "Y" && (
<TableRow>
<TableHead>팝업여부</TableHead>
<TableCell className="border" colSpan={3}>
<TableCell className="border">
<FormField
control={form.control}
name={"popupYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
name="popupYn"
value={String(field.value) ?? ""}
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>
<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" colSpan={3}>
<TableCell className="border">
<FormField
control={form.control}
name={"upndFixYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
name="upndFixYn"
value={String(field.value) ?? ""}
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>
<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" colSpan={3}>
<TableCell className="border">
<FormField
control={form.control}
name={"prdPstgYn" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
name="prdPstgYn"
value={String(field.value) ?? ""}
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>
<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" colSpan={3}>
<FormField
control={form.control}
name={"atflCd" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl></FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
<div className="mt-4 flex justify-center">
<Button type="submit" size="lg">
저장
</Button>
</div>
</div>
</form>
</Form>
)}
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name={"atflCd" as FieldPath<T>}
render={() => (
<FormItem>
<FormControl></FormControl>
</FormItem>
)}
/>
</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 { NoticeForm };
@@ -44,7 +44,7 @@ function NoticePage({ bbsId }: { bbsId: string }) {
queryFn: () =>
PostApi.getPostList({
...postParams,
bbsId: Number(bbsId),
bbsId: parseInt(bbsId),
page: pagination.pageIndex,
size: pagination.pageSize,
}),
@@ -84,23 +84,7 @@ function NoticePage({ bbsId }: { bbsId: string }) {
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">상태</Label>
<Select
value={postParams.delYn}
onValueChange={(delYn) => {
setPostParams({ ...postParams, delYn });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="N">정상</SelectItem>
<SelectItem value="Y">삭제</SelectItem>
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">검색</Label>
<Label className="text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
@@ -133,13 +117,13 @@ function NoticePage({ bbsId }: { bbsId: string }) {
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">공지사항 목록</h3>
<div className="flex gap-2">
<Button onClick={() => router.push("/bbs/register")}>
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
@@ -1,3 +1,4 @@
export { getCookie, setCookie, removeCookie } from "./cookie";
export { useAlertStore } from "./use-alert-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
@@ -34,7 +34,7 @@ export const useTus = () => {
},
onProgress(bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
setProgress(Number(percentage));
setProgress(parseInt(percentage));
},
onAfterResponse(req, res) {
const fileInfo = res.getHeader("X-File-Info");