diff --git a/apps/common/features/app/(with-base-layout)/layout.tsx b/apps/common/features/app/(with-base-layout)/layout.tsx index bb1b7c0ba..970ccce5e 100644 --- a/apps/common/features/app/(with-base-layout)/layout.tsx +++ b/apps/common/features/app/(with-base-layout)/layout.tsx @@ -15,7 +15,7 @@ export default function WithBaseLayout({ const pathname = usePathname(); const queryClient = useQueryClient(); - const { setCurrentMenuInfo } = useMenuStore((state) => state); + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); const handleMenuClick = (item: MenuItem) => { setCurrentMenuInfo(item); diff --git a/apps/common/features/app/(with-base-layout)/pst/layout.tsx b/apps/common/features/app/(with-base-layout)/pst/layout.tsx index e0333f2d2..3f700d076 100644 --- a/apps/common/features/app/(with-base-layout)/pst/layout.tsx +++ b/apps/common/features/app/(with-base-layout)/pst/layout.tsx @@ -8,7 +8,7 @@ export default function PostLayout({ }: { children: React.ReactNode; }) { - const { bbsInfo } = useMenuStore((state) => state); + const bbsInfo = useMenuStore((state) => state.bbsInfo); return ( ) => { const queryClient = new QueryClient(); - - initializeNextClientConfig(); - return ( diff --git a/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx index d0b27b52c..b85c182b5 100644 --- a/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx +++ b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -33,7 +33,7 @@ function ManagementAuthorityDialog({ ...props }: ManagementAuthorityDialogProps) { const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { data: menuAuthorityList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], diff --git a/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx index 277cae8d3..2855e5ef0 100644 --- a/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx +++ b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx @@ -2,7 +2,6 @@ import { useEffect } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; import { SITE_ID } from "@/shared/config"; @@ -13,15 +12,9 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); const pathname = usePathname(); - const { setMessage: alert } = useAlertStore((state) => state); - const { - pageMenuAuthority, - setBbsInfo, - setCurrentMenuInfo, - setPageMenuAuthority, - } = useMenuStore((state) => state); - - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); + const alert = useAlertStore((state) => state.setMessage); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); const getCurrentMenuInfo = async () => { const menuRoles = await MenuApi.getMenuRoles(SITE_ID); @@ -60,11 +53,6 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); }, [pageMenuAuthority]); - useEffect(() => { - if (!storedBbsInfo) return; - setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - return <>{children}; }; diff --git a/apps/common/features/src/entities/post/index.ts b/apps/common/features/src/entities/post/index.ts index d2b94ba84..cdc99b102 100644 --- a/apps/common/features/src/entities/post/index.ts +++ b/apps/common/features/src/entities/post/index.ts @@ -9,3 +9,4 @@ export { type PostFormData, type PostUpdateFormData, } from "./model/post-interface"; +export { PostPageGuard } from "./lib/post-page-guard"; diff --git a/apps/common/features/src/entities/post/lib/post-page-guard.tsx b/apps/common/features/src/entities/post/lib/post-page-guard.tsx new file mode 100644 index 000000000..c72f1be85 --- /dev/null +++ b/apps/common/features/src/entities/post/lib/post-page-guard.tsx @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { usePathname, useRouter } from "next/navigation"; +import { BoardApi } from "@/entities/board"; +import { useAlertStore, useMenuStore } from "@/shared/lib"; + +function PostPageGuard({ + bbsId, + children, +}: { + bbsId: string; + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const alert = useAlertStore((state) => state.setMessage); + const setBbsInfo = useMenuStore((state) => state.setBbsInfo); + + const { data: boardInfo, isLoading } = useQuery({ + queryKey: [bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + + useEffect(() => { + if (!boardInfo) return; + setBbsInfo(boardInfo); + }, [boardInfo]); + + if (!isLoading && pathname && boardInfo) { + if (!pathname.includes(boardInfo.bbsTypeCd.toLowerCase())) { + alert("잘못된 경로로 접속하였습니다.", router.back); + return null; + } + } + + return children; +} + +export { PostPageGuard }; diff --git a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx index 3cf0eaeeb..6bc8a02a7 100644 --- a/apps/common/features/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx @@ -61,7 +61,7 @@ const POSTING_TYPE_LABEL = { function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [selectedRows, setSelectedRows] = useState([]); const [rememberPeriodCode, setRememberPeriodCode] = useState< Record @@ -289,7 +289,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { {parseInt(rememberPeriodCode[site.siteId] ?? "") === 0 && ( + onValueChange={(date) => handlePeriodDateChange(site.siteId)( date as DateRange | undefined, ) diff --git a/apps/common/features/src/entities/site/@x/posting.ts b/apps/common/features/src/entities/site/@x/posting.ts index 6470b3cb2..355f41e4d 100644 --- a/apps/common/features/src/entities/site/@x/posting.ts +++ b/apps/common/features/src/entities/site/@x/posting.ts @@ -1,2 +1,3 @@ export { SiteApi } from "../api/site-service"; export type { Site } from "../model/site-interface"; +export { SiteSelect } from "../ui/site-select"; \ No newline at end of file diff --git a/apps/common/features/src/entities/site/index.ts b/apps/common/features/src/entities/site/index.ts index c7b437a64..998c13625 100644 --- a/apps/common/features/src/entities/site/index.ts +++ b/apps/common/features/src/entities/site/index.ts @@ -5,3 +5,5 @@ export { type Site, type SiteFormData, } from "./model/site-interface"; + +export { SiteSelect } from "./ui/site-select"; \ No newline at end of file diff --git a/apps/common/features/src/entities/site/ui/site-select.tsx b/apps/common/features/src/entities/site/ui/site-select.tsx new file mode 100644 index 000000000..b7500d0e7 --- /dev/null +++ b/apps/common/features/src/entities/site/ui/site-select.tsx @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { SiteApi } from "../api/site-service"; + +type SiteSelectProps = { + siteId: string; + placeholder?: string; +} & React.ComponentPropsWithoutRef; + +function SiteSelect({ + siteId, + defaultValue, + placeholder = "선택", + ...props +}: SiteSelectProps) { + const { data: sites } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.getSiteList(), + enabled: !!siteId, + }); + + const defaultValueText = + sites?.content.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder; + + return ( + + ); +} + +export { SiteSelect }; diff --git a/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx index 8b564cad4..2d436c3c8 100644 --- a/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx +++ b/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx @@ -10,8 +10,8 @@ import { 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 userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { mutateAsync: createPost } = useMutation({ mutationFn: (data: PostFormData) => PostApi.createPost(data), diff --git a/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx index a5d7129d0..aa67a17c5 100644 --- a/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx +++ b/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx @@ -31,8 +31,8 @@ function FaqPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); const { confirm } = useConfirm(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); const [postParams, setPostParams] = useState({ diff --git a/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx index 7f56031d9..6987cb0bc 100644 --- a/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx @@ -15,8 +15,8 @@ import { useAlertStore, useUserStore } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], diff --git a/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx index 0efcd2c30..dc5e4f894 100644 --- a/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx +++ b/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx @@ -10,8 +10,8 @@ import { 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 userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { mutateAsync: createPost } = useMutation({ mutationFn: (data: PostFormData) => PostApi.createPost(data), diff --git a/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx index 8b09be8c3..d292fea42 100644 --- a/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx @@ -15,8 +15,8 @@ import { useAlertStore, useUserStore } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], diff --git a/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx index f7e2e837f..b5e9af8a8 100644 --- a/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx +++ b/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx @@ -10,8 +10,8 @@ import { 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 userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { mutateAsync: createPost } = useMutation({ mutationFn: (data: PostFormData) => PostApi.createPost(data), diff --git a/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx index 6bbdd8d94..f9fc11f3d 100644 --- a/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx @@ -15,8 +15,8 @@ import { useAlertStore, useUserStore } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], diff --git a/apps/common/features/src/pages/(pst)/normal/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/normal/ui/create-page.tsx index c5cbf7923..df2ab2a70 100644 --- a/apps/common/features/src/pages/(pst)/normal/ui/create-page.tsx +++ b/apps/common/features/src/pages/(pst)/normal/ui/create-page.tsx @@ -12,8 +12,8 @@ function CreatePage({ bbsId, pstNo }: { bbsId: string; pstNo?: string }) { const answerRegisterMode = !!pstNo; const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { mutateAsync: createPost } = useMutation({ mutationFn: (data: PostFormData) => PostApi.createPost(data), diff --git a/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx index e0d1834d4..c47b1999c 100644 --- a/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx @@ -15,8 +15,8 @@ import { useAlertStore, useUserStore } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], diff --git a/apps/common/features/src/pages/(pst)/notice/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/notice/ui/create-page.tsx index 37c21cb0d..5033bff60 100644 --- a/apps/common/features/src/pages/(pst)/notice/ui/create-page.tsx +++ b/apps/common/features/src/pages/(pst)/notice/ui/create-page.tsx @@ -10,8 +10,8 @@ import { 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 userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const { mutateAsync: createPost } = useMutation({ mutationFn: (data: PostFormData) => PostApi.createPost(data), diff --git a/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx index 9fe65782d..7a46a7d6b 100644 --- a/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx @@ -15,8 +15,8 @@ import { useAlertStore, useUserStore } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], diff --git a/apps/common/features/src/shared/ui/alert-dialog.tsx b/apps/common/features/src/shared/ui/alert-dialog.tsx index 012d4ab86..3bd4fece8 100644 --- a/apps/common/features/src/shared/ui/alert-dialog.tsx +++ b/apps/common/features/src/shared/ui/alert-dialog.tsx @@ -6,7 +6,6 @@ import { Button, } from "@common/components"; import { useAlertStore } from "../lib/use-alert-store"; - function AlertDialog() { const { message, callback, setMessage } = useAlertStore((state) => state); const handleOpenChange = () => { diff --git a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx index 99088aa3e..2012201da 100644 --- a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx @@ -4,10 +4,9 @@ import type { HttpResponse, Upload } from "tus-js-client"; import { Button, FileUploader } from "@common/components"; import { handleTusUpload } from "@/entities/tus"; import { type Attachment } from "@/entities/attachment"; -import type { PostFormData, PostUpdateFormData } from "@/entities/post"; import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib"; -function GeneralFileUploader({ +function GeneralFileUploader({ form, attachmentData, deleteAttachmentIds, @@ -18,8 +17,8 @@ function GeneralFileUploader({ deleteAttachmentIds: string[]; setDeleteAttachmentIds: (deleteAttachmentIds: string[]) => void; }) { - const { setMessage: alert } = useAlertStore((state) => state); - const { bbsInfo } = useMenuStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const bbsInfo = useMenuStore((state) => state.bbsInfo); const [attachments, setAttachments] = useState>( {}, @@ -89,10 +88,10 @@ function GeneralFileUploader({ +
{Object.entries(attachments).map(([atflId, attachment]) => { if (deleteAttachmentIds.includes(atflId)) return null; return ( diff --git a/apps/common/features/src/widgets/post/ui/post-action.tsx b/apps/common/features/src/widgets/post/ui/post-action.tsx index 0169e313c..4eaa960cb 100644 --- a/apps/common/features/src/widgets/post/ui/post-action.tsx +++ b/apps/common/features/src/widgets/post/ui/post-action.tsx @@ -14,8 +14,8 @@ function PostAction({ children, handleDelete }: PostActionProps) { const router = useRouter(); const pathname = usePathname(); const { confirm } = useConfirm(); - const { bbsInfo } = useMenuStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const bbsInfo = useMenuStore((state) => state.bbsInfo); + const userInfo = useUserStore((state) => state.userInfo); const onDelete = async () => { if (await confirm("삭제하시겠습니까?")) handleDelete(); diff --git a/apps/common/features/src/widgets/post/ui/post-comment.tsx b/apps/common/features/src/widgets/post/ui/post-comment.tsx index 1800b2581..df05afb9b 100644 --- a/apps/common/features/src/widgets/post/ui/post-comment.tsx +++ b/apps/common/features/src/widgets/post/ui/post-comment.tsx @@ -19,8 +19,8 @@ function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) { const { prompt } = usePrompt(); const { confirm } = useConfirm(); const queryClient = useQueryClient(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); const [editingCmntNo, setEditingCmntNo] = useState(null); const [editValue, setEditValue] = useState(""); diff --git a/apps/common/features/src/widgets/post/ui/post-form.tsx b/apps/common/features/src/widgets/post/ui/post-form.tsx index 928aef922..ae2a84856 100644 --- a/apps/common/features/src/widgets/post/ui/post-form.tsx +++ b/apps/common/features/src/widgets/post/ui/post-form.tsx @@ -40,8 +40,8 @@ function PostForm({ const router = useRouter(); const { confirm } = useConfirm(); - const { bbsInfo } = useMenuStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const bbsInfo = useMenuStore((state) => state.bbsInfo); + const userInfo = useUserStore((state) => state.userInfo); const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); diff --git a/apps/common/features/src/widgets/post/ui/post-page.tsx b/apps/common/features/src/widgets/post/ui/post-page.tsx index c4bf33576..8c4c8c998 100644 --- a/apps/common/features/src/widgets/post/ui/post-page.tsx +++ b/apps/common/features/src/widgets/post/ui/post-page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useQuery } from "@tanstack/react-query"; @@ -22,7 +22,12 @@ import { SelectValue, Skeleton, } from "@common/components"; -import { PostApi, type Post, type PostParams } from "@/entities/post"; +import { + PostApi, + PostPageGuard, + type Post, + type PostParams, +} from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; import { useMenuStore, useUserStore } from "@/shared/lib"; import { GridViewDataTable } from "@/shared/ui/grid-view-data-table"; @@ -33,8 +38,9 @@ function PostPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); const { confirm } = useConfirm(); - const { userInfo } = useUserStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + const thumbRefs = useRef>(new Map()); const [postParams, setPostParams] = useState({ keywordType: "title", @@ -46,8 +52,6 @@ function PostPage({ bbsId }: { bbsId: string }) { }); const [rowSelection, setRowSelection] = useState({}); - const thumbRefs = useRef>(new Map()); - const { data: upndFixPostList } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo], queryFn: () => @@ -70,7 +74,7 @@ function PostPage({ bbsId }: { bbsId: string }) { const { data: postList, - isFetched, + isLoading, refetch, } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId], @@ -80,7 +84,6 @@ function PostPage({ bbsId }: { bbsId: string }) { page: pagination.pageIndex, size: pagination.pageSize, }), - enabled: false, }); const isGridViewPage = @@ -128,94 +131,92 @@ function PostPage({ bbsId }: { bbsId: string }) { router.push(`${pathname}/${row.pstNo}`); }; - useEffect(() => { - refetch(); - }, [pagination]); - return ( -
-
-
- - - - setPostParams({ ...postParams, keyword: e.target.value }) - } - className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" - /> - -
-
-
-
-
-

콘텐츠 목록

-
- {pageMenuAuthority?.regAuthrtYn === "Y" && ( - - )} - {pageMenuAuthority?.delAuthrtYn === "Y" && ( - + +
+
+
+
+

콘텐츠 목록

+
+ {pageMenuAuthority?.regAuthrtYn === "Y" && ( + + )} + {pageMenuAuthority?.delAuthrtYn === "Y" && ( + + )} + { - table.setPageSize(parseInt(value)); - }} - > - - - - - {[10, 20, 30, 40, 50].map((pageSize) => ( - - {pageSize}개씩 보기 - - ))} - - + + + + + {[10, 20, 30, 40, 50].map((pageSize) => ( + + {pageSize}개씩 보기 + + ))} + + +
-
-
- {isFetched ? ( - - ) : ( - - )} +
+ {isLoading ? ( + + ) : ( + + )} +
-
+ ); } diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx index 44e549a70..bd8db7c21 100644 --- a/apps/common/features/src/widgets/post/ui/post.tsx +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -21,9 +21,9 @@ import { PostComment } from "./post-comment"; function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); const { prompt } = usePrompt(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); - const { bbsInfo } = useMenuStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); + const bbsInfo = useMenuStore((state) => state.bbsInfo); const [filename, setFilename] = useState(""); @@ -168,7 +168,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
)} -
+          
             {postData?.pstCn ?? ""}
           
{isFetching ? ( diff --git a/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx b/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx index 1673df8c9..9c23995a6 100644 --- a/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx +++ b/apps/common/features/src/widgets/post/ui/posting-period-selector.tsx @@ -76,9 +76,7 @@ function PostingPeriodSelector({ ) )} - {periodCode === "0" && ( - - )} + {periodCode === "0" && }
); } diff --git a/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx b/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx index 9496280e6..760a3e7ef 100644 --- a/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx +++ b/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx @@ -16,8 +16,8 @@ function WithPreviewFileUploader({ form: UseFormReturn; attachmentData?: Attachment[]; }) { - const { setMessage: alert } = useAlertStore((state) => state); - const { bbsInfo } = useMenuStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const bbsInfo = useMenuStore((state) => state.bbsInfo); const [thumbnailUrl, setThumbnailUrl] = useState(""); const [progress, setProgress] = useState(0); diff --git a/apps/cs/app/(with-base-layout)/code/page.tsx b/apps/cs/app/(with-base-layout)/code/page.tsx new file mode 100644 index 000000000..3ac19ec1c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/code/page.tsx @@ -0,0 +1,2 @@ +import { CodePage } from "@/pages/code"; +export default CodePage; diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(with-base-layout)/layout.tsx index 8af048390..3c1a527d7 100644 --- a/apps/cs/app/(with-base-layout)/layout.tsx +++ b/apps/cs/app/(with-base-layout)/layout.tsx @@ -16,7 +16,7 @@ export default function WithBaseLayout({ const pathname = usePathname(); const queryClient = useQueryClient(); - const { setCurrentMenuInfo } = useMenuStore((state) => state); + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); const handleMenuClick = (item: MenuItem) => { setCurrentMenuInfo(item); diff --git a/apps/cs/app/(with-base-layout)/popup/page.tsx b/apps/cs/app/(with-base-layout)/popup/page.tsx new file mode 100644 index 000000000..684e5bc58 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/page.tsx @@ -0,0 +1,2 @@ +import { PopupPage } from "@/pages/popup"; +export default PopupPage; diff --git a/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx b/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx new file mode 100644 index 000000000..e7388967f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/register/[popupSn]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/popup"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ popupSn: string }>; +}) { + const { popupSn } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/popup/register/page.tsx b/apps/cs/app/(with-base-layout)/popup/register/page.tsx new file mode 100644 index 000000000..d330844d3 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/popup/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/popup"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/role/page.tsx b/apps/cs/app/(with-base-layout)/role/page.tsx new file mode 100644 index 000000000..fa962e93c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/role/page.tsx @@ -0,0 +1,2 @@ +import { RolePage } from "@/pages/role"; +export default RolePage; diff --git a/apps/cs/app/(with-base-layout)/role/register/[roleId]/page.tsx b/apps/cs/app/(with-base-layout)/role/register/[roleId]/page.tsx new file mode 100644 index 000000000..53ba411c8 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/role/register/[roleId]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VMModifyPage } from "@/pages/role"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ roleId: string }>; +}) { + const { roleId } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/role/register/page.tsx b/apps/cs/app/(with-base-layout)/role/register/page.tsx new file mode 100644 index 000000000..d0a8eb9e0 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/role/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/role"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/schedule/page.tsx b/apps/cs/app/(with-base-layout)/schedule/page.tsx new file mode 100644 index 000000000..ec64bf14c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/page.tsx @@ -0,0 +1,2 @@ +import { SchedulePage } from "@/pages/schedule"; +export default SchedulePage; diff --git a/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx b/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx new file mode 100644 index 000000000..90c4eb133 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/register/[schdlSn]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/schedule"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ schdlSn: string }>; +}) { + const { schdlSn } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/schedule/register/page.tsx b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx new file mode 100644 index 000000000..7013caac7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/schedule/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/schedule"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/survey/[srvySn]/modify/page.tsx b/apps/cs/app/(with-base-layout)/survey/[srvySn]/modify/page.tsx new file mode 100644 index 000000000..23c487092 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/survey/[srvySn]/modify/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/survey"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ srvySn: string }>; +}) { + const { srvySn } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/survey/[srvySn]/page.tsx b/apps/cs/app/(with-base-layout)/survey/[srvySn]/page.tsx new file mode 100644 index 000000000..382c62b5a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/survey/[srvySn]/page.tsx @@ -0,0 +1,10 @@ +import { DetailPage as VDetailPage } from "@/pages/survey"; + +export default async function DetailPage({ + params, +}: { + params: Promise<{ srvySn: string }>; +}) { + const { srvySn } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/survey/page.tsx b/apps/cs/app/(with-base-layout)/survey/page.tsx new file mode 100644 index 000000000..85db42486 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/survey/page.tsx @@ -0,0 +1,2 @@ +import { SurveyPage } from "@/pages/survey"; +export default SurveyPage; diff --git a/apps/cs/app/(with-base-layout)/survey/register/page.tsx b/apps/cs/app/(with-base-layout)/survey/register/page.tsx new file mode 100644 index 000000000..3d7307937 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/survey/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/survey"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/users/page.tsx b/apps/cs/app/(with-base-layout)/users/page.tsx new file mode 100644 index 000000000..35a8db5ae --- /dev/null +++ b/apps/cs/app/(with-base-layout)/users/page.tsx @@ -0,0 +1,2 @@ +import { UsersPage } from "@/pages/users"; +export default UsersPage; diff --git a/apps/cs/app/(without-base-layout)/view/popup/[popupSn]/page.tsx b/apps/cs/app/(without-base-layout)/view/popup/[popupSn]/page.tsx new file mode 100644 index 000000000..16cd8e0af --- /dev/null +++ b/apps/cs/app/(without-base-layout)/view/popup/[popupSn]/page.tsx @@ -0,0 +1,10 @@ +import { ViewPopup as VViewPopup } from "@/pages/popup"; + +export default async function ViewPopup({ + params, +}: { + params: Promise<{ popupSn: string }>; +}) { + const { popupSn } = await params; + return ; +} diff --git a/apps/cs/app/(without-base-layout)/view/survey-article/[srvySn]/page.tsx b/apps/cs/app/(without-base-layout)/view/survey-article/[srvySn]/page.tsx new file mode 100644 index 000000000..6155852fb --- /dev/null +++ b/apps/cs/app/(without-base-layout)/view/survey-article/[srvySn]/page.tsx @@ -0,0 +1,10 @@ +import { ViewSurveyArticle } from "@/pages/survey"; + +export default async function ViewSurveyArticlePage({ + params, +}: { + params: Promise<{ srvySn: string }>; +}) { + const { srvySn } = await params; + return ; +} diff --git a/apps/cs/app/(without-base-layout)/view/survey-result/[srvySn]/page.tsx b/apps/cs/app/(without-base-layout)/view/survey-result/[srvySn]/page.tsx new file mode 100644 index 000000000..8ac4b076c --- /dev/null +++ b/apps/cs/app/(without-base-layout)/view/survey-result/[srvySn]/page.tsx @@ -0,0 +1,10 @@ +import { ViewSurveyResult } from "@/pages/survey"; + +export default async function ViewSurveyResultPage({ + params, +}: { + params: Promise<{ srvySn: string }>; +}) { + const { srvySn } = await params; + return ; +} diff --git a/apps/cs/package.json b/apps/cs/package.json index f699334d3..a072c1d82 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -1,6 +1,6 @@ { "name": "cs", - "version": "0.0.0", + "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", @@ -14,18 +14,25 @@ "@common/config": "workspace:*", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", + "@fullcalendar/core": "^6.1.17", + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", + "@fullcalendar/timegrid": "^6.1.17", "@hookform/resolvers": "^4.1.3", "@minoru/react-dnd-treeview": "3.5.1-alpha.1", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.67.1", + "@tanstack/react-query-devtools": "^5.75.5", "@tanstack/react-table": "^8.21.2", "@tinymce/tinymce-react": "^6.1.0", "axios": "^1.8.1", "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", + "lucide-react": "^0.474.0", "next": "15.3.0", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 7732d34ba..1a2189b03 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -3,6 +3,7 @@ import { DndProvider } from "react-dnd"; import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +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"; @@ -10,13 +11,9 @@ 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 { initializeNextClientConfig } from "@common/config"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); - - initializeNextClientConfig(); - return ( @@ -38,6 +35,9 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { +
+ +
); }; diff --git a/apps/cs/src/pages/module/api/authorization-service.ts b/apps/cs/src/entities/authorization/api/authorization-service.ts similarity index 94% rename from apps/cs/src/pages/module/api/authorization-service.ts rename to apps/cs/src/entities/authorization/api/authorization-service.ts index d68c6920a..fd5f042f5 100644 --- a/apps/cs/src/pages/module/api/authorization-service.ts +++ b/apps/cs/src/entities/authorization/api/authorization-service.ts @@ -6,11 +6,11 @@ import type { AuthorizationsList, AuthorizationFormData, WithRoleAuthorizations, -} from "../model/authorizations-interface"; +} from "../model/authorization-interface"; export class AuthorizationsApiService implements AuthorizationsService { public async getAuthorizationsList( - params: AuthorizationsParams, + params?: AuthorizationsParams, ): Promise { const response = await client.get( `${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}`, diff --git a/apps/cs/src/entities/authorization/index.ts b/apps/cs/src/entities/authorization/index.ts new file mode 100644 index 000000000..afb6d2ce4 --- /dev/null +++ b/apps/cs/src/entities/authorization/index.ts @@ -0,0 +1,10 @@ +export { AuthorizationsApi } from "./api/authorization-service"; +export { + authorizationFormSchema, + type Authorizations, + type AuthorizationsParams, + type AuthorizationFormData, + type WithRoleAuthorizations, + type AuthorizationsService, + type AuthorizationsList, +} from "./model/authorization-interface"; diff --git a/apps/cs/src/pages/module/model/authorizations-interface.ts b/apps/cs/src/entities/authorization/model/authorization-interface.ts similarity index 97% rename from apps/cs/src/pages/module/model/authorizations-interface.ts rename to apps/cs/src/entities/authorization/model/authorization-interface.ts index e34d150e0..2230f5226 100644 --- a/apps/cs/src/pages/module/model/authorizations-interface.ts +++ b/apps/cs/src/entities/authorization/model/authorization-interface.ts @@ -47,7 +47,7 @@ export type AuthorizationFormData = z.infer; export interface AuthorizationsService { getAuthorizationsList: ( - params: AuthorizationsParams, + params?: AuthorizationsParams, ) => Promise; getAuthorizationInfo: ( authorizationNo: string, diff --git a/apps/cs/src/entities/board/@x/post.ts b/apps/cs/src/entities/board/@x/post.ts new file mode 100644 index 000000000..9d2b29076 --- /dev/null +++ b/apps/cs/src/entities/board/@x/post.ts @@ -0,0 +1 @@ +export { BoardApi } from "../api/board-service"; diff --git a/apps/cs/src/entities/board/model/board-interface.ts b/apps/cs/src/entities/board/model/board-interface.ts index 5abd002bc..60ed9f743 100644 --- a/apps/cs/src/entities/board/model/board-interface.ts +++ b/apps/cs/src/entities/board/model/board-interface.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { PageableList, PageableParams } from "@/shared/model"; -import type { BbsType } from "@/shared/store/menu-store"; +import type { BbsType } from "@/shared/store"; export const BOARD_POSTING_CODE = { 팝업게시: "01", diff --git a/apps/cs/src/entities/code/api/lwpocode-service.ts b/apps/cs/src/entities/code/api/lwpocode-service.ts new file mode 100644 index 000000000..b8f11fa22 --- /dev/null +++ b/apps/cs/src/entities/code/api/lwpocode-service.ts @@ -0,0 +1,67 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + LwpoCodeFormSchema, + type LwpoCodeService, + type LwpoCodeList, + type LwpoCodeDetail, + type LwpoCodeParams, + type LwpoCodeFormData, +} from "../model/lwpocode-interface"; + +export class LwpoCodeApiService implements LwpoCodeService { + public async getLwpoCodeList(params?: LwpoCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + { params }, + ); + return response.data; + } + + public async getUpCodeLwpoCodeList(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`, + ); + return response.data; + } + + public async lwpoCodeDetail( + upCd: string, + lwpoCd: string, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + ); + return response.data; + } + + public async createLwpoCode(data: LwpoCodeFormData): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise { + const validatedData = LwpoCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + validatedData, + ); + return response.data; + } + + public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`, + ); + } +} + +export const LwpoCodeApi = new LwpoCodeApiService(); diff --git a/apps/cs/src/entities/code/api/upcode-service.ts b/apps/cs/src/entities/code/api/upcode-service.ts new file mode 100644 index 000000000..dcccd9534 --- /dev/null +++ b/apps/cs/src/entities/code/api/upcode-service.ts @@ -0,0 +1,47 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + upCodeFormSchema, + type UpCodeService, + type UpCodeList, + type UpCodeDetail, + type UpCodeParams, + type UpCodeFormData, +} from "../model/upcode-interface"; + +export class UpCodeApiService implements UpCodeService { + public async getUpCodeList(params?: UpCodeParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + { params }, + ); + return response.data; + } + + public async upCodeDetail(upCd: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + ); + return response.data; + } + + public async createUpCode(data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.UP_CODE}`, + validatedData, + ); + return response.data; + } + + public async updateUpCode(upCd: string, data: UpCodeFormData): Promise { + const validatedData = upCodeFormSchema.parse(data); + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`, + validatedData, + ); + return response.data; + } +} + +export const UpCodeApi = new UpCodeApiService(); diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index dc74bbc47..e166e2c29 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -1,3 +1,19 @@ export { CodeApi } from "./api/code-service"; export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; + +export { UpCodeApi } from "./api/upcode-service"; +export { + upCodeFormSchema, + type UpCode, + type UpCodeFormData, + type UpCodeParams, +} from "./model/upcode-interface"; + +export { LwpoCodeApi } from "./api/lwpocode-service"; +export { + LwpoCodeFormSchema, + type LwpoCode, + type LwpoCodeFormData, + type LwpoCodeParams, +} from "./model/lwpocode-interface"; diff --git a/apps/cs/src/entities/code/model/lwpocode-interface.ts b/apps/cs/src/entities/code/model/lwpocode-interface.ts new file mode 100644 index 000000000..785d86e1e --- /dev/null +++ b/apps/cs/src/entities/code/model/lwpocode-interface.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface LwpoCodeParams extends PageableParams { + upCd?: string; + lwpoCd?: string; + lwpoCdNm?: string; + useYn?: string; +} + +export interface LwpoCode { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: number; + regDt: string; + rgTr: string; + rgTrNm: string; + mdfcnDt: string; + mdFr: string; +} + +export interface LwpoCodeDetail extends LwpoCodeFormData { + lwpoCd: string; + upCd: string; + lwpoCdNm: string; + lwpoCdExpln: string; + unit: string; + prntLwpoCd: string; + useYn: string; + sortSeq: 0; +} + +export const LwpoCodeFormSchema = z.object({ + lwpoCd: z.string().max(10).nonempty(), + upCd: z.string().max(10).nonempty(), + lwpoCdNm: z.string().max(100).nonempty(), + lwpoCdExpln: z.string().max(1000).nullish(), + unit: z.string().max(20).nullish(), + prntLwpoCd: z.string().max(10).nullish(), + useYn: z.string(), + sortSeq: z.number(), + delYn: z.string(), +}); + +export type LwpoCodeFormData = z.infer; +export type LwpoCodeList = PageableList; + +export interface LwpoCodeService { + getLwpoCodeList(params: LwpoCodeParams): Promise; + getUpCodeLwpoCodeList(upCd: string): Promise; + lwpoCodeDetail(upCd: string, lwpoCd: string): Promise; + updateLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; + createLwpoCode(data: LwpoCodeFormData): Promise; + deleteLwpoCode( + upCd: string, + lwpoCd: string, + data: LwpoCodeFormData, + ): Promise; +} diff --git a/apps/cs/src/entities/code/model/upcode-interface.ts b/apps/cs/src/entities/code/model/upcode-interface.ts new file mode 100644 index 000000000..73bf4f3ae --- /dev/null +++ b/apps/cs/src/entities/code/model/upcode-interface.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import type { PageableParams, PageableList } from "@/shared/model"; + +export interface UpCodeParams extends PageableParams { + keyword?: string; + upCd?: string; + cdSe?: string; + useYn?: string; + siteId?: string; +} + +export interface UpCode { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + regDt: string; +} + +export interface UpCodeDetail extends UpCodeFormData { + upCd: string; + upCdNm: string; + upCdExpln: string; + cdSe: string; + siteId: string; + useYn: string; + delYn: string; + regDt: string; + rgTrNm: string; +} + +export const upCodeFormSchema = z.object({ + upCd: z.string().max(10).nonempty(), + upCdNm: z.string().max(100).nonempty(), + upCdExpln: z.string().max(1000).nullish(), + cdSe: z.string().max(10).nonempty(), + siteId: z.string().max(2).nullish(), + useYn: z.string(), + delYn: z.string(), +}); + +export type UpCodeFormData = z.infer; +export type UpCodeList = PageableList; + +export interface UpCodeService { + getUpCodeList(params: UpCodeParams): Promise; + upCodeDetail(upCodeId: string): Promise; + updateUpCode(upCodeId: string, data: UpCodeFormData): Promise; + createUpCode(data: UpCodeFormData): Promise; +} diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx index 441f334e3..850282375 100644 --- a/apps/cs/src/entities/code/ui/code-select.tsx +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { + cn, Select, SelectContent, SelectItem, @@ -12,12 +13,14 @@ import { CodeApi } from "../api/code-service"; type CodeSelectProps = { upCd: string; placeholder?: string; + className?: string; } & React.ComponentPropsWithoutRef; function CodeSelect({ upCd, defaultValue, placeholder = "선택", + className, ...props }: CodeSelectProps) { const { data: code } = useQuery({ @@ -31,7 +34,7 @@ function CodeSelect({ return ( + + + + + {sites?.map((item) => ( + + {item.siteNm} + + ))} + + + ); +} + +export { SiteSelect }; diff --git a/apps/cs/src/entities/user/api/user-service.ts b/apps/cs/src/entities/user/api/user-service.ts new file mode 100644 index 000000000..d7718c523 --- /dev/null +++ b/apps/cs/src/entities/user/api/user-service.ts @@ -0,0 +1,213 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + UserService, + UserSearchParams, + UserList, + UserDetailResponse, + UserSttsStatsList, + UserUpdateData, + UserPasswordUpdateData, + UserFindPasswordUpdateData, + UserPasswordMatchData, + UserUpdateInfoData, + UserSaveData, + UserFindPasswordSaveData, + UserVerifyData, + UserEmailData, + UserIdData, +} from "../model/user-interface"; + +export class UserApiService implements UserService { + // 사용자 목록 조회 + public async findPageUser(params: UserSearchParams): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}`, + { params }, + ); + return response.data; + } + + // 사용자 단건 조회 + public async findByUserUnqId(userUnqId: string): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`, + ); + return response.data; + } + + // 로그인한 사용자 정보 조회 + public async findByUserUnqIdByLogin(): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + return response.data; + } + + // 사용자 회원상태 통계 조회 + public async selectUserSttsStats(): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/stts/stats`, + ); + return response.data; + } + + // 사용자 저장 + public async saveUser(data: UserSaveData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}`, + data, + ); + return response.data; + } + + // 사용자 수정 + public async updateUser( + userUnqId: string, + data: UserUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 삭제 + public async deleteUser(userUnqId: string): Promise { + const response = await client.delete( + `${ENDPOINT.USER_SERVICE.USERS}/delete/${userUnqId}`, + ); + return response.data; + } + + // 사용자 계정잠금 처리 + public async lockUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/lock/${userUnqId}`, + ); + return response.data; + } + + // 사용자 계정잠금 해제처리 + public async unlockUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/unlock/${userUnqId}`, + ); + return response.data; + } + + // 리프레시 토큰 정보 재발급 + public async refreshToken(): Promise { + await client.put(`${ENDPOINT.USER_SERVICE.USERS}/token/refresh`); + } + + // 사용자 비밀번호 변경 + public async updatePassword(data: UserPasswordUpdateData): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/update`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 초기화(관리자용) + public async initializePassword( + userUnqId: string, + data: UserPasswordUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/initialize/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 변경 + public async changePassword( + data: UserFindPasswordUpdateData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/password/change`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 확인 + public async matchPassword(data: UserPasswordMatchData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/password/match`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 + public async findPasswordUser( + data: UserFindPasswordSaveData, + ): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/password/find`, + data, + ); + return response.data; + } + + // 사용자 비밀번호 찾기 유효성 확인 + public async validPassword(token: string): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/password/valid/${token}`, + ); + return response.data; + } + + // 사용자 회원정보 변경 + public async updateInfo( + userUnqId: string, + data: UserUpdateInfoData, + ): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/info/${userUnqId}`, + data, + ); + return response.data; + } + + // 사용자 승인처리 + public async applyUser(userUnqId: string): Promise { + const response = await client.put( + `${ENDPOINT.USER_SERVICE.USERS}/apply/${userUnqId}`, + ); + return response.data; + } + + // 사용자 회원탈퇴 + public async leaveUser(data: UserVerifyData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/leave`, + data, + ); + return response.data; + } + + // 사용자 이메일 중복 체크 + public async existsEmail(data: UserEmailData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/exists`, + data, + ); + return response.data; + } + + // 사용자 아이디 중복 체크 + public async existsUserId(data: UserIdData): Promise { + const response = await client.post( + `${ENDPOINT.USER_SERVICE.USERS}/existsUserId`, + data, + ); + return response.data; + } +} + +export const UserApi = new UserApiService(); diff --git a/apps/cs/src/entities/user/index.ts b/apps/cs/src/entities/user/index.ts new file mode 100644 index 000000000..937942481 --- /dev/null +++ b/apps/cs/src/entities/user/index.ts @@ -0,0 +1,5 @@ +export { UserApi } from "./api/user-service"; +export { + type UserSearchParams, + type UserListResponse, +} from "./model/user-interface"; diff --git a/apps/cs/src/entities/user/model/user-interface.ts b/apps/cs/src/entities/user/model/user-interface.ts new file mode 100644 index 000000000..7860039b4 --- /dev/null +++ b/apps/cs/src/entities/user/model/user-interface.ts @@ -0,0 +1,308 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +// 사용자 검색 파라미터 +export interface UserSearchParams extends PageableParams { + keywordType?: string; + keyword?: string; + userStateCode?: string; + aprvYn?: string; + grpId?: string; + frDateYmd?: string; + toDateYmd?: string; +} + +// 사용자 목록 응답 인터페이스 +export interface UserListResponse { + userId: string; + userName: string; + email: string; + roleId: string; + roleName: string; + userStateCode: string; + userStateCodeName: string; + lastLoginDate: string; + loginFailCount: number; + userUnqId: string; + mphn: string; + telno: string; + aprvYn: string; + regDt: string; + laeSeCd: string; + laeSeNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + instSeCd: string; + instSeNm: string; + aprvYmd: string; +} + +// 사용자 상세 응답 인터페이스 +export interface UserDetailResponse { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; + twoFactorEnabled: boolean; + adminRoleYn: string; + instSeCd: string; + aprvYmd: string; + enoCd: string; + laeSeNm: string; + ogdpSeNm: string; + instSeNm: string; + userStateNm: string; + pswdHntNm: string; + ecoNm: string; +} + +// 사용자 상태 통계 응답 인터페이스 +export interface UserSttsStatsResponse { + sttsCd: string; + sttsNm: string; + cnt: number; +} + +// 사용자 수정 요청 스키마 +export const userUpdateSchema = z.object({ + userId: z.string().optional(), + userName: z.string(), + email: z.string().email().optional(), + password: z + .string() + .regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/) + .optional(), + roleId: z.string().optional(), + userStateCode: z.string(), + pswdHnt: z.string().max(10), + pswdAns: z.string().max(100).optional(), + mphn: z.string().max(11).optional(), + telno: z.string().max(11).optional(), + zip: z.string().max(5).optional(), + addr: z.string().max(200).optional(), + daddr: z.string().max(200).optional(), + laeSeCd: z.string().max(10), + ogdpSeCd: z.string().max(10), + aprvYn: z.string(), + autzrId: z.string().uuid().optional(), + expryYmd: z.string().max(8), + instSeCd: z.string().optional(), + aprvYmd: z.string().max(8).optional(), + enoCd: z.string().optional(), +}); + +// 사용자 비밀번호 변경 스키마 +export const userPasswordUpdateSchema = z.object({ + password: z + .string() + .regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/) + .optional() + .describe("기존 패스워드"), + provider: z.string().optional().describe("공급자(기본값:password)"), + token: z.string().optional(), + newPassword: z + .string() + .regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/) + .describe("신규 패스워드"), + newPasswordConfirm: z + .string() + .regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/) + .describe("신규 패스워드 확인"), +}); + +// 사용자 비밀번호 찾기 변경 스키마 +export const userFindPasswordUpdateSchema = z.object({ + tokenValue: z.string(), + password: z + .string() + .regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/), +}); + +// 사용자 비밀번호 확인 스키마 +export const userPasswordMatchSchema = z.object({ + password: z + .string() + .regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/), +}); + +// 사용자 회원정보 변경 스키마 +export const userUpdateInfoSchema = z.object({ + password: z + .string() + .regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/) + .optional() + .describe("기존 패스워드"), + provider: z.string().optional().describe("공급자(기본값:password)"), + token: z.string().optional(), + userName: z.string(), + email: z.string().email().optional(), +}); + +// 사용자 저장 스키마 +export const userSaveSchema = z.object({ + userId: z.string().describe("사용자 ID"), + userName: z.string().describe("사용자명"), + email: z.string().email().optional().describe("이메일"), + password: z + .string() + .regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/) + .describe("패스워드"), + pswdHnt: z.string().max(10).describe("비밀번호 힌트"), + pswdAns: z.string().max(100).optional().describe("비밀번호 답변"), + mphn: z.string().max(11).optional().describe("휴대폰 번호"), + telno: z.string().max(11).optional().describe("전화번호"), + zip: z.string().max(5).optional().describe("우편번호"), + addr: z.string().max(200).optional().describe("주소"), + daddr: z.string().max(200).optional().describe("상세 주소"), + laeSeCd: z.string().max(10).describe("법적 구분 코드"), + ogdpSeCd: z.string().max(10).describe("조직 구분 코드"), + expryYmd: z.string().max(8).describe("만료 일자(YYYYMMDD)"), + instSeCd: z.string().optional().describe("기관구분코드"), + enoCd: z.string().optional().describe("환경청코드"), +}); + +// 사용자 비밀번호 찾기 저장 스키마 +export const userFindPasswordSaveSchema = z.object({ + userName: z.string(), + emailAddr: z.string().email(), + mainUrl: z.string(), + changePasswordUrl: z.string(), +}); + +// 사용자 인증 요청 스키마 +export const userVerifySchema = z.object({ + password: z + .string() + .regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/) + .optional() + .describe("기존 패스워드"), + provider: z.string().optional().describe("공급자(기본값:password)"), + token: z.string().optional(), +}); + +// 사용자 이메일 요청 스키마 +export const userEmailSchema = z.object({ + userId: z.string().optional(), + email: z.string().email(), +}); + +// 사용자 ID 요청 스키마 +export const userIdSchema = z.object({ + userId: z.string(), +}); + +// Zod 스키마 타입 정의 +export type UserUpdateData = z.infer; +export type UserPasswordUpdateData = z.infer; +export type UserFindPasswordUpdateData = z.infer< + typeof userFindPasswordUpdateSchema +>; +export type UserPasswordMatchData = z.infer; +export type UserUpdateInfoData = z.infer; +export type UserSaveData = z.infer; +export type UserFindPasswordSaveData = z.infer< + typeof userFindPasswordSaveSchema +>; +export type UserVerifyData = z.infer; +export type UserEmailData = z.infer; +export type UserIdData = z.infer; + +// 페이지네이션 타입 정의 +export type UserList = PageableList; +export type UserSttsStatsList = UserSttsStatsResponse[]; + +// 사용자 서비스 인터페이스 +export interface UserService { + // 사용자 목록 조회 + findPageUser: (params: UserSearchParams) => Promise; + + // 사용자 단건 조회 + findByUserUnqId: (userUnqId: string) => Promise; + + // 로그인한 사용자 정보 조회 + findByUserUnqIdByLogin: () => Promise; + + // 사용자 회원상태 통계 조회 + selectUserSttsStats: () => Promise; + + // 사용자 저장 + saveUser: (data: UserSaveData) => Promise; + + // 사용자 수정 + updateUser: (userUnqId: string, data: UserUpdateData) => Promise; + + // 사용자 삭제 + deleteUser: (userUnqId: string) => Promise; + + // 사용자 계정잠금 처리 + lockUser: (userUnqId: string) => Promise; + + // 사용자 계정잠금 해제처리 + unlockUser: (userUnqId: string) => Promise; + + // 리프레시 토큰 정보 재발급 + refreshToken: () => Promise; + + // 사용자 비밀번호 변경 + updatePassword: (data: UserPasswordUpdateData) => Promise; + + // 사용자 비밀번호 초기화(관리자용) + initializePassword: ( + userUnqId: string, + data: UserPasswordUpdateData, + ) => Promise; + + // 사용자 비밀번호 찾기 변경 + changePassword: (data: UserFindPasswordUpdateData) => Promise; + + // 사용자 비밀번호 확인 + matchPassword: (data: UserPasswordMatchData) => Promise; + + // 사용자 비밀번호 찾기 + findPasswordUser: (data: UserFindPasswordSaveData) => Promise; + + // 사용자 비밀번호 찾기 유효성 확인 + validPassword: (token: string) => Promise; + + // 사용자 회원정보 변경 + updateInfo: (userUnqId: string, data: UserUpdateInfoData) => Promise; + + // 사용자 승인처리 + applyUser: (userUnqId: string) => Promise; + + // 사용자 회원탈퇴 + leaveUser: (data: UserVerifyData) => Promise; + + // 사용자 이메일 중복 체크 + existsEmail: (data: UserEmailData) => Promise; + + // 사용자 아이디 중복 체크 + existsUserId: (data: UserIdData) => Promise; +} diff --git a/apps/cs/src/pages/(post)/faq/ui/create-page.tsx b/apps/cs/src/pages/(post)/faq/ui/create-page.tsx index 8b564cad4..1adf6f227 100644 --- a/apps/cs/src/pages/(post)/faq/ui/create-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/create-page.tsx @@ -3,17 +3,16 @@ 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 { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function CreatePage({ bbsId }: { bbsId: string }) { const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createPost } = useMutation({ + const { mutateAsync: createPost } = useMutationWrapper({ mutationFn: (data: PostFormData) => PostApi.createPost(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx index 24ffada45..1057fd2b2 100644 --- a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -3,7 +3,7 @@ import dayjs from "dayjs"; import React, { useEffect, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { Accordion, AccordionContent, @@ -22,15 +22,21 @@ import { } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; import { PostApi, type PostParams } from "@/entities/post"; -import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; +import { AuthorityButton } from "@/entities/menu-authority"; import { ENDPOINT } from "@/shared/config"; +import { + useAlertStore, + useMenuStore, + usePrompt, + useMutationWrapper, +} from "@/shared/lib"; function FaqPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); const { prompt } = usePrompt(); - const { setMessage: alert } = useAlertStore((state) => state); - const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const bbsInfo = useMenuStore((state) => state.bbsInfo); const [postParams, setPostParams] = useState({ keywordType: "title", @@ -52,7 +58,7 @@ function FaqPage({ bbsId }: { bbsId: string }) { enabled: !!bbsInfo?.bbsTypeCd, }); - const { mutate: deletePost } = useMutation({ + const { mutate: deletePost } = useMutationWrapper({ mutationFn: ({ pstNo, delRsn }: { pstNo: number; delRsn?: string }) => PostApi.deletePost(parseInt(bbsId), pstNo, delRsn), onSuccess: () => { @@ -140,20 +146,20 @@ function FaqPage({ bbsId }: { bbsId: string }) {

콘텐츠 목록

- {pageMenuAuthority?.regAuthrtYn === "Y" && ( - - )} - {pageMenuAuthority?.delAuthrtYn === "Y" && ( - - )} + router.push(`${pathname}/register`)} + > + 등록 + + + 선택삭제 +
diff --git a/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx b/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx index 1a0ed8006..b6da52232 100644 --- a/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { PostForm } from "@/widgets/post"; import { PostApi, @@ -11,19 +11,19 @@ import { type PostUpdateFormData, } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); - const { mutateAsync: updatePost } = useMutation({ + const { mutateAsync: updatePost } = useMutationWrapper({ mutationFn: (data: PostUpdateFormData) => PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { diff --git a/apps/cs/src/pages/(post)/image/ui/create-page.tsx b/apps/cs/src/pages/(post)/image/ui/create-page.tsx index 0efcd2c30..e986ca058 100644 --- a/apps/cs/src/pages/(post)/image/ui/create-page.tsx +++ b/apps/cs/src/pages/(post)/image/ui/create-page.tsx @@ -3,17 +3,16 @@ 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 { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function CreatePage({ bbsId }: { bbsId: string }) { const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createPost } = useMutation({ + const { mutateAsync: createPost } = useMutationWrapper({ mutationFn: (data: PostFormData) => PostApi.createPost(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/(post)/image/ui/modify-page.tsx b/apps/cs/src/pages/(post)/image/ui/modify-page.tsx index c2426c134..c856cb215 100644 --- a/apps/cs/src/pages/(post)/image/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/image/ui/modify-page.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { PostForm } from "@/widgets/post"; import { PostApi, @@ -11,19 +11,19 @@ import { type PostUpdateFormData, } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); - const { mutateAsync: updatePost } = useMutation({ + const { mutateAsync: updatePost } = useMutationWrapper({ mutationFn: (data: PostUpdateFormData) => PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { diff --git a/apps/cs/src/pages/(post)/movie/ui/create-page.tsx b/apps/cs/src/pages/(post)/movie/ui/create-page.tsx index f7e2e837f..ff20486c5 100644 --- a/apps/cs/src/pages/(post)/movie/ui/create-page.tsx +++ b/apps/cs/src/pages/(post)/movie/ui/create-page.tsx @@ -3,17 +3,16 @@ 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 { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function CreatePage({ bbsId }: { bbsId: string }) { const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createPost } = useMutation({ + const { mutateAsync: createPost } = useMutationWrapper({ mutationFn: (data: PostFormData) => PostApi.createPost(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx b/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx index bee473938..0f4b87701 100644 --- a/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { PostForm } from "@/widgets/post"; import { PostApi, @@ -11,19 +11,19 @@ import { type PostUpdateFormData, } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); - const { mutateAsync: updatePost } = useMutation({ + const { mutateAsync: updatePost } = useMutationWrapper({ mutationFn: (data: PostUpdateFormData) => PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { diff --git a/apps/cs/src/pages/(post)/normal/ui/create-page.tsx b/apps/cs/src/pages/(post)/normal/ui/create-page.tsx index c5cbf7923..5dae6aeab 100644 --- a/apps/cs/src/pages/(post)/normal/ui/create-page.tsx +++ b/apps/cs/src/pages/(post)/normal/ui/create-page.tsx @@ -3,19 +3,18 @@ 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 { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function CreatePage({ bbsId, pstNo }: { bbsId: string; pstNo?: string }) { const answerRegisterMode = !!pstNo; const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createPost } = useMutation({ + const { mutateAsync: createPost } = useMutationWrapper({ mutationFn: (data: PostFormData) => PostApi.createPost(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx b/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx index 2e23f62a0..dc7eb2338 100644 --- a/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { PostForm } from "@/widgets/post"; import { PostApi, @@ -11,19 +11,19 @@ import { type PostUpdateFormData, } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); - const { mutateAsync: updatePost } = useMutation({ + const { mutateAsync: updatePost } = useMutationWrapper({ mutationFn: (data: PostUpdateFormData) => PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { diff --git a/apps/cs/src/pages/(post)/notice/ui/create-page.tsx b/apps/cs/src/pages/(post)/notice/ui/create-page.tsx index 37c21cb0d..eb5a9cb6f 100644 --- a/apps/cs/src/pages/(post)/notice/ui/create-page.tsx +++ b/apps/cs/src/pages/(post)/notice/ui/create-page.tsx @@ -3,17 +3,16 @@ 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 { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function CreatePage({ bbsId }: { bbsId: string }) { const router = useRouter(); - const { userInfo } = useUserStore((state) => state); - const { setMessage: alert } = useAlertStore((state) => state); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createPost } = useMutation({ + const { mutateAsync: createPost } = useMutationWrapper({ mutationFn: (data: PostFormData) => PostApi.createPost(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx b/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx index fa12dbcce..a42190a3e 100644 --- a/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { PostForm } from "@/widgets/post"; import { PostApi, @@ -11,19 +11,19 @@ import { type PostUpdateFormData, } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore, useUserStore } from "@/shared/lib"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { userInfo } = useUserStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); + const userInfo = useUserStore((state) => state.userInfo); const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); - const { mutateAsync: updatePost } = useMutation({ + const { mutateAsync: updatePost } = useMutationWrapper({ mutationFn: (data: PostUpdateFormData) => PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { diff --git a/apps/cs/src/pages/banner/ui/banner-page.tsx b/apps/cs/src/pages/banner/ui/banner-page.tsx index baa372b5f..f2dd98d51 100644 --- a/apps/cs/src/pages/banner/ui/banner-page.tsx +++ b/apps/cs/src/pages/banner/ui/banner-page.tsx @@ -133,7 +133,7 @@ function BannerPage() { -
+
diff --git a/apps/cs/src/pages/banner/ui/create-page.tsx b/apps/cs/src/pages/banner/ui/create-page.tsx index 23d082c1d..803d2b2ec 100644 --- a/apps/cs/src/pages/banner/ui/create-page.tsx +++ b/apps/cs/src/pages/banner/ui/create-page.tsx @@ -3,8 +3,7 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { bannerFormSchema, type BannerFormData, @@ -14,9 +13,9 @@ import { BannerForm } from "./banner-form"; function CreatePage() { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createBanner } = useMutation({ + const { mutateAsync: createBanner } = useMutationWrapper({ mutationFn: (data: BannerFormData) => BannerApi.createBanner(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/banner/ui/modify-page.tsx b/apps/cs/src/pages/banner/ui/modify-page.tsx index 515eb7dca..070db0e46 100644 --- a/apps/cs/src/pages/banner/ui/modify-page.tsx +++ b/apps/cs/src/pages/banner/ui/modify-page.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, @@ -13,7 +13,7 @@ import { import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable } from "@common/components"; import { PostingApi, type PostPayload } from "@/entities/posting"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { bannerFormSchema, @@ -27,7 +27,7 @@ function ModifyPage({ sequential }: { sequential: string }) { const router = useRouter(); const { confirm } = useConfirm(); const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [rowSelection, setRowSelection] = useState({}); @@ -42,7 +42,7 @@ function ModifyPage({ sequential }: { sequential: string }) { PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }), }); - const { mutateAsync: updateBanner } = useMutation({ + const { mutateAsync: updateBanner } = useMutationWrapper({ mutationFn: (data: BannerFormData) => BannerApi.updateBanner(sequential, data), onSuccess: () => { @@ -50,14 +50,14 @@ function ModifyPage({ sequential }: { sequential: string }) { }, }); - const { mutateAsync: deleteBanner } = useMutation({ + const { mutateAsync: deleteBanner } = useMutationWrapper({ mutationFn: () => BannerApi.deleteBanner(sequential), onSuccess: () => { alert("삭제되었습니다.", router.back); }, }); - const { mutateAsync: savePosting } = useMutation({ + const { mutateAsync: savePosting } = useMutationWrapper({ mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { queryClient.invalidateQueries({ diff --git a/apps/cs/src/pages/bbs/model/table-columns.tsx b/apps/cs/src/pages/bbs/model/table-columns.tsx index ccd5cdecd..3edb5c8a4 100644 --- a/apps/cs/src/pages/bbs/model/table-columns.tsx +++ b/apps/cs/src/pages/bbs/model/table-columns.tsx @@ -2,16 +2,19 @@ import dayjs from "dayjs"; import { usePathname, useRouter } from "next/navigation"; import type { ColumnDef, Row } from "@tanstack/react-table"; -import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; -import type { Board } from "@/entities/board/model/board-interface"; -import { useMenuStore } from "@/shared/lib"; +import type { Board } from "@/entities/board"; import { Button } from "@common/components"; export const columns: ColumnDef[] = [ { id: "no", header: "NO", - cell: ({ row }) => row.index + 1, + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, }, { accessorKey: "bbsNm", @@ -47,19 +50,14 @@ export const columns: ColumnDef[] = [ const ManageButton = ({ row }: { row: Row }) => { const router = useRouter(); const pathname = usePathname(); - const [, setBbsInfo] = useLocalStorage("bbs-info", ""); - const { setBbsInfo: setStoreBbsInfo } = useMenuStore((state) => state); - const { bbsTypeCd, bbsId } = row.original; + return ( + + ); +}; diff --git a/apps/cs/src/pages/code/model/upcode-table-columns.tsx b/apps/cs/src/pages/code/model/upcode-table-columns.tsx new file mode 100644 index 000000000..c35ea4d70 --- /dev/null +++ b/apps/cs/src/pages/code/model/upcode-table-columns.tsx @@ -0,0 +1,64 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@common/components"; +import type { UpCode } from "@/entities/code"; +import { UpCodeManagementDialog } from "../ui/upcode-dialog"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const { pageIndex, pageSize } = table.getState().pagination; + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "upCd", + header: "상위코드", + }, + { + accessorKey: "upCdNm", + header: "상위코드명", + }, + { + accessorKey: "cdSe", + header: "코드구분", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => { + const date = dayjs(row.original.regDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => ( +
e.stopPropagation()}> + +
+ ), + }, +]; + +const UpCodeManageButton = ({ upCd }: { upCd: string }) => { + const [dialogOpen, setDialogOpen] = useState(false); + return ( + + + + ); +}; diff --git a/apps/cs/src/pages/code/ui/code-page.tsx b/apps/cs/src/pages/code/ui/code-page.tsx new file mode 100644 index 000000000..61e7fb8ea --- /dev/null +++ b/apps/cs/src/pages/code/ui/code-page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useState } from "react"; + +import { UpCodeSection } from "./upcode-section"; +import { LwpoCodeSection } from "./lwpocode-section"; + +function CodePage() { + const [selectedUpCd, setSelectedUpCd] = useState(""); + return ( +
+
+ + +
+
+ ); +} + +export { CodePage }; diff --git a/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx new file mode 100644 index 000000000..c11c57332 --- /dev/null +++ b/apps/cs/src/pages/code/ui/lwpocode-dialog.tsx @@ -0,0 +1,321 @@ +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Input, + RadioGroup, + RadioGroupItem, + Label, + DialogTrigger, +} from "@common/components"; +import { + UpCodeApi, + LwpoCodeApi, + LwpoCodeFormSchema, + type LwpoCodeFormData, +} from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; + +type ManagementLwpoCodeDialogProps = { + lwpoCd?: string; + upCd?: string; + children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +const LwpoCodeManagementDialog = memo( + ({ + lwpoCd, + upCd, + children, + open, + onOpenChange, + ...props + }: ManagementLwpoCodeDialogProps) => { + const isRegistration = !lwpoCd; + + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd], + queryFn: () => LwpoCodeApi.lwpoCodeDetail(upCd!, lwpoCd!), + enabled: !!upCd && !isRegistration, + }); + + const { data: upCodeData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => UpCodeApi.upCodeDetail(upCd!), + enabled: !!upCd, + }); + + const { mutateAsync: createLwpoCode } = useMutationWrapper({ + mutationFn: (data: LwpoCodeFormData) => LwpoCodeApi.createLwpoCode(data), + onSuccess: () => { + alert("등록되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const { mutateAsync: updateLwpoCode } = useMutationWrapper({ + mutationFn: (data: LwpoCodeFormData) => + LwpoCodeApi.updateLwpoCode(upCd!, lwpoCd!, data), + onSuccess: () => { + alert("수정되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const form = useForm({ + resolver: zodResolver(LwpoCodeFormSchema), + values: { + lwpoCd: detailData?.lwpoCd ?? "", + upCd: upCd ?? "", + lwpoCdNm: detailData?.lwpoCdNm ?? "", + lwpoCdExpln: detailData?.lwpoCdExpln ?? "", + unit: detailData?.unit ?? "", + prntLwpoCd: detailData?.prntLwpoCd ?? "", + useYn: detailData?.useYn ?? "Y", + sortSeq: detailData?.sortSeq ?? 0, + delYn: "N", + }, + }); + + const onSubmit = + (callback: (data: LwpoCodeFormData) => void) => + async (data: LwpoCodeFormData) => { + const confirmMessage = isRegistration + ? "등록하시겠습니까?" + : "수정하시겠습니까?"; + + if (await confirm(confirmMessage)) callback(data); + }; + + return ( + + {children} + + + + {isRegistration ? "하위코드 등록" : "하위코드 관리"} + + +
+ + + + + + + + + 하위코드 + + ( + + + + + + )} + /> + + + + 하위코드명 + + ( + + + + + + )} + /> + + + + 하위코드설명 + + ( + + + + + + )} + /> + + + {upCodeData?.cdSe === "CODECLUNIT" && ( + + 단위 + + ( + + + + + + )} + /> + + + )} + {upCodeData?.cdSe === "CODECLTREE" && ( + + 상위하위코드 + + ( + + + + + + )} + /> + + + )} + + 정렬순서 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+ + + + + + +
+ +
+
+ ); + }, + (prevProps, nextProps) => { + return ( + prevProps.lwpoCd === nextProps.lwpoCd && + prevProps.upCd === nextProps.upCd && + prevProps.open === nextProps.open + ); + }, +); + +LwpoCodeManagementDialog.displayName = "LwpoCodeManagementDialog"; + +export { LwpoCodeManagementDialog }; diff --git a/apps/cs/src/pages/code/ui/lwpocode-section.tsx b/apps/cs/src/pages/code/ui/lwpocode-section.tsx new file mode 100644 index 000000000..fd1df2d8e --- /dev/null +++ b/apps/cs/src/pages/code/ui/lwpocode-section.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { LwpoCodeApi, type LwpoCodeParams } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/lwpocode-table-columns"; +import { LwpoCodeManagementDialog } from "./lwpocode-dialog"; + +type LwpoCodeSectionProps = { + upCd?: string; +}; + +function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) { + const [dialogOpen, setDialogOpen] = useState(false); + const [lwpoCodeQuery, setLwpoCodeQuery] = useState({ + lwpoCdNm: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: lwpoCodeList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, pagination], + queryFn: () => + LwpoCodeApi.getLwpoCodeList({ + ...lwpoCodeQuery, + upCd, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: !!upCd, + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + data: lwpoCodeList?.content ?? [], + columns, + rowCount: lwpoCodeList?.totalElements ?? 0, + state: { pagination }, + manualPagination: true, + enableMultiRowSelection: false, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + getRowId: (row) => row.lwpoCd, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+
+
+ + + setLwpoCodeQuery({ + ...lwpoCodeQuery, + lwpoCdNm: e.target.value, + }) + } + /> + +
+
+
+
+

하위코드 목록

+ + + +
+
+ +
+
+
+ ); +} + +export { LwpoCodeSection }; diff --git a/apps/cs/src/pages/code/ui/upcode-dialog.tsx b/apps/cs/src/pages/code/ui/upcode-dialog.tsx new file mode 100644 index 000000000..fafb7429f --- /dev/null +++ b/apps/cs/src/pages/code/ui/upcode-dialog.tsx @@ -0,0 +1,280 @@ +import { memo } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Input, + RadioGroup, + RadioGroupItem, + Label, + DialogTrigger, +} from "@common/components"; +import { + CodeSelect, + UpCodeApi, + upCodeFormSchema, + type UpCodeFormData, +} from "@/entities/code"; +import { SiteSelect } from "@/entities/site"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form"; + +type UpCodeManagementDialogProps = { + upCd?: string; + children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +const UpCodeManagementDialog = memo( + ({ + upCd, + children, + open, + onOpenChange, + ...props + }: UpCodeManagementDialogProps) => { + const isRegistration = !upCd; + + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: detailData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd], + queryFn: () => UpCodeApi.upCodeDetail(upCd!), + enabled: open && !!upCd, + }); + + const { mutateAsync: createUpCode } = useMutationWrapper({ + mutationFn: (data: UpCodeFormData) => UpCodeApi.createUpCode(data), + onSuccess: () => { + alert("등록되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const { mutateAsync: updateUpCode } = useMutationWrapper({ + mutationFn: (data: UpCodeFormData) => UpCodeApi.updateUpCode(upCd!, data), + onSuccess: () => { + alert("수정되었습니다.", () => onOpenChange?.(false)); + }, + }); + + const form = useForm({ + resolver: zodResolver(upCodeFormSchema), + values: { + upCd: detailData?.upCd ?? "", + upCdNm: detailData?.upCdNm ?? "", + cdSe: detailData?.cdSe ?? "", + siteId: detailData?.siteId ?? "", + upCdExpln: detailData?.upCdExpln ?? "", + useYn: detailData?.useYn ?? "Y", + delYn: "N", + }, + }); + + const onSubmit = + (callback: (data: UpCodeFormData) => void) => + async (data: UpCodeFormData) => { + const confirmMessage = isRegistration + ? "등록하시겠습니까?" + : "수정하시겠습니까?"; + + if (await confirm(confirmMessage)) callback(data); + }; + + return ( + + {children} + + + + {isRegistration ? "상위코드 등록" : "상위코드 관리"} + + +
+ + + + + + + + + 상위코드 + + ( + + + + + + )} + /> + + + + 상위코드명 + + ( + + + + + + )} + /> + + + + 상위코드설명 + + ( + + + + + + )} + /> + + + + 코드구분 + + ( + + + + + + )} + /> + + + + 사이트 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+ + + + + + +
+ +
+
+ ); + }, + (prevProps, nextProps) => { + return ( + prevProps.upCd === nextProps.upCd && prevProps.open === nextProps.open + ); + }, +); + +UpCodeManagementDialog.displayName = "UpCodeManagementDialog"; + +export { UpCodeManagementDialog }; diff --git a/apps/cs/src/pages/code/ui/upcode-section.tsx b/apps/cs/src/pages/code/ui/upcode-section.tsx new file mode 100644 index 000000000..7e3baefc9 --- /dev/null +++ b/apps/cs/src/pages/code/ui/upcode-section.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { UpCodeApi, type UpCode, type UpCodeParams } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/upcode-table-columns"; +import { UpCodeManagementDialog } from "./upcode-dialog"; + +type UpCodeSectionProps = { + handleSelectedUpCd: (upCd: string) => void; +}; + +function UpCodeSection({ handleSelectedUpCd }: UpCodeSectionProps) { + const [dialogOpen, setDialogOpen] = useState(false); + const [upCodeQuery, setUpCodeQuery] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: upCodeList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, pagination], + queryFn: () => + UpCodeApi.getUpCodeList({ + ...upCodeQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + data: upCodeList?.content ?? [], + columns, + rowCount: upCodeList?.totalElements ?? 0, + state: { pagination }, + manualPagination: true, + enableMultiRowSelection: false, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + getRowId: (row) => row.upCd, + }); + + const handleRowClick = (row: UpCode) => { + table.setRowSelection({ [row.upCd]: true }); + handleSelectedUpCd(row.upCd); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + return ( +
+
+
+ + + setUpCodeQuery({ + ...upCodeQuery, + keyword: e.target.value, + }) + } + /> + +
+
+
+
+

상위코드 목록

+ + + +
+
+ +
+
+
+ ); +} + +export { UpCodeSection }; diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx index 823d30173..9db8035b0 100644 --- a/apps/cs/src/pages/contents/ui/contents-form.tsx +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -1,4 +1,6 @@ +import Image from "next/image"; import dynamic from "next/dynamic"; +import { useQuery } from "@tanstack/react-query"; import { type UseFormReturn } from "react-hook-form"; import { Input, @@ -12,12 +14,10 @@ import { TableRow, } from "@common/components"; import { FormWrapper } from "@/widgets/form-wrapper"; -import { FormItem, FormControl, FormField } from "@/shared/ui/form"; import type { ContentsFormData } from "@/entities/content"; -import { useQuery } from "@tanstack/react-query"; +import { CodeApi } from "@/entities/code"; import { ENDPOINT } from "@/shared/config"; -import { CodeApi } from "@/entities/code/api/code-service"; -import Image from "next/image"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; const Editor = dynamic( () => import("@/entities/editor").then((module) => module.Editor), diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index dd47313e7..4fc9ae798 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -89,7 +89,7 @@ function ContentsPage() { 등록 -
+
state); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createContents } = useMutation({ + const { mutateAsync: createContents } = useMutationWrapper({ mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx index d771834c0..8bf3b629d 100644 --- a/apps/cs/src/pages/contents/ui/modify-page.tsx +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -1,19 +1,10 @@ "use client"; import { useEffect, useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; -import { - CONTENTS_APPROVE_STATUS, - ContentsApi, - contentsFormSchema, - type ContentsFormData, -} from "@/entities/content"; -import { ContentsForm } from "./contents-form"; import { Select, SelectTrigger, @@ -22,11 +13,20 @@ import { SelectItem, Button, } from "@common/components"; +import { + CONTENTS_APPROVE_STATUS, + ContentsApi, + contentsFormSchema, + type ContentsFormData, +} from "@/entities/content"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { ContentsForm } from "./contents-form"; function ModifyPage({ sequential }: { sequential: string }) { const { confirm } = useConfirm(); const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const contsSn = parseInt(sequential); const [contsVer, setContsVer] = useState(0); @@ -47,7 +47,7 @@ function ModifyPage({ sequential }: { sequential: string }) { enabled: !!contsVer, }); - const { mutateAsync: createContents } = useMutation({ + const { mutateAsync: createContents } = useMutationWrapper({ mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), onSuccess: () => { setContsVer(0); @@ -58,7 +58,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }, }); - const { mutateAsync: updateContents } = useMutation({ + const { mutateAsync: updateContents } = useMutationWrapper({ mutationFn: (data: ContentsFormData) => ContentsApi.updateContents(contsSn, contsVer, data), onSuccess: () => { @@ -66,7 +66,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }, }); - const { mutateAsync: deleteContents } = useMutation({ + const { mutateAsync: deleteContents } = useMutationWrapper({ mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer), onSuccess: () => { setContsVer(0); @@ -77,7 +77,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }, }); - const { mutateAsync: requestApprove } = useMutation({ + const { mutateAsync: requestApprove } = useMutationWrapper({ mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer), onSuccess: () => { alert("승인 요청이 완료되었습니다."); @@ -87,7 +87,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }, }); - const { mutateAsync: approveContents } = useMutation({ + const { mutateAsync: approveContents } = useMutationWrapper({ mutationFn: () => ContentsApi.approveContents(contsSn, contsVer), onSuccess: () => { alert("승인되었습니다."); diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index cb80e00b0..9b58b3828 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, @@ -10,13 +10,13 @@ import { import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable, Input, Label } from "@common/components"; import { AttachmentApi, type AttachmentParams } from "@/entities/attachment"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { createColumns } from "../model/table-columns"; function FilePage() { const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [attachmentQuery, setAttachmentQuery] = useState({ keyword: "", @@ -38,7 +38,7 @@ function FilePage() { enabled: false, }); - const { mutate: deleteAttachment } = useMutation({ + const { mutate: deleteAttachment } = useMutationWrapper({ mutationFn: (atflId: string) => AttachmentApi.deleteAttachmentByAtflId(atflId), onSuccess: () => { @@ -108,7 +108,7 @@ function FilePage() {

파일 목록

-
+
diff --git a/apps/cs/src/pages/image/ui/create-page.tsx b/apps/cs/src/pages/image/ui/create-page.tsx index 5b3ea3924..e7dda81d5 100644 --- a/apps/cs/src/pages/image/ui/create-page.tsx +++ b/apps/cs/src/pages/image/ui/create-page.tsx @@ -3,17 +3,16 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { imageFormSchema, type ImageFormData } from "../model/image-interface"; import { ImageApi } from "../api/image-service"; import { ImageForm } from "./image-form"; function CreatePage() { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createImage } = useMutation({ + const { mutateAsync: createImage } = useMutationWrapper({ mutationFn: (data: ImageFormData) => ImageApi.createImage(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/image/ui/image-form.tsx b/apps/cs/src/pages/image/ui/image-form.tsx index 7018e6919..0368ace33 100644 --- a/apps/cs/src/pages/image/ui/image-form.tsx +++ b/apps/cs/src/pages/image/ui/image-form.tsx @@ -44,7 +44,9 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) { progress, setFile, } = useTus(); - const { setAttachmentFileCode } = useAttachmentStore((state) => state); + const setAttachmentFileCode = useAttachmentStore( + (state) => state.setAttachmentFileCode, + ); const [imageUrl, setImageUrl] = useState(""); const { data: attachmentData } = useQuery({ @@ -122,11 +124,9 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) { type="number" min={0} value={field.value ?? ""} - onChange={(e) => { - const value = e.target.value; - if (isNaN(parseInt(value))) return; - field.onChange(parseInt(value)); - }} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } className="w-[12rem] !text-[1.3rem]" placeholder="정렬순서 입력" /> diff --git a/apps/cs/src/pages/image/ui/image-page.tsx b/apps/cs/src/pages/image/ui/image-page.tsx index 3571a4a1f..173f77554 100644 --- a/apps/cs/src/pages/image/ui/image-page.tsx +++ b/apps/cs/src/pages/image/ui/image-page.tsx @@ -121,7 +121,7 @@ function ImagePage() {
-
+
diff --git a/apps/cs/src/pages/image/ui/modify-page.tsx b/apps/cs/src/pages/image/ui/modify-page.tsx index e58bace78..e04787a9e 100644 --- a/apps/cs/src/pages/image/ui/modify-page.tsx +++ b/apps/cs/src/pages/image/ui/modify-page.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, @@ -13,7 +13,7 @@ import { import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable } from "@common/components"; import { PostingApi, type PostPayload } from "@/entities/posting"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { imageFormSchema, type ImageFormData } from "../model/image-interface"; import { columns } from "../model/posting-table-columns"; @@ -24,7 +24,7 @@ function ModifyPage({ sequential }: { sequential: string }) { const router = useRouter(); const { confirm } = useConfirm(); const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [rowSelection, setRowSelection] = useState({}); @@ -39,21 +39,21 @@ function ModifyPage({ sequential }: { sequential: string }) { PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(sequential) }), }); - const { mutateAsync: updateImage } = useMutation({ + const { mutateAsync: updateImage } = useMutationWrapper({ mutationFn: (data: ImageFormData) => ImageApi.updateImage(sequential, data), onSuccess: () => { alert("수정되었습니다."); }, }); - const { mutateAsync: deleteImage } = useMutation({ + const { mutateAsync: deleteImage } = useMutationWrapper({ mutationFn: () => ImageApi.deleteImage(sequential), onSuccess: () => { alert("삭제되었습니다.", router.back); }, }); - const { mutateAsync: savePosting } = useMutation({ + const { mutateAsync: savePosting } = useMutationWrapper({ mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { queryClient.invalidateQueries({ diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index f6bb7ee6e..5cda4f522 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; import { Button, cn } from "@common/components"; +import { useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { MenuApi } from "../api/menu-service"; import type { TreeMenu } from "../model/menu-interface"; @@ -33,7 +34,7 @@ function MenuNode({ const queryClient = useQueryClient(); - const { mutateAsync: updateMenuName } = useMutation({ + const { mutateAsync: updateMenuName } = useMutationWrapper({ mutationFn: (nodeName: string) => { const encodedName = encodeURIComponent(nodeName); return MenuApi.updateMenuName(id as number, encodedName); diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 19ee1478b..6ff6f89bc 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -5,7 +5,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import type { NodeModel } from "@minoru/react-dnd-treeview"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Button, Select, @@ -16,7 +16,7 @@ import { } from "@common/components"; import { SiteApi } from "@/entities/site"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import type { TreeMenu } from "../model/menu-interface"; import { MenuApi } from "../api/menu-service"; import { @@ -30,7 +30,7 @@ import { generateMenuTreeData } from "../lib/utils"; function MenuPage() { const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { confirm } = useConfirm(); const [siteId, setSiteId] = useState(""); @@ -68,7 +68,7 @@ function MenuPage() { })), }); - const { mutateAsync: createMenu } = useMutation({ + const { mutateAsync: createMenu } = useMutationWrapper({ mutationFn: (data: MenuFormData) => MenuApi.createMenu(data), onSuccess: () => { alert("저장되었습니다."); @@ -78,7 +78,7 @@ function MenuPage() { }, }); - const { mutate: updateMenu } = useMutation({ + const { mutate: updateMenu } = useMutationWrapper({ mutationFn: ({ menuCd, data, @@ -94,7 +94,7 @@ function MenuPage() { }, }); - const { mutate: deleteMenu } = useMutation({ + const { mutate: deleteMenu } = useMutationWrapper({ mutationFn: (menuCd: number) => MenuApi.deleteMenu(menuCd), onSuccess: () => { alert("삭제되었습니다."); diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index 0a926a27b..a1fa4dacb 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import type { UseFormReturn } from "react-hook-form"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQueryClient } from "@tanstack/react-query"; import { Tree, type NodeModel, @@ -10,6 +10,7 @@ import { } from "@minoru/react-dnd-treeview"; import { Button } from "@common/components"; import { ENDPOINT } from "@/shared/config"; +import { useMutationWrapper } from "@/shared/lib"; import { MenuApi } from "../api/menu-service"; import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface"; import { handleDropNode, handleSelectNode } from "../lib/utils"; @@ -38,7 +39,7 @@ function MenuTree({ const queryClient = useQueryClient(); const treeMethods = useRef(null); - const { mutate: updateTreeMenu } = useMutation({ + const { mutate: updateTreeMenu } = useMutationWrapper({ mutationFn: (newTree: TreeNode) => MenuApi.updateTreeMenu(siteId, [newTree]), onSuccess: () => { diff --git a/apps/cs/src/pages/module/model/authorizations-table-columns.tsx b/apps/cs/src/pages/module/model/authorizations-table-columns.tsx index a2ccb2a88..359afab19 100644 --- a/apps/cs/src/pages/module/model/authorizations-table-columns.tsx +++ b/apps/cs/src/pages/module/model/authorizations-table-columns.tsx @@ -1,6 +1,6 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; -import type { Authorizations } from "./authorizations-interface"; +import type { Authorizations } from "@/entities/authorization"; import { ManagementAuthorityDialog } from "../ui/management-authority-dialog"; export const columns: ColumnDef[] = [ diff --git a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx index 052bd02db..105e6ffaf 100644 --- a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx +++ b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx @@ -1,8 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table"; import type { UseFormReturn } from "react-hook-form"; import { Checkbox } from "@common/components"; -import type { RoleAuthorizations } from "./role-authorizations-interface"; -import type { RoleAuthorizationsFormData } from "./role-authorizations-interface"; +import type { + RoleAuthorizations, + RoleAuthorizationsFormData, +} from "@/entities/role-authorization"; export const createColumns = ( form: UseFormReturn, diff --git a/apps/cs/src/pages/module/ui/create-page.tsx b/apps/cs/src/pages/module/ui/create-page.tsx index 50a78fc4c..59884c19d 100644 --- a/apps/cs/src/pages/module/ui/create-page.tsx +++ b/apps/cs/src/pages/module/ui/create-page.tsx @@ -3,20 +3,19 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { + AuthorizationsApi, authorizationFormSchema, type AuthorizationFormData, -} from "../model/authorizations-interface"; -import { AuthorizationsApi } from "../api/authorization-service"; +} from "@/entities/authorization"; import { ModuleForm } from "./module-form"; function CreatePage() { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); - const { mutateAsync: createAuthorization } = useMutation({ + const { mutateAsync: createAuthorization } = useMutationWrapper({ mutationFn: (data: AuthorizationFormData) => AuthorizationsApi.createAuthorization(data), onSuccess: () => { diff --git a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx index dae061340..8ca38a570 100644 --- a/apps/cs/src/pages/module/ui/management-authority-dialog.tsx +++ b/apps/cs/src/pages/module/ui/management-authority-dialog.tsx @@ -1,7 +1,7 @@ -import { useState } from "react"; +import { useState, memo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, @@ -25,126 +25,133 @@ import { TableHeader, TableRow, } from "@common/components"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; import { + RoleAuthorizationsApi, roleAuthorizationsFormSchema, type RoleAuthorizationsFormData, type RoleAuthorizationsData, -} from "../model/role-authorizations-interface"; +} from "@/entities/role-authorization"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { createColumns } from "../model/role-authorizations-table-columns"; -import type { Authorizations } from "../model/authorizations-interface"; -import { RoleAuthorizationsApi } from "../api/role-authorization-service"; +import type { Authorizations } from "@/entities/authorization"; type ManagementAuthorityDialogProps = { authorizationNo?: string; rowData?: Authorizations; } & React.ComponentProps; -function ManagementAuthorityDialog({ - authorizationNo, - rowData, - ...props -}: ManagementAuthorityDialogProps) { - const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); +const ManagementAuthorityDialog = memo( + ({ authorizationNo, rowData, ...props }: ManagementAuthorityDialogProps) => { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); - const [isOpened, setIsOpened] = useState(false); - const [rowSelection, setRowSelection] = useState({}); + const [isOpened, setIsOpened] = useState(false); + const [rowSelection, setRowSelection] = useState({}); - const { data: roleAuthorizationList } = useQuery({ - queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo], - queryFn: () => - RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }), - enabled: !!authorizationNo && isOpened, - }); + const { data: roleAuthorizationList } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo], + queryFn: () => + RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }), + enabled: !!authorizationNo && isOpened, + }); - const { mutateAsync: saveRoleAuthorization } = useMutation({ - mutationFn: (data: RoleAuthorizationsData) => - RoleAuthorizationsApi.createRoleAuthorizations(data), - onSuccess: () => { - alert("저장되었습니다."); - }, - }); + const { mutateAsync: saveRoleAuthorization } = useMutationWrapper({ + mutationFn: (data: RoleAuthorizationsData) => + RoleAuthorizationsApi.createRoleAuthorizations(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); - const form = useForm({ - resolver: zodResolver(roleAuthorizationsFormSchema), - values: { - roleAuthorizations: - roleAuthorizationList?.content.map( - ({ roleId, authorizationNo, useYn }) => ({ - roleId, - authorizationNo, - authYn: useYn, - }), - ) ?? [], - }, - }); + const form = useForm({ + resolver: zodResolver(roleAuthorizationsFormSchema), + values: { + roleAuthorizations: + roleAuthorizationList?.content.map( + ({ roleId, authorizationNo, useYn }) => ({ + roleId, + authorizationNo, + authYn: useYn, + }), + ) ?? [], + }, + }); - const table = useReactTable({ - data: roleAuthorizationList?.content ?? [], - columns: createColumns(form), - state: { - rowSelection, - }, - getCoreRowModel: getCoreRowModel(), - onRowSelectionChange: setRowSelection, - }); + const table = useReactTable({ + data: roleAuthorizationList?.content ?? [], + columns: createColumns(form), + state: { + rowSelection, + }, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); - const handleSave = async () => { - if (await confirm("저장하시겠습니까?")) { - const data = form.getValues("roleAuthorizations"); - await saveRoleAuthorization(data); - } - }; + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("roleAuthorizations"); + await saveRoleAuthorization(data); + } + }; - return ( - - - - - - - 모듈API 권한관리 - -
- - - - API명 - 요청 메소드 - API URL - - - - - - {rowData?.authorizationName} - - {rowData?.httpMethodCode} - {rowData?.urlPatternValue} - - -
-
- -
-
- - - - - - -
-
- ); -} + + + + + + + ); + }, + (prevProps, nextProps) => { + return prevProps.authorizationNo === nextProps.authorizationNo; + }, +); + +ManagementAuthorityDialog.displayName = "ManagementAuthorityDialog"; export { ManagementAuthorityDialog }; diff --git a/apps/cs/src/pages/module/ui/modify-page.tsx b/apps/cs/src/pages/module/ui/modify-page.tsx index e22fd988a..e26dfca4b 100644 --- a/apps/cs/src/pages/module/ui/modify-page.tsx +++ b/apps/cs/src/pages/module/ui/modify-page.tsx @@ -2,25 +2,25 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useQuery } from "@tanstack/react-query"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { + AuthorizationsApi, authorizationFormSchema, type AuthorizationFormData, -} from "../model/authorizations-interface"; -import { AuthorizationsApi } from "../api/authorization-service"; +} from "@/entities/authorization"; import { ModuleForm } from "./module-form"; function ModifyPage({ sequential }: { sequential: string }) { - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { data: authorizationInfo } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, sequential], queryFn: () => AuthorizationsApi.getAuthorizationInfo(sequential), }); - const { mutateAsync: updateAuthorization } = useMutation({ + const { mutateAsync: updateAuthorization } = useMutationWrapper({ mutationFn: (data: AuthorizationFormData) => AuthorizationsApi.updateAuthorization(sequential, data), onSuccess: () => { diff --git a/apps/cs/src/pages/module/ui/module-form.tsx b/apps/cs/src/pages/module/ui/module-form.tsx index 8ee685407..8c6e7face 100644 --- a/apps/cs/src/pages/module/ui/module-form.tsx +++ b/apps/cs/src/pages/module/ui/module-form.tsx @@ -18,7 +18,7 @@ import { FormControl, FormField, FormItem } from "@/shared/ui/form"; import type { AuthorizationFormData, WithRoleAuthorizations, -} from "../model/authorizations-interface"; +} from "@/entities/authorization"; type BannerFormProps = { form: UseFormReturn; @@ -113,11 +113,9 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) { type="number" min={0} value={field.value ?? ""} - onChange={(e) => { - const value = e.target.value; - if (isNaN(parseInt(value))) return; - field.onChange(parseInt(value)); - }} + onChange={(e) => + field.onChange(parseInt(e.target.value)) + } className="w-[12rem] !text-[1.3rem]" placeholder="정렬순서 입력" /> diff --git a/apps/cs/src/pages/module/ui/module-page.tsx b/apps/cs/src/pages/module/ui/module-page.tsx index b6c98cb14..3ae4cd9f4 100644 --- a/apps/cs/src/pages/module/ui/module-page.tsx +++ b/apps/cs/src/pages/module/ui/module-page.tsx @@ -11,12 +11,12 @@ import { import { Button, DataTable, Input, Label } from "@common/components"; import { CodeSelect } from "@/entities/code"; import { ENDPOINT } from "@/shared/config"; -import type { - Authorizations, - AuthorizationsParams, -} from "../model/authorizations-interface"; +import { + AuthorizationsApi, + type Authorizations, + type AuthorizationsParams, +} from "@/entities/authorization"; import { columns } from "../model/authorizations-table-columns"; -import { AuthorizationsApi } from "../api/authorization-service"; function ModulePage() { const router = useRouter(); @@ -113,7 +113,7 @@ function ModulePage() {

모듈API 목록

-
+
diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index d481137e8..d07477630 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -5,7 +5,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import type { NodeModel } from "@minoru/react-dnd-treeview"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Button } from "@common/components"; import { DndTree, EditableNode } from "@/widgets/dnd-tree"; import { @@ -18,13 +18,13 @@ import { type OrganizationTreeForDnd, } from "@/entities/organization"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { OrganizationForm } from "./organization-form"; import { UserOrganizationSection } from "./user-organization-section"; function OrganizationPage() { const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { confirm } = useConfirm(); const [additionalNode, setAdditionalNode] = @@ -48,7 +48,7 @@ function OrganizationPage() { enabled: !!selectedOrganization?.ognzSn, }); - const { mutateAsync: createOrganization } = useMutation({ + const { mutateAsync: createOrganization } = useMutationWrapper({ mutationFn: (form: OrganizationFormData) => OrganizationApi.createOrganization(form), onSuccess: () => { @@ -59,7 +59,7 @@ function OrganizationPage() { }, }); - const { mutateAsync: deleteOrganization } = useMutation({ + const { mutateAsync: deleteOrganization } = useMutationWrapper({ mutationFn: (ognzSn: number) => OrganizationApi.deleteOrganization(ognzSn), onSuccess: () => { alert("삭제되었습니다."); @@ -70,7 +70,7 @@ function OrganizationPage() { }, }); - const { mutateAsync: updateOrganizationName } = useMutation({ + const { mutateAsync: updateOrganizationName } = useMutationWrapper({ mutationFn: ({ ognzSn, name }: { ognzSn: number; name: string }) => OrganizationApi.updateOrganizationName(ognzSn, name), onSuccess: () => { @@ -81,7 +81,7 @@ function OrganizationPage() { }, }); - const { mutateAsync: updateOrganization } = useMutation({ + const { mutateAsync: updateOrganization } = useMutationWrapper({ mutationFn: ({ ognzSn, data, @@ -97,7 +97,7 @@ function OrganizationPage() { }, }); - const { mutateAsync: updateOrganizationByDnd } = useMutation({ + const { mutateAsync: updateOrganizationByDnd } = useMutationWrapper({ mutationFn: (data: OrganizationTreeForDnd) => OrganizationApi.updateOrganizationByDnd([data]), onSuccess: () => { diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx index 60e401562..000af3c32 100644 --- a/apps/cs/src/pages/organization/ui/user-organization-section.tsx +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -5,7 +5,6 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery, - useMutation, useQueryClient, keepPreviousData, } from "@tanstack/react-query"; @@ -37,7 +36,7 @@ import { import { CodeApi, type ICode } from "@/entities/code"; import type { OrganizationTree } from "@/entities/organization"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { AdditionalUserDialog } from "./additional-user-dialog"; import { MoveOrganizationDialog } from "./move-organization-dialog"; @@ -48,7 +47,7 @@ type UserOrganizationSectionProps = { function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { const queryClient = useQueryClient(); const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [userList, setUserList] = useState([]); const [selectedUsers, setSelectedUsers] = useState([]); @@ -73,7 +72,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { queryFn: () => CodeApi.getCode("JDGDCLCD"), }); - const { mutate: saveUserOrganization } = useMutation({ + const { mutate: saveUserOrganization } = useMutationWrapper({ mutationFn: (data: UserOrganizationPayload[]) => UserOrganizationApi.saveUserOrganization(data), onSuccess: () => { @@ -84,7 +83,7 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { }, }); - const { mutate: moveUserOrganization } = useMutation({ + const { mutate: moveUserOrganization } = useMutationWrapper({ mutationFn: (data: MoveUserOrganizationPayload[]) => UserOrganizationApi.moveUserOrganization(data), onSuccess: () => { diff --git a/apps/cs/src/pages/popup/api/popup-service.ts b/apps/cs/src/pages/popup/api/popup-service.ts new file mode 100644 index 000000000..b82d5f473 --- /dev/null +++ b/apps/cs/src/pages/popup/api/popup-service.ts @@ -0,0 +1,46 @@ +import { ENDPOINT } from "@/shared/config"; +import { + popupFormSchema, + popupUpdateFormSchema, + type PopupFormData, + type PopupInfo, + type PopupList, + type PopupParams, + type PopupService, + type PopupUpdateFormData, +} from "../model/popup-interface"; +import { client } from "@/shared/api"; + +export class PopupApiService implements PopupService { + public async getPopupList(params: PopupParams): Promise { + const response = await client.get(ENDPOINT.CMS_SERVICE.POPUPS, { + params, + }); + return response.data; + } + + public async getPopupInfo(popupSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, + ); + return response.data; + } + + public async createPopup(data: PopupFormData): Promise { + const validatedData = popupFormSchema.parse(data); + await client.post(ENDPOINT.CMS_SERVICE.POPUPS, validatedData); + } + + public async updatePopup( + popupSn: number, + data: PopupUpdateFormData, + ): Promise { + const validatedData = popupUpdateFormSchema.parse(data); + await client.put( + `${ENDPOINT.CMS_SERVICE.POPUPS}/${popupSn}`, + validatedData, + ); + } +} + +export const PopupApi = new PopupApiService(); diff --git a/apps/cs/src/pages/popup/index.ts b/apps/cs/src/pages/popup/index.ts new file mode 100644 index 000000000..44e21e42c --- /dev/null +++ b/apps/cs/src/pages/popup/index.ts @@ -0,0 +1,4 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { PopupPage } from "./ui/popup-page"; +export { ViewPopup } from "./ui/view-popup"; diff --git a/apps/cs/src/pages/popup/model/popup-interface.ts b/apps/cs/src/pages/popup/model/popup-interface.ts new file mode 100644 index 000000000..6952d873f --- /dev/null +++ b/apps/cs/src/pages/popup/model/popup-interface.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface PopupParams extends PageableParams { + keyword?: string; + popupTypeCd?: string; + contsTypeCd?: string; + useYn?: string; +} + +export interface Popup { + popupSn: number; + popupTypeCd: string; + popupTypeNm: string; + contsTypeCd: string; + contsTypeNm: string; + popupTtl: string; + popupWdth: number; + popupHgt: number; + popupEndUseYn: string; + useYn: string; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + +export interface PopupInfo extends Popup { + popupCn: string; + popupVtclPstn: number; + popupHrzntltyPstn: number; + popupLnkgUrl: string; + atflCd: string; + regDt: string; + rgTrNm: string; +} + +export const popupFormSchema = z.object({ + popupTypeCd: z.string(), + contsTypeCd: z.string(), + popupTtl: z.string(), + popupCn: z.string().nullish(), + popupWdth: z.number(), + popupHgt: z.number(), + popupVtclPstn: z.number(), + popupHrzntltyPstn: z.number(), + popupEndUseYn: z.string(), + popupLnkgUrl: z.string().nullish(), + atflCd: z.string().nullish(), + useYn: z.string(), + rgtr: z.string(), +}); + +export const popupUpdateFormSchema = popupFormSchema + .omit({ + rgtr: true, + }) + .extend({ + mdfr: z.string(), + }) + .partial(); + +export type PopupList = PageableList; +export type PopupFormData = z.infer; +export type PopupUpdateFormData = z.infer; + +export interface PopupService { + getPopupList(params: PopupParams): Promise; + getPopupInfo(popupSn: number): Promise; + createPopup(data: PopupFormData): Promise; + updatePopup(popupSn: number, data: PopupUpdateFormData): Promise; +} diff --git a/apps/cs/src/pages/popup/model/posting-table-columns.tsx b/apps/cs/src/pages/popup/model/posting-table-columns.tsx new file mode 100644 index 000000000..d9de20543 --- /dev/null +++ b/apps/cs/src/pages/popup/model/posting-table-columns.tsx @@ -0,0 +1,42 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components"; +import type { Post } from "@/entities/posting"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + accessorKey: "siteNm", + header: "사이트", + }, + { + accessorKey: "pstgPrdSeNm", + header: "게시기간", + cell: ({ row }) => { + const pstgPrdSeNm = row.original.pstgPrdSeNm; + const pstgBgngDt = row.original.pstgBgngDt; + const pstgEndDt = row.original.pstgEndDt; + return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; + }, + }, +]; diff --git a/apps/cs/src/pages/popup/model/table-columns.tsx b/apps/cs/src/pages/popup/model/table-columns.tsx new file mode 100644 index 000000000..4f9491112 --- /dev/null +++ b/apps/cs/src/pages/popup/model/table-columns.tsx @@ -0,0 +1,100 @@ +import dayjs from "dayjs"; +import { type ColumnDef } from "@tanstack/react-table"; +import { Button, Checkbox } from "@common/components"; +import { PopupApi } from "../api/popup-service"; +import type { Popup } from "./popup-interface"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const { pageIndex, pageSize } = table.getState().pagination; + const totalRows = table.getRowCount(); + return totalRows - (pageIndex * pageSize + row.index); + }, + }, + { + accessorKey: "popupTtl", + header: "제목", + }, + { + accessorKey: "popupTypeNm", + header: "팝업유형", + }, + { + accessorKey: "contsTypeNm", + header: "콘텐츠유형", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + const date = dayjs(row.original.mdfcnDt); + return date.format("YYYY-MM-DD"); + }, + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + id: "preview", + header: "미리보기", + cell: ({ row }) => , + }, +]; + +const PreviewButton = ({ popupSn }: { popupSn: number }) => { + const handlePreview = async (e: React.MouseEvent) => { + e.stopPropagation(); + + try { + const response = await PopupApi.getPopupInfo(popupSn); + if (!response) return; + + const width = response.popupWdth; + const height = response.popupHgt; + const left = response.popupHrzntltyPstn + window.screenX; + const top = response.popupVtclPstn + window.screenY; + window.open( + `/view/popup/${popupSn}`, + "", + `width=${width},height=${height},left=${left},top=${top}`, + ); + } catch (error) { + console.error(error); + } + }; + + return ( + + ); +}; diff --git a/apps/cs/src/pages/popup/ui/create-page.tsx b/apps/cs/src/pages/popup/ui/create-page.tsx new file mode 100644 index 000000000..5b428e0e1 --- /dev/null +++ b/apps/cs/src/pages/popup/ui/create-page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useUserStore, useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { popupFormSchema, type PopupFormData } from "../model/popup-interface"; +import { PopupApi } from "../api/popup-service"; +import { PopupForm } from "./popup-form"; + +function CreatePage() { + const router = useRouter(); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); + + const { mutateAsync: createPopup } = useMutationWrapper({ + mutationFn: (data: PopupFormData) => PopupApi.createPopup(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(popupFormSchema), + defaultValues: { + contsTypeCd: "39", + popupEndUseYn: "Y", + useYn: "Y", + }, + }); + + useEffect(() => { + if (!userInfo || !form) return; + form.setValue("rgtr", userInfo.userUnqId); + }, [userInfo, form]); + + return ( +
+
+

팝업 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/popup/ui/modify-page.tsx b/apps/cs/src/pages/popup/ui/modify-page.tsx new file mode 100644 index 000000000..34bc2ae66 --- /dev/null +++ b/apps/cs/src/pages/popup/ui/modify-page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type RowSelectionState, +} from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { DataTable } from "@common/components"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { PostingApi, type PostPayload } from "@/entities/posting"; +import { useAlertStore, useUserStore, useMutationWrapper } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { + popupUpdateFormSchema, + type PopupUpdateFormData, +} from "../model/popup-interface"; +import { columns } from "../model/posting-table-columns"; +import { PopupApi } from "../api/popup-service"; +import { PopupForm } from "./popup-form"; + +function ModifyPage({ popupSn }: { popupSn: number }) { + const router = useRouter(); + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); + const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); + + const [rowSelection, setRowSelection] = useState({}); + + const { data: popupInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, popupSn], + queryFn: () => PopupApi.getPopupInfo(popupSn), + enabled: !!popupSn, + }); + + const { data: postHistory } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, popupSn], + queryFn: () => PostingApi.getPostingHistory({ pstgTrgtNo: popupSn }), + }); + + const { mutateAsync: updatePopup } = useMutationWrapper({ + mutationFn: (data: PopupUpdateFormData) => + PopupApi.updatePopup(popupSn, data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const { mutateAsync: savePostingHistory } = useMutationWrapper({ + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, popupSn], + }); + alert("삭제되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(popupUpdateFormSchema), + values: { + ...popupInfo, + mdfr: userInfo?.userUnqId ?? "", + }, + }); + + const table = useReactTable({ + data: postHistory?.content ?? [], + columns, + state: { + rowSelection, + }, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const handleDeletePostingHistory = async () => { + const data = table.getFilteredSelectedRowModel().rows.map((row) => ({ + ...row.original, + mode: "D", + })); + + if (await confirm("삭제하시겠습니까?")) { + savePostingHistory(data); + setRowSelection({}); + } + }; + + return ( +
+
+
+

팝업 수정

+
+ +
+
+
+

사이트 게시 이력

+
+
+
+ + 이력삭제 + +
+ +
+
+
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/popup/ui/popup-form.tsx b/apps/cs/src/pages/popup/ui/popup-form.tsx new file mode 100644 index 000000000..8e1e5783f --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-form.tsx @@ -0,0 +1,366 @@ +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; +import type { Path, UseFormReturn } from "react-hook-form"; +import { useQuery } from "@tanstack/react-query"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { AttachmentApi } from "@/entities/attachment"; +import { GeneralFileUploader } from "@/widgets/post"; +import { CodeSelect } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { setCookie } from "@/shared/lib"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import type { + PopupFormData, + PopupUpdateFormData, +} from "../model/popup-interface"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +type PopupFormProps = { + form: UseFormReturn; + popupSn?: string; + handleSave: (data: T) => Promise; + handleDelete?: () => Promise; +}; + +function PopupForm({ + form, + popupSn, + handleSave, + ...props +}: PopupFormProps) { + const atflCd = form.getValues("atflCd" as Path); + const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + + const { data: attachmentData } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd], + queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd as string), + enabled: !!atflCd, + }); + + const saveHandlerInterceptor = async (data: T) => { + if (deleteAttachmentIds.length > 0) { + await Promise.all( + deleteAttachmentIds.map(AttachmentApi.deleteAttachmentByAtflId), + ); + } + await handleSave(data); + }; + + useEffect(() => { + setCookie("atfl-cd-key", ""); + }, []); + + return ( + +
+ + + + + + + + 제목 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 팝업 크기 + +
+ } + render={({ field }) => ( + + + + field.onChange(parseInt(e.target.value)) + } + /> + + + )} + /> + × + } + render={({ field }) => ( + + + + field.onChange(parseInt(e.target.value)) + } + /> + + + )} + /> +
+
+
+ + 팝업 위치 + +
+ } + render={({ field }) => ( + + + + field.onChange(parseInt(e.target.value)) + } + /> + + + )} + /> + / + } + render={({ field }) => ( + + + + field.onChange(parseInt(e.target.value)) + } + /> + + + )} + /> +
+
+
+ + 팝업 유형 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 콘텐츠 유형 + + } + render={({ field }) => ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 링크 주소 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 팝업 내용 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 첨부 파일 + + + + + + 팝업 그만 보기 사용 여부 + + } + render={({ field }) => ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + } + render={({ field }) => ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { PopupForm }; diff --git a/apps/cs/src/pages/popup/ui/popup-page.tsx b/apps/cs/src/pages/popup/ui/popup-page.tsx new file mode 100644 index 000000000..891a10f45 --- /dev/null +++ b/apps/cs/src/pages/popup/ui/popup-page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { POSTING_TYPE, PostingDialog } from "@/entities/posting"; +import { ENDPOINT } from "@/shared/config"; +import { PopupApi } from "../api/popup-service"; +import { columns } from "../model/table-columns"; +import type { Popup, PopupParams } from "../model/popup-interface"; + +type PostingTarget = { + pstgTrgtNm: string; + pstgTrgtNo: number; +}; + +function PopupPage() { + const router = useRouter(); + + const [popupParams, setPopupParams] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + const [postingTargets, setPostingTargets] = useState([]); + + const { data: popupList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], + queryFn: () => + PopupApi.getPopupList({ + ...popupParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Popup) => { + router.push(`/popup/register/${row.popupSn}`); + }; + + const table = useReactTable({ + data: popupList?.content ?? [], + columns, + rowCount: popupList?.totalElements ?? 0, + state: { + pagination, + rowSelection, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + useEffect(() => { + const postingTargets = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + pstgTrgtNm: row.original.popupTtl, + pstgTrgtNo: row.original.popupSn, + })); + + if (!postingTargets.length) { + setPostingTargets([]); + return; + } + + setPostingTargets(postingTargets); + }, [rowSelection]); + + return ( +
+
+
+ + + setPopupParams({ ...popupParams, keyword: e.target.value }) + } + /> + +
+
+
+
+

팝업 목록

+
+ + router.push("/popup/register")} + > + 등록 + +
+
+
+ +
+
+
+ ); +} + +export { PopupPage }; diff --git a/apps/cs/src/pages/popup/ui/view-popup.tsx b/apps/cs/src/pages/popup/ui/view-popup.tsx new file mode 100644 index 000000000..e587b17cb --- /dev/null +++ b/apps/cs/src/pages/popup/ui/view-popup.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { Button, Checkbox, Label } from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { PopupApi } from "../api/popup-service"; +import { useEffect, useRef } from "react"; +import { setCookie, getCookie } from "@/shared/lib/cookie"; + +function ViewPopup({ popupSn }: { popupSn: string }) { + const doNotShowTodayRef = useRef(false); + + const { data: popupInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, popupSn], + queryFn: () => PopupApi.getPopupInfo(parseInt(popupSn)), + }); + + const handleClose = () => { + if (doNotShowTodayRef.current) { + const expireDate = new Date(); + expireDate.setHours(23, 59, 59, 999); + + setCookie(`cs-popup-hide-for-day-${popupSn}`, "true", { + expires: expireDate, + path: "/", + }); + } + + window.close(); + }; + + const handleCheckboxChange = (checked: boolean) => { + doNotShowTodayRef.current = checked; + }; + + useEffect(() => { + const cookieName = `cs-popup-hide-for-day-${popupSn}`; + const hasCookie = getCookie(cookieName); + + if (hasCookie) { + window.close(); + } + }, [popupSn]); + + return ( +
+
+
+

+ {popupInfo?.popupTtl} +

+
+
+
+
+
+ + +
+ +
+
+ ); +} + +export { ViewPopup }; diff --git a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx index 64afef317..0afa6b75c 100644 --- a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx +++ b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx @@ -12,6 +12,7 @@ import { } from "@common/components"; import type { Board } from "@/entities/board"; import type { MovePost } from "@/entities/post"; +import { AuthorityButton } from "@/entities/menu-authority"; type MovePostDialogProps = { selectedPosts: MovePost[]; @@ -36,9 +37,12 @@ function MovePostDialog({ return ( - +

diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx index 407b90d49..3c25f5837 100644 --- a/apps/cs/src/pages/recent/ui/recent-page.tsx +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -4,7 +4,7 @@ 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 { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, useReactTable, @@ -30,8 +30,9 @@ import { type RecentPost, type RecentPostParams, } from "@/entities/post"; -import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; import { BoardApi } from "@/entities/board"; +import { AuthorityButton } from "@/entities/menu-authority"; +import { useAlertStore, useMutationWrapper, usePrompt } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { columns } from "../model/table-columns"; import { MovePostDialog } from "./move-post-dialog"; @@ -45,8 +46,7 @@ function RecentPage() { const router = useRouter(); const { prompt } = usePrompt(); const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); - const { pageMenuAuthority } = useMenuStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const [dateRange, setDateRange] = useState(); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -79,7 +79,7 @@ function RecentPage() { queryFn: () => BoardApi.getBoardList(), }); - const { mutate: movePost } = useMutation({ + const { mutate: movePost } = useMutationWrapper({ mutationFn: (data: MovePost[]) => PostApi.movePost(data), onSuccess: () => { alert("이동이 완료되었습니다.", refetch); @@ -272,45 +272,46 @@ function RecentPage() {

콘텐츠 목록

- {pageMenuAuthority?.mngAuthrtYn === "Y" && - (isDeletePostData ? ( - <> - - - - ) : ( - <> - ({ - fromBbsId: row.original.bbsId, - pstNo: row.original.pstNo, - }))} - boardList={boardList?.content ?? []} - onMovePost={movePost} - /> - - - ))} + {isDeletePostData ? ( + <> + + 게시물 복원 + + + 완전삭제 + + + ) : ( + <> + ({ + fromBbsId: row.original.bbsId, + pstNo: row.original.pstNo, + }))} + boardList={boardList?.content ?? []} + onMovePost={movePost} + /> + + 선택삭제 + + + )} { + field.onChange(e); + setIsValidRoleId(false); + }} + className="w-[20rem] !text-[1.3rem]" + placeholder="권한ID를 입력하세요" + disabled={isModifyMode} + /> + + + {!isModifyMode && ( + + )} +
+ )} + /> + + + + 권한명 + + ( + + + + + + )} + /> + + + + 권한내용 + + ( + + + + + + )} + /> + + + + 관리권한여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + +
+ + ); +} + +export { RoleForm }; diff --git a/apps/cs/src/pages/role/ui/role-page.tsx b/apps/cs/src/pages/role/ui/role-page.tsx new file mode 100644 index 000000000..ca57084b0 --- /dev/null +++ b/apps/cs/src/pages/role/ui/role-page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, +} from "@tanstack/react-table"; +import { Button, DataTable, Input, Label } from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { RoleApi } from "../api/role-service"; +import { columns } from "../model/table-columns"; +import type { Role, RoleParams } from "../model/role-interface"; + +function RolePage() { + const router = useRouter(); + + const [roleQuery, setRoleQuery] = useState({ + keyword: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: roles } = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.ROLES, pagination], + queryFn: () => + RoleApi.getRoles({ + ...roleQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + const table = useReactTable({ + data: roles?.content ?? [], + columns, + state: { + pagination, + }, + rowCount: roles?.totalElements ?? 0, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + }; + + const handleRowClick = (row: Role) => { + router.push(`/role/register/${row.roleId}`); + }; + + return ( +
+
+
+ + + setRoleQuery({ ...roleQuery, keyword: e.target.value }) + } + /> + +
+
+
+
+
+

권한 목록

+
+ +
+
+
+
+ +
+
+
+ ); +} + +export { RolePage }; diff --git a/apps/cs/src/pages/schedule/api/schedule-service.ts b/apps/cs/src/pages/schedule/api/schedule-service.ts new file mode 100644 index 000000000..b333b43f4 --- /dev/null +++ b/apps/cs/src/pages/schedule/api/schedule-service.ts @@ -0,0 +1,39 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Schedule, + ScheduleFormData, + ScheduleParams, + ScheduleService, + ScheduleUpdateFormData, +} from "../model/schedule-interface"; + +class ScheduleApiService implements ScheduleService { + public async getSchedules(params?: ScheduleParams): Promise { + const response = await client.get( + ENDPOINT.CMS_SERVICE.SCHEDULES, + { params }, + ); + return response.data; + } + + public async getScheduleInfo(schdlSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, + ); + return response.data; + } + + public async createSchedule(data: ScheduleFormData): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SCHEDULES, data); + } + + public async updateSchedule( + schdlSn: number, + data: ScheduleUpdateFormData, + ): Promise { + await client.put(`${ENDPOINT.CMS_SERVICE.SCHEDULES}/${schdlSn}`, data); + } +} + +export const ScheduleApi = new ScheduleApiService(); diff --git a/apps/cs/src/pages/schedule/index.ts b/apps/cs/src/pages/schedule/index.ts new file mode 100644 index 000000000..07ad3f59a --- /dev/null +++ b/apps/cs/src/pages/schedule/index.ts @@ -0,0 +1,3 @@ +export { SchedulePage } from "./ui/schedule-page"; +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/schedule/lib/utils.ts b/apps/cs/src/pages/schedule/lib/utils.ts new file mode 100644 index 000000000..22aadacc4 --- /dev/null +++ b/apps/cs/src/pages/schedule/lib/utils.ts @@ -0,0 +1,67 @@ +import dayjs from "dayjs"; +import type { + RepeatDto, + ScheduleFormData, + ScheduleUpdateFormData, +} from "../model/schedule-interface"; + +const DAY_BY_BITMASK = { + 0: 1, // 일요일 + 1: 2, // 월요일 + 2: 4, // 화요일 + 3: 8, // 수요일 + 4: 16, // 목요일 + 5: 32, // 금요일 + 6: 64, // 토요일 +} as const; + +export const generateRepeatDto = ( + data: ScheduleFormData | ScheduleUpdateFormData, +) => { + const reptDto: RepeatDto = { ...data.reptDto }; + + switch (data.reptDto?.reptCycleCd) { + case "DAILY": + break; + case "WEEKLY": + const beginDay = dayjs(data.schdlBgngDt).day(); + const endDay = dayjs(data.schdlEndDt).day(); + + reptDto.reptDowVl = + beginDay === endDay + ? DAY_BY_BITMASK[beginDay] + : calculateDaysBitmask(beginDay, endDay); + break; + case "MONTHLY": + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + case "QUARTERLY": + const month = dayjs(data.schdlBgngDt).month(); + reptDto.reptQuVl = Math.floor(month / 3) + 1; + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + case "YEARLY": + reptDto.reptMmVl = dayjs(data.schdlBgngDt).month() + 1; + reptDto.reptDayVl = dayjs(data.schdlBgngDt).date(); + break; + default: + } + reptDto.reptCycleCd = data.reptDto?.reptCycleCd; + + return reptDto; +}; + +const calculateDaysBitmask = (startDay: number, endDay: number) => { + const length = ((endDay - startDay + 7) % 7) + 1; + return Array.from({ length }, (_, i) => { + const day = ((startDay + i) % 7) as keyof typeof DAY_BY_BITMASK; + return DAY_BY_BITMASK[day]; + }).reduce((acc, val) => acc + val, 0); +}; + +export const calculateEventDuration = (startDate: string, endDate: string) => { + const start = dayjs(startDate); + const end = dayjs(endDate); + const minuteDiff = end.diff(start, "minute"); + return minuteDiff; +}; diff --git a/apps/cs/src/pages/schedule/model/schedule-interface.ts b/apps/cs/src/pages/schedule/model/schedule-interface.ts new file mode 100644 index 000000000..98b6a703a --- /dev/null +++ b/apps/cs/src/pages/schedule/model/schedule-interface.ts @@ -0,0 +1,74 @@ +import { z } from "zod"; + +export const REPEAT_CYCLE_CODES = { + DAILY: "DAILY", + WEEKLY: "WEEKLY", + MONTHLY: "MONTHLY", + QUARTERLY: "QUARTERLY", + YEARLY: "YEARLY", +} as const; + +export type RepeatCycleCode = + (typeof REPEAT_CYCLE_CODES)[keyof typeof REPEAT_CYCLE_CODES]; + +export interface ScheduleParams { + siteId?: string; + schdlTtl?: string; + schdlSeCd?: string; + schdlBgngYmd?: string; + schdlEndYmd?: string; + reptYn?: string; + commSchdlYn?: string; + useYn?: string; +} + +export interface Schedule { + schdlSn: number; + siteId: string; + schdlTtl: string; + schdlSeCd: string; + schdlSeNm: string; + schdlBgngDt: string; + schdlEndDt: string; + reptYn: string; + commSchdlYn: string; +} + +const scheduleRepeatSchema = z.object({ + schdlSn: z.number().nullish(), + reptCycleCd: z.nativeEnum(REPEAT_CYCLE_CODES).nullish(), + reptDayVl: z.number().nullish(), + reptDowVl: z.number().nullish(), + reptMmVl: z.number().nullish(), + reptQuVl: z.number().nullish(), + reptEndDt: z.string().nullish(), +}); + +export const scheduleFormSchema = z.object({ + siteId: z.string(), + schdlTtl: z.string(), + schdlSeCd: z.string(), + schdlBgngDt: z.string(), + schdlEndDt: z.string(), + schdlCn: z.string().nullish(), + reptYn: z.string().nullish(), + commSchdlYn: z.string().nullish(), + useYn: z.string().nullish(), + atflCd: z.string().nullish(), + reptDto: scheduleRepeatSchema.nullish(), +}); + +export const scheduleUpdateFormSchema = scheduleFormSchema.omit({ + siteId: true, +}); + +export type ScheduleFormData = z.infer; +export type ScheduleUpdateFormData = z.infer; +export type RepeatDto = z.infer; + +export interface ScheduleService { + getSchedules(params?: ScheduleParams): Promise; + getScheduleInfo(schdlSn: number): Promise; + createSchedule(data: ScheduleFormData): Promise; + updateSchedule(schdlSn: number, data: ScheduleUpdateFormData): Promise; +} diff --git a/apps/cs/src/pages/schedule/ui/create-page.tsx b/apps/cs/src/pages/schedule/ui/create-page.tsx new file mode 100644 index 000000000..6222249bc --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/create-page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import dayjs from "dayjs"; +import { useForm } from "react-hook-form"; +import { useRouter, useSearchParams } from "next/navigation"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { SITE_ID } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + scheduleFormSchema, + type ScheduleFormData, +} from "../model/schedule-interface"; +import { generateRepeatDto } from "../lib/utils"; +import { ScheduleApi } from "../api/schedule-service"; +import { ScheduleForm } from "./schedule-form"; + +function CreatePage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const dateStr = searchParams?.get("dateStr"); + const alert = useAlertStore((state) => state.setMessage); + + const { mutateAsync: createSchedule } = useMutationWrapper({ + mutationFn: ScheduleApi.createSchedule, + onSuccess: () => { + alert("일정이 등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(scheduleFormSchema), + defaultValues: { + siteId: SITE_ID, + schdlBgngDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 00:00:00") + : undefined, + schdlEndDt: dateStr + ? dayjs(dateStr).format("YYYY-MM-DD 23:59:59") + : undefined, + reptYn: "N", + commSchdlYn: "N", + useYn: "Y", + atflCd: null, + reptDto: null, + }, + }); + + const handleSave = async (data: ScheduleFormData) => { + const reptDto = generateRepeatDto(data); + const formData = { ...data, reptDto }; + createSchedule(formData); + }; + + return ( +
+
+

스케줄 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/schedule/ui/date-time-picker.tsx b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx new file mode 100644 index 000000000..0302253b9 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/date-time-picker.tsx @@ -0,0 +1,123 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { CalendarIcon } from "lucide-react"; +import { + Button, + Calendar, + Popover, + PopoverContent, + PopoverTrigger, + ScrollArea, + ScrollBar, + cn, +} from "@common/components"; + +type DateTimePickerProps = { + date?: Date; + classes?: { trigger?: string }; + onDateChange?: (date: Date) => void; +}; + +function DateTimePicker({ date, classes, onDateChange }: DateTimePickerProps) { + const [isOpen, setIsOpen] = useState(false); + + const hours = Array.from({ length: 24 }, (_, i) => i); + const minutes = Array.from({ length: 60 }, (_, i) => i); + + const handleDateSelect = (selectedDate: Date | undefined) => { + if (!selectedDate) return; + onDateChange?.(selectedDate); + }; + + const handleTimeChange = (type: "hour" | "minute", value: string) => { + if (date) { + const newDate = new Date(date); + if (type === "hour") { + newDate.setHours(parseInt(value)); + } else if (type === "minute") { + newDate.setMinutes(parseInt(value)); + } + onDateChange?.(newDate); + } + }; + + return ( + + + + + +
+ +
+ +
+ {hours.reverse().map((hour) => ( + + ))} +
+ +
+ +
+ {minutes.map((minute) => ( + + ))} +
+ +
+
+
+
+
+ ); +} + +export { DateTimePicker }; diff --git a/apps/cs/src/pages/schedule/ui/modify-page.tsx b/apps/cs/src/pages/schedule/ui/modify-page.tsx new file mode 100644 index 000000000..e8f55534b --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/modify-page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + scheduleUpdateFormSchema, + type ScheduleUpdateFormData, +} from "../model/schedule-interface"; +import { ScheduleApi } from "../api/schedule-service"; +import { ScheduleForm } from "./schedule-form"; +import { generateRepeatDto } from "../lib/utils"; + +function ModifyPage({ schdlSn }: { schdlSn: string }) { + const router = useRouter(); + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: scheduleInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES, schdlSn], + queryFn: () => ScheduleApi.getScheduleInfo(parseInt(schdlSn)), + }); + + const { mutateAsync: updateSchedule } = useMutationWrapper({ + mutationFn: (data: ScheduleUpdateFormData) => + ScheduleApi.updateSchedule(parseInt(schdlSn), data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SCHEDULES], + }); + alert("일정이 수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(scheduleUpdateFormSchema), + values: { + ...scheduleInfo, + schdlTtl: scheduleInfo?.schdlTtl ?? "", + schdlSeCd: scheduleInfo?.schdlSeCd ?? "", + schdlBgngDt: scheduleInfo?.schdlBgngDt ?? "", + schdlEndDt: scheduleInfo?.schdlEndDt ?? "", + }, + }); + + const handleSave = async (data: ScheduleUpdateFormData) => { + const reptDto = generateRepeatDto(data); + const formData = { ...data, reptDto }; + updateSchedule(formData); + }; + + return ( +
+
+

스케줄 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/schedule/ui/schedule-form.tsx b/apps/cs/src/pages/schedule/ui/schedule-form.tsx new file mode 100644 index 000000000..98cfe6c73 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/schedule-form.tsx @@ -0,0 +1,357 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import dynamic from "next/dynamic"; +import type { Path, PathValue, UseFormReturn } from "react-hook-form"; +import { + Checkbox, + DatePicker, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { GeneralFileUploader } from "@/widgets/post"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeSelect } from "@/entities/code"; +import { FormItem, FormControl, FormField } from "@/shared/ui/form"; +import { + type ScheduleFormData, + type ScheduleUpdateFormData, +} from "../model/schedule-interface"; +import { DateTimePicker } from "./date-time-picker"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +type ScheduleFormProps = { + form: UseFormReturn; + scheduleId?: string; + handleSave: (data: T) => Promise; + handleDelete?: () => Promise; +}; + +function ScheduleForm({ + form, + scheduleId, + ...props +}: ScheduleFormProps) { + const isModifyMode = !!scheduleId; + + const [isAllDay, setIsAllDay] = useState(false); + const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); + + const handleDateTimeChange = (name: Path) => (date?: Date) => { + if (!date) return; + form.setValue( + name, + dayjs(date).format("YYYY-MM-DD HH:mm:ss") as PathValue>, + ); + setIsAllDay(false); + }; + + return ( + +
+ + + + + + + + 제목 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 일정 기간 + +
+
+ } + render={({ field }) => ( + , + )} + {...field} + /> + )} + /> + ~ + } + render={({ field }) => ( + , + )} + {...field} + /> + )} + /> +
+
+ { + setIsAllDay(checked === true); + if (checked === true) { + const schdlBgngDt = form.getValues( + "schdlBgngDt" as Path, + ); + const schdlEndDt = form.getValues( + "schdlEndDt" as Path, + ); + const allSchdlBgngDt = dayjs( + schdlBgngDt as string, + ).format("YYYY-MM-DD 00:00:00"); + const allSchdlEndDt = dayjs( + schdlEndDt as string, + ).format("YYYY-MM-DD 23:59:59"); + form.setValue( + "schdlBgngDt" as Path, + allSchdlBgngDt as PathValue>, + ); + form.setValue( + "schdlEndDt" as Path, + allSchdlEndDt as PathValue>, + ); + } + }} + /> + +
+
+
+
+ + 일정 반복 + +
+
+ } + render={({ field }) => ( + + +
+ { + field.onChange(checked ? "Y" : "N"); + }} + /> + +
+
+
+ )} + /> + } + render={({ field }) => ( + + + ) === "N"} + /> + + + )} + /> + } + render={({ field }) => ( + + + { + if (!value) return; + field.onChange(value); + form.setValue( + "reptDto.reptEndDt" as Path, + dayjs(value).format( + "YYYY-MM-DD 23:59:59", + ) as PathValue>, + ); + }} + className="w-auto px-4" + placeholder="반복 종료일 선택" + disabled={form.watch("reptYn" as Path) === "N"} + /> + + + )} + /> +
+
+
+
+ + 일정 유형 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 일정 내용 + + } + render={({ field }) => ( + + + + + + )} + /> + + + + 대상 사이트 + + } + render={({ field }) => ( + + +
+ { + field.onChange(checked ? "Y" : "N"); + }} + /> + +
+
+
+ )} + /> +
+
+ + 첨부 파일 + + + + + + 사용 여부 + + } + render={({ field }) => ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { ScheduleForm }; diff --git a/apps/cs/src/pages/schedule/ui/schedule-page.tsx b/apps/cs/src/pages/schedule/ui/schedule-page.tsx new file mode 100644 index 000000000..eba260f47 --- /dev/null +++ b/apps/cs/src/pages/schedule/ui/schedule-page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import dayjs from "dayjs"; +import { useRouter } from "next/navigation"; +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import timeGridPlugin from "@fullcalendar/timegrid"; +import type { EventClickArg } from "@fullcalendar/core"; +import interactionPlugin, { + type DateClickArg, +} from "@fullcalendar/interaction"; +import { SITE_ID } from "@/shared/config"; +import { ScheduleApi } from "../api/schedule-service"; +import { calculateEventDuration } from "../lib/utils"; + +function SchedulePage() { + const router = useRouter(); + + const handleEventClick = (info: EventClickArg) => { + router.push(`/schedule/register/${info.event.id}`); + }; + + const handleDateClick = (info: DateClickArg) => { + router.push(`/schedule/register?dateStr=${info.dateStr}`); + }; + + return ( +
+
+ { + const params = { + siteId: SITE_ID, + schdlBgngYmd: dayjs(fetchInfo.startStr).format("YYYY-MM-DD"), + schdlEndYmd: dayjs(fetchInfo.endStr).format("YYYY-MM-DD"), + useYn: "Y", + }; + + try { + const response = await ScheduleApi.getSchedules(params); + const events = response.map((item) => { + const minuteDiff = calculateEventDuration( + item.schdlBgngDt, + item.schdlEndDt, + ); + + return { + id: item.schdlSn.toString(), + title: item.schdlTtl, + start: item.schdlBgngDt, + end: item.schdlEndDt, + allDay: minuteDiff >= 1439, + classNames: ["cursor-pointer"], + }; + }); + + successCallback(events); + } catch (error) { + failureCallback(error as Error); + } + }, + ]} + locale="ko" + dayCellClassNames="cursor-pointer hover:bg-accent" + eventClick={handleEventClick} + dateClick={handleDateClick} + headerToolbar={{ + left: "prev,next", + center: "title", + right: "dayGridMonth,timeGridWeek", + }} + buttonText={{ + month: "월", + week: "주", + }} + /> +
+
+ ); +} + +export { SchedulePage }; diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx index 730a6f412..070d53a27 100644 --- a/apps/cs/src/pages/site/ui/create-page.tsx +++ b/apps/cs/src/pages/site/ui/create-page.tsx @@ -2,16 +2,15 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; -import { useMutation } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { SiteForm } from "./site-form"; function CreatePage() { const router = useRouter(); - const { setMessage: alert } = useAlertStore((state) => state); - const { mutateAsync: createSite } = useMutation({ + const alert = useAlertStore((state) => state.setMessage); + const { mutateAsync: createSite } = useMutationWrapper({ mutationFn: (data: SiteFormData) => SiteApi.createSite(data), onSuccess: () => { alert("등록되었습니다.", router.back); diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx index 12b9aee6e..f6a2e67fb 100644 --- a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useForm, useFieldArray } from "react-hook-form"; @@ -26,13 +26,13 @@ import { } from "@common/components"; import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { DomainApi } from "../api/domain-service"; import { domainFormSchema, type Domain, type DomainFormData, } from "../model/domain-interface"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; type DomainManagementDialogProps = { siteId: string; @@ -47,7 +47,7 @@ function DomainManagementDialog({ ...props }: DomainManagementDialogProps) { const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { data: domainList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.DOMAINS, props.open], @@ -63,7 +63,7 @@ function DomainManagementDialog({ }, }); - const { mutateAsync } = useMutation({ + const { mutateAsync } = useMutationWrapper({ mutationFn: (body: Domain[]) => DomainApi.saveDomainList(body), }); diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index e17577d16..df682f06f 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useForm, useFieldArray } from "react-hook-form"; @@ -24,11 +24,11 @@ import { Checkbox, Label, } from "@common/components"; -import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; import { IpApi } from "../api/ip-service"; import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; type IPManagementDialogProps = { siteId: string; @@ -43,7 +43,7 @@ function IPManagementDialog({ ...props }: IPManagementDialogProps) { const { confirm } = useConfirm(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { data: ipList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.IPS, props.open], @@ -59,7 +59,7 @@ function IPManagementDialog({ }, }); - const { mutateAsync } = useMutation({ + const { mutateAsync } = useMutationWrapper({ mutationFn: (body: IP[]) => IpApi.saveIpList(body), }); diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index cf2fa2200..5f162d368 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -3,16 +3,16 @@ import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib/use-alert-store"; import { SiteForm } from "./site-form"; function ModifyPage({ siteId }: { siteId: string }) { const router = useRouter(); const queryClient = useQueryClient(); - const { setMessage: alert } = useAlertStore((state) => state); + const alert = useAlertStore((state) => state.setMessage); const { data: siteDetail } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId], @@ -31,7 +31,7 @@ function ModifyPage({ siteId }: { siteId: string }) { enabled: !!siteId, }); - const { mutateAsync: updateSite } = useMutation({ + const { mutateAsync: updateSite } = useMutationWrapper({ mutationFn: (data: SiteFormData) => SiteApi.updateSite(siteId, data), onSuccess: () => { alert("수정되었습니다.", () => { @@ -42,7 +42,7 @@ function ModifyPage({ siteId }: { siteId: string }) { }, }); - const { mutateAsync: deleteSite } = useMutation({ + const { mutateAsync: deleteSite } = useMutationWrapper({ mutationFn: () => SiteApi.deleteSite(siteId), onSuccess: () => { alert("삭제되었습니다.", router.back); diff --git a/apps/cs/src/pages/site/ui/site-page.tsx b/apps/cs/src/pages/site/ui/site-page.tsx index 9ffd1294e..5a22d9107 100644 --- a/apps/cs/src/pages/site/ui/site-page.tsx +++ b/apps/cs/src/pages/site/ui/site-page.tsx @@ -23,7 +23,7 @@ function SitePage() { }); const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 5, + pageSize: 10, }); const { data: siteList, refetch } = useQuery({ @@ -85,7 +85,7 @@ function SitePage() {

사이트 목록

-
+
diff --git a/apps/cs/src/pages/survey/api/survey-service.ts b/apps/cs/src/pages/survey/api/survey-service.ts new file mode 100644 index 000000000..6db4ea52d --- /dev/null +++ b/apps/cs/src/pages/survey/api/survey-service.ts @@ -0,0 +1,120 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + SurveyService, + SurveyList, + SurveyParams, + SurveyDetail, + SurveyFormData, + Question, + QuestionList, + QuestionParams, + QuestionFormData, + Article, + ArticleList, + ArticleParams, + UserSurvey, + UserSurveyFormData, + UserSurveyResultParams, + UserSurveyResult, +} from "../model/survey-interface"; + +export class SurveyApiService implements SurveyService { + public async getSurveyList(params: SurveyParams): Promise { + const response = await client.get(ENDPOINT.CMS_SERVICE.SURVEY, { + params, + }); + return response.data; + } + + public async getSurveyInfo(srvySn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY}/${srvySn}`, + ); + return response.data; + } + + public async createSurvey(data: SurveyFormData): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SURVEY, data); + } + + public async updateSurvey( + srvySn: number, + data: SurveyFormData, + ): Promise { + await client.put(`${ENDPOINT.CMS_SERVICE.SURVEY}/${srvySn}`, data); + } + + public async getQuestionList(params: QuestionParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}`, + { params }, + ); + return response.data; + } + + public async getQuestionInfo(srvyQitemNo: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`, + ); + return response.data; + } + + public async createQuestion( + data: QuestionFormData & { srvySn: number }, + ): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, data); + } + + public async updateQuestion( + srvyQitemNo: number, + data: QuestionFormData, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`, + data, + ); + } + + public async deleteQuestion(srvyQitemNo: number): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.SURVEY_QUESTION}/${srvyQitemNo}`, + ); + } + + public async getArticleList(params: ArticleParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE}`, + { params }, + ); + return response.data; + } + + public async saveArticleList(data: Article[]): Promise { + await client.post(ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, data); + } + + public async getUserSurvey(srvySn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY}/user/${srvySn}`, + ); + return response.data; + } + + public async saveUserSurvey(data: UserSurveyFormData): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.SURVEY}/user`, data); + } + + public async getUserSurveyResult( + srvySn: number, + params: UserSurveyResultParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.SURVEY}/rslt/stat/${srvySn}`, + { params }, + ); + return response.data; + } +} + +export const SurveyApi = new SurveyApiService(); diff --git a/apps/cs/src/pages/survey/index.ts b/apps/cs/src/pages/survey/index.ts new file mode 100644 index 000000000..6247debfb --- /dev/null +++ b/apps/cs/src/pages/survey/index.ts @@ -0,0 +1,6 @@ +export { SurveyPage } from "./ui/survey-page"; +export { CreatePage } from "./ui/create-page"; +export { DetailPage } from "./ui/detail-page"; +export { ModifyPage } from "./ui/modify-page"; +export { ViewSurveyArticle } from "./ui/view-survey-article"; +export { ViewSurveyResult } from "./ui/view-survey-result"; diff --git a/apps/cs/src/pages/survey/model/posting-table-columns.tsx b/apps/cs/src/pages/survey/model/posting-table-columns.tsx new file mode 100644 index 000000000..cf4fb2bb0 --- /dev/null +++ b/apps/cs/src/pages/survey/model/posting-table-columns.tsx @@ -0,0 +1,45 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components"; +import type { Post } from "@/entities/posting"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + meta: { + width: 50, + }, + }, + { + accessorKey: "siteNm", + header: "사이트", + }, + { + accessorKey: "pstgPrdSeNm", + header: "게시기간", + cell: ({ row }) => { + const pstgPrdSeNm = row.original.pstgPrdSeNm; + const pstgBgngDt = row.original.pstgBgngDt; + const pstgEndDt = row.original.pstgEndDt; + return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; + }, + }, +]; diff --git a/apps/cs/src/pages/survey/model/question-table-columns.tsx b/apps/cs/src/pages/survey/model/question-table-columns.tsx new file mode 100644 index 000000000..357b25adb --- /dev/null +++ b/apps/cs/src/pages/survey/model/question-table-columns.tsx @@ -0,0 +1,116 @@ +import { memo } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import type { ColumnDef } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { Button } from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib"; +import type { Question } from "./survey-interface"; +import { SurveyApi } from "../api/survey-service"; +import { QuestionEditDialog } from "../ui/question-edit-dialog"; +import { ArticleManagementDialog } from "../ui/article-management-dialog"; + +const QUESTION_TYPE = { + RADIO: "객관식", + CHECKBOX: "다중응답", + INPUT: "주관식", +} as const; + +export const columns: ColumnDef[] = [ + { + accessorKey: "qstnSeq", + header: "문항순번", + meta: { + width: 80, + }, + }, + { + accessorKey: "qstnCn", + header: "문항내용", + }, + { + accessorKey: "qstnTypeCd", + header: "문항유형", + cell: ({ row }) => ( + + {QUESTION_TYPE[row.original.qstnTypeCd]} + + ), + meta: { + width: 100, + }, + }, + { + accessorKey: "maxChcCnt", + header: "최대선택갯수", + meta: { + width: 100, + }, + }, + { + accessorKey: "etcAnsYn", + header: "기타답변여부", + meta: { + width: 100, + }, + }, + { + id: "action", + header: "관리", + cell: ({ row }) => ( + + ), + meta: { + width: 240, + }, + }, +]; + +type ActionCellProps = { + srvySn: number; + srvyQitemNo: number; + qstnCn: string; +}; + +const ActionCell = memo( + ({ srvySn, srvyQitemNo, qstnCn }: ActionCellProps) => { + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const onDelete = async () => { + if (await confirm("문항을 삭제하시겠습니까?")) { + try { + await SurveyApi.deleteQuestion(srvyQitemNo); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()], + }); + alert("삭제되었습니다."); + } catch (error) { + console.error(error); + } + } + }; + + return ( +
+ + + +
+ ); + }, + (prevProps, nextProps) => { + return prevProps.srvyQitemNo === nextProps.srvyQitemNo; + }, +); + +ActionCell.displayName = "ActionCell"; + +export { ActionCell }; diff --git a/apps/cs/src/pages/survey/model/survey-interface.ts b/apps/cs/src/pages/survey/model/survey-interface.ts new file mode 100644 index 000000000..b7d8f6027 --- /dev/null +++ b/apps/cs/src/pages/survey/model/survey-interface.ts @@ -0,0 +1,195 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const QUESTION_TYPE_CODE = { + RADIO: "RADIO", + CHECKBOX: "CHECKBOX", + INPUT: "INPUT", +} as const; + +export type QuestionTypeCode = + (typeof QUESTION_TYPE_CODE)[keyof typeof QUESTION_TYPE_CODE]; + +export interface SurveyParams extends PageableParams { + keywordType?: string; + keyword?: string; + srvyTtl?: string; + srvyPrps?: string; + srvyTrgt?: string; + srvyWrtGdCn?: string; + delYn?: string; +} + +export interface QuestionParams extends PageableParams { + keywordType?: string; + keyword?: string; + srvySn?: number; + qstnTypeCd?: QuestionTypeCode; + qstnCn?: string; + etcAnsYn?: string; + useYn?: string; +} + +export interface ArticleParams extends PageableParams { + keywordType?: string; + keyword?: string; + srvyQitemNo?: number; + artclCn?: string; + delYn?: string; +} + +export interface UserSurveyResultParams { + keywordType?: string; + keyword?: string; + siteId?: string; +} + +export interface Survey { + srvySn: number; + srvyTtl: string; + srvyPrps: string; + srvyTrgt: string; + srvyTrgtNm: string; + delYn: string; + regDt: string; + rgTrNm: string; +} + +export interface SurveyDetail extends Survey { + srvyWrtGdCn: string; +} + +export interface Question { + srvyQitemNo: number; + srvySn: number; + qstnSeq: number; + qstnTypeCd: QuestionTypeCode; + qstnTypeNm: string; + qstnCn: string; + maxChcCnt: number; + etcAnsYn: string; + regDt: string; + rgTrNm: string; +} + +export const surveyFormSchema = z.object({ + srvyTtl: z.string(), + srvyPrps: z.string(), + srvyTrgt: z.string().nullish(), + srvyWrtGdCn: z.string().nullish(), +}); + +export const questionFormSchema = z.object({ + qstnSeq: z.number(), + qstnTypeCd: z.nativeEnum(QUESTION_TYPE_CODE), + qstnCn: z.string(), + maxChcCnt: z.number().nullish(), + etcAnsYn: z.string().nullish(), +}); + +export const articleSchema = z.object({ + srvyArtclNo: z.number().nullish(), + srvyQitemNo: z.number(), + artclSeq: z.number(), + artclCn: z.string(), + mode: z.string(), +}); + +export const userSurveyFormSchema = z.object({ + srvySn: z.number(), + rspdnts: z.object({ + rspdntNm: z.string().min(1), + gndrCd: z.string().min(1), + crTypeCd: z.string().min(1), + brdt: z.string().min(8), + }), + rspnsRslts: z.array( + z + .object({ + srvyQitemNo: z.number(), + qstnTypeCd: z.string(), + ansCn: z.string(), + etcAnsCn: z.string(), + }) + .refine( + (data) => + data.etcAnsCn.trim().length > 0 || data.ansCn.trim().length > 0, + ), + ), +}); + +const inputResultSchema = z.object({ + rspdntNm: z.string(), + ansCn: z.string(), + ansDt: z.string(), +}); + +const articleWithResultSchema = articleSchema.omit({ mode: true }).extend({ + inputRslts: z.array(inputResultSchema), + rspnsCount: z.number(), + rspnsRate: z.number(), +}); + +export const questionWithArticlesSchema = questionFormSchema.extend({ + srvyQitemNo: z.number(), + articles: z.array(articleWithResultSchema), +}); + +const respondentStateSchema = z.object({ + ageN20대: z.number(), + ageN30대: z.number(), + ageN40대: z.number(), + ageN50대: z.number(), + ageN60대이상: z.number(), + job학생: z.number(), + job대학생: z.number(), + job회사원: z.number(), + job공단직원: z.number(), + job공무원: z.number(), + job기타: z.number(), + mf남성: z.number(), + mf여성: z.number(), + 구분: z.string(), +}); + +export const userSurveyResultSchema = surveyFormSchema.extend({ + srvySn: z.number(), + srvyPeriodFrTo: z.string(), + qitems: z.array(questionWithArticlesSchema), + srvyRspntStatList: z.array(respondentStateSchema), + totalRspdnt: z.number(), +}); + +export type SurveyList = PageableList; +export type SurveyFormData = z.infer; +export type QuestionList = PageableList; +export type QuestionFormData = z.infer; +export type Article = z.infer; +export type ArticleList = PageableList
; +export type UserSurvey = SurveyDetail & { + qitems: (Question & { + articles: Article[]; + })[]; +}; +export type UserSurveyFormData = z.infer; +export type UserSurveyResult = z.infer; + +export interface SurveyService { + getSurveyList(params: SurveyParams): Promise; + getSurveyInfo(srvySn: number): Promise; + createSurvey(data: SurveyFormData): Promise; + updateSurvey(srvySn: number, data: SurveyFormData): Promise; + getQuestionList(params: QuestionParams): Promise; + getQuestionInfo(srvyQitemNo: number): Promise; + createQuestion(data: QuestionFormData): Promise; + updateQuestion(srvyQitemNo: number, data: QuestionFormData): Promise; + deleteQuestion(srvyQitemNo: number): Promise; + getArticleList(params: ArticleParams): Promise; + saveArticleList(data: Article[]): Promise; + getUserSurvey(srvySn: number): Promise; + saveUserSurvey(data: UserSurveyFormData): Promise; + getUserSurveyResult( + srvySn: number, + params: UserSurveyResultParams, + ): Promise; +} diff --git a/apps/cs/src/pages/survey/model/survey-table-columns.tsx b/apps/cs/src/pages/survey/model/survey-table-columns.tsx new file mode 100644 index 000000000..a498d1859 --- /dev/null +++ b/apps/cs/src/pages/survey/model/survey-table-columns.tsx @@ -0,0 +1,109 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { Button, Checkbox } from "@common/components"; +import type { Survey } from "./survey-interface"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + meta: { + width: 50, + }, + }, + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const { pageIndex, pageSize } = table.getState().pagination; + const totalRows = table.getRowCount(); + return totalRows - (pageIndex * pageSize + row.index); + }, + meta: { + width: 50, + }, + }, + { + accessorKey: "srvyTtl", + header: "설문제목", + }, + { + accessorKey: "srvyTrgtNm", + header: "설문대상", + meta: { + width: 120, + }, + }, + { + id: "preview", + header: "설문 보기", + cell: ({ row }) => ( + + ), + meta: { + width: 100, + }, + }, + { + id: "result", + header: "설문 결과", + cell: ({ row }) => ( + + ), + meta: { + width: 100, + }, + }, +]; diff --git a/apps/cs/src/pages/survey/ui/article-management-dialog.tsx b/apps/cs/src/pages/survey/ui/article-management-dialog.tsx new file mode 100644 index 000000000..38225d092 --- /dev/null +++ b/apps/cs/src/pages/survey/ui/article-management-dialog.tsx @@ -0,0 +1,281 @@ +import { z } from "zod"; +import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm, useFieldArray } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + Dialog, + DialogTrigger, + Button, + DialogContent, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Input, + DialogFooter, + DialogClose, + Checkbox, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; +import { SurveyApi } from "../api/survey-service"; +import { articleSchema, type Article } from "../model/survey-interface"; + +type ArticleFormData = { + articles: Article[]; +}; + +function ArticleManagementDialog({ + srvyQitemNo, + qstnCn, +}: { + srvyQitemNo: number; + qstnCn: string; +}) { + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const [open, setOpen] = useState(false); + + const { data: articleList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, srvyQitemNo], + queryFn: () => SurveyApi.getArticleList({ srvyQitemNo }), + select: (data) => { + const articles = data.content.map((item) => ({ + ...item, + mode: "U", + })); + + initialForm(); + return articles; + }, + enabled: !!srvyQitemNo && !!open, + }); + + const { mutateAsync } = useMutationWrapper({ + mutationFn: (body: Article[]) => SurveyApi.saveArticleList(body), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_ARTICLE, srvyQitemNo], + }); + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver( + z.object({ + articles: articleSchema.array(), + }), + ), + values: { + articles: articleList ?? [], + }, + }); + + const { fields, append, remove, update } = useFieldArray({ + control: form.control, + name: "articles", + }); + + const [selectedRows, setSelectedRows] = useState([]); + + const initialForm = () => { + form.reset(); + setSelectedRows([]); + }; + + const onSubmit = async (data: ArticleFormData) => { + if (await confirm("저장하시겠습니까?")) { + await mutateAsync(data.articles); + alert("저장되었습니다."); + } + }; + + const handleAddArticle = () => { + const maxSeq = + fields.length > 0 + ? Math.max(...fields.map((field) => field.artclSeq)) + : 0; + + append({ + srvyArtclNo: null, + srvyQitemNo, + artclSeq: maxSeq + 1, + artclCn: "", + mode: "C", + }); + }; + + const handleSelectAll = () => { + if (selectedRows.length === fields.length) { + setSelectedRows([]); + } else { + setSelectedRows(fields.map((_, index) => index)); + } + }; + + const handleSelectRow = (i: number) => { + setSelectedRows((prev) => + prev.includes(i) ? prev.filter((j) => j !== i) : [...prev, i], + ); + }; + + const handleDeleteArticle = async () => { + if (selectedRows.length === 0) return; + + const sortedIndices = [...selectedRows].sort((a, b) => b - a); + sortedIndices.forEach((i) => { + const field = fields[i]; + if (field?.srvyArtclNo) { + update(i, { + ...field, + mode: "D", + }); + } else { + remove(i); + } + }); + + setSelectedRows([]); + }; + + const isAllSelected = + fields.length > 0 && selectedRows.length === fields.length; + + return ( + + + + + + + 답변 항목 관리 + +
+ { + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + > +
+
{qstnCn}
+
+ + +
+
+
+ + + + + + + + + + + + 답변 순번 + 답변 내용 + + + + {fields.map((field, i) => { + if (field.mode === "D") return null; + return ( + + + handleSelectRow(i)} + /> + + + ( + + + + field.onChange(parseInt(e.target.value)) + } + /> + + + )} + /> + + + ( + + + + + + )} + /> + + + ); + })} + +
+
+ + + + + + +
+ +
+
+ ); +} + +export { ArticleManagementDialog }; diff --git a/apps/cs/src/pages/survey/ui/create-page.tsx b/apps/cs/src/pages/survey/ui/create-page.tsx new file mode 100644 index 000000000..867c2d749 --- /dev/null +++ b/apps/cs/src/pages/survey/ui/create-page.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + surveyFormSchema, + type SurveyFormData, +} from "../model/survey-interface"; +import { SurveyApi } from "../api/survey-service"; +import { SurveyForm } from "./survey-form"; + +function CreatePage() { + const router = useRouter(); + const alert = useAlertStore((state) => state.setMessage); + + const { mutateAsync: createSurvey } = useMutationWrapper({ + mutationFn: (data: SurveyFormData) => SurveyApi.createSurvey(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(surveyFormSchema), + }); + + return ( +
+
+

설문 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/survey/ui/detail-page.tsx b/apps/cs/src/pages/survey/ui/detail-page.tsx new file mode 100644 index 000000000..464a49f35 --- /dev/null +++ b/apps/cs/src/pages/survey/ui/detail-page.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type RowSelectionState, +} from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { Button, DataTable } from "@common/components"; +import { PostingApi, type PostPayload } from "@/entities/posting"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { SurveyApi } from "../api/survey-service"; +import { columns as postingColumns } from "../model/posting-table-columns"; +import { columns as questionColumns } from "../model/question-table-columns"; +import { QuestionAddDialog } from "./question-add-dialog"; + +function DetailPage({ srvySn }: { srvySn: string }) { + const router = useRouter(); + const pathname = usePathname(); + const { confirm } = useConfirm(); + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const [rowSelection, setRowSelection] = useState({}); + const [questionAddDialogOpen, setQuestionAddDialogOpen] = useState(false); + + const { data: surveyInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, srvySn], + queryFn: () => SurveyApi.getSurveyInfo(parseInt(srvySn)), + }); + + const { data: postHistory } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, srvySn], + queryFn: () => + PostingApi.getPostingHistory({ pstgTrgtNo: parseInt(srvySn) }), + }); + + const { data: questionList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn], + queryFn: () => SurveyApi.getQuestionList({ srvySn: parseInt(srvySn) }), + }); + + const { mutateAsync: savePostingHistory } = useMutationWrapper({ + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, srvySn], + }); + alert("삭제되었습니다."); + }, + }); + + const postingTable = useReactTable({ + data: postHistory?.content ?? [], + columns: postingColumns, + state: { + rowSelection, + }, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const questionTable = useReactTable({ + data: questionList?.content ?? [], + columns: questionColumns, + state: { + rowSelection, + }, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const handleDeletePostingHistory = async () => { + const data = postingTable.getFilteredSelectedRowModel().rows.map((row) => ({ + ...row.original, + mode: "D", + })); + + if (await confirm("삭제하시겠습니까?")) { + savePostingHistory(data); + setRowSelection({}); + } + }; + + return ( +
+
+
+

설문 상세

+
+ + + +
+
+
    +
  • + 설문 제목 +

    {surveyInfo?.srvyTtl}

    +
  • +
  • + 설문 목적 +

    {surveyInfo?.srvyPrps}

    +
  • +
  • + + 설문 작성 안내 내용 + +

    {surveyInfo?.srvyWrtGdCn}

    +
  • +
  • + 설문 대상 +

    {surveyInfo?.srvyTrgtNm}

    +
  • +
+
+
+
+

사이트 게시 이력

+ +
+
+ +
+
+
+
+

설문 문항

+ +
+
+ +
+
+
+ ); +} + +export { DetailPage }; diff --git a/apps/cs/src/pages/survey/ui/modify-page.tsx b/apps/cs/src/pages/survey/ui/modify-page.tsx new file mode 100644 index 000000000..d5a6cd964 --- /dev/null +++ b/apps/cs/src/pages/survey/ui/modify-page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { + surveyFormSchema, + type SurveyFormData, +} from "../model/survey-interface"; +import { SurveyApi } from "../api/survey-service"; +import { SurveyForm } from "./survey-form"; + +function ModifyPage({ srvySn }: { srvySn: string }) { + const router = useRouter(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: surveyInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, srvySn], + queryFn: () => SurveyApi.getSurveyInfo(parseInt(srvySn)), + enabled: !!srvySn, + }); + + const { mutateAsync: updateSurvey } = useMutationWrapper({ + mutationFn: (data: SurveyFormData) => + SurveyApi.updateSurvey(parseInt(srvySn), data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(surveyFormSchema), + values: { + ...surveyInfo, + srvyTtl: surveyInfo?.srvyTtl ?? "", + srvyPrps: surveyInfo?.srvyPrps ?? "", + }, + }); + + return ( +
+
+
+

팝업 수정

+
+ +
+
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/survey/ui/question-add-dialog.tsx b/apps/cs/src/pages/survey/ui/question-add-dialog.tsx new file mode 100644 index 000000000..4af6e6a4f --- /dev/null +++ b/apps/cs/src/pages/survey/ui/question-add-dialog.tsx @@ -0,0 +1,90 @@ +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + questionFormSchema, + type QuestionFormData, +} from "../model/survey-interface"; +import { QuestionForm } from "./question-form"; +import { SurveyApi } from "../api/survey-service"; + +type QuestionAddDialogProps = { + srvySn: number; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +function QuestionAddDialog({ + srvySn, + open, + onOpenChange, + ...props +}: QuestionAddDialogProps) { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const { mutateAsync: createQuestion } = useMutationWrapper({ + mutationFn: (data: QuestionFormData) => + SurveyApi.createQuestion({ ...data, srvySn }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()], + }); + alert("문항이 추가되었습니다."); + onOpenChange?.(false); + }, + }); + + const form = useForm({ + resolver: zodResolver(questionFormSchema), + defaultValues: { maxChcCnt: 0, etcAnsYn: "N" }, + }); + + useEffect(() => { + if (!open) form.reset({ maxChcCnt: 0, etcAnsYn: "N" }); + }, [open]); + + return ( + + + + + + + 설문 문항 추가 + + + + + + + + } + /> + + + ); +} + +export { QuestionAddDialog }; diff --git a/apps/cs/src/pages/survey/ui/question-edit-dialog.tsx b/apps/cs/src/pages/survey/ui/question-edit-dialog.tsx new file mode 100644 index 000000000..6d722f738 --- /dev/null +++ b/apps/cs/src/pages/survey/ui/question-edit-dialog.tsx @@ -0,0 +1,98 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { + questionFormSchema, + type QuestionTypeCode, + type QuestionFormData, +} from "../model/survey-interface"; +import { SurveyApi } from "../api/survey-service"; +import { QuestionForm } from "./question-form"; + +type QuestionEditDialogProps = { + srvySn: number; + srvyQitemNo: number; +}; + +function QuestionEditDialog({ + srvySn, + srvyQitemNo, + ...props +}: QuestionEditDialogProps) { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const [open, setOpen] = useState(false); + + const { data: questionInfo } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvyQitemNo], + queryFn: () => SurveyApi.getQuestionInfo(srvyQitemNo), + enabled: open, + }); + + const { mutateAsync: updateQuestion } = useMutationWrapper({ + mutationFn: (data: QuestionFormData) => + SurveyApi.updateQuestion(srvyQitemNo, data), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.SURVEY_QUESTION, srvySn.toString()], + }); + alert("문항이 수정되었습니다."); + setOpen(false); + }, + }); + + const form = useForm({ + resolver: zodResolver(questionFormSchema), + values: { + ...questionInfo, + qstnSeq: questionInfo?.qstnSeq ?? 0, + qstnTypeCd: questionInfo?.qstnTypeCd as QuestionTypeCode, + qstnCn: questionInfo?.qstnCn ?? "", + }, + }); + + return ( + + + + + + + 설문 문항 수정 + + + + + + + + } + handleSave={updateQuestion} + /> + + + ); +} + +export { QuestionEditDialog }; diff --git a/apps/cs/src/pages/survey/ui/question-form.tsx b/apps/cs/src/pages/survey/ui/question-form.tsx new file mode 100644 index 000000000..3ea196f8c --- /dev/null +++ b/apps/cs/src/pages/survey/ui/question-form.tsx @@ -0,0 +1,176 @@ +import type { UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; +import type { QuestionFormData } from "../model/survey-interface"; + +type QuestionFormProps = { + form: UseFormReturn; + actionSlot?: React.ReactNode; + handleSave: (data: QuestionFormData) => Promise; +}; + +function QuestionForm({ form, actionSlot, handleSave }: QuestionFormProps) { + const qstnTypeCd = form.watch("qstnTypeCd"); + + const { confirm } = useConfirm(); + + const onSubmit = async (data: QuestionFormData) => { + if (await confirm("저장하시겠습니까?")) handleSave(data); + }; + + return ( +
+ + + + + + + + + 문항순번 + + ( + + + + field.onChange(parseInt(e.target.value)) + } + className="w-[12rem] !text-[1.3rem]" + placeholder="문항순번을 입력하십시오." + /> + + + )} + /> + + + + 문항유형 + + ( + + + + + + )} + /> + + + + 문항내용 + + ( + + +