wr: sn: fo: BaseLayout 호출 param 추가
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<JwtState>(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 (
|
||||
<div className="space-y-4 p-8 text-base">
|
||||
<h1 className="text-2xl font-bold">JWT Exp Test</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
void handleSilentRefresh();
|
||||
}}
|
||||
>
|
||||
Silent Refresh 호출
|
||||
</button>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">쿠키명</div>
|
||||
<div>{CLAIM_NAME}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">exp</div>
|
||||
<div>{jwtState.exp ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">만료시각</div>
|
||||
<div>{jwtState.expDateLocal ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">남은시간</div>
|
||||
<div>{formatRemainingMs(jwtState.remainingMs)}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">상태</div>
|
||||
<div>{jwtState.error ?? "정상"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">payload</div>
|
||||
<pre className="overflow-auto whitespace-pre-wrap break-all text-sm">
|
||||
{jwtState.payload ? JSON.stringify(jwtState.payload, null, 2) : "-"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -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 */
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
const useAdminStoreHook = create<AdminStore>((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();
|
||||
@@ -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 (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
const useAdminStoreHook = create<AdminStore>((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();
|
||||
@@ -1,22 +1,3 @@
|
||||
// "use client";
|
||||
// import Base from "../base";
|
||||
|
||||
// export default function RootLayout({
|
||||
// children,
|
||||
// }: Readonly<{
|
||||
// children: React.ReactNode;
|
||||
// }>) {
|
||||
|
||||
|
||||
// return (
|
||||
|
||||
// <Base>
|
||||
// {children}
|
||||
// </Base>
|
||||
|
||||
// );
|
||||
// }
|
||||
|
||||
"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 (
|
||||
<BaseLayout queryClient={queryClient} currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
|
||||
<BaseLayout queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={changeMenu}>
|
||||
<PageLayout>
|
||||
<MainDiv>{children}</MainDiv>
|
||||
</PageLayout>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
@@ -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 */
|
||||
@@ -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<void>;
|
||||
}
|
||||
|
||||
const useAdminStoreHook = create<AdminStore>((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();
|
||||
@@ -47,7 +47,7 @@ sequenceDiagram
|
||||
else 취소
|
||||
Header->>API: declineSessionExtend()
|
||||
API->>RefreshTimer: clearRefreshTimer()
|
||||
Note over RefreshTimer: 만료까지 자동 갱신 없음
|
||||
Note over RefreshTimer: 취소시 만료까지 자동 갱신 없음
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user