feat: 상단고정 게시글 로직 작업

This commit is contained in:
2025-05-08 17:35:33 +09:00
parent 6bb1fc312f
commit 56249060c7
20 changed files with 683 additions and 253 deletions
@@ -0,0 +1,10 @@
import { ModifyPage } from "@/pages/(pst)/normal";
export default async function PstBbsNormalPostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -0,0 +1,10 @@
import { NormalPost } from "@/pages/(pst)/normal";
export default async function PstBbsNormalPostPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <NormalPost bbsId={bbsId} pstNo={pstNo} />;
}
@@ -0,0 +1,10 @@
import { NormalPage } from "@/pages/(pst)/normal";
export default async function PstBbsNormalPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <NormalPage bbsId={bbsId} />;
}
@@ -0,0 +1,10 @@
import { CreatePage } from "@/pages/(pst)/normal";
export default async function PstBbsNormalRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,8 +1,10 @@
export { BoardApi } from "./api/board-service";
export {
boardFormSchema,
BOARD_POSTING_CODE,
type Board,
type BoardFormData,
type BoardParams,
type BoardPopup,
type BoardPostingCode,
} from "./model/board-interface";
@@ -8,7 +8,15 @@ export const BOARD_TYPE = {
BBSMOVIE: "BBSMOVIE",
} as const;
export const BOARD_POSTING_CODE = {
팝업게시: "01",
상단게시: "02",
기간게시: "03",
} as const;
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
export type BoardPostingCode =
(typeof BOARD_POSTING_CODE)[keyof typeof BOARD_POSTING_CODE];
export interface BoardParams extends PageableParams {
keywordType?: string;
@@ -36,14 +36,24 @@ export interface PostInfo extends Post {
mdfr: string;
}
const postEtcSchema = z.object({
bbsPstgTypeCd: z.string(),
pstgPrdSeCd: z.string(),
pstgBgngDt: z.string().nullish(),
pstgEndDt: z.string().nullish(),
});
export const postFormSchema = z.object({
bbsId: z.number(),
pstTtl: z.string().nonempty(),
pstCn: z.string().nullish(),
rfrncUrl: z.string().nullish(),
popupYn: z.string().nullish(),
popupYnEtc: z.array(postEtcSchema).nullish(),
upndFixYn: z.string().nullish(),
upndFixYnEtc: z.array(postEtcSchema).nullish(),
prdPstgYn: z.string().nullish(),
prdPstgYnEtc: z.array(postEtcSchema).nullish(),
pstPswd: z.string(),
wrtrNm: z.string().nullish(),
wrtrCplc: z.string().nullish(),
@@ -0,0 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { NormalPage } from "./ui/normal-page";
export { NormalPost } from "./ui/normal-post";
@@ -0,0 +1,51 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
import { setCookie, useAlertStore, useUserStore } from "@/shared/lib";
function CreatePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const { userInfo } = useUserStore((state) => state);
const { setMessage: alert } = useAlertStore((state) => state);
const { mutateAsync: createPost } = useMutation({
mutationFn: (data: PostFormData) => PostApi.createPost(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
},
});
const form = useForm<PostFormData>({
resolver: zodResolver(postFormSchema),
values: {
bbsId: parseInt(bbsId),
pstTtl: "",
wrtrNm: userInfo?.userName ?? "",
pstPswd: userInfo?.userUnqId ?? "",
popupYn: "N",
upndFixYn: "N",
prdPstgYn: "N",
},
});
useEffect(() => {
setCookie("atfl-cd-key", "");
}, []);
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">게시글 등록</h3>
</div>
<PostForm form={form} handleSave={createPost} />
</div>
);
}
export { CreatePage };
@@ -0,0 +1,51 @@
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { PostForm } from "@/widgets/post";
import {
PostApi,
postUpdateFormSchema,
type PostUpdateFormData,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore } from "@/shared/lib";
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const { setMessage: alert } = useAlertStore((state) => state);
const { userInfo } = useUserStore((state) => state);
const { data: postInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
});
const { mutateAsync: updatePost } = useMutation({
mutationFn: (data: PostUpdateFormData) =>
PostApi.updatePost(bbsId, pstNo, data),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const form = useForm<PostUpdateFormData>({
resolver: zodResolver(postUpdateFormSchema),
values: {
...postInfo,
pstTtl: postInfo?.pstTtl ?? "",
pstPswd: userInfo?.userUnqId ?? "",
},
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">게시글 수정</h3>
</div>
<PostForm form={form} handleSave={updatePost} />
</div>
);
}
export { ModifyPage };
@@ -0,0 +1,3 @@
"use client";
export { PostPage as NormalPage } from "@/widgets/post";
@@ -0,0 +1,3 @@
"use client";
export { Post as NormalPost } from "@/widgets/post";
@@ -1,4 +1,4 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { NoticePage } from "./ui/notice-page";
export { NoticePost } from "./ui/notice-post";
export { ModifyPage } from "./ui/modify-page";
export { CreatePage } from "./ui/create-page";
@@ -1,152 +1,3 @@
"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
type PaginationState,
type RowSelectionState,
} from "@tanstack/react-table";
import {
Button,
DataTable,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { PostApi, type Post, type PostParams } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useMenuStore } from "@/shared/lib";
import { columns } from "../model/notice-table-columns";
function NoticePage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const { pageMenuAuthority } = useMenuStore((state) => state);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [postParams, setPostParams] = useState<PostParams>({
keywordType: "title",
keyword: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: postList, refetch } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId],
queryFn: () =>
PostApi.getPostList(bbsId, {
...postParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: false,
});
const table = useReactTable({
data: postList?.content ?? [],
columns,
state: {
pagination,
rowSelection,
},
rowCount: postList?.totalElements ?? 0,
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
};
const handleRowClick = (row: Post) => {
router.push(`${pathname}/${row.pstNo}`);
};
useEffect(() => {
refetch();
}, [pagination]);
return (
<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.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">공지사항 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}개씩 보기
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
</div>
);
}
export { NoticePage };
export { PostPage as NoticePage } from "@/widgets/post";
@@ -1,9 +1,3 @@
"use client";
import { Post } from "@/widgets/post";
function NoticePost({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
return <Post bbsId={bbsId} pstNo={pstNo} />;
}
export { NoticePost };
export { Post as NoticePost } from "@/widgets/post";
@@ -1,2 +1,3 @@
export { Post } from "./ui/post";
export { PostForm } from "./ui/post-form";
export { PostPage } from "./ui/post-page";
@@ -0,0 +1,103 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Button } from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import type { Post } from "@/entities/post";
export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
{
id: "no",
header: "NO",
cell: ({ row }) => {
if (row.original.isUpndFixPost) {
return (
<Badge
color="orange"
className="flex size-10 items-center justify-center"
>
<i className="diveicon di-volume-up-fill" />
</Badge>
);
}
return row.index + 1;
},
},
{
accessorKey: "pstTtl",
header: "제목",
cell: ({ row }) => {
const { pstTtl, inqNocs } = row.original;
return (
<div className="flex min-w-[20vw] items-center gap-2 text-left">
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
<Badge color="blue">NEW</Badge>
)}
{pstTtl}
</div>
);
},
},
{
accessorKey: "inqNocs",
header: "조회수",
},
{
accessorKey: "atflCd",
header: "첨부파일",
cell: ({ row }) => {
const atflCd = row.original.atflCd;
if (!atflCd) return null;
return <AttachmentButton atflCd={atflCd} />;
},
},
{
accessorKey: "rgtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
];
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
try {
let filename = "";
const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd);
const contentDisposition = response.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch) {
filename = decodeURIComponent(filenameMatch[1]);
}
}
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} catch (error) {
console.error(error);
}
};
return (
<Button size="icon" color="white" onClick={handleDownload}>
<i className="diveicon di-download" />
</Button>
);
};
@@ -20,8 +20,8 @@ import {
Button,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
import type { Board } from "@/entities/board";
import { handleTusUpload } from "@/entities/tus";
import { BOARD_POSTING_CODE, type Board } from "@/entities/board";
import { AttachmentApi, type Attachment } from "@/entities/attachment";
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
@@ -38,7 +38,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
form,
handleSave,
}: PostFormProps<T>) {
const atflCd = form.watch("atflCd" as FieldPath<T>);
const atflCd = form.getValues("atflCd" as FieldPath<T>);
const { userInfo } = useUserStore((state) => state);
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
@@ -125,7 +125,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<FormControl>
<Input
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="!text-[1.3rem]"
placeholder="제목을 입력해주세요"
/>
@@ -146,7 +146,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<FormControl>
<Input
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="작성자를 입력해주세요"
/>
@@ -169,7 +169,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<Input
{...field}
type="password"
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="w-[20rem] !text-[1.3rem]"
placeholder="비밀번호를 입력해주세요"
/>
@@ -191,7 +191,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
onChange={field.onChange}
className="h-[20rem] w-full !text-[1.3rem]"
placeholder="내용을 입력해주세요"
@@ -214,7 +214,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<FormControl>
<Input
{...field}
value={field.value ?? ""}
value={(field.value as string) ?? ""}
className="!text-[1.3rem]"
placeholder="URL을 입력해주세요"
/>
@@ -229,13 +229,13 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<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">
<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() ?? ""}
@@ -250,12 +250,48 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<Label htmlFor="popupN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"popupYnEtc" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<PeriodSelector
periodCode={
form.getValues().popupYnEtc?.[0]?.pstgPrdSeCd ??
null
}
disabled={
form.getValues("popupYn" as FieldPath<T>) ===
"N"
}
handlePeriodValueChange={(periodCode) => {
field.onChange([
{
...(form.getValues().popupYnEtc?.[0] ?? {}),
bbsPstgTypeCd: BOARD_POSTING_CODE.팝업게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{
...(form.getValues().popupYnEtc?.[0] ?? {}),
...date,
},
]);
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</TableCell>
</TableRow>
)}
@@ -263,13 +299,13 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<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">
<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() ?? ""}
@@ -284,12 +320,50 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<Label htmlFor="upndFixN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"upndFixYnEtc" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<PeriodSelector
periodCode={
form.getValues().upndFixYnEtc?.[0]
?.pstgPrdSeCd ?? null
}
disabled={
form.getValues("upndFixYn" as FieldPath<T>) ===
"N"
}
handlePeriodValueChange={(periodCode) => {
field.onChange([
{
...(form.getValues().upndFixYnEtc?.[0] ??
{}),
bbsPstgTypeCd: BOARD_POSTING_CODE.상단게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{
...(form.getValues().upndFixYnEtc?.[0] ??
{}),
...date,
},
]);
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</TableCell>
</TableRow>
)}
@@ -297,13 +371,13 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<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">
<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() ?? ""}
@@ -318,12 +392,50 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
<Label htmlFor="prdPstgN">사용안함</Label>
</div>
</RadioGroup>
<PeriodSelector disabled={field.value === "N"} />
</div>
</FormControl>
</FormItem>
)}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={"prdPstgYnEtc" as FieldPath<T>}
render={({ field }) => (
<FormItem>
<FormControl>
<PeriodSelector
periodCode={
form.getValues().prdPstgYnEtc?.[0]
?.pstgPrdSeCd ?? null
}
disabled={
form.getValues("prdPstgYn" as FieldPath<T>) ===
"N"
}
handlePeriodValueChange={(periodCode) => {
field.onChange([
{
...(form.getValues().prdPstgYnEtc?.[0] ??
{}),
bbsPstgTypeCd: BOARD_POSTING_CODE.기간게시,
pstgPrdSeCd: periodCode,
},
]);
}}
handlePostingDateChange={(date) => {
field.onChange([
{
...(form.getValues().prdPstgYnEtc?.[0] ??
{}),
...date,
},
]);
}}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</TableCell>
</TableRow>
)}
@@ -382,10 +494,36 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
);
}
const PeriodSelector = ({ disabled }: { disabled: boolean }) => {
const [periodCode, setPeriodCode] = useState<number | null>(null);
const handlePeriodCodeChange = (value: string) => {
setPeriodCode(parseInt(value));
const PeriodSelector = ({
periodCode,
disabled,
handlePeriodValueChange,
handlePostingDateChange,
}: {
periodCode: string | null;
disabled: boolean;
handlePeriodValueChange: (periodCode: string) => void;
handlePostingDateChange: (date?: {
pstgBgngDt: string | undefined;
pstgEndDt: string | undefined;
}) => void;
}) => {
const handlePeriodCodeChange = (periodCode: string) => {
handlePeriodValueChange(periodCode);
handlePostingDateChange({
pstgBgngDt: undefined,
pstgEndDt: undefined,
});
};
const onDateChange = (date?: {
from: Date | undefined;
to?: Date | undefined;
}) => {
handlePostingDateChange({
pstgBgngDt: dayjs(date?.from).format("YYYY-MM-DD 00:00:00"),
pstgEndDt: dayjs(date?.to).format("YYYY-MM-DD 23:59:59"),
});
};
const postingPeriod = (periodCode: number | null) => {
@@ -400,16 +538,19 @@ const PeriodSelector = ({ disabled }: { disabled: boolean }) => {
<div className="flex items-center gap-4">
<CodeSelect
upCd="PSTGPRDCD"
defaultValue={periodCode ?? ""}
onValueChange={handlePeriodCodeChange}
disabled={disabled}
placeholder="기간 선택"
/>
{!!periodCode && (
<span className="text-lg text-label">
게시 기간: <u>{postingPeriod(periodCode)}</u>
게시 기간: <u>{postingPeriod(parseInt(periodCode))}</u>
</span>
)}
{periodCode === 0 && <DateRangePicker />}
{periodCode === "0" && (
<DateRangePicker handleDateChange={onDateChange} />
)}
</div>
);
};
@@ -0,0 +1,174 @@
"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
import { useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
type PaginationState,
} from "@tanstack/react-table";
import {
Button,
DataTable,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { PostApi, type Post, type PostParams } from "@/entities/post";
import type { Board } from "@/entities/board";
import { ENDPOINT } from "@/shared/config";
import { useMenuStore } from "@/shared/lib";
import { columns } from "../model/post-table-columns";
function PostPage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
const { pageMenuAuthority } = useMenuStore((state) => state);
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
const [postParams, setPostParams] = useState<PostParams>({
keywordType: "title",
keyword: "",
});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const { data: upndFixPostList } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo],
queryFn: () =>
PostApi.getPostList(bbsId, {
...postParams,
upndFixYn: bbsInfo?.upndFixUseYn,
}),
select: (data) => {
const newContent = data.content.map((post) => ({
...post,
isUpndFixPost: post.upndFixYn === "Y",
}));
return {
...data,
content: newContent,
};
},
enabled: bbsInfo?.upndFixUseYn === "Y",
});
const { data: postList, refetch } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId],
queryFn: () =>
PostApi.getPostList(bbsId, {
...postParams,
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: false,
});
const table = useReactTable({
data: [...(upndFixPostList?.content ?? []), ...(postList?.content ?? [])],
columns,
state: { pagination },
rowCount: postList?.totalElements ?? 0,
manualPagination: true,
enableMultiRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
refetch();
};
const handleRowClick = (row: Post) => {
router.push(`${pathname}/${row.pstNo}`);
};
useEffect(() => {
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
}, [storedBbsInfo]);
useEffect(() => {
refetch();
}, [pagination]);
return (
<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.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}개씩 보기
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
</div>
);
}
export { PostPage };
@@ -1,68 +1,63 @@
"use client";
import dayjs from "dayjs";
import type { AxiosResponse } from "axios";
import { useEffect, useState, useTransition } from "react";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button, Skeleton } from "@common/components";
import { PostApi } from "@/entities/post";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { PostAction } from "./post-action";
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const [isPending, startTransition] = useTransition();
const [filename, setFilename] = useState("");
const [downloadResponse, setDownloadResponse] =
useState<AxiosResponse | null>(null);
const { data: postData } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
});
useEffect(() => {
if (!postData?.atflCd) return;
startTransition(async () => {
try {
let filename = "";
const response = await AttachmentApi.downloadAttachmentByAtflCd(
postData.atflCd,
);
setDownloadResponse(response);
const { data: downloadResponse, isFetching } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, postData?.atflCd],
queryFn: () =>
AttachmentApi.downloadAttachmentByAtflCd(postData?.atflCd as string),
enabled: !!postData?.atflCd,
});
const contentDisposition = response.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch && filenameMatch[1]) {
filename = decodeURIComponent(filenameMatch[1]);
}
useEffect(() => {
if (!downloadResponse) return;
try {
let filename = "";
const contentDisposition =
downloadResponse.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch && filenameMatch[1]) {
filename = decodeURIComponent(filenameMatch[1]);
}
setFilename(filename);
} catch {
setFilename("");
}
});
}, [postData?.atflCd]);
setFilename(filename);
} catch {
setFilename("");
}
}, [downloadResponse]);
const handleDownload = async () => {
if (isPending || !downloadResponse) return;
if (!downloadResponse) return;
try {
startTransition(() => {
const blob = new Blob([downloadResponse.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
const blob = new Blob([downloadResponse.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename || "download";
document.body.appendChild(link);
link.click();
link.href = url;
link.download = filename || "download";
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
});
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} catch (error) {
console.error(error);
}
@@ -93,7 +88,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
<hr className="mt-8" />
<div className="flex items-center gap-4 p-8">
<span className="font-bold">첨부파일</span>
{isPending ? (
{isFetching ? (
<Skeleton className="h-[2.5rem] w-[20rem]" />
) : (
filename && (
@@ -102,7 +97,6 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
disabled={isPending}
>
<i className="diveicon di-download" />
<span>{filename}</span>