diff --git a/apps/as/app/api/proxy/[...url]/route.ts b/apps/as/app/api/proxy/[...url]/route.ts index 15abf4c72..b32008942 100644 --- a/apps/as/app/api/proxy/[...url]/route.ts +++ b/apps/as/app/api/proxy/[...url]/route.ts @@ -5,6 +5,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, + COOKIE_DOMAIN, REFRESH_TOKEN, SERVER_API_URL, SITE_ID, @@ -20,6 +21,10 @@ async function handler( "Content-Type": "application/json", "X-Site-Id": SITE_ID, }; + const deviceId = request.headers.get("X-Device-Id"); + if (deviceId) { + headers["X-Device-Id"] = deviceId; + } let body = {}; const isRefreshHandler = params.url.includes("refresh"); @@ -39,27 +44,13 @@ async function handler( } const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`; - console.log(`[Proxy API] ${method} ${params.url.join("/")} -> ${targetUrl}`); - console.log(`[Proxy API] Headers:`, headers); - if (!isRefreshHandler) { - console.log(`[Proxy API] Body:`, body); - } - const result = await fetch(targetUrl, { method, headers, body: JSON.stringify(body), }); - console.log(`[Proxy API] Response Status: ${result.status}`); - console.log(`[Proxy API] Response Headers:`, { - [ACCESS_TOKEN]: result.headers.get(ACCESS_TOKEN), - [REFRESH_TOKEN]: result.headers.get(REFRESH_TOKEN) ? '***hidden***' : undefined, - [AUTH_USER_ID]: result.headers.get(AUTH_USER_ID), - }); - if (!result.ok) { - console.error(`[Proxy API] Request failed with status ${result.status}`); return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 }); } @@ -68,24 +59,16 @@ async function handler( const userId = result.headers.get(AUTH_USER_ID); if (accessToken && userId) { - console.log(`[Proxy API] Login successful for user: ${userId}`); const response = NextResponse.json({ [ACCESS_TOKEN]: accessToken, [AUTH_USER_ID]: userId, }); - const forwardedHost = request.headers.get("x-forwarded-host"); - const host = request.headers.get("host"); - const actualHost = forwardedHost || host || new URL(request.url).hostname.toString(); - - // 운영 환경에서 domain 설정 - const domain = actualHost.includes("hasuon.or.kr") ? ".hasuon.or.kr" : actualHost; - if (refreshToken) { cookieStore.set(REFRESH_TOKEN, refreshToken, { httpOnly: true, sameSite: "strict", - domain: domain, + domain: getCookieDomain(), path: "/", }); } @@ -109,3 +92,14 @@ export async function PUT(request: Request, props: { params: Promise<{ url: stri const params = await props.params; return handler(request, params, "PUT"); } + +function getCookieDomain(): string | undefined { + if (typeof window === "undefined") return undefined; + + if (COOKIE_DOMAIN) return COOKIE_DOMAIN; + + const { hostname } = window.location; + if (hostname === "localhost" || hostname === "127.0.0.1") return undefined; + + return hostname; +} diff --git a/apps/as/package.json b/apps/as/package.json index 71addd9fe..e88c78f02 100644 --- a/apps/as/package.json +++ b/apps/as/package.json @@ -39,6 +39,7 @@ "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": { diff --git a/apps/as/src/entities/auth/api/auth-service.ts b/apps/as/src/entities/auth/api/auth-service.ts index 97b664d6f..934471c50 100644 --- a/apps/as/src/entities/auth/api/auth-service.ts +++ b/apps/as/src/entities/auth/api/auth-service.ts @@ -1,6 +1,7 @@ import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config"; import { type AuthPayload, type AuthService, getRemainingMs } from "@common/auth"; +import { clearDeviceId, getOrCreateDeviceId } from "../lib/device-id"; import { clearRefreshTimer, removeToken, @@ -12,7 +13,10 @@ import { async function login(payload: AuthPayload): Promise { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-Device-Id": getOrCreateDeviceId(), + }, body: JSON.stringify(payload), }); @@ -39,6 +43,9 @@ async function silentRefresh(): Promise { try { const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`, { method: "PUT", + headers: { + "X-Device-Id": getOrCreateDeviceId(), + }, }); const data = await response.json(); @@ -74,6 +81,7 @@ async function logout(): Promise { tokenManager.clearAuthToken(); removeToken("site-id-key"); removeToken("site-theme-key"); + clearDeviceId(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("auth:logout")); diff --git a/apps/as/src/entities/auth/lib/device-id.ts b/apps/as/src/entities/auth/lib/device-id.ts new file mode 100644 index 000000000..ce173b141 --- /dev/null +++ b/apps/as/src/entities/auth/lib/device-id.ts @@ -0,0 +1,17 @@ +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); +} diff --git a/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts b/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts index f82fc9a96..55e0250bc 100644 --- a/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts +++ b/apps/as/src/pages/checklist-management/model/build-rvw-display-rows.ts @@ -49,21 +49,15 @@ export function formatTreeBranchPrefix(depth: number): string { return ""; } -function compareSortSeq(a: CklstRvwRow, b: CklstRvwRow): number { - return (a.sortSeq ?? 0) - (b.sortSeq ?? 0); -} - export function buildRvwDisplayRows(rows: CklstRvwRow[]): CklstRvwDisplayRow[] { const result: CklstRvwDisplayRow[] = []; - const roots = rows - .filter((row) => (row.rvwCnLvNo ?? 1) === 1 && row.upCklstRvwSn == null) - .sort(compareSortSeq); + const roots = rows.filter((row) => (row.rvwCnLvNo ?? 1) === 1 && row.upCklstRvwSn == null); function appendSubtree(parent: CklstRvwRow, depth: number) { - const children = rows - .filter((row) => row.upCklstRvwSn != null && row.upCklstRvwSn === parent.cklstRvwSn) - .sort(compareSortSeq); + const children = rows.filter( + (row) => row.upCklstRvwSn != null && row.upCklstRvwSn === parent.cklstRvwSn, + ); for (const child of children) { result.push({ row: child, depth }); @@ -82,7 +76,7 @@ export function buildRvwDisplayRows(rows: CklstRvwRow[]): CklstRvwDisplayRow[] { (row) => !result.some((display) => display.row.rowKey === row.rowKey), ); - for (const orphan of orphans.sort(compareSortSeq)) { + for (const orphan of orphans) { result.push({ row: orphan, depth: Math.max(0, (orphan.rvwCnLvNo ?? 1) - 1) }); } diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx index 06f5ca6f0..bf9884324 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx @@ -279,13 +279,13 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { ) : ( - - + + - - + + @@ -293,7 +293,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) { 주요 검토내용 사용 여부 체크 사용 - 순서 + 정렬 순서 구분 관리 diff --git a/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx b/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx index 4a83d392f..c31a6ee2a 100644 --- a/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx +++ b/apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx @@ -107,19 +107,18 @@ function CklstRvwRowComponent({ - + onChange({ - sortSeq: e.target.value === "" ? undefined : Number(e.target.value), + sortSeq: e.target.value ? Number(e.target.value) : undefined, isDirty: true, }) } - placeholder="순서" - className="w-full text-center" - min={0} + className="w-20" /> {levelLabel} diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts index 332ef9e14..276fe6e9c 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts @@ -10,7 +10,10 @@ export const fnrsCnsltnRegistSchema = z.object({ cfmtnBgtBizNo: z.string().optional(), bgtAplyYr: z.string().optional(), dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), - dsgnrTelno: z.string().min(1, "설계자 전화번호는 필수입니다"), + dsgnrTelno: z + .string() + .min(1, "설계자 전화번호는 필수입니다") + .regex(/^[0-9-]+$/, "숫자와 하이픈(-)만 입력 가능합니다"), dsgnrOgdpNm: z.string().min(1, "설계자 소속은 필수입니다"), dsgnrEmlAddr: z .string() diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx index 4e0100089..1e4d186e6 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/checklist-task-panel.tsx @@ -2,6 +2,7 @@ import type { Dispatch, SetStateAction } from "react"; import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn"; import { + Button, cn, Table, TableBody, @@ -36,6 +37,24 @@ function ChecklistTaskPanel({ const currentUserReviewStep = userRoleCode ? ROLE_TO_REVIEW_STEP[userRoleCode] : undefined; const isStepEditable = currentUserReviewStep === selectedReviewStep; + function handleSelectAll(value: "Y" | "N") { + if (!isStepEditable) return; + setFormState((prev) => { + const nextResults = { ...prev.results }; + selectedRows + .filter((row) => row.bchckUseYn === "Y") + .forEach((row) => { + nextResults[buildChecklistRowKey(row, selectedReviewStep)] = value; + }); + return patchChecklistFormState( + prev, + { results: nextResults }, + allRows, + selectedReviewStep, + ); + }); + } + return ( <>
@@ -54,8 +73,27 @@ function ChecklistTaskPanel({ ) : null} - 예 - 아니오 + + + + + + @@ -131,7 +169,7 @@ function ChecklistTaskPanel({