Merge branch 'iipbs/wr/dev'
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "@/app/store/common";
|
||||
import { useFoStore } from "@/app/store";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -16,6 +17,7 @@ export default function RootLayout({
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleClick = (id: MenuItem) => {
|
||||
setCurrentMenuInfo(id);
|
||||
@@ -25,6 +27,7 @@ export default function RootLayout({
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -459,7 +459,18 @@ const fetchFacilityList = async (pageNo: number, pageSize: number, partValue?: s
|
||||
}
|
||||
};
|
||||
|
||||
// 시설정보 리스트 데이터 호출 (대시보드용)
|
||||
/**
|
||||
* 시설정보 리스트 데이터 호출 (대시보드용)
|
||||
* @param pageNo 페이지번호
|
||||
* @param pageSize 페이지크기
|
||||
* @param partValue 조회구분값(0 전체,A: 행정, R: 유역, G: 환경청)
|
||||
* @param nameCd 조회구분에 따른 지역코드(0 전체)
|
||||
* @param fcltCpct 시설용량 필터값(0 전체)
|
||||
* @param oprtngSttsCd 운영상태 코드(0 전체,1: 가동중, 2:재가동, 3:가동중지, 4:시설폐쇄)
|
||||
* @param facilityPartValue 시설구분 코드(0 전체,ET:기타기초시설,SW:공공하수처리시설,SS:오수처리장,NS:분뇨처리장)
|
||||
* @param facilityName 시설명 검색어
|
||||
* @returns 시설목록과 전체건수
|
||||
*/
|
||||
const fetchFacilityListSearching = async (pageNo: number, pageSize: number, partValue?: string, nameCd?: string
|
||||
, fcltCpct?: string, oprtngSttsCd?: string, facilityPartValue?: string, facilityName?: string): Promise<{ data: facilityListProps[], count: number }> => {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ NEXT_PUBLIC_PROXY_HOST = http://localhost:3001
|
||||
NEXT_PUBLIC_SUB_DOMAIN = http://localhost:13000
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
|
||||
# NEXT_PUBLIC_FO_SERVER_API_URL = http://localhost:8883/operation-service/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { LoginApi } from "@/src/entities/auth/api/login-service";
|
||||
import { CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie } from "@/src/shared/lib";
|
||||
|
||||
@@ -112,14 +113,51 @@ function readJwtState(): JwtState {
|
||||
|
||||
export default function Page() {
|
||||
const [jwtState, setJwtState] = useState<JwtState>(() => readJwtState());
|
||||
const isRefreshingRef = useRef(false);
|
||||
|
||||
const handleSilentRefresh = async () => {
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
} catch (error) {
|
||||
console.error("Manual silent refresh failed:", error);
|
||||
} finally {
|
||||
setJwtState(readJwtState());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const syncJwtState = () => {
|
||||
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 LoginApi.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;
|
||||
}
|
||||
};
|
||||
|
||||
syncJwtState();
|
||||
const timer = window.setInterval(syncJwtState, 1000);
|
||||
void syncJwtState();
|
||||
const timer = window.setInterval(() => {
|
||||
void syncJwtState();
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
@@ -128,6 +166,16 @@ export default function Page() {
|
||||
<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>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
"use client";
|
||||
import { BaseLayout } from "@common/components/ui";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useWrStore } from "../store";
|
||||
import { MainDiv } from "@common/business_components/ui";
|
||||
import type { MenuItem } from "../store/common/type/menu";
|
||||
@@ -35,6 +36,7 @@ export default function RootLayout({
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const changeMenu = (path: MenuItem) => {
|
||||
setCurrentMenuInfo(path);
|
||||
@@ -46,7 +48,7 @@ export default function RootLayout({
|
||||
}
|
||||
};
|
||||
return (
|
||||
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
|
||||
<BaseLayout queryClient={queryClient} currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
|
||||
<PageLayout>
|
||||
<MainDiv>{children}</MainDiv>
|
||||
</PageLayout>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"use client";
|
||||
import { useWrStore } from "@/app/store";
|
||||
import { btnClass } from "@/app/store/common/code";
|
||||
import { prfRegPageSize } from "@/app/store/prfmncReg/pbnstPrfmncReg";
|
||||
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
|
||||
import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
|
||||
import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type";
|
||||
import { excelDownloadNumStyle, yearOptions } from "@/components/lib/func/common";
|
||||
@@ -42,13 +42,13 @@ export default function PbnstSttn() {
|
||||
pbnstSttnSearch,
|
||||
pbnSttnInit,
|
||||
getPbnStnDetail,
|
||||
getFlcts, // 하수재이용시설 조회
|
||||
getFlcts, // 하수재이용시설(tb_wr_pbnst_ruse_fclt) 조회
|
||||
resetPbnstDetail,
|
||||
getLaeSeCdCheck,
|
||||
} = useWrStore((state) => state);
|
||||
|
||||
const { pbnstSttnRes, fclts, pbnstSttnResCount } = pbnstSttn;
|
||||
const { laeSeCd } = auth;
|
||||
const { laeSeCd } = auth; // 지자체구분코드
|
||||
|
||||
const [isReset, setIsReset] = useState(false);
|
||||
const ref = useRef<ReturnType>({} as ReturnType);
|
||||
@@ -66,7 +66,7 @@ export default function PbnstSttn() {
|
||||
const [pbnRuseNm, setPbnRuseNm] = useState<string>("");
|
||||
const { t } = useTranslation("base");
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isAllAreaUser = laeSeCd === "0"; // 모든 지자체 권한이 있는 경우
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
@@ -77,7 +77,8 @@ export default function PbnstSttn() {
|
||||
|
||||
// 유역(R) 탭 선택 시, 이전 행정구역 탭에서 선택된 시군구 값이 남아있어
|
||||
// 검색 결과가 시군구로 필터링되는 현상을 방지하기 위해 sigunValue를 '0'으로 초기화합니다.
|
||||
if (cleanedValue.partValue === "R"|| cleanedValue.partValue === "G") {
|
||||
// if (cleanedValue.partValue === "R"|| cleanedValue.partValue === "G") {
|
||||
if (cleanedValue.partValue === "R") {
|
||||
cleanedValue.sigunValue = "0";
|
||||
cleanedValue.sigunText = "";
|
||||
}
|
||||
@@ -143,7 +144,7 @@ export default function PbnstSttn() {
|
||||
|
||||
useEffect(() => {
|
||||
if (useInitRef.current === true && laeSeCd !== undefined && fclts.length > 0) {
|
||||
pbnSttnInit("1", "15", getFixedSearchValue(ref.current), searchYear, fclts);
|
||||
pbnSttnInit("1", String(DEFAULT_LIST_SIZE), getFixedSearchValue(ref.current), searchYear, fclts);
|
||||
}
|
||||
}, [fclts, laeSeCd, searchYear, pbnSttnInit, getFixedSearchValue]);
|
||||
|
||||
@@ -253,7 +254,7 @@ export default function PbnstSttn() {
|
||||
pbnstSttnSearch(
|
||||
false,
|
||||
"1",
|
||||
String(prfRegPageSize),
|
||||
String(DEFAULT_LIST_SIZE),
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
@@ -276,33 +277,33 @@ export default function PbnstSttn() {
|
||||
<div className="grid grid-cols-6 gap-4 ">
|
||||
<div className="col-span-6">
|
||||
<ListDiv>
|
||||
<div className="h-full w-full overflow-x-scroll min-[1900px]:overflow-x-hidden">
|
||||
<div className="w-[160%] min-[1900px]:w-[100%]">
|
||||
<UiTable
|
||||
headName={undefined}
|
||||
tableData={bdstTable || []}
|
||||
headlist={pbnstSttn.pbnstSttHeadList}
|
||||
pageSize={15}
|
||||
total={pbnstSttnResCount}
|
||||
cellClick={(index: number | undefined) => {
|
||||
cellCLick(index);
|
||||
}}
|
||||
onPageChange={(page: number) => {
|
||||
const fixedSearchValue = getFixedSearchValue(ref.current);
|
||||
pbnstSttnSearch(
|
||||
false,
|
||||
String(page),
|
||||
String(prfRegPageSize),
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
fclts
|
||||
);
|
||||
setIsReset(false);
|
||||
}}
|
||||
resetPageNo={isReset}
|
||||
widthFixed={true}
|
||||
>
|
||||
<div className="h-full w-full overflow-x-scroll min-[2100px]:overflow-x-hidden">
|
||||
<div className="w-[160%] min-[2100px]:w-[100%]">
|
||||
<UiTable
|
||||
headName={undefined}
|
||||
tableData={bdstTable || []}
|
||||
headlist={pbnstSttn.pbnstSttHeadList}
|
||||
pageSize={DEFAULT_LIST_SIZE}
|
||||
total={pbnstSttnResCount}
|
||||
cellClick={(index: number | undefined) => {
|
||||
cellCLick(index);
|
||||
}}
|
||||
onPageChange={(page: number) => {
|
||||
const fixedSearchValue = getFixedSearchValue(ref.current);
|
||||
pbnstSttnSearch(
|
||||
false,
|
||||
String(page),
|
||||
String(DEFAULT_LIST_SIZE),
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
fclts
|
||||
);
|
||||
setIsReset(false);
|
||||
}}
|
||||
resetPageNo={isReset}
|
||||
widthFixed={true}
|
||||
>
|
||||
<HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} />
|
||||
</UiTable>
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,10 @@ 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 COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
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";
|
||||
const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "WR";
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
@@ -35,6 +36,7 @@ async function handler(
|
||||
// silent refresh
|
||||
if (isRefreshHandler) {
|
||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||
console.log("refreshToken", refreshToken?.value);
|
||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||
|
||||
if (!headers[CLAIM_NAME]) {
|
||||
@@ -52,12 +54,11 @@ async function handler(
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// console.log(result);
|
||||
if (!result.ok) {
|
||||
console.log("Invalid Credentials ");
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
{ message: "first Invalid Credentials" },
|
||||
{ status: result.status },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,11 +73,13 @@ async function handler(
|
||||
});
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
response.cookies.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
sameSite: "strict", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정
|
||||
domain: getCookieDomain(),
|
||||
path: "/",
|
||||
});
|
||||
console.log("refresh cookie setting : " + refreshToken);
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -109,3 +112,14 @@ export async function PUT(
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "../src/app";
|
||||
import { setCookie } from "../src/shared/lib/cookie";
|
||||
|
||||
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "WR");
|
||||
export const metadata: Metadata = {
|
||||
title: "물재이용매칭시스템",
|
||||
description: "물재이용매칭 시스템입니다",
|
||||
|
||||
@@ -120,77 +120,62 @@ export const hasuListFetch = async (
|
||||
|
||||
let url = ``;
|
||||
let filteredFclt = flcts;
|
||||
const filterData =
|
||||
reurnValue.sigunValue !== "0"
|
||||
? reurnValue.sigunValue
|
||||
: reurnValue.sidoValue;
|
||||
|
||||
if (filterData !== "0")
|
||||
filteredFclt = flcts.filter((fclt: string) => {
|
||||
return filterData.length == 2
|
||||
? fclt.slice(0, 2) === filterData
|
||||
: fclt.slice(0, 5) === filterData;
|
||||
});
|
||||
if (reurnValue.partValue === "A") {
|
||||
url += `${reurnValue.partValue}/${
|
||||
// reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue
|
||||
reurnValue.sigunValue !== "0"
|
||||
? reurnValue.sigunValue
|
||||
: reurnValue.sidoValue
|
||||
}/0/1/SW`;
|
||||
|
||||
if (filterData !== "0")
|
||||
filteredFclt = flcts.filter((fclt: string) => {
|
||||
return filterData.length == 2
|
||||
? fclt.slice(0, 2) === filterData
|
||||
: fclt.slice(0, 5) === filterData;
|
||||
});
|
||||
} else if (reurnValue.partValue === "R") {
|
||||
url += `${reurnValue.partValue}/${
|
||||
reurnValue.regionValue
|
||||
? reurnValue.regionValue
|
||||
: reurnValue.unitWaterShedValue
|
||||
? reurnValue.unitWaterShedValue
|
||||
: reurnValue.middleRegionValue
|
||||
}/0/1/SW`;
|
||||
} else if (reurnValue.partValue === "G") {
|
||||
url += `${reurnValue.envPartValue}/${reurnValue.sigunValue !== "0" ? reurnValue.sigunValue : reurnValue.sidoValue}/0/1/SW`;
|
||||
if (filterData !== "0")
|
||||
filteredFclt = flcts.filter((fclt: string) => {
|
||||
return filterData.length == 2
|
||||
? fclt.slice(0, 2) === filterData
|
||||
: fclt.slice(0, 5) === filterData;
|
||||
});
|
||||
} else {
|
||||
url += `A/0/0/1/SW`;
|
||||
let nameCd="0", partStr="A", sidogunValue="0";
|
||||
switch (reurnValue.partValue) {
|
||||
case 'R': // 유역별
|
||||
if (reurnValue.regionValue !== '0') {
|
||||
if(reurnValue.unitWaterShedValue==='0') nameCd = reurnValue.regionValue;
|
||||
else if (reurnValue.middleRegionValue==='0') nameCd = reurnValue.unitWaterShedValue;
|
||||
else nameCd = reurnValue.middleRegionValue;
|
||||
}
|
||||
partStr = reurnValue.partValue;
|
||||
break;
|
||||
default:
|
||||
if(reurnValue.sidoValue && reurnValue.sidoValue !== '0') nameCd = reurnValue.sidoValue;
|
||||
if(reurnValue.sigunValue &&reurnValue.sigunValue !== '0') nameCd = reurnValue.sigunValue;
|
||||
sidogunValue = nameCd;
|
||||
partStr = reurnValue.envPartValue !== '0' ? reurnValue.envPartValue:'A';
|
||||
break;
|
||||
}
|
||||
|
||||
if (sidogunValue && sidogunValue !== "0") { // 시도, 시군구 조회 조건이 있다면
|
||||
filteredFclt = flcts.filter((fclt: string) => {
|
||||
return sidogunValue.length == 2
|
||||
? fclt.slice(0, 2) === sidogunValue
|
||||
: fclt.slice(0, 5) === sidogunValue;
|
||||
});
|
||||
}
|
||||
|
||||
url = `${partStr}/${nameCd}/0/1/SW`;
|
||||
|
||||
if (searchNm && searchNm?.length > 0) {
|
||||
url += "/" + searchNm;
|
||||
}
|
||||
// console.log(url);
|
||||
|
||||
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
|
||||
if (filteredFclt.length === 0) {
|
||||
return { data: [], count: 0 };
|
||||
}
|
||||
|
||||
// fo의 하수처리시설을 조회
|
||||
const pbnList = await postFectch(
|
||||
`/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url,
|
||||
filteredFclt,
|
||||
fetchType.COMMON // FO
|
||||
);
|
||||
|
||||
const checkRuseFcltList =
|
||||
searchNm && searchNm?.length > 0
|
||||
? pbnList.data.map((item) => item.bscFctCd)
|
||||
: filteredFclt;
|
||||
|
||||
const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd);
|
||||
// console.log(checkRuseFcltList);
|
||||
|
||||
|
||||
const getRuseList = await postFectch(
|
||||
`/reuseFc/pubDetailList/${year}/${filterData}/${page}/${total}`,
|
||||
`/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`,
|
||||
checkRuseFcltList,
|
||||
fetchType.WR
|
||||
);
|
||||
const checkWrRegListRes = getRuseList.data;
|
||||
// console.log(getRuseList);
|
||||
|
||||
const data: WaterReuseFacility[] = checkWrRegListRes
|
||||
.map((item: FcPrfType) => {
|
||||
@@ -292,13 +277,13 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
|
||||
isExcel: boolean,
|
||||
page: string,
|
||||
total: string,
|
||||
reurnValue: ReturnType,
|
||||
reurnValue: ReturnType, // 검색조건
|
||||
searchNm: string | undefined,
|
||||
year: string,
|
||||
fclts: string[]
|
||||
) => {
|
||||
try {
|
||||
console.log(reurnValue);
|
||||
// console.log(reurnValue);
|
||||
const pbnstSttnRes = await hasuListFetch(
|
||||
page,
|
||||
total,
|
||||
|
||||
@@ -30,6 +30,7 @@ function buildAuthHeaders(req: NextRequest): Headers {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
return requestHeaders;
|
||||
@@ -38,7 +39,9 @@ function buildAuthHeaders(req: NextRequest): Headers {
|
||||
/** 공통: rewrite 응답 생성 (헤더 주입 여부 선택) */
|
||||
function rewriteWith(url: string, req: NextRequest, withAuth = false): NextResponse {
|
||||
return withAuth
|
||||
? NextResponse.rewrite(new URL(url), { headers: buildAuthHeaders(req) })
|
||||
? NextResponse.rewrite(new URL(url), {
|
||||
headers: buildAuthHeaders(req),
|
||||
})
|
||||
: NextResponse.rewrite(new URL(url));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ import koBase from "@/public/locales/ko/message.json";
|
||||
import { StoreProvider as WrStoreProvider } from "@/app/store";
|
||||
//import { LoginApi } from "@/src/entities/auth";
|
||||
//import { useEffect } from "react";
|
||||
import { setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "WR");
|
||||
|
||||
const i18n = i18next.createInstance();
|
||||
i18n.init({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import type { LoginService, ILogin } from "../model/login-interface";
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config";
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, SITE_ID, CLAIM_NAME, ENDPOINT } from "../../../shared/config";
|
||||
|
||||
export class LoginApiService implements LoginService {
|
||||
private static instance: LoginApiService;
|
||||
@@ -18,31 +18,33 @@ export class LoginApiService implements LoginService {
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
tokenManager.clearAuthToken();
|
||||
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
}),
|
||||
});
|
||||
// const data = response.headers.get(ACCESS_TOKEN);
|
||||
const accessToken = response.headers.get(ACCESS_TOKEN);
|
||||
const tokenId = response.headers.get(AUTH_USER_ID);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "Login failed");
|
||||
}
|
||||
|
||||
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.setToken("site-id-key", SITE_ID)
|
||||
tokenManager.setupRefreshTimer(
|
||||
() => this.silentRefresh(),
|
||||
JWT_EXPIRATION_TIME,
|
||||
);
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setToken("site-id-key", SITE_ID);
|
||||
tokenManager.setupRefreshTimer(() => this.silentRefresh());
|
||||
} catch (error) {
|
||||
|
||||
throw new Error(`Login failed: ${error}`);
|
||||
@@ -54,17 +56,19 @@ export class LoginApiService implements LoginService {
|
||||
console.log(' silentRefresh ')
|
||||
|
||||
try {
|
||||
const response = await fetch(PROXY_HOST+
|
||||
`${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "Silent refresh failed");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
@@ -74,6 +78,7 @@ export class LoginApiService implements LoginService {
|
||||
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setupRefreshTimer(() => this.silentRefresh());
|
||||
} catch (error) {
|
||||
tokenManager.clearAuthToken();
|
||||
throw new Error(`Silent refresh failed: ${error}`);
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
import { tokenManager } from "./token-manager";
|
||||
import { client } from "../../../shared/api";
|
||||
import type { IUserInfo } from "../../../shared/store";
|
||||
import { useUserStore } from "../../../shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config";
|
||||
import { ENDPOINT, AUTH_USER_ID, CLAIM_NAME } from "../../../shared/config";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { getCookie } from "../../../shared/lib";
|
||||
|
||||
@@ -17,22 +19,37 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
// 현재 페이지가 로그인 페이지이면 authCheck(쿠키점검) 안함.
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
if (!userId) { // token_id 가 없으면 로그인 페이지로 이동
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
if (userInfo) return;
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const token = getCookie(CLAIM_NAME);
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
|
||||
if (!token || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
tokenManager.setupRefreshTimer(() => LoginApi.silentRefresh());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const isJwtTestPage = pathname?.startsWith("/jwttest");
|
||||
(async () => {
|
||||
try {
|
||||
await authCheck();
|
||||
@@ -40,7 +57,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
console.error(error);
|
||||
setUserInfo(null);
|
||||
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL) || isJwtTestPage) return;
|
||||
|
||||
// const hostname = window.location.hostname;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/src/shared/config";
|
||||
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
type JwtPayload = {
|
||||
@@ -11,6 +11,7 @@ type JwtPayload = {
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
private static readonly REFRESH_BUFFER_MS = 60000; // 60초
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -79,20 +80,37 @@ export class TokenManager {
|
||||
removeCookie(key);
|
||||
}
|
||||
|
||||
public setupRefreshTimer(
|
||||
callback: () => Promise<void>,
|
||||
expirationTime: number,
|
||||
): void {
|
||||
private handleRefreshFailure(error: unknown, message: string): void {
|
||||
console.error(message, error);
|
||||
this.clearAuthToken();
|
||||
}
|
||||
|
||||
public setupRefreshTimer(callback: () => Promise<void>): void {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
const remainingMs = this.getRemainingMsFromToken(CLAIM_NAME);
|
||||
if (remainingMs === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
this.clearAuthToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshDelay = Math.max(
|
||||
remainingMs - TokenManager.REFRESH_BUFFER_MS,
|
||||
0,
|
||||
);
|
||||
|
||||
this.refreshTimer = setTimeout(() => {
|
||||
callback().catch((error) => {
|
||||
console.error("Silent refresh failed:", error);
|
||||
this.clearAuthToken();
|
||||
this.handleRefreshFailure(error, "Silent refresh failed:");
|
||||
});
|
||||
}, expirationTime - 60000);
|
||||
}, refreshDelay);
|
||||
}
|
||||
|
||||
public clearAuthToken(): void {
|
||||
@@ -102,6 +120,7 @@ export class TokenManager {
|
||||
}
|
||||
|
||||
this.removeToken(CLAIM_NAME);
|
||||
this.removeToken(REFRESH_TOKEN);
|
||||
this.removeToken(AUTH_USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,4 +82,4 @@ export const ENDPOINT = {
|
||||
|
||||
};
|
||||
|
||||
export const JWT_EXPIRATION_TIME = 1800000;
|
||||
export const JWT_EXPIRATION_TIME = 3600000;
|
||||
|
||||
@@ -30,15 +30,15 @@ type SearchBaseType = {
|
||||
excelDisabled?: boolean;
|
||||
}
|
||||
export type ReturnType = {
|
||||
partValue: string;
|
||||
sidoValue: string;
|
||||
sigunValue: string;
|
||||
regionValue: string;
|
||||
unitWaterShedValue: string;
|
||||
middleRegionValue: string;
|
||||
envPartValue: string;
|
||||
sidoText: string;
|
||||
sigunText: string;
|
||||
partValue: string; // 조회 구분값(A: 행정, R: 유역, G: 환경청)
|
||||
sidoValue: string; // 선택된 시도 코드
|
||||
sigunValue: string; // 선택된 시군구 코드
|
||||
regionValue: string; // 선택된 권역(유역) 코드
|
||||
unitWaterShedValue: string; // 선택된 단위유역 코드
|
||||
middleRegionValue: string; // 선택된 중권역 코드
|
||||
envPartValue: string; // 선택된 환경청 코드
|
||||
sidoText: string; // 선택된 시도명
|
||||
sigunText: string; // 선택된 시군구명
|
||||
}
|
||||
|
||||
function SearchBaseForm ({
|
||||
|
||||
@@ -201,31 +201,51 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
let selectedEnvPartSido;
|
||||
let selectedEnvPartSigun;
|
||||
|
||||
const resetSelectStateByPartChange = () => {
|
||||
setSidoValue('0');
|
||||
setSigunValue('0');
|
||||
setSidoText('');
|
||||
setSigunText('');
|
||||
setRegionValue('0');
|
||||
setUnitWaterShedValue('0');
|
||||
setMiddleRegionValue('0');
|
||||
setEnvPartValue('0');
|
||||
|
||||
setFilteredSigunData([{ text: "전체", val: '0' }]);
|
||||
setFilteredUnitWaterShedData([{ text: "전체", val: '0' }]);
|
||||
setFilteredMiddleRegionData([{ text: "전체", val: '0' }]);
|
||||
setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd:'' }]);
|
||||
setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd:'' }]);
|
||||
setFilteredEnvPartSigunData([{ text: "전체", val: '0' }]);
|
||||
setFilteredEnvPartSigunDataSelected([]);
|
||||
|
||||
if (setRtnSidoValue) setRtnSidoValue('0');
|
||||
if (setRtnSigunValue) setRtnSigunValue('0');
|
||||
if (setRtnSidoText) setRtnSidoText('');
|
||||
if (setRtnSigunText) setRtnSigunText('');
|
||||
if (setRtnRegionValue) setRtnRegionValue('0');
|
||||
if (setRtnUnitWaterShedValue) setRtnUnitWaterShedValue('0');
|
||||
if (setRtnMiddleRegionValue) setRtnMiddleRegionValue('0');
|
||||
if (setRtnEnvPartValue) setRtnEnvPartValue('0');
|
||||
if (setRtnEnvPartSidoValue) setRtnEnvPartSidoValue('0');
|
||||
if (setRtnEnvPartSigunValue) setRtnEnvPartSigunValue('0');
|
||||
};
|
||||
|
||||
switch(type) {
|
||||
case 'part':
|
||||
setPartValue(val);
|
||||
if (setRtnPartValue) {
|
||||
setRtnPartValue(val);
|
||||
}
|
||||
// 구분
|
||||
if(val == 'R') {
|
||||
initializeMiddleRegionData('0');
|
||||
initializeUnitWaterShedData('0');
|
||||
} else if(val == 'G') {
|
||||
|
||||
resetSelectStateByPartChange();
|
||||
|
||||
if (val == 'G') {
|
||||
setFilteredEnvPartData([
|
||||
{ text: "전체", val: '0', adstDliStrCd:'' },
|
||||
...selectEnvPartData.filter((item: EnvPartSelectDataType) => item.val !== null && item.adstDliStrCd !== '')
|
||||
]);
|
||||
setEnvPartValue('0');
|
||||
initializeCityDataByEnvPartSido('0');
|
||||
} else {
|
||||
initializeSigunData('0');
|
||||
//onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
|
||||
}
|
||||
|
||||
// 다른 selectbox 전체로 초기화
|
||||
//onSelectValue(selectSidoData[0]?.val || '0', 'sido');
|
||||
//onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
|
||||
break;
|
||||
case 'sido':
|
||||
setSidoValue(val);
|
||||
|
||||
Reference in New Issue
Block a user