diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 3c390c8bd..c43f5af55 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -5,7 +5,7 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_PORT = 3005 #NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 -NEXT_PUBLIC_PROXY_HOST = http://localhost:3005/ +NEXT_PUBLIC_PROXY_HOST = http://localhost:3005 NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 diff --git a/apps/dm/.env.production b/apps/dm/.env.production index b9d81ccc4..c8a653bd1 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -1,9 +1,9 @@ NEXT_PUBLIC_SITE_ID = DM -NEXT_PUBLIC_PORT = 11035 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035 -NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = https://reuse.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl @@ -11,9 +11,9 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_REUSE_API_URL= http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api diff --git a/apps/dm/app/(popup-layout)/map/service/map.ts b/apps/dm/app/(popup-layout)/map/service/map.ts index aa98a0bc2..b527ac1c4 100644 --- a/apps/dm/app/(popup-layout)/map/service/map.ts +++ b/apps/dm/app/(popup-layout)/map/service/map.ts @@ -1,9 +1,8 @@ - /** * 관망도 API */ - const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map` - const REUSE_API_URL = `${process.env.NEXT_PUBLIC_REUSE_API_URL}` +import { DEV_URL_WR, DEV_URL_SN } from "@/components/ui/config"; +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || ""; export type LyrStngProps = { lyr_sn: string @@ -31,7 +30,7 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 getLyrStngs: async () => { - const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, { + const response = await fetch(`${PROXY_HOST}${DEV_URL_SN}/app/v1/map/lyrStngs/DM_NETWORK`, { method: 'GET', headers: { 'Accept': 'application/json', @@ -45,7 +44,7 @@ export const mapService = { return response.json(); }, getPbnstRuseFclts: async (ruseFcltPk:any) => { - const response = await fetch(`${REUSE_API_URL}/reuseFc/pubFc/${ruseFcltPk}`, { + const response = await fetch(`${PROXY_HOST}${DEV_URL_WR}/api/v1/reuseFc/pubFc/${ruseFcltPk}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/dm/app/store/map/index.tsx b/apps/dm/app/store/map/index.tsx index 5ed066f73..79bdd19e5 100644 --- a/apps/dm/app/store/map/index.tsx +++ b/apps/dm/app/store/map/index.tsx @@ -1,7 +1,13 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { fetchType, getFetch } from "@/components/ui/api"; import { DEV_URL_FO } from "@/components/ui/config"; +import { getCookie } from "@common/components/lib/cookie"; import type { FacilityInfo, SubsInfo } from "./type"; +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST; +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); + const checkWrRegList = async () => { const wrRegList = await getFetch(fetchType.WR, `/reuseFc/pubFc`); return wrRegList; @@ -22,10 +28,12 @@ const getRuseFclt = async ( url = url + `/${searchTxt}`; } - const response = await fetch(DEV_URL_FO + url, { + const response = await fetch(PROXY_HOST + DEV_URL_FO + url, { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(ruseList), }); diff --git a/apps/dm/components/ui/api/index.ts b/apps/dm/components/ui/api/index.ts index 28644b00e..c8f2cc9e7 100644 --- a/apps/dm/components/ui/api/index.ts +++ b/apps/dm/components/ui/api/index.ts @@ -1,5 +1,11 @@ //8880 +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { DEV_URL_FO, DEV_URL_WR } from "../config"; +import { getCookie } from "@common/components/lib/cookie"; + +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST; +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); export const fetchType = { COMMON: "common", @@ -13,10 +19,13 @@ interface RequestParams { export const getFetch = async (type: string, url: string) => { const baseUrl = type === fetchType.FO ? DEV_URL_FO : DEV_URL_WR; - const response = await fetch(baseUrl + url, { + console.log("호출주소 ", PROXY_HOST+baseUrl + url); + const response = await fetch(PROXY_HOST+baseUrl + url, { method: "GET", headers: { accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -28,10 +37,12 @@ export const getFetch = async (type: string, url: string) => { }; export const postFetch = async (url: string, params: RequestParams) => { - const response = await fetch(DEV_URL_WR + url, { + const response = await fetch(PROXY_HOST+DEV_URL_WR + url, { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -43,10 +54,12 @@ export const postFetch = async (url: string, params: RequestParams) => { }; export const putFetch = async (url: string, params: RequestParams) => { - const response = await fetch(DEV_URL_WR + url, { + const response = await fetch(PROXY_HOST+DEV_URL_WR + url, { method: "PUT", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -58,10 +71,12 @@ export const putFetch = async (url: string, params: RequestParams) => { }; export const deleteFetch = async (url: string, params: RequestParams) => { - const response = await fetch(DEV_URL_WR + url, { + const response = await fetch(PROXY_HOST+DEV_URL_WR + url, { method: "DELETE", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); diff --git a/apps/dm/components/ui/config/index.ts b/apps/dm/components/ui/config/index.ts index 2ed4ef03d..1585bb767 100644 --- a/apps/dm/components/ui/config/index.ts +++ b/apps/dm/components/ui/config/index.ts @@ -1,5 +1,6 @@ -import { PROXY_HOST, SERVER_API_FO_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; +import { PROXY_HOST } from "@/src/shared/config/env"; export const LOCAL_URL = PROXY_HOST; -export const DEV_URL_FO = SERVER_API_FO_URL; -export const DEV_URL_WR = SERVER_API_WR_URL; +export const DEV_URL_FO = "/operation-service"; +export const DEV_URL_WR = "/reuse-service"; +export const DEV_URL_SN = "/sewerage-service"; diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts index 4ac2e3d99..8616a6e80 100644 --- a/apps/dm/middleware.ts +++ b/apps/dm/middleware.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; +import { SERVER_API_FO_URL, SERVER_API_WR_URL } from "./src/shared/config/env"; // const CLAIM_NAME = "Authorization"; // const AUTH_USER_ID = "token-id"; @@ -115,12 +116,30 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname.startsWith("/sewerage-service")) { + if (pathname.startsWith("/reuse-service")) { const queryParams = searchParams.toString(); - const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + console.log(`[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/operation-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/sewerage-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/sewerage-service' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } if (pathname.startsWith("/bussiness-juso-service")) { @@ -150,6 +169,8 @@ export const config = { "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", + "/operation-service/:path*", + "/reuse-service/:path*", "/sewerage-service/:path*", "/bussiness-juso-service/:path*", "/", diff --git a/apps/fo/.env.local b/apps/fo/.env.local index 2e937af0c..a87d13587 100644 --- a/apps/fo/.env.local +++ b/apps/fo/.env.local @@ -1,18 +1,21 @@ NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_CLAIM_NAME = Authorization +NEXT_PUBLIC_AUTH_USER_ID = token-id +NEXT_PUBLIC_ACCESS_TOKEN = access-token +NEXT_PUBLIC_REFRESH_TOKEN = refresh-token +NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login +NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3001/auth/login NEXT_PUBLIC_PORT = 3000 NEXT_PUBLIC_PROXY_HOST = http://localhost:3000 -NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082 #NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 #NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver -NEXT_PUBLIC_CLAIM_NAME = Authorization -NEXT_PUBLIC_AUTH_USER_ID = token-id -NEXT_PUBLIC_ACCESS_TOKEN = access-token -NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API - - +NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service +REACT_EDITOR=atom \ No newline at end of file diff --git a/apps/fo/.env.production b/apps/fo/.env.production index a1da344b8..7d9b84a8f 100644 --- a/apps/fo/.env.production +++ b/apps/fo/.env.production @@ -15,11 +15,11 @@ NEXT_PUBLIC_SITE_ID = FO NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = https://fo.hasuon.or.kr -# NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_SERVER_API_URL = https://fo.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = https://apigateway.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_PROXY_HOST = https://larva.hasuon.or.kr +# NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl @@ -27,6 +27,6 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_WR_SERVER_API_URL = https://fo.hasuon.or.kr/reuse-service/api/v1 - -NEXT_PUBLIC_SEWERAGE_API_URL = https://fo.hasuon.or.kr/sewerage-service/api \ No newline at end of file +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service \ No newline at end of file diff --git a/apps/fo/app/base.tsx b/apps/fo/app/base.tsx deleted file mode 100644 index a01ce8643..000000000 --- a/apps/fo/app/base.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { BaseLayout } from "@common/components"; -import type { MenuItem } from "./store/common"; -import { useFoStore } from "./store"; -import { usePathname, useRouter } from "next/navigation"; - -export default function Base({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const { getMenuAuth } = useFoStore((state) => state); - const pathname = usePathname(); - const router = useRouter(); - - const handleClick = (id: MenuItem) => { - getMenuAuth(id.menuCd); - router.push(id.urlPath); - }; - - return ( - - {children} - - ); -} diff --git a/apps/fo/app/components/api/config/env.ts b/apps/fo/app/components/api/config/env.ts index d419bfd1c..1c0ba8b0e 100644 --- a/apps/fo/app/components/api/config/env.ts +++ b/apps/fo/app/components/api/config/env.ts @@ -1,5 +1,5 @@ -import { PROXY_HOST, SERVER_API_URL, SERVER_API_WR_URL } from "@/src/shared/config/env"; +import { PROXY_HOST } from "@/src/shared/config/env"; -export const DEV_URL_FO = SERVER_API_URL; +export const DEV_URL_FO = "/operation-service/api/v1"; export const LOCAL_URL = PROXY_HOST; -export const DEV_URL_WR = SERVER_API_WR_URL; \ No newline at end of file +export const DEV_URL_WR = "/reuse-service/api/v1"; \ No newline at end of file diff --git a/apps/fo/app/components/api/index.ts b/apps/fo/app/components/api/index.ts index 3bba85af2..ba4e9863e 100644 --- a/apps/fo/app/components/api/index.ts +++ b/apps/fo/app/components/api/index.ts @@ -1,12 +1,22 @@ -import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; +import { getCookie } from "@common/components/lib/cookie"; + +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || ""; + +const tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); export const getFetch = async (part : string, url: string) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : part === 'local' ? LOCAL_URL+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl , { method: "GET", headers: { - accept: "application/json", + accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", } }); if(!response.ok) { @@ -17,10 +27,12 @@ export const getFetch = async (part : string, url: string) => { } export const getFetchAreaList = async (url: string) => { - const response = await fetch(url , { + const response = await fetch(PROXY_HOST+url , { method: "GET", headers: { - accept: "application/json", + accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", } }); if(!response.ok) { @@ -33,10 +45,13 @@ export const getFetchAreaList = async (url: string) => { export const postFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl, { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -49,10 +64,13 @@ export const postFetch = async (part : string, url: string, params: unknown) => export const putFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; + reqUrl = PROXY_HOST + reqUrl; const response = await fetch(reqUrl, { method: "PUT", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -65,10 +83,13 @@ export const putFetch = async (part : string, url: string, params: unknown) => { export const deleteFetch = async (part : string, url: string, params: unknown) => { let reqUrl = ""; reqUrl = part === "fo" ? DEV_URL_FO+url : DEV_URL_WR+url; - const response = await fetch(reqUrl, { + reqUrl = PROXY_HOST + reqUrl; + const response = await fetch(reqUrl, { method: "DELETE", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); diff --git a/apps/fo/app/layout.tsx b/apps/fo/app/layout.tsx index 842ec5ed1..683e6d0ec 100644 --- a/apps/fo/app/layout.tsx +++ b/apps/fo/app/layout.tsx @@ -1,29 +1,14 @@ -"use client" -import "./globals.css"; -import { StoreProvider } from "./store"; -import { LoginApi } from "@/src/entities/auth"; -import { useEffect } from "react"; +import type { Metadata } from "next"; +import { App } from "@/src/app"; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export const metadata: Metadata = { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다", + keywords: ["하수관로"], + openGraph: { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다" + } +}; - useEffect(() => { - // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 - void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) - .catch((e) => { - console.error("Auto login failed:", e); - }); - }, []); - return ( - - - - {children} - - - - ); -} +export default App; diff --git a/apps/fo/app/page.tsx b/apps/fo/app/page.tsx index 823e5a408..b1a103bca 100644 --- a/apps/fo/app/page.tsx +++ b/apps/fo/app/page.tsx @@ -1,44 +1,28 @@ -"use client" +"use client"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect } from "react"; +import { AUTH_USER_ID } from "@/src/shared/config"; +import { getCookie } from "@/src/shared/lib/cookie"; +import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { Button } from "@common/components/ui"; export default function Home() { - return ( -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- ); -} + const router = useRouter(); + const checkToken = getCookie(AUTH_USER_ID); + + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); + router.push("/pages/dashboard"); + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/pages/dashboard"); + } + }, []); +} \ No newline at end of file diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index f7c0e62ba..851d79497 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -1,13 +1,30 @@ "use client" -import Base from "@/app/base"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/app/store/common"; +import { useFoStore } from "@/app/store"; +import { usePathname, useRouter } from "next/navigation"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const { getMenuAuth } = useFoStore((state) => state); + const pathname = usePathname(); + const router = useRouter(); + + const handleClick = (id: MenuItem) => { + getMenuAuth(id.menuCd); + router.push(id.urlPath); + }; + return ( - {children} + + {children} + ); } diff --git a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx index fbda7a140..7e3f7c52f 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/modal/page.tsx @@ -117,7 +117,7 @@ export default function ProcessFacilityInfoModal(props: ModalType) { { value: 'common_info', label: '기본정보' }, { value: 'history_info', label: '이력정보' }, ]; - const fctCpc = props.fctCpc ? props.fctCpc.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : ''; + const fctCpc = props.fctCpc ? Number(props.fctCpc).toLocaleString() : ''; const upHeadListPlanInfoChange = headMakeList(upHeadListPlanInfo, 2); const upHeadListFcltyHistInfoChange = headMakeList(upHeadListFcltyHistInfo, 3); const upHeadListFcltyWaterQltyInfoChange = headMakeList(upHeadListFcltyWaterQltyInfo, 4); @@ -222,9 +222,9 @@ export default function ProcessFacilityInfoModal(props: ModalType) { {'시설명'} - {props.fcltyName} - {'시설구분'} - {props.bscFctClsNm} + {props.fcltyName} + {/* {'시설구분'} + {props.bscFctClsNm} */} {'시설용량(m³/일)'} {fctCpc} diff --git a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index 0e9085cb4..775dd1a45 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -210,12 +210,13 @@ export default function ProcessFacilitySearch() { const cellClick = (index: number) => { const facility = processFacilityList[index]; if (facility) { + const fctCpc = Number(String(facility.fctCpc).replace(/,/g, "").trim()); setModalFcltyNm(facility.bscFctNm); setModalFcltyCd(facility.bscFctCd); setModalSido(facility.sido); setModalGugun(facility.gugun); setModalAddr(facility.daddr); - setModalFctCpc(Number(facility.fctCpc)); + setModalFctCpc(fctCpc); setModalTrtFrfNm(facility.trtFrfNm); setModalRnDt(facility.rnDt); setModalCmplDt(facility.cmplDt); diff --git a/apps/fo/app/store/common/index.tsx b/apps/fo/app/store/common/index.tsx index e75492d52..e4aebc982 100644 --- a/apps/fo/app/store/common/index.tsx +++ b/apps/fo/app/store/common/index.tsx @@ -232,7 +232,7 @@ export const COD_YEAR = 2021; */ // 처리시설 계획정보 호출 (알에스원) -export const planInfoUrl = 'https://apigateway.apps.rsonesoft.com/dc-service/api/v1/nowDmgMtncGnsvy'; +export const planInfoUrl = '/dc-service/api/v1/nowDmgMtncGnsvy'; export const fetchPlanInfo = async (targetYear : string | undefined, ctpvNm : string | undefined, sggNm : string | undefined, fcltyCd : string | undefined) => { try { diff --git a/apps/fo/middleware.ts b/apps/fo/middleware.ts index 32257341d..bb9c9effa 100644 --- a/apps/fo/middleware.ts +++ b/apps/fo/middleware.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { SERVER_API_CS_URL } from "./src/shared/config/env"; +import { SERVER_API_CS_URL, SERVER_API_FO_URL, SERVER_API_WR_URL } from "./src/shared/config/env"; const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; @@ -7,6 +7,8 @@ const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || ""; const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; const serverApiUrl = SERVER_API_CS_URL; +const DC_URL = process.env.NEXT_PUBLIC_DC_URL || ""; + export function middleware(req: NextRequest) { console.log("Middleware 진입:", req.nextUrl.pathname); const pathname = req.nextUrl.pathname; @@ -63,12 +65,12 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname.startsWith("/sewerage-service/api/v1/map/geometry")) { + if (pathname.startsWith("/sewerage-service")) { const queryParams = searchParams.toString(); - const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${ + const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service", "")}${ queryParams ? `?${queryParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); + console.log(`[Middleware] Rewriting from '/sewerage-service' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } @@ -89,40 +91,42 @@ export function middleware(req: NextRequest) { }); } - if (pathname.startsWith("/operation-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); - - console.log( - `[Middleware] Rewriting from '/operation-service' path to: ${newUrl}` - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - if (pathname.startsWith("/reuse-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_WR_URL}${pathname.replace("/reuse-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/operation-service")) { + const queryParams = searchParams.toString(); + const newUrl = `${SERVER_API_FO_URL}${pathname.replace("/operation-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/operation-service' path to: ${newUrl}`); + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname.startsWith("/dc-service")) { const requestHeaders = new Headers(req.headers); + const queryParams = searchParams.toString(); + const newUrl = `${DC_URL}${pathname.replace("/dc-service", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + console.log(`[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`); + 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); - console.log( - `[Middleware] Rewriting from '/reuse-service' path to: ${newUrl}` - ); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, }); } - + if (pathname.startsWith("/user-service")) { const requestHeaders = new Headers(req.headers); const newUrl = `${SERVER_API_CS_URL}${pathname}`; @@ -135,21 +139,23 @@ export function middleware(req: NextRequest) { }); } - if (pathname === "/") { - return NextResponse.redirect(new URL("/pages/dashboard", req.url)); - } - return NextResponse.next(); + // if (pathname === "/") { + // return NextResponse.redirect(new URL("/pages/dashboard", req.url)); + // } + // return NextResponse.next(); } export const config = { matcher: [ - "/", - "/server/:path*", - "/geoserver/:path*", - "/attach-service/:path*", - "/user-service/:path*", - "/cms-service/:path*", - "/operation-service/:path*", - "/reuse-service/:path*", + "/", + "/server/:path*", + "/geoserver/:path*", + "/sewerage-service/:path*", + "/attach-service/:path*", + "/user-service/:path*", + "/cms-service/:path*", + "/operation-service/:path*", + "/reuse-service/:path*", + "/dc-service/:path*", ], }; diff --git a/apps/fo/package.json b/apps/fo/package.json index 8697a116a..53423decf 100644 --- a/apps/fo/package.json +++ b/apps/fo/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@common/assets": "workspace:*", "@common/business_components": "workspace:*", "@common/components": "workspace:*", "@tailwindcss/vite": "^4.0.10", diff --git a/apps/fo/src/app/index.tsx b/apps/fo/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/fo/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/fo/src/app/providers/app-provider.tsx b/apps/fo/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..edce6071e --- /dev/null +++ b/apps/fo/src/app/providers/app-provider.tsx @@ -0,0 +1,50 @@ +"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 "@/src/entities/auth/lib/auth-wrapper"; +import { MenuWrapper } from "@/src/entities/menu"; +import { PromptProvider, StoreProvider } from "@/src/shared/providers"; +import { AlertDialog } from "@/src/shared/ui/alert-dialog"; +import { PromptDialog } from "@/src/shared/ui/prompt-dialog"; +import { ConfirmDialog } from "@/src/shared/ui/confirm-dialog"; +import { StoreProvider as FoStoreProvider } from "@/app/store"; + + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/fo/src/app/providers/index.ts b/apps/fo/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/fo/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/fo/src/app/styles/base.css b/apps/fo/src/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/fo/src/app/styles/base.css @@ -0,0 +1,12 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} diff --git a/apps/fo/src/app/styles/components.css b/apps/fo/src/app/styles/components.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/fo/src/app/styles/components.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx index 678ff50d5..65bcf48cc 100644 --- a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx @@ -1,45 +1,54 @@ -// "use client"; +"use client"; -// import { useEffect } from "react"; -// import { useRouter } from "next/navigation"; -// import { LoginApi } from "../api/login-service"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "@/src/shared/api"; +import type { IUserInfo } from "@/src/shared/store"; +import { useUserStore, getCookie } from "@/src/shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "@/src/shared/config"; +import { LoginApi } from "../api/login-service"; -// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { -// const router = useRouter(); -// const { userInfo, setUserInfo } = useUserStore((state) => state); +const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3001/auth/login"; // 개발 환경 URL -// async function authCheck() { -// const userId = getCookie(AUTH_USER_ID); -// if (!userId) { -// throw new Error("Missing required authentication tokens"); -// } +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const { userInfo, setUserInfo } = useUserStore((state) => state); -// const response = await client.get( -// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, -// ); -// if (!userInfo) setUserInfo(response.data); -// } + async function authCheck() { + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } -// useEffect(() => { -// (async () => { -// try { -// await authCheck(); -// } catch (error) { -// console.error(error); + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); + } -// try { -// await LoginApi.silentRefresh(); -// await authCheck(); -// } catch (refreshError) { -// console.error("Token refresh failed:", refreshError); -// setUserInfo(null); -// router.push("/auth/login"); -// } -// } -// })(); -// }, []); + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); -// return <>{children}; -// }; + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + router.push("/"); // 개발시 홈으로 돌아가기 -// export { AuthWrapper }; + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + } + } + })(); + }, []); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/fo/src/entities/menu-authority/@x/menu.ts b/apps/fo/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/fo/src/entities/menu-authority/@x/posting.ts b/apps/fo/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/fo/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..c7ac056f4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/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; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/fo/src/entities/menu-authority/index.ts b/apps/fo/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/fo/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..f659f58af --- /dev/null +++ b/apps/fo/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/src/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; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/fo/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/fo/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/fo/src/entities/menu-authority/ui/authority-button.tsx b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..a4cd666d9 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/authority-button.tsx @@ -0,0 +1,26 @@ +import { useMenuStore } from "@/src/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/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..839b9a7a4 --- /dev/null +++ b/apps/fo/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/src/shared/config"; +import { useAlertStore, useMutationWrapper } from "@/src/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, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = 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("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + 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 ( + + + + + + + 그룹별 메뉴 권한 + +
+ +
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/fo/src/entities/menu/api/menu-service.ts b/apps/fo/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..8b08d7fa5 --- /dev/null +++ b/apps/fo/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { MenuItem, MenuService } 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; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/fo/src/entities/menu/index.ts b/apps/fo/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/fo/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/fo/src/entities/menu/lib/menu-wrapper.tsx b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..f445123d3 --- /dev/null +++ b/apps/fo/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/src/shared/lib"; +import { getCookie } from "@/src/shared/lib";; +import { SITE_ID } from "@/src/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]!); + })(); + }, []); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}; +}; + +export { MenuWrapper }; diff --git a/apps/fo/src/entities/menu/lib/utils.ts b/apps/fo/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/fo/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/fo/src/entities/menu/model/menu-interface.ts b/apps/fo/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/fo/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +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 MenuService { + getMenuRoles(siteId: string): Promise; +} diff --git a/apps/fo/src/shared/api/client.ts b/apps/fo/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/fo/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/fo/src/shared/api/index.ts b/apps/fo/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/fo/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/fo/src/shared/config/env.ts b/apps/fo/src/shared/config/env.ts index 8baf1b0f0..6e3bf223e 100644 --- a/apps/fo/src/shared/config/env.ts +++ b/apps/fo/src/shared/config/env.ts @@ -2,7 +2,7 @@ export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "FO"; 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_WR_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL ||'http://con.mind-one.co.kr:11080/api/v1'; -export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; +export const SERVER_API_FO_URL = process.env.NEXT_PUBLIC_SERVER_API_URL||' http://con.mind-one.co.kr:11082/api/v1'; export const SERVER_API_CS_URL = process.env.NEXT_PUBLIC_CS_SERVER_API_URL||'http://dev.onasys.co.kr:8000'; export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; diff --git a/apps/fo/src/shared/lib/index.ts b/apps/fo/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/fo/src/shared/lib/index.ts @@ -0,0 +1,6 @@ +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/fo/src/shared/lib/use-alert-store.ts b/apps/fo/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/fo/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/fo/src/shared/lib/use-menu-store.ts b/apps/fo/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/fo/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/fo/src/shared/lib/use-mutation-wrapper.tsx b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/fo/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,31 @@ +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?.status) + errorMessage += `status: ${error.response.status}\n`; + 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(`[Error] ${errorMessage}`); + }, + }); +}; diff --git a/apps/fo/src/shared/lib/use-prompt.tsx b/apps/fo/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/fo/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/fo/src/shared/lib/use-user-store.ts b/apps/fo/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/fo/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/fo/src/shared/providers/index.ts b/apps/fo/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/fo/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/fo/src/shared/providers/prompt-provider.tsx b/apps/fo/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/fo/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, useState } from "react"; + +interface PromptContext { + message?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/fo/src/shared/providers/store-provider.tsx b/apps/fo/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/fo/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/fo/src/shared/store/alert-store.ts b/apps/fo/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/fo/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/fo/src/shared/store/attachment-store.ts b/apps/fo/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/fo/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/fo/src/shared/store/index.ts b/apps/fo/src/shared/store/index.ts new file mode 100644 index 000000000..b0ca59c6c --- /dev/null +++ b/apps/fo/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"; \ No newline at end of file diff --git a/apps/fo/src/shared/store/menu-store.ts b/apps/fo/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/fo/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/fo/src/shared/store/user-store.ts b/apps/fo/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/fo/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/fo/src/shared/ui/alert-dialog.tsx b/apps/fo/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/fo/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/fo/src/shared/ui/confirm-dialog.tsx b/apps/fo/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/fo/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/fo/src/shared/ui/map-layout.tsx b/apps/fo/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..e05666401 --- /dev/null +++ b/apps/fo/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/fo/src/shared/ui/page-layout.tsx b/apps/fo/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/fo/src/shared/ui/page-layout.tsx @@ -0,0 +1,22 @@ +import { PageTitle } from "@common/components"; +import { useMenuStore } from "../lib/use-menu-store"; + +function PageLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + const menuPaths = currentMenuInfo?.menuPath + ?.split(">") + .map((path) => path.trim()); + + return ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/fo/src/shared/ui/prompt-dialog.tsx b/apps/fo/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..496a18b5b --- /dev/null +++ b/apps/fo/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/src/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog }; diff --git a/apps/fo/tailwind.config.ts b/apps/fo/tailwind.config.ts index 33ac9204f..91db77945 100644 --- a/apps/fo/tailwind.config.ts +++ b/apps/fo/tailwind.config.ts @@ -9,4 +9,11 @@ export default { "../../packages/common/business_components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", "../../packages/common/components/src/ui/**/*.{js,ts,jsx,tsx,mdx}", ], + theme: { + extend: { + boxShadow: { + blur: "0 0 10px -1px rgba(0, 0, 0, 0.06)", + }, + }, + }, } satisfies Config; \ No newline at end of file diff --git a/apps/fo/tsconfig.json b/apps/fo/tsconfig.json index 493584782..59bd02446 100644 --- a/apps/fo/tsconfig.json +++ b/apps/fo/tsconfig.json @@ -28,12 +28,8 @@ "@/*": ["./*"], "@common/components/*": ["../../packages/common/components/src/*"], "@common/components": ["../../packages/common/components/src/index"], - "@common/business_components/*": [ - "../../packages/common/business_components/src/*" - ], - "@common/business_components": [ - "../../packages/common/business_components/src/index" - ], + "@common/business_components/*": ["../../packages/common/business_components/src/*"], + "@common/business_components": ["../../packages/common/business_components/src/index"], "@common/network": ["../../packages/common/network"] } }, diff --git a/apps/sn/.env.production b/apps/sn/.env.production index 158f22aaf..3be1e8f4c 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -4,10 +4,10 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr +NEXT_PUBLIC_PROXY_HOST = https://lion.hasuon.or.kr NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 -NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service diff --git a/apps/wr/.env.production b/apps/wr/.env.production index e5900440f..67ff9784b 100644 --- a/apps/wr/.env.production +++ b/apps/wr/.env.production @@ -1,9 +1,9 @@ NEXT_PUBLIC_SITE_ID = WR NEXT_PUBLIC_PORT = 3000 -NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr -NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 -NEXT_PUBLIC_FO_SERVER_API_URL = https://wr.hasuon.or.kr/operation-service/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 +NEXT_PUBLIC_PROXY_HOST = https://tiger.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl @@ -11,13 +11,9 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_REFRESH_TOKEN = refresh-token -NEXT_PUBLIC_WR_SERVER_API_URL = https://wr.hasuon.or.kr/reuse-service/api/v1 -NEXT_PUBLIC_SEWERAGE_API_URL = https://wr.hasuon.or.kr/sewerage-service/api +NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service/api NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -#NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1 -#NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1 -#NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -#NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do diff --git a/apps/wr/app/(with-layout)/drgtSttn/page.tsx b/apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx similarity index 100% rename from apps/wr/app/(with-layout)/drgtSttn/page.tsx rename to apps/wr/app/(with-layout)/matMng/coMng/drgtSttn/page.tsx diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index f728f5de0..25530bfef 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -1,49 +1,14 @@ -"use client"; -import "./globals.css"; -import i18next from "i18next"; -import { I18nextProvider } from "react-i18next"; -import koBase from "../public/locales/ko/message.json"; -import { StoreProvider } from "./store"; -import { LoginApi } from "@/src/entities/auth"; -import { useEffect } from "react"; +import type { Metadata } from "next"; +import { App } from "../src/app"; -const i18n = i18next.createInstance(); -i18n.init({ - lng: "ko", - fallbackLng: "ko", - resources: { - ko: { - base: koBase, - }, - }, -}); +export const metadata: Metadata = { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다", + keywords: ["하수관로"], + openGraph: { + title: "디지털 하수관로", + description: "디지털 하수관로 시스템입니다" + } +}; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - - useEffect(() => { - // 렌더 중이 아니라 클라이언트에서만 실행 + 에러 캐치 - void LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }) - .finally(()=>{ - // getCommonUpCd() - } ) - .catch((e) => { - console.error("Auto login failed:", e); - }); - }, []); - - return ( - - - - - {children} - - - - - ); -} +export default App; \ No newline at end of file diff --git a/apps/wr/app/page.tsx b/apps/wr/app/page.tsx index 8729ac5f0..37fbe8b00 100644 --- a/apps/wr/app/page.tsx +++ b/apps/wr/app/page.tsx @@ -1,166 +1,31 @@ "use client"; -import { Button } from "@common/components/ui"; + +import { LoginApi } from "../src/entities/auth"; +import { useEffect } from "react"; +import { getCookie } from "../src/shared/lib"; +import { AUTH_USER_ID } from "../src/shared/config"; import { useRouter } from "next/navigation"; + export default function Home() { const router = useRouter(); - return ( - <> -
- {/* 시설 등록 */} -
-

시설 등록

-
-
- -
-
- -
-
- -
-
- -
-
-
+ const userId = "CLEANWATER"; + const password = "gktneh12!@"; + const checkToken = getCookie(AUTH_USER_ID); - {/* 지도점검 결과 */} -
-

지도점검 결과

-
-
- -
-
- -
-
- -
-
- -
-
-
- - {/* 시설 현황 */} -
-

시설 현황

-
-
- -
-
- -
-
- -
-
- -
-
-
- - {/* 수요처 관리 */} -
-

수요처 관리

-
- -
- -
-
- -
-
- -
-
-
- - {/* 수요처 매칭 */} -
-

수요처 매칭

-
-
- -
-
- -
-
- -
-
-
- {/* 통계*/} -
-

통계 매칭

-
-
- -
-
- -
-
-
- - {/* 시설 현황(지도) */} -
-

시설 현황(지도)

-
-
- -
-
-
-
- - ); + useEffect(() => { + if (!checkToken) { + async function doLogin() { + try { + await LoginApi.login({ userId, password }); + window.location.href = "/map/mainMap"; + } catch (err) { + console.error("로그인 실패", err); + } + } + doLogin(); + } else { + router.push("/map/mainMap"); + } + }, []); } diff --git a/apps/wr/app/store/common/auth/index.tsx b/apps/wr/app/store/common/auth/index.tsx index 5e65f3b6a..7eceacb39 100644 --- a/apps/wr/app/store/common/auth/index.tsx +++ b/apps/wr/app/store/common/auth/index.tsx @@ -39,8 +39,7 @@ export const authActionsExport: (set: any, get: any) => authActions = ( getMenuAuthrts: async (menuCd: number) => { try { const response = await getFetchWithCount( - fetchType.LOCAL, - + fetchType.CS, `/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}` ); set((state: authState) => { diff --git a/apps/wr/app/store/common/code/index.ts b/apps/wr/app/store/common/code/index.ts index 0601c5e41..f6e28339c 100644 --- a/apps/wr/app/store/common/code/index.ts +++ b/apps/wr/app/store/common/code/index.ts @@ -42,7 +42,7 @@ export const getCommonUpCd = async (set: any) => { try { let lwCdCode: Record = {}; const upCd = await getFetchWithCount( - fetchType.LOCAL, + fetchType.CS, "/cms-service/api/v1/up-codes?keyword=&upCd=&cdSe=&useYn=Y&siteId=WR&page=0&size=100&sort=string" ); @@ -56,7 +56,7 @@ export const getCommonUpCd = async (set: any) => { if (!mapping) return; const lwCd = await getFetchWithCount( - fetchType.LOCAL, + fetchType.CS, `/cms-service/api/v1/lwpo-cds/up-cd/${cd}` ); diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts index 6b1f4292c..dc1147abb 100644 --- a/apps/wr/components/lib/api/index.ts +++ b/apps/wr/components/lib/api/index.ts @@ -4,6 +4,7 @@ import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/"; import { getCookie } from "@common/components/lib/cookie"; +const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST; export const fetchType = { COMMON: "common", WR: "wr", @@ -20,15 +21,15 @@ export const getFetch = async (type: string, url: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; - + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "GET", headers: { accept: "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -71,15 +72,16 @@ export const getFetchWithCount = async (type: string, url: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "GET", headers: { accept: "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -116,16 +118,16 @@ export const getFetchWithCount = async (type: string, url: string) => { export const postFectch = async (url: string, params: any, type?: string) => { const div = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR; - const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const apiUrl = PROXY_HOST + div + url; + const response = await fetch(apiUrl, { method: "POST", headers: { accept: "application/json", "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -144,16 +146,16 @@ export const putFectch = async (url: string, params: any, type?: string) => { : type === fetchType.CS ? "" : DEV_URL_WR; - + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "PUT", headers: { "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -176,15 +178,16 @@ export const deleteFectch = async ( : type === fetchType.CS ? "" : DEV_URL_WR; + const apiUrl = PROXY_HOST + div + url; const tokenId = getCookie(AUTH_USER_ID); const Authorization = getCookie(CLAIM_NAME); - const response = await fetch(div + url, { + const response = await fetch(apiUrl, { method: "DELETE", headers: { "Content-Type": "application/json", - AUTH_USER_ID: tokenId ?? "", - CLAIM_NAME: Authorization ?? "", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: params ? JSON.stringify(params) : undefined, }); diff --git a/apps/wr/components/lib/config/index.ts b/apps/wr/components/lib/config/index.ts index f05a38e62..979862f12 100644 --- a/apps/wr/components/lib/config/index.ts +++ b/apps/wr/components/lib/config/index.ts @@ -2,9 +2,9 @@ import { PROXY_HOST, SERVER_API_CS_TUS_URL, SERVER_API_CS_URL, - SERVER_API_FO_URL, - SERVER_API_WR_URL, - SERVER_SEWERAGE_API_URL, + //SERVER_API_FO_URL, + //SERVER_API_WR_URL, + //SERVER_SEWERAGE_API_URL, } from "@/src/shared/config"; //const DEV_PORT = "11080"; @@ -18,9 +18,10 @@ import { //export const CS_TUS_UPLOAD= "/attach-service/api/v1/tus/upload"; export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`; -export const DEV_URL_WR = SERVER_API_WR_URL; -export const DEV_URL_FO = SERVER_API_FO_URL; -export const DEV_URL_SN = SERVER_SEWERAGE_API_URL; +export const DEV_URL_WR = '/reuse-service'; +export const DEV_URL_FO = '/operation-service'; +//export const DEV_URL_FO = SERVER_API_FO_URL; +export const DEV_URL_SN = '/sewerage-service/api'; export const LOCAL_URL = PROXY_HOST; export const CS_URL = SERVER_API_CS_URL; export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL; diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 5dc77eab4..0a3658a0b 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -29,10 +29,6 @@ export function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(newUrl)); } - if (pathname === "/") { - return NextResponse.redirect(new URL("/map/mainMap", req.url)); - } - if (pathname.startsWith("/cms-service")) { const requestHeaders = new Headers(req.headers); const Authorization = req.cookies.get(CLAIM_NAME)?.value; diff --git a/apps/wr/src/app/index.tsx b/apps/wr/src/app/index.tsx new file mode 100644 index 000000000..692b55ef0 --- /dev/null +++ b/apps/wr/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/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx new file mode 100644 index 000000000..b8eb35ec6 --- /dev/null +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -0,0 +1,68 @@ +"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 { PromptProvider, StoreProvider } from "../../shared/providers"; +import { AlertDialog } from "../../shared/ui/alert-dialog"; +import { PromptDialog } from "../../shared/ui/prompt-dialog"; +import { ConfirmDialog } from "../../shared/ui/confirm-dialog"; +// import "./globals.css"; +import i18next from "i18next"; +import { I18nextProvider } from "react-i18next"; +import koBase from "@/public/locales/ko/message.json"; +import { StoreProvider as WrStoreProvider } from "@/app/store"; +import { LoginApi } from "@/src/entities/auth"; +import { useEffect } from "react"; + + +const i18n = i18next.createInstance(); +i18n.init({ + lng: "ko", + fallbackLng: "ko", + resources: { + ko: { + base: koBase, + }, + }, +}); + +const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + const queryClient = new QueryClient(); + return ( + + + + + + + + + + {children} + + + + + + + + + + + +
+ +
+
+ ); +}; + +export { AppProvider }; diff --git a/apps/wr/src/app/providers/index.ts b/apps/wr/src/app/providers/index.ts new file mode 100644 index 000000000..15f99b7fd --- /dev/null +++ b/apps/wr/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProvider } from "./app-provider"; diff --git a/apps/wr/src/app/styles/base.css b/apps/wr/src/app/styles/base.css new file mode 100644 index 000000000..8453ff9e8 --- /dev/null +++ b/apps/wr/src/app/styles/base.css @@ -0,0 +1,12 @@ +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + font-family: "Noto Sans KR", sans-serif; + font-size: 10px; +} diff --git a/apps/wr/src/app/styles/components.css b/apps/wr/src/app/styles/components.css new file mode 100644 index 000000000..4dc30bafd --- /dev/null +++ b/apps/wr/src/app/styles/components.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components { + .card { + @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; + } + .card-border { + @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + } + .card-header { + @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; + } + .card-footer { + @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; + } + + [data-theme="black"] { + .card-border { + @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; + } + .card-header { + @apply border-b-[#3f4042]; + } + } + + input[type="file"] { + @apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer; + } +} diff --git a/apps/wr/src/entities/auth/index.ts b/apps/wr/src/entities/auth/index.ts index 9dc9dd368..97e142225 100644 --- a/apps/wr/src/entities/auth/index.ts +++ b/apps/wr/src/entities/auth/index.ts @@ -1,2 +1,2 @@ export { LoginApi } from "./api/login-service"; -//export { AuthWrapper } from "./lib/auth-wrapper"; +export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..deee0e8fe --- /dev/null +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { client } from "../../../shared/api"; +import type { IUserInfo } from "../../../shared/store"; +import { useUserStore } from "../../../shared/lib"; +import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config"; +import { getCookie } from "../../../shared/lib"; +import { LoginApi } from "../api/login-service"; + + +const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL + +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("/"); // 개발시 홈으로 돌아가기 + + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + } + } + })(); + }, []); + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/wr/src/entities/menu-authority/@x/menu.ts b/apps/wr/src/entities/menu-authority/@x/menu.ts new file mode 100644 index 000000000..86479f859 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/@x/menu.ts @@ -0,0 +1 @@ +export { MenuAuthorityApi } from "../api/menu-authority-service"; diff --git a/apps/wr/src/entities/menu-authority/@x/posting.ts b/apps/wr/src/entities/menu-authority/@x/posting.ts new file mode 100644 index 000000000..bfe84fa0d --- /dev/null +++ b/apps/wr/src/entities/menu-authority/@x/posting.ts @@ -0,0 +1 @@ +export { AuthorityButton } from "../ui/authority-button"; diff --git a/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..c7ac056f4 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,40 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/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; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/wr/src/entities/menu-authority/index.ts b/apps/wr/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..2a555b77e --- /dev/null +++ b/apps/wr/src/entities/menu-authority/index.ts @@ -0,0 +1,2 @@ +export { AuthorityButton } from "./ui/authority-button"; +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..b59574c2f --- /dev/null +++ b/apps/wr/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,48 @@ +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; + getPageMenuAuthority(menuCd: number): Promise; + saveMenuAuthority(data: MenuAuthorityPayload[]): Promise; +} diff --git a/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/wr/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..6969a85e9 --- /dev/null +++ b/apps/wr/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/wr/src/entities/menu-authority/ui/authority-button.tsx b/apps/wr/src/entities/menu-authority/ui/authority-button.tsx new file mode 100644 index 000000000..257ab6e5d --- /dev/null +++ b/apps/wr/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/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..4a73cb596 --- /dev/null +++ b/apps/wr/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,103 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, 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, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const alert = useAlertStore((state) => state.setMessage); + + const { data: menuAuthorityList } = 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("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + 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 ( + + + + + + + 그룹별 메뉴 권한 + +
+ +
+ + + + + + +
+
+ ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/wr/src/entities/menu/api/menu-service.ts b/apps/wr/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..8b08d7fa5 --- /dev/null +++ b/apps/wr/src/entities/menu/api/menu-service.ts @@ -0,0 +1,14 @@ +import { client } from "@/src/shared/api"; +import { ENDPOINT } from "@/src/shared/config"; +import type { MenuItem, MenuService } 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; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/wr/src/entities/menu/index.ts b/apps/wr/src/entities/menu/index.ts new file mode 100644 index 000000000..04f63e38b --- /dev/null +++ b/apps/wr/src/entities/menu/index.ts @@ -0,0 +1,2 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; +export type { MenuItem } from "./model/menu-interface"; diff --git a/apps/wr/src/entities/menu/lib/menu-wrapper.tsx b/apps/wr/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..f445123d3 --- /dev/null +++ b/apps/wr/src/entities/menu/lib/menu-wrapper.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { MenuAuthorityApi } from "@/src/entities/menu-authority/@x/menu"; +import { useAlertStore, useMenuStore } from "@/src/shared/lib"; +import { getCookie } from "@/src/shared/lib";; +import { SITE_ID } from "@/src/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]!); + })(); + }, []); + + useEffect(() => { + if (!pageMenuAuthority) return; + const inquiryAuthority = checkInquiryAuthority(); + if (!inquiryAuthority) + alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); + }, [pageMenuAuthority]); + + return <>{children}; +}; + +export { MenuWrapper }; diff --git a/apps/wr/src/entities/menu/lib/utils.ts b/apps/wr/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..1cf4514d9 --- /dev/null +++ b/apps/wr/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/wr/src/entities/menu/model/menu-interface.ts b/apps/wr/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/wr/src/entities/menu/model/menu-interface.ts @@ -0,0 +1,23 @@ +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 MenuService { + getMenuRoles(siteId: string): Promise; +} diff --git a/apps/wr/src/shared/api/client.ts b/apps/wr/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/wr/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/wr/src/shared/api/index.ts b/apps/wr/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/wr/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/wr/src/shared/lib/index.ts b/apps/wr/src/shared/lib/index.ts new file mode 100644 index 000000000..b8b4c52c0 --- /dev/null +++ b/apps/wr/src/shared/lib/index.ts @@ -0,0 +1,6 @@ +export { useMutationWrapper } from "./use-mutation-wrapper"; +export { useAlertStore } from "./use-alert-store"; +export { useMenuStore } from "./use-menu-store"; +export { useUserStore } from "./use-user-store"; +export { usePrompt } from "./use-prompt"; +export { getCookie, setCookie, removeCookie } from "./cookie"; diff --git a/apps/wr/src/shared/lib/use-alert-store.ts b/apps/wr/src/shared/lib/use-alert-store.ts new file mode 100644 index 000000000..c21a00ed3 --- /dev/null +++ b/apps/wr/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/wr/src/shared/lib/use-menu-store.ts b/apps/wr/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/wr/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/wr/src/shared/lib/use-mutation-wrapper.tsx b/apps/wr/src/shared/lib/use-mutation-wrapper.tsx new file mode 100644 index 000000000..ce119d2a7 --- /dev/null +++ b/apps/wr/src/shared/lib/use-mutation-wrapper.tsx @@ -0,0 +1,31 @@ +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?.status) + errorMessage += `status: ${error.response.status}\n`; + 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(`[Error] ${errorMessage}`); + }, + }); +}; diff --git a/apps/wr/src/shared/lib/use-prompt.tsx b/apps/wr/src/shared/lib/use-prompt.tsx new file mode 100644 index 000000000..f36858eb0 --- /dev/null +++ b/apps/wr/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/wr/src/shared/lib/use-user-store.ts b/apps/wr/src/shared/lib/use-user-store.ts new file mode 100644 index 000000000..748caed4c --- /dev/null +++ b/apps/wr/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/wr/src/shared/providers/index.ts b/apps/wr/src/shared/providers/index.ts new file mode 100644 index 000000000..fa8320fe7 --- /dev/null +++ b/apps/wr/src/shared/providers/index.ts @@ -0,0 +1,2 @@ +export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/wr/src/shared/providers/prompt-provider.tsx b/apps/wr/src/shared/providers/prompt-provider.tsx new file mode 100644 index 000000000..10338c338 --- /dev/null +++ b/apps/wr/src/shared/providers/prompt-provider.tsx @@ -0,0 +1,37 @@ +import { createContext, useState } from "react"; + +interface PromptContext { + message?: string; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; + setMessage: (message: string) => void; + setResolve: (resolve: () => void) => void; +} + +export const PromptContext = createContext({ + message: "", + resolve() {}, + setMessage() {}, + setResolve() {}, +}); + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [message, setMessage] = useState(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); + + return ( + + {children} + + ); +}; diff --git a/apps/wr/src/shared/providers/store-provider.tsx b/apps/wr/src/shared/providers/store-provider.tsx new file mode 100644 index 000000000..a0ec17b61 --- /dev/null +++ b/apps/wr/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/wr/src/shared/store/alert-store.ts b/apps/wr/src/shared/store/alert-store.ts new file mode 100644 index 000000000..3bba65848 --- /dev/null +++ b/apps/wr/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/wr/src/shared/store/attachment-store.ts b/apps/wr/src/shared/store/attachment-store.ts new file mode 100644 index 000000000..faa700949 --- /dev/null +++ b/apps/wr/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/wr/src/shared/store/index.ts b/apps/wr/src/shared/store/index.ts new file mode 100644 index 000000000..b0ca59c6c --- /dev/null +++ b/apps/wr/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"; \ No newline at end of file diff --git a/apps/wr/src/shared/store/menu-store.ts b/apps/wr/src/shared/store/menu-store.ts new file mode 100644 index 000000000..8536011ee --- /dev/null +++ b/apps/wr/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/wr/src/shared/store/user-store.ts b/apps/wr/src/shared/store/user-store.ts new file mode 100644 index 000000000..65d897cf1 --- /dev/null +++ b/apps/wr/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/wr/src/shared/ui/alert-dialog.tsx b/apps/wr/src/shared/ui/alert-dialog.tsx new file mode 100644 index 000000000..6160d5301 --- /dev/null +++ b/apps/wr/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/wr/src/shared/ui/confirm-dialog.tsx b/apps/wr/src/shared/ui/confirm-dialog.tsx new file mode 100644 index 000000000..6a4996bfb --- /dev/null +++ b/apps/wr/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/wr/src/shared/ui/map-layout.tsx b/apps/wr/src/shared/ui/map-layout.tsx new file mode 100644 index 000000000..e05666401 --- /dev/null +++ b/apps/wr/src/shared/ui/map-layout.tsx @@ -0,0 +1,13 @@ +import { useMenuStore } from "../lib/use-menu-store"; + +function MapLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + return ( +
+ {children} +
+ ); +} + +export { MapLayout }; diff --git a/apps/wr/src/shared/ui/page-layout.tsx b/apps/wr/src/shared/ui/page-layout.tsx new file mode 100644 index 000000000..358d3b5db --- /dev/null +++ b/apps/wr/src/shared/ui/page-layout.tsx @@ -0,0 +1,22 @@ +import { PageTitle } from "@common/components"; +import { useMenuStore } from "../lib/use-menu-store"; + +function PageLayout({ children }: { children: React.ReactNode }) { + const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); + + const menuPaths = currentMenuInfo?.menuPath + ?.split(">") + .map((path) => path.trim()); + + return ( +
+ + {children} +
+ ); +} + +export { PageLayout }; diff --git a/apps/wr/src/shared/ui/prompt-dialog.tsx b/apps/wr/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..496a18b5b --- /dev/null +++ b/apps/wr/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/src/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog };