From 19dfe45b0be2de02d1ca5e58e417b95764e5438f Mon Sep 17 00:00:00 2001 From: ljpark Date: Thu, 16 Jul 2026 16:58:26 +0900 Subject: [PATCH] =?UTF-8?q?wr:=20sn:=20fo:=20BaseLayout=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20param=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/fo/app/components/api/index.ts | 5 +- apps/fo/app/page.tsx | 4 +- apps/fo/app/pages/(map-layout)/layout.tsx | 1 + .../app/pages/(with-layout)/jwttest/page.tsx | 220 +++++++++++++ apps/fo/app/pages/(with-layout)/layout.tsx | 2 + apps/fo/src/features/auth/login/index.ts | 1 - .../src/features/auth/login/ui/login-page.tsx | 139 -------- apps/fo/src/features/auth/login/ui/login.css | 310 ------------------ apps/fo/src/store/admin-store.ts | 29 -- apps/sn/app/(with-base-layout)/layout.tsx | 2 + apps/sn/app/components/lib/api/index.ts | 4 +- apps/sn/src/store/admin-store.ts | 29 -- apps/wr/app/(with-layout)/layout.tsx | 25 +- apps/wr/components/lib/api/index.ts | 4 +- apps/wr/src/features/auth/login/index.ts | 1 - .../src/features/auth/login/ui/login-page.tsx | 138 -------- apps/wr/src/features/auth/login/ui/login.css | 310 ------------------ apps/wr/src/store/admin-store.ts | 29 -- docs/session-extend-confirm-guide.md | 2 +- 19 files changed, 243 insertions(+), 1012 deletions(-) create mode 100644 apps/fo/app/pages/(with-layout)/jwttest/page.tsx delete mode 100644 apps/fo/src/features/auth/login/index.ts delete mode 100644 apps/fo/src/features/auth/login/ui/login-page.tsx delete mode 100644 apps/fo/src/features/auth/login/ui/login.css delete mode 100644 apps/fo/src/store/admin-store.ts delete mode 100644 apps/sn/src/store/admin-store.ts delete mode 100644 apps/wr/src/features/auth/login/index.ts delete mode 100644 apps/wr/src/features/auth/login/ui/login-page.tsx delete mode 100644 apps/wr/src/features/auth/login/ui/login.css delete mode 100644 apps/wr/src/store/admin-store.ts diff --git a/apps/fo/app/components/api/index.ts b/apps/fo/app/components/api/index.ts index 542e72947..5fab2ee12 100644 --- a/apps/fo/app/components/api/index.ts +++ b/apps/fo/app/components/api/index.ts @@ -1,14 +1,17 @@ import { CLAIM_NAME, SITE_ID } from "@/src/shared/config/env"; import { tokenManager } from "@/src/entities/auth/lib/token-manager"; +import { getRemainingMs } from "@common/auth"; import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env"; const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; +// client 화면인 경우, 토큰 만료여부 점검하여 로그인 페이지로 이동 const handleUnauthorizedResponse = () => { if (typeof window === "undefined") return; - const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME); + const accessToken = tokenManager.getToken(CLAIM_NAME); + const remainingMs = accessToken ? getRemainingMs(accessToken) : null; const isExpired = remainingMs === null || remainingMs <= 0; if (!isExpired) return; diff --git a/apps/fo/app/page.tsx b/apps/fo/app/page.tsx index 143dfdc90..351e87885 100644 --- a/apps/fo/app/page.tsx +++ b/apps/fo/app/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { LoginApi } from "@/src/entities/auth"; +import { authService } from "@/src/entities/auth"; import { useEffect } from "react"; import { AUTH_USER_ID } from "@/src/shared/config"; import { getCookie } from "@/src/shared/lib/cookie"; @@ -17,7 +17,7 @@ export default function Home() { if (!checkToken) { async function doLogin() { try { - await LoginApi.login({ userId: "CLEANWATER", password: "gktneh12!@" }); + await authService.login({ userId: "CLEANWATER", password: "gktneh12!@" }); window.location.href = "/pages/dashboard"; } catch (err) { console.error("로그인 실패", err); diff --git a/apps/fo/app/pages/(map-layout)/layout.tsx b/apps/fo/app/pages/(map-layout)/layout.tsx index 69a60b7a2..9c45eabc8 100644 --- a/apps/fo/app/pages/(map-layout)/layout.tsx +++ b/apps/fo/app/pages/(map-layout)/layout.tsx @@ -3,6 +3,7 @@ import { NoFooterLayout } from "@common/components"; import type { MenuItem } from "../../store/common"; import { useFoStore } from "../../store"; import { usePathname, useRouter } from "next/navigation"; +// import { authService } from "@/src/entities/auth"; export default function RootLayout({ children, diff --git a/apps/fo/app/pages/(with-layout)/jwttest/page.tsx b/apps/fo/app/pages/(with-layout)/jwttest/page.tsx new file mode 100644 index 000000000..c9df447ef --- /dev/null +++ b/apps/fo/app/pages/(with-layout)/jwttest/page.tsx @@ -0,0 +1,220 @@ +/** + * JWT를 decoding하면 만료시간을 읽을 수 있다. + * JWT는 3부분으로 구성 : Header.Payload.Signature + * Payload 부분에 만료시간(exp)이 있다. + */ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { authService } from "@/src/entities/auth"; +import { CLAIM_NAME } from "@/src/shared/config"; +import { getCookie } from "@/src/shared/lib"; + +interface JwtPayload { + sub?: string; // 토큰 subject. 일반적으로 사용자 식별자나 주체 정보 + exp?: number; // 만료 시각(Unix timestamp, 초 단위) + iat?: number; // 토큰 발급 시각(Unix timestamp, 초 단위) + [key: string]: unknown; // 그 외 JWT payload에 포함될 수 있는 추가 클레임 +} + +interface JwtState { + token: string; // 쿠키에서 읽어온 JWT 원본 문자열 + payload: JwtPayload | null; // JWT를 파싱한 payload 객체 + exp: number | null; // payload에서 추출한 exp 값 + expDateLocal: string | null; // exp를 로컬 시간 문자열로 변환한 값 + remainingMs: number | null; // 현재 시각 기준 남은 시간(ms) + error: string | null; // 쿠키 없음, 파싱 실패 등 오류 메시지 +} + +const initialJwtState: JwtState = { + token: "", + payload: null, + exp: null, + expDateLocal: null, + remainingMs: null, + error: null, +}; + +function parseJwt(token: string): JwtPayload { + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("유효한 JWT 형식이 아닙니다."); + } + + const base64Url = parts[1] ?? ""; // Payload를 가져온다. + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); + const jsonPayload = decodeURIComponent( + atob(padded) + .split("") + .map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`) + .join("") + ); + + return JSON.parse(jsonPayload) as JwtPayload; +} + +function formatRemainingMs(remainingMs: number | null): string { + if (remainingMs === null) return "-"; + if (remainingMs <= 0) return "만료됨"; + + const totalSeconds = Math.floor(remainingMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return `${hours}시간 ${minutes}분 ${seconds}초`; +} + +function readJwtState(): JwtState { + const token = getCookie(CLAIM_NAME) ?? ""; + + if (!token) { + return { + token: "", + payload: null, + exp: null, + expDateLocal: null, + remainingMs: null, + error: `${CLAIM_NAME} 쿠키가 없습니다.`, + }; + } + + try { + const payload = parseJwt(token); + + if (typeof payload.exp !== "number") { + return { + token, + payload, + exp: null, + expDateLocal: null, + remainingMs: null, + error: "JWT payload에 exp 값이 없습니다.", + }; + } + + const expMs = payload.exp * 1000; + + return { + token, + payload, + exp: payload.exp, + expDateLocal: new Date(expMs).toLocaleString(), + remainingMs: Math.max(expMs - Date.now(), 0), + error: null, + }; + } catch (error) { + return { + token, + payload: null, + exp: null, + expDateLocal: null, + remainingMs: null, + error: + error instanceof Error + ? error.message + : "JWT 파싱 중 오류가 발생했습니다.", + }; + } +} + +export default function Page() { + const [jwtState, setJwtState] = useState(initialJwtState); + const isRefreshingRef = useRef(false); + + const handleSilentRefresh = async () => { + try { + await authService.silentRefresh(); + } catch (error) { + console.error("Manual silent refresh failed:", error); + } finally { + setJwtState(readJwtState()); + } + }; + + useEffect(() => { + const syncJwtState = async () => { + const nextState = readJwtState(); + + if ( + nextState.remainingMs !== null && + nextState.remainingMs > 0 && + nextState.remainingMs < 5 * 60 * 1000 && + !isRefreshingRef.current + ) { + isRefreshingRef.current = true; + try { + await authService.silentRefresh(); + } catch (error) { + console.error("JWT test silent refresh failed:", error); + } + } + + setJwtState(readJwtState()); + + if ( + nextState.remainingMs === null || + nextState.remainingMs >= 5 * 60 * 1000 || + nextState.remainingMs <= 0 + ) { + isRefreshingRef.current = false; + } + }; + + void syncJwtState(); + const timer = window.setInterval(() => { + void syncJwtState(); + }, 1000); + + return () => window.clearInterval(timer); + }, []); + + return ( +
+

JWT Exp Test

+ + + +
+
쿠키명
+
{CLAIM_NAME}
+
+ +
+
exp
+
{jwtState.exp ?? "-"}
+
+ +
+
만료시각
+
{jwtState.expDateLocal ?? "-"}
+
+ +
+
남은시간
+
{formatRemainingMs(jwtState.remainingMs)}
+
+ +
+
상태
+
{jwtState.error ?? "정상"}
+
+ +
+
payload
+
+          {jwtState.payload ? JSON.stringify(jwtState.payload, null, 2) : "-"}
+        
+
+
+ ); +} diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index 38276b541..768d66670 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -7,6 +7,7 @@ import { useFoStore } from "@/app/store"; import { usePathname, useRouter } from "next/navigation"; import { useMenuStore } from "@/src/shared/lib"; import { PageLayout } from "@/src/shared/ui/page-layout"; +import { authService } from "@/src/entities/auth"; export default function RootLayout({ children, @@ -28,6 +29,7 @@ export default function RootLayout({ return ( diff --git a/apps/fo/src/features/auth/login/index.ts b/apps/fo/src/features/auth/login/index.ts deleted file mode 100644 index 2093c3d8d..000000000 --- a/apps/fo/src/features/auth/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginPage } from "./ui/login-page"; diff --git a/apps/fo/src/features/auth/login/ui/login-page.tsx b/apps/fo/src/features/auth/login/ui/login-page.tsx deleted file mode 100644 index fe14819f3..000000000 --- a/apps/fo/src/features/auth/login/ui/login-page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { useEffect } from "react"; -import { images } from "@common/assets"; -import { LoginApi } from "@/src/entities/auth"; -import "./login.css"; - -function LoginPage() { - useEffect(() => { - const hostname = window.location.hostname; - if (hostname.includes("hasuon.or.kr")) { - window.location.href = - process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; - } - }, []); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - const formData = new FormData(e.target as HTMLFormElement); - const userId = formData.get("id") as string; - const password = formData.get("password") as string; - - await LoginApi.login({ userId, password }); - window.location.href = "/pages/dashboard"; - }; - - return ( -
-
-
-

- 하수도정보시스템 -
- CMS 통합관리시스템 -

-

하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.

-
-
- logo -
-
-
-
-
    -
  • -

    ID

    -
    - - -
    -
  • -
  • -

    PASSWORD

    -
    - - -
    -
  • -
-
-
- - -
- {/* 비밀번호 또는 아이디를 잃어버리셨나요? */} -
- -
-
-

copyright © 2023 KECO. All rights reserved.

-
- -

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

-
-
- -
- ); -} - -export { LoginPage }; - diff --git a/apps/fo/src/features/auth/login/ui/login.css b/apps/fo/src/features/auth/login/ui/login.css deleted file mode 100644 index af73c9c68..000000000 --- a/apps/fo/src/features/auth/login/ui/login.css +++ /dev/null @@ -1,310 +0,0 @@ -/** -* OLYM Cascading Style Sheets -* File Name : login.css -* Description : 로그인 스타일시트 -* Author : Web business Team / Choi Hyun Mi -* Email : desingtj@olym.co.kr -* Date : 2023.07.20 -* Update : 2023.07.26 -* Copyright(c) 2023 OLYM Communications. All Rights Reserved. -*/ -html, -body { - background: #fff; -} -.fx { - display: flex; - -ms-display: flex; - -webkit-display: flex; - box-sizing: border-box; -} -.login { - width: 100%; - height: 100vh; -} -.login .left_box { - width: 100%; - background: url(/images/bg_login.jpg) no-repeat center/cover; - flex-direction: column; - justify-content: space-between; - padding: 7% 5% 5% 5%; -} -.login .left_box .text h1 { - font-size: 53px; - color: #fff; -} -.login .left_box .text h1 span { - font-weight: 800; -} -.login .left_box .text p { - margin-top: 40px; - font-size: 18px; - color: #fff; -} -.login .login_box { - width: 100%; - flex-direction: column; - justify-content: center; - padding: 4% 12% 4% 6%; -} -.login .login_box > ul { - flex-direction: column; - gap: 40px; -} -.login .login_box > ul > li h2 { - font-size: 22px; - font-weight: 600; - color: #2640a5; - margin-bottom: 14px; -} -.login .login_box > ul > li .input_st { - position: relative; -} -.login .login_box > ul > li .input_st .diveicon { - position: absolute; - top: 50%; - left: 28px; - transform: translateY(-50%); - font-family: "Xeicon"; - font-size: 25px; - color: #003288; -} -.login .login_box > ul > li input { - width: 100%; - padding: 20px 20px 20px 70px; - background: #f7f7f7; - box-sizing: border-box; - border: none; - border-radius: 5px; - font-size: 18px; - font-weight: 700; - color: #000; - height: auto; -} -.login .login_box > ul > li input:focus { - outline: 1px solid #3c5fe5; -} -.login .login_box > ul > li input::placeholder { - color: #afabab; - font-weight: 500; -} -.login .login_box .bottom_box { - justify-content: space-between; - gap: 20px; - padding: 50px 0 45px 0; -} -.login .login_box .bottom_box .checks { - display: flex; - align-items: center; - gap: 8px; -} -.login .login_box .bottom_box .checks input[type="checkbox"] { - width: 18px; - height: 18px; -} -.login .login_box .bottom_box .checks label { - font-size: 14px; -} -.login .login_box .bottom_box .pw_message { - align-items: center; - gap: 5px; - color: #636363; - font-weight: 500; - border-bottom: 1px solid #ccc; - padding-bottom: 5px; -} -.login .login_box .bottom_box .pw_message i { - color: #327af5; - font-size: 20px; -} -.login .login_box .login_btn { - position: relative; - border-radius: 5px; - padding: 24px 15px; - background: linear-gradient( - to left, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - overflow: hidden; - justify-content: center; - align-items: center; - gap: 10px; - color: #fff; - cursor: pointer; -} -.login .login_box .login_btn span { - font-family: "Montserrat"; - font-size: 19px; - font-weight: 700; - position: relative; -} -.login .login_box .login_btn i { - font-size: 22px; - transform: translateY(-2px); -} -.login .login_box .login_btn:before { - display: block; - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - background: linear-gradient( - to right, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - transition: opacity 0.5s; -} -.login .login_box .login_btn:hover::before { - opacity: 1; -} -.login .footer .copy { - margin-top: 36px; - justify-content: space-between; - align-items: center; - gap: 3%; -} -.login .footer .copy p { - font-size: 13px; - color: #d4d4d4; - width: 44%; - text-align: center; -} -.login .footer .copy::before, -.login .login_box .copy::after { - content: ""; - width: 20%; - height: 1px; - background: #d4d4d4; -} -.login .footer .fot_btn { - gap: 20px; - margin: 30px 0 10px 0; -} -.login .footer .fot_btn a { - color: #333; - font-weight: 700; - font-size: 13px; -} -.login .footer .fot_btn a:hover { - color: #003288; -} -.login .footer .addr { - color: #666; - font-size: 13px; - gap: 3px 20px; - flex-wrap: wrap; -} -/* 비밀번호 변경 팝업창 */ -.login .modal .modal_container { - width: 750px; -} -.login .pw_desc { - padding: 10px 10px 30px 10px; - color: #666; -} -.login .pw_desc p { - font-size: 1.25rem; - color: #000; - font-weight: 700; - margin-bottom: 10px; -} -.login .pw_desc span { - color: #f60; - font-weight: 700; -} -.login .modal .form_write { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - padding: 20px 50px; -} -@media (max-width: 1024px) { - .login { - flex-direction: column; - height: auto; - } - .login .left_box { - padding: 30px; - flex-direction: column-reverse; - gap: 20px; - } - .login .left_box .text h1 { - font-size: 30px; - line-height: 1.2em; - } - .login .left_box .text p { - margin-top: 20px; - font-size: 14px; - } - .login .left_box .img img { - height: 40px; - } - .login .login_box { - padding: 30px; - } - .login .login_box > ul { - gap: 20px; - } - .login .login_box > ul > li h2 { - margin-bottom: 7px; - } - .login .login_box > ul > li .input_st::after { - left: 15px; - font-size: 20px; - } - .login .login_box > ul > li input { - padding: 15px 10px 15px 50px; - font-size: 16px; - } - .login .login_box .bottom_box { - flex-direction: column; - align-items: flex-start; - padding: 30px 0; - } - .login .login_box .bottom_box .id_save label { - font-size: 14px; - } - .login .login_box .login_btn { - padding: 15px; - } - .login .login_box .login_btn span { - font-size: 16px; - } - .login .login_box .login_btn i { - font-size: 18px; - transform: translateY(-1px); - } - .login .login_box .copy p { - width: 100%; - } - .login .login_box .copy::before, - .login .login_box .copy::after { - display: none; - } - .login .footer .addr { - font-size: 11px; - gap: 3px 10px; - } - .login .modal .modal_container { - width: 90%; - } - .login .pw_desc { - padding: 0 0 20px 0; - } - .login .modal .form_write { - padding: 10px; - } -} -/* // login */ diff --git a/apps/fo/src/store/admin-store.ts b/apps/fo/src/store/admin-store.ts deleted file mode 100644 index 098ff816e..000000000 --- a/apps/fo/src/store/admin-store.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { create } from "zustand"; -import { UserApi } from "@/src/entities/user"; -import { getUserInfo } from "../../../../packages/common/components/src/api"; - -interface AdminStore { - /** 현재 사용자가 관리자 목록에 있는지 */ - isAdminInfo: boolean; - setAdminInfo: (value: boolean) => void; - /** API로 관리자 목록 조회 후 현재 사용자 일치 여부 설정 */ - fetchAdminInfo: () => Promise; -} - -const useAdminStoreHook = create((set) => ({ - isAdminInfo: false, - setAdminInfo: (value) => set({ isAdminInfo: value }), - fetchAdminInfo: async () => { - try { - const userInfo = await getUserInfo(); - const adminList = await UserApi.findPageAdminUser({ userName: userInfo.userName }); - const matched = adminList.content?.filter((a) => a.userName === userInfo.userName) ?? []; - set({ isAdminInfo: matched.length > 0 }); - } catch { - set({ isAdminInfo: false }); - } - }, -})); - -export const useAdminStore = useAdminStoreHook; -export const getAdminStoreState = () => useAdminStoreHook.getState(); diff --git a/apps/sn/app/(with-base-layout)/layout.tsx b/apps/sn/app/(with-base-layout)/layout.tsx index 346521d17..60f4a2ffe 100644 --- a/apps/sn/app/(with-base-layout)/layout.tsx +++ b/apps/sn/app/(with-base-layout)/layout.tsx @@ -21,6 +21,7 @@ import type { MenuItem } from "@/entities/menu"; import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; import useStore from '../store/useStore'; +import { authService } from "@/entities/auth"; export default function WithBaseLayout({ children, @@ -50,6 +51,7 @@ export default function WithBaseLayout({ return ( diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 28c9f4500..73cc2e984 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -13,6 +13,7 @@ import { useEffect, useState } from "react"; import axios from 'axios'; import { CLAIM_NAME, SITE_ID } from "@/shared/config"; import { tokenManager } from "@/entities/auth/lib/token-manager"; +import { getRemainingMs } from "@common/auth"; const BASE_URL = '/sewerage-service/api'; const DEFAULT_HEADERS = { @@ -32,7 +33,8 @@ const resolveApiUrl = (url: string) => const handleUnauthorizedResponse = () => { if (typeof window === "undefined") return; - const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME); + const accessToken = tokenManager.getToken(CLAIM_NAME); + const remainingMs = accessToken ? getRemainingMs(accessToken) : null; const isExpired = remainingMs === null || remainingMs <= 0; if (!isExpired) return; diff --git a/apps/sn/src/store/admin-store.ts b/apps/sn/src/store/admin-store.ts deleted file mode 100644 index 098ff816e..000000000 --- a/apps/sn/src/store/admin-store.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { create } from "zustand"; -import { UserApi } from "@/src/entities/user"; -import { getUserInfo } from "../../../../packages/common/components/src/api"; - -interface AdminStore { - /** 현재 사용자가 관리자 목록에 있는지 */ - isAdminInfo: boolean; - setAdminInfo: (value: boolean) => void; - /** API로 관리자 목록 조회 후 현재 사용자 일치 여부 설정 */ - fetchAdminInfo: () => Promise; -} - -const useAdminStoreHook = create((set) => ({ - isAdminInfo: false, - setAdminInfo: (value) => set({ isAdminInfo: value }), - fetchAdminInfo: async () => { - try { - const userInfo = await getUserInfo(); - const adminList = await UserApi.findPageAdminUser({ userName: userInfo.userName }); - const matched = adminList.content?.filter((a) => a.userName === userInfo.userName) ?? []; - set({ isAdminInfo: matched.length > 0 }); - } catch { - set({ isAdminInfo: false }); - } - }, -})); - -export const useAdminStore = useAdminStoreHook; -export const getAdminStoreState = () => useAdminStoreHook.getState(); diff --git a/apps/wr/app/(with-layout)/layout.tsx b/apps/wr/app/(with-layout)/layout.tsx index 1e43f4c32..03d48aa1c 100644 --- a/apps/wr/app/(with-layout)/layout.tsx +++ b/apps/wr/app/(with-layout)/layout.tsx @@ -1,22 +1,3 @@ -// "use client"; -// import Base from "../base"; - -// export default function RootLayout({ -// children, -// }: Readonly<{ -// children: React.ReactNode; -// }>) { - - -// return ( - -// -// {children} -// - -// ); -// } - "use client"; import { BaseLayout } from "@common/components/ui"; import { useQueryClient } from "@tanstack/react-query"; @@ -26,6 +7,7 @@ import type { MenuItem } from "../store/common/type/menu"; import { usePathname, useRouter } from "next/navigation"; import { useMenuStore } from "@/src/shared/lib"; import { PageLayout } from "@/src/shared/ui/page-layout"; +import { authService } from "@/src/entities/auth"; export default function RootLayout({ children, @@ -48,7 +30,10 @@ export default function RootLayout({ } }; return ( - + {children} diff --git a/apps/wr/components/lib/api/index.ts b/apps/wr/components/lib/api/index.ts index 44eef5924..fba544685 100644 --- a/apps/wr/components/lib/api/index.ts +++ b/apps/wr/components/lib/api/index.ts @@ -1,6 +1,7 @@ import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/"; import { CLAIM_NAME, SITE_ID } from "@/src/shared/config"; import { tokenManager } from "@/src/entities/auth/lib/token-manager"; +import { getRemainingMs } from "@common/auth"; export const fetchType = { COMMON: "common", @@ -20,7 +21,8 @@ const getDefaultHeaders = (headers: HeadersInit = {}) => ({ const handleUnauthorizedResponse = () => { if (typeof window === "undefined") return; - const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME); + const accessToken = tokenManager.getToken(CLAIM_NAME); + const remainingMs = accessToken ? getRemainingMs(accessToken) : null; const isExpired = remainingMs === null || remainingMs <= 0; if (!isExpired) return; diff --git a/apps/wr/src/features/auth/login/index.ts b/apps/wr/src/features/auth/login/index.ts deleted file mode 100644 index 2093c3d8d..000000000 --- a/apps/wr/src/features/auth/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginPage } from "./ui/login-page"; diff --git a/apps/wr/src/features/auth/login/ui/login-page.tsx b/apps/wr/src/features/auth/login/ui/login-page.tsx deleted file mode 100644 index 4c340f78e..000000000 --- a/apps/wr/src/features/auth/login/ui/login-page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { useEffect } from "react"; -import { images } from "@common/assets"; -import { LoginApi } from "@/src/entities/auth"; -import "./login.css"; - -function LoginPage() { - useEffect(() => { - const hostname = window.location.hostname; - if (hostname.includes("hasuon.or.kr")) { - window.location.href = - process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; - } - }, []); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - const formData = new FormData(e.target as HTMLFormElement); - const userId = formData.get("id") as string; - const password = formData.get("password") as string; - - await LoginApi.login({ userId, password }); - window.location.href = "/map/mainMap"; - }; - - return ( -
-
-
-

- 하수도정보시스템 -
- CMS 통합관리시스템 -

-

하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.

-
-
- logo -
-
-
-
-
    -
  • -

    ID

    -
    - - -
    -
  • -
  • -

    PASSWORD

    -
    - - -
    -
  • -
-
-
- - -
- {/* 비밀번호 또는 아이디를 잃어버리셨나요? */} -
- -
-
-

copyright © 2023 KECO. All rights reserved.

-
- -

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

-
-
- -
- ); -} - -export { LoginPage }; diff --git a/apps/wr/src/features/auth/login/ui/login.css b/apps/wr/src/features/auth/login/ui/login.css deleted file mode 100644 index af73c9c68..000000000 --- a/apps/wr/src/features/auth/login/ui/login.css +++ /dev/null @@ -1,310 +0,0 @@ -/** -* OLYM Cascading Style Sheets -* File Name : login.css -* Description : 로그인 스타일시트 -* Author : Web business Team / Choi Hyun Mi -* Email : desingtj@olym.co.kr -* Date : 2023.07.20 -* Update : 2023.07.26 -* Copyright(c) 2023 OLYM Communications. All Rights Reserved. -*/ -html, -body { - background: #fff; -} -.fx { - display: flex; - -ms-display: flex; - -webkit-display: flex; - box-sizing: border-box; -} -.login { - width: 100%; - height: 100vh; -} -.login .left_box { - width: 100%; - background: url(/images/bg_login.jpg) no-repeat center/cover; - flex-direction: column; - justify-content: space-between; - padding: 7% 5% 5% 5%; -} -.login .left_box .text h1 { - font-size: 53px; - color: #fff; -} -.login .left_box .text h1 span { - font-weight: 800; -} -.login .left_box .text p { - margin-top: 40px; - font-size: 18px; - color: #fff; -} -.login .login_box { - width: 100%; - flex-direction: column; - justify-content: center; - padding: 4% 12% 4% 6%; -} -.login .login_box > ul { - flex-direction: column; - gap: 40px; -} -.login .login_box > ul > li h2 { - font-size: 22px; - font-weight: 600; - color: #2640a5; - margin-bottom: 14px; -} -.login .login_box > ul > li .input_st { - position: relative; -} -.login .login_box > ul > li .input_st .diveicon { - position: absolute; - top: 50%; - left: 28px; - transform: translateY(-50%); - font-family: "Xeicon"; - font-size: 25px; - color: #003288; -} -.login .login_box > ul > li input { - width: 100%; - padding: 20px 20px 20px 70px; - background: #f7f7f7; - box-sizing: border-box; - border: none; - border-radius: 5px; - font-size: 18px; - font-weight: 700; - color: #000; - height: auto; -} -.login .login_box > ul > li input:focus { - outline: 1px solid #3c5fe5; -} -.login .login_box > ul > li input::placeholder { - color: #afabab; - font-weight: 500; -} -.login .login_box .bottom_box { - justify-content: space-between; - gap: 20px; - padding: 50px 0 45px 0; -} -.login .login_box .bottom_box .checks { - display: flex; - align-items: center; - gap: 8px; -} -.login .login_box .bottom_box .checks input[type="checkbox"] { - width: 18px; - height: 18px; -} -.login .login_box .bottom_box .checks label { - font-size: 14px; -} -.login .login_box .bottom_box .pw_message { - align-items: center; - gap: 5px; - color: #636363; - font-weight: 500; - border-bottom: 1px solid #ccc; - padding-bottom: 5px; -} -.login .login_box .bottom_box .pw_message i { - color: #327af5; - font-size: 20px; -} -.login .login_box .login_btn { - position: relative; - border-radius: 5px; - padding: 24px 15px; - background: linear-gradient( - to left, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - overflow: hidden; - justify-content: center; - align-items: center; - gap: 10px; - color: #fff; - cursor: pointer; -} -.login .login_box .login_btn span { - font-family: "Montserrat"; - font-size: 19px; - font-weight: 700; - position: relative; -} -.login .login_box .login_btn i { - font-size: 22px; - transform: translateY(-2px); -} -.login .login_box .login_btn:before { - display: block; - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; - background: linear-gradient( - to right, - #1d3969 0%, - #264078 10%, - #172558 40%, - #162254 50%, - #111c43 60%, - #111c42 100% - ); - transition: opacity 0.5s; -} -.login .login_box .login_btn:hover::before { - opacity: 1; -} -.login .footer .copy { - margin-top: 36px; - justify-content: space-between; - align-items: center; - gap: 3%; -} -.login .footer .copy p { - font-size: 13px; - color: #d4d4d4; - width: 44%; - text-align: center; -} -.login .footer .copy::before, -.login .login_box .copy::after { - content: ""; - width: 20%; - height: 1px; - background: #d4d4d4; -} -.login .footer .fot_btn { - gap: 20px; - margin: 30px 0 10px 0; -} -.login .footer .fot_btn a { - color: #333; - font-weight: 700; - font-size: 13px; -} -.login .footer .fot_btn a:hover { - color: #003288; -} -.login .footer .addr { - color: #666; - font-size: 13px; - gap: 3px 20px; - flex-wrap: wrap; -} -/* 비밀번호 변경 팝업창 */ -.login .modal .modal_container { - width: 750px; -} -.login .pw_desc { - padding: 10px 10px 30px 10px; - color: #666; -} -.login .pw_desc p { - font-size: 1.25rem; - color: #000; - font-weight: 700; - margin-bottom: 10px; -} -.login .pw_desc span { - color: #f60; - font-weight: 700; -} -.login .modal .form_write { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - padding: 20px 50px; -} -@media (max-width: 1024px) { - .login { - flex-direction: column; - height: auto; - } - .login .left_box { - padding: 30px; - flex-direction: column-reverse; - gap: 20px; - } - .login .left_box .text h1 { - font-size: 30px; - line-height: 1.2em; - } - .login .left_box .text p { - margin-top: 20px; - font-size: 14px; - } - .login .left_box .img img { - height: 40px; - } - .login .login_box { - padding: 30px; - } - .login .login_box > ul { - gap: 20px; - } - .login .login_box > ul > li h2 { - margin-bottom: 7px; - } - .login .login_box > ul > li .input_st::after { - left: 15px; - font-size: 20px; - } - .login .login_box > ul > li input { - padding: 15px 10px 15px 50px; - font-size: 16px; - } - .login .login_box .bottom_box { - flex-direction: column; - align-items: flex-start; - padding: 30px 0; - } - .login .login_box .bottom_box .id_save label { - font-size: 14px; - } - .login .login_box .login_btn { - padding: 15px; - } - .login .login_box .login_btn span { - font-size: 16px; - } - .login .login_box .login_btn i { - font-size: 18px; - transform: translateY(-1px); - } - .login .login_box .copy p { - width: 100%; - } - .login .login_box .copy::before, - .login .login_box .copy::after { - display: none; - } - .login .footer .addr { - font-size: 11px; - gap: 3px 10px; - } - .login .modal .modal_container { - width: 90%; - } - .login .pw_desc { - padding: 0 0 20px 0; - } - .login .modal .form_write { - padding: 10px; - } -} -/* // login */ diff --git a/apps/wr/src/store/admin-store.ts b/apps/wr/src/store/admin-store.ts deleted file mode 100644 index 098ff816e..000000000 --- a/apps/wr/src/store/admin-store.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { create } from "zustand"; -import { UserApi } from "@/src/entities/user"; -import { getUserInfo } from "../../../../packages/common/components/src/api"; - -interface AdminStore { - /** 현재 사용자가 관리자 목록에 있는지 */ - isAdminInfo: boolean; - setAdminInfo: (value: boolean) => void; - /** API로 관리자 목록 조회 후 현재 사용자 일치 여부 설정 */ - fetchAdminInfo: () => Promise; -} - -const useAdminStoreHook = create((set) => ({ - isAdminInfo: false, - setAdminInfo: (value) => set({ isAdminInfo: value }), - fetchAdminInfo: async () => { - try { - const userInfo = await getUserInfo(); - const adminList = await UserApi.findPageAdminUser({ userName: userInfo.userName }); - const matched = adminList.content?.filter((a) => a.userName === userInfo.userName) ?? []; - set({ isAdminInfo: matched.length > 0 }); - } catch { - set({ isAdminInfo: false }); - } - }, -})); - -export const useAdminStore = useAdminStoreHook; -export const getAdminStoreState = () => useAdminStoreHook.getState(); diff --git a/docs/session-extend-confirm-guide.md b/docs/session-extend-confirm-guide.md index 0668f1f3d..23a1ce099 100644 --- a/docs/session-extend-confirm-guide.md +++ b/docs/session-extend-confirm-guide.md @@ -47,7 +47,7 @@ sequenceDiagram else 취소 Header->>API: declineSessionExtend() API->>RefreshTimer: clearRefreshTimer() - Note over RefreshTimer: 만료까지 자동 갱신 없음 + Note over RefreshTimer: 취소시 만료까지 자동 갱신 없음 end ```