From 5f302a5711be5b67afa26523d9889217891e3e7f Mon Sep 17 00:00:00 2001 From: leedj312 Date: Thu, 17 Jul 2025 15:57:20 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wr/app/store/common/login/index.tsx | 32 +++++++ apps/wr/app/store/store.tsx | 3 + apps/wr/components/lib/api/index.ts | 18 ++++ apps/wr/components/lib/func/prfData.tsx | 88 +++++++++++++++++++ apps/wr/middleware.ts | 54 ++++++++---- apps/wr/package.json | 1 + .../wr/src/entities/auth/api/login-service.ts | 80 +++++++++++++++++ apps/wr/src/entities/auth/index.ts | 2 + .../wr/src/entities/auth/lib/auth-wrapper.tsx | 45 ++++++++++ .../wr/src/entities/auth/lib/token-manager.ts | 59 +++++++++++++ .../entities/auth/model/login-interface.ts | 9 ++ apps/wr/src/shared/config/env.ts | 1 + apps/wr/src/shared/lib/cookie.ts | 31 +++++++ 13 files changed, 407 insertions(+), 16 deletions(-) create mode 100644 apps/wr/app/store/common/login/index.tsx create mode 100644 apps/wr/components/lib/func/prfData.tsx create mode 100644 apps/wr/src/entities/auth/api/login-service.ts create mode 100644 apps/wr/src/entities/auth/index.ts create mode 100644 apps/wr/src/entities/auth/lib/auth-wrapper.tsx create mode 100644 apps/wr/src/entities/auth/lib/token-manager.ts create mode 100644 apps/wr/src/entities/auth/model/login-interface.ts create mode 100644 apps/wr/src/shared/lib/cookie.ts diff --git a/apps/wr/app/store/common/login/index.tsx b/apps/wr/app/store/common/login/index.tsx new file mode 100644 index 000000000..2ddd5b495 --- /dev/null +++ b/apps/wr/app/store/common/login/index.tsx @@ -0,0 +1,32 @@ +import { cmsPostFectch } from "@/components/lib/api"; + +export const isLogin = async () => { + const headers = { + accept: "application/json", + "Content-Type": "application/json", + "X-Site-Id": "WR", + "Access-Control-Expose-Headers" : ["Authorization","refresh-token","token-id"] + }; + const param = { userId: "tuser10003", password: "test1234!" }; + await cmsPostFectch("/user-service/login",param,headers); +}; + +// export type LoginState = { +// login: {} as any; +// }; + +// export type loginType = LoginState & LoginActions; + +// export const loginState: LoginState = { +// login: {}, +// }; + +// export type LoginActions = { +// isLogin: () => void; +// }; + +// export const loginActionExport: (set: any) => LoginActions = (set: any) => { +// return { +// isLogin: async () => {}, +// }; +// }; diff --git a/apps/wr/app/store/store.tsx b/apps/wr/app/store/store.tsx index 63a4eee13..b61477862 100644 --- a/apps/wr/app/store/store.tsx +++ b/apps/wr/app/store/store.tsx @@ -96,6 +96,8 @@ import { } from "./ruseFcltMng/fcltSttn/rswtSttn"; import { rainStatActionsExport, rainStatExport, type rainStatState, type rainStatType } from "./stats/rainStat"; import { rswtStatActionsExport, rswtStatExport, type rswtStatState, type rswtStatType } from "./stats/rswtStat"; +//import { isLogin } from "./common/login"; +import { LoginApi } from "@/src/entities/auth"; export type WrStore = comCodeType & pubFacType & @@ -188,6 +190,7 @@ export const StoreZus = ( getMiddleRegionCd(set); getUnitWaterShedCd(set); getEnvPartCd(set); + LoginApi.login({userId: "tuser10003", password: "test1234!"}) return { ...initState, ...comFcltSearchAction(set), diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts index 8772959e1..7823c2ddd 100644 --- a/apps/wr/components/lib/api/index.ts +++ b/apps/wr/components/lib/api/index.ts @@ -108,6 +108,24 @@ export const postFectch = async (url: string, params: any, type?: string) => { return await response.json(); }; +export const cmsPostFectch = async (url: string, params: any,headers:any) => { + const response = fetch(url, { + method: "POST", + headers:headers , + body: JSON.stringify(params), + }).then((res) => + { + console.log(' res ' , res) + console.log(' access-token ' , res.headers.get('access-token')) + console.log(' refresh-token ' , res.headers.get('refresh-token')) + console.log(' token-id ' , res.headers.get('token-id')) + + }) + + + return await response; +}; + export const putFectch = async (url: string, params: any,type?:string) => { const div = type === fetchType.COMMON diff --git a/apps/wr/components/lib/func/prfData.tsx b/apps/wr/components/lib/func/prfData.tsx new file mode 100644 index 000000000..08cc93444 --- /dev/null +++ b/apps/wr/components/lib/func/prfData.tsx @@ -0,0 +1,88 @@ +import { + initValInUsage, + initValOutUsage, + subTotalObj, + totalObj, + type PrfType, + type PrfUsageSummary, +} from "@/app/store/prfmncReg/pbnstPrfmncReg/type"; + +export const getTablePrfData = (prfList: PrfType[]) => { + const inUsageData = structuredClone(initValInUsage); + const outUsageData = structuredClone(initValOutUsage); + const inSubtotal = structuredClone(subTotalObj); + const outSubtotal = structuredClone(subTotalObj); + + if (prfList) { + prfList?.forEach((prfItem) => { + const month = Number(prfItem?.ym?.slice(4)); // "MM" + const monthKey = `${month}월`; + + inUsageData.forEach((inData: PrfUsageSummary) => { + const usageKey = inData.usage as keyof PrfType; + const value = (prfItem[usageKey] as number) ?? 0; + + (inData[monthKey as keyof PrfUsageSummary] as number) = value; + inData.total += value; + + if (inSubtotal&& usageKey !== "subtotal" as keyof PrfType) { + (inSubtotal[monthKey as keyof PrfUsageSummary] as number) = + ((inSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + + value; + inSubtotal.total += value; + } + }); + + outUsageData.forEach((outData: PrfUsageSummary) => { + const usageKey = outData.usage as keyof PrfType; + const value = (prfItem[usageKey] as number) ?? 0; // Type assertion for prfItem[usageKey] + + (outData[monthKey as keyof PrfUsageSummary] as number) = value; + outData.total += value; + + if (outSubtotal && usageKey !== "subtotal" as keyof PrfType) { + (outSubtotal [monthKey as keyof PrfUsageSummary] as number) = + ((outSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + value; + outSubtotal.total += value; + } // End of if (outSubtotal && usageKey !== "subtotal") + }); // End of outUsageData.forEach + }); + (inSubtotal as any)["구분"] = "장내용수"; + (outSubtotal as any)["구분"] = "장내용수"; + + + inUsageData.push(inSubtotal); + outUsageData.push(outSubtotal); + const usageTotalObj: { [key: string]: any } = structuredClone(totalObj); + for (const key in usageTotalObj) { + if (["usage", "gubun", "edit"].includes(key)) continue; + const val = + (Number((inSubtotal as any)[key]) || 0) + + (Number((outSubtotal as any)[key]) || 0); + + (usageTotalObj as any)[key] = val; + } + const tableData = [...inUsageData, ...outUsageData, usageTotalObj]; + + return { + inPrf: inUsageData, + outPrf: outUsageData, + totalPrf: tableData, + }; + } +}; + + + +// const usageKey = inData.usage as keyof PrfType; +// const value = (prfItem[usageKey] as number) ?? 0; + +// (inData[monthKey as keyof PrfUsageSummary] as number) = value; +// inData.total += value; + +// if (inSubtotal&& usageKey !== "subtotal" as keyof PrfType) { +// (inSubtotal[monthKey as keyof PrfUsageSummary] as number) = +// ((inSubtotal[monthKey as keyof PrfUsageSummary] as number) ?? 0) + +// value; +// inSubtotal.total += value; +// } \ No newline at end of file diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 9b0c8dc84..b3928e4ae 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { CS_URL } from "./components/lib/config"; -//import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; +import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; -const CLAIM_NAME = "Authorization"; -const AUTH_USER_ID = "token-id"; -const serverApiUrl = CS_URL; +// const CLAIM_NAME = "Authorization"; +// const AUTH_USER_ID = "token-id"; + const serverApiUrl = CS_URL; 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 || ""; @@ -14,28 +14,38 @@ export function middleware(req: NextRequest) { console.log(`[Middleware] Request URL: ${req.url}`); const searchParams = req.nextUrl.searchParams; + // if (pathname.startsWith("/server")) { + // const queryParams = searchParams.toString(); + // const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + // queryParams ? `?${queryParams}` : "" + // }`; + // console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); + // return NextResponse.rewrite(new URL(newUrl)); + // } + if (pathname.startsWith("/server")) { - const queryParams = searchParams.toString(); + const decodedParams = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${decodeURIComponent(value)}`) + .join("&"); + + console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server') const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ - queryParams ? `?${queryParams}` : "" + decodedParams ? `?${decodedParams}` : "" }`; - console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); return NextResponse.rewrite(new URL(newUrl)); } + if (pathname.startsWith("/attach-service")) { const requestHeaders = new Headers(req.headers); - const token = - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzQ4ODQ4NDkxfQ.ZiYpKxAMyyoPuUquxsWxfQbMBzYN4tsa7itZ49_OEsdGvbUUmKzrIV6Or7VVbEjtAqhh7W97nqB5EP6GRzFC9Q"; - const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0"; - //const token = req.cookies.get(CLAIM_NAME)?.value; - //const userId = req.cookies.get(AUTH_USER_ID)?.value; + // const token = + // "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww"; + // const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0"; + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; const newUrl = `${serverApiUrl}${pathname}`; - console.log("serverApiUrl ", serverApiUrl); - console.log("pathname ", pathname); //wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad - //http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -48,6 +58,18 @@ export function middleware(req: NextRequest) { }); } + if (pathname.startsWith("/user-service")) { + const requestHeaders = new Headers(req.headers); + const newUrl = `${serverApiUrl}${pathname}`; + + console.log( + `[Middleware] Rewriting from '/user-service' path to: ${newUrl}` + ); + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders + }); + } + if (pathname.startsWith("/geoserver")) { let queryParams = searchParams.toString(); const typename = searchParams.get("TYPENAME"); @@ -103,5 +125,5 @@ export function middleware(req: NextRequest) { } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"], + matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"], }; diff --git a/apps/wr/package.json b/apps/wr/package.json index 5507b08fa..a29dcd66b 100644 --- a/apps/wr/package.json +++ b/apps/wr/package.json @@ -17,6 +17,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next": "^25.3.2", + "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "moment": "^2.30.1", "next": "15.1.7", diff --git a/apps/wr/src/entities/auth/api/login-service.ts b/apps/wr/src/entities/auth/api/login-service.ts new file mode 100644 index 000000000..3cd387507 --- /dev/null +++ b/apps/wr/src/entities/auth/api/login-service.ts @@ -0,0 +1,80 @@ + +import type { LoginService, ILogin } from "../model/login-interface"; +import { tokenManager } from "../lib/token-manager"; +import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config"; + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise { + try { + console.log(' ENDPOINT.USER_SERVICE.LOGIN ', ENDPOINT.USER_SERVICE.LOGIN) + const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + // const data = response.headers.get(ACCESS_TOKEN); + const accessToken = response.headers.get(ACCESS_TOKEN); + const tokenId = response.headers.get(AUTH_USER_ID); + + console.log('accessToken ', accessToken) + console.log('tokenId ', tokenId) + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + tokenManager.setToken("site-id-key", SITE_ID); + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + + console.log(' login err ' , error) + throw new Error(`Login failed: ${error}`); + } + } + + + public async silentRefresh(): Promise { + console.log(' silentRefresh ') + + try { + const response = await fetch(PROXY_HOST+ + `${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { method: "PUT" }, + ); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/wr/src/entities/auth/index.ts b/apps/wr/src/entities/auth/index.ts new file mode 100644 index 000000000..9dc9dd368 --- /dev/null +++ b/apps/wr/src/entities/auth/index.ts @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +//export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..678ff50d5 --- /dev/null +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -0,0 +1,45 @@ +// "use client"; + +// import { useEffect } from "react"; +// import { useRouter } from "next/navigation"; +// import { LoginApi } from "../api/login-service"; + +// const AuthWrapper = ({ children }: { children: React.ReactNode }) => { +// const router = useRouter(); +// const { userInfo, setUserInfo } = useUserStore((state) => state); + +// async function authCheck() { +// const userId = getCookie(AUTH_USER_ID); +// if (!userId) { +// throw new Error("Missing required authentication tokens"); +// } + +// const response = await client.get( +// `${ENDPOINT.USER_SERVICE.USERS}/info/me`, +// ); +// if (!userInfo) setUserInfo(response.data); +// } + +// useEffect(() => { +// (async () => { +// try { +// await authCheck(); +// } catch (error) { +// console.error(error); + +// try { +// await LoginApi.silentRefresh(); +// await authCheck(); +// } catch (refreshError) { +// console.error("Token refresh failed:", refreshError); +// setUserInfo(null); +// router.push("/auth/login"); +// } +// } +// })(); +// }, []); + +// return <>{children}; +// }; + +// export { AuthWrapper }; diff --git a/apps/wr/src/entities/auth/lib/token-manager.ts b/apps/wr/src/entities/auth/lib/token-manager.ts new file mode 100644 index 000000000..40d32f33e --- /dev/null +++ b/apps/wr/src/entities/auth/lib/token-manager.ts @@ -0,0 +1,59 @@ +import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config"; +import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie"; + +export class TokenManager { + private static instance: TokenManager; + private refreshTimer: NodeJS.Timeout | null = null; + + private constructor() {} + + public static getInstance(): TokenManager { + if (!TokenManager.instance) { + TokenManager.instance = new TokenManager(); + } + return TokenManager.instance; + } + + public getToken(key: string): string | undefined { + return getCookie(key); + } + + public setToken(key: string, value: string): void { + setCookie(key, value, { + secure: window.location.protocol === "https:", + sameSite: "lax", + }); + } + + public removeToken(key: string): void { + removeCookie(key); + } + + public setupRefreshTimer( + callback: () => Promise, + expirationTime: number, + ): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + this.refreshTimer = setTimeout(() => { + callback().catch((error) => { + console.error("Silent refresh failed:", error); + this.clearAuthToken(); + }); + }, expirationTime - 60000); + } + + public clearAuthToken(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + this.removeToken(CLAIM_NAME); + this.removeToken(AUTH_USER_ID); + } +} + +export const tokenManager = TokenManager.getInstance(); diff --git a/apps/wr/src/entities/auth/model/login-interface.ts b/apps/wr/src/entities/auth/model/login-interface.ts new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/wr/src/entities/auth/model/login-interface.ts @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise; + silentRefresh(): Promise; +} diff --git a/apps/wr/src/shared/config/env.ts b/apps/wr/src/shared/config/env.ts index 7fe0226ac..49cc16b3a 100644 --- a/apps/wr/src/shared/config/env.ts +++ b/apps/wr/src/shared/config/env.ts @@ -12,3 +12,4 @@ export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; export const SERVER_SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL; + diff --git a/apps/wr/src/shared/lib/cookie.ts b/apps/wr/src/shared/lib/cookie.ts new file mode 100644 index 000000000..4bc0fe142 --- /dev/null +++ b/apps/wr/src/shared/lib/cookie.ts @@ -0,0 +1,31 @@ + +import Cookies from "js-cookie"; + + +export function getCookie(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + return Cookies.get(name); +} + +export function setCookie( + name: string, + value: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.set(name, value, options); +} + +export function removeCookie( + name: string, + options?: Cookies.CookieAttributes, +): void { + if (typeof window === "undefined") { + return; + } + Cookies.remove(name, options); +}