Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev

This commit is contained in:
2026-08-03 10:55:33 +09:00
78 changed files with 833 additions and 739 deletions
+9 -5
View File
@@ -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"]
}
-1
View File
@@ -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);
}
+8 -2
View File
@@ -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"]
}
-1
View File
@@ -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
View File
@@ -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"
]
}
+10 -19
View File
@@ -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"
]
}
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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
+11 -2
View File
@@ -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>
);
}
+1 -2
View File
@@ -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",
+79 -79
View File
@@ -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);
}
+2
View File
@@ -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
+2 -1
View File
@@ -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
+2
View File
@@ -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}
>
-1
View File
@@ -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",
+79 -78
View File
@@ -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);
-1
View File
@@ -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>;
}
@@ -6,6 +6,7 @@ export interface SewagePipeMaintainTotalRequest {
rowDataYn?: boolean;
summaryYn?: boolean;
summaryYearCnt?: number;
capacityGbn?: string;
}
export interface SewagePipeMaintainRepairTotalItem {
@@ -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
View File
@@ -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"
]
}
+1
View File
@@ -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
+1
View File
@@ -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
+9 -1
View File
@@ -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>
);
-1
View File
@@ -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",
+79 -78
View File
@@ -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);
}
+7 -1
View File
@@ -1,6 +1,12 @@
{
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"exclude": [
"dist",
"build",
"node_modules",
"coverage",
"storybook-static"
],
"compilerOptions": {
"moduleResolution": "node",
"baseUrl": "."
+1
View File
@@ -19,6 +19,7 @@ packages/common/auth/
├── types.ts # 타입 정의
├── token-manager.ts # 토큰 관리 로직
├── cookie.ts # 쿠키 유틸리티
├── device-id.ts # 기기 ID (쿠키)
├── jwt-utils.ts # JWT 유틸리티
└── README.md # 문서
```
+27
View File
@@ -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: "/" });
}
+1
View File
@@ -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,
+2 -1
View File
@@ -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",
+7 -1
View File
@@ -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"
]
}
+2 -2
View File
@@ -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> => {
+2
View File
@@ -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>
);
}
+9 -8
View File
@@ -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"
]
}
+6 -1
View File
@@ -9,5 +9,10 @@
"incremental": false
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules",
"dist",
"coverage",
"storybook-static"
]
}
+4
View File
@@ -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;
});
+4 -3
View File
@@ -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"]
}
+1
View File
@@ -0,0 +1 @@
declare module "*.css";
+3 -21
View File
@@ -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
View File
@@ -18,5 +18,12 @@
"incremental": true,
"newLine": "lf"
},
"exclude": ["**/node_modules", "**/.*/"]
"exclude": [
"**/node_modules",
"**/.*/",
"**/dist",
"**/coverage",
"**/storybook-static",
"**/.next"
]
}