diff --git a/apps/common/feature/app/(with-base-layout)/board/page.tsx b/apps/common/feature/app/(with-base-layout)/board/page.tsx deleted file mode 100644 index 9f8a68c08..000000000 --- a/apps/common/feature/app/(with-base-layout)/board/page.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import { BoardPage } from "@/pages/board"; -export default BoardPage; diff --git a/apps/common/feature/src/entities/attachment/index.ts b/apps/common/feature/src/entities/attachment/index.ts deleted file mode 100644 index 182ef9fd6..000000000 --- a/apps/common/feature/src/entities/attachment/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AttachmentApi } from "./api/attachment-service"; diff --git a/apps/common/feature/src/entities/attachment/model/attachment-interface.ts b/apps/common/feature/src/entities/attachment/model/attachment-interface.ts deleted file mode 100644 index 5986a96d1..000000000 --- a/apps/common/feature/src/entities/attachment/model/attachment-interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface Attachment { - atflCd: string; - atflSn: number; - atflId: string; - physFileNm: string; - orgnlFileNm: string; - atflFlieSz: number; - atflType: string; - dwnldNocs: number; - delYn: string; - lnkgDmnId: string; - lnkgDmnNm: string; - regDt: string; -} - -export interface AttachmentService { - getAttachments: (atflCd: string) => Promise; - deleteAttachment: (atflId: string) => Promise; - deleteAttachmentByAtflCd: (atflCd: string) => Promise; - getImageUrl: (atflId: string) => Promise; - getImageUrlByAtflCd: (atflCd: string) => Promise; -} diff --git a/apps/common/feature/src/entities/auth/index.ts b/apps/common/feature/src/entities/auth/index.ts deleted file mode 100644 index 97e142225..000000000 --- a/apps/common/feature/src/entities/auth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { LoginApi } from "./api/login-service"; -export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/common/feature/src/entities/auth/lib/auth-wrapper.tsx b/apps/common/feature/src/entities/auth/lib/auth-wrapper.tsx deleted file mode 100644 index 705c03231..000000000 --- a/apps/common/feature/src/entities/auth/lib/auth-wrapper.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { client } from "@/shared/api"; -import type { IUserInfo } from "@/shared/store"; -import { getCookie, useUserStore } from "@/shared/lib"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; - -const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); - const { userInfo, setUserInfo } = useUserStore((state) => state); - - async function authCheck() { - const userId = getCookie(AUTH_USER_ID); - if (!userId) { - throw new Error("Missing required authentication tokens"); - } - - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/${userId}`, - ); - if (!userInfo) setUserInfo(response.data); - } - - useEffect(() => { - (async () => { - try { - await authCheck(); - } catch (error) { - console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); - } - } - })(); - }, []); - - return <>{children}; -}; - -export { AuthWrapper }; diff --git a/apps/common/feature/src/entities/board/api/board-service.ts b/apps/common/feature/src/entities/board/api/board-service.ts deleted file mode 100644 index 9cf29599b..000000000 --- a/apps/common/feature/src/entities/board/api/board-service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - BoardPopupList, - BoardPopupParams, - BoardService, -} from "../model/board-interface"; - -export class BoardApiService implements BoardService { - public async getBoardPopupList( - params: BoardPopupParams, - ): Promise { - const response = await client.get( - `${ENDPOINT.BOARD_SERVICE.BBS}`, - { params }, - ); - return response.data; - } -} - -export const BoardApi = new BoardApiService(); diff --git a/apps/common/feature/src/entities/board/index.ts b/apps/common/feature/src/entities/board/index.ts deleted file mode 100644 index 2173bb4a0..000000000 --- a/apps/common/feature/src/entities/board/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BoardApi } from "./api/board-service"; -export { type BoardPopup } from "./model/board-interface"; diff --git a/apps/common/feature/src/entities/board/model/board-interface.ts b/apps/common/feature/src/entities/board/model/board-interface.ts deleted file mode 100644 index 942840d74..000000000 --- a/apps/common/feature/src/entities/board/model/board-interface.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { PageableList, PageableParams } from "@/shared/model"; - -export const BOARD_TYPE = { - BBSNORMAL: "BBSNORMAL", - BBSFAQ: "BBSFAQ", - BBSIMAGE: "BBSIMAGE", - BBSMOVIE: "BBSMOVIE", -} as const; - -export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE]; - -export interface BoardPopupParams extends PageableParams { - bbsNm?: string; -} - -export interface BoardPopup { - bbsId: number; - bbsTypeCd: BoardType; - bbsTypeNm: string; - bbsNm: string; - bbsExpln: string; - rfrncUseYn: string; - atflUseYn: string; - atflMaxCnt: number; - atflMaxFileSz: number; - ansUseYn: string; - cmntUseYn: string; - dgstfnEvlUseYn: string; - smsUseYn: string; - upndFixUseYn: string; - pstgPrdUseYn: string; - popupUseYn: string; - delYn: string; - regDt: string; - pstCnt: number; -} - -export type BoardPopupList = PageableList; - -export interface BoardService { - getBoardPopupList: (params: BoardPopupParams) => Promise; -} diff --git a/apps/common/feature/src/entities/code/index.ts b/apps/common/feature/src/entities/code/index.ts deleted file mode 100644 index 0b6633f4b..000000000 --- a/apps/common/feature/src/entities/code/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CodeSelect } from "./ui/code-select"; diff --git a/apps/common/feature/src/entities/menu/index.ts b/apps/common/feature/src/entities/menu/index.ts deleted file mode 100644 index 1fa9dd218..000000000 --- a/apps/common/feature/src/entities/menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MenuWrapper } from "./lib/menu-wrapper"; diff --git a/apps/common/feature/src/pages/board/index.ts b/apps/common/feature/src/pages/board/index.ts deleted file mode 100644 index 483a2f96b..000000000 --- a/apps/common/feature/src/pages/board/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BoardPage } from "./ui/board-page"; diff --git a/apps/common/feature/src/pages/board/ui/board-page.tsx b/apps/common/feature/src/pages/board/ui/board-page.tsx deleted file mode 100644 index 15960674e..000000000 --- a/apps/common/feature/src/pages/board/ui/board-page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function BoardPage() { - return
게시판 관리
; -} - -export { BoardPage }; diff --git a/apps/common/feature/src/shared/ui/page-layout.tsx b/apps/common/feature/src/shared/ui/page-layout.tsx deleted file mode 100644 index 17100389a..000000000 --- a/apps/common/feature/src/shared/ui/page-layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { PageTitle } from "@common/components"; -import { useMenuStore } from "../lib/use-menu-store"; - -function PageLayout({ children }: { children: React.ReactNode }) { - const { currentMenuInfo } = useMenuStore((state) => state); - - const menuPaths = currentMenuInfo?.menuPath - ?.split(">") - .map((path) => path.trim()); - - return ( -
- - {children} -
- ); -} - -export { PageLayout }; diff --git a/apps/common/feature/.gitignore b/apps/common/features/.gitignore similarity index 100% rename from apps/common/feature/.gitignore rename to apps/common/features/.gitignore diff --git a/apps/common/feature/.prettierrc b/apps/common/features/.prettierrc similarity index 100% rename from apps/common/feature/.prettierrc rename to apps/common/features/.prettierrc diff --git a/apps/common/feature/app/(with-base-layout)/layout.tsx b/apps/common/features/app/(with-base-layout)/layout.tsx similarity index 77% rename from apps/common/feature/app/(with-base-layout)/layout.tsx rename to apps/common/features/app/(with-base-layout)/layout.tsx index c3254a935..bb1b7c0ba 100644 --- a/apps/common/feature/app/(with-base-layout)/layout.tsx +++ b/apps/common/features/app/(with-base-layout)/layout.tsx @@ -2,10 +2,9 @@ import { usePathname, useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; -import type { MenuItem } from "@common/components/api"; -import { BaseLayout } from "@common/components/ui"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; import { useMenuStore } from "@/shared/lib"; -import { PageLayout } from "@/shared/ui/page-layout"; export default function WithBaseLayout({ children, @@ -29,7 +28,7 @@ export default function WithBaseLayout({ currentPath={pathname ?? ""} handleMenuClick={handleMenuClick} > - {children} + {children} ); } diff --git a/apps/common/feature/app/(with-base-layout)/loading.tsx b/apps/common/features/app/(with-base-layout)/loading.tsx similarity index 100% rename from apps/common/feature/app/(with-base-layout)/loading.tsx rename to apps/common/features/app/(with-base-layout)/loading.tsx diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/page.tsx new file mode 100644 index 000000000..0f59903d4 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/[pstNo]/page.tsx @@ -0,0 +1,10 @@ +import { NoticePost } from "@/pages/notice"; + +export default async function PstBbsNoticePostPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/page.tsx new file mode 100644 index 000000000..df9ae763f --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsnotice/[bbsId]/page.tsx @@ -0,0 +1,10 @@ +import { NoticePage } from "@/pages/notice"; + +export default async function PstBbsNoticePage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/feature/app/api/proxy/[...url]/route.ts b/apps/common/features/app/api/proxy/[...url]/route.ts similarity index 100% rename from apps/common/feature/app/api/proxy/[...url]/route.ts rename to apps/common/features/app/api/proxy/[...url]/route.ts diff --git a/apps/common/feature/app/auth/login/page.tsx b/apps/common/features/app/auth/login/page.tsx similarity index 100% rename from apps/common/feature/app/auth/login/page.tsx rename to apps/common/features/app/auth/login/page.tsx diff --git a/apps/common/feature/app/layout.tsx b/apps/common/features/app/layout.tsx similarity index 100% rename from apps/common/feature/app/layout.tsx rename to apps/common/features/app/layout.tsx diff --git a/apps/common/feature/eslint.config.mjs b/apps/common/features/eslint.config.mjs similarity index 100% rename from apps/common/feature/eslint.config.mjs rename to apps/common/features/eslint.config.mjs diff --git a/apps/common/feature/middleware.ts b/apps/common/features/middleware.ts similarity index 80% rename from apps/common/feature/middleware.ts rename to apps/common/features/middleware.ts index 5b38dc5a8..018aab11a 100644 --- a/apps/common/feature/middleware.ts +++ b/apps/common/features/middleware.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; -const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL; +const serverApiUrl = + process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:13000"; export function middleware(req: NextRequest) { const pathname = req.nextUrl.pathname; @@ -12,6 +13,7 @@ export function middleware(req: NextRequest) { const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; + console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -24,6 +26,9 @@ export function middleware(req: NextRequest) { if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, + ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); diff --git a/apps/common/feature/next.config.ts b/apps/common/features/next.config.ts similarity index 100% rename from apps/common/feature/next.config.ts rename to apps/common/features/next.config.ts diff --git a/apps/common/feature/package.json b/apps/common/features/package.json similarity index 91% rename from apps/common/feature/package.json rename to apps/common/features/package.json index bf44a814c..8423560be 100644 --- a/apps/common/feature/package.json +++ b/apps/common/features/package.json @@ -1,5 +1,5 @@ { - "name": "feature", + "name": "features", "version": "0.0.0", "private": true, "scripts": { @@ -11,6 +11,8 @@ "dependencies": { "@common/assets": "workspace:*", "@common/components": "workspace:*", + "@common/config": "workspace:*", + "@common/network": "workspace:*", "@frontend-opensource/use-react-hooks": "^1.1.4", "@hookform/resolvers": "^4.1.3", "@minoru/react-dnd-treeview": "^3.5.0", @@ -23,7 +25,7 @@ "chart.js": "^4.4.8", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", - "next": "15.1.7", + "next": "15.3.0", "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-hook-form": "^7.54.2", diff --git a/apps/common/feature/pages/.gitkeep b/apps/common/features/pages/.gitkeep similarity index 100% rename from apps/common/feature/pages/.gitkeep rename to apps/common/features/pages/.gitkeep diff --git a/apps/common/feature/postcss.config.mjs b/apps/common/features/postcss.config.mjs similarity index 100% rename from apps/common/feature/postcss.config.mjs rename to apps/common/features/postcss.config.mjs diff --git a/apps/common/feature/public/images/bg_login.jpg b/apps/common/features/public/images/bg_login.jpg similarity index 100% rename from apps/common/feature/public/images/bg_login.jpg rename to apps/common/features/public/images/bg_login.jpg diff --git a/apps/common/features/public/images/img_cc_by.png b/apps/common/features/public/images/img_cc_by.png new file mode 100755 index 000000000..628cc2bfd Binary files /dev/null and b/apps/common/features/public/images/img_cc_by.png differ diff --git a/apps/common/features/public/images/img_cc_by_nc.png b/apps/common/features/public/images/img_cc_by_nc.png new file mode 100755 index 000000000..86f60c5db Binary files /dev/null and b/apps/common/features/public/images/img_cc_by_nc.png differ diff --git a/apps/common/features/public/images/img_cc_by_nc_nd.png b/apps/common/features/public/images/img_cc_by_nc_nd.png new file mode 100755 index 000000000..9d8ccb18d Binary files /dev/null and b/apps/common/features/public/images/img_cc_by_nc_nd.png differ diff --git a/apps/common/features/public/images/img_cc_by_nc_sa.png b/apps/common/features/public/images/img_cc_by_nc_sa.png new file mode 100755 index 000000000..00f402c9e Binary files /dev/null and b/apps/common/features/public/images/img_cc_by_nc_sa.png differ diff --git a/apps/common/features/public/images/img_cc_by_nd.png b/apps/common/features/public/images/img_cc_by_nd.png new file mode 100755 index 000000000..0da64acd8 Binary files /dev/null and b/apps/common/features/public/images/img_cc_by_nd.png differ diff --git a/apps/common/features/public/images/img_cc_by_sa.png b/apps/common/features/public/images/img_cc_by_sa.png new file mode 100755 index 000000000..6d94e60b9 Binary files /dev/null and b/apps/common/features/public/images/img_cc_by_sa.png differ diff --git a/apps/common/features/public/images/img_opentype01.png b/apps/common/features/public/images/img_opentype01.png new file mode 100755 index 000000000..456c7c6bb Binary files /dev/null and b/apps/common/features/public/images/img_opentype01.png differ diff --git a/apps/common/features/public/images/img_opentype02.png b/apps/common/features/public/images/img_opentype02.png new file mode 100755 index 000000000..a6c5bfb32 Binary files /dev/null and b/apps/common/features/public/images/img_opentype02.png differ diff --git a/apps/common/features/public/images/img_opentype03.png b/apps/common/features/public/images/img_opentype03.png new file mode 100755 index 000000000..970a6a0d4 Binary files /dev/null and b/apps/common/features/public/images/img_opentype03.png differ diff --git a/apps/common/features/public/images/img_opentype04.png b/apps/common/features/public/images/img_opentype04.png new file mode 100755 index 000000000..41d284cab Binary files /dev/null and b/apps/common/features/public/images/img_opentype04.png differ diff --git a/apps/common/feature/src/app/index.tsx b/apps/common/features/src/app/index.tsx similarity index 100% rename from apps/common/feature/src/app/index.tsx rename to apps/common/features/src/app/index.tsx diff --git a/apps/common/feature/src/app/providers/app-provider.tsx b/apps/common/features/src/app/providers/app-provider.tsx similarity index 77% rename from apps/common/feature/src/app/providers/app-provider.tsx rename to apps/common/features/src/app/providers/app-provider.tsx index 6fd488b90..eaf0463a3 100644 --- a/apps/common/feature/src/app/providers/app-provider.tsx +++ b/apps/common/features/src/app/providers/app-provider.tsx @@ -2,23 +2,24 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; -import { AuthWrapper } from "@/entities/auth"; import { MenuWrapper } from "@/entities/menu"; import { StoreProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-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 ( - - {children} - - - + {children} + + diff --git a/apps/common/feature/src/app/providers/index.ts b/apps/common/features/src/app/providers/index.ts similarity index 100% rename from apps/common/feature/src/app/providers/index.ts rename to apps/common/features/src/app/providers/index.ts diff --git a/apps/common/feature/src/app/styles/base.css b/apps/common/features/src/app/styles/base.css similarity index 83% rename from apps/common/feature/src/app/styles/base.css rename to apps/common/features/src/app/styles/base.css index e24111afe..a4f54680f 100644 --- a/apps/common/feature/src/app/styles/base.css +++ b/apps/common/features/src/app/styles/base.css @@ -1,5 +1,6 @@ @import "@common/assets/styles/base.css"; @import "@common/assets/styles/diveicon.css"; +@import "@common/components/index.css"; @tailwind base; @tailwind utilities; diff --git a/apps/common/feature/src/app/styles/components.css b/apps/common/features/src/app/styles/components.css similarity index 100% rename from apps/common/feature/src/app/styles/components.css rename to apps/common/features/src/app/styles/components.css diff --git a/apps/common/feature/src/entities/attachment/api/attachment-service.ts b/apps/common/features/src/entities/attachment/api/attachment-service.ts similarity index 77% rename from apps/common/feature/src/entities/attachment/api/attachment-service.ts rename to apps/common/features/src/entities/attachment/api/attachment-service.ts index c930b7012..f956f6580 100644 --- a/apps/common/feature/src/entities/attachment/api/attachment-service.ts +++ b/apps/common/features/src/entities/attachment/api/attachment-service.ts @@ -2,11 +2,23 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { Attachment, + AttachmentList, + AttachmentParams, AttachmentService, } from "../model/attachment-interface"; export class AttachmentApiService implements AttachmentService { - public async getAttachments(atflCd: string): Promise { + public async getAttachmentsList( + params: AttachmentParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}`, + { params }, + ); + return response.data; + } + + public async getAttachmentsInfo(atflCd: string): Promise { const response = await client.get( `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`, ); diff --git a/apps/common/features/src/entities/attachment/index.ts b/apps/common/features/src/entities/attachment/index.ts new file mode 100644 index 000000000..385425537 --- /dev/null +++ b/apps/common/features/src/entities/attachment/index.ts @@ -0,0 +1,5 @@ +export { AttachmentApi } from "./api/attachment-service"; +export type { + AttachmentParams, + Attachment, +} from "./model/attachment-interface"; diff --git a/apps/common/features/src/entities/attachment/model/attachment-interface.ts b/apps/common/features/src/entities/attachment/model/attachment-interface.ts new file mode 100644 index 000000000..5133e4a72 --- /dev/null +++ b/apps/common/features/src/entities/attachment/model/attachment-interface.ts @@ -0,0 +1,42 @@ +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface AttachmentParams extends PageableParams { + keywordType?: string; + keyword?: string; + atflCd?: string; + atflSn?: number; + atflId?: string; + orgnlFileNm?: string; + atflFlieSz?: number; + atflType?: string; + dwnldNocs?: number; + delYn?: string; + lnkgDmnId?: string; + lnkgDmnNm?: string; +} + +export interface Attachment { + atflCd: string; + atflSn: number; + atflId: string; + physFileNm: string; + orgnlFileNm: string; + atflFlieSz: number; + atflType: string; + dwnldNocs: number; + delYn: string; + lnkgDmnId: string; + lnkgDmnNm: string; + regDt: string; +} + +export type AttachmentList = PageableList; + +export interface AttachmentService { + getAttachmentsList: (params: AttachmentParams) => Promise; + getAttachmentsInfo: (atflCd: string) => Promise; + deleteAttachment: (atflId: string) => Promise; + deleteAttachmentByAtflCd: (atflCd: string) => Promise; + getImageUrl: (atflId: string) => Promise; + getImageUrlByAtflCd: (atflCd: string) => Promise; +} diff --git a/apps/common/feature/src/entities/auth/api/login-service.ts b/apps/common/features/src/entities/auth/api/login-service.ts similarity index 100% rename from apps/common/feature/src/entities/auth/api/login-service.ts rename to apps/common/features/src/entities/auth/api/login-service.ts diff --git a/apps/common/features/src/entities/auth/index.ts b/apps/common/features/src/entities/auth/index.ts new file mode 100644 index 000000000..8c7981ac9 --- /dev/null +++ b/apps/common/features/src/entities/auth/index.ts @@ -0,0 +1 @@ +export { LoginApi } from "./api/login-service"; diff --git a/apps/common/feature/src/entities/auth/lib/token-manager.ts b/apps/common/features/src/entities/auth/lib/token-manager.ts similarity index 100% rename from apps/common/feature/src/entities/auth/lib/token-manager.ts rename to apps/common/features/src/entities/auth/lib/token-manager.ts diff --git a/apps/common/feature/src/entities/auth/model/login-interface.ts b/apps/common/features/src/entities/auth/model/login-interface.ts similarity index 100% rename from apps/common/feature/src/entities/auth/model/login-interface.ts rename to apps/common/features/src/entities/auth/model/login-interface.ts diff --git a/apps/common/features/src/entities/board/api/board-service.ts b/apps/common/features/src/entities/board/api/board-service.ts new file mode 100644 index 000000000..150ff677a --- /dev/null +++ b/apps/common/features/src/entities/board/api/board-service.ts @@ -0,0 +1,60 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Board, + BoardFormData, + BoardList, + BoardParams, + BoardPopupList, + BoardPopupParams, + BoardService, +} from "../model/board-interface"; + +export class BoardApiService implements BoardService { + public async getBoardList(params: BoardParams): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + + public async getBoardPopupList( + params: BoardPopupParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + + public async getBoardInfo(bbsId: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + ); + return response.data; + } + + public async createBoard(data: BoardFormData): Promise { + const response = await client.post( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + data, + ); + return response.data; + } + + public async updateBoard(bbsId: string, data: BoardFormData): Promise { + const response = await client.put( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + data, + ); + return response.data; + } + + public async deleteBoard(bbsId: string): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`); + } +} + +export const BoardApi = new BoardApiService(); diff --git a/apps/common/features/src/entities/board/index.ts b/apps/common/features/src/entities/board/index.ts new file mode 100644 index 000000000..f9ef1b610 --- /dev/null +++ b/apps/common/features/src/entities/board/index.ts @@ -0,0 +1,8 @@ +export { BoardApi } from "./api/board-service"; +export { + boardFormSchema, + type Board, + type BoardFormData, + type BoardParams, + type BoardPopup, +} from "./model/board-interface"; diff --git a/apps/common/features/src/entities/board/model/board-interface.ts b/apps/common/features/src/entities/board/model/board-interface.ts new file mode 100644 index 000000000..1ed8f1d9f --- /dev/null +++ b/apps/common/features/src/entities/board/model/board-interface.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const BOARD_TYPE = { + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE]; + +export interface BoardParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + bbsTypeCd?: string; + bbsNm?: string; + bbsExpln?: string; + rfrncUseYn?: string; + atflUseYn?: string; + atflMaxCnt?: number; + atflMaxFileSz?: number; + ansUseYn?: string; + cmntUseYn?: string; + dgstfnEvlUseYn?: string; + smsUseYn?: string; + upndFixUseYn?: string; + pstgPrdUseYn?: string; + popupUseYn?: string; + delYn?: string; +} + +export interface BoardPopupParams extends PageableParams { + bbsNm?: string; +} + +export interface Board { + bbsId: number; + bbsTypeCd: string; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +export interface BoardPopup extends Board { + pstCnt: number; +} + +export const boardFormSchema = z.object({ + bbsTypeCd: z.string(), + bbsNm: z.string(), + bbsExpln: z.string(), + rfrncUseYn: z.string(), + atflUseYn: z.string(), + atflMaxCnt: z.number().nullish(), + atflMaxFileSz: z.number().nullish(), + ansUseYn: z.string(), + ansUseAuthrt: z.string().nullish(), + cmntUseYn: z.string(), + dgstfnEvlUseYn: z.string(), + smsUseYn: z.string(), + upndFixUseYn: z.string(), + pstgPrdUseYn: z.string(), + popupUseYn: z.string(), + useYn: z.string(), +}); + +export type BoardList = PageableList; +export type BoardPopupList = PageableList; +export type BoardFormData = z.infer; + +export interface BoardService { + getBoardList: (params: BoardParams) => Promise; + getBoardPopupList: (params: BoardPopupParams) => Promise; + getBoardInfo: (bbsId: string) => Promise; + createBoard: (data: BoardFormData) => Promise; + updateBoard: (bbsId: string, data: BoardFormData) => Promise; + deleteBoard: (bbsId: string) => Promise; +} diff --git a/apps/common/feature/src/entities/code/@x/posting.ts b/apps/common/features/src/entities/code/@x/posting.ts similarity index 100% rename from apps/common/feature/src/entities/code/@x/posting.ts rename to apps/common/features/src/entities/code/@x/posting.ts diff --git a/apps/common/feature/src/entities/code/api/code-service.ts b/apps/common/features/src/entities/code/api/code-service.ts similarity index 100% rename from apps/common/feature/src/entities/code/api/code-service.ts rename to apps/common/features/src/entities/code/api/code-service.ts diff --git a/apps/common/features/src/entities/code/index.ts b/apps/common/features/src/entities/code/index.ts new file mode 100644 index 000000000..dc74bbc47 --- /dev/null +++ b/apps/common/features/src/entities/code/index.ts @@ -0,0 +1,3 @@ +export { CodeApi } from "./api/code-service"; +export type { ICode } from "./model/code-interface"; +export { CodeSelect } from "./ui/code-select"; diff --git a/apps/common/feature/src/entities/code/model/code-interface.ts b/apps/common/features/src/entities/code/model/code-interface.ts similarity index 90% rename from apps/common/feature/src/entities/code/model/code-interface.ts rename to apps/common/features/src/entities/code/model/code-interface.ts index 0d066e6da..4562e2559 100644 --- a/apps/common/feature/src/entities/code/model/code-interface.ts +++ b/apps/common/features/src/entities/code/model/code-interface.ts @@ -2,6 +2,7 @@ export interface ICode { lwpoCd: string; upCd: string; lwpoCdNm: string; + unit: string; } export interface CodeService { diff --git a/apps/common/feature/src/entities/code/ui/code-select.tsx b/apps/common/features/src/entities/code/ui/code-select.tsx similarity index 100% rename from apps/common/feature/src/entities/code/ui/code-select.tsx rename to apps/common/features/src/entities/code/ui/code-select.tsx diff --git a/apps/common/features/src/entities/content/api/contents-service.ts b/apps/common/features/src/entities/content/api/contents-service.ts new file mode 100644 index 000000000..00d2a8c67 --- /dev/null +++ b/apps/common/features/src/entities/content/api/contents-service.ts @@ -0,0 +1,90 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + ContentsService, + ContentsParams, + ContentsList, + ContentsFormData, + ContentsVersionParams, + ContentsVersionList, + ContentsDetail, +} from "../model/contents-interface"; + +export class ContentsApiService implements ContentsService { + public async getContentsList(params: ContentsParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS}`, + { params }, + ); + return response.data; + } + + public async getContentsVersionList( + params: ContentsVersionParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, + { params }, + ); + return response.data; + } + + public async getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + ); + return response.data; + } + + public async createContents(data: ContentsFormData): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, + data, + ); + return response.data; + } + + public async updateContents( + contsSn: number, + contsVer: number, + data: ContentsFormData, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + data, + ); + return response.data; + } + + public async deleteContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + ); + } + + public async requestApprove( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`, + ); + } + + public async approveContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`, + ); + } +} + +export const ContentsApi = new ContentsApiService(); diff --git a/apps/common/features/src/entities/content/index.ts b/apps/common/features/src/entities/content/index.ts new file mode 100644 index 000000000..2446dd716 --- /dev/null +++ b/apps/common/features/src/entities/content/index.ts @@ -0,0 +1,8 @@ +export { ContentsApi } from "./api/contents-service"; +export { + CONTENTS_APPROVE_STATUS, + contentsFormSchema, + type Contents, + type ContentsParams, + type ContentsFormData, +} from "./model/contents-interface"; diff --git a/apps/common/features/src/entities/content/model/contents-interface.ts b/apps/common/features/src/entities/content/model/contents-interface.ts new file mode 100644 index 000000000..72456149b --- /dev/null +++ b/apps/common/features/src/entities/content/model/contents-interface.ts @@ -0,0 +1,100 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const CONTENTS_APPROVE_STATUS = { + DRAFT: "01", + REQUESTED: "02", + APPROVED: "99", +} as const; + +export type ContentsApproveStatus = + (typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS]; + +export interface ContentsParams extends PageableParams { + contsTtl?: string; + aprvContsVer?: number; + useYn?: string; +} + +export interface ContentsVersionParams extends PageableParams { + contsSn?: number; + contsCn?: string; + aprvCd?: ContentsApproveStatus; + cclCd?: string; + koglCd?: string; + tag?: string; + useYn?: string; +} + +export interface ContentsVersion { + contsSn: number; + contsVer: number; + contsCn: string; + useYn: string; +} + +export const contentsSchema = z.object({ + contsSn: z.number(), + contsTtl: z.string(), + aprvContsVer: z.number(), + useYn: z.string(), + mdfcnDt: z.string(), + mdfr: z.string(), + mdfrNm: z.string(), + aprvDt: z.string(), + autzrNm: z.string(), + workAprvCd: z.string(), + workAprvNm: z.string(), + workContsVer: z.number(), +}); + +export const contentsDetailSchema = z.object({ + contsSn: z.number(), + contsVer: z.number(), + contsTtl: z.string(), + contsCn: z.string(), + contsRmrk: z.string().nullish(), + aprvCd: z.enum([ + CONTENTS_APPROVE_STATUS.DRAFT, + CONTENTS_APPROVE_STATUS.REQUESTED, + CONTENTS_APPROVE_STATUS.APPROVED, + ]), + cclCd: z.string().nullish(), + koglCd: z.string().nullish(), + tag: z.string().nullish(), + aprvDt: z.string(), + autzr: z.string(), + useYn: z.string(), +}); + +export const contentsFormSchema = contentsDetailSchema.omit({ + contsVer: true, + aprvDt: true, + autzr: true, +}); + +export type Contents = z.infer; +export type ContentsDetail = z.infer; +export type ContentsFormData = z.infer; +export type ContentsList = PageableList; +export type ContentsVersionList = PageableList; + +export interface ContentsService { + getContentsList(params: ContentsParams): Promise; + getContentsVersionList( + params: ContentsVersionParams, + ): Promise; + getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise; + createContents(data: ContentsFormData): Promise; + updateContents( + contsSn: number, + contsVer: number, + data: ContentsFormData, + ): Promise; + deleteContents(contsSn: number, contsVer: number): Promise; + requestApprove(contsSn: number, contsVer: number): Promise; + approveContents(contsSn: number, contsVer: number): Promise; +} diff --git a/apps/common/feature/src/entities/editor/api/editor-service.ts b/apps/common/features/src/entities/editor/api/editor-service.ts similarity index 100% rename from apps/common/feature/src/entities/editor/api/editor-service.ts rename to apps/common/features/src/entities/editor/api/editor-service.ts diff --git a/apps/common/feature/src/entities/editor/index.ts b/apps/common/features/src/entities/editor/index.ts similarity index 100% rename from apps/common/feature/src/entities/editor/index.ts rename to apps/common/features/src/entities/editor/index.ts diff --git a/apps/common/feature/src/entities/editor/lib/blob-to-base64.ts b/apps/common/features/src/entities/editor/lib/blob-to-base64.ts similarity index 100% rename from apps/common/feature/src/entities/editor/lib/blob-to-base64.ts rename to apps/common/features/src/entities/editor/lib/blob-to-base64.ts diff --git a/apps/common/feature/src/entities/editor/lib/generate-file-info.ts b/apps/common/features/src/entities/editor/lib/generate-file-info.ts similarity index 100% rename from apps/common/feature/src/entities/editor/lib/generate-file-info.ts rename to apps/common/features/src/entities/editor/lib/generate-file-info.ts diff --git a/apps/common/feature/src/entities/editor/model/editor-base-config.ts b/apps/common/features/src/entities/editor/model/editor-base-config.ts similarity index 100% rename from apps/common/feature/src/entities/editor/model/editor-base-config.ts rename to apps/common/features/src/entities/editor/model/editor-base-config.ts diff --git a/apps/common/feature/src/entities/editor/model/editor-interface.ts b/apps/common/features/src/entities/editor/model/editor-interface.ts similarity index 100% rename from apps/common/feature/src/entities/editor/model/editor-interface.ts rename to apps/common/features/src/entities/editor/model/editor-interface.ts diff --git a/apps/common/feature/src/entities/editor/ui/editor.tsx b/apps/common/features/src/entities/editor/ui/editor.tsx similarity index 98% rename from apps/common/feature/src/entities/editor/ui/editor.tsx rename to apps/common/features/src/entities/editor/ui/editor.tsx index b5ef02153..797a6c1bf 100644 --- a/apps/common/feature/src/entities/editor/ui/editor.tsx +++ b/apps/common/features/src/entities/editor/ui/editor.tsx @@ -49,6 +49,7 @@ const Editor = ({ ...props }: IAllProps) => { }} init={{ ...baseConfig.init, + disabled: props.disabled, images_upload_handler: imagesUploadHandler, file_picker_callback: filePickerCallback, }} diff --git a/apps/common/feature/src/entities/menu-authority/api/menu-authority-service.ts b/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts similarity index 86% rename from apps/common/feature/src/entities/menu-authority/api/menu-authority-service.ts rename to apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts index f9b01b4d8..22e3dcefa 100644 --- a/apps/common/feature/src/entities/menu-authority/api/menu-authority-service.ts +++ b/apps/common/features/src/entities/menu-authority/api/menu-authority-service.ts @@ -3,7 +3,7 @@ import { ENDPOINT } from "@/shared/config"; import type { MenuAuthorityList, MenuAuthorityParams, - MenuAuthorityPayloadData, + MenuAuthorityPayload, MenuAuthorityService, } from "../model/menu-authority-interface"; @@ -18,9 +18,7 @@ export class MenuAuthorityApiService implements MenuAuthorityService { return response.data; } - public async saveMenuAuthority( - data: MenuAuthorityPayloadData[], - ): Promise { + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { const response = await client.post( `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, data, diff --git a/apps/common/feature/src/entities/menu-authority/index.ts b/apps/common/features/src/entities/menu-authority/index.ts similarity index 100% rename from apps/common/feature/src/entities/menu-authority/index.ts rename to apps/common/features/src/entities/menu-authority/index.ts diff --git a/apps/common/feature/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts similarity index 86% rename from apps/common/feature/src/entities/menu-authority/model/menu-authority-interface.ts rename to apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts index d6775838c..7467728fe 100644 --- a/apps/common/feature/src/entities/menu-authority/model/menu-authority-interface.ts +++ b/apps/common/features/src/entities/menu-authority/model/menu-authority-interface.ts @@ -30,12 +30,10 @@ export const menuAuthorityFormSchema = z.object({ export type MenuAuthority = z.infer; export type MenuAuthorityList = PageableList; -export type MenuAuthorityPayloadData = z.infer< - typeof menuAuthorityPayloadSchema ->; +export type MenuAuthorityPayload = z.infer; export type MenuAuthorityFormData = z.infer; export interface MenuAuthorityService { getMenuAuthorityList(params: MenuAuthorityParams): Promise; - saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; } diff --git a/apps/common/feature/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx similarity index 95% rename from apps/common/feature/src/entities/menu-authority/model/menu-authority-table-columns.tsx rename to apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx index 1a9863e15..6969a85e9 100644 --- a/apps/common/feature/src/entities/menu-authority/model/menu-authority-table-columns.tsx +++ b/apps/common/features/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -1,10 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table"; import type { UseFormReturn } from "react-hook-form"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import type { MenuAuthority, MenuAuthorityFormData, - MenuAuthorityPayloadData, + MenuAuthorityPayload, } from "./menu-authority-interface"; export const createColumns = ( @@ -13,7 +13,7 @@ export const createColumns = ( const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; const handleCheckedChange = - (i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => { + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { if (getCurrentRow(i)?.mode === "R") { form.setValue(`menuAuthority.${i}.mode`, "U"); } diff --git a/apps/common/feature/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx similarity index 96% rename from apps/common/feature/src/entities/menu-authority/ui/management-authority-dialog.tsx rename to apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx index 62915e2ed..a6d4d2128 100644 --- a/apps/common/feature/src/entities/menu-authority/ui/management-authority-dialog.tsx +++ b/apps/common/features/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -13,13 +13,13 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@common/components/ui"; +} from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { menuAuthorityFormSchema, type MenuAuthorityFormData, - type MenuAuthorityPayloadData, + type MenuAuthorityPayload, } from "../model/menu-authority-interface"; import { createColumns } from "../model/menu-authority-table-columns"; import { MenuAuthorityApi } from "../api/menu-authority-service"; @@ -42,7 +42,7 @@ function ManagementAuthorityDialog({ }); const { mutateAsync: saveMenuAuthority } = useMutation({ - mutationFn: (data: MenuAuthorityPayloadData[]) => + mutationFn: (data: MenuAuthorityPayload[]) => MenuAuthorityApi.saveMenuAuthority(data), onSuccess: () => { alert("저장되었습니다."); diff --git a/apps/common/feature/src/entities/menu/api/menu-service.ts b/apps/common/features/src/entities/menu/api/menu-service.ts similarity index 89% rename from apps/common/feature/src/entities/menu/api/menu-service.ts rename to apps/common/features/src/entities/menu/api/menu-service.ts index b6c1510cd..4424f8d37 100644 --- a/apps/common/feature/src/entities/menu/api/menu-service.ts +++ b/apps/common/features/src/entities/menu/api/menu-service.ts @@ -1,4 +1,4 @@ -import { client } from "@common/components/api"; +import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { MenuItem, MenuService } from "../model/menu-interface"; diff --git a/apps/common/features/src/entities/menu/index.ts b/apps/common/features/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/common/features/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/common/feature/src/entities/menu/lib/menu-wrapper.tsx b/apps/common/features/src/entities/menu/lib/menu-wrapper.tsx similarity index 100% rename from apps/common/feature/src/entities/menu/lib/menu-wrapper.tsx rename to apps/common/features/src/entities/menu/lib/menu-wrapper.tsx diff --git a/apps/common/feature/src/entities/menu/lib/utils.ts b/apps/common/features/src/entities/menu/lib/utils.ts similarity index 100% rename from apps/common/feature/src/entities/menu/lib/utils.ts rename to apps/common/features/src/entities/menu/lib/utils.ts diff --git a/apps/common/feature/src/entities/menu/model/menu-interface.ts b/apps/common/features/src/entities/menu/model/menu-interface.ts similarity index 100% rename from apps/common/feature/src/entities/menu/model/menu-interface.ts rename to apps/common/features/src/entities/menu/model/menu-interface.ts diff --git a/apps/common/features/src/entities/organization/api/organization-service.ts b/apps/common/features/src/entities/organization/api/organization-service.ts new file mode 100644 index 000000000..ae7fc44ac --- /dev/null +++ b/apps/common/features/src/entities/organization/api/organization-service.ts @@ -0,0 +1,66 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + type Organization, + type OrganizationFormData, + type OrganizationService, + type OrganizationTree, + type OrganizationTreeForDnd, +} from "../model/organization-interface"; + +export class OrganizationApiService implements OrganizationService { + public async getOrganizationsTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/tree`, + ); + return response.data; + } + + public async getOrganizationInfo(ognzSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + ); + return response.data; + } + + public async createOrganization(data: OrganizationFormData): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, data); + } + + public async deleteOrganization(ognzSn: number): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + ); + } + + public async updateOrganizationName( + ognzSn: number, + name: string, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}/${name}`, + ); + return response.data; + } + + public async updateOrganization( + ognzSn: number, + data: OrganizationFormData, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + data, + ); + } + + public async updateOrganizationByDnd( + data: OrganizationTreeForDnd[], + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`, + data, + ); + } +} + +export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/common/features/src/entities/organization/index.ts b/apps/common/features/src/entities/organization/index.ts new file mode 100644 index 000000000..6b4cb0357 --- /dev/null +++ b/apps/common/features/src/entities/organization/index.ts @@ -0,0 +1,8 @@ +export { OrganizationApi } from "./api/organization-service"; +export { + organizationFormSchema, + type OrganizationTree, + type OrganizationFormData, + type OrganizationTreeForDnd, +} from "./model/organization-interface"; +export { generateOrganizationTreeData, handleDropNode } from "./lib/utils"; diff --git a/apps/common/features/src/entities/organization/lib/utils.ts b/apps/common/features/src/entities/organization/lib/utils.ts new file mode 100644 index 000000000..45591fc9e --- /dev/null +++ b/apps/common/features/src/entities/organization/lib/utils.ts @@ -0,0 +1,78 @@ +import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; +import type { + OrganizationTree, + OrganizationTreeForDnd, +} from "../model/organization-interface"; + +export const generateOrganizationTreeData = ( + data: OrganizationTree[], +): NodeModel[] => { + return data.reduce[]>((acc, menu) => { + const node: NodeModel = { + id: menu.ognzSn ?? 0, + parent: menu.upOgnzSn ?? 0, + text: menu.ognzNm, + droppable: true, + data: menu, + }; + acc.push(node); + + if (menu.children?.length) { + acc.push(...generateOrganizationTreeData(menu.children)); + } + + return acc; + }, []); +}; + +export const generateDndOrganizationTreeData = ( + treeData: NodeModel[], + options: DropOptions, +): OrganizationTreeForDnd | null => { + const { dragSourceId, dragSource, dropTargetId } = options; + if (!dragSource?.data) return null; + + const siblings = treeData.filter((node) => node.parent === dropTargetId); + const dragSourceIndex = siblings.findIndex( + (node) => node.id === dragSourceId, + ); + const prevSibling = siblings[dragSourceIndex - 1]; + const newSortSeq = + dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1; + + const treeNode: OrganizationTreeForDnd = { + ognzSn: dragSource.data.ognzSn, + ognzNm: dragSource.data.ognzNm, + ognzExpln: dragSource.data.ognzExpln, + upOgnzSn: dropTargetId === 0 ? null : (dropTargetId as number), + sortSeq: newSortSeq, + }; + + const childNodes = treeData.filter( + (node) => node.parent === dragSourceId && node.data, + ); + const result: OrganizationTreeForDnd = { + ...treeNode, + children: + childNodes?.map((child) => ({ + ognzSn: child.data?.ognzSn, + ognzNm: child.data?.ognzNm ?? "", + ognzExpln: child.data?.ognzExpln, + upOgnzSn: child.data?.upOgnzSn === 0 ? null : (dragSourceId as number), + sortSeq: child.data?.sortSeq ?? 1, + })) ?? [], + }; + + return result; +}; + +export const handleDropNode = + (callback: (newTree: OrganizationTreeForDnd) => void) => + ( + newTree: NodeModel[], + options: DropOptions, + ) => { + const newTreeData = generateDndOrganizationTreeData(newTree, options); + if (!newTreeData) return; + callback(newTreeData); + }; diff --git a/apps/common/features/src/entities/organization/model/organization-interface.ts b/apps/common/features/src/entities/organization/model/organization-interface.ts new file mode 100644 index 000000000..a63b85571 --- /dev/null +++ b/apps/common/features/src/entities/organization/model/organization-interface.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const organizationSchema = z.object({ + ognzSn: z.number().nullish(), + ognzNm: z.string(), + ognzExpln: z.string().nullish(), + upOgnzSn: z.number().nullish(), + sortSeq: z.number(), + useYn: z.string().nullish(), +}); + +export const organizationFormSchema = organizationSchema.omit({ + ognzSn: true, +}); + +export type Organization = z.infer; +export type OrganizationFormData = z.infer; +export type OrganizationTree = Organization & { + children?: OrganizationTree[]; +}; +export type OrganizationTreeForDnd = Omit; + +export interface OrganizationService { + getOrganizationsTreeList(): Promise; + getOrganizationInfo(ognzSn: number): Promise; + createOrganization(data: OrganizationFormData): Promise; + deleteOrganization(ognzSn: number): Promise; + updateOrganizationName(ognzSn: number, name: string): Promise; + updateOrganization(ognzSn: number, data: OrganizationFormData): Promise; + updateOrganizationByDnd(data: OrganizationTreeForDnd[]): Promise; +} diff --git a/apps/common/features/src/entities/post/api/post-service.ts b/apps/common/features/src/entities/post/api/post-service.ts new file mode 100644 index 000000000..04034bc78 --- /dev/null +++ b/apps/common/features/src/entities/post/api/post-service.ts @@ -0,0 +1,30 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + PostInfo, + PostList, + PostParams, + PostService, +} from "../model/post-interface"; + +export class PostApiService implements PostService { + public async getPostList(params: PostParams): Promise { + const response = await client.get(ENDPOINT.BOARD_SERVICE.POSTS, { + params, + }); + return response.data; + } + + public async getPostInfo(bbsId: string, pstNo: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, + ); + return response.data; + } + + public async deletePost(bbsId: string, pstNo: string): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`); + } +} + +export const PostApi = new PostApiService(); diff --git a/apps/common/features/src/entities/post/index.ts b/apps/common/features/src/entities/post/index.ts new file mode 100644 index 000000000..933fb0a51 --- /dev/null +++ b/apps/common/features/src/entities/post/index.ts @@ -0,0 +1,7 @@ +export { PostApi } from "./api/post-service"; +export type { + Post, + PostList, + PostParams, + PostService, +} from "./model/post-interface"; diff --git a/apps/common/features/src/entities/post/model/post-interface.ts b/apps/common/features/src/entities/post/model/post-interface.ts new file mode 100644 index 000000000..ec9021fad --- /dev/null +++ b/apps/common/features/src/entities/post/model/post-interface.ts @@ -0,0 +1,47 @@ +import { + type PageableList, + type PageableParams, +} from "@/shared/model/api-interface"; + +export interface PostParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + delYn?: string; + frDateYmd?: string; + toDateYmd?: string; +} + +export interface Post { + bbsId: number; + pstNo: number; + pstTtl: string; + inqNocs: number; + rfrncUrl: string; + popupYn: string; + upndFixYn: string; + prdPstgYn: string; + rootPath: string; + pstPath: string; + ansPstn: number; + wrtrNm: string; + wrtrCplc: string; + atflCd: string; + regDt: string; + rgtrNm: string; +} + +export interface PostInfo extends Post { + pstCn: string; + rgtr: string; + mdfcnDt: string; + mdfr: string; +} + +export type PostList = PageableList; + +export interface PostService { + getPostList(params: PostParams): Promise; + getPostInfo(bbsId: string, pstNo: string): Promise; + deletePost(bbsId: string, pstNo: string): Promise; +} diff --git a/apps/common/feature/src/entities/posting/api/posting-service.ts b/apps/common/features/src/entities/posting/api/posting-service.ts similarity index 89% rename from apps/common/feature/src/entities/posting/api/posting-service.ts rename to apps/common/features/src/entities/posting/api/posting-service.ts index 2fafd6557..d1360b46a 100644 --- a/apps/common/feature/src/entities/posting/api/posting-service.ts +++ b/apps/common/features/src/entities/posting/api/posting-service.ts @@ -5,7 +5,7 @@ import type { PostService, PostingParams, Post, - PostPayloadData, + PostPayload, } from "../model/posting-interface"; export class PostingApiService implements PostService { @@ -19,7 +19,7 @@ export class PostingApiService implements PostService { return response.data; } - public async savePosting(data: PostPayloadData[]): Promise { + public async savePosting(data: PostPayload[]): Promise { const response = await client.post( `${ENDPOINT.CMS_SERVICE.POSTINGS}`, data, diff --git a/apps/common/feature/src/entities/posting/index.ts b/apps/common/features/src/entities/posting/index.ts similarity index 88% rename from apps/common/feature/src/entities/posting/index.ts rename to apps/common/features/src/entities/posting/index.ts index c6029d757..75c5c69ed 100644 --- a/apps/common/feature/src/entities/posting/index.ts +++ b/apps/common/features/src/entities/posting/index.ts @@ -2,6 +2,6 @@ export { PostingApi } from "./api/posting-service"; export { POSTING_TYPE, type Post, - type PostPayloadData, + type PostPayload, } from "./model/posting-interface"; export { PostingDialog } from "./ui/posting-dialog"; diff --git a/apps/common/feature/src/entities/posting/model/posting-interface.ts b/apps/common/features/src/entities/posting/model/posting-interface.ts similarity index 90% rename from apps/common/feature/src/entities/posting/model/posting-interface.ts rename to apps/common/features/src/entities/posting/model/posting-interface.ts index b14b2ab66..72c49bb03 100644 --- a/apps/common/feature/src/entities/posting/model/posting-interface.ts +++ b/apps/common/features/src/entities/posting/model/posting-interface.ts @@ -41,10 +41,10 @@ export const postingFormSchema = z.object({ }); export type Post = z.infer; -export type PostPayloadData = z.infer; +export type PostPayload = z.infer; export type PostingFormData = z.infer; export interface PostService { getPostingHistory: (params: PostingParams) => Promise>; - savePosting: (data: PostPayloadData[]) => Promise; + savePosting: (data: PostPayload[]) => Promise; } diff --git a/apps/common/feature/src/entities/posting/ui/posting-dialog.tsx b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx similarity index 98% rename from apps/common/feature/src/entities/posting/ui/posting-dialog.tsx rename to apps/common/features/src/entities/posting/ui/posting-dialog.tsx index 9e27a4485..bbddf9778 100644 --- a/apps/common/feature/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/common/features/src/entities/posting/ui/posting-dialog.tsx @@ -23,8 +23,8 @@ import { DateRangePicker, DialogClose, DialogFooter, -} from "@common/components/ui"; -import { cn } from "@common/components/lib"; + cn, +} from "@common/components"; import { CodeSelect } from "@/entities/code/@x/posting"; import { SiteApi, type Site } from "@/entities/site/@x/posting"; import type { PageableList } from "@/shared/model"; @@ -34,7 +34,7 @@ import { postingFormSchema, type PostingFormData, type PostingType, - type PostPayloadData, + type PostPayload, } from "../model/posting-interface"; import { useAlertStore } from "@/shared/lib/use-alert-store"; @@ -76,7 +76,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { }); const { mutateAsync: savePosting } = useMutation({ - mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { alert("저장되었습니다."); }, diff --git a/apps/common/feature/src/entities/site/@x/posting.ts b/apps/common/features/src/entities/site/@x/posting.ts similarity index 100% rename from apps/common/feature/src/entities/site/@x/posting.ts rename to apps/common/features/src/entities/site/@x/posting.ts diff --git a/apps/common/feature/src/entities/site/api/site-service.ts b/apps/common/features/src/entities/site/api/site-service.ts similarity index 100% rename from apps/common/feature/src/entities/site/api/site-service.ts rename to apps/common/features/src/entities/site/api/site-service.ts diff --git a/apps/common/feature/src/entities/site/index.ts b/apps/common/features/src/entities/site/index.ts similarity index 100% rename from apps/common/feature/src/entities/site/index.ts rename to apps/common/features/src/entities/site/index.ts diff --git a/apps/common/feature/src/entities/site/model/site-interface.ts b/apps/common/features/src/entities/site/model/site-interface.ts similarity index 100% rename from apps/common/feature/src/entities/site/model/site-interface.ts rename to apps/common/features/src/entities/site/model/site-interface.ts diff --git a/apps/common/features/src/entities/user-organization/api/user-organization-service.ts b/apps/common/features/src/entities/user-organization/api/user-organization-service.ts new file mode 100644 index 000000000..a4e588310 --- /dev/null +++ b/apps/common/features/src/entities/user-organization/api/user-organization-service.ts @@ -0,0 +1,62 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + TreeUserOrganization, + UserOrganizationParams, + UserOrganizationList, + UserOrganizationService, + UserOrganizationPayload, + SearchUserParams, + SearchUserList, + User, + MoveUserOrganizationPayload, +} from "../model/user-organization-interface"; + +export class UserOrganizationApiService implements UserOrganizationService { + public async getUserOrganizationList( + params: UserOrganizationParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + { params }, + ); + return response.data; + } + + public async getUserOrganizationTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, + ); + return response.data; + } + + public async searchUser(params: SearchUserParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`, + { params }, + ); + return response.data; + } + + public async saveUserOrganization( + data: UserOrganizationPayload[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + data, + ); + return response.data; + } + + public async moveUserOrganization( + data: MoveUserOrganizationPayload[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/move-ognz`, + data, + ); + return response.data; + } +} + +export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/common/features/src/entities/user-organization/index.ts b/apps/common/features/src/entities/user-organization/index.ts new file mode 100644 index 000000000..87317f1da --- /dev/null +++ b/apps/common/features/src/entities/user-organization/index.ts @@ -0,0 +1,11 @@ +export { UserOrganizationApi } from "./api/user-organization-service"; +export { + userOrganizationFormSchema, + type TreeUserOrganization, + type TreeUser, + type UserOrganizationFormData, + type UserOrganizationPayload, + type User, + type SearchUser, + type MoveUserOrganizationPayload, +} from "./model/user-organization-interface"; diff --git a/apps/common/features/src/entities/user-organization/model/user-organization-interface.ts b/apps/common/features/src/entities/user-organization/model/user-organization-interface.ts new file mode 100644 index 000000000..25d7c7032 --- /dev/null +++ b/apps/common/features/src/entities/user-organization/model/user-organization-interface.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface UserOrganizationParams extends PageableParams { + keywordType?: string; + keyword?: string; + ognzSn?: number; + userUnqId?: string; + jbpsSeCd?: string; + jbgdSeCd?: string; +} + +export interface SearchUserParams extends PageableParams { + userNm?: string; + ogdpSeCd?: string; +} + +export interface User { + mode: string; + ognzSn: number; + userUnqId: string; + userUnqNm?: string; + jbpsSeCd: string; + jbpsSeNm?: string; + jbgdSeCd: string; + jbgdSeNm?: string; + sortSeq: number; + mdfcnDt?: string; + mdfr?: string; + mdfrNm?: string; +} + +export interface SearchUser { + userUnqId: string; + userNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + regDt: string; +} + +export interface TreeUser { + userUnqId: string; + userNm: string; + jbpsSeCd: string; + jbgdSeCd: string; +} + +export interface TreeUserOrganization { + ognzSn: number; + upOgnzSn: number; + ognzNm: string; + users: TreeUser[]; + children: TreeUserOrganization[]; +} + +export interface MoveUserOrganizationPayload { + ognzSn: number; + toOgnzSn: number; + userUnqId: string; +} + +export const userOrganizationPayloadSchema = z.object({ + mode: z.string(), + ognzSn: z.number(), + userUnqId: z.string(), + jbpsSeCd: z.string(), + jbgdSeCd: z.string(), + sortSeq: z.number(), +}); + +export const userOrganizationFormSchema = z.object({ + userOrganization: z.array(userOrganizationPayloadSchema), +}); + +export type UserOrganizationList = PageableList; +export type SearchUserList = PageableList; +export type UserOrganizationPayload = z.infer< + typeof userOrganizationPayloadSchema +>; +export type UserOrganizationFormData = z.infer< + typeof userOrganizationFormSchema +>; + +export interface UserOrganizationService { + getUserOrganizationList( + params: UserOrganizationParams, + ): Promise; + getUserOrganizationTreeList(): Promise; + searchUser(params: SearchUserParams): Promise; + saveUserOrganization(data: UserOrganizationPayload[]): Promise; + moveUserOrganization(data: MoveUserOrganizationPayload[]): Promise; +} diff --git a/apps/common/feature/src/pages/auth/login/index.ts b/apps/common/features/src/pages/auth/login/index.ts similarity index 100% rename from apps/common/feature/src/pages/auth/login/index.ts rename to apps/common/features/src/pages/auth/login/index.ts diff --git a/apps/common/feature/src/pages/auth/login/ui/login-page.tsx b/apps/common/features/src/pages/auth/login/ui/login-page.tsx similarity index 100% rename from apps/common/feature/src/pages/auth/login/ui/login-page.tsx rename to apps/common/features/src/pages/auth/login/ui/login-page.tsx diff --git a/apps/common/feature/src/pages/auth/login/ui/login.css b/apps/common/features/src/pages/auth/login/ui/login.css similarity index 100% rename from apps/common/feature/src/pages/auth/login/ui/login.css rename to apps/common/features/src/pages/auth/login/ui/login.css diff --git a/apps/common/features/src/pages/notice/index.ts b/apps/common/features/src/pages/notice/index.ts new file mode 100644 index 000000000..c75abbdc8 --- /dev/null +++ b/apps/common/features/src/pages/notice/index.ts @@ -0,0 +1,2 @@ +export { NoticePage } from "./ui/notice-page"; +export { NoticePost } from "./ui/notice-post"; diff --git a/apps/common/features/src/pages/notice/model/notice-table-columns.tsx b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx new file mode 100644 index 000000000..f6036f050 --- /dev/null +++ b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx @@ -0,0 +1,61 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { Post } from "@/entities/post"; +import { Badge, Button } from "@common/components"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row }) => row.index + 1, + }, + { + accessorKey: "pstTtl", + header: "제목", + cell: ({ row }) => { + const { pstTtl, inqNocs } = row.original; + + return ( +
+ {inqNocs >= 100 && HOT} + {dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && ( + NEW + )} + {pstTtl} +
+ ); + }, + }, + { + accessorKey: "inqNocs", + header: "조회수", + }, + { + accessorKey: "atflCd", + header: "첨부파일", + cell: ({ row }) => { + const atflCd = row.original.atflCd; + if (!atflCd) return null; + return ; + }, + }, + { + accessorKey: "rgtrNm", + header: "작성자", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + }, +]; + +const AttachmentButton = ({ atflCd }: { atflCd: string }) => { + if (!atflCd) return null; + return ( + + ); +}; diff --git a/apps/common/features/src/pages/notice/ui/notice-layout.tsx b/apps/common/features/src/pages/notice/ui/notice-layout.tsx new file mode 100644 index 000000000..b4b5e37fb --- /dev/null +++ b/apps/common/features/src/pages/notice/ui/notice-layout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { Board } from "@/entities/board"; +import { PageLayout } from "@/shared/ui/page-layout"; +import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; + +function NoticeLayout({ children }: { children: React.ReactNode }) { + const [bbsInfo, setBbsInfo] = useState(null); + const [storedBbsInfo] = useLocalStorage("bbs-info", ""); + + useEffect(() => { + if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); + }, [storedBbsInfo]); + + return ( + + {children} + + ); +} + +export { NoticeLayout }; diff --git a/apps/common/features/src/pages/notice/ui/notice-page.tsx b/apps/common/features/src/pages/notice/ui/notice-page.tsx new file mode 100644 index 000000000..ceb63ec6a --- /dev/null +++ b/apps/common/features/src/pages/notice/ui/notice-page.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DataTable, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { PostApi, type Post, type PostParams } from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/notice-table-columns"; +import { NoticeLayout } from "./notice-layout"; + +function NoticePage({ bbsId }: { bbsId: string }) { + const router = useRouter(); + const pathname = usePathname(); + const [rowSelection, setRowSelection] = useState({}); + const [postParams, setPostParams] = useState({ + keywordType: "title", + keyword: "", + delYn: "N", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: postList, refetch } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => + PostApi.getPostList({ + ...postParams, + bbsId: Number(bbsId), + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: false, + }); + + const table = useReactTable({ + data: postList?.content ?? [], + columns, + state: { + pagination, + rowSelection, + }, + rowCount: postList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Post) => { + 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]" + /> + +
+
+
+
+
+

공지사항 목록

+
+ + +
+
+
+
+ +
+
+
+
+ ); +} + +export { NoticePage }; diff --git a/apps/common/features/src/pages/notice/ui/notice-post.tsx b/apps/common/features/src/pages/notice/ui/notice-post.tsx new file mode 100644 index 000000000..964e16a91 --- /dev/null +++ b/apps/common/features/src/pages/notice/ui/notice-post.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { Post } from "@/widgets/post"; +import { NoticeLayout } from "./notice-layout"; + +function NoticePost({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + return ( + + + + ); +} + +export { NoticePost }; diff --git a/apps/common/feature/src/shared/api/client.ts b/apps/common/features/src/shared/api/client.ts similarity index 100% rename from apps/common/feature/src/shared/api/client.ts rename to apps/common/features/src/shared/api/client.ts diff --git a/apps/common/feature/src/shared/api/index.ts b/apps/common/features/src/shared/api/index.ts similarity index 100% rename from apps/common/feature/src/shared/api/index.ts rename to apps/common/features/src/shared/api/index.ts diff --git a/apps/common/feature/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts similarity index 82% rename from apps/common/feature/src/shared/config/api.ts rename to apps/common/features/src/shared/config/api.ts index 94d2e728d..b82a8eb23 100644 --- a/apps/common/feature/src/shared/config/api.ts +++ b/apps/common/features/src/shared/config/api.ts @@ -13,8 +13,8 @@ export const ENDPOINT = { LOGIN: "/user-service/login", USERS: "/user-service/api/v1/users", SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", - AUTHRTS: "/user-service/api/v1/authorizations", - ROLES_AUTHRTS: "/user-service/api/v1/role-authorizations", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", }, CMS_SERVICE: { SITES: "/cms-service/api/v1/sites", @@ -26,6 +26,8 @@ export const ENDPOINT = { MENU_ROLES: "/cms-service/api/v1/menu-roles", MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", BANNERS: "/cms-service/api/v1/bnrs", @@ -34,6 +36,7 @@ export const ENDPOINT = { }, BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", + POSTS: "/board-service/api/v1/posts", }, }; diff --git a/apps/common/feature/src/shared/config/env.ts b/apps/common/features/src/shared/config/env.ts similarity index 90% rename from apps/common/feature/src/shared/config/env.ts rename to apps/common/features/src/shared/config/env.ts index f0b4c83b1..e8d3161eb 100644 --- a/apps/common/feature/src/shared/config/env.ts +++ b/apps/common/features/src/shared/config/env.ts @@ -1,10 +1,8 @@ -export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +export const PORT = process.env.NEXT_PUBLIC_PORT || "13000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; - export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; - export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; diff --git a/apps/common/feature/src/shared/config/index.ts b/apps/common/features/src/shared/config/index.ts similarity index 100% rename from apps/common/feature/src/shared/config/index.ts rename to apps/common/features/src/shared/config/index.ts diff --git a/apps/common/feature/src/shared/lib/cookie.ts b/apps/common/features/src/shared/lib/cookie.ts similarity index 100% rename from apps/common/feature/src/shared/lib/cookie.ts rename to apps/common/features/src/shared/lib/cookie.ts diff --git a/apps/common/feature/src/shared/lib/index.ts b/apps/common/features/src/shared/lib/index.ts similarity index 100% rename from apps/common/feature/src/shared/lib/index.ts rename to apps/common/features/src/shared/lib/index.ts diff --git a/apps/common/feature/src/shared/lib/use-alert-store.ts b/apps/common/features/src/shared/lib/use-alert-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-alert-store.ts rename to apps/common/features/src/shared/lib/use-alert-store.ts diff --git a/apps/common/feature/src/shared/lib/use-attachment-store.ts b/apps/common/features/src/shared/lib/use-attachment-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-attachment-store.ts rename to apps/common/features/src/shared/lib/use-attachment-store.ts diff --git a/apps/common/feature/src/shared/lib/use-menu-store.ts b/apps/common/features/src/shared/lib/use-menu-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-menu-store.ts rename to apps/common/features/src/shared/lib/use-menu-store.ts diff --git a/apps/common/feature/src/shared/lib/use-tus.ts b/apps/common/features/src/shared/lib/use-tus.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-tus.ts rename to apps/common/features/src/shared/lib/use-tus.ts diff --git a/apps/common/feature/src/shared/lib/use-user-store.ts b/apps/common/features/src/shared/lib/use-user-store.ts similarity index 100% rename from apps/common/feature/src/shared/lib/use-user-store.ts rename to apps/common/features/src/shared/lib/use-user-store.ts diff --git a/apps/common/feature/src/shared/model/api-interface.ts b/apps/common/features/src/shared/model/api-interface.ts similarity index 100% rename from apps/common/feature/src/shared/model/api-interface.ts rename to apps/common/features/src/shared/model/api-interface.ts diff --git a/apps/common/feature/src/shared/model/index.ts b/apps/common/features/src/shared/model/index.ts similarity index 100% rename from apps/common/feature/src/shared/model/index.ts rename to apps/common/features/src/shared/model/index.ts diff --git a/apps/common/feature/src/shared/providers/index.ts b/apps/common/features/src/shared/providers/index.ts similarity index 100% rename from apps/common/feature/src/shared/providers/index.ts rename to apps/common/features/src/shared/providers/index.ts diff --git a/apps/common/feature/src/shared/providers/store-provider.tsx b/apps/common/features/src/shared/providers/store-provider.tsx similarity index 100% rename from apps/common/feature/src/shared/providers/store-provider.tsx rename to apps/common/features/src/shared/providers/store-provider.tsx diff --git a/apps/common/feature/src/shared/store/alert-store.ts b/apps/common/features/src/shared/store/alert-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/alert-store.ts rename to apps/common/features/src/shared/store/alert-store.ts diff --git a/apps/common/feature/src/shared/store/attachment-store.ts b/apps/common/features/src/shared/store/attachment-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/attachment-store.ts rename to apps/common/features/src/shared/store/attachment-store.ts diff --git a/apps/common/feature/src/shared/store/index.ts b/apps/common/features/src/shared/store/index.ts similarity index 100% rename from apps/common/feature/src/shared/store/index.ts rename to apps/common/features/src/shared/store/index.ts diff --git a/apps/common/feature/src/shared/store/menu-store.ts b/apps/common/features/src/shared/store/menu-store.ts similarity index 90% rename from apps/common/feature/src/shared/store/menu-store.ts rename to apps/common/features/src/shared/store/menu-store.ts index f0c6340c9..9aec06603 100644 --- a/apps/common/feature/src/shared/store/menu-store.ts +++ b/apps/common/features/src/shared/store/menu-store.ts @@ -1,5 +1,5 @@ import { createStore } from "zustand/vanilla"; -import type { MenuItem } from "@common/components/api"; +import type { MenuItem } from "@/entities/menu"; export type MenuState = { currentMenuInfo: MenuItem | null; diff --git a/apps/common/feature/src/shared/store/user-store.ts b/apps/common/features/src/shared/store/user-store.ts similarity index 100% rename from apps/common/feature/src/shared/store/user-store.ts rename to apps/common/features/src/shared/store/user-store.ts diff --git a/apps/common/feature/src/shared/ui/alert-dialog.tsx b/apps/common/features/src/shared/ui/alert-dialog.tsx similarity index 96% rename from apps/common/feature/src/shared/ui/alert-dialog.tsx rename to apps/common/features/src/shared/ui/alert-dialog.tsx index 728276713..012d4ab86 100644 --- a/apps/common/feature/src/shared/ui/alert-dialog.tsx +++ b/apps/common/features/src/shared/ui/alert-dialog.tsx @@ -4,7 +4,7 @@ import { DialogFooter, DialogClose, Button, -} from "@common/components/ui"; +} from "@common/components"; import { useAlertStore } from "../lib/use-alert-store"; function AlertDialog() { diff --git a/apps/common/feature/src/shared/ui/confirm-dialog.tsx b/apps/common/features/src/shared/ui/confirm-dialog.tsx similarity index 76% rename from apps/common/feature/src/shared/ui/confirm-dialog.tsx rename to apps/common/features/src/shared/ui/confirm-dialog.tsx index 4223d197e..6a4996bfb 100644 --- a/apps/common/feature/src/shared/ui/confirm-dialog.tsx +++ b/apps/common/features/src/shared/ui/confirm-dialog.tsx @@ -6,25 +6,27 @@ import { DialogClose, Button, type ButtonProps, -} from "@common/components/ui"; +} from "@common/components"; type ConfirmDialogProps = { - open?: boolean; + isOpen?: boolean; confirmButtonProps?: ButtonProps & { label?: string }; cancelButtonProps?: ButtonProps & { label?: string }; }; function ConfirmDialog({ - open, + isOpen, confirmButtonProps, cancelButtonProps, }: ConfirmDialogProps) { - const { message, isOpen, onConfirm, onCancel } = useConfirm(); + const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm(); return ( - +
-

{message}

+

+ {message} +

diff --git a/apps/common/feature/src/shared/ui/form.tsx b/apps/common/features/src/shared/ui/form.tsx similarity index 98% rename from apps/common/feature/src/shared/ui/form.tsx rename to apps/common/features/src/shared/ui/form.tsx index bd579d263..f88fdf80d 100644 --- a/apps/common/feature/src/shared/ui/form.tsx +++ b/apps/common/features/src/shared/ui/form.tsx @@ -9,7 +9,7 @@ import { type FieldPath, type FieldValues, } from "react-hook-form"; -import { cn, Label } from "@common/components"; +import { Label, cn } from "@common/components"; const Form = FormProvider; diff --git a/apps/common/features/src/shared/ui/page-layout.tsx b/apps/common/features/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..bc9503dae --- /dev/null +++ b/apps/common/features/src/shared/ui/page-layout.tsx @@ -0,0 +1,18 @@ +import { PageTitle } from "@common/components"; + +type PageLayoutProps = { + title: string; + menuPaths: string[]; + children: React.ReactNode; +}; + +function PageLayout({ children, ...props }: PageLayoutProps) { + return ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/common/feature/src/shared/ui/skeleton-page.tsx b/apps/common/features/src/shared/ui/skeleton-page.tsx similarity index 78% rename from apps/common/feature/src/shared/ui/skeleton-page.tsx rename to apps/common/features/src/shared/ui/skeleton-page.tsx index fca5c8e76..cf788aca2 100644 --- a/apps/common/feature/src/shared/ui/skeleton-page.tsx +++ b/apps/common/features/src/shared/ui/skeleton-page.tsx @@ -1,4 +1,6 @@ -import { Skeleton } from "@common/components/ui/skeleton"; +"use client"; + +import { Skeleton } from "@common/components"; function SkeletonPage() { return ( diff --git a/apps/common/features/src/widgets/dnd-tree/index.ts b/apps/common/features/src/widgets/dnd-tree/index.ts new file mode 100644 index 000000000..ca1c20f8d --- /dev/null +++ b/apps/common/features/src/widgets/dnd-tree/index.ts @@ -0,0 +1,2 @@ +export { DndTree } from "./ui/dnd-tree"; +export { EditableNode } from "./ui/editable-node"; diff --git a/apps/common/features/src/widgets/dnd-tree/ui/custom-node.tsx b/apps/common/features/src/widgets/dnd-tree/ui/custom-node.tsx new file mode 100644 index 000000000..1176711f5 --- /dev/null +++ b/apps/common/features/src/widgets/dnd-tree/ui/custom-node.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; +import { Button, cn } from "@common/components"; +import { EditableNode } from "./editable-node"; + +interface TreeData extends Record { + children?: unknown[]; +} + +type CustomNodeProps = { + node: NodeModel; + depth: number; + isOpen: boolean; + isSelected: boolean; + isAdditionalParent: boolean; + treeMethods: TreeMethods | null; + readOnly: boolean; + onToggle: (id: NodeModel["id"]) => void; + onSelect: (node: NodeModel) => void; + updateNodeName: (id: NodeModel["id"], nodeName: string) => Promise; +}; + +function CustomNode({ + node, + depth, + isOpen, + isSelected, + isAdditionalParent, + treeMethods, + readOnly, + onToggle, + onSelect, + updateNodeName, +}: CustomNodeProps) { + const { id, text, data } = node; + const indent = depth * 10 + 5; + + const [editMode, setEditMode] = useState(false); + const [nodeName, setNodeName] = useState(text); + + const handleToggle = () => onToggle(id); + const handleSelect = () => onSelect(node); + + const handleSave = async () => { + await updateNodeName(id, nodeName); + setEditMode(false); + }; + + const handleCancel = () => { + setEditMode(false); + setNodeName(text); + }; + + useEffect(() => { + if (isAdditionalParent) treeMethods?.open(id); + }, [isAdditionalParent]); + + return ( +
+ {editMode ? ( + ) => + setNodeName(e.target.value) + } + onSave={handleSave} + onCancel={handleCancel} + /> + ) : ( + <> +
+ {data && data.children && data.children.length > 0 && ( + { + e.stopPropagation(); + handleToggle(); + }} + /> + )} + {text} +
+ + + )} +
+ ); +} + +export { CustomNode }; diff --git a/apps/common/features/src/widgets/dnd-tree/ui/dnd-tree.tsx b/apps/common/features/src/widgets/dnd-tree/ui/dnd-tree.tsx new file mode 100644 index 000000000..b1509006e --- /dev/null +++ b/apps/common/features/src/widgets/dnd-tree/ui/dnd-tree.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { type ReactElement, useRef } from "react"; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions, + type NodeModel, + type TreeMethods, + type DropOptions, + type Classes, +} from "@minoru/react-dnd-treeview"; +import { Button, cn } from "@common/components"; +import { CustomNode } from "./custom-node"; + +interface TreeData extends Record { + children?: unknown[]; +} + +type EditableNodeProps = { depth: number }; + +type DndTreeProps = { + tree: NodeModel[]; + selectedNodeId?: number; + classes?: { + wrapper?: string; + } & Classes; + renderEditableNode?: (props: EditableNodeProps) => ReactElement; + handleSelectNode?: (node: NodeModel) => void; + handleDropNode?: (newTree: NodeModel[], options: DropOptions) => void; + updateNodeName?: (id: NodeModel["id"], nodeName: string) => Promise; +}; + +function DndTree({ + tree, + selectedNodeId, + classes, + renderEditableNode, + handleSelectNode, + handleDropNode, + updateNodeName, +}: DndTreeProps) { + const treeMethods = useRef(null); + + const isAdditionalParent = (node: NodeModel): boolean => { + const additionalNode = tree.find((n) => n.id === -1); + if (!additionalNode) return false; + return additionalNode?.parent === node.id; + }; + + return ( + <> +
+ + , { depth, isOpen, onToggle }) => { + if (node.id === -1) + return renderEditableNode?.({ depth }) ?? <>; + return ( + handleSelectNode?.(node)} + updateNodeName={(id, nodeName) => + updateNodeName?.(id, nodeName) ?? Promise.resolve() + } + /> + ); + }} + onDrop={(newTree, options) => handleDropNode?.(newTree, options)} + classes={{ + root: "p-4", + container: "flex flex-col", + dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", + placeholder: "relative", + ...classes, + }} + sort={false} + dropTargetOffset={5} + insertDroppableFirst={false} + canDrop={(_, { dragSource, dropTargetId }) => { + if (dragSource?.parent === dropTargetId) { + return true; + } + }} + placeholderRender={(_, { depth }) => { + return ( +
+ ); + }} + /> + +
+
+ + +
+ + ); +} + +export { DndTree }; diff --git a/apps/common/features/src/widgets/dnd-tree/ui/editable-node.tsx b/apps/common/features/src/widgets/dnd-tree/ui/editable-node.tsx new file mode 100644 index 000000000..f2274f4d8 --- /dev/null +++ b/apps/common/features/src/widgets/dnd-tree/ui/editable-node.tsx @@ -0,0 +1,44 @@ +import type { ComponentProps } from "react"; +import { Button, Input } from "@common/components"; + +type EditableNodeProps = { + depth?: number; + onSave: () => void; + onCancel: () => void; +} & ComponentProps; + +function EditableNode({ + depth, + onSave, + onCancel, + ...props +}: EditableNodeProps) { + return ( +
+ +
+ + +
+
+ ); +} + +export { EditableNode }; diff --git a/apps/common/feature/src/widgets/form-wrapper/index.ts b/apps/common/features/src/widgets/form-wrapper/index.ts similarity index 100% rename from apps/common/feature/src/widgets/form-wrapper/index.ts rename to apps/common/features/src/widgets/form-wrapper/index.ts diff --git a/apps/common/feature/src/widgets/form-wrapper/ui/form-wrapper.tsx b/apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx similarity index 89% rename from apps/common/feature/src/widgets/form-wrapper/ui/form-wrapper.tsx rename to apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx index 19f941106..caba0b595 100644 --- a/apps/common/feature/src/widgets/form-wrapper/ui/form-wrapper.tsx +++ b/apps/common/features/src/widgets/form-wrapper/ui/form-wrapper.tsx @@ -1,15 +1,15 @@ import { useRouter } from "next/navigation"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { type FieldValues, type UseFormReturn } from "react-hook-form"; -import { Button } from "@common/components/ui"; +import { Button } from "@common/components"; import { Form } from "@/shared/ui/form"; type FormWrapperProps = { form: UseFormReturn; children: React.ReactNode; isModifyMode?: boolean; - handleSave: (data: T) => Promise; - handleDelete?: () => Promise; + handleSave: (data: T) => Promise; + handleDelete?: () => Promise; }; function FormWrapper({ @@ -54,7 +54,7 @@ function FormWrapper({ - {isModifyMode && ( + {isModifyMode && handleDelete && ( diff --git a/apps/common/features/src/widgets/post/index.ts b/apps/common/features/src/widgets/post/index.ts new file mode 100644 index 000000000..9c833e568 --- /dev/null +++ b/apps/common/features/src/widgets/post/index.ts @@ -0,0 +1 @@ +export { Post } from "./ui/post"; diff --git a/apps/common/features/src/widgets/post/ui/post-action.tsx b/apps/common/features/src/widgets/post/ui/post-action.tsx new file mode 100644 index 000000000..3756fd1aa --- /dev/null +++ b/apps/common/features/src/widgets/post/ui/post-action.tsx @@ -0,0 +1,51 @@ +import { useRouter } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { Button } from "@common/components"; +import { PostApi } from "@/entities/post"; + +type PostActionProps = { + bbsId: string; + pstNo: string; + children: React.ReactNode; +}; + +function PostAction({ children, bbsId, pstNo }: PostActionProps) { + const router = useRouter(); + const { confirm } = useConfirm(); + + const { mutate: deletePost } = useMutation({ + mutationFn: () => PostApi.deletePost(bbsId, pstNo), + onSuccess: () => { + router.back(); + }, + }); + + const onDelete = async () => { + if (await confirm("삭제하시겠습니까?")) deletePost(); + }; + + return ( +
+ {children} +
+ + + +
+
+ ); +} + +export { PostAction }; diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx new file mode 100644 index 000000000..5ed0235f3 --- /dev/null +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -0,0 +1,46 @@ +"use client"; + +import dayjs from "dayjs"; +import { useQuery } from "@tanstack/react-query"; +import { PostApi } from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { PostAction } from "./post-action"; + +function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + const { data: postData } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + }); + + return ( + +
+

{postData?.pstTtl}

+
+
    +
  • + 작성자: {postData?.wrtrNm} +
  • +
  • + 등록일: {dayjs(postData?.regDt).format("YYYY-MM-DD")} +
  • +
  • + 조회수: {postData?.inqNocs} +
  • +
+
+
+
+
+
+ 첨부파일 +
+
+ + ); +} + +export { Post }; diff --git a/apps/common/feature/tailwind.config.ts b/apps/common/features/tailwind.config.ts similarity index 100% rename from apps/common/feature/tailwind.config.ts rename to apps/common/features/tailwind.config.ts diff --git a/apps/common/feature/tsconfig.json b/apps/common/features/tsconfig.json similarity index 86% rename from apps/common/feature/tsconfig.json rename to apps/common/features/tsconfig.json index 60991f388..f0333a1b7 100644 --- a/apps/common/feature/tsconfig.json +++ b/apps/common/features/tsconfig.json @@ -25,9 +25,9 @@ "baseUrl": "./src", "paths": { "@/*": ["./*"], - "@common/components/*": ["../../../../packages/common/components/src/*"], - "@common/components": ["../../../../packages/common/components/src"], - "@common/network/*": ["../../../../packages/common/network/*"], + "@common/assets": ["../../../../packages/common/assets/dist/"], + "@common/components": ["../../../../packages/common/components/dist/"], + "@common/config": ["../../../../packages/common/config/dist/"], "@common/network": ["../../../../packages/common/network/"] } }, diff --git a/apps/cs/app/(with-base-layout)/bbs/page.tsx b/apps/cs/app/(with-base-layout)/bbs/page.tsx new file mode 100644 index 000000000..d9aab376a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/page.tsx @@ -0,0 +1,2 @@ +import { BbsPage } from "@/pages/bbs"; +export default BbsPage; diff --git a/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx b/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx new file mode 100644 index 000000000..07cc78b6f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/register/[id]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/bbs"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/bbs/register/page.tsx b/apps/cs/app/(with-base-layout)/bbs/register/page.tsx new file mode 100644 index 000000000..3e74aef97 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/bbs"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx new file mode 100644 index 000000000..aa9320b8f --- /dev/null +++ b/apps/cs/app/(with-base-layout)/contents/register/[sequential]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/contents"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ sequential: string }>; +}) { + const { sequential } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/contents/register/page.tsx b/apps/cs/app/(with-base-layout)/contents/register/page.tsx new file mode 100644 index 000000000..fc33228e2 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/contents/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/contents"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(with-base-layout)/layout.tsx index c3254a935..8af048390 100644 --- a/apps/cs/app/(with-base-layout)/layout.tsx +++ b/apps/cs/app/(with-base-layout)/layout.tsx @@ -2,8 +2,8 @@ import { usePathname, useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; -import type { MenuItem } from "@common/components/api"; -import { BaseLayout } from "@common/components/ui"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; diff --git a/apps/cs/app/(with-base-layout)/organization/page.tsx b/apps/cs/app/(with-base-layout)/organization/page.tsx new file mode 100644 index 000000000..39a15cf83 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/organization/page.tsx @@ -0,0 +1,2 @@ +import { OrganizationPage } from "@/pages/organization"; +export default OrganizationPage; diff --git a/apps/cs/env.d.ts b/apps/cs/env.d.ts deleted file mode 100644 index 31ff74044..000000000 --- a/apps/cs/env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly VITE_PORT: string; - readonly VITE_PROXY_HOST: string; - readonly VITE_SERVER_API_URL: string; - readonly VITE_SITE_ID: string; - readonly VITE_CLAIM_NAME: string; - readonly VITE_ACCESS_TOKEN: string; - readonly VITE_REFRESH_TOKEN: string; - readonly VITE_AUTH_USER_ID: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index 923069eb9..d56c1f8ab 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -13,7 +13,7 @@ export function middleware(req: NextRequest) { const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log("Rewriting to >>>>", newUrl); + console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -26,6 +26,9 @@ export function middleware(req: NextRequest) { if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); + console.log( + `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, + ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index 7cbf8ee2e..efcddfcd5 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -13,15 +13,16 @@ const nextConfig: NextConfig = { has: [ { type: "header", - key: "x-rewrite-me", + key: "referer", + value: "(.*)/pst(.*)", }, ], }, ], afterFiles: [ { - source: "/board/:path*", - destination: `${subDomain}/board/:path*`, + source: "/pst/:path*", + destination: `${subDomain}/pst/:path*`, }, ], fallback: [], diff --git a/apps/cs/package.json b/apps/cs/package.json index b2b5f2355..6e5f62b96 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -11,9 +11,10 @@ "dependencies": { "@common/assets": "workspace:*", "@common/components": "workspace:*", + "@common/config": "workspace:*", "@frontend-opensource/use-react-hooks": "^1.1.4", "@hookform/resolvers": "^4.1.3", - "@minoru/react-dnd-treeview": "^3.5.0", + "@minoru/react-dnd-treeview": "3.5.1-alpha.1", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.67.1", diff --git a/apps/cs/public/images/img_cc_by.png b/apps/cs/public/images/img_cc_by.png new file mode 100755 index 000000000..628cc2bfd Binary files /dev/null and b/apps/cs/public/images/img_cc_by.png differ diff --git a/apps/cs/public/images/img_cc_by_nc.png b/apps/cs/public/images/img_cc_by_nc.png new file mode 100755 index 000000000..86f60c5db Binary files /dev/null and b/apps/cs/public/images/img_cc_by_nc.png differ diff --git a/apps/cs/public/images/img_cc_by_nc_nd.png b/apps/cs/public/images/img_cc_by_nc_nd.png new file mode 100755 index 000000000..9d8ccb18d Binary files /dev/null and b/apps/cs/public/images/img_cc_by_nc_nd.png differ diff --git a/apps/cs/public/images/img_cc_by_nc_sa.png b/apps/cs/public/images/img_cc_by_nc_sa.png new file mode 100755 index 000000000..00f402c9e Binary files /dev/null and b/apps/cs/public/images/img_cc_by_nc_sa.png differ diff --git a/apps/cs/public/images/img_cc_by_nd.png b/apps/cs/public/images/img_cc_by_nd.png new file mode 100755 index 000000000..0da64acd8 Binary files /dev/null and b/apps/cs/public/images/img_cc_by_nd.png differ diff --git a/apps/cs/public/images/img_cc_by_sa.png b/apps/cs/public/images/img_cc_by_sa.png new file mode 100755 index 000000000..6d94e60b9 Binary files /dev/null and b/apps/cs/public/images/img_cc_by_sa.png differ diff --git a/apps/cs/public/images/img_opentype01.png b/apps/cs/public/images/img_opentype01.png new file mode 100755 index 000000000..456c7c6bb Binary files /dev/null and b/apps/cs/public/images/img_opentype01.png differ diff --git a/apps/cs/public/images/img_opentype02.png b/apps/cs/public/images/img_opentype02.png new file mode 100755 index 000000000..a6c5bfb32 Binary files /dev/null and b/apps/cs/public/images/img_opentype02.png differ diff --git a/apps/cs/public/images/img_opentype03.png b/apps/cs/public/images/img_opentype03.png new file mode 100755 index 000000000..970a6a0d4 Binary files /dev/null and b/apps/cs/public/images/img_opentype03.png differ diff --git a/apps/cs/public/images/img_opentype04.png b/apps/cs/public/images/img_opentype04.png new file mode 100755 index 000000000..41d284cab Binary files /dev/null and b/apps/cs/public/images/img_opentype04.png differ diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 6fd488b90..713bc172c 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -1,5 +1,7 @@ "use client"; +import { DndProvider } from "react-dnd"; +import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; import { AuthWrapper } from "@/entities/auth"; @@ -7,15 +9,23 @@ import { MenuWrapper } from "@/entities/menu"; import { StoreProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-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 ( - {children} + + + {children} + + diff --git a/apps/cs/src/entities/attachment/index.ts b/apps/cs/src/entities/attachment/index.ts index 182ef9fd6..385425537 100644 --- a/apps/cs/src/entities/attachment/index.ts +++ b/apps/cs/src/entities/attachment/index.ts @@ -1 +1,5 @@ export { AttachmentApi } from "./api/attachment-service"; +export type { + AttachmentParams, + Attachment, +} from "./model/attachment-interface"; diff --git a/apps/cs/src/entities/board/api/board-service.ts b/apps/cs/src/entities/board/api/board-service.ts index 9cf29599b..150ff677a 100644 --- a/apps/cs/src/entities/board/api/board-service.ts +++ b/apps/cs/src/entities/board/api/board-service.ts @@ -1,12 +1,24 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + Board, + BoardFormData, + BoardList, + BoardParams, BoardPopupList, BoardPopupParams, BoardService, } from "../model/board-interface"; export class BoardApiService implements BoardService { + public async getBoardList(params: BoardParams): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + { params }, + ); + return response.data; + } + public async getBoardPopupList( params: BoardPopupParams, ): Promise { @@ -16,6 +28,33 @@ export class BoardApiService implements BoardService { ); return response.data; } + + public async getBoardInfo(bbsId: string): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + ); + return response.data; + } + + public async createBoard(data: BoardFormData): Promise { + const response = await client.post( + `${ENDPOINT.BOARD_SERVICE.BBS}`, + data, + ); + return response.data; + } + + public async updateBoard(bbsId: string, data: BoardFormData): Promise { + const response = await client.put( + `${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`, + data, + ); + return response.data; + } + + public async deleteBoard(bbsId: string): Promise { + await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`); + } } export const BoardApi = new BoardApiService(); diff --git a/apps/cs/src/entities/board/index.ts b/apps/cs/src/entities/board/index.ts index 2173bb4a0..f9ef1b610 100644 --- a/apps/cs/src/entities/board/index.ts +++ b/apps/cs/src/entities/board/index.ts @@ -1,2 +1,8 @@ export { BoardApi } from "./api/board-service"; -export { type BoardPopup } from "./model/board-interface"; +export { + boardFormSchema, + type Board, + type BoardFormData, + type BoardParams, + type BoardPopup, +} from "./model/board-interface"; diff --git a/apps/cs/src/entities/board/model/board-interface.ts b/apps/cs/src/entities/board/model/board-interface.ts index 942840d74..1ed8f1d9f 100644 --- a/apps/cs/src/entities/board/model/board-interface.ts +++ b/apps/cs/src/entities/board/model/board-interface.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import type { PageableList, PageableParams } from "@/shared/model"; export const BOARD_TYPE = { @@ -9,13 +10,34 @@ export const BOARD_TYPE = { export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE]; +export interface BoardParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number; + bbsTypeCd?: string; + bbsNm?: string; + bbsExpln?: string; + rfrncUseYn?: string; + atflUseYn?: string; + atflMaxCnt?: number; + atflMaxFileSz?: number; + ansUseYn?: string; + cmntUseYn?: string; + dgstfnEvlUseYn?: string; + smsUseYn?: string; + upndFixUseYn?: string; + pstgPrdUseYn?: string; + popupUseYn?: string; + delYn?: string; +} + export interface BoardPopupParams extends PageableParams { bbsNm?: string; } -export interface BoardPopup { +export interface Board { bbsId: number; - bbsTypeCd: BoardType; + bbsTypeCd: string; bbsTypeNm: string; bbsNm: string; bbsExpln: string; @@ -24,6 +46,7 @@ export interface BoardPopup { atflMaxCnt: number; atflMaxFileSz: number; ansUseYn: string; + ansUseAuthrt: string; cmntUseYn: string; dgstfnEvlUseYn: string; smsUseYn: string; @@ -31,12 +54,43 @@ export interface BoardPopup { pstgPrdUseYn: string; popupUseYn: string; delYn: string; - regDt: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + +export interface BoardPopup extends Board { pstCnt: number; } +export const boardFormSchema = z.object({ + bbsTypeCd: z.string(), + bbsNm: z.string(), + bbsExpln: z.string(), + rfrncUseYn: z.string(), + atflUseYn: z.string(), + atflMaxCnt: z.number().nullish(), + atflMaxFileSz: z.number().nullish(), + ansUseYn: z.string(), + ansUseAuthrt: z.string().nullish(), + cmntUseYn: z.string(), + dgstfnEvlUseYn: z.string(), + smsUseYn: z.string(), + upndFixUseYn: z.string(), + pstgPrdUseYn: z.string(), + popupUseYn: z.string(), + useYn: z.string(), +}); + +export type BoardList = PageableList; export type BoardPopupList = PageableList; +export type BoardFormData = z.infer; export interface BoardService { + getBoardList: (params: BoardParams) => Promise; getBoardPopupList: (params: BoardPopupParams) => Promise; + getBoardInfo: (bbsId: string) => Promise; + createBoard: (data: BoardFormData) => Promise; + updateBoard: (bbsId: string, data: BoardFormData) => Promise; + deleteBoard: (bbsId: string) => Promise; } diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index 0b6633f4b..dc74bbc47 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -1 +1,3 @@ +export { CodeApi } from "./api/code-service"; +export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; diff --git a/apps/cs/src/entities/code/model/code-interface.ts b/apps/cs/src/entities/code/model/code-interface.ts index 0d066e6da..4562e2559 100644 --- a/apps/cs/src/entities/code/model/code-interface.ts +++ b/apps/cs/src/entities/code/model/code-interface.ts @@ -2,6 +2,7 @@ export interface ICode { lwpoCd: string; upCd: string; lwpoCdNm: string; + unit: string; } export interface CodeService { diff --git a/apps/cs/src/entities/content/api/contents-service.ts b/apps/cs/src/entities/content/api/contents-service.ts new file mode 100644 index 000000000..00d2a8c67 --- /dev/null +++ b/apps/cs/src/entities/content/api/contents-service.ts @@ -0,0 +1,90 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + ContentsService, + ContentsParams, + ContentsList, + ContentsFormData, + ContentsVersionParams, + ContentsVersionList, + ContentsDetail, +} from "../model/contents-interface"; + +export class ContentsApiService implements ContentsService { + public async getContentsList(params: ContentsParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS}`, + { params }, + ); + return response.data; + } + + public async getContentsVersionList( + params: ContentsVersionParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, + { params }, + ); + return response.data; + } + + public async getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + ); + return response.data; + } + + public async createContents(data: ContentsFormData): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`, + data, + ); + return response.data; + } + + public async updateContents( + contsSn: number, + contsVer: number, + data: ContentsFormData, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + data, + ); + return response.data; + } + + public async deleteContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`, + ); + } + + public async requestApprove( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`, + ); + } + + public async approveContents( + contsSn: number, + contsVer: number, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`, + ); + } +} + +export const ContentsApi = new ContentsApiService(); diff --git a/apps/cs/src/entities/content/index.ts b/apps/cs/src/entities/content/index.ts new file mode 100644 index 000000000..2446dd716 --- /dev/null +++ b/apps/cs/src/entities/content/index.ts @@ -0,0 +1,8 @@ +export { ContentsApi } from "./api/contents-service"; +export { + CONTENTS_APPROVE_STATUS, + contentsFormSchema, + type Contents, + type ContentsParams, + type ContentsFormData, +} from "./model/contents-interface"; diff --git a/apps/cs/src/entities/content/model/contents-interface.ts b/apps/cs/src/entities/content/model/contents-interface.ts new file mode 100644 index 000000000..72456149b --- /dev/null +++ b/apps/cs/src/entities/content/model/contents-interface.ts @@ -0,0 +1,100 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const CONTENTS_APPROVE_STATUS = { + DRAFT: "01", + REQUESTED: "02", + APPROVED: "99", +} as const; + +export type ContentsApproveStatus = + (typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS]; + +export interface ContentsParams extends PageableParams { + contsTtl?: string; + aprvContsVer?: number; + useYn?: string; +} + +export interface ContentsVersionParams extends PageableParams { + contsSn?: number; + contsCn?: string; + aprvCd?: ContentsApproveStatus; + cclCd?: string; + koglCd?: string; + tag?: string; + useYn?: string; +} + +export interface ContentsVersion { + contsSn: number; + contsVer: number; + contsCn: string; + useYn: string; +} + +export const contentsSchema = z.object({ + contsSn: z.number(), + contsTtl: z.string(), + aprvContsVer: z.number(), + useYn: z.string(), + mdfcnDt: z.string(), + mdfr: z.string(), + mdfrNm: z.string(), + aprvDt: z.string(), + autzrNm: z.string(), + workAprvCd: z.string(), + workAprvNm: z.string(), + workContsVer: z.number(), +}); + +export const contentsDetailSchema = z.object({ + contsSn: z.number(), + contsVer: z.number(), + contsTtl: z.string(), + contsCn: z.string(), + contsRmrk: z.string().nullish(), + aprvCd: z.enum([ + CONTENTS_APPROVE_STATUS.DRAFT, + CONTENTS_APPROVE_STATUS.REQUESTED, + CONTENTS_APPROVE_STATUS.APPROVED, + ]), + cclCd: z.string().nullish(), + koglCd: z.string().nullish(), + tag: z.string().nullish(), + aprvDt: z.string(), + autzr: z.string(), + useYn: z.string(), +}); + +export const contentsFormSchema = contentsDetailSchema.omit({ + contsVer: true, + aprvDt: true, + autzr: true, +}); + +export type Contents = z.infer; +export type ContentsDetail = z.infer; +export type ContentsFormData = z.infer; +export type ContentsList = PageableList; +export type ContentsVersionList = PageableList; + +export interface ContentsService { + getContentsList(params: ContentsParams): Promise; + getContentsVersionList( + params: ContentsVersionParams, + ): Promise; + getLatestContentsInfo( + contsSn: number, + contsVer: number, + ): Promise; + createContents(data: ContentsFormData): Promise; + updateContents( + contsSn: number, + contsVer: number, + data: ContentsFormData, + ): Promise; + deleteContents(contsSn: number, contsVer: number): Promise; + requestApprove(contsSn: number, contsVer: number): Promise; + approveContents(contsSn: number, contsVer: number): Promise; +} diff --git a/apps/cs/src/entities/contents/api/contents-service.ts b/apps/cs/src/entities/contents/api/contents-service.ts deleted file mode 100644 index 8d73d2bf0..000000000 --- a/apps/cs/src/entities/contents/api/contents-service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - ContentsService, - ContentsParams, - ContentsList, - Contents, - ContentsFormData, -} from "../model/contents-interface"; - -export class ContentsApiService implements ContentsService { - public async getContentsList(params: ContentsParams): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.CONTENTS}`, - { params }, - ); - return response.data; - } - - public async getContentsInfo(contsSn: number): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`, - ); - return response.data; - } - - public async createContents(data: ContentsFormData): Promise { - const response = await client.post( - `${ENDPOINT.CMS_SERVICE.CONTENTS}`, - data, - ); - return response.data; - } - - public async updateContents( - contsSn: number, - data: ContentsFormData, - ): Promise { - const response = await client.put( - `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`, - data, - ); - return response.data; - } - - public async deleteContents(contsSn: number): Promise { - await client.delete(`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`); - } -} - -export const ContentsApi = new ContentsApiService(); diff --git a/apps/cs/src/entities/contents/index.ts b/apps/cs/src/entities/contents/index.ts deleted file mode 100644 index 6c612996c..000000000 --- a/apps/cs/src/entities/contents/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ContentsApi } from "./api/contents-service"; -export type { Contents, ContentsParams } from "./model/contents-interface"; diff --git a/apps/cs/src/entities/contents/model/contents-interface.ts b/apps/cs/src/entities/contents/model/contents-interface.ts deleted file mode 100644 index d5a9f833d..000000000 --- a/apps/cs/src/entities/contents/model/contents-interface.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod"; -import type { PageableList, PageableParams } from "@/shared/model"; - -export interface ContentsParams extends PageableParams { - contsTtl?: string; - aprvContsVer?: number; - useYn?: string; -} - -export const contentsSchema = z.object({ - contsSn: z.number(), - contsTtl: z.string(), - aprvContsVer: z.number(), - useYn: z.string(), - regDt: z.string(), - mdfcnDt: z.string(), - rgTrNm: z.string(), - mdfrNm: z.string(), -}); - -export const contentsFormSchema = z.object({ - contsTtl: z.string(), - useYn: z.string(), - rgtr: z.string().nullish(), -}); - -export type Contents = z.infer; -export type ContentsFormData = z.infer; -export type ContentsList = PageableList; - -export interface ContentsService { - getContentsList(params: ContentsParams): Promise; - getContentsInfo(contsSn: number): Promise; - createContents(data: ContentsFormData): Promise; - updateContents(contsSn: number, data: ContentsFormData): Promise; - deleteContents(contsSn: number): Promise; -} diff --git a/apps/cs/src/entities/editor/ui/editor.tsx b/apps/cs/src/entities/editor/ui/editor.tsx index b5ef02153..797a6c1bf 100644 --- a/apps/cs/src/entities/editor/ui/editor.tsx +++ b/apps/cs/src/entities/editor/ui/editor.tsx @@ -49,6 +49,7 @@ const Editor = ({ ...props }: IAllProps) => { }} init={{ ...baseConfig.init, + disabled: props.disabled, images_upload_handler: imagesUploadHandler, file_picker_callback: filePickerCallback, }} diff --git a/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts b/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts index f9b01b4d8..22e3dcefa 100644 --- a/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts +++ b/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts @@ -3,7 +3,7 @@ import { ENDPOINT } from "@/shared/config"; import type { MenuAuthorityList, MenuAuthorityParams, - MenuAuthorityPayloadData, + MenuAuthorityPayload, MenuAuthorityService, } from "../model/menu-authority-interface"; @@ -18,9 +18,7 @@ export class MenuAuthorityApiService implements MenuAuthorityService { return response.data; } - public async saveMenuAuthority( - data: MenuAuthorityPayloadData[], - ): Promise { + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { const response = await client.post( `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, data, diff --git a/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts index d6775838c..7467728fe 100644 --- a/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts +++ b/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts @@ -30,12 +30,10 @@ export const menuAuthorityFormSchema = z.object({ export type MenuAuthority = z.infer; export type MenuAuthorityList = PageableList; -export type MenuAuthorityPayloadData = z.infer< - typeof menuAuthorityPayloadSchema ->; +export type MenuAuthorityPayload = z.infer; export type MenuAuthorityFormData = z.infer; export interface MenuAuthorityService { getMenuAuthorityList(params: MenuAuthorityParams): Promise; - saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; } diff --git a/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx index 1a9863e15..6969a85e9 100644 --- a/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx +++ b/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -1,10 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table"; import type { UseFormReturn } from "react-hook-form"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import type { MenuAuthority, MenuAuthorityFormData, - MenuAuthorityPayloadData, + MenuAuthorityPayload, } from "./menu-authority-interface"; export const createColumns = ( @@ -13,7 +13,7 @@ export const createColumns = ( const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; const handleCheckedChange = - (i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => { + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { if (getCurrentRow(i)?.mode === "R") { form.setValue(`menuAuthority.${i}.mode`, "U"); } diff --git a/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx index 62915e2ed..a6d4d2128 100644 --- a/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx +++ b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -13,13 +13,13 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@common/components/ui"; +} from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { menuAuthorityFormSchema, type MenuAuthorityFormData, - type MenuAuthorityPayloadData, + type MenuAuthorityPayload, } from "../model/menu-authority-interface"; import { createColumns } from "../model/menu-authority-table-columns"; import { MenuAuthorityApi } from "../api/menu-authority-service"; @@ -42,7 +42,7 @@ function ManagementAuthorityDialog({ }); const { mutateAsync: saveMenuAuthority } = useMutation({ - mutationFn: (data: MenuAuthorityPayloadData[]) => + mutationFn: (data: MenuAuthorityPayload[]) => MenuAuthorityApi.saveMenuAuthority(data), onSuccess: () => { alert("저장되었습니다."); diff --git a/apps/cs/src/entities/menu/api/menu-service.ts b/apps/cs/src/entities/menu/api/menu-service.ts index b6c1510cd..4424f8d37 100644 --- a/apps/cs/src/entities/menu/api/menu-service.ts +++ b/apps/cs/src/entities/menu/api/menu-service.ts @@ -1,4 +1,4 @@ -import { client } from "@common/components/api"; +import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { MenuItem, MenuService } from "../model/menu-interface"; diff --git a/apps/cs/src/entities/menu/index.ts b/apps/cs/src/entities/menu/index.ts index 1fa9dd218..04f63e38b 100644 --- a/apps/cs/src/entities/menu/index.ts +++ b/apps/cs/src/entities/menu/index.ts @@ -1 +1,2 @@ export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/cs/src/entities/organization/api/organization-service.ts b/apps/cs/src/entities/organization/api/organization-service.ts new file mode 100644 index 000000000..ae7fc44ac --- /dev/null +++ b/apps/cs/src/entities/organization/api/organization-service.ts @@ -0,0 +1,66 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import { + type Organization, + type OrganizationFormData, + type OrganizationService, + type OrganizationTree, + type OrganizationTreeForDnd, +} from "../model/organization-interface"; + +export class OrganizationApiService implements OrganizationService { + public async getOrganizationsTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/tree`, + ); + return response.data; + } + + public async getOrganizationInfo(ognzSn: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + ); + return response.data; + } + + public async createOrganization(data: OrganizationFormData): Promise { + await client.post(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, data); + } + + public async deleteOrganization(ognzSn: number): Promise { + await client.delete( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + ); + } + + public async updateOrganizationName( + ognzSn: number, + name: string, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}/${name}`, + ); + return response.data; + } + + public async updateOrganization( + ognzSn: number, + data: OrganizationFormData, + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`, + data, + ); + } + + public async updateOrganizationByDnd( + data: OrganizationTreeForDnd[], + ): Promise { + await client.put( + `${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`, + data, + ); + } +} + +export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/entities/organization/index.ts b/apps/cs/src/entities/organization/index.ts new file mode 100644 index 000000000..6b4cb0357 --- /dev/null +++ b/apps/cs/src/entities/organization/index.ts @@ -0,0 +1,8 @@ +export { OrganizationApi } from "./api/organization-service"; +export { + organizationFormSchema, + type OrganizationTree, + type OrganizationFormData, + type OrganizationTreeForDnd, +} from "./model/organization-interface"; +export { generateOrganizationTreeData, handleDropNode } from "./lib/utils"; diff --git a/apps/cs/src/entities/organization/lib/utils.ts b/apps/cs/src/entities/organization/lib/utils.ts new file mode 100644 index 000000000..45591fc9e --- /dev/null +++ b/apps/cs/src/entities/organization/lib/utils.ts @@ -0,0 +1,78 @@ +import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; +import type { + OrganizationTree, + OrganizationTreeForDnd, +} from "../model/organization-interface"; + +export const generateOrganizationTreeData = ( + data: OrganizationTree[], +): NodeModel[] => { + return data.reduce[]>((acc, menu) => { + const node: NodeModel = { + id: menu.ognzSn ?? 0, + parent: menu.upOgnzSn ?? 0, + text: menu.ognzNm, + droppable: true, + data: menu, + }; + acc.push(node); + + if (menu.children?.length) { + acc.push(...generateOrganizationTreeData(menu.children)); + } + + return acc; + }, []); +}; + +export const generateDndOrganizationTreeData = ( + treeData: NodeModel[], + options: DropOptions, +): OrganizationTreeForDnd | null => { + const { dragSourceId, dragSource, dropTargetId } = options; + if (!dragSource?.data) return null; + + const siblings = treeData.filter((node) => node.parent === dropTargetId); + const dragSourceIndex = siblings.findIndex( + (node) => node.id === dragSourceId, + ); + const prevSibling = siblings[dragSourceIndex - 1]; + const newSortSeq = + dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1; + + const treeNode: OrganizationTreeForDnd = { + ognzSn: dragSource.data.ognzSn, + ognzNm: dragSource.data.ognzNm, + ognzExpln: dragSource.data.ognzExpln, + upOgnzSn: dropTargetId === 0 ? null : (dropTargetId as number), + sortSeq: newSortSeq, + }; + + const childNodes = treeData.filter( + (node) => node.parent === dragSourceId && node.data, + ); + const result: OrganizationTreeForDnd = { + ...treeNode, + children: + childNodes?.map((child) => ({ + ognzSn: child.data?.ognzSn, + ognzNm: child.data?.ognzNm ?? "", + ognzExpln: child.data?.ognzExpln, + upOgnzSn: child.data?.upOgnzSn === 0 ? null : (dragSourceId as number), + sortSeq: child.data?.sortSeq ?? 1, + })) ?? [], + }; + + return result; +}; + +export const handleDropNode = + (callback: (newTree: OrganizationTreeForDnd) => void) => + ( + newTree: NodeModel[], + options: DropOptions, + ) => { + const newTreeData = generateDndOrganizationTreeData(newTree, options); + if (!newTreeData) return; + callback(newTreeData); + }; diff --git a/apps/cs/src/entities/organization/model/organization-interface.ts b/apps/cs/src/entities/organization/model/organization-interface.ts new file mode 100644 index 000000000..a63b85571 --- /dev/null +++ b/apps/cs/src/entities/organization/model/organization-interface.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +export const organizationSchema = z.object({ + ognzSn: z.number().nullish(), + ognzNm: z.string(), + ognzExpln: z.string().nullish(), + upOgnzSn: z.number().nullish(), + sortSeq: z.number(), + useYn: z.string().nullish(), +}); + +export const organizationFormSchema = organizationSchema.omit({ + ognzSn: true, +}); + +export type Organization = z.infer; +export type OrganizationFormData = z.infer; +export type OrganizationTree = Organization & { + children?: OrganizationTree[]; +}; +export type OrganizationTreeForDnd = Omit; + +export interface OrganizationService { + getOrganizationsTreeList(): Promise; + getOrganizationInfo(ognzSn: number): Promise; + createOrganization(data: OrganizationFormData): Promise; + deleteOrganization(ognzSn: number): Promise; + updateOrganizationName(ognzSn: number, name: string): Promise; + updateOrganization(ognzSn: number, data: OrganizationFormData): Promise; + updateOrganizationByDnd(data: OrganizationTreeForDnd[]): Promise; +} diff --git a/apps/cs/src/entities/posting/api/posting-service.ts b/apps/cs/src/entities/posting/api/posting-service.ts index 2fafd6557..d1360b46a 100644 --- a/apps/cs/src/entities/posting/api/posting-service.ts +++ b/apps/cs/src/entities/posting/api/posting-service.ts @@ -5,7 +5,7 @@ import type { PostService, PostingParams, Post, - PostPayloadData, + PostPayload, } from "../model/posting-interface"; export class PostingApiService implements PostService { @@ -19,7 +19,7 @@ export class PostingApiService implements PostService { return response.data; } - public async savePosting(data: PostPayloadData[]): Promise { + public async savePosting(data: PostPayload[]): Promise { const response = await client.post( `${ENDPOINT.CMS_SERVICE.POSTINGS}`, data, diff --git a/apps/cs/src/entities/posting/index.ts b/apps/cs/src/entities/posting/index.ts index c6029d757..75c5c69ed 100644 --- a/apps/cs/src/entities/posting/index.ts +++ b/apps/cs/src/entities/posting/index.ts @@ -2,6 +2,6 @@ export { PostingApi } from "./api/posting-service"; export { POSTING_TYPE, type Post, - type PostPayloadData, + type PostPayload, } from "./model/posting-interface"; export { PostingDialog } from "./ui/posting-dialog"; diff --git a/apps/cs/src/entities/posting/model/posting-interface.ts b/apps/cs/src/entities/posting/model/posting-interface.ts index b14b2ab66..72c49bb03 100644 --- a/apps/cs/src/entities/posting/model/posting-interface.ts +++ b/apps/cs/src/entities/posting/model/posting-interface.ts @@ -41,10 +41,10 @@ export const postingFormSchema = z.object({ }); export type Post = z.infer; -export type PostPayloadData = z.infer; +export type PostPayload = z.infer; export type PostingFormData = z.infer; export interface PostService { getPostingHistory: (params: PostingParams) => Promise>; - savePosting: (data: PostPayloadData[]) => Promise; + savePosting: (data: PostPayload[]) => Promise; } diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx index 9e27a4485..bbddf9778 100644 --- a/apps/cs/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -23,8 +23,8 @@ import { DateRangePicker, DialogClose, DialogFooter, -} from "@common/components/ui"; -import { cn } from "@common/components/lib"; + cn, +} from "@common/components"; import { CodeSelect } from "@/entities/code/@x/posting"; import { SiteApi, type Site } from "@/entities/site/@x/posting"; import type { PageableList } from "@/shared/model"; @@ -34,7 +34,7 @@ import { postingFormSchema, type PostingFormData, type PostingType, - type PostPayloadData, + type PostPayload, } from "../model/posting-interface"; import { useAlertStore } from "@/shared/lib/use-alert-store"; @@ -76,7 +76,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { }); const { mutateAsync: savePosting } = useMutation({ - mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { alert("저장되었습니다."); }, diff --git a/apps/cs/src/entities/user-organization/api/user-organization-service.ts b/apps/cs/src/entities/user-organization/api/user-organization-service.ts new file mode 100644 index 000000000..a4e588310 --- /dev/null +++ b/apps/cs/src/entities/user-organization/api/user-organization-service.ts @@ -0,0 +1,62 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + TreeUserOrganization, + UserOrganizationParams, + UserOrganizationList, + UserOrganizationService, + UserOrganizationPayload, + SearchUserParams, + SearchUserList, + User, + MoveUserOrganizationPayload, +} from "../model/user-organization-interface"; + +export class UserOrganizationApiService implements UserOrganizationService { + public async getUserOrganizationList( + params: UserOrganizationParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + { params }, + ); + return response.data; + } + + public async getUserOrganizationTreeList(): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, + ); + return response.data; + } + + public async searchUser(params: SearchUserParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`, + { params }, + ); + return response.data; + } + + public async saveUserOrganization( + data: UserOrganizationPayload[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + data, + ); + return response.data; + } + + public async moveUserOrganization( + data: MoveUserOrganizationPayload[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/move-ognz`, + data, + ); + return response.data; + } +} + +export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/cs/src/entities/user-organization/index.ts b/apps/cs/src/entities/user-organization/index.ts new file mode 100644 index 000000000..87317f1da --- /dev/null +++ b/apps/cs/src/entities/user-organization/index.ts @@ -0,0 +1,11 @@ +export { UserOrganizationApi } from "./api/user-organization-service"; +export { + userOrganizationFormSchema, + type TreeUserOrganization, + type TreeUser, + type UserOrganizationFormData, + type UserOrganizationPayload, + type User, + type SearchUser, + type MoveUserOrganizationPayload, +} from "./model/user-organization-interface"; diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts new file mode 100644 index 000000000..25d7c7032 --- /dev/null +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface UserOrganizationParams extends PageableParams { + keywordType?: string; + keyword?: string; + ognzSn?: number; + userUnqId?: string; + jbpsSeCd?: string; + jbgdSeCd?: string; +} + +export interface SearchUserParams extends PageableParams { + userNm?: string; + ogdpSeCd?: string; +} + +export interface User { + mode: string; + ognzSn: number; + userUnqId: string; + userUnqNm?: string; + jbpsSeCd: string; + jbpsSeNm?: string; + jbgdSeCd: string; + jbgdSeNm?: string; + sortSeq: number; + mdfcnDt?: string; + mdfr?: string; + mdfrNm?: string; +} + +export interface SearchUser { + userUnqId: string; + userNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + regDt: string; +} + +export interface TreeUser { + userUnqId: string; + userNm: string; + jbpsSeCd: string; + jbgdSeCd: string; +} + +export interface TreeUserOrganization { + ognzSn: number; + upOgnzSn: number; + ognzNm: string; + users: TreeUser[]; + children: TreeUserOrganization[]; +} + +export interface MoveUserOrganizationPayload { + ognzSn: number; + toOgnzSn: number; + userUnqId: string; +} + +export const userOrganizationPayloadSchema = z.object({ + mode: z.string(), + ognzSn: z.number(), + userUnqId: z.string(), + jbpsSeCd: z.string(), + jbgdSeCd: z.string(), + sortSeq: z.number(), +}); + +export const userOrganizationFormSchema = z.object({ + userOrganization: z.array(userOrganizationPayloadSchema), +}); + +export type UserOrganizationList = PageableList; +export type SearchUserList = PageableList; +export type UserOrganizationPayload = z.infer< + typeof userOrganizationPayloadSchema +>; +export type UserOrganizationFormData = z.infer< + typeof userOrganizationFormSchema +>; + +export interface UserOrganizationService { + getUserOrganizationList( + params: UserOrganizationParams, + ): Promise; + getUserOrganizationTreeList(): Promise; + searchUser(params: SearchUserParams): Promise; + saveUserOrganization(data: UserOrganizationPayload[]): Promise; + moveUserOrganization(data: MoveUserOrganizationPayload[]): Promise; +} diff --git a/apps/cs/src/pages/banner/model/banner-table-columns.tsx b/apps/cs/src/pages/banner/model/banner-table-columns.tsx index 74da7c31c..22c654a5a 100644 --- a/apps/cs/src/pages/banner/model/banner-table-columns.tsx +++ b/apps/cs/src/pages/banner/model/banner-table-columns.tsx @@ -4,7 +4,7 @@ import dayjs from "dayjs"; import Image from "next/image"; import { useQuery } from "@tanstack/react-query"; import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox, Skeleton } from "@common/components/ui"; +import { Checkbox, Skeleton } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; import { ENDPOINT } from "@/shared/config"; import type { Banner } from "./banner-interface"; diff --git a/apps/cs/src/pages/banner/model/posting-table-columns.tsx b/apps/cs/src/pages/banner/model/posting-table-columns.tsx index a43cf8693..3daee86f8 100644 --- a/apps/cs/src/pages/banner/model/posting-table-columns.tsx +++ b/apps/cs/src/pages/banner/model/posting-table-columns.tsx @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import type { Post } from "@/entities/posting"; export const columns: ColumnDef[] = [ diff --git a/apps/cs/src/pages/banner/ui/modify-page.tsx b/apps/cs/src/pages/banner/ui/modify-page.tsx index 4063b83c4..c8a53c423 100644 --- a/apps/cs/src/pages/banner/ui/modify-page.tsx +++ b/apps/cs/src/pages/banner/ui/modify-page.tsx @@ -11,8 +11,8 @@ import { type RowSelectionState, } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { Button, DataTable } from "@common/components/ui"; -import { PostingApi, type PostPayloadData } from "@/entities/posting"; +import { Button, DataTable } from "@common/components"; +import { PostingApi, type PostPayload } from "@/entities/posting"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { ENDPOINT } from "@/shared/config"; import { @@ -58,7 +58,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }); const { mutateAsync: savePosting } = useMutation({ - mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential], diff --git a/apps/cs/src/pages/bbs/index.ts b/apps/cs/src/pages/bbs/index.ts new file mode 100644 index 000000000..bed839012 --- /dev/null +++ b/apps/cs/src/pages/bbs/index.ts @@ -0,0 +1,3 @@ +export { BbsPage } from "./ui/bbs-page"; +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/bbs/model/table-columns.tsx b/apps/cs/src/pages/bbs/model/table-columns.tsx new file mode 100644 index 000000000..6ae60fe73 --- /dev/null +++ b/apps/cs/src/pages/bbs/model/table-columns.tsx @@ -0,0 +1,61 @@ +import dayjs from "dayjs"; +import { 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 { Button } from "@common/components"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row }) => row.index + 1, + }, + { + accessorKey: "bbsNm", + header: "게시판명", + }, + { + accessorKey: "bbsTypeNm", + header: "게시판유형", + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => + row.original.mdfcnDt + ? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD") + : "", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + id: "actions", + header: "관리", + cell: ({ row }) => , + }, +]; + +const ManageButton = ({ row }: { row: Row }) => { + const router = useRouter(); + const { bbsTypeCd, bbsId } = row.original; + const [, setBbsInfo] = useLocalStorage("bbs-info", ""); + return ( + + ); +}; diff --git a/apps/cs/src/pages/bbs/ui/bbs-form.tsx b/apps/cs/src/pages/bbs/ui/bbs-form.tsx new file mode 100644 index 000000000..ac2b5ebd5 --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/bbs-form.tsx @@ -0,0 +1,447 @@ +"use client"; + +import type { UseFormReturn } from "react-hook-form"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { FormWrapper } from "@/widgets/form-wrapper"; +import { CodeSelect } from "@/entities/code"; +import type { Board, BoardFormData } from "@/entities/board"; +import { FormControl, FormField, FormItem } from "@/shared/ui/form"; + +type BbsFormProps = { + form: UseFormReturn; + bbsId?: string; + handleSave: (data: BoardFormData) => Promise; +}; + +function BbsForm({ form, bbsId, ...props }: BbsFormProps) { + return ( + +
+ + + + + + + + 게시판명 + + ( + + + + + + )} + /> + + + + 게시판 설명 + + ( + + + + + + )} + /> + + + + 게시판 유형 + + ( + + + + + + )} + /> + + + + 참조 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 첨부파일 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 첨부파일 최대 개수 + + ( + + + { + const value = e.target.value; + if (isNaN(Number(value))) return; + field.onChange(value === "" ? null : Number(value)); + }} + className="w-[12rem] !text-[1.3rem]" + placeholder="최대 개수 입력" + disabled={form.watch("atflUseYn") === "N"} + /> + + + )} + /> + + + + 첨부파일 최대 크기 + + ( + + + { + const value = e.target.value; + if (isNaN(Number(value))) return; + field.onChange(value === "" ? null : Number(value)); + }} + className="w-[12rem] !text-[1.3rem]" + placeholder="최대 크기 입력" + disabled={form.watch("atflUseYn") === "N"} + /> + + + )} + /> + + + + 답글 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 답글 사용 권한 + + ( + + + + + + )} + /> + + + + 댓글 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 게시물평가 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 팝업 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + SMS알림 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 상단고정 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 게시기간 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { BbsForm }; diff --git a/apps/cs/src/pages/bbs/ui/bbs-page.tsx b/apps/cs/src/pages/bbs/ui/bbs-page.tsx new file mode 100644 index 000000000..89562cb9c --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/bbs-page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DataTable, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { CodeSelect } from "@/entities/code"; +import { BoardApi, type BoardParams } from "@/entities/board"; +import { ENDPOINT } from "@/shared/config"; +import { type Board } from "@/entities/board"; +import { columns } from "../model/table-columns"; + +function BbsPage() { + const router = useRouter(); + + const [rowSelection, setRowSelection] = useState({}); + const [boardParams, setBoardParams] = useState({ + bbsTypeCd: "", + bbsNm: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: boardList, refetch } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS], + queryFn: () => + BoardApi.getBoardList({ + ...boardParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: false, + }); + + const table = useReactTable({ + data: boardList?.content ?? [], + columns, + state: { + pagination, + rowSelection, + }, + rowCount: boardList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Board) => { + router.push(`/bbs/register/${row.bbsId}`); + }; + + useEffect(() => { + refetch(); + }, [pagination]); + + return ( +
+
+
+ + + setBoardParams({ ...boardParams, bbsTypeCd: value }) + } + /> + + + setBoardParams({ ...boardParams, bbsNm: e.target.value }) + } + /> + + +
+
+
+
+

게시판 목록

+
+ + +
+
+
+
+ +
+
+
+ ); +} + +export { BbsPage }; diff --git a/apps/cs/src/pages/bbs/ui/create-page.tsx b/apps/cs/src/pages/bbs/ui/create-page.tsx new file mode 100644 index 000000000..bb710c33b --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/create-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 { useMutation } from "@tanstack/react-query"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { + BoardApi, + boardFormSchema, + type BoardFormData, +} from "@/entities/board"; +import { BbsForm } from "./bbs-form"; + +function CreatePage() { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createBoard } = useMutation({ + mutationFn: (data: BoardFormData) => BoardApi.createBoard(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(boardFormSchema), + defaultValues: { + bbsTypeCd: "", + bbsNm: "", + bbsExpln: "", + rfrncUseYn: "Y", + atflUseYn: "Y", + ansUseYn: "Y", + cmntUseYn: "Y", + dgstfnEvlUseYn: "Y", + smsUseYn: "Y", + upndFixUseYn: "Y", + pstgPrdUseYn: "Y", + popupUseYn: "Y", + useYn: "Y", + }, + }); + + return ( +
+
+

게시판 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/bbs/ui/modify-page.tsx b/apps/cs/src/pages/bbs/ui/modify-page.tsx new file mode 100644 index 000000000..f411cbd5c --- /dev/null +++ b/apps/cs/src/pages/bbs/ui/modify-page.tsx @@ -0,0 +1,45 @@ +"use client"; + +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 { ENDPOINT } from "@/shared/config"; +import { + BoardApi, + boardFormSchema, + type BoardFormData, +} from "@/entities/board"; +import { BbsForm } from "./bbs-form"; + +function ModifyPage({ bbsId }: { bbsId: string }) { + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: boardInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId], + queryFn: () => BoardApi.getBoardInfo(bbsId), + }); + + const { mutateAsync: updateBoard } = useMutation({ + mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data), + onSuccess: () => { + alert("수정되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(boardFormSchema), + values: boardInfo, + }); + + return ( +
+
+

게시판 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/contents/index.ts b/apps/cs/src/pages/contents/index.ts index c14f174e2..2bb0d4fb2 100644 --- a/apps/cs/src/pages/contents/index.ts +++ b/apps/cs/src/pages/contents/index.ts @@ -1 +1,3 @@ export { ContentsPage } from "./ui/contents-page"; +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; diff --git a/apps/cs/src/pages/contents/model/table-columns.tsx b/apps/cs/src/pages/contents/model/table-columns.tsx index 8790c2176..19d98a48b 100644 --- a/apps/cs/src/pages/contents/model/table-columns.tsx +++ b/apps/cs/src/pages/contents/model/table-columns.tsx @@ -1,47 +1,68 @@ import dayjs from "dayjs"; -import { type ColumnDef } from "@tanstack/react-table"; -import type { Contents } from "@/entities/contents"; +import { createColumnHelper } from "@tanstack/react-table"; +import type { Contents } from "@/entities/content"; -export const columns: ColumnDef[] = [ - { +const columnHelper = createColumnHelper(); + +export const columns = [ + columnHelper.display({ id: "no", header: "NO", cell: ({ row }) => row.index + 1, - }, - { - accessorKey: "contsTtl", + }), + columnHelper.accessor("contsTtl", { + id: "contsTtl", header: "콘텐츠명", - }, - { - accessorKey: "aprvContsVer", - header: "승인여부", - }, - { - accessorKey: "rgTrNm", - header: "승인자", - }, - { - accessorKey: "regDt", - header: "승인일자", - cell: ({ row }) => { - const date = dayjs(row.original.regDt); - return date.format("YYYY-MM-DD"); - }, - }, - { - accessorKey: "useYn", + }), + columnHelper.group({ + id: "workInfo", + header: "작성정보", + columns: [ + columnHelper.accessor("workContsVer", { + header: "작성버전", + cell: ({ row }) => + row.original.workContsVer ? `rev. ${row.original.workContsVer}` : "", + }), + columnHelper.accessor("workAprvNm", { + header: "작성상태", + }), + ], + }), + columnHelper.group({ + id: "aprvInfo", + header: "승인정보", + columns: [ + columnHelper.accessor("aprvContsVer", { + header: "승인버전", + cell: ({ row }) => + row.original.aprvContsVer ? `rev. ${row.original.aprvContsVer}` : "", + }), + columnHelper.accessor("autzrNm", { + header: "승인자", + }), + columnHelper.accessor("aprvDt", { + header: "승인일자", + cell: ({ row }) => + row.original.aprvDt + ? dayjs(row.original.aprvDt).format("YYYY-MM-DD") + : "", + }), + ], + }), + columnHelper.accessor("useYn", { + id: "useYn", header: "사용여부", - }, - { - accessorKey: "mdfrNm", + }), + columnHelper.accessor("mdfrNm", { + id: "mdfrNm", header: "최종수정자", - }, - { - accessorKey: "mdfcnDt", + }), + columnHelper.accessor("mdfcnDt", { + id: "mdfcnDt", header: "최종수정일", - cell: ({ row }) => { - const date = dayjs(row.original.mdfcnDt); - return date.format("YYYY-MM-DD"); - }, - }, + cell: ({ row }) => + row.original.mdfcnDt + ? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD") + : "", + }), ]; diff --git a/apps/cs/src/pages/contents/ui/contents-form.tsx b/apps/cs/src/pages/contents/ui/contents-form.tsx new file mode 100644 index 000000000..823d30173 --- /dev/null +++ b/apps/cs/src/pages/contents/ui/contents-form.tsx @@ -0,0 +1,268 @@ +import dynamic from "next/dynamic"; +import { type UseFormReturn } from "react-hook-form"; +import { + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + 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 { ENDPOINT } from "@/shared/config"; +import { CodeApi } from "@/entities/code/api/code-service"; +import Image from "next/image"; + +const Editor = dynamic( + () => import("@/entities/editor").then((module) => module.Editor), + { ssr: false }, +); + +interface ContentsFormProps { + form: UseFormReturn; + contsSn?: number; + isEditable?: boolean; + isContsTtlDisabled?: boolean; + handleSave: (data: ContentsFormData) => Promise; + handleDelete?: () => Promise; +} + +function ContentsForm({ + form, + contsSn, + isEditable, + isContsTtlDisabled, + ...props +}: ContentsFormProps) { + const { data: cclCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"], + queryFn: () => CodeApi.getCode("CCLCD"), + }); + + const { data: koglCode } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"], + queryFn: () => CodeApi.getCode("KOGLCD"), + }); + + return ( + +
+ + + + + + + + 콘텐츠 제목 + + ( + + + + + + )} + /> + + + + 콘텐츠 내용 + + ( + + + + + + )} + /> + + + + CCL + + ( + + + +
+ + +
+ {cclCode?.map((code) => ( +
+ + +
+ ))} +
+
+
+ )} + /> +
+
+ + 공공누리 + + ( + + + +
+ + +
+ {koglCode?.map((code) => ( +
+ + +
+ ))} +
+
+
+ )} + /> +
+
+ + 태그 + + ( + + + + + + )} + /> + + + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ ); +} + +export { ContentsForm }; diff --git a/apps/cs/src/pages/contents/ui/contents-page.tsx b/apps/cs/src/pages/contents/ui/contents-page.tsx index 2687b2606..dd47313e7 100644 --- a/apps/cs/src/pages/contents/ui/contents-page.tsx +++ b/apps/cs/src/pages/contents/ui/contents-page.tsx @@ -13,7 +13,7 @@ import { ContentsApi, type Contents, type ContentsParams, -} from "@/entities/contents"; +} from "@/entities/content"; import { ENDPOINT } from "@/shared/config"; import { columns } from "../model/table-columns"; @@ -89,8 +89,13 @@ function ContentsPage() { 등록
-
- +
+
diff --git a/apps/cs/src/pages/contents/ui/create-page.tsx b/apps/cs/src/pages/contents/ui/create-page.tsx new file mode 100644 index 000000000..8f7c662f3 --- /dev/null +++ b/apps/cs/src/pages/contents/ui/create-page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { useMutation } from "@tanstack/react-query"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + ContentsApi, + contentsFormSchema, + type ContentsFormData, +} from "@/entities/content"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { ContentsForm } from "./contents-form"; + +function CreatePage() { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createContents } = useMutation({ + mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(contentsFormSchema), + defaultValues: { + useYn: "Y", + }, + }); + + return ( +
+
+

콘텐츠 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/contents/ui/modify-page.tsx b/apps/cs/src/pages/contents/ui/modify-page.tsx new file mode 100644 index 000000000..8986648c7 --- /dev/null +++ b/apps/cs/src/pages/contents/ui/modify-page.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useMutation, 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, + SelectValue, + SelectContent, + SelectItem, + Button, +} from "@common/components"; + +function ModifyPage({ sequential }: { sequential: string }) { + const { confirm } = useConfirm(); + const queryClient = useQueryClient(); + const { setMessage: alert } = useAlertStore((state) => state); + + const contsSn = Number(sequential); + const [contsVer, setContsVer] = useState(0); + + const { data: contentsVersionList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], + queryFn: () => + ContentsApi.getContentsVersionList({ + contsSn: contsSn, + page: 0, + size: 100, + }), + }); + + const { data: contentsDetail } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsVer], + queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contsVer), + enabled: !!contsVer, + }); + + const { mutateAsync: createContents } = useMutation({ + mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data), + onSuccess: () => { + setContsVer(0); + alert("수정되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], + }); + }, + }); + + const { mutateAsync: updateContents } = useMutation({ + mutationFn: (data: ContentsFormData) => + ContentsApi.updateContents(contsSn, contsVer, data), + onSuccess: () => { + alert("수정되었습니다."); + }, + }); + + const { mutateAsync: deleteContents } = useMutation({ + mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer), + onSuccess: () => { + setContsVer(0); + alert("삭제되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn], + }); + }, + }); + + const { mutateAsync: requestApprove } = useMutation({ + mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer), + onSuccess: () => { + alert("승인 요청이 완료되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS], + }); + }, + }); + + const { mutateAsync: approveContents } = useMutation({ + mutationFn: () => ContentsApi.approveContents(contsSn, contsVer), + onSuccess: () => { + alert("승인되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS], + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(contentsFormSchema), + values: contentsDetail, + }); + + const approveStatus = contentsDetail?.aprvCd; + const isContsTtlDisabled = + (contentsVersionList?.content?.[0]?.contsVer ?? 0) > 1; + + const onRequestApprove = async () => { + const message = + "승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?"; + if (await confirm(message)) requestApprove(); + }; + + const onApprove = async () => { + const message = "승인하시겠습니까?"; + if (await confirm(message)) approveContents(); + }; + + useEffect(() => { + if (contsVer) return; + if (!contentsVersionList?.content?.length) return; + + const latestContent = contentsVersionList?.content[0]; + if (!latestContent) return; + + setContsVer(latestContent?.contsVer ?? 0); + }, [contentsVersionList?.content]); + + return ( +
+
+

템플릿 수정

+
+ + {approveStatus === CONTENTS_APPROVE_STATUS.DRAFT && ( + + )} + {approveStatus === CONTENTS_APPROVE_STATUS.REQUESTED && ( + <> + + + + )} +
+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/file/model/table-columns.tsx b/apps/cs/src/pages/file/model/table-columns.tsx index 2a73bf54a..1974808f5 100644 --- a/apps/cs/src/pages/file/model/table-columns.tsx +++ b/apps/cs/src/pages/file/model/table-columns.tsx @@ -3,7 +3,7 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; import { Button } from "@common/components"; -import type { Attachment } from "@/entities/attachment/model/attachment-interface"; +import type { Attachment } from "@/entities/attachment"; export const createColumns = ( handleDeleteAttachment: (atflId: string) => void, @@ -28,6 +28,10 @@ export const createColumns = ( { accessorKey: "atflFlieSz", header: "파일크기", + cell: ({ row }) => { + const size = row.getValue("atflFlieSz"); + return size ? formatFileSize(Number(size)) : "0 Bytes"; + }, }, { @@ -60,3 +64,13 @@ export const createColumns = ( ), }, ]; + +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; +}; diff --git a/apps/cs/src/pages/file/ui/file-page.tsx b/apps/cs/src/pages/file/ui/file-page.tsx index c896531bb..4f6b79781 100644 --- a/apps/cs/src/pages/file/ui/file-page.tsx +++ b/apps/cs/src/pages/file/ui/file-page.tsx @@ -9,8 +9,7 @@ import { } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, DataTable, Input, Label } from "@common/components"; -import type { AttachmentParams } from "@/entities/attachment/model/attachment-interface"; -import { AttachmentApi } from "@/entities/attachment/api/attachment-service"; +import { AttachmentApi, type AttachmentParams } from "@/entities/attachment"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { ENDPOINT } from "@/shared/config"; import { createColumns } from "../model/table-columns"; diff --git a/apps/cs/src/pages/image/model/image-table-columns.tsx b/apps/cs/src/pages/image/model/image-table-columns.tsx index dd80ad9c7..535ed16f2 100644 --- a/apps/cs/src/pages/image/model/image-table-columns.tsx +++ b/apps/cs/src/pages/image/model/image-table-columns.tsx @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import { type ColumnDef } from "@tanstack/react-table"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import { type Image } from "./image-interface"; import { IMAGE_TYPE } from "./image-interface"; diff --git a/apps/cs/src/pages/image/model/posting-table-columns.tsx b/apps/cs/src/pages/image/model/posting-table-columns.tsx index a43cf8693..3daee86f8 100644 --- a/apps/cs/src/pages/image/model/posting-table-columns.tsx +++ b/apps/cs/src/pages/image/model/posting-table-columns.tsx @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import type { Post } from "@/entities/posting"; export const columns: ColumnDef[] = [ diff --git a/apps/cs/src/pages/image/ui/modify-page.tsx b/apps/cs/src/pages/image/ui/modify-page.tsx index 79e26c811..16163688e 100644 --- a/apps/cs/src/pages/image/ui/modify-page.tsx +++ b/apps/cs/src/pages/image/ui/modify-page.tsx @@ -11,8 +11,8 @@ import { type RowSelectionState, } from "@tanstack/react-table"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { Button, DataTable } from "@common/components/ui"; -import { PostingApi, type PostPayloadData } from "@/entities/posting"; +import { Button, DataTable } from "@common/components"; +import { PostingApi, type PostPayload } from "@/entities/posting"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { ENDPOINT } from "@/shared/config"; import { imageFormSchema, type ImageFormData } from "../model/image-interface"; @@ -54,7 +54,7 @@ function ModifyPage({ sequential }: { sequential: string }) { }); const { mutateAsync: savePosting } = useMutation({ - mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), + mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential], diff --git a/apps/cs/src/pages/menu/api/organization-service.ts b/apps/cs/src/pages/menu/api/organization-service.ts deleted file mode 100644 index 9409f81b5..000000000 --- a/apps/cs/src/pages/menu/api/organization-service.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { client } from "@/shared/api"; -import { ENDPOINT } from "@/shared/config"; -import type { - Organization, - OrganizationService, -} from "../model/organization-interface"; - -export class OrganizationApiService implements OrganizationService { - public async getOrganizationUserTreeList(): Promise { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, - ); - return response.data; - } -} - -export const OrganizationApi = new OrganizationApiService(); diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index b1dbafe5f..f6d13be33 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -1,6 +1,9 @@ import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview"; +import type { + TreeUserOrganization, + TreeUser, +} from "@/entities/user-organization"; import type { TreeMenu, TreeNode } from "../model/menu-interface"; -import type { Organization, User } from "../model/organization-interface"; export const generateMenuTreeData = ( data: TreeMenu[], @@ -107,44 +110,47 @@ export const handleNodeTextChange = callback(newTree); }; -export const generateOrganizationTreeData = ( - organizations: Organization[], -): NodeModel[] => { - return organizations.reduce[]>((acc, org) => { - const orgNode: NodeModel = { - id: org.ognzId, - parent: org.upOgnzId ?? 0, - text: org.ognzNm, - data: org, - droppable: false, - }; +export const generateUserOrganizationTreeData = ( + organizations: TreeUserOrganization[], +): NodeModel[] => { + return organizations.reduce[]>( + (acc, org) => { + const orgNode: NodeModel = { + id: org.ognzSn, + parent: org.upOgnzSn ?? 0, + text: org.ognzNm, + data: org, + droppable: false, + }; - acc.push(orgNode); + acc.push(orgNode); - if (org.children?.length) { - acc.push(...generateOrganizationTreeData(org.children)); - } + if (org.children?.length) { + acc.push(...generateUserOrganizationTreeData(org.children)); + } - if (org.users?.length) { - acc.push( - ...org.users.map((user) => ({ - id: user.userUnqId, - parent: org.ognzId, - text: user.userNm, - data: user, - droppable: false, - })), - ); - } + if (org.users?.length) { + acc.push( + ...org.users.map((user) => ({ + id: user.userUnqId, + parent: org.ognzSn, + text: user.userNm, + data: user, + droppable: false, + })), + ); + } - return acc; - }, []); + return acc; + }, + [], + ); }; -export const filterOrganizationTreeDataByKeyword = ( - treeData: NodeModel[], +export const filterUserOrganizationTreeDataByKeyword = ( + treeData: NodeModel[], keyword: string, -): NodeModel[] => { +): NodeModel[] => { return Array.from( treeData.reduce((matchedNodes, node) => { if (!node.text.includes(keyword)) return matchedNodes; @@ -160,6 +166,6 @@ export const filterOrganizationTreeDataByKeyword = ( } return matchedNodes; - }, new Set>()), + }, new Set>()), ); }; diff --git a/apps/cs/src/pages/menu/model/board-table-columns.tsx b/apps/cs/src/pages/menu/model/board-table-columns.tsx index 4cdf29498..ba78b06c7 100644 --- a/apps/cs/src/pages/menu/model/board-table-columns.tsx +++ b/apps/cs/src/pages/menu/model/board-table-columns.tsx @@ -1,5 +1,5 @@ import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox } from "@common/components/ui"; +import { Checkbox } from "@common/components"; import type { BoardPopup } from "@/entities/board"; export const columns: ColumnDef[] = [ diff --git a/apps/cs/src/pages/menu/model/content-table-colums.tsx b/apps/cs/src/pages/menu/model/contents-table-colums.tsx similarity index 85% rename from apps/cs/src/pages/menu/model/content-table-colums.tsx rename to apps/cs/src/pages/menu/model/contents-table-colums.tsx index 8670058bc..0f274210b 100644 --- a/apps/cs/src/pages/menu/model/content-table-colums.tsx +++ b/apps/cs/src/pages/menu/model/contents-table-colums.tsx @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { ColumnDef } from "@tanstack/react-table"; -import { Checkbox } from "@common/components/ui"; -import type { Contents } from "@/entities/contents"; +import { Checkbox } from "@common/components"; +import type { Contents } from "@/entities/content"; export const columns: ColumnDef[] = [ { @@ -19,10 +19,10 @@ export const columns: ColumnDef[] = [ header: "승인자", }, { - accessorKey: "regDt", + accessorKey: "aprvDt", header: "승인일자", cell: ({ row }) => { - const date = dayjs(row.original.regDt); + const date = dayjs(row.original.aprvDt); return date.format("YYYY-MM-DD"); }, }, diff --git a/apps/cs/src/pages/menu/model/organization-interface.ts b/apps/cs/src/pages/menu/model/organization-interface.ts deleted file mode 100644 index 86a8b8e31..000000000 --- a/apps/cs/src/pages/menu/model/organization-interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface User { - userUnqId: string; - userNm: string; - jbpsSeCd: string; - jbgdSeCd: string; -} - -export interface Organization { - ognzId: string; - upOgnzId: string; - ognzNm: string; - users: User[]; - children: Organization[]; -} - -export interface OrganizationService { - getOrganizationUserTreeList(): Promise; -} diff --git a/apps/cs/src/pages/menu/ui/board-search-dialog.tsx b/apps/cs/src/pages/menu/ui/board-search-dialog.tsx index 531e17c86..0afb6b1bd 100644 --- a/apps/cs/src/pages/menu/ui/board-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/board-search-dialog.tsx @@ -14,7 +14,7 @@ import { DialogHeader, DialogTitle, Input, -} from "@common/components/ui"; +} from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { BoardApi, type BoardPopup } from "@/entities/board"; import { columns } from "../model/board-table-columns"; diff --git a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx index a3910eda9..d877e1b83 100644 --- a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx @@ -14,10 +14,10 @@ import { DialogHeader, DialogTitle, Input, -} from "@common/components/ui"; +} from "@common/components"; import { ENDPOINT } from "@/shared/config"; -import { ContentsApi, type Contents } from "@/entities/contents"; -import { columns } from "../model/content-table-colums"; +import { ContentsApi, type Contents } from "@/entities/content"; +import { columns } from "../model/contents-table-colums"; type ContentSearchDialogProps = { handleContentSelect: (content: Contents) => void; diff --git a/apps/cs/src/pages/menu/ui/editable-node.tsx b/apps/cs/src/pages/menu/ui/editable-node.tsx index 9ada835e2..f2274f4d8 100644 --- a/apps/cs/src/pages/menu/ui/editable-node.tsx +++ b/apps/cs/src/pages/menu/ui/editable-node.tsx @@ -1,5 +1,5 @@ import type { ComponentProps } from "react"; -import { Button, Input } from "@common/components/ui"; +import { Button, Input } from "@common/components"; type EditableNodeProps = { depth?: number; diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index 1cde10b99..f6bb7ee6e 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; -import { Button } from "@common/components/ui"; -import { cn } from "@common/components/lib"; +import { Button, cn } from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { MenuApi } from "../api/menu-service"; import type { TreeMenu } from "../model/menu-interface"; diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 5f5dd6c70..19ee1478b 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -13,7 +13,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@common/components/ui"; +} from "@common/components"; import { SiteApi } from "@/entities/site"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; @@ -44,7 +44,7 @@ function MenuPage() { enabled: !!selectedMenu?.menuCd, }); - const { data: treeData } = useQuery({ + const { data: menuTreeData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], queryFn: () => MenuApi.getTreeMenuList(siteId), select: (data) => { @@ -187,8 +187,8 @@ function MenuPage() { }; const tree = additionalNode - ? [...(additionalNode ? [additionalNode] : []), ...(treeData ?? [])] - : (treeData ?? []); + ? [...(additionalNode ? [additionalNode] : []), ...(menuTreeData ?? [])] + : (menuTreeData ?? []); return (
diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index 45a4cbb37..0a926a27b 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -1,17 +1,14 @@ "use client"; import { useRef } from "react"; -import { DndProvider } from "react-dnd"; import type { UseFormReturn } from "react-hook-form"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Tree, - MultiBackend, - getBackendOptions, type NodeModel, type TreeMethods, } from "@minoru/react-dnd-treeview"; -import { Button } from "@common/components/ui"; +import { Button } from "@common/components"; import { ENDPOINT } from "@/shared/config"; import { MenuApi } from "../api/menu-service"; import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface"; @@ -61,66 +58,61 @@ function MenuTree({ return ( <>
- - , - { depth, isOpen, onToggle }, - ) => { - if (node.id === -1) { - return ( - - additionalNodeForm.setValue("menuKornNm", e.target.value) - } - depth={depth} - onSave={onAdditionalNodeSave} - onCancel={onAdditionalNodeCancel} - /> - ); - } + , { depth, isOpen, onToggle }) => { + if (node.id === -1) { return ( - + additionalNodeForm.setValue("menuKornNm", e.target.value) + } depth={depth} - treeMethods={treeMethods.current} - isOpen={isOpen} - isSelected={selectedMenu?.menuCd === node.id} - isAdditionalParent={isAdditionalParent(node)} - onToggle={onToggle} - onSelect={handleSelectNode(onSelectNode)} + onSave={onAdditionalNodeSave} + onCancel={onAdditionalNodeCancel} /> ); - }} - onDrop={handleDropNode(updateTreeMenu)} - classes={{ - root: "p-4", - container: "flex flex-col", - dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", - placeholder: "relative", - }} - sort={false} - dropTargetOffset={5} - insertDroppableFirst={false} - canDrop={(_, { dragSource, dropTargetId }) => { - if (dragSource?.parent === dropTargetId) { - return true; - } - }} - placeholderRender={(_, { depth }) => { - return ( -
- ); - }} - /> - + } + return ( + + ); + }} + onDrop={handleDropNode(updateTreeMenu)} + classes={{ + root: "p-4", + container: "flex flex-col", + dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", + placeholder: "relative", + }} + sort={false} + dropTargetOffset={5} + insertDroppableFirst={false} + canDrop={(_, { dragSource, dropTargetId }) => { + if (dragSource?.parent === dropTargetId) { + return true; + } + }} + placeholderRender={(_, { depth }) => { + return ( +
+ ); + }} + />
+ + + + 콘텐츠 조회 + +
+
+ setUserNm(e.target.value)} + /> + +
+
+ +
+
+ + + + + + + + +
+
+ ); +} + +export { AdditionalUserDialog }; diff --git a/apps/cs/src/pages/organization/ui/move-organization-dialog.tsx b/apps/cs/src/pages/organization/ui/move-organization-dialog.tsx new file mode 100644 index 000000000..4f4a5f22e --- /dev/null +++ b/apps/cs/src/pages/organization/ui/move-organization-dialog.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import type { NodeModel } from "@minoru/react-dnd-treeview"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { DndTree } from "@/widgets/dnd-tree"; +import { + generateOrganizationTreeData, + type OrganizationTree, +} from "@/entities/organization"; +import { ENDPOINT } from "@/shared/config"; + +type MoveOrganizationDialogProps = { + buttonDisabled?: boolean; + handleMoveOrganization: (organization: OrganizationTree) => Promise; +}; + +function MoveOrganizationDialog({ + buttonDisabled, + handleMoveOrganization, +}: MoveOrganizationDialogProps) { + const { confirm } = useConfirm(); + const queryClient = useQueryClient(); + const queryData = queryClient.getQueryData([ + ENDPOINT.CMS_SERVICE.ORGANIZATIONS, + ]); + const organizationTreeData = generateOrganizationTreeData(queryData ?? []); + + const [open, setOpen] = useState(false); + const [selectedOrganization, setSelectedOrganization] = + useState(null); + + const handleNodeSelect = (node: NodeModel) => { + if (!node.data) return; + setSelectedOrganization(node.data); + }; + + const onMoveOrganization = async () => { + if (await confirm("이동하시겠습니까?")) { + await handleMoveOrganization(selectedOrganization as OrganizationTree); + setOpen(false); + } + }; + + return ( + + + + + + + 조직 이동 + + + + + + + + + + + ); +} + +export { MoveOrganizationDialog }; diff --git a/apps/cs/src/pages/organization/ui/organization-form.tsx b/apps/cs/src/pages/organization/ui/organization-form.tsx new file mode 100644 index 000000000..4ddb94a57 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/organization-form.tsx @@ -0,0 +1,207 @@ +"use client"; + +import type { UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Input, + Label, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import type { OrganizationFormData } from "@/entities/organization"; +import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; + +type OrganizationFormProps = { + form: UseFormReturn; + ognzSn?: number; + handleCreate: (data: OrganizationFormData) => Promise; + handleUpdate: ({ + ognzSn, + data, + }: { + ognzSn: number; + data: OrganizationFormData; + }) => Promise; + handleDelete?: (ognzSn: number) => Promise; +}; + +function OrganizationForm({ + form, + ognzSn, + handleCreate, + handleUpdate, + handleDelete, +}: OrganizationFormProps) { + const isModifyMode = !!ognzSn; + + const { confirm } = useConfirm(); + + const onSubmit = async (data: OrganizationFormData) => { + if (isModifyMode) { + await onUpdate(data); + } else { + await onCreate(data); + } + }; + + const onCreate = async (data: OrganizationFormData) => { + if (await confirm("저장하시겠습니까?")) { + handleCreate(data); + } + }; + + const onUpdate = async (data: OrganizationFormData) => { + if (await confirm("수정하시겠습니까?")) { + handleUpdate({ ognzSn: ognzSn!, data }); + } + }; + + const onDelete = async () => { + if (await confirm("삭제하시겠습니까?")) { + handleDelete?.(ognzSn!); + } + }; + + return ( +
+ +
+
+

+ {isModifyMode ? "조직도 수정" : "조직도 등록"} +

+
+
+ + + + + + + + + + 조직명 + + ( + + + + + + )} + /> + + 정렬순서 + + ( + + + + field.onChange(Number(e.target.value)) + } + className="!text-[1.3rem]" + placeholder="정렬순서를 입력해주세요" + /> + + + )} + /> + + + + 조직설명 + + ( + + + + + + )} + /> + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+
+ + {isModifyMode && ( + + )} +
+
+ + ); +} + +export { OrganizationForm }; diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx new file mode 100644 index 000000000..d481137e8 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState } from "react"; +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 { Button } from "@common/components"; +import { DndTree, EditableNode } from "@/widgets/dnd-tree"; +import { + OrganizationApi, + organizationFormSchema, + generateOrganizationTreeData, + handleDropNode, + type OrganizationFormData, + type OrganizationTree, + type OrganizationTreeForDnd, +} from "@/entities/organization"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { OrganizationForm } from "./organization-form"; +import { UserOrganizationSection } from "./user-organization-section"; + +function OrganizationPage() { + const queryClient = useQueryClient(); + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + + const [additionalNode, setAdditionalNode] = + useState | null>(null); + const [selectedOrganization, setSelectedOrganization] = + useState(null); + + const { data: organizationTreeData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + queryFn: () => OrganizationApi.getOrganizationsTreeList(), + select: (data) => generateOrganizationTreeData(data), + }); + + const { data: organizationInfo } = useQuery({ + queryKey: [ + ENDPOINT.CMS_SERVICE.ORGANIZATIONS, + selectedOrganization?.ognzSn, + ], + queryFn: () => + OrganizationApi.getOrganizationInfo(selectedOrganization!.ognzSn!), + enabled: !!selectedOrganization?.ognzSn, + }); + + const { mutateAsync: createOrganization } = useMutation({ + mutationFn: (form: OrganizationFormData) => + OrganizationApi.createOrganization(form), + onSuccess: () => { + alert("저장되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const { mutateAsync: deleteOrganization } = useMutation({ + mutationFn: (ognzSn: number) => OrganizationApi.deleteOrganization(ognzSn), + onSuccess: () => { + alert("삭제되었습니다."); + setSelectedOrganization(null); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const { mutateAsync: updateOrganizationName } = useMutation({ + mutationFn: ({ ognzSn, name }: { ognzSn: number; name: string }) => + OrganizationApi.updateOrganizationName(ognzSn, name), + onSuccess: () => { + alert("저장되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const { mutateAsync: updateOrganization } = useMutation({ + mutationFn: ({ + ognzSn, + data, + }: { + ognzSn: number; + data: OrganizationFormData; + }) => OrganizationApi.updateOrganization(ognzSn, data), + onSuccess: () => { + alert("수정되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const { mutateAsync: updateOrganizationByDnd } = useMutation({ + mutationFn: (data: OrganizationTreeForDnd) => + OrganizationApi.updateOrganizationByDnd([data]), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS], + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(organizationFormSchema), + defaultValues: { + ognzNm: "", + sortSeq: 1, + useYn: "Y", + }, + values: { + ognzNm: organizationInfo?.ognzNm ?? "", + ognzExpln: organizationInfo?.ognzExpln ?? "", + sortSeq: organizationInfo?.sortSeq ?? 1, + useYn: organizationInfo?.useYn ?? "Y", + }, + }); + + const additionalNodeForm = useForm({ + resolver: zodResolver(organizationFormSchema), + defaultValues: { + ognzNm: "", + sortSeq: 1, + }, + }); + + const tree = additionalNode + ? [ + ...(additionalNode ? [additionalNode] : []), + ...(organizationTreeData ?? []), + ] + : (organizationTreeData ?? []); + + const onSaveAdditionalNode = async () => { + await createOrganization({ ...additionalNodeForm.getValues() }); + resetAdditionalNode(); + }; + + const onDeleteOrganization = async () => { + if (await confirm("삭제하시겠습니까?")) { + deleteOrganization(selectedOrganization!.ognzSn!); + } + }; + + const resetAdditionalNode = () => { + additionalNodeForm.setValue("ognzNm", ""); + setAdditionalNode(null); + }; + + const handleNodeSelect = (node: NodeModel) => { + if (!node.data) return; + if (node.data.ognzSn === selectedOrganization?.ognzSn) { + setSelectedOrganization(null); + additionalNodeForm.setValue("upOgnzSn", null); + return; + } + setSelectedOrganization(node.data); + additionalNodeForm.setValue("upOgnzSn", (node.id as number) ?? 0); + }; + + const handleAddOrganization = () => { + if (additionalNode) return; + setAdditionalNode({ + id: -1, + parent: selectedOrganization?.ognzSn ?? 0, + text: "", + }); + }; + + return ( +
+
+
+ + +
+
+ ( + + additionalNodeForm.setValue("ognzNm", e.target.value) + } + onSave={onSaveAdditionalNode} + onCancel={resetAdditionalNode} + {...props} + /> + )} + handleSelectNode={handleNodeSelect} + handleDropNode={handleDropNode(updateOrganizationByDnd)} + updateNodeName={async (id, name) => { + await updateOrganizationName({ ognzSn: id as number, name }); + }} + classes={{ wrapper: "max-h-[calc(100vh-29rem)]" }} + /> +
+
+
+ + {selectedOrganization?.ognzSn && ( + + )} +
+
+ ); +} + +export { OrganizationPage }; diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx new file mode 100644 index 000000000..a3e4c5f68 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -0,0 +1,383 @@ +import dayjs from "dayjs"; +import React, { useEffect, useState, useCallback, memo } from "react"; +import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + useQuery, + useMutation, + useQueryClient, + keepPreviousData, +} from "@tanstack/react-query"; +import { + Button, + Checkbox, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { + UserOrganizationApi, + userOrganizationFormSchema, + type UserOrganizationFormData, + type UserOrganizationPayload, + type User, + type SearchUser, + type MoveUserOrganizationPayload, +} from "@/entities/user-organization"; +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 { AdditionalUserDialog } from "./additional-user-dialog"; +import { MoveOrganizationDialog } from "./move-organization-dialog"; + +type UserOrganizationSectionProps = { + ognzSn: number; +}; + +function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { + const queryClient = useQueryClient(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + + const [userList, setUserList] = useState([]); + const [selectedUsers, setSelectedUsers] = useState([]); + + const isAllSelected = + userList.length > 0 && selectedUsers.length === userList.length; + + const { data: userOrganizationList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + queryFn: () => UserOrganizationApi.getUserOrganizationList({ ognzSn }), + enabled: !!ognzSn, + placeholderData: keepPreviousData, + }); + + const { data: jobPositionCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JPBSCLCD"], + queryFn: () => CodeApi.getCode("JPBSCLCD"), + }); + + const { data: jobGradeCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JDGDCLCD"], + queryFn: () => CodeApi.getCode("JDGDCLCD"), + }); + + const { mutate: saveUserOrganization } = useMutation({ + mutationFn: (data: UserOrganizationPayload[]) => + UserOrganizationApi.saveUserOrganization(data), + onSuccess: () => { + alert("저장되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + }); + }, + }); + + const { mutate: moveUserOrganization } = useMutation({ + mutationFn: (data: MoveUserOrganizationPayload[]) => + UserOrganizationApi.moveUserOrganization(data), + onSuccess: () => { + alert("이동되었습니다."); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(userOrganizationFormSchema), + values: { + userOrganization: + userOrganizationList?.content.map((user) => ({ + mode: user.mode, + ognzSn: user.ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: user.jbpsSeCd, + jbgdSeCd: user.jbgdSeCd, + sortSeq: user.sortSeq, + })) ?? [], + }, + }); + + const { append } = useFieldArray({ + name: "userOrganization", + control: form.control, + }); + + const handleAddUsers = (users: SearchUser[]) => { + const generateUserData = (user: SearchUser) => ({ + mode: "C", + ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: "", + jbgdSeCd: "", + sortSeq: 0, + }); + + const newFormData = users.map(generateUserData); + append(newFormData); + + const newUserList = users.map((user) => ({ + ...generateUserData(user), + userUnqNm: user.userNm, + })); + const existingUserIds = new Set(newUserList.map((user) => user.userUnqId)); + const filteredUserList = userList.filter( + (user) => !existingUserIds.has(user.userUnqId), + ); + setUserList([...filteredUserList, ...newUserList]); + }; + + const handleToggleSelectUser = useCallback((user: User) => { + setSelectedUsers((prevSelected) => { + const isSelected = prevSelected.some( + (item) => item.userUnqId === user.userUnqId, + ); + + if (isSelected) { + return prevSelected.filter((item) => item.userUnqId !== user.userUnqId); + } else { + return [...prevSelected, user]; + } + }); + }, []); + + const handleToggleSelectAll = () => { + if (isAllSelected) { + setSelectedUsers([]); + } else { + setSelectedUsers([...userList]); + } + }; + + const handleDeleteUsers = () => { + const selectedUserIds = selectedUsers.map((user) => user.userUnqId); + const updatedFormData = form.getValues("userOrganization").map((item) => { + if (selectedUserIds.includes(item.userUnqId)) { + return { ...item, mode: "D" }; + } + return item; + }); + form.setValue("userOrganization", updatedFormData); + setUserList((prevUsers) => + prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)), + ); + setSelectedUsers([]); + }; + + const handleSaveUsers = async () => { + if (await confirm("저장하시겠습니까?")) { + const filteredData = form + .watch("userOrganization") + .filter((item) => item.mode !== "R"); + saveUserOrganization(filteredData); + } + }; + + const handleMoveOrganization = async (organization: OrganizationTree) => { + const payloadData: MoveUserOrganizationPayload[] = selectedUsers.map( + (user) => ({ + ognzSn: user.ognzSn, + toOgnzSn: organization.ognzSn!, + userUnqId: user.userUnqId, + }), + ); + moveUserOrganization(payloadData); + }; + + const isUserSelected = useCallback( + (userUnqId: string) => + selectedUsers.some((item) => item.userUnqId === userUnqId), + [selectedUsers], + ); + + useEffect(() => { + if (userOrganizationList?.content) { + setUserList(userOrganizationList.content); + setSelectedUsers([]); + } + }, [userOrganizationList]); + + return ( +
+
+

소속원 목록

+
+ + + + +
+
+
+ + + + + + + NO + 이름 + 직위 + 직급 + 정렬순서 + 최종수정자 + 최종수정일 + + + + {userList.map((user, index) => ( + + + handleToggleSelectUser(user)} + /> + + + + ))} + +
+
+
+ ); +} + +type UserCellsProps = { + form: UseFormReturn; + user: User; + index: number; + jobPositionCodeData?: ICode[]; + jobGradeCodeData?: ICode[]; +}; + +const UserCells = memo( + ({ + form, + user, + index, + jobPositionCodeData, + jobGradeCodeData, + }: UserCellsProps) => { + const updateMode = (index: number) => { + const mode = form.watch(`userOrganization.${index}.mode`); + if (mode === "R") { + form.setValue(`userOrganization.${index}.mode`, "U"); + } + }; + + const updateFormValue = + (index: number, key: keyof UserOrganizationPayload) => + (value: string | number) => { + updateMode(index); + form.setValue(`userOrganization.${index}.${key}`, value); + }; + + return ( + <> + {index + 1} + {user.userUnqNm} + + + + + + + + + updateFormValue(index, "sortSeq")(Number(e.target.value)) + } + className="w-[6rem] !text-[1.3rem]" + /> + + {user.mdfrNm} + + {dayjs(user.mdfcnDt).format("YYYY-MM-DD")} + + + ); + }, +); + +UserCells.displayName = "UserCells"; + +type CodeSelectProps = React.ComponentPropsWithoutRef & { + codeData?: ICode[]; +}; + +const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => { + if (!codeData) return null; + + const placeholderText = + codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ?? + "선택"; + + return ( + + ); +}); + +CodeSelect.displayName = "CodeSelect"; + +export { UserOrganizationSection }; diff --git a/apps/cs/src/pages/template/ui/create-page.tsx b/apps/cs/src/pages/template/ui/create-page.tsx index 8057ddd00..f3b445809 100644 --- a/apps/cs/src/pages/template/ui/create-page.tsx +++ b/apps/cs/src/pages/template/ui/create-page.tsx @@ -1,7 +1,7 @@ "use client"; -import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; import { useMutation } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { TemplateApi } from "../api/template-service"; @@ -15,6 +15,7 @@ import { TemplateForm } from "./template-form"; function CreatePage() { const router = useRouter(); const { setMessage: alert } = useAlertStore((state) => state); + const { mutateAsync: createTemplate } = useMutation({ mutationFn: (data: TemplateFormData) => TemplateApi.createTemplate(data), onSuccess: () => { diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 2b98fb4e5..49379f521 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -26,6 +26,8 @@ export const ENDPOINT = { MENU_ROLES: "/cms-service/api/v1/menu-roles", MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", CONTENTS: "/cms-service/api/v1/contents", + CONTENTS_DETAILS: "/cms-service/api/v1/contents-details", + ORGANIZATIONS: "/cms-service/api/v1/ognzs", USER_ORGANIZATION: "/cms-service/api/v1/user-ognz", IMAGES: "/cms-service/api/v1/images", BANNERS: "/cms-service/api/v1/bnrs", diff --git a/apps/cs/src/shared/store/menu-store.ts b/apps/cs/src/shared/store/menu-store.ts index f0c6340c9..9aec06603 100644 --- a/apps/cs/src/shared/store/menu-store.ts +++ b/apps/cs/src/shared/store/menu-store.ts @@ -1,5 +1,5 @@ import { createStore } from "zustand/vanilla"; -import type { MenuItem } from "@common/components/api"; +import type { MenuItem } from "@/entities/menu"; export type MenuState = { currentMenuInfo: MenuItem | null; diff --git a/apps/cs/src/shared/ui/alert-dialog.tsx b/apps/cs/src/shared/ui/alert-dialog.tsx index 728276713..012d4ab86 100644 --- a/apps/cs/src/shared/ui/alert-dialog.tsx +++ b/apps/cs/src/shared/ui/alert-dialog.tsx @@ -4,7 +4,7 @@ import { DialogFooter, DialogClose, Button, -} from "@common/components/ui"; +} from "@common/components"; import { useAlertStore } from "../lib/use-alert-store"; function AlertDialog() { diff --git a/apps/cs/src/shared/ui/confirm-dialog.tsx b/apps/cs/src/shared/ui/confirm-dialog.tsx index 955b742ee..6a4996bfb 100644 --- a/apps/cs/src/shared/ui/confirm-dialog.tsx +++ b/apps/cs/src/shared/ui/confirm-dialog.tsx @@ -6,7 +6,7 @@ import { DialogClose, Button, type ButtonProps, -} from "@common/components/ui"; +} from "@common/components"; type ConfirmDialogProps = { isOpen?: boolean; @@ -24,7 +24,9 @@ function ConfirmDialog({
-

{message}

+

+ {message} +

diff --git a/apps/cs/src/shared/ui/skeleton-page.tsx b/apps/cs/src/shared/ui/skeleton-page.tsx index fca5c8e76..cf788aca2 100644 --- a/apps/cs/src/shared/ui/skeleton-page.tsx +++ b/apps/cs/src/shared/ui/skeleton-page.tsx @@ -1,4 +1,6 @@ -import { Skeleton } from "@common/components/ui/skeleton"; +"use client"; + +import { Skeleton } from "@common/components"; function SkeletonPage() { return ( diff --git a/apps/cs/src/widgets/dnd-tree/index.ts b/apps/cs/src/widgets/dnd-tree/index.ts new file mode 100644 index 000000000..ca1c20f8d --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/index.ts @@ -0,0 +1,2 @@ +export { DndTree } from "./ui/dnd-tree"; +export { EditableNode } from "./ui/editable-node"; diff --git a/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx new file mode 100644 index 000000000..11defd25f --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/custom-node.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview"; +import { Button, cn } from "@common/components"; +import { EditableNode } from "./editable-node"; + +interface TreeData extends Record { + children?: unknown[]; +} + +type CustomNodeProps = { + node: NodeModel; + depth: number; + isOpen: boolean; + isSelected: boolean; + isAdditionalParent: boolean; + treeMethods: TreeMethods | null; + readOnly: boolean; + onToggle: (id: NodeModel["id"]) => void; + onSelect: (node: NodeModel) => void; + updateNodeName: (id: NodeModel["id"], nodeName: string) => Promise; +}; + +function CustomNode({ + node, + depth, + isOpen, + isSelected, + isAdditionalParent, + treeMethods, + readOnly, + onToggle, + onSelect, + updateNodeName, +}: CustomNodeProps) { + const { id, text, data } = node; + const indent = depth * 10 + 5; + + const [editMode, setEditMode] = useState(false); + const [nodeName, setNodeName] = useState(text); + + const handleToggle = () => onToggle(id); + const handleSelect = () => onSelect(node); + + const handleSave = async () => { + await updateNodeName(id, nodeName); + setEditMode(false); + }; + + const handleCancel = () => { + setEditMode(false); + setNodeName(text); + }; + + useEffect(() => { + if (isAdditionalParent) treeMethods?.open(id); + }, [isAdditionalParent]); + + return ( +
+ {editMode ? ( + setNodeName(e.target.value)} + onSave={handleSave} + onCancel={handleCancel} + /> + ) : ( + <> +
+ {data && data.children && data.children.length > 0 && ( + { + e.stopPropagation(); + handleToggle(); + }} + /> + )} + {text} +
+ + + )} +
+ ); +} + +export { CustomNode }; diff --git a/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx new file mode 100644 index 000000000..b1509006e --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/dnd-tree.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { type ReactElement, useRef } from "react"; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions, + type NodeModel, + type TreeMethods, + type DropOptions, + type Classes, +} from "@minoru/react-dnd-treeview"; +import { Button, cn } from "@common/components"; +import { CustomNode } from "./custom-node"; + +interface TreeData extends Record { + children?: unknown[]; +} + +type EditableNodeProps = { depth: number }; + +type DndTreeProps = { + tree: NodeModel[]; + selectedNodeId?: number; + classes?: { + wrapper?: string; + } & Classes; + renderEditableNode?: (props: EditableNodeProps) => ReactElement; + handleSelectNode?: (node: NodeModel) => void; + handleDropNode?: (newTree: NodeModel[], options: DropOptions) => void; + updateNodeName?: (id: NodeModel["id"], nodeName: string) => Promise; +}; + +function DndTree({ + tree, + selectedNodeId, + classes, + renderEditableNode, + handleSelectNode, + handleDropNode, + updateNodeName, +}: DndTreeProps) { + const treeMethods = useRef(null); + + const isAdditionalParent = (node: NodeModel): boolean => { + const additionalNode = tree.find((n) => n.id === -1); + if (!additionalNode) return false; + return additionalNode?.parent === node.id; + }; + + return ( + <> +
+ + , { depth, isOpen, onToggle }) => { + if (node.id === -1) + return renderEditableNode?.({ depth }) ?? <>; + return ( + handleSelectNode?.(node)} + updateNodeName={(id, nodeName) => + updateNodeName?.(id, nodeName) ?? Promise.resolve() + } + /> + ); + }} + onDrop={(newTree, options) => handleDropNode?.(newTree, options)} + classes={{ + root: "p-4", + container: "flex flex-col", + dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm", + placeholder: "relative", + ...classes, + }} + sort={false} + dropTargetOffset={5} + insertDroppableFirst={false} + canDrop={(_, { dragSource, dropTargetId }) => { + if (dragSource?.parent === dropTargetId) { + return true; + } + }} + placeholderRender={(_, { depth }) => { + return ( +
+ ); + }} + /> + +
+
+ + +
+ + ); +} + +export { DndTree }; diff --git a/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx b/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx new file mode 100644 index 000000000..f2274f4d8 --- /dev/null +++ b/apps/cs/src/widgets/dnd-tree/ui/editable-node.tsx @@ -0,0 +1,44 @@ +import type { ComponentProps } from "react"; +import { Button, Input } from "@common/components"; + +type EditableNodeProps = { + depth?: number; + onSave: () => void; + onCancel: () => void; +} & ComponentProps; + +function EditableNode({ + depth, + onSave, + onCancel, + ...props +}: EditableNodeProps) { + return ( +
+ +
+ + +
+
+ ); +} + +export { EditableNode }; diff --git a/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx b/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx index 5b6edb3bc..caba0b595 100644 --- a/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx +++ b/apps/cs/src/widgets/form-wrapper/ui/form-wrapper.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/navigation"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { type FieldValues, type UseFormReturn } from "react-hook-form"; -import { Button } from "@common/components/ui"; +import { Button } from "@common/components"; import { Form } from "@/shared/ui/form"; type FormWrapperProps = { @@ -54,7 +54,7 @@ function FormWrapper({ - {isModifyMode && ( + {isModifyMode && handleDelete && ( diff --git a/apps/cs/tsconfig.json b/apps/cs/tsconfig.json index 9e77e1146..17db758c4 100644 --- a/apps/cs/tsconfig.json +++ b/apps/cs/tsconfig.json @@ -25,9 +25,8 @@ "baseUrl": "./src", "paths": { "@/*": ["./*"], - "@common/components/*": ["../../../packages/common/components/src/*"], - "@common/components": ["../../../packages/common/components/src"], - "@common/network/*": ["../../../packages/common/network/*"], + "@common/components": ["../../../packages/common/components/dist/"], + "@common/config": ["../../../packages/common/config/dist/"], "@common/network": ["../../../packages/common/network/"] } }, diff --git a/packages/common/components/package.json b/packages/common/components/package.json index cab68595e..ca166abc7 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -7,16 +7,17 @@ "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" - } + }, + "./index.css": "./dist/index.css" }, - "files": [ - "dist/**" - ], "scripts": { "build": "tsup", "storybook": "storybook dev -p 6006", @@ -26,6 +27,7 @@ }, "dependencies": { "@common/assets": "workspace:*", + "@common/config": "workspace:*", "@frontend-opensource/use-react-hooks": "^1.1.4", "@minoru/react-dnd-treeview": "^3.5.0", "@radix-ui/react-accordion": "^1.2.3", diff --git a/packages/common/components/src/api/client.ts b/packages/common/components/src/api/client.ts index 231887202..0226a4f9a 100644 --- a/packages/common/components/src/api/client.ts +++ b/packages/common/components/src/api/client.ts @@ -1,7 +1,15 @@ import { Client } from "@common/network"; -import { BASE_URL, SITE_ID } from "../config"; +import { getConfig } from "@common/config"; -export const client = Client.getInstance({ - baseURL: BASE_URL, - siteId: SITE_ID, -}); +const getClientInstance = () => { + const config = getConfig(); + const { PROXY_HOST, SITE_ID } = config; + const BASE_URL = `${PROXY_HOST}/server`; + + return Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID ?? "", + }); +}; + +export const getApiClient = () => getClientInstance(); diff --git a/packages/common/components/src/api/menu.ts b/packages/common/components/src/api/menu.ts index 4855718a2..bd8fe1599 100644 --- a/packages/common/components/src/api/menu.ts +++ b/packages/common/components/src/api/menu.ts @@ -1,4 +1,4 @@ -import { client } from "./client"; +import { getApiClient } from "./client"; import { ENDPOINT } from "../config"; export interface MenuItem { @@ -21,6 +21,8 @@ export interface MenuItem { requiredUrlPath: boolean; } +const client = getApiClient(); + export const getMenuList = async (siteId: string) => { const response = await client.get( `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, diff --git a/packages/common/components/src/api/site.ts b/packages/common/components/src/api/site.ts index 2ca35bd26..8d8109571 100644 --- a/packages/common/components/src/api/site.ts +++ b/packages/common/components/src/api/site.ts @@ -1,4 +1,4 @@ -import { client } from "./client"; +import { getApiClient } from "./client"; import { ENDPOINT } from "../config"; export interface SiteInfo { @@ -23,6 +23,8 @@ export interface SiteInfo { mdfrNm: string; } +const client = getApiClient(); + export const getSiteInfo = async (siteId: string) => { const response = await client.get( `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, diff --git a/packages/common/components/src/api/user.ts b/packages/common/components/src/api/user.ts index 760f2c78b..78b833325 100644 --- a/packages/common/components/src/api/user.ts +++ b/packages/common/components/src/api/user.ts @@ -1,4 +1,4 @@ -import { client } from "./client"; +import { getApiClient } from "./client"; import { ENDPOINT } from "../config"; export interface IUser { @@ -35,6 +35,8 @@ export interface IUser { hasPassword: boolean; } +const client = getApiClient(); + export const getUserInfo = async (userUniqId: string) => { const response = await client.get( `${ENDPOINT.USER_SERVICE.USERS}/${userUniqId}`, diff --git a/packages/common/components/src/config/api.ts b/packages/common/components/src/config/api.ts index b06ffcd13..07428479b 100644 --- a/packages/common/components/src/config/api.ts +++ b/packages/common/components/src/config/api.ts @@ -1,7 +1,3 @@ -import { PROXY_HOST } from "./env"; - -export const BASE_URL = `${PROXY_HOST}/server`; - export const ENDPOINT = { CMS_SERVICE: { MENU_ROLES: "/cms-service/api/v1/menu-roles", diff --git a/packages/common/components/src/config/env.ts b/packages/common/components/src/config/env.ts deleted file mode 100644 index dfe49efdb..000000000 --- a/packages/common/components/src/config/env.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const PORT = process.env.PORT || "3000"; -export const PROXY_HOST = process.env.PROXY_HOST || `http://localhost:${PORT}`; - -export const SERVER_API_URL = process.env.SERVER_API_URL; -export const SITE_ID = process.env.SITE_ID || "CS"; - -export const CLAIM_NAME = process.env.CLAIM_NAME || "Authorization"; -export const ACCESS_TOKEN = process.env.ACCESS_TOKEN || "access-token"; -export const REFRESH_TOKEN = process.env.REFRESH_TOKEN || "refresh-token"; -export const AUTH_USER_ID = process.env.AUTH_USER_ID || "token-id"; diff --git a/packages/common/components/src/config/index.ts b/packages/common/components/src/config/index.ts index cd83447d5..d158c5764 100644 --- a/packages/common/components/src/config/index.ts +++ b/packages/common/components/src/config/index.ts @@ -1,2 +1 @@ export * from "./api"; -export * from "./env"; diff --git a/packages/common/components/src/ui/checkbox/checkbox.css b/packages/common/components/src/ui/checkbox/checkbox.css index c25f8d8e4..b1f1af872 100644 --- a/packages/common/components/src/ui/checkbox/checkbox.css +++ b/packages/common/components/src/ui/checkbox/checkbox.css @@ -2,13 +2,13 @@ @layer components { button[role="checkbox"] { - @apply size-[1.5rem] shrink-0 overflow-hidden rounded-[0.5rem] border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground; + @apply size-[1.5rem] shrink-0 overflow-hidden rounded-[0.5rem] border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground; } button[role="checkbox"] > span { @apply flex size-full items-center justify-center text-current; } button[role="checkbox"] + label { - @apply text-label text-[1.3rem] font-medium; + @apply text-[1.3rem] font-medium text-label; } button[role="checkbox"]:disabled + label { @apply cursor-not-allowed opacity-70; diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index ac82afde9..bcb072bb1 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -13,11 +13,11 @@ import { cn } from "../../lib"; import { Button } from "../button"; import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table"; import type { HTMLAttributes } from "react"; - declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { - align: "left" | "center" | "right"; + align?: "left" | "center" | "right"; + rowSpan?: number; } } @@ -70,15 +70,35 @@ function THead({ table }: { table: TableInstance }) { {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { + const columnRelativeDepth = header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = (leafs?.[leafs.length - 1]?.depth ?? 0) - header.depth; + } + const { meta } = header.column.columnDef; + return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} + + {flexRender( + header.column.columnDef.header, + header.getContext(), + )} ); })} diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index c9a150f58..9fb310408 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -1,8 +1,9 @@ import { useQuery } from "@tanstack/react-query"; import { images } from "@common/assets"; +import { getConfig } from "@common/config"; +import { ENDPOINT } from "../../config"; import { getCookie, setCookie } from "../../lib"; import { getSiteInfo, getUserInfo } from "../../api"; -import { AUTH_USER_ID, ENDPOINT, SITE_ID } from "../../config"; import { DropdownMenu, DropdownMenuTrigger, @@ -11,8 +12,10 @@ import { } from "../dropdown-menu"; import { Button } from "../button"; +const { AUTH_USER_ID, SITE_ID } = getConfig(); + export function Header({ viewOnly }: { viewOnly?: boolean }) { - const userUniqId = getCookie(AUTH_USER_ID) ?? ""; + const userUniqId = getCookie(AUTH_USER_ID ?? "") ?? ""; const { data: userInfo } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS], @@ -22,7 +25,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { const { data: siteInfo } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.SITES, SITE_ID], - queryFn: () => getSiteInfo(SITE_ID), + queryFn: () => getSiteInfo(SITE_ID ?? ""), select: (data) => { if (data.siteSkn) { const siteSkin = data.siteSkn.toLowerCase(); diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index f2655be9d..3ea573a76 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ChevronRight } from "lucide-react"; +import { getConfig } from "@common/config"; +import { ENDPOINT } from "../../config"; import { cn, setCookie } from "../../lib"; -import { ENDPOINT, SITE_ID } from "../../config"; import { getMenuList, type MenuItem } from "../../api"; import { Sidebar, @@ -19,6 +20,8 @@ import { } from "../collapsible"; import { getParentIds } from "./lib"; +const { SITE_ID } = getConfig(); + export function LNB({ viewOnly, currentPath, @@ -30,7 +33,7 @@ export function LNB({ }) { const { data: menuList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], - queryFn: () => getMenuList(SITE_ID), + queryFn: () => getMenuList(SITE_ID ?? ""), enabled: !viewOnly, }); diff --git a/packages/common/components/tsconfig.json b/packages/common/components/tsconfig.json index c7fbf7206..5db0dadbc 100644 --- a/packages/common/components/tsconfig.json +++ b/packages/common/components/tsconfig.json @@ -15,7 +15,7 @@ "baseUrl": ".", "paths": { "@/*": ["src/*"], - "@common/network/*": ["../network/*"], + "@common/config": ["../config/dist/"], "@common/network": ["../network/"] } }, diff --git a/packages/common/components/tsup.config.ts b/packages/common/components/tsup.config.ts index 57cb34abf..2f18d7df3 100644 --- a/packages/common/components/tsup.config.ts +++ b/packages/common/components/tsup.config.ts @@ -10,7 +10,7 @@ export default defineConfig((options) => ({ format: ["esm"], external: ["react", "react-dom"], platform: "browser", - target: ["es2020", "chrome70", "edge18", "firefox70", "node18"], + target: ["es2020"], tsconfig: path.resolve(__dirname, "tsconfig.build.json"), sourcemap: !options.watch, minify: !options.watch, diff --git a/packages/common/config/package.json b/packages/common/config/package.json new file mode 100644 index 000000000..5f71fca42 --- /dev/null +++ b/packages/common/config/package.json @@ -0,0 +1,28 @@ +{ + "name": "@common/config", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "clean": "rm -rf dist .turbo node_modules" + }, + "dependencies": {}, + "devDependencies": { + "eslint": "^9.8.0", + "tsup": "^8.2.3", + "typescript": "^5.5.3", + "vite": "^5.3.5" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + } +} diff --git a/packages/common/config/src/index.ts b/packages/common/config/src/index.ts new file mode 100644 index 000000000..dd33ecdef --- /dev/null +++ b/packages/common/config/src/index.ts @@ -0,0 +1,2 @@ +export * from "./initialize-config"; +export * from "./initializer"; diff --git a/packages/common/config/src/initialize-config.ts b/packages/common/config/src/initialize-config.ts new file mode 100644 index 000000000..e25010321 --- /dev/null +++ b/packages/common/config/src/initialize-config.ts @@ -0,0 +1,54 @@ +export interface EnvConfig { + PORT?: string; + PROXY_HOST?: string; + SERVER_API_URL?: string; + SITE_ID?: string; + CLAIM_NAME?: string; + ACCESS_TOKEN?: string; + REFRESH_TOKEN?: string; + AUTH_USER_ID?: string; +} + +const defaultConfig: EnvConfig = { + PORT: "3000", + PROXY_HOST: "", + SERVER_API_URL: "", + SITE_ID: "CS", + CLAIM_NAME: "Authorization", + ACCESS_TOKEN: "access-token", + REFRESH_TOKEN: "refresh-token", + AUTH_USER_ID: "token-id", +}; + +let isInitialized = false; +let envConfig: EnvConfig = { ...defaultConfig }; + +export function initializeConfig(values: Partial): void { + if (isInitialized) { + console.warn("Configuration already initialized. Skipping."); + return; + } + + const filteredValues = Object.entries(values).reduce((acc, [key, value]) => { + if (value !== undefined && value !== null) { + acc[key as keyof EnvConfig] = value; + } + return acc; + }, {} as Partial); + + if (filteredValues.PORT && !filteredValues.PROXY_HOST) { + filteredValues.PROXY_HOST = `http://localhost:${filteredValues.PORT}`; + } + + envConfig = { ...defaultConfig, ...filteredValues }; + isInitialized = true; +} + +export function getConfig(): EnvConfig { + if (!isInitialized) { + console.warn( + "Attempted to access config before initialization. Returning default values." + ); + } + return envConfig; +} diff --git a/packages/common/config/src/initializer.ts b/packages/common/config/src/initializer.ts new file mode 100644 index 000000000..896225d88 --- /dev/null +++ b/packages/common/config/src/initializer.ts @@ -0,0 +1,20 @@ +import { initializeConfig, type EnvConfig } from "./initialize-config"; + +export function initializeNextClientConfig(): void { + if (typeof window === "undefined") { + console.warn("initializeNextClientConfig called on the server. Skipping."); + return; + } + + const env: Partial = { + PORT: process.env.NEXT_PUBLIC_PORT, + PROXY_HOST: process.env.NEXT_PUBLIC_PROXY_HOST, + SERVER_API_URL: process.env.NEXT_PUBLIC_SERVER_API_URL, + SITE_ID: process.env.NEXT_PUBLIC_SITE_ID, + CLAIM_NAME: process.env.NEXT_PUBLIC_CLAIM_NAME, + ACCESS_TOKEN: process.env.NEXT_PUBLIC_ACCESS_TOKEN, + REFRESH_TOKEN: process.env.NEXT_PUBLIC_REFRESH_TOKEN, + AUTH_USER_ID: process.env.NEXT_PUBLIC_AUTH_USER_ID, + }; + initializeConfig(env); +} diff --git a/packages/common/config/tsconfig.json b/packages/common/config/tsconfig.json new file mode 100644 index 000000000..20cbe8801 --- /dev/null +++ b/packages/common/config/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node", "vite/client"], + "incremental": false + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/common/config/tsup.config.ts b/packages/common/config/tsup.config.ts new file mode 100644 index 000000000..1aea0cc6e --- /dev/null +++ b/packages/common/config/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + external: [], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3c29453c..a56c9aeb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: specifier: 2.4.2 version: 2.4.2 - apps/common/feature: + apps/common/features: dependencies: '@common/assets': specifier: workspace:* @@ -45,6 +45,12 @@ importers: '@common/components': specifier: workspace:* version: link:../../../packages/common/components + '@common/config': + specifier: workspace:* + version: link:../../../packages/common/config + '@common/network': + specifier: workspace:* + version: link:../../../packages/common/network '@frontend-opensource/use-react-hooks': specifier: ^1.1.4 version: 1.1.4 @@ -82,8 +88,8 @@ importers: specifier: ^3.0.5 version: 3.0.5 next: - specifier: 15.1.7 - version: 15.1.7(react-dom@18.3.1)(react@18.3.1) + specifier: 15.3.0 + version: 15.3.0(react-dom@18.3.1)(react@18.3.1) react-chartjs-2: specifier: ^5.3.0 version: 5.3.0(chart.js@4.4.8)(react@18.3.1) @@ -161,8 +167,8 @@ importers: specifier: ^4.1.3 version: 4.1.3(react-hook-form@7.54.2) '@minoru/react-dnd-treeview': - specifier: ^3.5.0 - version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) + specifier: 3.5.1-alpha.1 + version: 3.5.1-alpha.1(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -475,6 +481,9 @@ importers: '@common/assets': specifier: workspace:* version: link:../assets + '@common/config': + specifier: workspace:* + version: link:../config '@frontend-opensource/use-react-hooks': specifier: ^1.1.4 version: 1.1.4 @@ -678,6 +687,21 @@ importers: specifier: 5.1.4 version: 5.1.4(typescript@5.6.3)(vite@6.2.0) + packages/common/config: + devDependencies: + eslint: + specifier: ^9.8.0 + version: 9.21.0 + tsup: + specifier: ^8.2.3 + version: 8.3.6(postcss@8.5.3)(typescript@5.6.3) + typescript: + specifier: ^5.5.3 + version: 5.6.3 + vite: + specifier: ^5.3.5 + version: 5.4.18(@types/node@22.13.9) + packages/common/network: dependencies: '@types/js-cookie': @@ -915,14 +939,6 @@ packages: node-source-walk: 7.0.1 dev: true - /@emnapi/runtime@1.3.1: - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: false - optional: true - /@emnapi/runtime@1.4.3: resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} requiresBuild: true @@ -931,6 +947,15 @@ packages: dev: false optional: true + /@esbuild/aix-ppc64@0.21.5: + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + /@esbuild/aix-ppc64@0.24.2: resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} @@ -949,6 +974,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.21.5: + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm64@0.24.2: resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} engines: {node: '>=18'} @@ -967,6 +1001,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.21.5: + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.24.2: resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} engines: {node: '>=18'} @@ -985,6 +1028,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.21.5: + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.24.2: resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} engines: {node: '>=18'} @@ -1003,6 +1055,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.21.5: + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.24.2: resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} engines: {node: '>=18'} @@ -1021,6 +1082,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.21.5: + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.24.2: resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} engines: {node: '>=18'} @@ -1039,6 +1109,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.21.5: + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.24.2: resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} engines: {node: '>=18'} @@ -1057,6 +1136,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.21.5: + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.24.2: resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} engines: {node: '>=18'} @@ -1075,6 +1163,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.21.5: + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.24.2: resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} engines: {node: '>=18'} @@ -1093,6 +1190,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.21.5: + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.24.2: resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} engines: {node: '>=18'} @@ -1111,6 +1217,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.21.5: + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.24.2: resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} engines: {node: '>=18'} @@ -1129,6 +1244,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.21.5: + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.24.2: resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} engines: {node: '>=18'} @@ -1147,6 +1271,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.21.5: + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.24.2: resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} engines: {node: '>=18'} @@ -1165,6 +1298,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.21.5: + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.24.2: resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} engines: {node: '>=18'} @@ -1183,6 +1325,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.21.5: + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.24.2: resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} engines: {node: '>=18'} @@ -1201,6 +1352,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.21.5: + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.24.2: resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} engines: {node: '>=18'} @@ -1219,6 +1379,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.21.5: + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.24.2: resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} engines: {node: '>=18'} @@ -1255,6 +1424,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.21.5: + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.24.2: resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} engines: {node: '>=18'} @@ -1291,6 +1469,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.21.5: + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.24.2: resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} engines: {node: '>=18'} @@ -1309,6 +1496,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.21.5: + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.24.2: resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} engines: {node: '>=18'} @@ -1327,6 +1523,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.21.5: + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.24.2: resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} engines: {node: '>=18'} @@ -1345,6 +1550,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.21.5: + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.24.2: resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} engines: {node: '>=18'} @@ -1363,6 +1577,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.21.5: + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.24.2: resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} engines: {node: '>=18'} @@ -1866,7 +2089,7 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.4.3 dev: false optional: true @@ -1996,6 +2219,27 @@ packages: /@minoru/react-dnd-treeview@3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-kDHGQ0UK18w6ie5EH68NynATSP9BAzNLmWBXuVSoaGkraWWFdEdAWqQxAZiMorAixFu40MzYkn2N9/Gam6q17w==} + peerDependencies: + react: ^18.3.1 + react-dnd: 15.x || 16.x + react-dom: ^18.3.1 + dependencies: + '@juggle/resize-observer': 3.4.0 + dnd-multi-backend: 8.1.2(dnd-core@16.0.1) + framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dnd: 16.0.1(@types/node@22.13.9)(@types/react@18.3.18)(react@18.3.1) + react-dnd-html5-backend: 16.0.1 + react-dnd-touch-backend: 16.0.1 + react-dom: 18.3.1(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - dnd-core + dev: false + + /@minoru/react-dnd-treeview@3.5.1-alpha.1(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-XEKBjhyltG0gzn+ioZRwhlV7+Q/uVZqTMS/tAO3On/rPNuNgqWHROFsyGdHQS/DYoENGOG8JX03chCxExOgEpA==} peerDependencies: react: ^18.3.1 react-dnd: 15.x || 16.x @@ -5412,6 +5656,37 @@ packages: - supports-color dev: true + /esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + dev: true + /esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} engines: {node: '>=18'} @@ -8980,6 +9255,45 @@ packages: - typescript dev: true + /vite@5.4.18(@types/node@22.13.9): + resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 22.13.9 + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.34.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@6.2.0(@types/node@22.13.5): resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}