feat: 게시글 삭제 사유 셀렉트 기능 추가

This commit is contained in:
2025-06-26 17:26:28 +09:00
parent 9779934f08
commit 78724e9d61
12 changed files with 196 additions and 12 deletions
+3 -1
View File
@@ -7,10 +7,11 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { PromptProvider, StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { PromptDialog } from "@/shared/ui/prompt-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
import { PromptProvider, StoreProvider } from "@/shared/providers";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const queryClient = new QueryClient();
@@ -31,6 +32,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
<AlertDialog />
<PromptDialog />
<ConfirmDialog />
<SelectPromptDialog />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
+1
View File
@@ -13,4 +13,5 @@ export {
type PostUpdateFormData,
type PostService,
} from "./model/post-interface";
export { deleteReasonMessages } from "./model/delete-reason-messages";
export { PostPageGuard } from "./lib/post-page-guard";
@@ -0,0 +1,11 @@
export const deleteReasonMessages = {
"저작권 침해":
"타인의 저작물을 허락 없이 게시하는 경우 저작권 침해로 간주되어 삭제될 수 있습니다.",
명예훼손: "타인의 명예를 훼손하는 내용의 게시물은 삭제될 수 있습니다.",
"개인정보 노출": "타인의 개인정보를 노출하는 게시물도 삭제될 수 있습니다.",
"불법 정보 유통":
"불법적인 상품이나 서비스를 판매하거나 홍보하는 게시물은 삭제될 수 있습니다.",
"욕설 및 비방":
"심한 욕설이나 비방, 모욕적인 내용이 포함된 게시물은 삭제될 수 있습니다.",
"광고성 정보": "지나치게 광고성이 짙은 게시물은 삭제될 수 있습니다.",
};
+13 -3
View File
@@ -21,20 +21,28 @@ import {
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi, type PostParams } from "@/entities/post";
import {
PostApi,
deleteReasonMessages,
type PostParams,
} from "@/entities/post";
import { AuthorityButton } from "@/entities/menu-authority";
import { ENDPOINT } from "@/shared/config";
import {
useAlertStore,
useMenuStore,
usePrompt,
useSelectPrompt,
useMutationWrapper,
} from "@/shared/lib";
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function FaqPage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const alert = useAlertStore((state) => state.setMessage);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
@@ -75,11 +83,13 @@ function FaqPage({ bbsId }: { bbsId: string }) {
const handleDeletePost = async (pstNo: number) => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
if (!delRsn) return;
deletePost({ pstNo, delRsn });
};
const onSelectedPostDelete = async () => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
if (!delRsn) return;
await Promise.all(
selectedPostNumbers.map((pstNo) =>
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
+7 -2
View File
@@ -23,13 +23,14 @@ import {
} from "@common/components";
import {
PostApi,
deleteReasonMessages,
type DeletePost,
type RecentPost,
type RecentPostParams,
} from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { AuthorityButton } from "@/entities/menu-authority";
import { useAlertStore, usePrompt, useDataTable } from "@/shared/lib";
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "../model/table-columns";
import { MovePostDialog } from "./move-post-dialog";
@@ -39,9 +40,13 @@ type DateRange = {
to?: Date | undefined;
};
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function RecentPage() {
const router = useRouter();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [dateRange, setDateRange] = useState<DateRange>();
+1
View File
@@ -5,3 +5,4 @@ export { useDataTable } from "./use-data-table";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export { usePrompt } from "./use-prompt";
export { useSelectPrompt } from "./use-select-prompt";
@@ -0,0 +1,48 @@
import { useContext, useEffect } from "react";
import { PromptContext } from "../providers/prompt-provider";
type SelectData = {
label: string;
value: string;
};
export const useSelectPrompt = (data?: SelectData[]) => {
const {
selectData,
selectionGuide,
resolve,
setResolve,
setSelectData,
setSelectionGuide,
} = useContext(PromptContext);
const prompt = (message: string) => {
setSelectionGuide(message);
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
setResolve(() => resolve);
});
};
const onSubmit = (value: string) => {
resolve?.({ resolved: true, value });
setSelectionGuide("");
};
const onCancel = () => {
resolve?.({ resolved: false });
setSelectionGuide("");
};
useEffect(() => {
if (data) setSelectData(data);
}, [data]);
return {
selectData,
selectionGuide,
isOpen: !!selectionGuide,
prompt,
onSubmit,
onCancel,
};
};
@@ -1,7 +1,14 @@
import { createContext, useState } from "react";
type SelectData = {
label: string;
value: string;
};
interface PromptContext {
message?: string;
selectData?: SelectData[];
selectionGuide?: string;
resolve?: ({
resolved,
value,
@@ -10,18 +17,26 @@ interface PromptContext {
value?: string;
}) => void;
setMessage: (message: string) => void;
setSelectData: (selectData: SelectData[]) => void;
setSelectionGuide: (message: string) => void;
setResolve: (resolve: () => void) => void;
}
export const PromptContext = createContext<PromptContext>({
message: "",
selectData: [],
selectionGuide: "",
resolve() {},
setMessage() {},
setSelectData() {},
setSelectionGuide() {},
setResolve() {},
});
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
const [message, setMessage] = useState<string>();
const [selectData, setSelectData] = useState<SelectData[]>([]);
const [selectionGuide, setSelectionGuide] = useState<string>();
const [resolve, setResolve] =
useState<
({ resolved, value }: { resolved: boolean; value?: string }) => void
@@ -29,7 +44,16 @@ export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
return (
<PromptContext.Provider
value={{ message, resolve, setMessage, setResolve }}
value={{
message,
selectData,
selectionGuide,
resolve,
setMessage,
setSelectData,
setSelectionGuide,
setResolve,
}}
>
{children}
</PromptContext.Provider>
+5 -1
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import {
Button,
Dialog,
@@ -13,6 +13,10 @@ function PromptDialog() {
const { message, isOpen, onSubmit, onCancel } = usePrompt();
const [value, setValue] = useState("");
useEffect(() => {
if (isOpen) setValue("");
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
@@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogClose,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Textarea,
} from "@common/components";
import { useSelectPrompt } from "../lib/use-select-prompt";
function SelectPromptDialog() {
const { selectData, selectionGuide, isOpen, onSubmit, onCancel } =
useSelectPrompt();
const [value, setValue] = useState("");
useEffect(() => {
if (isOpen) setValue("");
}, [isOpen]);
return (
<Dialog open={isOpen} onOpenChange={onCancel}>
<DialogContent aria-describedby={undefined}>
<div className="flex flex-col gap-6 pb-4 pt-8">
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
{selectionGuide}
</p>
<div className="flex flex-col gap-2">
<Select onValueChange={setValue}>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{selectData?.map((data) => (
<SelectItem key={data.value} value={data.value}>
{data.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Textarea
value={value}
className="!text-[1.3rem] !leading-6"
onChange={(e) => setValue(e.target.value)}
/>
</div>
</div>
<DialogFooter className="!justify-center">
<DialogClose asChild>
<Button onClick={() => onSubmit(value)}>확인</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onCancel} color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { SelectPromptDialog };
+7 -2
View File
@@ -1,6 +1,7 @@
import { useRouter, usePathname } from "next/navigation";
import { Button } from "@common/components";
import { useMenuStore, usePrompt, useUserStore } from "@/shared/lib";
import { deleteReasonMessages } from "@/entities/post";
import { useMenuStore, useSelectPrompt, useUserStore } from "@/shared/lib";
type PostActionProps = {
bbsId: string;
@@ -9,10 +10,14 @@ type PostActionProps = {
handleDelete: (delRsn?: string) => Promise<void>;
};
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function PostAction({ children, handleDelete }: PostActionProps) {
const router = useRouter();
const pathname = usePathname();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const userInfo = useUserStore((state) => state.userInfo);
+7 -2
View File
@@ -25,19 +25,24 @@ import { AuthorityButton } from "@/entities/menu-authority";
import {
PostApi,
PostPageGuard,
deleteReasonMessages,
type Post,
type PostParams,
} from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { useMenuStore, usePrompt } from "@/shared/lib";
import { useMenuStore, useSelectPrompt } from "@/shared/lib";
import { GridViewDataTable } from "@/shared/ui/grid-view-data-table";
import { columns as tableColumns } from "../model/post-table-columns";
import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns";
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function PostPage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
const thumbRefs = useRef<Map<number, string>>(new Map());