feat: 최근 게시물 관리 작업
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
import { RecentPage } from "@/pages/recent";
|
||||
export default RecentPage;
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
} from "../model/board-interface";
|
||||
|
||||
export class BoardApiService implements BoardService {
|
||||
public async getBoardList(params: BoardParams): Promise<BoardList> {
|
||||
public async getBoardList(params?: BoardParams): Promise<BoardList> {
|
||||
const response = await client.get<BoardList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}`,
|
||||
{ params },
|
||||
|
||||
@@ -4,12 +4,16 @@ import {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type MovePost,
|
||||
type DeletePost,
|
||||
type PostInfo,
|
||||
type PostList,
|
||||
type RecentPostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type RecentPostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
} from "../model/post-interface";
|
||||
|
||||
export class PostApiService implements PostService {
|
||||
@@ -24,6 +28,16 @@ export class PostApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getRecentPostList(
|
||||
params?: RecentPostParams,
|
||||
): Promise<RecentPostList> {
|
||||
const response = await client.get<RecentPostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
|
||||
@@ -62,6 +76,22 @@ export class PostApiService implements PostService {
|
||||
params: { delRsn },
|
||||
});
|
||||
}
|
||||
|
||||
public async softDeletePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data);
|
||||
}
|
||||
|
||||
public async hardDeletePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data);
|
||||
}
|
||||
|
||||
public async movePost(data: MovePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data);
|
||||
}
|
||||
|
||||
public async restorePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const PostApi = new PostApiService();
|
||||
|
||||
@@ -3,9 +3,13 @@ export {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type RecentPost,
|
||||
type MovePost,
|
||||
type DeletePost,
|
||||
type PostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type RecentPostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
} from "./model/post-interface";
|
||||
|
||||
@@ -11,6 +11,15 @@ export interface PostParams extends PageableParams {
|
||||
bbsTypeCd?: string;
|
||||
}
|
||||
|
||||
export interface RecentPostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number | null;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
@@ -33,6 +42,11 @@ export interface Post {
|
||||
cmntCnt: number;
|
||||
}
|
||||
|
||||
export interface RecentPost extends Post {
|
||||
bbsNm: string;
|
||||
bbsTypeCd: string;
|
||||
}
|
||||
|
||||
export interface PostInfo extends Post {
|
||||
pstCn: string;
|
||||
rgtr: string;
|
||||
@@ -40,6 +54,18 @@ export interface PostInfo extends Post {
|
||||
mdfr: string;
|
||||
}
|
||||
|
||||
export interface MovePost {
|
||||
fromBbsId: number;
|
||||
toBbsId?: number;
|
||||
pstNo: number;
|
||||
}
|
||||
|
||||
export interface DeletePost {
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
delRsn?: string;
|
||||
}
|
||||
|
||||
const postEtcSchema = z.object({
|
||||
bbsPstgTypeCd: z.string(),
|
||||
pstgPrdSeCd: z.string(),
|
||||
@@ -71,11 +97,13 @@ export const postUpdateFormSchema = postFormSchema.omit({
|
||||
});
|
||||
|
||||
export type PostList = PageableList<Post>;
|
||||
export type RecentPostList = PageableList<RecentPost>;
|
||||
export type PostFormData = z.infer<typeof postFormSchema>;
|
||||
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getRecentPostList(params: RecentPostParams): Promise<RecentPostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
@@ -84,4 +112,7 @@ export interface PostService {
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post>;
|
||||
deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise<void>;
|
||||
softDeletePost(data: DeletePost[]): Promise<void>;
|
||||
hardDeletePost(data: DeletePost[]): Promise<void>;
|
||||
movePost(data: MovePost[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
{parseInt(rememberPeriodCode[site.siteId] ?? "") ===
|
||||
0 && (
|
||||
<DateRangePicker
|
||||
handleDateChange={(date) =>
|
||||
onValueChange={(date) =>
|
||||
handlePeriodDateChange(site.siteId)(
|
||||
date as DateRange | undefined,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -23,8 +24,6 @@ import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi, type PostParams } from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
|
||||
function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { RecentPage } from "./ui/recent-page";
|
||||
@@ -0,0 +1,117 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Checkbox } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { RecentPost } from "@/entities/post";
|
||||
|
||||
export const columns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
width: 40,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
meta: {
|
||||
width: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판",
|
||||
meta: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => <div className="text-left">{row.original.pstTtl}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "atflCd",
|
||||
header: "첨부파일",
|
||||
cell: ({ row }) => {
|
||||
const atflCd = row.original.atflCd;
|
||||
if (!atflCd) return null;
|
||||
return <AttachmentButton atflCd={atflCd} />;
|
||||
},
|
||||
meta: {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rgtrNm",
|
||||
header: "작성자",
|
||||
meta: {
|
||||
width: 120,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
meta: {
|
||||
width: 120,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import type { Board } from "@/entities/board";
|
||||
import type { MovePost } from "@/entities/post";
|
||||
|
||||
type MovePostDialogProps = {
|
||||
selectedPosts: MovePost[];
|
||||
boardList: Board[];
|
||||
onMovePost: (data: MovePost[]) => void;
|
||||
};
|
||||
|
||||
function MovePostDialog({
|
||||
selectedPosts,
|
||||
boardList,
|
||||
onMovePost,
|
||||
}: MovePostDialogProps) {
|
||||
const selectedPostsRef = useRef<MovePost[]>(selectedPosts);
|
||||
|
||||
const onSelectValueChange = (toBbsId: string) => {
|
||||
selectedPostsRef.current = selectedPosts.map((post) => ({
|
||||
...post,
|
||||
toBbsId: parseInt(toBbsId),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="emerald" disabled={!selectedPosts.length}>
|
||||
게시물 이동
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<p className="pt-8 text-center text-[1.3rem]">
|
||||
이동할 게시판을 선택해 주세요.
|
||||
</p>
|
||||
<Select onValueChange={onSelectValueChange}>
|
||||
<SelectTrigger className="my-4">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{boardList.map((board) => (
|
||||
<SelectItem key={board.bbsId} value={board.bbsId.toString()}>
|
||||
{board.bbsNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button onClick={() => onMovePost(selectedPostsRef.current)}>
|
||||
이동
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { MovePostDialog };
|
||||
@@ -0,0 +1,342 @@
|
||||
"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, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
DataTable,
|
||||
} from "@common/components";
|
||||
import {
|
||||
PostApi,
|
||||
type DeletePost,
|
||||
type MovePost,
|
||||
type RecentPost,
|
||||
type RecentPostParams,
|
||||
} from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function RecentPage() {
|
||||
const router = useRouter();
|
||||
const { prompt } = usePrompt();
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [recentPostParams, setRecentPostParams] = useState<RecentPostParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
|
||||
const { data: recentPostList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
...recentPostParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: boardList } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () => BoardApi.getBoardList(),
|
||||
});
|
||||
|
||||
const { mutate: movePost } = useMutation({
|
||||
mutationFn: (data: MovePost[]) => PostApi.movePost(data),
|
||||
onSuccess: () => {
|
||||
alert("이동이 완료되었습니다.", refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: recentPostList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: recentPostList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(recentPostParams.delYn === "Y");
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: RecentPost) => {
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleRecentPostParamsChange =
|
||||
(key: keyof RecentPostParams) => (value: unknown) => {
|
||||
setRecentPostParams({ ...recentPostParams, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
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 = undefined;
|
||||
}
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
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("삭제가 완료되었습니다.", 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("삭제가 완료되었습니다.", 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("복원이 완료되었습니다.", 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>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select onValueChange={handleRecentPostParamsChange("bbsId")}>
|
||||
<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 onValueChange={handleRecentPostParamsChange("delYn")}>
|
||||
<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={recentPostParams.keywordType}
|
||||
onValueChange={handleRecentPostParamsChange("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={recentPostParams.keyword}
|
||||
onChange={(e) =>
|
||||
handleRecentPostParamsChange("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?.mngAuthrtYn === "Y" &&
|
||||
(isDeletePostData ? (
|
||||
<>
|
||||
<Button
|
||||
color="emerald"
|
||||
onClick={onRestorePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
게시물 복원
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onHardDeletePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
완전삭제
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MovePostDialog
|
||||
selectedPosts={table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
fromBbsId: row.original.bbsId,
|
||||
pstNo: row.original.pstNo,
|
||||
}))}
|
||||
boardList={boardList?.content ?? []}
|
||||
onMovePost={movePost}
|
||||
/>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSoftDeletePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
선택삭제
|
||||
</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 { RecentPage };
|
||||
@@ -76,9 +76,7 @@ function PostingPeriodSelector({
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{periodCode === "0" && (
|
||||
<DateRangePicker handleDateChange={onDateChange} />
|
||||
)}
|
||||
{periodCode === "0" && <DateRangePicker onValueChange={onDateChange} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import "./button-group.css";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { RadioGroup, RadioGroupItem } from "../radio-group";
|
||||
import { ButtonGroup, ButtonGroupItem } from "../button-group";
|
||||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
const meta: Meta<typeof ButtonGroup> = {
|
||||
title: "Elements/ButtonGroup",
|
||||
component: RadioGroup,
|
||||
component: ButtonGroup,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
@@ -19,49 +19,49 @@ ButtonGroup 컴포넌트는 RadioGroup 컴포넌트를 기반으로 버튼 모
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RadioGroup>;
|
||||
type Story = StoryObj<typeof ButtonGroup>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Rounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const SecondaryRounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup
|
||||
<ButtonGroup
|
||||
defaultValue="comfortable"
|
||||
variant="button"
|
||||
color="secondary"
|
||||
shape="rounded"
|
||||
>
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import "./button-group.css";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
|
||||
const ButtonGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {
|
||||
variant?: "radio" | "button";
|
||||
color?: "primary" | "secondary";
|
||||
shape?: "square" | "rounded";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ variant = "radio", color = "primary", shape = "square", ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
data-shape={shape}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
ButtonGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const ButtonGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ ...props }, ref) => {
|
||||
return <RadioGroupPrimitive.Item ref={ref} {...props} />;
|
||||
});
|
||||
ButtonGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ButtonGroup, ButtonGroupItem };
|
||||
@@ -16,6 +16,7 @@ import type { HTMLAttributes } from "react";
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
width?: number;
|
||||
align?: "left" | "center" | "right";
|
||||
rowSpan?: number;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
@@ -11,20 +11,38 @@ import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
value?: DateRange;
|
||||
defaultValue?: DateRange;
|
||||
disabled?: boolean;
|
||||
handleDateChange?: (date: DateRange | undefined) => void;
|
||||
onValueChange?: (date: DateRange | undefined) => void;
|
||||
};
|
||||
|
||||
export function DateRangePicker({
|
||||
value,
|
||||
defaultValue,
|
||||
disabled,
|
||||
className,
|
||||
handleDateChange,
|
||||
children,
|
||||
onValueChange,
|
||||
}: DateRangePickerProps) {
|
||||
const [date, setDate] = useState<DateRange>();
|
||||
const [dateState, setDateState] = useState<DateRange | undefined>(
|
||||
defaultValue,
|
||||
);
|
||||
|
||||
const isControlled = value !== undefined;
|
||||
const dateValue = isControlled ? value : dateState;
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setDateState(value);
|
||||
}
|
||||
}, [isControlled, value]);
|
||||
|
||||
const handleSelect = (date: DateRange | undefined) => {
|
||||
setDate(date);
|
||||
handleDateChange?.(date);
|
||||
if (!isControlled) {
|
||||
setDateState(date);
|
||||
}
|
||||
onValueChange?.(date);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,19 +54,19 @@ export function DateRangePicker({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent",
|
||||
!date && "text-muted-foreground",
|
||||
!dateValue && "text-muted-foreground",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
{dateValue?.from ? (
|
||||
dateValue.to ? (
|
||||
<>
|
||||
{format(date.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(date.to, "yyyy-MM-dd")}
|
||||
{format(dateValue.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(dateValue.to, "yyyy-MM-dd")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "yyyy-MM-dd")
|
||||
format(dateValue.from, "yyyy-MM-dd")
|
||||
)
|
||||
) : (
|
||||
<span>기간 선택</span>
|
||||
@@ -56,14 +74,17 @@ export function DateRangePicker({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
<div>
|
||||
{children}
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={dateValue?.from}
|
||||
selected={dateValue}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./accordion";
|
||||
export * from "./badge";
|
||||
export * from "./button";
|
||||
export * from "./button-group";
|
||||
export * from "./calendar";
|
||||
export * from "./carousel";
|
||||
export * from "./checkbox";
|
||||
|
||||
Reference in New Issue
Block a user