406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import dayjs from "dayjs";
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
import {
|
|
getCoreRowModel,
|
|
type PaginationState,
|
|
type RowSelectionState,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
Button,
|
|
Input,
|
|
Label,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@common/components";
|
|
import { SortSelect } from "@/widgets/post";
|
|
import {
|
|
PostApi,
|
|
deleteReasonMessages,
|
|
type DeletePost,
|
|
type RecentPost,
|
|
type RecentPostParams,
|
|
} from "@/entities/post";
|
|
import { BoardApi } from "@/entities/board";
|
|
import { AuthorityButton } from "@/entities/menu-authority";
|
|
import { ENDPOINT } from "@/shared/config";
|
|
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
|
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
|
import { columns } from "../model/table-columns";
|
|
import { MovePostDialog } from "./move-post-dialog";
|
|
|
|
type DateRange = {
|
|
from?: Date | undefined;
|
|
to?: Date | undefined;
|
|
};
|
|
|
|
const selectData = Object.entries(deleteReasonMessages).map(
|
|
([label, value]) => ({ label, value }),
|
|
);
|
|
|
|
function RecentPage() {
|
|
const router = useRouter();
|
|
const { prompt } = useSelectPrompt(selectData);
|
|
const { confirm } = useConfirm();
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
|
|
const [tableSort, setTableSort] = useState("regDt,desc");
|
|
const [pagination, setPagination] = useState<PaginationState>({
|
|
pageIndex: 0,
|
|
pageSize: 10,
|
|
});
|
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
|
const [presetDateRange, setPresetDateRange] = useState("");
|
|
const [dateRange, setDateRange] = useState<DateRange>({
|
|
from: undefined,
|
|
to: undefined,
|
|
});
|
|
const [keywordType, setKeywordType] = useState("");
|
|
const [keyword, setKeyword] = useState("");
|
|
const [bbsId, setBbsId] = useState<string | null>(null);
|
|
const [delYn, setDelYn] = useState("N");
|
|
const [searchParams, setSearchParams] = useState<RecentPostParams>({
|
|
keywordType: "",
|
|
keyword: "",
|
|
bbsId: null,
|
|
delYn: "N",
|
|
frDateYmd: "",
|
|
toDateYmd: "",
|
|
sort: "regDt,desc",
|
|
});
|
|
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
|
|
|
const query = useQuery({
|
|
queryKey: [
|
|
ENDPOINT.BOARD_SERVICE.POSTS,
|
|
searchParams,
|
|
pagination,
|
|
tableSort,
|
|
],
|
|
queryFn: () =>
|
|
PostApi.getRecentPostList({
|
|
...searchParams,
|
|
page: pagination.pageIndex,
|
|
size: pagination.pageSize,
|
|
sort: tableSort,
|
|
}),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
|
|
const { data: boardList } = useQuery({
|
|
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
|
queryFn: () => BoardApi.getBoardList(),
|
|
});
|
|
|
|
const handleRowClick = (row: RecentPost) => {
|
|
router.push(
|
|
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
|
);
|
|
};
|
|
|
|
const { tableComponent, tableInstance: table } = useDataTable({
|
|
query,
|
|
tableOptions: {
|
|
columns,
|
|
state: {
|
|
pagination,
|
|
rowSelection,
|
|
},
|
|
manualPagination: true,
|
|
enableMultiRowSelection: true,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
onPaginationChange: setPagination,
|
|
onRowSelectionChange: setRowSelection,
|
|
},
|
|
dataTableOptions: {
|
|
onRowClick: handleRowClick,
|
|
viewPagination: true,
|
|
},
|
|
});
|
|
|
|
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
|
|
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
setIsDeletePostData(delYn === "Y");
|
|
setPagination({
|
|
...pagination,
|
|
pageIndex: 0,
|
|
});
|
|
setSearchParams({
|
|
...searchParams,
|
|
keywordType,
|
|
keyword,
|
|
bbsId: bbsId ? parseInt(bbsId) : null,
|
|
delYn,
|
|
frDateYmd: dateRange?.from
|
|
? dayjs(dateRange.from).format("YYYYMMDD")
|
|
: "",
|
|
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
|
|
});
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setKeywordType("");
|
|
setKeyword("");
|
|
setBbsId(null);
|
|
setDelYn("N");
|
|
setDateRange({
|
|
from: undefined,
|
|
to: undefined,
|
|
});
|
|
setSearchParams({
|
|
...searchParams,
|
|
keywordType: "",
|
|
keyword: "",
|
|
bbsId: null,
|
|
delYn: "N",
|
|
frDateYmd: "",
|
|
toDateYmd: "",
|
|
});
|
|
setIsDeletePostData(false);
|
|
};
|
|
|
|
const handlePresetDateRange = (value: string) => {
|
|
let presetDateRange: DateRange;
|
|
switch (value) {
|
|
case "1W":
|
|
presetDateRange = {
|
|
from: dayjs().subtract(7, "day").toDate(),
|
|
to: dayjs().toDate(),
|
|
};
|
|
break;
|
|
case "1M":
|
|
presetDateRange = {
|
|
from: dayjs().subtract(30, "day").toDate(),
|
|
to: dayjs().toDate(),
|
|
};
|
|
break;
|
|
case "3M":
|
|
presetDateRange = {
|
|
from: dayjs().subtract(90, "day").toDate(),
|
|
to: dayjs().toDate(),
|
|
};
|
|
break;
|
|
default:
|
|
presetDateRange = {
|
|
from: undefined,
|
|
to: undefined,
|
|
};
|
|
}
|
|
setPresetDateRange(value);
|
|
setDateRange(presetDateRange);
|
|
};
|
|
|
|
const handleDateRangeChange = (dateRange: DateRange) => {
|
|
setDateRange(dateRange);
|
|
setPresetDateRange("");
|
|
};
|
|
|
|
const onSoftDeletePost = async () => {
|
|
const { resolved, value: delRsn } =
|
|
await prompt("삭제 사유를 입력해주세요.");
|
|
if (!resolved) return;
|
|
|
|
const selectedPosts: DeletePost[] = table
|
|
.getFilteredSelectedRowModel()
|
|
.rows.map((row) => ({
|
|
bbsId: row.original.bbsId,
|
|
pstNo: row.original.pstNo,
|
|
delRsn,
|
|
}));
|
|
PostApi.softDeletePost(selectedPosts);
|
|
alert("삭제가 완료되었습니다.", query.refetch);
|
|
setRowSelection({});
|
|
};
|
|
|
|
const onHardDeletePost = async () => {
|
|
if (
|
|
await confirm(
|
|
"게시물을 완전 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.",
|
|
)
|
|
) {
|
|
const selectedPosts: DeletePost[] = table
|
|
.getFilteredSelectedRowModel()
|
|
.rows.map((row) => ({
|
|
bbsId: row.original.bbsId,
|
|
pstNo: row.original.pstNo,
|
|
}));
|
|
PostApi.hardDeletePost(selectedPosts);
|
|
alert("삭제가 완료되었습니다.", query.refetch);
|
|
setRowSelection({});
|
|
}
|
|
};
|
|
|
|
const onRestorePost = async () => {
|
|
if (await confirm("게시물을 복원하시겠습니까?")) {
|
|
const selectedPosts: DeletePost[] = table
|
|
.getFilteredSelectedRowModel()
|
|
.rows.map((row) => ({
|
|
bbsId: row.original.bbsId,
|
|
pstNo: row.original.pstNo,
|
|
}));
|
|
PostApi.restorePost(selectedPosts);
|
|
alert("복원이 완료되었습니다.", query.refetch);
|
|
setRowSelection({});
|
|
}
|
|
};
|
|
|
|
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">날짜</Label>
|
|
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
|
|
<SelectTrigger className="w-[12rem]">
|
|
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1W">일주일</SelectItem>
|
|
<SelectItem value="1M">1개월</SelectItem>
|
|
<SelectItem value="3M">3개월</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<InputDateRangePicker
|
|
dateRange={dateRange}
|
|
setDateRange={handleDateRangeChange}
|
|
/>
|
|
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
|
<Select
|
|
value={bbsId?.toString() || ""}
|
|
onValueChange={(value) => setBbsId(value ? value : null)}
|
|
>
|
|
<SelectTrigger className="w-[12rem]">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{boardList?.content.map((board) => (
|
|
<SelectItem key={board.bbsId} value={board.bbsId.toString()}>
|
|
{board.bbsNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Label className="ml-4 mr-2 text-label">상태</Label>
|
|
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
|
<SelectTrigger className="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>
|
|
<Select
|
|
value={keywordType}
|
|
onValueChange={(value) => setKeywordType(value)}
|
|
>
|
|
<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={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
|
/>
|
|
<Button className="h-[3.2rem]" type="submit">
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
className="h-[3.2rem]"
|
|
onClick={handleReset}
|
|
>
|
|
초기화
|
|
</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">
|
|
{isDeletePostData ? (
|
|
<>
|
|
<AuthorityButton
|
|
authority="mngAuthrtYn"
|
|
onClick={onRestorePost}
|
|
disabled={!isSelectedRow}
|
|
>
|
|
게시물 복원
|
|
</AuthorityButton>
|
|
<AuthorityButton
|
|
authority="delAuthrtYn"
|
|
color="red"
|
|
onClick={onHardDeletePost}
|
|
disabled={!isSelectedRow}
|
|
>
|
|
완전삭제
|
|
</AuthorityButton>
|
|
</>
|
|
) : (
|
|
<>
|
|
<MovePostDialog
|
|
selectedPosts={table
|
|
.getFilteredSelectedRowModel()
|
|
.rows.map((row) => ({
|
|
fromBbsId: row.original.bbsId,
|
|
pstNo: row.original.pstNo,
|
|
}))}
|
|
boardList={boardList?.content ?? []}
|
|
resetSelectedPost={() => table.setRowSelection({})}
|
|
/>
|
|
<AuthorityButton
|
|
authority="delAuthrtYn"
|
|
color="red"
|
|
onClick={onSoftDeletePost}
|
|
disabled={!isSelectedRow}
|
|
>
|
|
선택삭제
|
|
</AuthorityButton>
|
|
</>
|
|
)}
|
|
<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>
|
|
<SortSelect value={tableSort} onValueChange={setTableSort} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="card !m-[1.2rem] !p-0">{tableComponent}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export { RecentPage };
|