From f5a49a44c476118f8445dab36e5814cd96b51718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A7=80=EC=B2=A0?= Date: Fri, 1 Aug 2025 14:34:19 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=202FA=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/.env.example | 7 + apps/cm/.gitignore | 41 +++ apps/cm/.prettierrc | 3 + apps/cm/app/api/auth/logout/route.ts | 17 + apps/cm/app/api/proxy/[...url]/route.ts | 145 ++++++++ apps/cm/app/auth/login/page.tsx | 2 + apps/cm/app/dashboard/page.tsx | 2 + apps/cm/app/layout.tsx | 8 + apps/cm/eslint.config.mjs | 21 ++ apps/cm/middleware.ts | 79 +++++ apps/cm/next.config.ts | 40 +++ apps/cm/package.json | 63 ++++ apps/cm/pages/.gitkeep | 0 apps/cm/postcss.config.mjs | 8 + apps/cm/public/favicon.ico | Bin 0 -> 4286 bytes apps/cm/public/images/bg_login.jpg | Bin 0 -> 135228 bytes apps/cm/public/images/img_cc_by.png | Bin 0 -> 4498 bytes apps/cm/public/images/img_cc_by_nc.png | Bin 0 -> 5637 bytes apps/cm/public/images/img_cc_by_nc_nd.png | Bin 0 -> 6651 bytes apps/cm/public/images/img_cc_by_nc_sa.png | Bin 0 -> 6694 bytes apps/cm/public/images/img_cc_by_nd.png | Bin 0 -> 5510 bytes apps/cm/public/images/img_cc_by_sa.png | Bin 0 -> 5668 bytes apps/cm/public/images/img_opentype01.png | Bin 0 -> 3246 bytes apps/cm/public/images/img_opentype02.png | Bin 0 -> 3628 bytes apps/cm/public/images/img_opentype03.png | Bin 0 -> 3605 bytes apps/cm/public/images/img_opentype04.png | Bin 0 -> 4117 bytes apps/cm/src/app/index.tsx | 20 ++ apps/cm/src/app/providers/app-provider.tsx | 49 +++ apps/cm/src/app/providers/index.ts | 1 + apps/cm/src/app/styles/base.css | 21 ++ apps/cm/src/app/styles/components.css | 129 ++++++++ .../cm/src/entities/auth/api/login-service.ts | 85 +++++ apps/cm/src/entities/auth/index.ts | 2 + .../cm/src/entities/auth/lib/auth-wrapper.tsx | 49 +++ .../cm/src/entities/auth/lib/token-manager.ts | 59 ++++ .../entities/auth/model/login-interface.ts | 9 + .../cm/src/entities/menu-authority/@x/menu.ts | 1 + .../src/entities/menu-authority/@x/posting.ts | 1 + .../api/menu-authority-service.ts | 50 +++ apps/cm/src/entities/menu-authority/index.ts | 9 + .../model/menu-authority-interface.ts | 51 +++ .../model/menu-authority-table-columns.tsx | 117 +++++++ .../menu-authority/ui/authority-button.tsx | 26 ++ .../ui/management-authority-dialog.tsx | 101 ++++++ apps/cm/src/entities/menu/api/menu-service.ts | 79 +++++ apps/cm/src/entities/menu/index.ts | 12 + .../cm/src/entities/menu/lib/menu-wrapper.tsx | 58 ++++ apps/cm/src/entities/menu/lib/utils.ts | 20 ++ .../src/entities/menu/model/menu-interface.ts | 107 ++++++ apps/cm/src/pages/auth/login/index.ts | 1 + apps/cm/src/pages/auth/login/ui/OtpModal.tsx | 153 +++++++++ .../cm/src/pages/auth/login/ui/TwoFAModal.tsx | 198 +++++++++++ .../cm/src/pages/auth/login/ui/login-page.tsx | 276 ++++++++++++++++ apps/cm/src/pages/auth/login/ui/login.css | 311 ++++++++++++++++++ apps/cm/src/pages/dashboard/index.ts | 1 + .../src/pages/dashboard/ui/dashboard-page.tsx | 94 ++++++ apps/cm/src/shared/api/client.ts | 11 + apps/cm/src/shared/api/index.ts | 1 + apps/cm/src/shared/config/api.ts | 73 ++++ apps/cm/src/shared/config/env.ts | 11 + apps/cm/src/shared/config/index.ts | 2 + apps/cm/src/shared/lib/cookie.ts | 29 ++ apps/cm/src/shared/lib/index.ts | 8 + apps/cm/src/shared/lib/match-query-status.ts | 65 ++++ apps/cm/src/shared/lib/use-alert-store.ts | 14 + .../cm/src/shared/lib/use-attachment-store.ts | 14 + apps/cm/src/shared/lib/use-data-table.tsx | 42 +++ apps/cm/src/shared/lib/use-menu-store.ts | 14 + .../src/shared/lib/use-mutation-wrapper.tsx | 29 ++ apps/cm/src/shared/lib/use-prompt.tsx | 26 ++ apps/cm/src/shared/lib/use-select-prompt.tsx | 48 +++ apps/cm/src/shared/lib/use-tus.ts | 90 +++++ apps/cm/src/shared/lib/use-user-store.ts | 14 + apps/cm/src/shared/model/api-interface.ts | 34 ++ apps/cm/src/shared/model/index.ts | 1 + apps/cm/src/shared/providers/index.ts | 2 + .../src/shared/providers/prompt-provider.tsx | 61 ++++ .../src/shared/providers/store-provider.tsx | 38 +++ apps/cm/src/shared/store/alert-store.ts | 25 ++ apps/cm/src/shared/store/attachment-store.ts | 25 ++ apps/cm/src/shared/store/index.ts | 4 + apps/cm/src/shared/store/menu-store.ts | 93 ++++++ apps/cm/src/shared/store/user-store.ts | 57 ++++ apps/cm/src/shared/ui/alert-dialog.tsx | 34 ++ apps/cm/src/shared/ui/confirm-dialog.tsx | 48 +++ apps/cm/src/shared/ui/form.tsx | 175 ++++++++++ .../cm/src/shared/ui/grid-view-data-table.tsx | 158 +++++++++ apps/cm/src/shared/ui/info-tooltip.tsx | 23 ++ .../src/shared/ui/input-date-range-picker.tsx | 47 +++ apps/cm/src/shared/ui/no-data.tsx | 17 + apps/cm/src/shared/ui/page-layout.tsx | 22 ++ apps/cm/src/shared/ui/prompt-dialog.tsx | 49 +++ .../cm/src/shared/ui/select-prompt-dialog.tsx | 68 ++++ apps/cm/src/shared/ui/skeleton-page.tsx | 14 + apps/cm/src/shared/ui/sonner.tsx | 25 ++ apps/cm/src/shared/ui/tooltip.tsx | 61 ++++ apps/cm/src/widgets/dnd-tree/index.ts | 2 + .../src/widgets/dnd-tree/ui/custom-node.tsx | 105 ++++++ apps/cm/src/widgets/dnd-tree/ui/dnd-tree.tsx | 138 ++++++++ .../src/widgets/dnd-tree/ui/editable-node.tsx | 44 +++ apps/cm/src/widgets/form-wrapper/index.ts | 1 + .../widgets/form-wrapper/ui/form-wrapper.tsx | 63 ++++ apps/cm/start_cs.sh | 25 ++ apps/cm/tailwind.config.ts | 27 ++ apps/cm/tsconfig.json | 47 +++ pnpm-lock.yaml | 162 ++++++++- 106 files changed, 4705 insertions(+), 7 deletions(-) create mode 100644 apps/cm/.env.example create mode 100644 apps/cm/.gitignore create mode 100644 apps/cm/.prettierrc create mode 100644 apps/cm/app/api/auth/logout/route.ts create mode 100644 apps/cm/app/api/proxy/[...url]/route.ts create mode 100644 apps/cm/app/auth/login/page.tsx create mode 100644 apps/cm/app/dashboard/page.tsx create mode 100644 apps/cm/app/layout.tsx create mode 100644 apps/cm/eslint.config.mjs create mode 100644 apps/cm/middleware.ts create mode 100644 apps/cm/next.config.ts create mode 100644 apps/cm/package.json create mode 100644 apps/cm/pages/.gitkeep create mode 100644 apps/cm/postcss.config.mjs create mode 100644 apps/cm/public/favicon.ico create mode 100644 apps/cm/public/images/bg_login.jpg create mode 100644 apps/cm/public/images/img_cc_by.png create mode 100644 apps/cm/public/images/img_cc_by_nc.png create mode 100644 apps/cm/public/images/img_cc_by_nc_nd.png create mode 100644 apps/cm/public/images/img_cc_by_nc_sa.png create mode 100644 apps/cm/public/images/img_cc_by_nd.png create mode 100644 apps/cm/public/images/img_cc_by_sa.png create mode 100644 apps/cm/public/images/img_opentype01.png create mode 100644 apps/cm/public/images/img_opentype02.png create mode 100644 apps/cm/public/images/img_opentype03.png create mode 100644 apps/cm/public/images/img_opentype04.png create mode 100644 apps/cm/src/app/index.tsx create mode 100644 apps/cm/src/app/providers/app-provider.tsx create mode 100644 apps/cm/src/app/providers/index.ts create mode 100644 apps/cm/src/app/styles/base.css create mode 100644 apps/cm/src/app/styles/components.css create mode 100644 apps/cm/src/entities/auth/api/login-service.ts create mode 100644 apps/cm/src/entities/auth/index.ts create mode 100644 apps/cm/src/entities/auth/lib/auth-wrapper.tsx create mode 100644 apps/cm/src/entities/auth/lib/token-manager.ts create mode 100644 apps/cm/src/entities/auth/model/login-interface.ts create mode 100644 apps/cm/src/entities/menu-authority/@x/menu.ts create mode 100644 apps/cm/src/entities/menu-authority/@x/posting.ts create mode 100644 apps/cm/src/entities/menu-authority/api/menu-authority-service.ts create mode 100644 apps/cm/src/entities/menu-authority/index.ts create mode 100644 apps/cm/src/entities/menu-authority/model/menu-authority-interface.ts create mode 100644 apps/cm/src/entities/menu-authority/model/menu-authority-table-columns.tsx create mode 100644 apps/cm/src/entities/menu-authority/ui/authority-button.tsx create mode 100644 apps/cm/src/entities/menu-authority/ui/management-authority-dialog.tsx create mode 100644 apps/cm/src/entities/menu/api/menu-service.ts create mode 100644 apps/cm/src/entities/menu/index.ts create mode 100644 apps/cm/src/entities/menu/lib/menu-wrapper.tsx create mode 100644 apps/cm/src/entities/menu/lib/utils.ts create mode 100644 apps/cm/src/entities/menu/model/menu-interface.ts create mode 100644 apps/cm/src/pages/auth/login/index.ts create mode 100644 apps/cm/src/pages/auth/login/ui/OtpModal.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/TwoFAModal.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/login-page.tsx create mode 100644 apps/cm/src/pages/auth/login/ui/login.css create mode 100644 apps/cm/src/pages/dashboard/index.ts create mode 100644 apps/cm/src/pages/dashboard/ui/dashboard-page.tsx create mode 100644 apps/cm/src/shared/api/client.ts create mode 100644 apps/cm/src/shared/api/index.ts create mode 100644 apps/cm/src/shared/config/api.ts create mode 100644 apps/cm/src/shared/config/env.ts create mode 100644 apps/cm/src/shared/config/index.ts create mode 100644 apps/cm/src/shared/lib/cookie.ts create mode 100644 apps/cm/src/shared/lib/index.ts create mode 100644 apps/cm/src/shared/lib/match-query-status.ts create mode 100644 apps/cm/src/shared/lib/use-alert-store.ts create mode 100644 apps/cm/src/shared/lib/use-attachment-store.ts create mode 100644 apps/cm/src/shared/lib/use-data-table.tsx create mode 100644 apps/cm/src/shared/lib/use-menu-store.ts create mode 100644 apps/cm/src/shared/lib/use-mutation-wrapper.tsx create mode 100644 apps/cm/src/shared/lib/use-prompt.tsx create mode 100644 apps/cm/src/shared/lib/use-select-prompt.tsx create mode 100644 apps/cm/src/shared/lib/use-tus.ts create mode 100644 apps/cm/src/shared/lib/use-user-store.ts create mode 100644 apps/cm/src/shared/model/api-interface.ts create mode 100644 apps/cm/src/shared/model/index.ts create mode 100644 apps/cm/src/shared/providers/index.ts create mode 100644 apps/cm/src/shared/providers/prompt-provider.tsx create mode 100644 apps/cm/src/shared/providers/store-provider.tsx create mode 100644 apps/cm/src/shared/store/alert-store.ts create mode 100644 apps/cm/src/shared/store/attachment-store.ts create mode 100644 apps/cm/src/shared/store/index.ts create mode 100644 apps/cm/src/shared/store/menu-store.ts create mode 100644 apps/cm/src/shared/store/user-store.ts create mode 100644 apps/cm/src/shared/ui/alert-dialog.tsx create mode 100644 apps/cm/src/shared/ui/confirm-dialog.tsx create mode 100644 apps/cm/src/shared/ui/form.tsx create mode 100644 apps/cm/src/shared/ui/grid-view-data-table.tsx create mode 100644 apps/cm/src/shared/ui/info-tooltip.tsx create mode 100644 apps/cm/src/shared/ui/input-date-range-picker.tsx create mode 100644 apps/cm/src/shared/ui/no-data.tsx create mode 100644 apps/cm/src/shared/ui/page-layout.tsx create mode 100644 apps/cm/src/shared/ui/prompt-dialog.tsx create mode 100644 apps/cm/src/shared/ui/select-prompt-dialog.tsx create mode 100644 apps/cm/src/shared/ui/skeleton-page.tsx create mode 100644 apps/cm/src/shared/ui/sonner.tsx create mode 100644 apps/cm/src/shared/ui/tooltip.tsx create mode 100644 apps/cm/src/widgets/dnd-tree/index.ts create mode 100644 apps/cm/src/widgets/dnd-tree/ui/custom-node.tsx create mode 100644 apps/cm/src/widgets/dnd-tree/ui/dnd-tree.tsx create mode 100644 apps/cm/src/widgets/dnd-tree/ui/editable-node.tsx create mode 100644 apps/cm/src/widgets/form-wrapper/index.ts create mode 100644 apps/cm/src/widgets/form-wrapper/ui/form-wrapper.tsx create mode 100644 apps/cm/start_cs.sh create mode 100644 apps/cm/tailwind.config.ts create mode 100644 apps/cm/tsconfig.json diff --git a/apps/cm/.env.example b/apps/cm/.env.example new file mode 100644 index 000000000..9f786831b --- /dev/null +++ b/apps/cm/.env.example @@ -0,0 +1,7 @@ +# apps/cs/src/shared/config/env.ts 참고 +NEXT_PUBLIC_PORT=3000 +NEXT_PUBLIC_PROXY_HOST=http://localhost:3000 +NEXT_PUBLIC_SITE_ID=CS +NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000 +NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 +NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 \ No newline at end of file diff --git a/apps/cm/.gitignore b/apps/cm/.gitignore new file mode 100644 index 000000000..b63718b61 --- /dev/null +++ b/apps/cm/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.env* +!.env.example \ No newline at end of file diff --git a/apps/cm/.prettierrc b/apps/cm/.prettierrc new file mode 100644 index 000000000..b4bfed357 --- /dev/null +++ b/apps/cm/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/apps/cm/app/api/auth/logout/route.ts b/apps/cm/app/api/auth/logout/route.ts new file mode 100644 index 000000000..fdcc9df7c --- /dev/null +++ b/apps/cm/app/api/auth/logout/route.ts @@ -0,0 +1,17 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; +import { + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, + REFRESH_TOKEN, +} from "@/shared/config"; + +export async function POST() { + const cookieStore = await cookies(); + cookieStore.delete(ACCESS_TOKEN); + cookieStore.delete(REFRESH_TOKEN); + cookieStore.delete(AUTH_USER_ID); + cookieStore.delete(CLAIM_NAME); + return NextResponse.json({ message: "Logged out" }, { status: 200 }); +} diff --git a/apps/cm/app/api/proxy/[...url]/route.ts b/apps/cm/app/api/proxy/[...url]/route.ts new file mode 100644 index 000000000..761991e39 --- /dev/null +++ b/apps/cm/app/api/proxy/[...url]/route.ts @@ -0,0 +1,145 @@ +import { + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, + REFRESH_TOKEN, + SERVER_API_URL, + SITE_ID, +} from "@/shared/config"; +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +async function handler( + request: Request, + params: { url: string[] }, + method: "GET" | "POST" | "PUT" | "DELETE", +) { + const cookieStore = await cookies(); + const headers: Record = { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + }; + let body = {}; + const isRefreshHandler = params.url.includes("refresh"); + + console.log(`[Proxy] ${method} ${params.url.join("/")}`); + console.log(`[Proxy] Server API URL: ${SERVER_API_URL}`); + + // silent refresh + if (isRefreshHandler) { + const refreshToken = cookieStore.get(REFRESH_TOKEN); + headers[CLAIM_NAME] = refreshToken?.value ?? ""; + + if (!headers[CLAIM_NAME]) { + return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); + } + } + + try { + if (!isRefreshHandler) { + body = await request.json(); + console.log(`[Proxy] Request body:`, body); + } + + const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`; + console.log(`[Proxy] Target URL: ${targetUrl}`); + + const result = await fetch(targetUrl, { + method, + headers, + body: JSON.stringify(body), + }); + + console.log(`[Proxy] Response status: ${result.status}`); + console.log(`[Proxy] Response headers:`, Object.fromEntries(result.headers.entries())); + + // 2FA 관련 엔드포인트인 경우 응답 본문도 확인 + const is2FAEndpoint = params.url.includes("2fa") || params.url.includes("auth"); + let responseData = {}; + + if (is2FAEndpoint) { + try { + responseData = await result.json(); + console.log(`[Proxy] 2FA Response data:`, responseData); + } catch { + // JSON 파싱 실패 시 빈 객체 사용 + console.log(`[Proxy] Failed to parse JSON response`); + } + } + + const accessToken = result.headers.get(ACCESS_TOKEN) || result.headers.get('ss-token'); + const refreshToken = result.headers.get(REFRESH_TOKEN); + const userId = result.headers.get(AUTH_USER_ID); + + console.log(`[Proxy] Tokens - accessToken: ${!!accessToken}, userId: ${!!userId}, refreshToken: ${!!refreshToken}`); + console.log(`[Proxy] Available headers:`, Array.from(result.headers.keys())); + + // 2FA 엔드포인트이고 실패한 경우 실제 오류 메시지 반환 + if (!result.ok && is2FAEndpoint) { + console.log(`[Proxy] 2FA endpoint failed, returning actual error`); + return NextResponse.json(responseData, { status: result.status }); + } + + // 2FA 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음) + if (result.ok && is2FAEndpoint) { + console.log(`[Proxy] 2FA endpoint succeeded, returning response data`); + return NextResponse.json(responseData, { status: result.status }); + } + + // 일반적인 실패 처리 + if (!result.ok) { + console.log(`[Proxy] General endpoint failed`); + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } + + if (accessToken && userId) { + const response = NextResponse.json({ + [ACCESS_TOKEN]: accessToken, + [AUTH_USER_ID]: userId, + ...responseData, // 2FA 관련 추가 데이터 포함 + }); + + if (refreshToken) { + cookieStore.set(REFRESH_TOKEN, refreshToken, { + httpOnly: true, + sameSite: "lax", + path: "/", + }); + } + + console.log(`[Proxy] Success response sent`); + return response; + } + + console.log(`[Proxy] Missing tokens in response`); + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } catch (error) { + console.error(`[Proxy] Error:`, error); + return NextResponse.json( + { message: "Server Error", error }, + { status: 500 }, + ); + } +} + +export async function POST( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "POST"); +} + +export async function PUT( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "PUT"); +} diff --git a/apps/cm/app/auth/login/page.tsx b/apps/cm/app/auth/login/page.tsx new file mode 100644 index 000000000..8f9e6adc6 --- /dev/null +++ b/apps/cm/app/auth/login/page.tsx @@ -0,0 +1,2 @@ +import { LoginPage } from "@/pages/auth/login"; +export default LoginPage; diff --git a/apps/cm/app/dashboard/page.tsx b/apps/cm/app/dashboard/page.tsx new file mode 100644 index 000000000..53d564ac6 --- /dev/null +++ b/apps/cm/app/dashboard/page.tsx @@ -0,0 +1,2 @@ +import { DashboardPage } from "@/pages/dashboard"; +export default DashboardPage; diff --git a/apps/cm/app/layout.tsx b/apps/cm/app/layout.tsx new file mode 100644 index 000000000..aa2049dcc --- /dev/null +++ b/apps/cm/app/layout.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import { App } from "@/app"; + +export const metadata: Metadata = { + title: "유역하수도 통합 정보플랫폼", +}; + +export default App; diff --git a/apps/cm/eslint.config.mjs b/apps/cm/eslint.config.mjs new file mode 100644 index 000000000..9109635e9 --- /dev/null +++ b/apps/cm/eslint.config.mjs @@ -0,0 +1,21 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + "react-hooks/exhaustive-deps": "off", + }, + }, +]; + +export default eslintConfig; diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts new file mode 100644 index 000000000..e0316fa9f --- /dev/null +++ b/apps/cm/middleware.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + CLAIM_NAME, + AUTH_USER_ID, + PORT, + PROXY_HOST, + SERVER_API_URL, + SITE_ID, +} from "./src/shared/config"; + +const serverApiUrl = + process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; + +export function middleware(req: NextRequest) { + const pathname = req.nextUrl.pathname; + const searchParams = req.nextUrl.searchParams; + + const logMessages: string[] = [ + `[Middleware Request] ${new Date().toISOString()}`, + `[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`, + `[Request]\n Original Pathname: ${pathname}`, + ]; + + if (pathname.startsWith("/server")) { + const queryParams = searchParams.toString(); + const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/attach-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; + const newUrl = `${serverApiUrl}${pathname}`; + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); + } + + if (pathname.startsWith("/user-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${serverApiUrl}${pathname}${ + queryParams ? `?${queryParams}` : "" + }`; + + logMessages.push(` User Service Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname === "/") { + return NextResponse.redirect(new URL("/dashboard", req.url)); + } + + logMessages.push("\n--------------------------------\n"); + console.log(logMessages.join("\n")); + return NextResponse.next(); +} + +export const config = { + matcher: ["/server/:path*", "/attach-service/:path*", "/user-service/:path*", "/"], +}; diff --git a/apps/cm/next.config.ts b/apps/cm/next.config.ts new file mode 100644 index 000000000..679528324 --- /dev/null +++ b/apps/cm/next.config.ts @@ -0,0 +1,40 @@ +import type { NextConfig } from "next"; + +const subDomain = + process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000"; + +const backendUrl = process.env.NEXT_PUBLIC_SERVER_API_URL || "http://192.168.0.102:8000"; + +const nextConfig: NextConfig = { + output: "standalone", + async rewrites() { + return { + beforeFiles: [ + { + source: "/_next/static/:path*", + destination: `${subDomain}/_next/static/:path*`, + has: [ + { + type: "header", + key: "referer", + value: "(.*)/cms(.*)", + }, + ], + }, + ], + afterFiles: [ + { + source: "/cms/:path*", + destination: `${subDomain}/cms/:path*`, + }, + { + source: "/user-service/:path*", + destination: `${backendUrl}/user-service/:path*`, + }, + ], + fallback: [], + }; + }, +}; + +export default nextConfig; diff --git a/apps/cm/package.json b/apps/cm/package.json new file mode 100644 index 000000000..81f22f095 --- /dev/null +++ b/apps/cm/package.json @@ -0,0 +1,63 @@ +{ + "name": "cm", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@common/assets": "workspace:*", + "@common/components": "workspace:*", + "@common/config": "workspace:*", + "@fontsource-variable/noto-sans-kr": "^5.2.5", + "@frontend-opensource/use-react-hooks": "^1.1.4", + "@fullcalendar/core": "^6.1.17", + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", + "@fullcalendar/timegrid": "^6.1.17", + "@hookform/resolvers": "^4.1.3", + "@minoru/react-dnd-treeview": "3.5.1-alpha.1", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@tanstack/react-query": "^5.67.1", + "@tanstack/react-query-devtools": "^5.75.5", + "@tanstack/react-table": "^8.21.2", + "@tinymce/tinymce-react": "^6.1.0", + "axios": "^1.8.1", + "chart.js": "^4.4.8", + "dayjs": "^1.11.13", + "js-cookie": "^3.0.5", + "lucide-react": "^0.474.0", + "next": "15.3.0", + "next-qrcode": "^2.5.1", + "next-themes": "^0.4.6", + "react-chartjs-2": "^5.3.0", + "react-dnd": "^16.0.1", + "react-hook-form": "^7.54.2", + "sonner": "^2.0.1", + "tailwindcss-animate": "^1.0.7", + "tus-js-client": "^4.3.1", + "zod": "^3.24.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.13.1", + "@types/webpack": "^5.28.5", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "postcss": "^8", + "prettier": "^3.4.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "steiger": "^0.5.5", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/apps/cm/pages/.gitkeep b/apps/cm/pages/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/cm/postcss.config.mjs b/apps/cm/postcss.config.mjs new file mode 100644 index 000000000..1a69fd2a4 --- /dev/null +++ b/apps/cm/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/apps/cm/public/favicon.ico b/apps/cm/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d304b69814b2d79060a24f36f667a7f53b768103 GIT binary patch literal 4286 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 z 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 literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_cc_by.png b/apps/cm/public/images/img_cc_by.png new file mode 100644 index 0000000000000000000000000000000000000000..628cc2bfd3fdd655056b888ff2e516eb66c45812 GIT binary patch literal 4498 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 literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_cc_by_nc_nd.png b/apps/cm/public/images/img_cc_by_nc_nd.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8ccb18dc00d7898c8a25181af55e2ed2143ed0 GIT binary patch literal 6651 zcmaJ`cUY6lv!+N_QHs(_sDkt!Kw2>L-ir_*Kms8^0wna_kq!b<1!>YniYN$((nOFZ zRhoeG-toqBf9Kr$$2~XCm#;lD@6PVd?mIJIjGhko7C8es9vj}QPeGDDkbYXR+% z2tmjn89@xf9jA?lC#QgMhuFJ7(QI~5CzzW&VCQW+fDPs#4=|O`7S?uGfjYy~d_AFt zzB)$szApAM4giHmY;qVN4gdi~L)b6~S2q+8BMN6<%15b5b8BqAdt^9MszQ~)O-fWo?=As7KS6vy8P zASlY-6XuSFA>G*iAVTbrUTAp$&eH$x0>b?tSU1#vh6y)dLKujH7C5>Nz9P zNR%_uolV71l8x6C=H`I(K|T0OURxWe?uJ4`-0Y$1Ab9`|K@bLW0E&wXi%1HKJXTc} zkrEM6l~IzB6qW|bC`(97Jr-9H6Z=~igtYfUK;6)P>pJ|SD=Pf2x_?{&;f`w=1oeb@ zLmeJ_A`xtVbr}fzcUwUJiuWH~hkvz2MD<^Fg>c3Q{h8SRHPL^+!gvQF#Xe!+y?C-3*> z)H3Mfy^f#wZ8!eBTvb@L@>Asp0POnG9<0-^TNuBtxSYM7tt!?~CyXOjxH?QVxj5cp zo_^!KF|+42T$%D)({R+_%l_{s@yPYHU)JYacIaiV-1zu-$l?@6E*x74CN1K z+UknUj%tdRhlGfdTU%R!K%mA$Ha$&jNpo}an(=Us6!JP)9K^n>0^_QQi{)!KlYjwR zfoQYpq{pSCFyxcWCht7=jZN=|!XU7v^5!&y3kLZr~SNb(HzD-Pkb6`H(oLrpp z1A%F&2S$Y@gEPN!vLu@t%aJBFnwA?YarGsIf@v=g^bejUCbToqf9z*b2((JF=?LQe zTx%iG1t)fNbQIXM1j@)tz@|W&`rDTMu9K8vQ7j5x4^J`sgO(*Ebo7jki1~3;aArv( zSITfRF-(z8Ghe~)+PP!6IJ|o#tkSrqM^3(dw;lnDtCr9tOd&^nOyA`X0TV1PErujd z1y>LoGc#FYZ|^P(y*J2Pg-O6iovU}`b`4mH_T(d!`7iA-Uy}>MC#46?pvhmho?irf zuw)k2f?hI5HD`JwyQXe=h}8Av6B7#x3ch*s=H<(m5fKsV>+8wM$x_HUqbU`edB4w! zJB49$WfoZ@X5PnPQ90v?_}J)W9yQnLH;DcWfe-ixEkVBnjf{+}te)?(zdl$|c#Ib} z@{2m;CQpBV|8#v|e*~)C1K$=XD=RB4{VXTPp2bWG2t1!^bg{$eo;-Q+De+!(+VuMH z3-EhXrctmni{Jif?;CAPp}cn9_PGM3FpEYwgyr6)+8B!*hHZxP(f|X(g!{3cEc`wl zsm)%f%*=ky+}H#9^ycRhp|yv?QlN3b6&W>8;MLK@V75eRBEIgz!a}7<75UAZhYHL2fkItQ zk(50+jn~i9vl=^nWJIyElXdnyx_PcnPA4G(8XC1TxZa#>&-(p3?TtS?TyJn1eP!Mt z{t>1uZlR{GbLMgO>n+UZBdw7zdhP4nJHICQSl{HNnJ>MT=h3gRrB8`F(@nb0&&LN| zet*4l4IAzt_$iPha(Fbd^KvLlL{^IDe#pSs+}x9yPEWt%&APfe4pSW;i=IEw7s28)G|6&Et|^H|zswa~aMainO9rMflNmt~pkE&8qQkt6m!NGU~ zlrGbl7$8qybLfmH!u2qxNdl6h#T#d%d>-Zrpt+t%}v*2QgRqb;ZWIXwhi0B za8_JgJkx@TY7gBg8E-<`NQdhL+u+ehh^wn{hywPTQ0?qNIOl;&^4EB6PI}9X7)nfzUHwz(qi_Kx3Q@1A2~5)+4FY5Aj# z7FRzfbFiBfjhARfjz|oj#Gi>GlBh0R? zuCA^c$F5p||2rr@1Da_rNLqwF`B6uB0Ox9zzr8xiZKkp(?|=bzOY4U1ou@lzM!Hgu z1ViwmMS2n#d4hxm1QJN!_@8;!VmHRC?s7ctFon1LNPA$Ic7v7$pDjG6i_T|fX3V(p z*|TRm?=Qae_O4&-v^#Mog;NJxPE=cqkO8^k`-yb&ALX{|n--}F)8WP9gYEnYk~lg2 zH#?oaJnyyfr+5}xdQxlyR0wN$N=!^Ky*<-nc*`vv$qYmXKki9n)&2eTy-IcNjBp|8 zT{TBOwC~X`qW1oLV8HodhJfX=sp4Xp{)^?*?cobcbTqsB1C&75;U|TC+N6rPXu3XZY?@?k4Ltj7Xts zJdZS?lPF-ZI6DJ|XBA9F`OSoPFw4tEMvBV)tk!8LFINUd9CpR+a(RO!D=TX!RXHHw z!lY`=!AahTmj=p_&FId`aF?ak_5;D0b^Oh@>}k719-8a9%^#0b&R`@bpZdzR$&l3!pEJ80`^=fU|w*Ze^siMch4sWe#oQ5P2yn z@FRmlVy6^T@(@J#EBmZnb+NP}aXpXko#;J14~ng&0Jcm^%$=sd>oceI4?~YGv<6Nc zN2;Rg<&-x2GUjL>SVwkLFdP%lxk1Y_iZcpRQi@DoQ&Uk9RNAt*5=PZmmm8MGp6da$ z)T13-8(Tu;f{_;xmqIvzrngZ@pw?A2?CEG4&2VOvU)inON)w%UZ9BLvoSIv!gTA+` z`J-L2Ww;-?oI7V8lq$5HeE&dfWaovm5n*KtDN27EU6j09hu6DqtJ|5)FrX^KcdQsY zPs2f-oAE#!Lo3F)$8xxhnE-)c!*l6)hp*kmn)(4Qnc>K5Z;2X_w-_GD$jCP}v$ z-gcs%l24Z9(wgfzSgb~Rx)J-2XIcCZ!H2~e(|OV!ML1$LSk!Vz1uIX@zV%VpL8HCc zM7UxbE1p?CLtUNB2brcujyVlHV;^xc;T++~3w>Ws)*JW3jp-1#ko#Ke+?Ye7Qp5)a zi5e>R((5Yd116gRHB`IplV&R~6JP;`D{k;Crd!LX{GX)r5Xq5LfP+0Sl#zM=23*o3 zlR?Y9=<~>O(;RAC_Sb-Ma>2+a%~Yl}c}9F-XA65bFJzrrC{H7zD!3^(Zc+ z-~okYquWuzk+*VM!S;BO{s#GDbaei=Hh22ZzuTBe$hdsml2> z5BuVUQ}1JeJt|@zPENH;3h)c1?TW!D{{8Nl($CTr6J>5t1edNpjc2Ey;pI6t@coa6 z55`wkSHXzne z%I>QY>)DuDSV}g+HSS#OePUf9!@DH5wxzWqp-r;IbC0D-A{FiI>MEqEGg$ThA`uVY zg`*L}wIR7-%<#_i6&lXnO8Q}b)9Pj9hGbs?3ARoDvo;}^Vrlnwp%T75=O>K5j0 zT_t=mBa&I4ZfW+kDI>~E8=iG|vMq#BKegbR?u?+Irl!WWMfvI0sYiON;gj4xe%T&) z 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 C&0xI% literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_cc_by_nc_sa.png b/apps/cm/public/images/img_cc_by_nc_sa.png new file mode 100644 index 0000000000000000000000000000000000000000..00f402c9e1a34e5d88e949b5cf5d31cd17b20fc5 GIT binary patch literal 6694 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 literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_cc_by_nd.png b/apps/cm/public/images/img_cc_by_nd.png new file mode 100644 index 0000000000000000000000000000000000000000..0da64acd8a24fed8fd21782589ba77e69fab6d8b GIT binary patch literal 5510 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 Ow^dWtR4RRB5%OP9)_QaR literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_opentype01.png b/apps/cm/public/images/img_opentype01.png new file mode 100644 index 0000000000000000000000000000000000000000..456c7c6bb211a83deb3ca26e155020c6a1ac1c53 GIT binary patch literal 3246 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 NfwytQmZFcG{2yPxrMmzC literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_opentype02.png b/apps/cm/public/images/img_opentype02.png new file mode 100644 index 0000000000000000000000000000000000000000..a6c5bfb3229e319b6d8e4a61982061f7aecc7145 GIT binary patch literal 3628 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 MMwYmHhNq+c58HlKYXATM literal 0 HcmV?d00001 diff --git a/apps/cm/public/images/img_opentype04.png b/apps/cm/public/images/img_opentype04.png new file mode 100644 index 0000000000000000000000000000000000000000..41d284cabcdccc032c0fee538260079c4d75d374 GIT binary patch literal 4117 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 E0FhT$uK)l5 literal 0 HcmV?d00001 diff --git a/apps/cm/src/app/index.tsx b/apps/cm/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/cm/src/app/index.tsx @@ -0,0 +1,20 @@ +import "./styles/base.css"; +import "./styles/components.css"; +import "@fontsource-variable/noto-sans-kr"; + +import { AppProvider } from "./providers"; +import { cookies } from "next/headers"; + +const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { + const theme = (await cookies()).get("site-theme-key"); + + return ( + + + {children} + + + ); +}; + +export { App }; diff --git a/apps/cm/src/app/providers/app-provider.tsx b/apps/cm/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..efd1f308e --- /dev/null +++ b/apps/cm/src/app/providers/app-provider.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { DndProvider } from "react-dnd"; +import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; +import { AuthWrapper } from "@/entities/auth"; +import { MenuWrapper } from "@/entities/menu"; +import { Toaster } from "@/shared/ui/sonner"; +import { AlertDialog } from "@/shared/ui/alert-dialog"; +import { PromptDialog } from "@/shared/ui/prompt-dialog"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog"; +import { PromptProvider, StoreProvider } from "@/shared/providers"; + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/cm/src/app/providers/index.ts b/apps/cm/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/cm/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/cm/src/app/styles/base.css b/apps/cm/src/app/styles/base.css new file mode 100644 index 000000000..7a5263bc4 --- /dev/null +++ b/apps/cm/src/app/styles/base.css @@ -0,0 +1,21 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; +@import "@common/components/index.css"; + +@tailwind base; +@tailwind utilities; + +@layer base { + :root { + --secondary-background: rgba(0, 0, 0, 0.04); + } + [data-theme="black"] { + --secondary-background: rgba(255, 255, 255, 0.09); + } + + html, + body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; + } +} diff --git a/apps/cm/src/app/styles/components.css b/apps/cm/src/app/styles/components.css new file mode 100644 index 000000000..5b9742c0f --- /dev/null +++ b/apps/cm/src/app/styles/components.css @@ -0,0 +1,129 @@ +.card { + position: relative; + margin: 0.6rem; + box-sizing: border-box; + min-height: 2rem; + border-radius: 0.5rem; + padding: 0.6rem; + font-size: 1.3rem; + color: #666; +} + +.card_width10 { + width: calc(10% - 12px); +} + +.card_width20 { + width: calc(20% - 12px); +} + +.card_width25 { + width: calc(25% - 12px); +} + +.card_width30 { + width: calc(30% - 12px); +} + +.card_width33 { + width: calc(33.333% - 12px); +} + +.card_width40 { + width: calc(40% - 12px); +} + +.card_width45 { + width: calc(45% - 12px); +} + +.card_width50 { + width: calc(50% - 12px); +} + +.card_width60 { + width: calc(60% - 12px); +} + +.card_width66 { + width: calc(66.666% - 12px); +} + +.card_width70 { + width: calc(70% - 12px); +} + +.card_width75 { + width: calc(75% - 12px); +} + +.card_width80 { + width: calc(80% - 12px); +} + +.card_width90 { + width: calc(90% - 12px); +} + +.card_width100 { + width: calc(100% - 12px); +} + +.card_nopadding { + padding: 0 !important; +} + +.card_nomargin { + width: 100%; + margin: 0 !important; + padding: 0 !important; +} + +.topline { + border-top: 1px solid #ccc; +} + +.bottomline { + border-bottom: 1px solid #e6eef3; + background: #fff; +} + +.card-border { + border: 1px solid #dbe2e6; + background-color: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.card-header { + margin-top: -0.6rem; + margin-left: -0.6rem; + margin-right: -0.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e1e7eb; + padding: 1rem; +} + +.card-footer { + margin-left: -0.6rem; + margin-right: -0.6rem; + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid #e1e7eb; + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* 블랙 테마 */ +[data-theme="black"] { + .card-border { + background-color: rgba(255, 255, 255, 0.065); + border-color: #38383a; + } + .card-header { + border-bottom-color: #3f4042; + } +} diff --git a/apps/cm/src/entities/auth/api/login-service.ts b/apps/cm/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..c1968fe36 --- /dev/null +++ b/apps/cm/src/entities/auth/api/login-service.ts @@ -0,0 +1,85 @@ +import { AxiosError } from "axios"; +import { + ENDPOINT, + JWT_EXPIRATION_TIME, + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, +} from "@/shared/config"; +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise { + try { + const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + if (error instanceof AxiosError) { + throw new Error(`Login failed: ${error.response?.data?.message}`); + } + throw new Error(`Login failed: ${error}`); + } + } + + public async silentRefresh(): Promise { + try { + const response = await fetch( + `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + if (error instanceof AxiosError) { + throw new Error( + `Silent refresh failed: ${error.response?.data?.message}`, + ); + } + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/cm/src/entities/auth/index.ts b/apps/cm/src/entities/auth/index.ts new file mode 100644 index 000000000..97e142225 --- /dev/null +++ b/apps/cm/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/cm/src/entities/auth/lib/auth-wrapper.tsx b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..c0be4c2dd --- /dev/null +++ b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,49 @@ +"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}/info/me`, + ); + 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/cm/src/entities/auth/lib/token-manager.ts b/apps/cm/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..0e761486a --- /dev/null +++ b/apps/cm/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config"; +import { getCookie, setCookie, removeCookie } from "@/shared/lib"; + +export class TokenManager { + private static instance: TokenManager; + private refreshTimer: NodeJS.Timeout | null = null; + + private constructor() {} + + public static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + public getToken(key: string): string | undefined { + return getCookie(key); + } + + public setToken(key: string, value: string): void { + setCookie(key, value, { + secure: window.location.protocol === "https:", + sameSite: "lax", + }); + } + + public removeToken(key: string): void { + removeCookie(key); + } + + public setupRefreshTimer( + callback: () => Promise, + expirationTime: number, + ): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + callback().catch((error) => { + console.error("Silent refresh failed:", error); + this.clearAuthToken(); + }); + }, expirationTime - 60000); + } + + public clearAuthToken(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + this.removeToken(CLAIM_NAME); + this.removeToken(AUTH_USER_ID); + } +} + +export const tokenManager = TokenManager.getInstance(); diff --git a/apps/cm/src/entities/auth/model/login-interface.ts b/apps/cm/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/cm/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise; + silentRefresh(): Promise; +} diff --git a/apps/cm/src/entities/menu-authority/@x/menu.ts b/apps/cm/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/cm/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/cm/src/entities/menu-authority/@x/posting.ts b/apps/cm/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/cm/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/cm/src/entities/menu-authority/api/menu-authority-service.ts b/apps/cm/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..ec07d88ad --- /dev/null +++ b/apps/cm/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,50 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayload, + MenuAuthorityService, + PageMenuAuthority, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async getPageMenuAuthority( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`, + ); + return response.data; + } + + public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } + + public async getGroupMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/group`, + { params }, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/cm/src/entities/menu-authority/index.ts b/apps/cm/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..5a1b11606 --- /dev/null +++ b/apps/cm/src/entities/menu-authority/index.ts @@ -0,0 +1,9 @@ +export { MenuAuthorityApi } from "./api/menu-authority-service"; +export { + menuAuthorityFormSchema, + type MenuAuthority, + type MenuAuthorityPayload, + type MenuAuthorityFormData, +} from "./model/menu-authority-interface"; +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/cm/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/cm/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..7f5401872 --- /dev/null +++ b/apps/cm/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + useYn?: string; +} + +export interface PageMenuAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export const menuAuthoritySchema = z.object({ + groupId: z.string(), + groupNm: z.string(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer; +export type MenuAuthorityList = PageableList; +export type MenuAuthorityPayload = z.infer; +export type MenuAuthorityFormData = z.infer; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise; + getGroupMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/cm/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/cm/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/cm/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayload, +} from "./menu-authority-interface"; + +export const createColumns = ( + form: UseFormReturn, +): ColumnDef[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/cm/src/entities/menu-authority/ui/authority-button.tsx b/apps/cm/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..257ab6e5d --- /dev/null +++ b/apps/cm/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/shared/lib"; +import { Button, cn, type ButtonProps } from "@common/components"; +import type { PageMenuAuthority } from "../model/menu-authority-interface"; + +type AuthorityButtonProps = { + authority: keyof PageMenuAuthority; +} & ButtonProps; + +function AuthorityButton({ + authority, + className, + children, + ...props +}: AuthorityButtonProps) { + const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority); + + const isAuthority = pageMenuAuthority?.[authority] === "Y"; + + return ( + + ); +} + +export { AuthorityButton }; diff --git a/apps/cm/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/cm/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..2d4f6a176 --- /dev/null +++ b/apps/cm/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayload, +} from "../model/menu-authority-interface"; +import { createColumns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function ManagementAuthorityDialog({ menuCd }: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + const [open, setOpen] = useState(false); + + const query = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutationWrapper({ + mutationFn: (data: MenuAuthorityPayload[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다.", () => setOpen(false)); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: query.data?.content ?? [], + }, + }); + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns: createColumns(form), + getCoreRowModel: getCoreRowModel(), + }, + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + + + + + + + 그룹별 메뉴 권한 + +
{tableComponent}
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/cm/src/entities/menu/api/menu-service.ts b/apps/cm/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..07e9e552a --- /dev/null +++ b/apps/cm/src/entities/menu/api/menu-service.ts @@ -0,0 +1,79 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + MenuService, + Menu, + MenuItem, + MenuFormData, + MenuTemporarilyStoredData, + SiteMapFormData, +} from "../model/menu-interface"; + +export class MenuApiService implements MenuService { + async getMenuRoles(siteId: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } + + public async getMenuInfo(menuCd: number): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, + ); + return response.data; + } + + public async createMenu(data: MenuFormData): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENUS}`, + data, + ); + return response.data; + } + + public async updateMenu( + menuCd: number, + data: Omit, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`, + data, + ); + return response.data; + } + + public async deleteMenu(menuCd: number): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`); + } + + public async menuTemporaryStorage( + menuCd: number, + data: MenuFormData, + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/${menuCd}`, + data, + ); + return response.data; + } + + public async getMenuTemporarilyStoredData( + menuCd: number, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_TEMPORARY_STORAGE}/menu/${menuCd}/user`, + ); + return response.data; + } + + public async publishSiteMap(data: SiteMapFormData["siteMap"]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENUS}/sitemap/publish`, + data, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/cm/src/entities/menu/index.ts b/apps/cm/src/entities/menu/index.ts new file mode 100644 index 000000000..e4c61dbb7 --- /dev/null +++ b/apps/cm/src/entities/menu/index.ts @@ -0,0 +1,12 @@ +export { MenuApi } from "./api/menu-service"; +export { + MENU_TYPE, + menuFormSchema, + siteMapFormSchema, + type MenuType, + type MenuItem, + type MenuFormData, + type SiteMapFormData, + type SiteMapPayloadData, +} from "./model/menu-interface"; +export { MenuWrapper } from "./lib/menu-wrapper"; diff --git a/apps/cm/src/entities/menu/lib/menu-wrapper.tsx b/apps/cm/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..1f1080cac --- /dev/null +++ b/apps/cm/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu"; +import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; +import { SITE_ID } from "@/shared/config"; +import { MenuApi } from "../api/menu-service"; +import { getCurrentMenu } from "./utils"; + +const MenuWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); + + const alert = useAlertStore((state) => state.setMessage); + const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = + useMenuStore((state) => state); + + const getCurrentMenuInfo = async () => { + const menuRoles = await MenuApi.getMenuRoles(SITE_ID); + if (!menuRoles) return; + return getCurrentMenu(menuRoles, pathname ?? ""); + }; + + const getPageMenuAuthority = async () => { + const menuCd = getCookie("menu-code-key"); + if (!menuCd) return; + const response = await MenuAuthorityApi.getPageMenuAuthority( + parseInt(menuCd), + ); + return response; + }; + + const checkInquiryAuthority = () => { + return pageMenuAuthority?.inqAuthrtYn === "Y"; + }; + + useEffect(() => { + (async () => { + const currentMenu = await getCurrentMenuInfo(); + if (currentMenu) setCurrentMenuInfo(currentMenu); + + const response = await getPageMenuAuthority(); + if (response?.length) setPageMenuAuthority(response[0]!); + })(); + }, [pathname]); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}; +}; + +export { MenuWrapper }; diff --git a/apps/cm/src/entities/menu/lib/utils.ts b/apps/cm/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/cm/src/entities/menu/lib/utils.ts @@ -0,0 +1,20 @@ +import type { MenuItem } from "../model/menu-interface"; + +export const getCurrentMenu = ( + menus: MenuItem[], + pathname: string, +): MenuItem | null => { + for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + + if (menu.children) { + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; + } + } + return null; +}; diff --git a/apps/cm/src/entities/menu/model/menu-interface.ts b/apps/cm/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..0c5f4b54c --- /dev/null +++ b/apps/cm/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,107 @@ +import { z } from "zod"; + +export const MENU_TYPE = { + EMPTY: "EMPTY", + CONTENTS: "CONTENTS", + BOARD: "BOARD", + INSIDE: "INSIDE", + OUTSIDE: "OUTSIDE", + BOTTOM: "BOTTOM", +} as const; + +export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE]; + +export interface MenuItem { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: MenuItem[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +export interface MenuTemporarilyStoredData { + tmprStrgSn: number; + menuCd: number; + menuTmprStrg: string; + regDt: string; + rgtr: string; + mdfcnDt: string; + mdfr: string; +} + +export const menuFormSchema = z.object({ + siteId: z.string(), + menuKornNm: z.string(), + menuSubKornNm: z.string().nullish(), + menuEngNm: z.string().nullish(), + menuSubEngNm: z.string().nullish(), + levelNo: z.number(), + urlPath: z.string().nullish(), + iconNm: z.string().nullish(), + menuLnkgTypeCd: z.enum([ + MENU_TYPE.EMPTY, + MENU_TYPE.CONTENTS, + MENU_TYPE.BOARD, + MENU_TYPE.INSIDE, + MENU_TYPE.OUTSIDE, + MENU_TYPE.BOTTOM, + ]), + menuLnkgSn: z.number().nullish(), + otptYn: z.string().default("Y"), + prntMenuCd: z.number().nullish(), + sortSeq: z.number(), + evlUseYn: z.string().default("Y"), + qrcd: z.string().nullish(), + useYn: z.string().default("Y"), + newWndwYn: z.string().nullish(), + menuLnkgSnNm: z.string().nullish(), + menuPicUserUnqId: z.string().nullish(), + menuPicUserUnqIdNm: z.string().nullish(), + menuPicDsgnYmd: z.string().nullish(), +}); + +export const siteMapPayloadSchema = z.object({ + menuCd: z.number(), + menuSubKornNm: z.string(), + otptYn: z.string(), +}); + +export const siteMapFormSchema = z.object({ + siteMap: z.array(siteMapPayloadSchema), +}); + +export type MenuFormData = z.infer; +export type SiteMapFormData = z.infer; +export type SiteMapPayloadData = z.infer; +export type Menu = Omit & { + menuCd: number; + iconName?: string; +}; + +export interface MenuService { + getMenuRoles(siteId: string): Promise; + getMenuInfo: (menuCd: number) => Promise; + createMenu: (data: MenuFormData) => Promise; + updateMenu: ( + menuCd: number, + data: Omit, + ) => Promise; + deleteMenu: (menuCd: number) => Promise; + menuTemporaryStorage: (menuCd: number, data: MenuFormData) => Promise; + getMenuTemporarilyStoredData: ( + menuCd: number, + ) => Promise; + publishSiteMap: (data: SiteMapFormData["siteMap"]) => Promise; +} diff --git a/apps/cm/src/pages/auth/login/index.ts b/apps/cm/src/pages/auth/login/index.ts new file mode 100644 index 000000000..2093c3d8d --- /dev/null +++ b/apps/cm/src/pages/auth/login/index.ts @@ -0,0 +1 @@ +export { LoginPage } from "./ui/login-page"; diff --git a/apps/cm/src/pages/auth/login/ui/OtpModal.tsx b/apps/cm/src/pages/auth/login/ui/OtpModal.tsx new file mode 100644 index 000000000..7c6aeb9af --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/OtpModal.tsx @@ -0,0 +1,153 @@ +import { useState } from "react"; +import { ACCESS_TOKEN, AUTH_USER_ID, REFRESH_TOKEN, CLAIM_NAME } from "@/shared/config"; +import { tokenManager } from "@/entities/auth/lib/token-manager"; +import { + Button, + Dialog, + DialogContent, +} from "@common/components"; + +interface OtpModalProps { + onClose: () => void; + onSuccess: () => void; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function OtpModal({ onClose, onSuccess, open, onOpenChange }: OtpModalProps) { + const [otp, setOtp] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + try { + setIsLoading(true); + setError(""); + + const tempToken = localStorage.getItem("tempToken"); + console.log("tempToken from localStorage:", tempToken); + + if (!tempToken) { + setError("임시 토큰이 없습니다. 로그인을 다시 시도해주세요."); + return; + } + + // Proxy를 통해 OTP 인증 처리 + const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + otpCode: otp, + tempToken + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + let errorMsg = errorData.message || 'OTP 인증 실패'; + if (Array.isArray(errorData.errors) && errorData.errors.length > 0) { + errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', '); + } + throw new Error(errorMsg); + } + + const data = await response.json(); + console.log("OTP verification response:", data); + console.log("Response headers:", Object.fromEntries(response.headers.entries())); + console.log("Response data keys:", Object.keys(data)); + console.log("Full response data:", JSON.stringify(data, null, 2)); + + // 2FA 응답에서는 access-token만 있고 token-id는 없을 수 있음 + const accessToken = data[ACCESS_TOKEN] || data.accessToken || data.access_token || data.token || data['ss-token']; + + console.log("Extracted accessToken:", !!accessToken); + console.log("Token value - accessToken:", accessToken?.substring(0, 20) + "..."); + + // access-token이 있는 경우 저장 + if (accessToken) { + console.log("Saving access token from OTP verification"); + tokenManager.setToken(CLAIM_NAME, accessToken); // Authorization으로 저장 + + // 리프레시 토큰도 확인 + const refreshToken = data[REFRESH_TOKEN] || data.refreshToken || data.refresh_token; + if (refreshToken) { + console.log("Saving refresh token"); + tokenManager.setToken(REFRESH_TOKEN, refreshToken); + } + + // userId는 원래 로그인 응답에서 가져온 값 사용 + const originalUserId = data.userId; + if (originalUserId) { + console.log("Saving userId from original login response"); + tokenManager.setToken(AUTH_USER_ID, originalUserId); + } + + console.log("All tokens saved successfully"); + } else { + console.log("No access token in OTP response"); + console.log("Available data keys:", Object.keys(data)); + console.log("Data values:", Object.entries(data).map(([k, v]) => `${k}: ${typeof v === 'string' ? v.substring(0, 20) + '...' : v}`)); + } + + onSuccess(); + } catch (error: unknown) { + console.error("OTP verification error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("OTP 인증 실패"); + } + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + onOpenChange(false); + onClose(); + }; + + return ( + + +
+

OTP 인증

+ +
+ setOtp(e.target.value)} + disabled={isLoading} + /> + + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+
+
+ ); +} diff --git a/apps/cm/src/pages/auth/login/ui/TwoFAModal.tsx b/apps/cm/src/pages/auth/login/ui/TwoFAModal.tsx new file mode 100644 index 000000000..d66747b0c --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/TwoFAModal.tsx @@ -0,0 +1,198 @@ +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { + Button, + Dialog, + DialogContent, +} from "@common/components"; + +interface TwoFAModalProps { + onClose: () => void; + onSuccess: () => void; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export default function TwoFAModal({ onClose, onSuccess, open, onOpenChange }: TwoFAModalProps) { + const [qrImage, setQrImage] = useState(""); + const [secret, setSecret] = useState(""); + const [otp, setOtp] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isSetupLoading, setIsSetupLoading] = useState(true); + + useEffect(() => { + const fetchQR = async () => { + try { + setIsSetupLoading(true); + setError(""); + + const tempToken = localStorage.getItem("tempToken"); + if (!tempToken) { + setError("임시 토큰이 없습니다"); + return; + } + + // Proxy를 통해 2FA setup 처리 + const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/setup`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tempToken }), + }); + + if (!response.ok) { + const errorData = await response.json(); + let errorMsg = errorData.message || 'QR코드 발급 실패'; + if (Array.isArray(errorData.errors) && errorData.errors.length > 0) { + errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', '); + } + throw new Error(errorMsg); + } + + const data = await response.json(); + console.log("2FA setup response:", data); + console.log("Response headers:", Object.fromEntries(response.headers.entries())); + + // Proxy 응답에서 QR 데이터 추출 + setQrImage(data.qrImage); + setSecret(data.secret); + } catch (error: unknown) { + console.error("2FA setup error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("QR코드 발급 실패"); + } + } finally { + setIsSetupLoading(false); + } + }; + + if (open) { + fetchQR(); + } + }, [open]); + + const handleEnable2FA = async () => { + try { + setIsLoading(true); + setError(""); + + const tempToken = localStorage.getItem("tempToken"); + if (!tempToken) { + setError("임시 토큰이 없습니다"); + return; + } + + // Proxy를 통해 2FA enable 처리 + const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/enable`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': tempToken, + }, + body: JSON.stringify({ + otpCode: otp, + tempToken: tempToken, + secret: secret, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + let errorMsg = errorData.message || '2FA 등록 실패'; + if (Array.isArray(errorData.errors) && errorData.errors.length > 0) { + errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', '); + } + throw new Error(errorMsg); + } + + const data = await response.json(); + console.log("2FA enable response:", data); + console.log("Response headers:", Object.fromEntries(response.headers.entries())); + console.log("Response data keys:", Object.keys(data)); + + // 2FA 설정 완료 후에는 토큰을 저장하지 않음 (실제 로그인은 OTP에서 처리) + console.log("2FA setup completed successfully"); + + onSuccess(); + } catch (error: unknown) { + console.error("2FA enable error:", error); + if (error instanceof Error) { + setError(error.message); + } else { + setError("2FA 등록 실패"); + } + } finally { + setIsLoading(false); + } + }; + + const handleClose = () => { + onOpenChange(false); + onClose(); + }; + + return ( + + +
+

2FA 등록

+ + {isSetupLoading ? ( +
+
QR코드 로딩 중...
+
+ ) : ( + <> + {qrImage && ( +
+ QR코드 +
+ )} + +
+ setOtp(e.target.value)} + disabled={isLoading} + /> + + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ + )} +
+
+
+ ); +} diff --git a/apps/cm/src/pages/auth/login/ui/login-page.tsx b/apps/cm/src/pages/auth/login/ui/login-page.tsx new file mode 100644 index 000000000..251cd2819 --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/login-page.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Image from "next/image"; +import { images } from "@common/assets"; +import { LoginApi } from "@/entities/auth"; +import { ENDPOINT } from "@/shared/config"; +import OtpModal from "./OtpModal"; +import TwoFAModal from "./TwoFAModal"; +import "./login.css"; +import { useRouter } from "next/navigation"; + +function LoginPage() { + const [isOtpEnabled, setIsOtpEnabled] = useState(false); + const [showOtpModal, setShowOtpModal] = useState(false); + const [showTwoFAModal, setShowTwoFAModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + + // 모달 상태 디버깅 + useEffect(() => { + console.log("Modal states - OTP:", showOtpModal, "2FA:", showTwoFAModal); + }, [showOtpModal, showTwoFAModal]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(""); + + try { + const formData = new FormData(e.target as HTMLFormElement); + const userId = formData.get("id") as string; + const password = formData.get("password") as string; + + console.log("Login attempt - OTP enabled:", isOtpEnabled); + console.log("User ID:", userId); + + if (isOtpEnabled) { + // 2FA 로그인 - Proxy를 통해 처리 (2FA 전용 엔드포인트 사용) + console.log("Starting 2FA login..."); + + const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN_2FA}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId, + password, + provider: 'password' + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + let errorMsg = errorData.message || '로그인 실패'; + if (Array.isArray(errorData.errors) && errorData.errors.length > 0) { + errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', '); + } + throw new Error(errorMsg); + } + + const data = await response.json(); + console.log("2FA login response:", data); + console.log("Response headers:", Object.fromEntries(response.headers.entries())); + + // 2FA 로그인 응답에는 tempToken과 registered2FA만 포함됨 + if (!data.tempToken) { + throw new Error("Missing tempToken in 2FA login response"); + } + + // 임시 토큰을 localStorage에 저장 (OTP 인증용) + localStorage.setItem('tempToken', data.tempToken); + console.log("tempToken saved:", data.tempToken); + + if (data.registered2FA) { + console.log("User has 2FA registered, showing OTP modal"); + setShowOtpModal(true); + } else { + console.log("User doesn't have 2FA registered, showing 2FA setup modal"); + setShowTwoFAModal(true); + } + } else { + // 기본 로그인 - proxy를 통해 처리 + console.log("Starting basic login..."); + await LoginApi.login({ userId, password }); + window.location.href = "/dashboard"; + } + } catch (error: unknown) { + console.error("Login error:", error); + + // 에러 처리 + if (error instanceof Error) { + setError(error.message); + } else { + setError("로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요."); + } + } finally { + setIsLoading(false); + } + }; + + const handleOtpSuccess = () => { + console.log("OTP authentication successful, redirecting to dashboard"); + localStorage.removeItem("tempToken"); // 임시 토큰 제거 + router.push("/dashboard"); + }; + + const handleTwoFASuccess = () => { + console.log("2FA setup successful, redirecting to dashboard"); + localStorage.removeItem("tempToken"); // 임시 토큰 제거 + router.push("/dashboard"); + }; + + const handleOtpClose = () => { + console.log("OTP modal closed"); + setShowOtpModal(false); + localStorage.removeItem("tempToken"); + }; + + const handleTwoFAClose = () => { + console.log("2FA modal closed"); + setShowTwoFAModal(false); + localStorage.removeItem("tempToken"); + }; + + return ( + <> +
+
+
+

+ 유역하수도 통합플랫폼 +
+ 통합관리시스템 +

+

유역하수도 통합플랫폼에 오신 것을 환영합니다.

+
+
+ logo +
+
+
+
+
    +
  • +

    ID

    +
    + + +
    +
  • +
  • +

    PASSWORD

    +
    + + +
    +
  • +
+
+
+ + +
+
+ setIsOtpEnabled(e.target.checked)} + disabled={isLoading} + /> + +
+
+ + {error && ( +
+ {error} +
+ )} +
+
+

copyright © 2023 KECO. All rights reserved.

+
+ +

+ + 주소 : (22689) 인천광역시 서구 환경로 42(경서동 + 종합환경연구단지) + + + 전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723, + 3722 + + 팩스 :032-590-3729 +

+
+
+ +
+ + {/* OTP 인증 모달 */} + + + {/* 2FA 등록 모달 */} + + + ); +} + +export { LoginPage }; diff --git a/apps/cm/src/pages/auth/login/ui/login.css b/apps/cm/src/pages/auth/login/ui/login.css new file mode 100644 index 000000000..831cc13a9 --- /dev/null +++ b/apps/cm/src/pages/auth/login/ui/login.css @@ -0,0 +1,311 @@ +/** +* OLYM Cascading Style Sheets +* File Name : login.css +* Description : 로그인 스타일시트 +* Author : Web business Team / Choi Hyun Mi +* Email : desingtj@olym.co.kr +* Date : 2023.07.20 +* Update : 2023.07.26 +* Copyright(c) 2023 OLYM Communications. All Rights Reserved. +*/ +html, +body { + background: #fff; +} +.fx { + display: flex; + -ms-display: flex; + -webkit-display: flex; + box-sizing: border-box; +} +.login { + width: 100%; + height: 100vh; +} +.login .left_box { + width: 100%; + background: url(../../../../../public/images/bg_login.jpg) no-repeat + center/cover; + flex-direction: column; + justify-content: space-between; + padding: 7% 5% 5% 5%; +} +.login .left_box .text h1 { + font-size: 53px; + color: #fff; +} +.login .left_box .text h1 span { + font-weight: 800; +} +.login .left_box .text p { + margin-top: 40px; + font-size: 18px; + color: #fff; +} +.login .login_box { + width: 100%; + flex-direction: column; + justify-content: center; + padding: 4% 12% 4% 6%; +} +.login .login_box > ul { + flex-direction: column; + gap: 40px; +} +.login .login_box > ul > li h2 { + font-size: 22px; + font-weight: 600; + color: #2640a5; + margin-bottom: 14px; +} +.login .login_box > ul > li .input_st { + position: relative; +} +.login .login_box > ul > li .input_st .diveicon { + position: absolute; + top: 50%; + left: 28px; + transform: translateY(-50%); + font-family: "Xeicon"; + font-size: 25px; + color: #003288; +} +.login .login_box > ul > li input { + width: 100%; + padding: 20px 20px 20px 70px; + background: #f7f7f7; + box-sizing: border-box; + border: none; + border-radius: 5px; + font-size: 18px; + font-weight: 700; + color: #000; + height: auto; +} +.login .login_box > ul > li input:focus { + outline: 1px solid #3c5fe5; +} +.login .login_box > ul > li input::placeholder { + color: #afabab; + font-weight: 500; +} +.login .login_box .bottom_box { + justify-content: space-between; + gap: 20px; + padding: 50px 0 45px 0; +} +.login .login_box .bottom_box .checks { + display: flex; + align-items: center; + gap: 8px; +} +.login .login_box .bottom_box .checks input[type="checkbox"] { + width: 18px; + height: 18px; +} +.login .login_box .bottom_box .checks label { + font-size: 14px; +} +.login .login_box .bottom_box .pw_message { + align-items: center; + gap: 5px; + color: #636363; + font-weight: 500; + border-bottom: 1px solid #ccc; + padding-bottom: 5px; +} +.login .login_box .bottom_box .pw_message i { + color: #327af5; + font-size: 20px; +} +.login .login_box .login_btn { + position: relative; + border-radius: 5px; + padding: 24px 15px; + background: linear-gradient( + to left, + #1d3969 0%, + #264078 10%, + #172558 40%, + #162254 50%, + #111c43 60%, + #111c42 100% + ); + overflow: hidden; + justify-content: center; + align-items: center; + gap: 10px; + color: #fff; + cursor: pointer; +} +.login .login_box .login_btn span { + font-family: "Montserrat"; + font-size: 19px; + font-weight: 700; + position: relative; +} +.login .login_box .login_btn i { + font-size: 22px; + transform: translateY(-2px); +} +.login .login_box .login_btn:before { + display: block; + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + background: linear-gradient( + to right, + #1d3969 0%, + #264078 10%, + #172558 40%, + #162254 50%, + #111c43 60%, + #111c42 100% + ); + transition: opacity 0.5s; +} +.login .login_box .login_btn:hover::before { + opacity: 1; +} +.login .footer .copy { + margin-top: 36px; + justify-content: space-between; + align-items: center; + gap: 3%; +} +.login .footer .copy p { + font-size: 13px; + color: #d4d4d4; + width: 44%; + text-align: center; +} +.login .footer .copy::before, +.login .login_box .copy::after { + content: ""; + width: 20%; + height: 1px; + background: #d4d4d4; +} +.login .footer .fot_btn { + gap: 20px; + margin: 30px 0 10px 0; +} +.login .footer .fot_btn a { + color: #333; + font-weight: 700; + font-size: 13px; +} +.login .footer .fot_btn a:hover { + color: #003288; +} +.login .footer .addr { + color: #666; + font-size: 13px; + gap: 3px 20px; + flex-wrap: wrap; +} +/* 비밀번호 변경 팝업창 */ +.login .modal .modal_container { + width: 750px; +} +.login .pw_desc { + padding: 10px 10px 30px 10px; + color: #666; +} +.login .pw_desc p { + font-size: 1.25rem; + color: #000; + font-weight: 700; + margin-bottom: 10px; +} +.login .pw_desc span { + color: #f60; + font-weight: 700; +} +.login .modal .form_write { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 20px 50px; +} +@media (max-width: 1024px) { + .login { + flex-direction: column; + height: auto; + } + .login .left_box { + padding: 30px; + flex-direction: column-reverse; + gap: 20px; + } + .login .left_box .text h1 { + font-size: 30px; + line-height: 1.2em; + } + .login .left_box .text p { + margin-top: 20px; + font-size: 14px; + } + .login .left_box .img img { + height: 40px; + } + .login .login_box { + padding: 30px; + } + .login .login_box > ul { + gap: 20px; + } + .login .login_box > ul > li h2 { + margin-bottom: 7px; + } + .login .login_box > ul > li .input_st::after { + left: 15px; + font-size: 20px; + } + .login .login_box > ul > li input { + padding: 15px 10px 15px 50px; + font-size: 16px; + } + .login .login_box .bottom_box { + flex-direction: column; + align-items: flex-start; + padding: 30px 0; + } + .login .login_box .bottom_box .id_save label { + font-size: 14px; + } + .login .login_box .login_btn { + padding: 15px; + } + .login .login_box .login_btn span { + font-size: 16px; + } + .login .login_box .login_btn i { + font-size: 18px; + transform: translateY(-1px); + } + .login .login_box .copy p { + width: 100%; + } + .login .login_box .copy::before, + .login .login_box .copy::after { + display: none; + } + .login .footer .addr { + font-size: 11px; + gap: 3px 10px; + } + .login .modal .modal_container { + width: 90%; + } + .login .pw_desc { + padding: 0 0 20px 0; + } + .login .modal .form_write { + padding: 10px; + } +} +/* // login */ diff --git a/apps/cm/src/pages/dashboard/index.ts b/apps/cm/src/pages/dashboard/index.ts new file mode 100644 index 000000000..5b787c2e2 --- /dev/null +++ b/apps/cm/src/pages/dashboard/index.ts @@ -0,0 +1 @@ +export { DashboardPage } from "./ui/dashboard-page"; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx new file mode 100644 index 000000000..3b59d4a23 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -0,0 +1,94 @@ +"use client"; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { tokenManager } from '@/entities/auth/lib/token-manager'; +import { client } from '@/shared/api'; +import { ENDPOINT } from '@/shared/config'; + +interface UserInfo { + email?: string; + userId?: string; + userName?: string; + twoFactorEnabled?: boolean; +} + +function DashboardPage() { + const [userInfo, setUserInfo] = useState(null); + const [error, setError] = useState(''); + const router = useRouter(); + + useEffect(() => { + const fetchUserInfo = async () => { + console.log('fetchUserInfo called'); + + try { + // tokenManager를 통해 토큰 확인 + const accessToken = tokenManager.getToken('Authorization'); + const userId = tokenManager.getToken('token-id'); + + if (!accessToken || !userId) { + console.log('No tokens found, redirecting to login'); + setError('로그인이 필요합니다.'); + router.push('/login'); + return; + } + + console.log('Fetching user info with userId:', userId); + + // client를 사용하여 API 호출 (자동 토큰 처리) + const response = await client.get(`${ENDPOINT.USER_SERVICE.USERS}/${userId}`); + + if (response.data) { + console.log('User info received:', response.data); + setUserInfo(response.data); + } else { + setError('사용자 정보를 불러올 수 없습니다.'); + } + } catch (error) { + console.error('Error fetching user info:', error); + if (error instanceof Error && error.message.includes('401')) { + setError('인증이 만료되었습니다. 다시 로그인해주세요.'); + tokenManager.clearAuthToken(); + router.push('/login'); + } else { + setError('네트워크 오류가 발생했습니다.'); + } + } + }; + + fetchUserInfo(); + }, [router]); + + const handleLogout = () => { + console.log('Logging out...'); + // localStorage 정리 + localStorage.removeItem('tempToken'); + // tokenManager를 통해 쿠키 정리 + tokenManager.clearAuthToken(); + router.push('/login'); + }; + + const handleChangePassword = () => { + router.push('/otp?next=change-password'); + }; + + return ( +
+

대시보드

+ {error &&
{error}
} + {userInfo ? ( +
+
이메일: {userInfo.email || userInfo.userId}
+
이름: {userInfo.userName || '-'}
+
2FA 활성화: {userInfo.twoFactorEnabled ? '예' : '아니오'}
+
+ ) : ( + !error &&
사용자 정보를 불러오는 중...
+ )} + + +
+ ); +} + +export { DashboardPage }; \ No newline at end of file diff --git a/apps/cm/src/shared/api/client.ts b/apps/cm/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/cm/src/shared/api/client.ts @@ -0,0 +1,11 @@ +import { Client } from "@common/network"; +import { BASE_URL } from "../config/api"; +import { SITE_ID } from "../config/env"; +import { setCookie } from "../lib"; + +setCookie("site-id-key", SITE_ID); + +export const client = Client.getInstance({ + baseURL: BASE_URL, + siteId: SITE_ID, +}); diff --git a/apps/cm/src/shared/api/index.ts b/apps/cm/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/cm/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts new file mode 100644 index 000000000..d6065bfc7 --- /dev/null +++ b/apps/cm/src/shared/config/api.ts @@ -0,0 +1,73 @@ +import { PROXY_HOST } from "./env"; + +export const BASE_URL = `${PROXY_HOST}/server`; + +export const ENDPOINT = { + ATTACH_SERVICE: { + TUS_UPLOAD: "/attach-service/api/v1/tus/upload", + EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", + IMAGES: "/attach-service/api/v1/images", + MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", + ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", + }, + USER_SERVICE: { + LOGIN: "/user-service/login", + LOGIN_2FA: "/user-service/api/v1/auth/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + ROLES: "/user-service/api/v1/roles", + LOGINLOG: "/user-service/api/v1/login-logs", + PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs", + OTP_VERIFY: "/user-service/api/v1/auth/2fa/verify", + TWOFA_SETUP: "/user-service/api/v1/auth/2fa/setup", + TWOFA_ENABLE: "/user-service/api/v1/auth/2fa/enable", + }, + CMS_SERVICE: { + DASHBOARD: "/cms-service/api/v1/dashboard", + SITES: "/cms-service/api/v1/sites", + IPS: "/cms-service/api/v1/site-ips", + DOMAINS: "/cms-service/api/v1/site-dmns", + POSTINGS: "/cms-service/api/v1/site-pstgs", + CODE: "/cms-service/api/v1/code/cache", + MENUS: "/cms-service/api/v1/menus", + MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts", + MENU_TEMPORARY_STORAGE: "/cms-service/api/v1/menu-tmpr-strg", + MENU_HISTORY: "/cms-service/api/v1/menu-hstry", + 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", + TEMPLATE: "/cms-service/api/v1/tmplt", + TERMS: "/cms-service/api/v1/trms", + LWPO_CODE: "/cms-service/api/v1/lwpo-cds", + UP_CODE: "/cms-service/api/v1/up-codes", + POPUPS: "/cms-service/api/v1/popups", + SCHEDULES: "/cms-service/api/v1/schdls", + SURVEY: "/cms-service/api/v1/srvy", + SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem", + SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl", + ZIPS: "/cms-service/api/v1/zips", + GROUPS: "/cms-service/api/v1/groups", + GROUP_USERS: "/cms-service/api/v1/group-users", + MANAGER_IP: "/cms-service/api/v1/mgrip", + APILOG: "/cms-service/api/v1/api-logs", + SITE_AUTHORITY: "/cms-service/api/v1/site-authrt", + CONNECTION_LOG: "/cms-service/api/v1/cntnlog", + }, + BOARD_SERVICE: { + BBS: "/board-service/api/v1/bbs", + BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", + POSTS: "/board-service/api/v1/posts", + POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", + POST_EVALUATION: "/board-service/api/v1/pstEvl", + }, +}; + +export const JWT_EXPIRATION_TIME = 1800000; diff --git a/apps/cm/src/shared/config/env.ts b/apps/cm/src/shared/config/env.ts new file mode 100644 index 000000000..c6373724f --- /dev/null +++ b/apps/cm/src/shared/config/env.ts @@ -0,0 +1,11 @@ +export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; +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"; +export const REFRESH_TOKEN = + process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; diff --git a/apps/cm/src/shared/config/index.ts b/apps/cm/src/shared/config/index.ts new file mode 100644 index 000000000..cd83447d5 --- /dev/null +++ b/apps/cm/src/shared/config/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./env"; diff --git a/apps/cm/src/shared/lib/cookie.ts b/apps/cm/src/shared/lib/cookie.ts new file mode 100644 index 000000000..8f5d0011b --- /dev/null +++ b/apps/cm/src/shared/lib/cookie.ts @@ -0,0 +1,29 @@ +import Cookies from "js-cookie"; + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +} diff --git a/apps/cm/src/shared/lib/index.ts b/apps/cm/src/shared/lib/index.ts new file mode 100644 index 000000000..5ac79bf08 --- /dev/null +++ b/apps/cm/src/shared/lib/index.ts @@ -0,0 +1,8 @@ +export { getCookie, setCookie, removeCookie } from "./cookie"; +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useDataTable } from "./use-data-table"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { useSelectPrompt } from "./use-select-prompt"; diff --git a/apps/cm/src/shared/lib/match-query-status.ts b/apps/cm/src/shared/lib/match-query-status.ts new file mode 100644 index 000000000..0e5f45829 --- /dev/null +++ b/apps/cm/src/shared/lib/match-query-status.ts @@ -0,0 +1,65 @@ +import { type UseQueryResult } from "@tanstack/react-query"; +import type { PageableList } from "../model"; + +/** + * Match the state of a query to a set of components. + * + * Useful for rendering different UI based on the state of a query. + */ +export function matchQueryStatus( + query: UseQueryResult>, + options: { + Loading?: JSX.Element; + Errored?: JSX.Element | ((error: unknown) => JSX.Element); + Empty?: JSX.Element; + Success: ( + data: UseQueryResult> & { + data: NonNullable["data"]>; + }, + ) => JSX.Element; + }, +): JSX.Element; +export function matchQueryStatus( + query: UseQueryResult>, + options: { + Loading?: JSX.Element; + Errored?: JSX.Element | ((error: unknown) => JSX.Element); + Success: (data: UseQueryResult>) => JSX.Element; + }, +): JSX.Element; +export function matchQueryStatus( + query: UseQueryResult>, + { + Loading, + Errored, + Empty, + Success, + }: { + Loading?: JSX.Element; + Errored?: JSX.Element | ((error: unknown) => JSX.Element); + Empty?: JSX.Element; + Success: (data: UseQueryResult>) => JSX.Element; + }, +): JSX.Element { + if (query.isLoading && Loading) { + return Loading; + } + + if (query.isError && Errored) { + if (typeof Errored === "function") { + return Errored(query.error); + } + return Errored; + } + + const isEmpty = + query.data?.content === undefined || + query.data?.content === null || + (Array.isArray(query.data?.content) && query.data?.content.length === 0); + + if (isEmpty && Empty) { + return Empty; + } + + return Success(query); +} diff --git a/apps/cm/src/shared/lib/use-alert-store.ts b/apps/cm/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/cm/src/shared/lib/use-alert-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AlertStore } from "../store/alert-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAlertStore = (selector: (store: AlertStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useAlertStore must be used within StoreProvider"); + } + + return useStore(storeContext.alertStore, selector); +}; diff --git a/apps/cm/src/shared/lib/use-attachment-store.ts b/apps/cm/src/shared/lib/use-attachment-store.ts new file mode 100644 index 000000000..285699cc6 --- /dev/null +++ b/apps/cm/src/shared/lib/use-attachment-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { AttachmentStore } from "../store/attachment-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useAttachmentStore = ( + selector: (store: AttachmentStore) => T, +): T => { + const store = useContext(StoreContext); + if (!store) { + throw new Error("useAttachmentStore must be used within StoreProvider"); + } + return useStore(store.attachmentStore, selector); +}; diff --git a/apps/cm/src/shared/lib/use-data-table.tsx b/apps/cm/src/shared/lib/use-data-table.tsx new file mode 100644 index 000000000..51638f746 --- /dev/null +++ b/apps/cm/src/shared/lib/use-data-table.tsx @@ -0,0 +1,42 @@ +import type { HTMLAttributes } from "react"; +import type { UseQueryResult } from "@tanstack/react-query"; +import { useReactTable, type TableOptions } from "@tanstack/react-table"; +import { DataTable } from "@common/components"; +import { NoData } from "../ui/no-data"; +import type { PageableList } from "../model"; +import { matchQueryStatus } from "./match-query-status"; + +type UseDataTableOptions = { + query: UseQueryResult>; + tableOptions: Omit, "data" | "rowCount">; + dataTableOptions?: DataTableOptions; +}; + +type DataTableOptions = { + variant?: "default" | "secondary" | "line"; + className?: HTMLAttributes["className"]; + viewPagination?: boolean; + onRowClick?: (row: T) => void; +}; + +export const useDataTable = ({ + query, + tableOptions, + dataTableOptions, +}: UseDataTableOptions) => { + const table = useReactTable({ + ...tableOptions, + data: query.data?.content ?? [], + rowCount: query.data?.totalElements ?? 0, + }); + + return { + tableComponent: matchQueryStatus(query, { + Empty: , + Success: () => { + return ; + }, + }), + tableInstance: table, + }; +}; diff --git a/apps/cm/src/shared/lib/use-menu-store.ts b/apps/cm/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/cm/src/shared/lib/use-menu-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { MenuStore } from "../store/menu-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useMenuStore = (selector: (store: MenuStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useMenuStore must be used within StoreProvider"); + } + + return useStore(storeContext.menuStore, selector); +}; diff --git a/apps/cm/src/shared/lib/use-mutation-wrapper.tsx b/apps/cm/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..cea1a8f83 --- /dev/null +++ b/apps/cm/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,29 @@ +import { AxiosError } from "axios"; +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { useAlertStore } from "./use-alert-store"; + +export const useMutationWrapper = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>( + mutationOptions: UseMutationOptions, +) => { + const alert = useAlertStore((state) => state.setMessage); + return useMutation({ + ...mutationOptions, + onError: (error) => { + let errorMessage = ""; + + if (error instanceof AxiosError) { + if (error.response?.data?.message) + errorMessage += `${error.response.data.message}\n`; + if (error.response?.data?.errors[0]?.message) + errorMessage += `${error.response.data.errors[0].message}\n`; + } + + alert(`${errorMessage}`); + }, + }); +}; diff --git a/apps/cm/src/shared/lib/use-prompt.tsx b/apps/cm/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/cm/src/shared/lib/use-prompt.tsx @@ -0,0 +1,26 @@ +import { useContext } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +export const usePrompt = () => { + const { message, resolve, setMessage, setResolve } = + useContext(PromptContext); + + const prompt = (message: string) => { + setMessage(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setMessage(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setMessage(""); + }; + + return { message, isOpen: !!message, prompt, onSubmit, onCancel }; +}; diff --git a/apps/cm/src/shared/lib/use-select-prompt.tsx b/apps/cm/src/shared/lib/use-select-prompt.tsx new file mode 100644 index 000000000..357be7a36 --- /dev/null +++ b/apps/cm/src/shared/lib/use-select-prompt.tsx @@ -0,0 +1,48 @@ +import { useContext, useEffect } from "react"; +import { PromptContext } from "../providers/prompt-provider"; + +type SelectData = { + label: string; + value: string; +}; + +export const useSelectPrompt = (data?: SelectData[]) => { + const { + selectData, + selectionGuide, + resolve, + setResolve, + setSelectData, + setSelectionGuide, + } = useContext(PromptContext); + + const prompt = (message: string) => { + setSelectionGuide(message); + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { + setResolve(() => resolve); + }); + }; + + const onSubmit = (value: string) => { + resolve?.({ resolved: true, value }); + setSelectionGuide(""); + }; + + const onCancel = () => { + resolve?.({ resolved: false }); + setSelectionGuide(""); + }; + + useEffect(() => { + if (data) setSelectData(data); + }, [data]); + + return { + selectData, + selectionGuide, + isOpen: !!selectionGuide, + prompt, + onSubmit, + onCancel, + }; +}; diff --git a/apps/cm/src/shared/lib/use-tus.ts b/apps/cm/src/shared/lib/use-tus.ts new file mode 100644 index 000000000..9d4bc7c00 --- /dev/null +++ b/apps/cm/src/shared/lib/use-tus.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { type HttpResponse, Upload } from "tus-js-client"; +import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "../config"; +import { useAttachmentStore } from "./use-attachment-store"; +import { useAlertStore } from "./use-alert-store"; +import { getCookie } from "./cookie"; + +type TusProps = { + accept?: string; +}; + +export const useTus = ({ accept }: TusProps = {}) => { + const alert = useAlertStore((state) => state.setMessage); + const { attachmentFileCode, setAttachmentFileCode } = useAttachmentStore( + (state) => state, + ); + + const [file, setFile] = useState(null); + const [upload, setUpload] = useState(null); + const [response, setResponse] = useState(null); + const [progress, setProgress] = useState(0); + const [isValid, setIsValid] = useState(false); + + const metadata = { + filename: file?.name ?? "", + filetype: file?.type ?? "", + atflCd: attachmentFileCode, + }; + + const validFileType = (file: File) => { + const allowedExtensions = accept?.split(",") ?? []; + const fileName = file.name; + const fileExtension = fileName + .substring(fileName.lastIndexOf(".")) + .toLowerCase(); + + if (!allowedExtensions.includes(fileExtension)) { + alert("파일 형식이 올바르지 않습니다."); + setIsValid(false); + return false; + } + + setIsValid(true); + return true; + }; + + const createUpload = () => { + return new Upload(file!, { + endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, + chunkSize: 10 * 1024 * 1024, + headers: { + [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", + [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", + }, + metadata, + onError(error) { + throw new Error(`Failed because: ${error}`); + }, + onProgress(bytesUploaded, bytesTotal) { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + setProgress(parseInt(percentage)); + }, + onAfterResponse(req, res) { + const fileInfo = res.getHeader("X-File-Info"); + if (fileInfo === undefined || fileInfo === null) return; + + setAttachmentFileCode(JSON.parse(fileInfo)?.atflCd ?? ""); + setResponse(res); + }, + }); + }; + + useEffect(() => { + if (!file) return; + if (!validFileType(file)) return; + + const upload = createUpload(); + upload.start(); + setUpload(upload); + }, [file]); + + return { + file, + upload, + response, + progress, + isValid, + setFile, + }; +}; diff --git a/apps/cm/src/shared/lib/use-user-store.ts b/apps/cm/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/cm/src/shared/lib/use-user-store.ts @@ -0,0 +1,14 @@ +import { useStore } from "zustand"; +import { useContext } from "react"; +import type { UserStore } from "../store/user-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useUserStore = (selector: (store: UserStore) => T): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useUserStore must be used within StoreProvider"); + } + + return useStore(storeContext.userStore, selector); +}; diff --git a/apps/cm/src/shared/model/api-interface.ts b/apps/cm/src/shared/model/api-interface.ts new file mode 100644 index 000000000..8218bfb0d --- /dev/null +++ b/apps/cm/src/shared/model/api-interface.ts @@ -0,0 +1,34 @@ +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +export interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +export interface PageableParams { + page?: number; + size?: number; + sort?: string; +} diff --git a/apps/cm/src/shared/model/index.ts b/apps/cm/src/shared/model/index.ts new file mode 100644 index 000000000..cf589d200 --- /dev/null +++ b/apps/cm/src/shared/model/index.ts @@ -0,0 +1 @@ +export * from "./api-interface"; diff --git a/apps/cm/src/shared/providers/index.ts b/apps/cm/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/cm/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/cm/src/shared/providers/prompt-provider.tsx b/apps/cm/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..71b86388c --- /dev/null +++ b/apps/cm/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,61 @@ +import { createContext, useState } from "react"; + +type SelectData = { + label: string; + value: string; +}; + +interface PromptContext { + message?: string; + selectData?: SelectData[]; + selectionGuide?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setSelectData: (selectData: SelectData[]) => void; + setSelectionGuide: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext({ + message: "", + selectData: [], + selectionGuide: "", + resolve() {}, + setMessage() {}, + setSelectData() {}, + setSelectionGuide() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [selectData, setSelectData] = useState([]); + const [selectionGuide, setSelectionGuide] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/cm/src/shared/providers/store-provider.tsx b/apps/cm/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/cm/src/shared/providers/store-provider.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { + createAlertStore, + createAttachmentStore, + createMenuStore, + createUserStore, +} from "../store"; + +type StoreContextType = { + alertStore: ReturnType; + attachmentStore: ReturnType; + menuStore: ReturnType; + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + alertStore: createAlertStore(), + attachmentStore: createAttachmentStore(), + menuStore: createMenuStore(), + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/cm/src/shared/store/alert-store.ts b/apps/cm/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/cm/src/shared/store/alert-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AlertState = { + message: string; + callback?: () => void; +}; + +export type AlertActions = { + setMessage: (message: string, callback?: () => void) => void; +}; + +export type AlertStore = AlertState & AlertActions; + +export const defaultInitState: AlertState = { + message: "", + callback: undefined, +}; + +export const createAlertStore = (initState: AlertState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setMessage: (message: string, callback?: () => void) => + set(() => ({ message, callback })), + })); +}; diff --git a/apps/cm/src/shared/store/attachment-store.ts b/apps/cm/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/cm/src/shared/store/attachment-store.ts @@ -0,0 +1,25 @@ +import { createStore } from "zustand/vanilla"; + +export type AttachmentState = { + attachmentFileCode: string; +}; + +export type AttachmentActions = { + setAttachmentFileCode: (attachmentFileCode: string) => void; +}; + +export type AttachmentStore = AttachmentState & AttachmentActions; + +export const defaultInitState: AttachmentState = { + attachmentFileCode: "", +}; + +export const createAttachmentStore = ( + initState: AttachmentState = defaultInitState, +) => { + return createStore()((set) => ({ + ...initState, + setAttachmentFileCode: (attachmentFileCode: string) => + set({ attachmentFileCode }), + })); +}; diff --git a/apps/cm/src/shared/store/index.ts b/apps/cm/src/shared/store/index.ts new file mode 100644 index 000000000..a41d6f928 --- /dev/null +++ b/apps/cm/src/shared/store/index.ts @@ -0,0 +1,4 @@ +export { createAlertStore } from "./alert-store"; +export { createAttachmentStore } from "./attachment-store"; +export { createMenuStore, type BbsType } from "./menu-store"; +export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/cm/src/shared/store/menu-store.ts b/apps/cm/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/cm/src/shared/store/menu-store.ts @@ -0,0 +1,93 @@ +import { createStore } from "zustand/vanilla"; + +export const BBS_TYPE = { + BBSNOTICE: "BBSNOTICE", + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE]; + +export interface IBbs { + bbsId: number; + bbsTypeCd: BbsType; + 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; +} + +interface IMenu { + menuCd: number; + menuKornNm: string; + menuEngNm: string; + parentId: number | null; + sortSeq: number; + iconNm?: string; + children: IMenu[] | null; + levelNo: number; + urlPath: string; + menuLnkgTypeCd: string; + menuLnkgSn: number; + menuPath: string; + qrcd: string; + useYn: string; + otptYn: string; + newWndwYn: string; + requiredUrlPath: boolean; +} + +interface IAuthority { + mngAuthrtYn: string; + inqAuthrtYn: string; + regAuthrtYn: string; + mdfcnAuthrtYn: string; + delAuthrtYn: string; +} + +export type MenuState = { + bbsInfo: IBbs | null; + currentMenuInfo: IMenu | null; + pageMenuAuthority: IAuthority | null; +}; + +export type MenuActions = { + setBbsInfo: (bbs: IBbs) => void; + setCurrentMenuInfo: (menu: IMenu) => void; + setPageMenuAuthority: (authority: IAuthority) => void; +}; + +export type MenuStore = MenuState & MenuActions; + +export const defaultInitState: MenuState = { + currentMenuInfo: null, + bbsInfo: null, + pageMenuAuthority: null, +}; + +export const createMenuStore = (initState: MenuState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), + setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), + setPageMenuAuthority: (authority: IAuthority) => + set({ pageMenuAuthority: authority }), + })); +}; diff --git a/apps/cm/src/shared/store/user-store.ts b/apps/cm/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/cm/src/shared/store/user-store.ts @@ -0,0 +1,57 @@ +import { createStore } from "zustand/vanilla"; + +export interface IUserInfo { + regDt: string; + mdfcnDt: string; + rgtr: string; + mdfr: string; + adminRoleYn: string; + userUnqId: string; + userId: string; + userName: string; + email: string; + encryptedPassword: string; + refreshToken: string; + userStateCode: string; + lastLoginDate: string; + loginFailCount: number; + pswdHnt: string; + pswdAns: string; + mphn: string; + telno: string; + zip: string; + addr: string; + daddr: string; + laeSeCd: string; + ogdpSeCd: string; + aprvYn: string; + autzrId: string; + expryYmd: string; + lastPswdChgDt: string; + googleId: string; + kakaoId: string; + naverId: string; + isSocialUser: boolean; + hasPassword: boolean; +} + +export type UserState = { + userInfo: IUserInfo | null; +}; + +export type UserActions = { + setUserInfo: (userInfo: IUserInfo | null) => void; +}; + +export type UserStore = UserState & UserActions; + +export const defaultInitState: UserState = { + userInfo: null, +}; + +export const createUserStore = (initState: UserState = defaultInitState) => { + return createStore()((set) => ({ + ...initState, + setUserInfo: (userInfo: IUserInfo | null) => set(() => ({ userInfo })), + })); +}; diff --git a/apps/cm/src/shared/ui/alert-dialog.tsx b/apps/cm/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/cm/src/shared/ui/alert-dialog.tsx @@ -0,0 +1,34 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, +} from "@common/components"; +import { useAlertStore } from "../lib/use-alert-store"; + +function AlertDialog() { + const { message, callback, setMessage } = useAlertStore((state) => state); + const handleOpenChange = () => { + setMessage(""); + callback?.(); + }; + return ( + + +
+

+ {message} +

+
+ + + + + +
+
+ ); +} + +export { AlertDialog }; diff --git a/apps/cm/src/shared/ui/confirm-dialog.tsx b/apps/cm/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/cm/src/shared/ui/confirm-dialog.tsx @@ -0,0 +1,48 @@ +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Button, + type ButtonProps, +} from "@common/components"; + +type ConfirmDialogProps = { + isOpen?: boolean; + confirmButtonProps?: ButtonProps & { label?: string }; + cancelButtonProps?: ButtonProps & { label?: string }; +}; + +function ConfirmDialog({ + isOpen, + confirmButtonProps, + cancelButtonProps, +}: ConfirmDialogProps) { + const { message, isOpen: isGlobalOpen, onConfirm, onCancel } = useConfirm(); + return ( + + +
+

+ {message} +

+
+ + + + + + + + +
+
+ ); +} + +export { ConfirmDialog }; diff --git a/apps/cm/src/shared/ui/form.tsx b/apps/cm/src/shared/ui/form.tsx new file mode 100644 index 000000000..bd579d263 --- /dev/null +++ b/apps/cm/src/shared/ui/form.tsx @@ -0,0 +1,175 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; +import { cn, Label } from "@common/components"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +