diff --git a/apps/cm/.gitignore b/apps/cm/.gitignore index b63718b61..c3326d730 100644 --- a/apps/cm/.gitignore +++ b/apps/cm/.gitignore @@ -10,6 +10,8 @@ !.yarn/releases !.yarn/versions +pnpm_lock.yaml + # testing /coverage diff --git a/apps/cm/src/entities/auth/lib/token-manager.ts b/apps/cm/src/entities/auth/lib/token-manager.ts index 0e761486a..21ed00218 100644 --- a/apps/cm/src/entities/auth/lib/token-manager.ts +++ b/apps/cm/src/entities/auth/lib/token-manager.ts @@ -19,14 +19,37 @@ export class TokenManager { } public setToken(key: string, value: string): void { + // 환경에 따른 도메인 설정 + const domain = this.getCookieDomain(); + setCookie(key, value, { secure: window.location.protocol === "https:", sameSite: "lax", + domain: domain, + path: "/", // 모든 경로에서 접근 가능 }); } + private getCookieDomain(): string | undefined { + const hostname = window.location.hostname; + + // 개발 환경에서는 localhost 도메인 사용 + if (hostname === "localhost" || hostname === "127.0.0.1") { + return undefined; // localhost에서는 도메인 설정하지 않음 + } + + // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 + if (hostname.includes("hasuon.or.kr")) { + return ".hasuon.or.kr"; + } + + // 기타 환경에서는 현재 호스트명 사용 + return hostname; + } + public removeToken(key: string): void { - removeCookie(key); + const domain = this.getCookieDomain(); + removeCookie(key, { domain, path: "/" }); } public setupRefreshTimer( diff --git a/apps/dm/.env.development b/apps/dm/.env.development index 7a7dc3f52..833207ed0 100644 --- a/apps/dm/.env.development +++ b/apps/dm/.env.development @@ -1,22 +1,22 @@ +# 공단 개발 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://dev.onasys.co.kr:8000 -NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_PORT = 3000 +NEXT_PUBLIC_PROXY_HOST = https://devreuse.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 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_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_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 +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-was.svc.cluster.local:8000/sewerage-service/api NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= 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.mindone b/apps/dm/.env.mindone new file mode 100644 index 000000000..7a7dc3f52 --- /dev/null +++ b/apps/dm/.env.mindone @@ -0,0 +1,22 @@ +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://dev.onasys.co.kr:8000 +NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +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_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_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 +#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 + + + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm/.env.production b/apps/dm/.env.production index 7b8f5c470..e627195cb 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -1,7 +1,8 @@ +# 공단 운영 NEXT_PUBLIC_SITE_ID = DM -NEXT_PUBLIC_PORT = 11035 +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/api/v1 +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 diff --git a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx index 5a7ca6dbd..ce38a1226 100644 --- a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx @@ -5,7 +5,9 @@ import { useDmStore } from '@/app/store'; import type { DmStatusDataType } from '@/app/store/wrStatus/type'; import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components'; import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { setCookie } from "../../../../src/shared/lib"; +setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "DM"); interface WrStatusChartProps { chartForm: string; @@ -149,7 +151,7 @@ export default function WrStatusPage() { useEffect(() => { - if(Common.selectSidoData&&Common.selectSidoData.length>0) + if(Common.selectSidoData&&Common.selectSidoData.length>1) getDmStatusData(Common.selectSidoData); },[Common.selectSidoData]) diff --git a/apps/dm/app/(popup-layout)/map/service/map.ts b/apps/dm/app/(popup-layout)/map/service/map.ts index aa98a0bc2..9c10ca190 100644 --- a/apps/dm/app/(popup-layout)/map/service/map.ts +++ b/apps/dm/app/(popup-layout)/map/service/map.ts @@ -1,9 +1,7 @@ - /** * 관망도 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"; export type LyrStngProps = { lyr_sn: string @@ -31,7 +29,7 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 getLyrStngs: async () => { - const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, { + const response = await fetch(`${DEV_URL_SN}/app/v1/map/lyrStngs/DM_NETWORK`, { method: 'GET', headers: { 'Accept': 'application/json', @@ -45,7 +43,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(`${DEV_URL_WR}/api/v1/reuseFc/pubFc/${ruseFcltPk}`, { method: 'GET', headers: { 'Accept': 'application/json', diff --git a/apps/dm/app/middleware.ts b/apps/dm/app/middleware.ts deleted file mode 100644 index ad35dfba7..000000000 --- a/apps/dm/app/middleware.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; -const SERVER_API_URL = process.env.NEXT_PUBLIC_DM_SERVER_API_URL || ""; -const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; -const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || ""; -const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_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; - const searchParams = req.nextUrl.searchParams; - - if (pathname.startsWith("/server")) { - - const decodedParams = Array.from(searchParams.entries()) - .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) - .join("&"); - - const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${ - decodedParams ? `?${decodedParams}` : "" - }`; - return NextResponse.rewrite(new URL(newUrl)); - } - - if (pathname.startsWith("/geoserver")) { - let queryParams = searchParams.toString(); - const typename = searchParams.get("TYPENAME"); - - let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - - if ( - typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설 - || typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역 - || typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도 - || typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도 - ) { - newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - } - else if ( - typename == "hasudo:tb_ms_swage_ppl" //관로 - || typename == "hasudo:tb_ms_swage_mhl" //맨홀 - || typename == "hasudo:tb_ms_swage_pmstn" //펌프장 - || typename == "hasudo:tb_ms_stofcbr" //우수토실 - ) { - newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`; - } - else if ( - typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실 - || typename == "hasudo:om_wqlt_station" //관측소 - || typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설 - || typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설 - || typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설 - || typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설 - || typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처 - || typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처 - ){ - const url = req.nextUrl.clone(); - url.searchParams.set('TYPENAME', typename.replace("hasudo:","")); - queryParams = url.searchParams.toString(); - newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`; - } - - console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`); - return NextResponse.rewrite(new URL(newUrl)); - } - - if ( - pathname.startsWith("/attach-service") || - pathname.startsWith("/cms-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 = `${SERVER_API_URL}${pathname}`; - - if (token) requestHeaders.set(CLAIM_NAME, token); - if (userId) requestHeaders.set(AUTH_USER_ID, userId); - - console.log( - `[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - - if (pathname.startsWith("/user-service")) { - const requestHeaders = new Headers(req.headers); - const newUrl = `${SERVER_API_URL}${pathname}`; - - console.log( - `[Middleware] Rewriting from '/user-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - - if (pathname.startsWith("/dc-service")) { - const requestHeaders = new Headers(req.headers); - const decodedParams = Array.from(searchParams.entries()) - .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) - .join("&"); - - const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`; - console.log( - `[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`, - ); - return NextResponse.rewrite(new URL(newUrl), { - headers: requestHeaders, - }); - } - return NextResponse.next(); -} - -export const config = { - matcher: [ - "/server/:path*", - "/geoserver/:path*", - "/attach-service/:path*", - "/user-service/:path*", - "/cms-service/:path*", - "/dc-service/:path*", - ], - -}; diff --git a/apps/dm/app/store/map/index.tsx b/apps/dm/app/store/map/index.tsx index 5ed066f73..f5b44c891 100644 --- a/apps/dm/app/store/map/index.tsx +++ b/apps/dm/app/store/map/index.tsx @@ -1,7 +1,12 @@ +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 tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); + const checkWrRegList = async () => { const wrRegList = await getFetch(fetchType.WR, `/reuseFc/pubFc`); return wrRegList; @@ -26,6 +31,8 @@ const getRuseFclt = async ( method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(ruseList), }); diff --git a/apps/dm/app/store/wrStatus/index.tsx b/apps/dm/app/store/wrStatus/index.tsx index 10ef19216..ec8911f51 100644 --- a/apps/dm/app/store/wrStatus/index.tsx +++ b/apps/dm/app/store/wrStatus/index.tsx @@ -32,7 +32,7 @@ export const DmStatusReducer = (set: any): DmStatusActions => { return { getDmStatusData: async (cityCd: SelectDataType[]) => { const data = await getFetch(fetchType.FO, "/dashBoard/daemin"); - + console.log('cityCd ', cityCd) const dataSum = data.data.reduce( (acc: DmStatusDataType, item: DmStatusDataType) => { acc.sewage = ( @@ -89,15 +89,19 @@ export const DmStatusReducer = (set: any): DmStatusActions => { const sewageCnt = item.sewageCnt; const rainwaterCnt = item.rainwaterCnt; const reclaimedCnt = item.reclaimedCnt; - - return { - region: regionText ? regionText.text : item.region, - sewage: sewageCnt + "(" + sewage + ")", - rainwater: rainwaterCnt + "(" + rainwater + ")", - reclaimed: reclaimedCnt + "(" + reclaimed + ")", - }; + console.log("regionText ", regionText); + if (regionText && regionText.text) { + return { + region: regionText ? regionText.text : item.region, + sewage: sewageCnt + "(" + sewage + ")", + rainwater: rainwaterCnt + "(" + rainwater + ")", + reclaimed: reclaimedCnt + "(" + reclaimed + ")", + }; + } } - ); + ).filter((item): item is DmStatusBoardType => item !== undefined); + + console.log("boardData ", boardData); const chartData = data.data.map((item) => ({ label: makeRegion( diff --git a/apps/dm/components/ui/api/index.ts b/apps/dm/components/ui/api/index.ts index 28644b00e..7ba66c4ed 100644 --- a/apps/dm/components/ui/api/index.ts +++ b/apps/dm/components/ui/api/index.ts @@ -1,5 +1,10 @@ //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 tokenId = getCookie(AUTH_USER_ID); +const Authorization = getCookie(CLAIM_NAME); export const fetchType = { COMMON: "common", @@ -17,6 +22,8 @@ export const getFetch = async (type: string, url: string) => { method: "GET", headers: { accept: "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, }); @@ -32,6 +39,8 @@ export const postFetch = async (url: string, params: RequestParams) => { method: "POST", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -47,6 +56,8 @@ export const putFetch = async (url: string, params: RequestParams) => { method: "PUT", headers: { "Content-Type": "application/json", + [AUTH_USER_ID]: tokenId ?? "", + [CLAIM_NAME]: Authorization ?? "", }, body: JSON.stringify(params), }); @@ -62,6 +73,8 @@ export const deleteFetch = async (url: string, params: RequestParams) => { 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/dm/next.config.ts b/apps/dm/next.config.ts index 48fc8dacb..bc21e16a4 100644 --- a/apps/dm/next.config.ts +++ b/apps/dm/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const subDomain = - process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:3000"; + process.env.NEXT_PUBLIC_PROXY_HOST || "http://localhost:3000"; const nextConfig: NextConfig = { output: "standalone", @@ -22,8 +22,16 @@ const nextConfig: NextConfig = { ], afterFiles: [ { - source: "/pst/:path*", - destination: `${subDomain}/pst/:path*`, + source: "/sewerage-service/:path*", + destination: `${subDomain}/sewerage-service/:path*`, + }, + { + source: "/reuse-service/:path*", + destination: `${subDomain}/reuse-service/:path*`, + }, + { + source: "/operation-service/:path*", + destination: `${subDomain}/operation-service/:path*`, }, ], fallback: [], diff --git a/apps/dm/src/shared/api/client.ts b/apps/dm/src/shared/api/client.ts new file mode 100644 index 000000000..cf37e6671 --- /dev/null +++ b/apps/dm/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/dm/src/shared/api/index.ts b/apps/dm/src/shared/api/index.ts new file mode 100644 index 000000000..b7b9d6121 --- /dev/null +++ b/apps/dm/src/shared/api/index.ts @@ -0,0 +1 @@ +export { client } from "./client"; diff --git a/apps/dm/src/shared/lib/index.ts b/apps/dm/src/shared/lib/index.ts new file mode 100644 index 000000000..a5679f1b5 --- /dev/null +++ b/apps/dm/src/shared/lib/index.ts @@ -0,0 +1,8 @@ +export { getCookie, setCookie, removeCookie } from "./cookie"; +// export { useMutationWrapper } from "./use-mutation-wrapper"; +// export { useAlertStore } from "./use-alert-store"; +// export { useDataTable } from "./use-data-table"; +// export { useMenuStore } from "./use-menu-store"; +// export { useUserStore } from "./use-user-store"; +// export { usePrompt } from "./use-prompt"; +// export { useSelectPrompt } from "./use-select-prompt"; diff --git a/apps/fo/.env.development b/apps/fo/.env.development index de81dde66..ffaf99feb 100644 --- a/apps/fo/.env.development +++ b/apps/fo/.env.development @@ -1,18 +1,17 @@ +# 공단 개발 NEXT_PUBLIC_SITE_ID = FO -NEXT_PUBLIC_PORT = 11300 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 -NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -#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_PORT = 3000 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_PROXY_HOST = https://devlarva.hasuon.or.kr +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 +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/.env.local b/apps/fo/.env.local index 2e937af0c..1838377df 100644 --- a/apps/fo/.env.local +++ b/apps/fo/.env.local @@ -1,18 +1,19 @@ NEXT_PUBLIC_SITE_ID = FO 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://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_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_PROXY_HOST = http://localhost:3000 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082 +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 = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver 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.mindone b/apps/fo/.env.mindone new file mode 100644 index 000000000..74d765a48 --- /dev/null +++ b/apps/fo/.env.mindone @@ -0,0 +1,14 @@ +NEXT_PUBLIC_SITE_ID = FO +NEXT_PUBLIC_PORT = 11300 +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_PROXY_HOST = http://con.mind-one.co.kr:11300 +NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 +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 = https://hasudo.geo.rsonesoft.com/geoserver +NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +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 \ No newline at end of file diff --git a/apps/fo/.env.production b/apps/fo/.env.production index fbc643258..3f5cf8bea 100644 --- a/apps/fo/.env.production +++ b/apps/fo/.env.production @@ -1,32 +1,17 @@ -# NEXT_PUBLIC_SITE_ID = FO -# NEXT_PUBLIC_PORT = 11300 -# NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300 -# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -# #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_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_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API - +# 공단 운영 NEXT_PUBLIC_SITE_ID = FO NEXT_PUBLIC_PORT = 3000 +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_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/api/v1 +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 -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 = 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 \ No newline at end of file +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/page.tsx b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx index e44420eaa..775dd1a45 100644 --- a/apps/fo/app/pages/(with-layout)/processFacility/page.tsx +++ b/apps/fo/app/pages/(with-layout)/processFacility/page.tsx @@ -210,13 +210,13 @@ export default function ProcessFacilitySearch() { const cellClick = (index: number) => { const facility = processFacilityList[index]; if (facility) { - const fctCpc = String(facility.fctCpc).replace(/,/g, "").trim(); + 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(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..8f77aa8af 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/model/api-interface.ts b/apps/fo/src/shared/model/api-interface.ts new file mode 100644 index 000000000..8218bfb0d --- /dev/null +++ b/apps/fo/src/shared/model/api-interface.ts @@ -0,0 +1,34 @@ +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +export interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +export interface PageableParams { + page?: number; + size?: number; + sort?: string; +} diff --git a/apps/fo/src/shared/model/index.ts b/apps/fo/src/shared/model/index.ts new file mode 100644 index 000000000..cf589d200 --- /dev/null +++ b/apps/fo/src/shared/model/index.ts @@ -0,0 +1 @@ +export * from "./api-interface"; 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..f743defbe --- /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.development b/apps/sn/.env.development index 29d633c9a..89dac3a16 100644 --- a/apps/sn/.env.development +++ b/apps/sn/.env.development @@ -1,14 +1,13 @@ NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 3000 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_PORT = 11302 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11302 -NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 -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_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g -NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com -NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083 \ No newline at end of file +NEXT_PUBLIC_PROXY_HOST = https://devlion.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 +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_SEWERAGE_API_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/sn/.env.mindone b/apps/sn/.env.mindone new file mode 100644 index 000000000..f5212f6bb --- /dev/null +++ b/apps/sn/.env.mindone @@ -0,0 +1,13 @@ +NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 11302 +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_PROXY_HOST = http://con.mind-one.co.kr:11302 +NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000 +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_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 +NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com \ No newline at end of file diff --git a/apps/sn/.env.production b/apps/sn/.env.production index c978045fd..08375c55f 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -1,14 +1,14 @@ NEXT_PUBLIC_SITE_ID = SN +NEXT_PUBLIC_PORT = 3000 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_PORT = 3000 NEXT_PUBLIC_PROXY_HOST = https://lion.hasuon.or.kr +NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 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_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login \ No newline at end of file +NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-service +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login diff --git a/apps/sn/src/shared/model/api-interface.ts b/apps/sn/src/shared/model/api-interface.ts new file mode 100644 index 000000000..8218bfb0d --- /dev/null +++ b/apps/sn/src/shared/model/api-interface.ts @@ -0,0 +1,34 @@ +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +export interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +export interface PageableParams { + page?: number; + size?: number; + sort?: string; +} diff --git a/apps/sn/src/shared/model/index.ts b/apps/sn/src/shared/model/index.ts new file mode 100644 index 000000000..cf589d200 --- /dev/null +++ b/apps/sn/src/shared/model/index.ts @@ -0,0 +1 @@ +export * from "./api-interface"; diff --git a/apps/wr/.env.development b/apps/wr/.env.development index 769d229e2..34ea78105 100644 --- a/apps/wr/.env.development +++ b/apps/wr/.env.development @@ -1,31 +1,20 @@ -# NEXT_PUBLIC_SITE_ID = WR -# NEXT_PUBLIC_PORT = 11301 -# NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 -# NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 -# NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -# NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -# NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -# 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_SITE_ID = WR -NEXT_PUBLIC_PORT = 11301 -NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11301 - 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_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload -NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_PORT = 3000 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_PROXY_HOST = https://devtiger.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 +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_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= \ No newline at end of file diff --git a/apps/wr/.env.local b/apps/wr/.env.local index df38635ea..e3c0e1daf 100644 --- a/apps/wr/.env.local +++ b/apps/wr/.env.local @@ -1,29 +1,21 @@ NEXT_PUBLIC_SITE_ID = WR NEXT_PUBLIC_PORT = 3001 -NEXT_PUBLIC_PROXY_HOST = http://localhost:3001 -NEXT_PUBLIC_WR_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 -NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 -NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload 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_PROXY_HOST = http://localhost:3001 +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 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver -# NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver +NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g - - - -NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PUBLIC_FINDADRESS_URL = https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= - - -NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 -#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정 - +REACT_EDITOR = atom diff --git a/apps/wr/.env.mindone b/apps/wr/.env.mindone new file mode 100644 index 000000000..f7d7445ce --- /dev/null +++ b/apps/wr/.env.mindone @@ -0,0 +1,19 @@ +NEXT_PUBLIC_SITE_ID = WR +NEXT_PUBLIC_PORT = 11301 +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_PROXY_HOST = http://con.mind-one.co.kr:11301 +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 +NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000 +NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload +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_WR_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1 +NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API +NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g + +NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do +NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/wr/.env.production b/apps/wr/.env.production index 67ff9784b..7086521c6 100644 --- a/apps/wr/.env.production +++ b/apps/wr/.env.production @@ -1,5 +1,10 @@ +# 공단 운영 NEXT_PUBLIC_SITE_ID = WR NEXT_PUBLIC_PORT = 3000 +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_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 @@ -7,19 +12,9 @@ NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:80 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 -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 = 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_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do -NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= - - - - - +NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= \ No newline at end of file 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..1b6370cc0 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -18,6 +18,9 @@ export function middleware(req: NextRequest) { console.log(`[Middleware] Request URL: ${req.url}`); const searchParams = req.nextUrl.searchParams; + // if (pathname === "/") + // return NextResponse.redirect(new URL("/map/mainMap", req.url)); + if (pathname.startsWith("/server")) { const decodedParams = Array.from(searchParams.entries()) .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) @@ -29,10 +32,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..0497e1f52 --- /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..94d39b6e4 --- /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..f659f58af --- /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 "@/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/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..a4cd666d9 --- /dev/null +++ b/apps/wr/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/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..839b9a7a4 --- /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 "@/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/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/model/api-interface.ts b/apps/wr/src/shared/model/api-interface.ts new file mode 100644 index 000000000..8218bfb0d --- /dev/null +++ b/apps/wr/src/shared/model/api-interface.ts @@ -0,0 +1,34 @@ +interface SortConfig { + empty: boolean; + sorted: boolean; + unsorted: boolean; +} + +interface PageableConfig { + sort: SortConfig; + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + unpaged: boolean; +} + +export interface PageableList { + content: T[]; + pageable: PageableConfig; + sort: SortConfig; + totalPages: number; + totalElements: number; + number: number; + numberOfElements: number; + size: number; + first: boolean; + last: boolean; + empty: boolean; +} + +export interface PageableParams { + page?: number; + size?: number; + sort?: string; +} diff --git a/apps/wr/src/shared/model/index.ts b/apps/wr/src/shared/model/index.ts new file mode 100644 index 000000000..cf589d200 --- /dev/null +++ b/apps/wr/src/shared/model/index.ts @@ -0,0 +1 @@ +export * from "./api-interface"; 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..f743defbe --- /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 };