feat: 상단고정 게시글 로직 작업
This commit is contained in:
+10
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user