This commit is contained in:
2025-06-12 10:31:02 +09:00
234 changed files with 8907 additions and 816 deletions
@@ -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);
@@ -8,7 +8,7 @@ export default function PostLayout({
}: {
children: React.ReactNode;
}) {
const { bbsInfo } = useMenuStore((state) => state);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
return (
<PageLayout
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "features",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 13000",
@@ -9,13 +9,9 @@ import { StoreProvider, PromptProvider } 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 (
<QueryClientProvider client={queryClient}>
<StoreProvider>
@@ -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],
@@ -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}</>;
};
@@ -9,3 +9,4 @@ export {
type PostFormData,
type PostUpdateFormData,
} from "./model/post-interface";
export { PostPageGuard } from "./lib/post-page-guard";
@@ -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 };
@@ -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<number[]>([]);
const [rememberPeriodCode, setRememberPeriodCode] = useState<
Record<string, string>
@@ -1,2 +1,3 @@
export { SiteApi } from "../api/site-service";
export type { Site } from "../model/site-interface";
export { SiteSelect } from "../ui/site-select";
@@ -5,3 +5,5 @@ export {
type Site,
type SiteFormData,
} from "./model/site-interface";
export { SiteSelect } from "./ui/site-select";
@@ -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<typeof Select>;
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 (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{sites?.content.map((item) => (
<SelectItem key={item.siteId} value={item.siteId}>
{item.siteNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { SiteSelect };
@@ -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),
@@ -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<PostParams>({
@@ -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],
@@ -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),
@@ -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],
@@ -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),
@@ -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],
@@ -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),
@@ -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],
@@ -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),
@@ -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],
@@ -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 = () => {
@@ -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<T extends PostFormData | PostUpdateFormData>({
function GeneralFileUploader<T extends { atflCd?: string | null }>({
form,
attachmentData,
deleteAttachmentIds,
@@ -18,8 +17,8 @@ function GeneralFileUploader<T extends PostFormData | PostUpdateFormData>({
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<Record<string, Attachment>>(
{},
@@ -89,10 +88,10 @@ function GeneralFileUploader<T extends PostFormData | PostUpdateFormData>({
<FileUploader
id="atflCd"
multiple
className="flex !h-auto max-w-full flex-row-reverse gap-4 [&>label]:h-[3.2rem]"
className="flex !h-auto max-w-full flex-row-reverse items-start gap-4 [&>label]:h-[3.2rem]"
onUpload={onUpload}
customPreview={
<div className="flex w-full gap-4">
<div className="flex w-full flex-wrap gap-4">
{Object.entries(attachments).map(([atflId, attachment]) => {
if (deleteAttachmentIds.includes(atflId)) return null;
return (
@@ -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();
@@ -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<number | null>(null);
const [editValue, setEditValue] = useState<string>("");
@@ -40,8 +40,8 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
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<string[]>([]);
@@ -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<Map<number, string>>(new Map());
const [postParams, setPostParams] = useState<PostParams>({
keywordType: "title",
@@ -46,8 +52,6 @@ function PostPage({ bbsId }: { bbsId: string }) {
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const thumbRefs = useRef<Map<number, string>>(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 (
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onPostDelete}
disabled={!table.getFilteredSelectedRowModel().rows.length}
<PostPageGuard bbsId={bbsId}>
<div className="card card-border !mt-6">
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2 text-label">검색</Label>
<Select
value={postParams.keywordType}
onValueChange={(keywordType) => {
setPostParams({ ...postParams, keywordType });
}}
>
<SelectTrigger className="h-[3.2rem] w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="all">제목+내용</SelectItem>
<SelectItem value="writer">작성자</SelectItem>
</SelectContent>
</Select>
<Input
value={postParams.keyword}
onChange={(e) =>
setPostParams({ ...postParams, keyword: e.target.value })
}
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
</form>
</div>
<div className="card card-border">
<div className="card-header">
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onPostDelete}
disabled={!table.getFilteredSelectedRowModel().rows.length}
>
선택삭제
</Button>
)}
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
>
선택삭제
</Button>
)}
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(parseInt(value));
}}
>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}개씩 보기
</SelectItem>
))}
</SelectContent>
</Select>
<SelectTrigger className="h-[3.2rem] w-[12rem]">
<SelectValue placeholder="페이지 크기" />
</SelectTrigger>
<SelectContent>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}개씩 보기
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
{isFetched ? (
<DataTableComponent
table={table}
onRowClick={handleRowClick}
viewPagination
/>
) : (
<Skeleton className="h-[36rem] w-full" />
)}
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
{isLoading ? (
<Skeleton className="h-[36rem] w-full" />
) : (
<DataTableComponent
table={table}
onRowClick={handleRowClick}
viewPagination
/>
)}
</div>
</div>
</div>
</div>
</PostPageGuard>
);
}
@@ -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 }) {
<video src={videoUrl} controls className="object-contain" />
</div>
)}
<pre className="font-noto min-h-[40rem] whitespace-pre-wrap p-8 text-2xl tracking-tighter text-foreground">
<pre className="min-h-[40rem] whitespace-pre-wrap p-8 font-noto text-2xl tracking-tighter text-foreground">
{postData?.pstCn ?? ""}
</pre>
{isFetching ? (
@@ -16,8 +16,8 @@ function WithPreviewFileUploader<T extends PostFormData | PostUpdateFormData>({
form: UseFormReturn<T>;
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);
@@ -0,0 +1,2 @@
import { CodePage } from "@/pages/code";
export default CodePage;
+1 -1
View File
@@ -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);
@@ -0,0 +1,2 @@
import { PopupPage } from "@/pages/popup";
export default PopupPage;
@@ -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 <VModifyPage popupSn={parseInt(popupSn)} />;
}
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/popup";
export default CreatePage;
@@ -0,0 +1,2 @@
import { RolePage } from "@/pages/role";
export default RolePage;
@@ -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 <VMModifyPage roleId={roleId} />;
}
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/role";
export default CreatePage;
@@ -0,0 +1,2 @@
import { SchedulePage } from "@/pages/schedule";
export default SchedulePage;
@@ -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 <VModifyPage schdlSn={schdlSn} />;
}
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/schedule";
export default CreatePage;
@@ -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 <VModifyPage srvySn={srvySn} />;
}
@@ -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 <VDetailPage srvySn={srvySn} />;
}
@@ -0,0 +1,2 @@
import { SurveyPage } from "@/pages/survey";
export default SurveyPage;
@@ -0,0 +1,2 @@
import { CreatePage } from "@/pages/survey";
export default CreatePage;
@@ -0,0 +1,2 @@
import { UsersPage } from "@/pages/users";
export default UsersPage;
@@ -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 <VViewPopup popupSn={popupSn} />;
}
@@ -0,0 +1,10 @@
import { ViewSurveyArticle } from "@/pages/survey";
export default async function ViewSurveyArticlePage({
params,
}: {
params: Promise<{ srvySn: string }>;
}) {
const { srvySn } = await params;
return <ViewSurveyArticle srvySn={srvySn} />;
}
@@ -0,0 +1,10 @@
import { ViewSurveyResult } from "@/pages/survey";
export default async function ViewSurveyResultPage({
params,
}: {
params: Promise<{ srvySn: string }>;
}) {
const { srvySn } = await params;
return <ViewSurveyResult srvySn={srvySn} />;
}
+8 -1
View File
@@ -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",
+4 -4
View File
@@ -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 (
<QueryClientProvider client={queryClient}>
<StoreProvider>
@@ -38,6 +35,9 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
};
@@ -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<AuthorizationsList> {
const response = await client.get<AuthorizationsList>(
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}`,
@@ -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";
@@ -47,7 +47,7 @@ export type AuthorizationFormData = z.infer<typeof authorizationFormSchema>;
export interface AuthorizationsService {
getAuthorizationsList: (
params: AuthorizationsParams,
params?: AuthorizationsParams,
) => Promise<AuthorizationsList>;
getAuthorizationInfo: (
authorizationNo: string,
+1
View File
@@ -0,0 +1 @@
export { BoardApi } from "../api/board-service";
@@ -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",
@@ -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<LwpoCodeList> {
const response = await client.get<LwpoCodeList>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
{ params },
);
return response.data;
}
public async getUpCodeLwpoCodeList(upCd: string): Promise<LwpoCodeList> {
const response = await client.get<LwpoCodeList>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/up-cd/${upCd}`,
);
return response.data;
}
public async lwpoCodeDetail(
upCd: string,
lwpoCd: string,
): Promise<LwpoCodeDetail> {
const response = await client.get<LwpoCodeDetail>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
);
return response.data;
}
public async createLwpoCode(data: LwpoCodeFormData): Promise<void> {
const validatedData = LwpoCodeFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}`,
validatedData,
);
return response.data;
}
public async updateLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void> {
const validatedData = LwpoCodeFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
validatedData,
);
return response.data;
}
public async deleteLwpoCode(upCd: string, lwpoCd: string): Promise<void> {
await client.delete<void>(
`${ENDPOINT.CMS_SERVICE.LWPO_CODE}/${lwpoCd}/up-cd/${upCd}`,
);
}
}
export const LwpoCodeApi = new LwpoCodeApiService();
@@ -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<UpCodeList> {
const response = await client.get<UpCodeList>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}`,
{ params },
);
return response.data;
}
public async upCodeDetail(upCd: string): Promise<UpCodeDetail> {
const response = await client.get<UpCodeDetail>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
);
return response.data;
}
public async createUpCode(data: UpCodeFormData): Promise<void> {
const validatedData = upCodeFormSchema.parse(data);
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}`,
validatedData,
);
return response.data;
}
public async updateUpCode(upCd: string, data: UpCodeFormData): Promise<void> {
const validatedData = upCodeFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.UP_CODE}/${upCd}`,
validatedData,
);
return response.data;
}
}
export const UpCodeApi = new UpCodeApiService();
+16
View File
@@ -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";
@@ -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<typeof LwpoCodeFormSchema>;
export type LwpoCodeList = PageableList<LwpoCode>;
export interface LwpoCodeService {
getLwpoCodeList(params: LwpoCodeParams): Promise<LwpoCodeList>;
getUpCodeLwpoCodeList(upCd: string): Promise<LwpoCodeList>;
lwpoCodeDetail(upCd: string, lwpoCd: string): Promise<LwpoCodeDetail>;
updateLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void>;
createLwpoCode(data: LwpoCodeFormData): Promise<void>;
deleteLwpoCode(
upCd: string,
lwpoCd: string,
data: LwpoCodeFormData,
): Promise<void>;
}
@@ -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<typeof upCodeFormSchema>;
export type UpCodeList = PageableList<UpCode>;
export interface UpCodeService {
getUpCodeList(params: UpCodeParams): Promise<UpCodeList>;
upCodeDetail(upCodeId: string): Promise<UpCodeDetail>;
updateUpCode(upCodeId: string, data: UpCodeFormData): Promise<void>;
createUpCode(data: UpCodeFormData): Promise<void>;
}
+4 -1
View File
@@ -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<typeof Select>;
function CodeSelect({
upCd,
defaultValue,
placeholder = "선택",
className,
...props
}: CodeSelectProps) {
const { data: code } = useQuery({
@@ -31,7 +34,7 @@ function CodeSelect({
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
@@ -0,0 +1 @@
export { AuthorityButton } from "../ui/authority-button";
@@ -1 +1,2 @@
export { AuthorityButton } from "./ui/authority-button";
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
@@ -0,0 +1,26 @@
import { useMenuStore } from "@/shared/lib";
import { Button, cn, type ButtonProps } from "@common/components";
import type { PageMenuAuthority } from "../model/menu-authority-interface";
type AuthorityButtonProps = {
authority: keyof PageMenuAuthority;
} & ButtonProps;
function AuthorityButton({
authority,
className,
children,
...props
}: AuthorityButtonProps) {
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
const isAuthority = pageMenuAuthority?.[authority] === "Y";
return (
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
{children}
</Button>
);
}
export { AuthorityButton };
@@ -1,6 +1,6 @@
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 } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
@@ -15,7 +15,7 @@ import {
DialogTrigger,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import {
menuAuthorityFormSchema,
type MenuAuthorityFormData,
@@ -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],
@@ -41,7 +41,7 @@ function ManagementAuthorityDialog({
enabled: !!menuCd,
});
const { mutateAsync: saveMenuAuthority } = useMutation({
const { mutateAsync: saveMenuAuthority } = useMutationWrapper({
mutationFn: (data: MenuAuthorityPayload[]) =>
MenuAuthorityApi.saveMenuAuthority(data),
onSuccess: () => {
+7 -20
View File
@@ -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);
@@ -32,10 +25,10 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
const getPageMenuAuthority = async () => {
const menuCd = getCookie("menu-code-key");
if (!menuCd) return;
const pageMenuAuthority = await MenuAuthorityApi.getPageMenuAuthority(
const response = await MenuAuthorityApi.getPageMenuAuthority(
parseInt(menuCd),
);
return pageMenuAuthority;
return response;
};
const checkInquiryAuthority = () => {
@@ -47,9 +40,8 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
const currentMenu = await getCurrentMenuInfo();
if (currentMenu) setCurrentMenuInfo(currentMenu);
const pageMenuAuthority = await getPageMenuAuthority();
if (pageMenuAuthority?.length)
setPageMenuAuthority(pageMenuAuthority[0]!);
const response = await getPageMenuAuthority();
if (response?.length) setPageMenuAuthority(response[0]!);
})();
}, []);
@@ -60,11 +52,6 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
}, [pageMenuAuthority]);
useEffect(() => {
if (!storedBbsInfo) return;
setBbsInfo(JSON.parse(storedBbsInfo));
}, [storedBbsInfo]);
return <>{children}</>;
};
+1
View File
@@ -13,3 +13,4 @@ export {
type PostUpdateFormData,
type PostService,
} from "./model/post-interface";
export { PostPageGuard } from "./lib/post-page-guard";
@@ -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/@x/post";
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 };
@@ -5,6 +5,7 @@ export const POSTING_TYPE = {
POPUP: "POPUP",
BANNER: "BANNER",
IMAGE: "IMAGE",
SURVEY: "SURVEY",
} as const;
export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE];
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Dialog,
@@ -26,7 +26,9 @@ import {
cn,
} from "@common/components";
import { CodeSelect } from "@/entities/code/@x/posting";
import { AuthorityButton } from "@/entities/menu-authority/@x/posting";
import { SiteApi, type Site } from "@/entities/site/@x/posting";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import type { PageableList } from "@/shared/model";
import { ENDPOINT } from "@/shared/config";
import { PostingApi } from "../api/posting-service";
@@ -36,7 +38,6 @@ import {
type PostingType,
type PostPayload,
} from "../model/posting-interface";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type DateRange = {
from: string;
@@ -57,11 +58,12 @@ const POSTING_TYPE_LABEL = {
POPUP: "팝업",
BANNER: "배너",
IMAGE: "이미지",
SURVEY: "설문",
} as const;
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const [rememberPeriodCode, setRememberPeriodCode] = useState<
Record<string, string>
@@ -75,7 +77,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
queryFn: () => SiteApi.getSiteList({}),
});
const { mutateAsync: savePosting } = useMutation({
const { mutateAsync: savePosting } = useMutationWrapper({
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
onSuccess: () => {
alert("저장되었습니다.");
@@ -218,10 +220,14 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
return (
<Dialog onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button color="emerald" disabled={!postingTargets.length}>
<AuthorityButton
authority="mngAuthrtYn"
color="emerald"
disabled={!postingTargets.length}
>
<i className="diveicon di-layer" />
사이트반영
</Button>
</AuthorityButton>
</DialogTrigger>
<DialogContent className="max-w-[68rem]" aria-describedby={undefined}>
<DialogHeader>
@@ -6,7 +6,9 @@ import type {
RoleAuthorizationsList,
RoleAuthorizationsData,
RoleAuthorizations,
} from "../model/role-authorizations-interface";
WithRoleIdRoleAuthorizationsParams,
RoleAuthorizationsByRoleIdList,
} from "../model/role-authorization-interface";
export class RoleAuthorizationsApiService implements RoleAuthorizationsService {
public async getRoleAuthorizationsList(
@@ -19,6 +21,16 @@ export class RoleAuthorizationsApiService implements RoleAuthorizationsService {
return response.data;
}
public async getRoleAuthorizationsListByRoleId(
params: WithRoleIdRoleAuthorizationsParams,
): Promise<RoleAuthorizationsByRoleIdList> {
const response = await client.get<RoleAuthorizationsByRoleIdList>(
`${ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS}`,
{ params },
);
return response.data;
}
public async createRoleAuthorizations(
data: RoleAuthorizationsData,
): Promise<RoleAuthorizations> {
@@ -0,0 +1,11 @@
export { RoleAuthorizationsApi } from "./api/role-authorization-service";
export {
roleAuthorizationsFormSchema,
type RoleAuthorizations,
type RoleAuthorizationsParams,
type RoleAuthorizationsList,
type RoleAuthorizationsFormData,
type RoleAuthorizationsData,
type RoleAuthorizationsByRoleId,
type RoleAuthorizationsByRoleIdList,
} from "./model/role-authorization-interface";
@@ -7,6 +7,12 @@ export interface RoleAuthorizationsParams extends PageableParams {
authorizationNo?: string;
}
export interface WithRoleIdRoleAuthorizationsParams extends PageableParams {
keywordType?: string;
keyword?: string;
roleId?: string;
}
export interface RoleAuthorizations {
roleId: string;
roleName: string;
@@ -16,6 +22,16 @@ export interface RoleAuthorizations {
authYn: string;
}
export interface RoleAuthorizationsByRoleId {
roleId: string;
authorizationNo: number;
authorizationName: string;
urlPatternValue: string;
httpMethodCode: string;
sortSeq: number;
createdAt: boolean;
}
export const roleAuthorizationsSchema = z.object({
roleId: z.string(),
authorizationNo: z.number(),
@@ -27,6 +43,8 @@ export const roleAuthorizationsFormSchema = z.object({
});
export type RoleAuthorizationsList = PageableList<RoleAuthorizations>;
export type RoleAuthorizationsByRoleIdList =
PageableList<RoleAuthorizationsByRoleId>;
export type RoleAuthorizationsData = z.infer<typeof roleAuthorizationsSchema>[];
export type RoleAuthorizationsFormData = z.infer<
typeof roleAuthorizationsFormSchema
@@ -36,6 +54,9 @@ export interface RoleAuthorizationsService {
getRoleAuthorizationsList: (
params: RoleAuthorizationsParams,
) => Promise<RoleAuthorizationsList>;
getRoleAuthorizationsListByRoleId: (
params: WithRoleIdRoleAuthorizationsParams,
) => Promise<RoleAuthorizationsByRoleIdList>;
createRoleAuthorizations: (
data: RoleAuthorizationsData,
) => Promise<RoleAuthorizations>;
+1
View File
@@ -5,3 +5,4 @@ export {
type Site,
type SiteFormData,
} from "./model/site-interface";
export { SiteSelect } from "./ui/site-select";
@@ -0,0 +1,53 @@
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 = {
placeholder?: string;
enabledAllSite?: boolean;
} & React.ComponentPropsWithoutRef<typeof Select>;
function SiteSelect({
defaultValue,
placeholder = "선택",
enabledAllSite,
...props
}: SiteSelectProps) {
const { data: sites } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
queryFn: () => SiteApi.getSiteList(),
select: (data) => {
if (enabledAllSite) {
return [{ siteId: "ALL", siteNm: "전체" }, ...data.content];
}
return data.content;
},
});
const defaultValueText =
sites?.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder;
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{sites?.map((item) => (
<SelectItem key={item.siteId} value={item.siteId}>
{item.siteNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { SiteSelect };
@@ -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<UserList> {
const response = await client.get<UserList>(
`${ENDPOINT.USER_SERVICE.USERS}`,
{ params },
);
return response.data;
}
// 사용자 단건 조회
public async findByUserUnqId(userUnqId: string): Promise<UserDetailResponse> {
const response = await client.get<UserDetailResponse>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`,
);
return response.data;
}
// 로그인한 사용자 정보 조회
public async findByUserUnqIdByLogin(): Promise<UserDetailResponse> {
const response = await client.get<UserDetailResponse>(
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
return response.data;
}
// 사용자 회원상태 통계 조회
public async selectUserSttsStats(): Promise<UserSttsStatsList> {
const response = await client.get<UserSttsStatsList>(
`${ENDPOINT.USER_SERVICE.USERS}/stts/stats`,
);
return response.data;
}
// 사용자 저장
public async saveUser(data: UserSaveData): Promise<string> {
const response = await client.post<string>(
`${ENDPOINT.USER_SERVICE.USERS}`,
data,
);
return response.data;
}
// 사용자 수정
public async updateUser(
userUnqId: string,
data: UserUpdateData,
): Promise<string> {
const response = await client.put<string>(
`${ENDPOINT.USER_SERVICE.USERS}/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 삭제
public async deleteUser(userUnqId: string): Promise<boolean> {
const response = await client.delete<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/delete/${userUnqId}`,
);
return response.data;
}
// 사용자 계정잠금 처리
public async lockUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/lock/${userUnqId}`,
);
return response.data;
}
// 사용자 계정잠금 해제처리
public async unlockUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/unlock/${userUnqId}`,
);
return response.data;
}
// 리프레시 토큰 정보 재발급
public async refreshToken(): Promise<void> {
await client.put(`${ENDPOINT.USER_SERVICE.USERS}/token/refresh`);
}
// 사용자 비밀번호 변경
public async updatePassword(data: UserPasswordUpdateData): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/update`,
data,
);
return response.data;
}
// 사용자 비밀번호 초기화(관리자용)
public async initializePassword(
userUnqId: string,
data: UserPasswordUpdateData,
): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/initialize/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기 변경
public async changePassword(
data: UserFindPasswordUpdateData,
): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/change`,
data,
);
return response.data;
}
// 사용자 비밀번호 확인
public async matchPassword(data: UserPasswordMatchData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/match`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기
public async findPasswordUser(
data: UserFindPasswordSaveData,
): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/find`,
data,
);
return response.data;
}
// 사용자 비밀번호 찾기 유효성 확인
public async validPassword(token: string): Promise<boolean> {
const response = await client.get<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/password/valid/${token}`,
);
return response.data;
}
// 사용자 회원정보 변경
public async updateInfo(
userUnqId: string,
data: UserUpdateInfoData,
): Promise<string> {
const response = await client.put<string>(
`${ENDPOINT.USER_SERVICE.USERS}/info/${userUnqId}`,
data,
);
return response.data;
}
// 사용자 승인처리
public async applyUser(userUnqId: string): Promise<boolean> {
const response = await client.put<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/apply/${userUnqId}`,
);
return response.data;
}
// 사용자 회원탈퇴
public async leaveUser(data: UserVerifyData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/leave`,
data,
);
return response.data;
}
// 사용자 이메일 중복 체크
public async existsEmail(data: UserEmailData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
data,
);
return response.data;
}
// 사용자 아이디 중복 체크
public async existsUserId(data: UserIdData): Promise<boolean> {
const response = await client.post<boolean>(
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
data,
);
return response.data;
}
}
export const UserApi = new UserApiService();
+5
View File
@@ -0,0 +1,5 @@
export { UserApi } from "./api/user-service";
export {
type UserSearchParams,
type UserListResponse,
} from "./model/user-interface";
@@ -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<typeof userUpdateSchema>;
export type UserPasswordUpdateData = z.infer<typeof userPasswordUpdateSchema>;
export type UserFindPasswordUpdateData = z.infer<
typeof userFindPasswordUpdateSchema
>;
export type UserPasswordMatchData = z.infer<typeof userPasswordMatchSchema>;
export type UserUpdateInfoData = z.infer<typeof userUpdateInfoSchema>;
export type UserSaveData = z.infer<typeof userSaveSchema>;
export type UserFindPasswordSaveData = z.infer<
typeof userFindPasswordSaveSchema
>;
export type UserVerifyData = z.infer<typeof userVerifySchema>;
export type UserEmailData = z.infer<typeof userEmailSchema>;
export type UserIdData = z.infer<typeof userIdSchema>;
// 페이지네이션 타입 정의
export type UserList = PageableList<UserListResponse>;
export type UserSttsStatsList = UserSttsStatsResponse[];
// 사용자 서비스 인터페이스
export interface UserService {
// 사용자 목록 조회
findPageUser: (params: UserSearchParams) => Promise<UserList>;
// 사용자 단건 조회
findByUserUnqId: (userUnqId: string) => Promise<UserDetailResponse>;
// 로그인한 사용자 정보 조회
findByUserUnqIdByLogin: () => Promise<UserDetailResponse>;
// 사용자 회원상태 통계 조회
selectUserSttsStats: () => Promise<UserSttsStatsList>;
// 사용자 저장
saveUser: (data: UserSaveData) => Promise<string>;
// 사용자 수정
updateUser: (userUnqId: string, data: UserUpdateData) => Promise<string>;
// 사용자 삭제
deleteUser: (userUnqId: string) => Promise<boolean>;
// 사용자 계정잠금 처리
lockUser: (userUnqId: string) => Promise<boolean>;
// 사용자 계정잠금 해제처리
unlockUser: (userUnqId: string) => Promise<boolean>;
// 리프레시 토큰 정보 재발급
refreshToken: () => Promise<void>;
// 사용자 비밀번호 변경
updatePassword: (data: UserPasswordUpdateData) => Promise<boolean>;
// 사용자 비밀번호 초기화(관리자용)
initializePassword: (
userUnqId: string,
data: UserPasswordUpdateData,
) => Promise<boolean>;
// 사용자 비밀번호 찾기 변경
changePassword: (data: UserFindPasswordUpdateData) => Promise<boolean>;
// 사용자 비밀번호 확인
matchPassword: (data: UserPasswordMatchData) => Promise<boolean>;
// 사용자 비밀번호 찾기
findPasswordUser: (data: UserFindPasswordSaveData) => Promise<boolean>;
// 사용자 비밀번호 찾기 유효성 확인
validPassword: (token: string) => Promise<boolean>;
// 사용자 회원정보 변경
updateInfo: (userUnqId: string, data: UserUpdateInfoData) => Promise<string>;
// 사용자 승인처리
applyUser: (userUnqId: string) => Promise<boolean>;
// 사용자 회원탈퇴
leaveUser: (data: UserVerifyData) => Promise<boolean>;
// 사용자 이메일 중복 체크
existsEmail: (data: UserEmailData) => Promise<boolean>;
// 사용자 아이디 중복 체크
existsUserId: (data: UserIdData) => Promise<boolean>;
}
@@ -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);
+25 -19
View File
@@ -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<PostParams>({
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 }) {
<div className="flex w-full items-center justify-between">
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
<div className="flex gap-2">
{pageMenuAuthority?.regAuthrtYn === "Y" && (
<Button onClick={() => router.push(`${pathname}/register`)}>
등록
</Button>
)}
{pageMenuAuthority?.delAuthrtYn === "Y" && (
<Button
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</Button>
)}
<AuthorityButton
authority="regAuthrtYn"
onClick={() => router.push(`${pathname}/register`)}
>
등록
</AuthorityButton>
<AuthorityButton
authority="delAuthrtYn"
color="red"
onClick={onSelectedPostDelete}
disabled={!selectedPostNumbers.length}
>
선택삭제
</AuthorityButton>
</div>
</div>
</div>
@@ -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: () => {
@@ -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);
@@ -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: () => {
@@ -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);
@@ -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: () => {
@@ -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);
@@ -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: () => {
@@ -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);
@@ -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: () => {
+1 -1
View File
@@ -133,7 +133,7 @@ function BannerPage() {
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
+3 -4
View File
@@ -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);
+6 -6
View File
@@ -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<RowSelectionState>({});
@@ -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({
+8 -10
View File
@@ -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<Board>[] = [
{
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<Board>[] = [
const ManageButton = ({ row }: { row: Row<Board> }) => {
const router = useRouter();
const pathname = usePathname();
const [, setBbsInfo] = useLocalStorage("bbs-info", "");
const { setBbsInfo: setStoreBbsInfo } = useMenuStore((state) => state);
const { bbsTypeCd, bbsId } = row.original;
return (
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
router.push(`${pathname}/${bbsTypeCd.toLowerCase()}/${bbsId}`);
// router.push(`/pst/${bbsTypeCd.toLowerCase()}/${bbsId}`); // 공통 개발용
setBbsInfo(JSON.stringify(row.original));
setStoreBbsInfo(row.original);
}}
>
관리
+6 -14
View File
@@ -189,13 +189,9 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
type="number"
min={0}
value={boardTemplate?.atflMaxCnt ?? field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
if (isNaN(parseInt(value))) return;
field.onChange(
value === "" ? null : parseInt(value),
);
}}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="최대 개수 입력"
disabled={
@@ -224,13 +220,9 @@ function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
value={
boardTemplate?.atflMaxFileSz ?? field.value ?? ""
}
onChange={(e) => {
const value = e.target.value;
if (isNaN(parseInt(value))) return;
field.onChange(
value === "" ? null : parseInt(value),
);
}}
onChange={(e) =>
field.onChange(parseInt(e.target.value))
}
className="w-[12rem] !text-[1.3rem]"
placeholder="최대 크기 입력"
disabled={
+1 -1
View File
@@ -134,7 +134,7 @@ function BbsPage() {
</div>
</div>
</div>
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
<div className="card !m-[1.2rem] !p-0">
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
</div>
</div>
+3 -4
View File
@@ -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 {
BoardApi,
boardFormSchema,
@@ -14,9 +13,9 @@ import { BbsForm } from "./bbs-form";
function CreatePage() {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { mutateAsync: createBoard } = useMutation({
const { mutateAsync: createBoard } = useMutationWrapper({
mutationFn: (data: BoardFormData) => BoardApi.createBoard(data),
onSuccess: () => {
alert("등록되었습니다.", router.back);
+4 -4
View File
@@ -2,8 +2,8 @@
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 {
BoardApi,
@@ -13,14 +13,14 @@ import {
import { BbsForm } from "./bbs-form";
function ModifyPage({ bbsId }: { bbsId: string }) {
const { setMessage: alert } = useAlertStore((state) => state);
const alert = useAlertStore((state) => state.setMessage);
const { data: boardInfo } = useQuery({
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId],
queryFn: () => BoardApi.getBoardInfo(bbsId),
});
const { mutateAsync: updateBoard } = useMutation({
const { mutateAsync: updateBoard } = useMutationWrapper({
mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data),
onSuccess: () => {
alert("수정되었습니다.");
+1
View File
@@ -0,0 +1 @@
export { CodePage } from "./ui/code-page";

Some files were not shown because too many files have changed in this diff Show More