Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev
This commit is contained in:
@@ -19,11 +19,15 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@common/components": ["../../packages/common/components/dist/"],
|
||||
"@common/network": ["../../packages/common/network/"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"next": "15.5.18",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
import { type AuthPayload, type AuthService } from "@common/auth";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
} from "@common/auth";
|
||||
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import { clearRefreshTimer, setToken, tokenManager } from "../lib/token-manager";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
@@ -22,6 +22,12 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules", ".next", "dist", "coverage", "storybook-static", "public"]
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
"sonner": "^2.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
||||
@@ -5,18 +5,17 @@ import {
|
||||
ENDPOINT,
|
||||
} from "@/shared/config";
|
||||
import {
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
} from "@common/auth";
|
||||
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
} from "../lib/token-manager";
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
|
||||
function completeSession(accessToken: string, tokenId: string): void {
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
+10
-9
@@ -27,17 +27,18 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
".eslintrc.*",
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.js",
|
||||
"**/*.cjs",
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/.*/*"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,26 +22,17 @@
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@common/assets": ["../../../../packages/common/assets/dist/"],
|
||||
"@common/components": ["../../../../packages/common/components/dist/"],
|
||||
"@common/network": ["../../../../packages/common/network/"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
".eslintrc.*",
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.js",
|
||||
"**/*.cjs",
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/.*/*"]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"sonner": "^2.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,13 @@ import {
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
} from "@/shared/config";
|
||||
import { type AuthPayload, type AuthService } from "@common/auth";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
} from "@common/auth";
|
||||
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
+10
-9
@@ -27,17 +27,18 @@
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
".eslintrc.*",
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.js",
|
||||
"**/*.cjs",
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/.*/*"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
|
||||
+1
-2
@@ -19,7 +19,6 @@ NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
|
||||
REACT_EDITOR=atom
|
||||
@@ -17,4 +17,5 @@ DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
|
||||
SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import { NoFooterLayout } from "@common/components";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "../../store/common";
|
||||
import { useFoStore } from "../../store";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
// import { authService } from "@/src/entities/auth";
|
||||
import { authService } from "@/src/entities/auth";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -14,12 +15,20 @@ export default function RootLayout({
|
||||
const { getMenuAuth } = useFoStore((state) => state);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleClick = (id: MenuItem) => {
|
||||
getMenuAuth(id.menuCd);
|
||||
router.push(id.urlPath);
|
||||
};
|
||||
return (
|
||||
<NoFooterLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}>{children}</NoFooterLayout>
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</NoFooterLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
"react-hook-form": "^7.54.2",
|
||||
"zod": "^3.24.2",
|
||||
"axios": "^1.8.1",
|
||||
"proj4": "^2.9.0",
|
||||
"uuid": "^14.0.1"
|
||||
"proj4": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
@@ -1,47 +1,82 @@
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
} from "@/src/shared/config";
|
||||
import { getCookie } from "@/src/shared/lib";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
} from "@common/auth";
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
} from "../lib/token-manager";
|
||||
|
||||
import { clearTabStorePersist } from "@/src/store/tab-store";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
@@ -49,67 +84,32 @@ async function login(payload: AuthPayload): Promise<void> {
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
|
||||
export const authService: AuthService = {
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
@@ -17,3 +17,5 @@ SEWERAGE_API_URL = http://localhost:8883
|
||||
DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
@@ -13,4 +13,5 @@ NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
@@ -20,6 +20,7 @@ import { NoFooterLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import useStore from '../store/useStore';
|
||||
import { authService } from "@/entities/auth";
|
||||
|
||||
export default function MapLayout({
|
||||
children,
|
||||
@@ -57,6 +58,7 @@ export default function MapLayout({
|
||||
return (
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"xlsx-populate": "^1.21.0",
|
||||
"zod": "^3.24.2",
|
||||
|
||||
@@ -1,47 +1,83 @@
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
} from "@/shared/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
} from "@common/auth";
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
} from "../lib/token-manager";
|
||||
|
||||
import { clearTabStorePersist } from "../../../store/tab-store";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
@@ -49,67 +85,32 @@ async function login(payload: AuthPayload): Promise<void> {
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
|
||||
export const authService: AuthService = {
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@rsone/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ENDPOINT, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
|
||||
import {
|
||||
@@ -20,8 +20,10 @@ import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-syn
|
||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
import { authService } from "@/entities/auth";
|
||||
import { getToken } from "@/entities/auth/lib/token-manager";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
@@ -93,6 +95,13 @@ function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken(CLAIM_NAME)) return;
|
||||
void statisticsApi.refreshCommonCodes().catch(() => {
|
||||
// 캐시 동기화 실패는 화면 진입을 막지 않음
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuListData) {
|
||||
setMenuList(menuListData);
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"swr": "^2.3.7",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
|
||||
@@ -69,6 +69,7 @@ function buildSearchFormValues(
|
||||
isDetail: false,
|
||||
isSummary: d.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
capacityGbn: d.capacityGbn || "ALL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ function buildSearchFormValues(
|
||||
isDetail: false,
|
||||
isSummary: d?.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
capacityGbn: d?.capacityGbn || "ALL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -362,6 +364,26 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.capacityGbn && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">구분</Label>
|
||||
<Select
|
||||
value={values.capacityGbn}
|
||||
onValueChange={(value) =>
|
||||
setValues({ ...values, capacityGbn: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='ALL'>전체</SelectItem>
|
||||
<SelectItem value='500톤이상'>500톤이상</SelectItem>
|
||||
<SelectItem value='500톤미만'>500톤미만</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{mergedProps.isDetail && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label cursor-pointer" htmlFor="isDetail">
|
||||
|
||||
@@ -311,15 +311,24 @@ export function VerificationResultDialog({
|
||||
}, [rows, loading]);
|
||||
|
||||
const handleExceptionSubmitClick = useCallback(async () => {
|
||||
if (checkedIndices.size === 0) {
|
||||
alert("작성된 예외 제출 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const gridRows = getGridDataRef.current?.() ?? [];
|
||||
const payload: VerificationExceptionSubmitItem[] = [];
|
||||
checkedIndices.forEach((idx) => {
|
||||
for (const idx of checkedIndices) {
|
||||
const prcsOpnnExpln = String(gridRows[idx]?.prcsOpnnExpln ?? "").trim();
|
||||
if (!prcsOpnnExpln) return;
|
||||
if (!prcsOpnnExpln) {
|
||||
alert("예외 제출 사유를 작성해주세요.");
|
||||
return;
|
||||
}
|
||||
const vrfcSn = Number(rows[idx]?.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) return;
|
||||
if (!Number.isFinite(vrfcSn)) continue;
|
||||
payload.push({ vrfcSn, prcsOpnnExpln });
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.length === 0) {
|
||||
alert("작성된 예외 제출 항목이 없습니다.");
|
||||
return;
|
||||
|
||||
@@ -872,6 +872,7 @@ const ExcretaTreatmentFacilityInp = () => {
|
||||
flushSync(() => {
|
||||
setSggOptions(list);
|
||||
setRowData([...currentRows, newRow]);
|
||||
setIsNotApplicable(false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -12,8 +15,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
export interface MainFacilityMappingSaveDialogValues {
|
||||
@@ -79,6 +84,95 @@ function validateContractRange(start: Date | undefined, end: Date | undefined):
|
||||
return null;
|
||||
}
|
||||
|
||||
const CONTRACT_DATE_END_MONTH = new Date(new Date().getFullYear() + 10, 11);
|
||||
|
||||
function isValidDate(date: Date | undefined) {
|
||||
if (!date) return false;
|
||||
return !Number.isNaN(date.getTime());
|
||||
}
|
||||
|
||||
/** 이 다이얼로그 전용 — 연도 드롭다운을 현재+10년까지 허용 */
|
||||
function ContractDatePicker({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
value?: Date;
|
||||
onChange: (date: Date | undefined) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [month, setMonth] = useState<Date | undefined>(value);
|
||||
const [displayValue, setDisplayValue] = useState<Date | undefined>(value);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(value);
|
||||
setMonth(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={displayValue ? dayjs(displayValue).format("YYYY-MM-DD") : ""}
|
||||
disabled={disabled}
|
||||
max={dayjs(CONTRACT_DATE_END_MONTH).format("YYYY-MM-DD")}
|
||||
className={`bg-background pr-10 ${className ?? ""}`}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
setDisplayValue(newDate);
|
||||
if (isValidDate(newDate)) {
|
||||
onChange(newDate);
|
||||
setMonth(newDate);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="white"
|
||||
disabled={disabled}
|
||||
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
|
||||
>
|
||||
<CalendarIcon className="!size-full" />
|
||||
<span className="sr-only">Select date</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="end"
|
||||
alignOffset={-8}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
captionLayout="dropdown"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
endMonth={CONTRACT_DATE_END_MONTH}
|
||||
onSelect={(newDate) => {
|
||||
onChange(newDate);
|
||||
setDisplayValue(newDate);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MainFacilityMappingSaveDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -220,7 +314,7 @@ export function MainFacilityMappingSaveDialog({
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">시작일</Label>
|
||||
<InputDatePicker
|
||||
<ContractDatePicker
|
||||
value={prxyCtrtStart}
|
||||
onChange={setPrxyCtrtStart}
|
||||
className="w-full"
|
||||
@@ -229,7 +323,7 @@ export function MainFacilityMappingSaveDialog({
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">종료일</Label>
|
||||
<InputDatePicker
|
||||
<ContractDatePicker
|
||||
value={prxyCtrtEnd}
|
||||
onChange={setPrxyCtrtEnd}
|
||||
className="w-full"
|
||||
|
||||
@@ -856,6 +856,7 @@ const PumpingStationInp = () => {
|
||||
flushSync(() => {
|
||||
setSggOptions(list);
|
||||
setRowData([...currentRows, newRow]);
|
||||
setIsNotApplicable(false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1681,12 +1681,6 @@ const SewageExcretaRelatedInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
if (noApplicable) {
|
||||
alert("해당사항 없음이 체크된 상태에서는 항목을 추가할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
let list = sggOptions;
|
||||
if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) {
|
||||
list = await loadSggOptions(ctx.sido);
|
||||
@@ -1715,6 +1709,7 @@ const SewageExcretaRelatedInp = () => {
|
||||
next.set(tabKey, [...currentRows, newRow]);
|
||||
return next;
|
||||
});
|
||||
setIsNotApplicableForTab(tabKey, false);
|
||||
});
|
||||
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
|
||||
@@ -1809,6 +1809,7 @@ const SewageExcretaSlurgeInp = () => {
|
||||
next.set(tabKey, [...currentRows, newRow]);
|
||||
return next;
|
||||
});
|
||||
setIsNotApplicableForTab(tabKey, false);
|
||||
});
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
|
||||
@@ -1426,6 +1426,7 @@ const SewageReservoirPumpInp = () => {
|
||||
next.set(tabKey, [...currentRows, newRow]);
|
||||
return next;
|
||||
});
|
||||
setIsNotApplicableForTab(tabKey, false);
|
||||
});
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
|
||||
@@ -44,6 +44,9 @@ const StatisticsReviewHistory = () => {
|
||||
statsYr: values.year.trim(),
|
||||
...(values.bsnmCd?.trim() ? { sewerBzmnCd: values.bsnmCd.trim() } : {}),
|
||||
...(values.sido && values.sido !== "ALL" ? { ctpvNm: values.sido } : {}),
|
||||
...(values.gugun?.trim() && values.gugun !== "ALL"
|
||||
? { sggNm: values.gugun.trim() }
|
||||
: {}),
|
||||
});
|
||||
|
||||
if (res.status === "success" && Array.isArray(res.data)) {
|
||||
|
||||
@@ -117,7 +117,10 @@ const ARTICLE_EDIT_TAB_KEYS = new Set<string>([
|
||||
TreatWaterReuseBusinessApprStatusInpColumn.tabKey,
|
||||
]);
|
||||
|
||||
const NO_APPLICABLE_TAB_KEYS = ARTICLE_EDIT_TAB_KEYS;
|
||||
const NO_APPLICABLE_TAB_KEYS = new Set<string>([
|
||||
PublicSewageTreatWaterReuseInpColumn.tabKey,
|
||||
...ARTICLE_EDIT_TAB_KEYS,
|
||||
]);
|
||||
|
||||
const WATER_REUSE_TAB_BY_KEY = new Map(
|
||||
WaterReuseInpTabList.map((tab) => [tab.tabKey, tab]),
|
||||
@@ -1073,19 +1076,11 @@ const WaterReuseInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPublicSewageTab) {
|
||||
const currentData = getTabGridRows(tabKey);
|
||||
if (currentData.length === 0) {
|
||||
alert("제출할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getTabGridRows(tabKey);
|
||||
if (!noApplicable && currentData.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getTabGridRows(tabKey);
|
||||
if (!noApplicable && currentData.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm("제출 하시겠습니까?")) {
|
||||
@@ -1179,17 +1174,20 @@ const WaterReuseInp = () => {
|
||||
};
|
||||
|
||||
if (isPublicSewageTab) {
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getTabGridRows(tabKey);
|
||||
const submitList = currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
|
||||
if (submitList.length === 0) {
|
||||
alert("제출할 데이터가 없습니다.");
|
||||
const submitList = noApplicable
|
||||
? []
|
||||
: currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
|
||||
if (!noApplicable && submitList.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
response = await statisticsInputWaterReuseApi.submitPublicSewageTreatWaterReuse({
|
||||
...submitRequestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
noApplicable: false,
|
||||
noApplicable,
|
||||
submitList,
|
||||
});
|
||||
} else {
|
||||
@@ -1420,9 +1418,13 @@ const WaterReuseInp = () => {
|
||||
};
|
||||
|
||||
if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) {
|
||||
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
||||
const currentData = getTabGridRows(tabKey);
|
||||
if (currentData.length === 0) {
|
||||
alert("저장할 데이터가 없습니다.");
|
||||
const saveList = noApplicable
|
||||
? []
|
||||
: currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
|
||||
if (!noApplicable && saveList.length === 0) {
|
||||
alert("해당사항 없음을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1433,8 +1435,8 @@ const WaterReuseInp = () => {
|
||||
await statisticsInputWaterReuseApi.savePublicSewageTreatWaterReuse({
|
||||
...requestBase,
|
||||
userId: userInfo.userUnqId,
|
||||
noApplicable: false,
|
||||
saveList: currentData.map(buildPublicSewageTreatWaterReuseSaveItem),
|
||||
noApplicable,
|
||||
saveList,
|
||||
});
|
||||
|
||||
if (response.status === "success") {
|
||||
@@ -1792,6 +1794,7 @@ const WaterReuseInp = () => {
|
||||
next.set(tabKey, [...currentRows, newRow]);
|
||||
return next;
|
||||
});
|
||||
setIsNotApplicableForTab(tabKey, false);
|
||||
});
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
|
||||
@@ -199,12 +199,6 @@ const StatisticsInputManagement = () => {
|
||||
header: { text: "분류" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "seCd",
|
||||
fieldName: "seCd",
|
||||
header: { text: "구분" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "inptBgngYmd",
|
||||
fieldName: "inptBgngYmd",
|
||||
@@ -230,7 +224,6 @@ const StatisticsInputManagement = () => {
|
||||
const columnLayout = [
|
||||
"statsYr",
|
||||
"seType",
|
||||
"seCd",
|
||||
"inptBgngYmd",
|
||||
"inptEndYmd",
|
||||
PERIOD_DETAIL_FIELD,
|
||||
|
||||
@@ -21,6 +21,7 @@ const PublicSewageFacilityTot = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const isSummaryEnabled = values.isSummary !== 'ALL';
|
||||
const isCapacityGbn = values.capacityGbn !== 'ALL'
|
||||
const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined;
|
||||
|
||||
const param = {
|
||||
@@ -29,6 +30,7 @@ const PublicSewageFacilityTot = () => {
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail,
|
||||
...(isCapacityGbn ? { capacityGbn: values.capacityGbn } : {}),
|
||||
summaryYn: isSummaryEnabled,
|
||||
...(isSummaryEnabled ? { summaryYearCnt } : {}),
|
||||
};
|
||||
@@ -50,7 +52,7 @@ const PublicSewageFacilityTot = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
<SearchForm isDetail={true} capacityGbn={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
|
||||
@@ -141,16 +141,6 @@ export function ApprovalInputDialog({
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">반려 의견 내용</Label>
|
||||
<Textarea
|
||||
value={rjctOpnnCn}
|
||||
onChange={(e) => setRjctOpnnCn(e.target.value)}
|
||||
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="반려 의견 내용을 입력하세요. (선택사항)"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
|
||||
@@ -213,7 +213,14 @@ function StatisticsVerificationReport() {
|
||||
})),
|
||||
});
|
||||
|
||||
const statisticsVerificationReportQuery = useStatsVrfcRptData(filters);
|
||||
const {
|
||||
ctpvSewerBzmnCd: _,
|
||||
sggSewerBzmnCd: __,
|
||||
...statisticsVerificationReportFilters
|
||||
} = filters;
|
||||
const statisticsVerificationReportQuery = useStatsVrfcRptData(
|
||||
statisticsVerificationReportFilters,
|
||||
);
|
||||
const statisticsVerificationResultQuery =
|
||||
useStatsVrfcRsltData(detailParams);
|
||||
|
||||
@@ -399,12 +406,16 @@ function StatisticsVerificationReport() {
|
||||
optionData?: { label: string; value: string },
|
||||
) => {
|
||||
resetReportState();
|
||||
if (key === "sewerBzmnCd") {
|
||||
|
||||
// 시도 변경 시
|
||||
if (key === "ctpvNm") {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
sewerBzmnCd: value,
|
||||
ctpvNm: optionData?.label,
|
||||
sewerBzmnCd: optionData?.value,
|
||||
ctpvSewerBzmnCd: value,
|
||||
sggNm: "",
|
||||
sggSewerBzmnCd: "",
|
||||
}));
|
||||
setSubFilters((prev) => ({
|
||||
...prev,
|
||||
@@ -414,6 +425,18 @@ function StatisticsVerificationReport() {
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 시군구 변경 시
|
||||
if (key === "sggNm") {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
sewerBzmnCd: value,
|
||||
sggNm: optionData?.label,
|
||||
sggSewerBzmnCd: value,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "statsCd") {
|
||||
setSubFilters((prev) => ({ ...prev, statsCd: "" }));
|
||||
}
|
||||
@@ -540,8 +563,8 @@ function StatisticsVerificationReport() {
|
||||
<div className="flex items-center gap-6">
|
||||
<FilterSelect
|
||||
label="시도"
|
||||
value={filters.sewerBzmnCd ?? ""}
|
||||
onChange={handleFilterChange("sewerBzmnCd")}
|
||||
value={filters.ctpvSewerBzmnCd ?? ""}
|
||||
onChange={handleFilterChange("ctpvNm")}
|
||||
options={
|
||||
sidoData?.map((item) => ({
|
||||
label: item.adstNm,
|
||||
@@ -551,7 +574,7 @@ function StatisticsVerificationReport() {
|
||||
/>
|
||||
<FilterSelect
|
||||
label="시군구"
|
||||
value={filters.sggNm ?? ""}
|
||||
value={filters.sggSewerBzmnCd ?? ""}
|
||||
onChange={handleFilterChange("sggNm")}
|
||||
options={
|
||||
sigunguData?.map((item) => ({
|
||||
|
||||
@@ -12,6 +12,7 @@ export type SearchFormValues = {
|
||||
isSummary?: string;
|
||||
/** 분류: 전체(ALL), 입력(INPT), 검증(VRFC) */
|
||||
clsfCd?: SearchFormClsfCd;
|
||||
capacityGbn?: string;
|
||||
};
|
||||
|
||||
export type SearchFormProps = {
|
||||
@@ -24,6 +25,7 @@ export type SearchFormProps = {
|
||||
/** 분류(입력/검증) 셀렉트 표시 */
|
||||
clsf?: boolean;
|
||||
userMenual?: boolean;
|
||||
capacityGbn?: boolean;
|
||||
isDetail?: boolean;
|
||||
isSummary?: boolean;
|
||||
onSubmit?: (values: SearchFormValues) => void;
|
||||
@@ -44,6 +46,7 @@ export const defaultSearchFormProps: Required<
|
||||
fctName: false,
|
||||
clsf: false,
|
||||
userMenual: false,
|
||||
capacityGbn: false,
|
||||
isDetail: false,
|
||||
isSummary: false,
|
||||
isReset: true,
|
||||
|
||||
@@ -5,9 +5,13 @@ import {
|
||||
ENDPOINT,
|
||||
} from "@/shared/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { type AuthPayload, type AuthService } from "@common/auth";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
} from "@common/auth";
|
||||
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type cancelSubmitResponse,
|
||||
type CheckStatusCodeRequest,
|
||||
type CheckStatusCodeResponse,
|
||||
type CodesRefreshResponse,
|
||||
type getReviewOpinionDetailRequest,
|
||||
type getReviewOpinionDetailResponse,
|
||||
type ImrRequest,
|
||||
@@ -368,6 +369,16 @@ export class StatisticsApiService implements StatisticsService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 공통 코드 메모리 캐시 동기화 */
|
||||
public async refreshCommonCodes(): Promise<CodesRefreshResponse> {
|
||||
const response = await stClient.post<CodesRefreshResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/codes-refresh`,
|
||||
null,
|
||||
withAuthHeader(),
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsApi = new StatisticsApiService();
|
||||
|
||||
@@ -455,9 +455,16 @@ export interface StatiscicsManagementReviewHistoryRequest {
|
||||
statsYr: string;
|
||||
sewerBzmnCd?: string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
statsCd?: string;
|
||||
}
|
||||
|
||||
/** 공통 코드 메모리 캐시 동기화 (`INPUTCOMMON/codes-refresh`) 응답 */
|
||||
export interface CodesRefreshResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 통계관리 검토 이력 조회 API `data` 배열 요소 */
|
||||
export interface StatiscicsManagementReviewHistoryItem {
|
||||
statsYr: string;
|
||||
@@ -564,4 +571,5 @@ export interface StatisticsService {
|
||||
StatiscicsExceptionReset(param: StatiscicsExceptionResetRequest): Promise<StatiscicsExceptionResetResponse>;
|
||||
CheckStatusCode(param: CheckStatusCodeRequest): Promise<CheckStatusCodeResponse>;
|
||||
getUnSubmitData(param: unSubmitDataRequest): Promise<unSubmitDataResponse>;
|
||||
refreshCommonCodes(): Promise<CodesRefreshResponse>;
|
||||
}
|
||||
+1
@@ -6,6 +6,7 @@ export interface SewagePipeMaintainTotalRequest {
|
||||
rowDataYn?: boolean;
|
||||
summaryYn?: boolean;
|
||||
summaryYearCnt?: number;
|
||||
capacityGbn?: string;
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairTotalItem {
|
||||
|
||||
+5
-1
@@ -2,7 +2,11 @@ import type { VerifyResultSearchRequest } from "../general-verification-interfac
|
||||
import type { StatsInputStatusListRequestParams } from "../input/statistics-input-status-interface";
|
||||
|
||||
export type StatisticsVerificationFilterState =
|
||||
StatsInputStatusListRequestParams;
|
||||
StatsInputStatusListRequestParams & {
|
||||
// 셀렉트 표시용 시도, 시군구 코드
|
||||
ctpvSewerBzmnCd?: string | null;
|
||||
sggSewerBzmnCd?: string | null;
|
||||
};
|
||||
|
||||
export type StatisticsVerificationSubFilterState = VerifyResultSearchRequest;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { authService } from "@/entities/auth";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { images } from "@common/assets";
|
||||
|
||||
import "./login.css";
|
||||
@@ -21,6 +22,11 @@ function LoginPage() {
|
||||
|
||||
try {
|
||||
await authService.login({ userId, password });
|
||||
try {
|
||||
await statisticsApi.refreshCommonCodes();
|
||||
} catch {
|
||||
// 로그인 성공 후 캐시 동기화 실패는 진입을 막지 않음
|
||||
}
|
||||
window.location.href = "/statistics";
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import type { DatesSetArg } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import dayjs from "dayjs";
|
||||
@@ -12,25 +14,48 @@ import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useDragHandle } from "../sortable-panel";
|
||||
|
||||
// FullCalendar는 클라이언트 전용이므로 dynamic import
|
||||
const FullCalendar = dynamic(() => import("@fullcalendar/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
function getMonthRange(date = dayjs()) {
|
||||
return {
|
||||
schdlBgngYmd: date.startOf("month").format("YYYY-MM-DD"),
|
||||
schdlEndYmd: date.endOf("month").format("YYYY-MM-DD"),
|
||||
};
|
||||
}
|
||||
|
||||
export function ScheduleCalendarPanel() {
|
||||
const { dragHandleProps, isDragging } = useDragHandle();
|
||||
const [dateRange, setDateRange] = useState(getMonthRange);
|
||||
|
||||
// TBD: 초기 날짜 범위 설정 로직
|
||||
const today = dayjs();
|
||||
const startOfMonth = today.startOf("month").format("YYYY-MM-DD");
|
||||
const endOfMonth = today.endOf("month").format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = useSchedules({
|
||||
const { data, isLoading, isFetching } = useSchedules({
|
||||
siteId: SITE_ID,
|
||||
schdlBgngYmd: startOfMonth,
|
||||
schdlEndYmd: endOfMonth,
|
||||
schdlBgngYmd: dateRange.schdlBgngYmd,
|
||||
schdlEndYmd: dateRange.schdlEndYmd,
|
||||
});
|
||||
|
||||
function handleDatesSet(dateInfo: DatesSetArg) {
|
||||
const nextRange = {
|
||||
schdlBgngYmd: dayjs(dateInfo.view.currentStart).format(
|
||||
"YYYY-MM-DD",
|
||||
),
|
||||
schdlEndYmd: dayjs(dateInfo.view.currentEnd)
|
||||
.subtract(1, "day")
|
||||
.format("YYYY-MM-DD"),
|
||||
};
|
||||
|
||||
setDateRange((prev) => {
|
||||
if (
|
||||
prev.schdlBgngYmd === nextRange.schdlBgngYmd &&
|
||||
prev.schdlEndYmd === nextRange.schdlEndYmd
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return nextRange;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
|
||||
<div
|
||||
@@ -40,41 +65,39 @@ export function ScheduleCalendarPanel() {
|
||||
>
|
||||
<h3 className="text-2xl font-semibold">일정관리</h3>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="card-body relative max-h-[calc(100%-3.25rem)] py-2.5">
|
||||
<div className="h-[48rem]">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="ko"
|
||||
headerToolbar={{
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek",
|
||||
}}
|
||||
buttonText={{
|
||||
month: "월",
|
||||
week: "주",
|
||||
}}
|
||||
datesSet={handleDatesSet}
|
||||
events={(data ?? []).map((schedule) => ({
|
||||
id: schedule.schdlSn.toString(),
|
||||
title: schedule.schdlTtl,
|
||||
start: schedule.schdlBgngDt,
|
||||
end: schedule.schdlEndDt,
|
||||
allDay: true,
|
||||
}))}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
{(isLoading || isFetching) && (
|
||||
<div className="bg-background/50 absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="h-[48rem]">
|
||||
<FullCalendar
|
||||
plugins={[dayGridPlugin, timeGridPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
locale="ko"
|
||||
headerToolbar={{
|
||||
left: "prev,next",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek",
|
||||
}}
|
||||
buttonText={{
|
||||
month: "월",
|
||||
week: "주",
|
||||
}}
|
||||
events={data.map((schedule) => ({
|
||||
id: schedule.schdlSn.toString(),
|
||||
title: schedule.schdlTtl,
|
||||
start: schedule.schdlBgngDt,
|
||||
end: schedule.schdlEndDt,
|
||||
// TBD: allDay 판단 기준 (1439분 이상?)
|
||||
allDay: true,
|
||||
}))}
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -83,36 +83,59 @@ export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge size="lg" className="font-semibold">
|
||||
입력기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.inptBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.inptEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
{data.inptSeCd &&
|
||||
data.inptBgngYmd &&
|
||||
data.inptEndYmd ? (
|
||||
<>
|
||||
<Badge
|
||||
size="lg"
|
||||
className="font-semibold"
|
||||
>
|
||||
입력기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.inptBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.inptEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="w-full text-center text-lg text-label">
|
||||
등록된 입력 일정이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-sm border border-border p-3">
|
||||
<Badge
|
||||
size="lg"
|
||||
color="gray"
|
||||
className="font-semibold"
|
||||
>
|
||||
검증기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.vrfcBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.vrfcEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
{data.vrfcSeCd &&
|
||||
data.vrfcBgngYmd &&
|
||||
data.vrfcEndYmd ? (
|
||||
<>
|
||||
<Badge
|
||||
size="lg"
|
||||
color="gray"
|
||||
className="font-semibold"
|
||||
>
|
||||
검증기간
|
||||
</Badge>
|
||||
<p className="text-2xl">
|
||||
{dayjs(data.vrfcBgngYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}{" "}
|
||||
~{" "}
|
||||
{dayjs(data.vrfcEndYmd).format(
|
||||
"YYYY.MM.DD",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="w-full text-center text-lg text-label">
|
||||
등록된 검증 일정이 없습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-4 py-4 tracking-tight">
|
||||
|
||||
+10
-8
@@ -31,13 +31,15 @@
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.js",
|
||||
"**/*.cjs",
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/common/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["**/node_modules", "**/.*/*"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static",
|
||||
"public"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,3 +26,4 @@ NEXT_PUBLIC_HASUDOINFO_IN = https://www.hasudoinfo.or.kr
|
||||
NEXT_PUBLIC_FINDADRESS_URL = https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
|
||||
REACT_EDITOR = atom
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
@@ -21,3 +21,4 @@ NEXT_PUBLIC_HASUDOINFO = https://www.hasudoinfo.or.kr
|
||||
NEXT_PUBLIC_HASUDOINFO_IN = https://www.hasudoinfo.or.kr
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
|
||||
NEXT_PUBLIC_COOKIE_COMAIN=.mind-one.co.kr
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
import { NoFooterLayout } from "@common/components/ui";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { usePathname,useRouter } from "next/navigation";
|
||||
import { useMenuStore } from "@/src/shared/lib";
|
||||
import { useWrStore } from "../store";
|
||||
import { authService } from "@/src/entities/auth";
|
||||
|
||||
export default function MapLayout({
|
||||
children,
|
||||
@@ -14,6 +16,7 @@ export default function MapLayout({
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const changeMenu = (path: MenuItem) => {
|
||||
setCurrentMenuInfo(path);
|
||||
@@ -21,7 +24,12 @@ export default function MapLayout({
|
||||
router.push(path.urlPath);
|
||||
};
|
||||
return (
|
||||
<NoFooterLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={changeMenu}
|
||||
>
|
||||
{children}
|
||||
</NoFooterLayout>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"uuid": "^14.0.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"xlsx-populate": "^1.21.0",
|
||||
"zod": "^3.24.2",
|
||||
|
||||
@@ -1,47 +1,83 @@
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
} from "@/src/shared/config";
|
||||
import { getCookie } from "@/src/shared/lib";
|
||||
import {
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
clearDeviceId,
|
||||
getOrCreateDeviceId,
|
||||
type AuthPayload,
|
||||
type AuthService,
|
||||
getRemainingMs,
|
||||
} from "@common/auth";
|
||||
import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id";
|
||||
import {
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
clearRefreshTimer,
|
||||
setToken,
|
||||
setupRefreshTimer,
|
||||
tokenManager,
|
||||
} from "../lib/token-manager";
|
||||
|
||||
import { clearTabStorePersist } from "@/src/store/tab-store";
|
||||
|
||||
async function login(payload: AuthPayload): Promise<void> {
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
@@ -49,67 +85,32 @@ async function login(payload: AuthPayload): Promise<void> {
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function silentRefresh(): Promise<void> {
|
||||
console.log(`[silentRefresh] Called at ${new Date().toISOString()}`);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Device-Id": getOrCreateDeviceId(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message ?? "토큰 갱신에 실패했습니다.");
|
||||
}
|
||||
|
||||
const accessToken = data[ACCESS_TOKEN];
|
||||
const tokenId = data[AUTH_USER_ID];
|
||||
|
||||
if (!accessToken || !tokenId) {
|
||||
throw new Error("갱신된 인증 토큰을 받지 못했습니다.");
|
||||
}
|
||||
|
||||
setToken(CLAIM_NAME, accessToken);
|
||||
setToken(AUTH_USER_ID, tokenId);
|
||||
|
||||
const remainingMs = getRemainingMs(accessToken);
|
||||
if (remainingMs > 0) {
|
||||
setupRefreshTimer(silentRefresh, remainingMs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[silentRefresh] Failed at ${new Date().toISOString()}`,
|
||||
error,
|
||||
);
|
||||
tokenManager.clearAuthToken();
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
tokenManager.clearAuthToken();
|
||||
clearDeviceId();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
}
|
||||
}
|
||||
|
||||
export const authService: AuthService = {
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
login,
|
||||
silentRefresh,
|
||||
declineSessionExtend: clearRefreshTimer,
|
||||
logout,
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
localStorage.setItem(DEVICE_ID_KEY, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
localStorage.removeItem(DEVICE_ID_KEY);
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules",
|
||||
"coverage",
|
||||
"storybook-static"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "."
|
||||
|
||||
@@ -19,6 +19,7 @@ packages/common/auth/
|
||||
├── types.ts # 타입 정의
|
||||
├── token-manager.ts # 토큰 관리 로직
|
||||
├── cookie.ts # 쿠키 유틸리티
|
||||
├── device-id.ts # 기기 ID (쿠키)
|
||||
├── jwt-utils.ts # JWT 유틸리티
|
||||
└── README.md # 문서
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
|
||||
|
||||
export function getOrCreateDeviceId(): string {
|
||||
const existing = getCookie(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const id = uuidv4();
|
||||
const isHttps =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:";
|
||||
setCookie(DEVICE_ID_KEY, id, {
|
||||
secure: isHttps,
|
||||
sameSite: "lax",
|
||||
domain: COOKIE_DOMAIN,
|
||||
path: "/",
|
||||
expires: 365,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export function clearDeviceId(): void {
|
||||
removeCookie(DEVICE_ID_KEY, { path: "/" });
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export { createTokenManager } from "./token-manager";
|
||||
export type { TokenManager } from "./token-manager";
|
||||
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { clearDeviceId, getOrCreateDeviceId } from "./device-id";
|
||||
export {
|
||||
getJwtExpiry,
|
||||
getRemainingMs,
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
".": "./index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-cookie": "^3.0.5"
|
||||
"js-cookie": "^3.0.5",
|
||||
"uuid": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"types": ["node"],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -19,5 +20,10 @@
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface SiteListResponse {
|
||||
const { PROXY_HOST } = getConfig();
|
||||
const BASE_URL = `${PROXY_HOST}/server`;
|
||||
|
||||
export const getSiteInfo = async (siteId: string) => {
|
||||
export const getSiteInfo = async (siteId: string): Promise<SiteInfo> => {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
|
||||
{
|
||||
@@ -66,7 +66,7 @@ export const getSiteInfo = async (siteId: string) => {
|
||||
credentials: "include",
|
||||
},
|
||||
);
|
||||
return response.json() as Promise<SiteInfo>;
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getSiteList = async (): Promise<SiteListResponse> => {
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./api";
|
||||
export * from "./config";
|
||||
export * from "./lib";
|
||||
export * from "./ui";
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getSiteInfo } from "../api/site";
|
||||
import { getIpList } from "../api/ip";
|
||||
import { ENDPOINT } from "../config";
|
||||
import { getCookie } from "../lib";
|
||||
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const ClientIpGuard = ({
|
||||
currentPath,
|
||||
children,
|
||||
}: {
|
||||
currentPath?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [clientIp, setClientIp] = useState("");
|
||||
const [isAllowed, setIsAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await fetch("https://api.ipify.org?format=json");
|
||||
const data = await response.json();
|
||||
if (!data?.ip) return;
|
||||
setClientIp(data.ip);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!siteId || !clientIp) return;
|
||||
|
||||
const siteInfo = await queryClient.ensureQueryData({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => getSiteInfo(siteId),
|
||||
});
|
||||
|
||||
if (siteInfo.siteKndCd === "O") return;
|
||||
|
||||
const { content: ipList } = await getIpList({
|
||||
siteId,
|
||||
prmYn: "Y",
|
||||
});
|
||||
|
||||
const allowedClientIp = ipList.some((ip) => ip.ipAddr === clientIp);
|
||||
setIsAllowed(allowedClientIp);
|
||||
})();
|
||||
}, [queryClient, clientIp, currentPath]);
|
||||
|
||||
if (!isAllowed) {
|
||||
alert("허용되지 않은 IP로 접근하였습니다.");
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { ClientIpGuard };
|
||||
@@ -3,7 +3,6 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import { HelpDialogProvider } from "../../provider/help-dialog-context";
|
||||
import { ClientIpGuard } from "../../lib/client-ip-guard";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
@@ -29,30 +28,28 @@ export function BaseLayout({
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<ClientIpGuard currentPath={currentPath}>
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header
|
||||
authService={authService}
|
||||
tokenManager={authService ? undefined : tokenManager}
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header
|
||||
authService={authService}
|
||||
tokenManager={authService ? undefined : tokenManager}
|
||||
/>
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
|
||||
<div className="flex-1">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col">
|
||||
<div className="flex-1">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</ClientIpGuard>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import { HelpDialogProvider } from "../../provider/help-dialog-context";
|
||||
import { ClientIpGuard } from "../../lib/client-ip-guard";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
@@ -25,26 +24,24 @@ export function NoFooterLayout({
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<ClientIpGuard currentPath={currentPath}>
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header authService={authService} />
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header authService={authService} />
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
/>
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</ClientIpGuard>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { AuthService, TokenManager } from "@common/auth";
|
||||
import type { AuthService } from "@common/auth";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import { HelpDialogProvider } from "../../provider/help-dialog-context";
|
||||
import { ClientIpGuard } from "../../lib/client-ip-guard";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { Header } from "../header";
|
||||
@@ -14,39 +13,30 @@ export function NoLNBLayout({
|
||||
children,
|
||||
queryClient,
|
||||
authService,
|
||||
tokenManager,
|
||||
currentPath,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
authService?: AuthService;
|
||||
/** @deprecated authService를 사용하세요. authService가 없을 때만 Header legacy logout에 사용됩니다. */
|
||||
tokenManager?: TokenManager;
|
||||
currentPath?: string;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<ClientIpGuard currentPath={currentPath}>
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header
|
||||
authService={authService}
|
||||
tokenManager={authService ? undefined : tokenManager}
|
||||
/>
|
||||
<div className="flex">
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
<PopupProvider>
|
||||
<HelpDialogProvider>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header authService={authService} />
|
||||
<div className="flex">
|
||||
<div className="flex min-h-[calc(100vh-50px)] flex-1 flex-col overflow-auto">
|
||||
<div className="flex-1">{children}</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</ClientIpGuard>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</HelpDialogProvider>
|
||||
</PopupProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,21 +12,22 @@
|
||||
"noEmit": false,
|
||||
"incremental": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["**/node_modules", "**/.*/*", "dist"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static",
|
||||
"public"
|
||||
],
|
||||
"include": [
|
||||
".eslintrc.*",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.cjs",
|
||||
"**/*.mjs",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
".storybook/env.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,10 @@
|
||||
"incremental": false
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage",
|
||||
"storybook-static"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ interface ClientConfig {
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
const DEVICE_ID_KEY = "x-device-id";
|
||||
|
||||
class Client {
|
||||
private static instance: Client;
|
||||
private axiosInstance: AxiosInstance;
|
||||
@@ -26,6 +28,7 @@ class Client {
|
||||
this.axiosInstance.interceptors.request.use((config) => {
|
||||
const token = Cookies.get(CLAIM_NAME);
|
||||
const userId = Cookies.get(AUTH_USER_ID);
|
||||
const deviceId = Cookies.get(DEVICE_ID_KEY);
|
||||
|
||||
if (!token || !userId) {
|
||||
return config;
|
||||
@@ -33,6 +36,7 @@ class Client {
|
||||
|
||||
config.headers[CLAIM_NAME] = token;
|
||||
config.headers[AUTH_USER_ID] = userId;
|
||||
config.headers["X-Device-Id"] = deviceId;
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext"
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "coverage", "storybook-static"]
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
Generated
+3
-21
@@ -195,9 +195,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.25.76
|
||||
@@ -352,9 +349,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.25.76
|
||||
@@ -639,9 +633,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.25.76
|
||||
@@ -951,9 +942,6 @@ importers:
|
||||
select:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
webpack:
|
||||
specifier: ^5.98.0
|
||||
version: 5.105.4
|
||||
@@ -1078,9 +1066,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
@@ -1280,9 +1265,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
@@ -1440,9 +1422,6 @@ importers:
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
xlsx:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
@@ -1495,6 +1474,9 @@ importers:
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
uuid:
|
||||
specifier: ^14.0.1
|
||||
version: 14.0.1
|
||||
devDependencies:
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
|
||||
+8
-1
@@ -18,5 +18,12 @@
|
||||
"incremental": true,
|
||||
"newLine": "lf"
|
||||
},
|
||||
"exclude": ["**/node_modules", "**/.*/"]
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
"**/.*/",
|
||||
"**/dist",
|
||||
"**/coverage",
|
||||
"**/storybook-static",
|
||||
"**/.next"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user