From d975e8fed0acbdaba5bdfd943336bfb1a0f8b24f Mon Sep 17 00:00:00 2001 From: yschoi Date: Mon, 26 May 2025 10:32:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/api/proxy/[...url]/route.ts | 100 +++++++++++++++ .../components/lib/auth/api/login-service.tsx | 105 ++++++++++++++++ apps/sn/app/components/lib/auth/index.tsx | 2 + .../components/lib/auth/lib/auth-wrapper.tsx | 87 +++++++++++++ .../components/lib/auth/lib/token-manager.tsx | 61 +++++++++ .../lib/auth/model/login-interface.tsx | 9 ++ apps/sn/app/layout.tsx | 20 +-- apps/sn/app/store/index.tsx | 46 +------ apps/sn/app/store/oper/index.tsx | 76 ------------ apps/sn/app/store/store-provider.tsx | 27 ++++ apps/sn/app/store/store.tsx | 14 --- apps/sn/app/store/table.tsx | 116 ------------------ apps/sn/app/store/user-store.tsx | 74 +++++++++++ apps/sn/middleware.ts | 19 ++- 14 files changed, 494 insertions(+), 262 deletions(-) create mode 100644 apps/sn/app/api/proxy/[...url]/route.ts create mode 100644 apps/sn/app/components/lib/auth/api/login-service.tsx create mode 100644 apps/sn/app/components/lib/auth/index.tsx create mode 100644 apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx create mode 100644 apps/sn/app/components/lib/auth/lib/token-manager.tsx create mode 100644 apps/sn/app/components/lib/auth/model/login-interface.tsx delete mode 100644 apps/sn/app/store/oper/index.tsx create mode 100644 apps/sn/app/store/store-provider.tsx delete mode 100644 apps/sn/app/store/store.tsx delete mode 100644 apps/sn/app/store/table.tsx create mode 100644 apps/sn/app/store/user-store.tsx diff --git a/apps/sn/app/api/proxy/[...url]/route.ts b/apps/sn/app/api/proxy/[...url]/route.ts new file mode 100644 index 000000000..791562e27 --- /dev/null +++ b/apps/sn/app/api/proxy/[...url]/route.ts @@ -0,0 +1,100 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; +const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ""; +const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "SN"; + +async function handler( + request: Request, + params: { url: string[] }, + method: "GET" | "POST" | "PUT" | "DELETE", +) { + const cookieStore = await cookies(); + const headers: Record = { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + }; + let body = {}; + const isRefreshHandler = params.url.includes("refresh"); + + // silent refresh + if (isRefreshHandler) { + const refreshToken = cookieStore.get(REFRESH_TOKEN); + headers[CLAIM_NAME] = refreshToken?.value ?? ""; + + if (!headers[CLAIM_NAME]) { + return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); + } + } + + try { + if (!isRefreshHandler) { + body = await request.json(); + } + + const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, { + method, + headers, + body: JSON.stringify(body), + }); + + if (!result.ok) { + console.log("Invalid Credentials "); + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } + + const accessToken = result.headers.get(ACCESS_TOKEN); + const refreshToken = result.headers.get(REFRESH_TOKEN); + const userId = result.headers.get(AUTH_USER_ID); + + if (accessToken && userId) { + const response = NextResponse.json({ + [ACCESS_TOKEN]: accessToken, + [AUTH_USER_ID]: userId, + }); + + if (refreshToken) { + cookieStore.set(REFRESH_TOKEN, refreshToken, { + httpOnly: true, + sameSite: "lax", + path: "/", + }); + } + + return response; + } + + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } catch (error) { + return NextResponse.json( + { message: "Server Error", error }, + { status: 500 }, + ); + } +} + +export async function POST( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "POST"); +} + +export async function PUT( + request: Request, + props: { params: Promise<{ url: string[] }> }, +) { + const params = await props.params; + return handler(request, params, "PUT"); +} diff --git a/apps/sn/app/components/lib/auth/api/login-service.tsx b/apps/sn/app/components/lib/auth/api/login-service.tsx new file mode 100644 index 000000000..44b7da488 --- /dev/null +++ b/apps/sn/app/components/lib/auth/api/login-service.tsx @@ -0,0 +1,105 @@ +import { AxiosError } from "axios"; +import { tokenManager } from "../lib/token-manager"; + +const ENDPOINT = { + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + }, +}; +const JWT_EXPIRATION_TIME = 1800000; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; + + +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise; + silentRefresh(): Promise; +} + +export class LoginApiService implements LoginService { + private static instance: LoginApiService; + + private constructor() {} + + public static getInstance(): LoginApiService { + if (!LoginApiService.instance) { + LoginApiService.instance = new LoginApiService(); + } + return LoginApiService.instance; + } + + public async login(payload: ILogin): Promise { + try { + const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { + method: "POST", + body: JSON.stringify({ + userId: payload.userId, + password: payload.password, + }), + }); + + const data = await response.json(); + const accessToken = data[ACCESS_TOKEN]; + const tokenId = data[AUTH_USER_ID]; + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + + tokenManager.setupRefreshTimer( + () => this.silentRefresh(), + JWT_EXPIRATION_TIME, + ); + } catch (error) { + if (error instanceof AxiosError) { + throw new Error(`Login failed: ${error.response?.data?.message}`); + } + throw new Error(`Login failed: ${error}`); + } + } + + public async silentRefresh(): Promise { + try { + const response = await fetch( + `/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, + { + method: "PUT", + }, + ); + + const data = await response.json(); + const accessToken = response.headers.get("access-token"); + const tokenId = response.headers.get("token-id"); + + if (!accessToken || !tokenId) { + throw new Error("Missing required authentication tokens"); + } + + tokenManager.setToken(CLAIM_NAME, accessToken); + tokenManager.setToken(AUTH_USER_ID, tokenId); + } catch (error) { + tokenManager.clearAuthToken(); + if (error instanceof AxiosError) { + throw new Error( + `Silent refresh failed: ${error.response?.data?.message}`, + ); + } + throw new Error(`Silent refresh failed: ${error}`); + } + } +} + +export const LoginApi = LoginApiService.getInstance(); diff --git a/apps/sn/app/components/lib/auth/index.tsx b/apps/sn/app/components/lib/auth/index.tsx new file mode 100644 index 000000000..97e142225 --- /dev/null +++ b/apps/sn/app/components/lib/auth/index.tsx @@ -0,0 +1,2 @@ +export { LoginApi } from "./api/login-service"; +export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx new file mode 100644 index 000000000..0b72cc768 --- /dev/null +++ b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import axios from "axios"; +import Cookies from "js-cookie"; +import { useUserStore, type IUserInfo } from "@store"; +import { getCookie } from "@utils"; +import { LoginApi } from "../api/login-service"; +import { tokenManager } from "../lib/token-manager"; + +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; + +const ENDPOINT = { + USER_SERVICE: { + LOGIN: "/user-service/login", + USERS: "/user-service/api/v1/users", + SILENT_REFRESH: "/user-service/api/v1/users/token/refresh", + AUTHORIZATIONS: "/user-service/api/v1/authorizations", + ROLE_AUTHORIZATIONS: "/user-service/api/v1/role-authorizations", + }, +}; + +const AuthWrapper = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const pathname = usePathname(); // 현재 경로 + const { userInfo, setUserInfo } = useUserStore((state) => state); + + async function authCheck() { + const checkToken = getCookie(AUTH_USER_ID); + + + if (!checkToken) { + //TODO 나중에 수정해야.... + const userId = "admin1gmail"; + const password = "test1234!"; + await LoginApi.login({ userId, password }); + // window.location.href = "http://dev.onasys.co.kr/auth/login"; + // throw new Error("Missing required authentication tokens"); + } + + const accessToken = tokenManager.getToken(CLAIM_NAME); + const tokenId = tokenManager.getToken(AUTH_USER_ID); + + const response = await axios.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + { + withCredentials: true, + headers: { + [CLAIM_NAME]: accessToken, + [AUTH_USER_ID]: tokenId ?? "", + }, + }, + ); + + if (!userInfo) setUserInfo(response.data); + } + + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error("Auth check failed:", error); + + try { + await LoginApi.silentRefresh(); + await authCheck(); + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + setUserInfo(null); + + // TODO 운영에 반영 할때 수정해야..... + // 현재 경로가 로그인 페이지가 아닐 때만 redirect + // if (!pathname.startsWith("/auth/login")) { + // window.location.href = "http://dev.onasys.co.kr/auth/login"; + // } + } + } + })(); + }, [pathname]); // pathname도 의존성에 포함 + + return <>{children}; +}; + +export { AuthWrapper }; diff --git a/apps/sn/app/components/lib/auth/lib/token-manager.tsx b/apps/sn/app/components/lib/auth/lib/token-manager.tsx new file mode 100644 index 000000000..9feab4af0 --- /dev/null +++ b/apps/sn/app/components/lib/auth/lib/token-manager.tsx @@ -0,0 +1,61 @@ +import { getCookie, setCookie, removeCookie } from "@utils"; + +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; + +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/sn/app/components/lib/auth/model/login-interface.tsx b/apps/sn/app/components/lib/auth/model/login-interface.tsx new file mode 100644 index 000000000..5aaf53044 --- /dev/null +++ b/apps/sn/app/components/lib/auth/model/login-interface.tsx @@ -0,0 +1,9 @@ +export interface ILogin { + userId: string; + password: string; +} + +export interface LoginService { + login(payload: ILogin): Promise; + silentRefresh(): Promise; +} diff --git a/apps/sn/app/layout.tsx b/apps/sn/app/layout.tsx index 1d3e231f7..f983b20bc 100644 --- a/apps/sn/app/layout.tsx +++ b/apps/sn/app/layout.tsx @@ -1,9 +1,10 @@ -import './styles/base.css' -import './styles/components.css' +import "./styles/base.css"; +import "./styles/components.css"; import type { Metadata } from "next"; import { cookies } from "next/headers"; -import { StoreProvider } from './store' -import { ClientLayout } from './clientLayout' +import { StoreProvider } from "./store/store-provider"; +import { AuthWrapper } from "./components/lib/auth"; +import { ClientLayout } from "./clientLayout"; export const metadata: Metadata = { title: "디지털 하수관로", @@ -20,18 +21,17 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const theme = (await cookies()).get("site-theme-key"); return ( - - - {children} - + + + {children} + - ) + ); } diff --git a/apps/sn/app/store/index.tsx b/apps/sn/app/store/index.tsx index d917c17f9..a9fc1288d 100644 --- a/apps/sn/app/store/index.tsx +++ b/apps/sn/app/store/index.tsx @@ -1,45 +1 @@ -'use client' - -import { type ReactNode, createContext, useContext, useRef } from 'react' -import { useStore } from 'zustand' -import { type SnStore, StoreZus } from './store' - - -export type StoreApi = ReturnType - -export const StoreContext = createContext( - undefined, -) - -export interface StoreProviderProps { - children: ReactNode -} - -export const StoreProvider = ({ - children, -}: StoreProviderProps) => { - const storeRef = useRef(null) - - if (!storeRef.current) { - storeRef.current = StoreZus() - } - - return ( - - {children} - - ) -} - -export const useSnStore = ( - //selector: (store: ReturnType) => T, - selector: (store: SnStore) => T, -): T => { - const storeContext = useContext(StoreContext) - - if (!storeContext) { - throw new Error(`useStore must be used within StoreProvider`) - } - - return useStore(storeContext, selector) -} \ No newline at end of file +export { useUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/sn/app/store/oper/index.tsx b/apps/sn/app/store/oper/index.tsx deleted file mode 100644 index 7fef98463..000000000 --- a/apps/sn/app/store/oper/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -export type TableUpperProps = { - id: string; - title: string; - upName?: string; - upSequnce?: number; -} -type PublicReuseFacility = { - sido: string, - sigungo: string, - facilityName: string, - location: string, - locationgubun: string, - facilityCapacity: string, - yn?: string, - authoDay?: string, - area?: string, - reuse?: string -} -const upHeadList:TableUpperProps[] = [ - { id: "sido", title: "자산ID"}, - { id: "sigungo", title: "설비시설"}, - { id: "facilityName", title: "주소"}, - { id: "location", title: "자산코드"}, - { id: "locationgubun", title: "자산명"}, - { id: "facilityCapacity", title: "용도"}, - { id: "yn", title: '신설/교체'}, - { id: "authoDay", title: '처리구역'}, - { id: "area", title: '내용연수' }, - { id: "reuse", title: '취득금액' }, -]; - -const publicReuseFacilityUper:PublicReuseFacility[] = [ - { - sido: "1048967", - sigungo: "관로", - facilityName: "양평군 양평읍 백안리 25-5전", - location: "S-T-S-2018000675", - locationgubun: "2018000675", - facilityCapacity: "오수관", - yn: "신설", - authoDay: "양평하수처리구역", - area: "30", - reuse: "28,313,220" - } - - -] - -export type tableState = { - isInit: boolean; - upHeadList: TableUpperProps[]; - publicReuseFacilityUper: PublicReuseFacility[]; -} -type tableActions = { - decrementList: () => void -} - -export type tableType = tableState & tableActions; - -export const tableStateExport:tableState= { - isInit:false, - upHeadList: upHeadList, - publicReuseFacilityUper: publicReuseFacilityUper, -} -export const tableActionsExport:(set:any)=>tableActions=(set: any) => { - return { - decrementList:()=>set( - (state:tableState) => { - return ( - { - upHeadList: state.upHeadList, - publicReuseFacilityUper: state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1) - })} - ) - } -} diff --git a/apps/sn/app/store/store-provider.tsx b/apps/sn/app/store/store-provider.tsx new file mode 100644 index 000000000..8a2e8347e --- /dev/null +++ b/apps/sn/app/store/store-provider.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { createContext, useRef } from "react"; +import { createUserStore } from "../store/user-store"; + +type StoreContextType = { + userStore: ReturnType; +}; + +export const StoreContext = createContext( + undefined, +); + +export const StoreProvider = ({ children }: { children: React.ReactNode }) => { + const storeRef = useRef(null); + if (storeRef.current === null) { + storeRef.current = { + userStore: createUserStore(), + }; + } + + return ( + + {children} + + ); +}; diff --git a/apps/sn/app/store/store.tsx b/apps/sn/app/store/store.tsx deleted file mode 100644 index 9b489fabf..000000000 --- a/apps/sn/app/store/store.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createStore } from 'zustand/vanilla'; -import { tableActionsExport, tableStateExport, type tableState, type tableType } from './table'; - -export type SnStore = tableType; -//export type SnStore = ReturnType["getState"]; -export const StoreZus = (initState: tableState = tableStateExport) => { - return createStore()((set) => { - return { - ...initState, - ...tableActionsExport(set) - } - }) -} -//store 등록하는 부분 diff --git a/apps/sn/app/store/table.tsx b/apps/sn/app/store/table.tsx deleted file mode 100644 index 30d884de4..000000000 --- a/apps/sn/app/store/table.tsx +++ /dev/null @@ -1,116 +0,0 @@ -export type TableUpperProps = { - id: string, - title: string, - upSequnce?: number, - upName?: string -} -type PublicReuseFacility = { - sido: string, - sigungo: string, - facilityName: string, - location: string, - locationgubun: string, - facilityCapacity: string, - yn?: string, - authoDay?: string, - area?: string, - reuse?: string -} -const upHeadList:TableUpperProps[] = [ - { id: "sido", title: "자산ID"}, - { id: "sigungo", title: "설비시설"}, - { id: "facilityName", title: "주소"}, - { id: "location", title: "자산코드"}, - { id: "locationgubun", title: "자산명"}, - { id: "facilityCapacity", title: "용도"}, - { id: "yn", title: '신설/교체'}, - { id: "authoDay", title: '처리구역'}, - { id: "area", title: '내용연수' }, - { id: "reuse", title: '취득금액' }, -]; - -const publicReuseFacilityUper:PublicReuseFacility[] = [ - { - sido: "1048967", - sigungo: "관로", - facilityName: "양평군 양평읍 백안리 25-5전", - location: "S-T-S-2018000675", - locationgubun: "2018000675", - facilityCapacity: "오수관", - yn: "신설", - authoDay: "양평하수처리구역", - area: "30", - reuse: "28,313,220" - } - -] - -export type tableState = { - isInit: boolean; - upHeadList: TableUpperProps[]; - publicReuseFacilityUper: PublicReuseFacility[]; -} -type tableActions = { - setHeadList: (newHeadList: TableUpperProps[]) => void - setTableData: (newTableDataList: PublicReuseFacility[]) => void - decrementList: () => void - addRow: (newTableData: PublicReuseFacility) => void -} - -export type tableType = tableState & tableActions; - -export const tableStateExport:tableState= { - isInit:false, - upHeadList: [ - { id: "sido", title: "자산ID"}, - { id: "sigungo", title: "설비시설"}, - { id: "facilityName", title: "주소"}, - { id: "location", title: "자산코드"}, - { id: "locationgubun", title: "자산명"}, - { id: "facilityCapacity", title: "용도"}, - { id: "yn", title: '신설/교체'}, - { id: "authoDay", title: '처리구역'}, - { id: "area", title: '내용연수' }, - { id: "reuse", title: '취득금액' }, - ], - publicReuseFacilityUper: publicReuseFacilityUper, -} -export const tableActionsExport:(set:any)=>tableActions=(set: any) => { - return { - // 마지막 데이터 삭제 - decrementList: ()=>set( - (state: tableState) => { - return ( - { - ...state, - upHeadList: state.upHeadList, - publicReuseFacilityUper: state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1) - })} - ), - // 데이터 추가 - addRow: (newTableData: PublicReuseFacility)=>set( - (state: tableState) => { - console.log('asdfasdfadsf') - return ( - { - ...state, - upHeadList: state.upHeadList, - publicReuseFacilityUper: [...state.publicReuseFacilityUper, newTableData] - })} - ), - // 테이블 헤더 변경 - setHeadList: (newHeadList: TableUpperProps[]) => set( - (state: tableState) => ({ - ...state, - upHeadList: newHeadList - }) - ), - // 시설 목록 데이터 변경 - setTableData: (newTableData: PublicReuseFacility[]) => set( - (state: tableState) => ({ - ...state, - publicReuseFacilityUper: newTableData - }) - ), - } -} diff --git a/apps/sn/app/store/user-store.tsx b/apps/sn/app/store/user-store.tsx new file mode 100644 index 000000000..8719dc842 --- /dev/null +++ b/apps/sn/app/store/user-store.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +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; +}; + +type UserStore = { + userInfo: IUserInfo | null; + setUserInfo: (userInfo: IUserInfo | null) => void; +}; + +// ✅ zustand 기본 create + persist 적용 +export const useUserStore = create()( + persist( + (set) => ({ + userInfo: null, + setUserInfo: (userInfo) => set({ userInfo }), + }), + { + name: "user-store", // localStorage 키 + partialize: (state) => ({ userInfo: state.userInfo }), // 선택적 저장 + }, + ), +); + +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/sn/middleware.ts b/apps/sn/middleware.ts index be1d093ae..d5a1e74f5 100644 --- a/apps/sn/middleware.ts +++ b/apps/sn/middleware.ts @@ -46,9 +46,26 @@ 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, + }); + } + return NextResponse.next(); } export const config = { - matcher: ["/server/:path*","/geoserver/:path*", "/attach-service/:path*"], + matcher: [ + "/server/:path*", + "/geoserver/:path*", + "/attach-service/:path*", + "/user-service/:path*", + ], };