Merge branch 'iipbs/as/dev' into feature/0612_sentry_fr_merge
# Conflicts: # apps/as/src/pages/checklist-management/ui/cklst-rvw-grid.tsx # apps/as/src/pages/checklist-management/ui/cklst-rvw-row.tsx
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
tokenManager.clearAuthToken();
|
||||
removeToken("site-id-key");
|
||||
removeToken("site-theme-key");
|
||||
clearDeviceId();
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.dispatchEvent(new CustomEvent("auth:logout"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
|
||||
|
||||
@@ -279,13 +279,13 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
) : (
|
||||
<Table className="w-full">
|
||||
<colgroup>
|
||||
<col width="18%" />
|
||||
<col width="37%" />
|
||||
<col width="16%" />
|
||||
<col width="36%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="8%" />
|
||||
<col width="6%" />
|
||||
<col width="11%" />
|
||||
<col width="5%" />
|
||||
<col width="15%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -293,7 +293,7 @@ function CklstRvwGrid({ selectedCklst }: CklstRvwGridProps) {
|
||||
<TableHead>주요 검토내용</TableHead>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableHead>체크 사용</TableHead>
|
||||
<TableHead className="text-center">순서</TableHead>
|
||||
<TableHead>정렬 순서</TableHead>
|
||||
<TableHead>구분</TableHead>
|
||||
<TableHead>관리</TableHead>
|
||||
</TableRow>
|
||||
|
||||
@@ -107,19 +107,18 @@ function CklstRvwRowComponent({
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell align="center">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
value={row.sortSeq ?? ""}
|
||||
onChange={(e) =>
|
||||
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"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">{levelLabel}</TableCell>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
@@ -54,8 +73,27 @@ function ChecklistTaskPanel({
|
||||
) : null}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="w-[9rem]">예</TableHead>
|
||||
<TableHead className="w-[9rem]">아니오</TableHead>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("Y")}
|
||||
>
|
||||
전체 예
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("N")}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -131,7 +169,7 @@ function ChecklistTaskPanel({
|
||||
<TableCell className="text-left">
|
||||
<Textarea
|
||||
className={cn(
|
||||
"min-h-[6rem] !text-xl",
|
||||
"min-h-[6rem] w-[32rem] !text-xl",
|
||||
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
|
||||
)}
|
||||
value={formState.comments[key] ?? ""}
|
||||
|
||||
@@ -9,7 +9,10 @@ export const instlSncnRegistSchema = z.object({
|
||||
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
|
||||
sncnSeComCd: z.string().min(1, "사업구분은 필수입니다"),
|
||||
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()
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -35,6 +36,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 (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
@@ -53,8 +72,27 @@ function ChecklistTaskPanel({
|
||||
) : null}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="w-[9rem]">예</TableHead>
|
||||
<TableHead className="w-[9rem]">아니오</TableHead>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("Y")}
|
||||
>
|
||||
전체 예
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!isStepEditable}
|
||||
onClick={() => handleSelectAll("N")}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -130,7 +168,7 @@ function ChecklistTaskPanel({
|
||||
<TableCell className="text-left">
|
||||
<Textarea
|
||||
className={cn(
|
||||
"min-h-[6rem] !text-xl",
|
||||
"min-h-[6rem] w-[32rem] !text-xl",
|
||||
isStepEditable ? "cursor-pointer" : "cursor-not-allowed",
|
||||
)}
|
||||
value={formState.comments[key] ?? ""}
|
||||
|
||||
@@ -9,7 +9,6 @@ export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-s
|
||||
export {
|
||||
buildChecklistTabs,
|
||||
isFormChecklistComplete,
|
||||
orderCklstTreeNodes,
|
||||
syncCklstResultListFromTree,
|
||||
} from "./lib/checklist-form-utils";
|
||||
export type { FnrsSharItemDef, FnrsSharListItem } from "./lib/fnrs-shar-form-utils";
|
||||
|
||||
@@ -19,3 +19,14 @@ export function createEtcUploadKey(file: File): string {
|
||||
export function isEtcFileKey(fileKey: string): boolean {
|
||||
return fileKey.startsWith(ETC_FILE_KEY_PREFIX);
|
||||
}
|
||||
|
||||
export function toFileAccept(rmrk: string | null | undefined): string {
|
||||
if (!rmrk?.trim()) return "";
|
||||
|
||||
return rmrk
|
||||
.split(",")
|
||||
.map((ext) => ext.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
@@ -26,41 +26,6 @@ interface CklstResultListItem {
|
||||
picRvwCn?: string | null;
|
||||
}
|
||||
|
||||
function compareCklstSiblings(a: CklstTreeNode, b: CklstTreeNode): number {
|
||||
if (a.cklstArtclSn !== b.cklstArtclSn) {
|
||||
return a.cklstArtclSn - b.cklstArtclSn;
|
||||
}
|
||||
if (a.sortSeq !== b.sortSeq) {
|
||||
return b.sortSeq - a.sortSeq;
|
||||
}
|
||||
return a.cklstRvwSn - b.cklstRvwSn;
|
||||
}
|
||||
|
||||
export function orderCklstTreeNodes(rows: CklstTreeNode[]): CklstTreeNode[] {
|
||||
const childMap = new Map<number | null, CklstTreeNode[]>();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const parent = row.upCklstRvwSn ?? null;
|
||||
if (!childMap.has(parent)) childMap.set(parent, []);
|
||||
childMap.get(parent)!.push(row);
|
||||
});
|
||||
|
||||
childMap.forEach((siblings) => siblings.sort(compareCklstSiblings));
|
||||
|
||||
const ordered: CklstTreeNode[] = [];
|
||||
|
||||
function walk(parent: number | null) {
|
||||
(childMap.get(parent) ?? []).forEach((child) => {
|
||||
ordered.push(child);
|
||||
walk(child.cklstRvwSn);
|
||||
});
|
||||
}
|
||||
|
||||
walk(null);
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function groupCklstTreeByTaskSeComCd(cklstTree: CklstTreeNode[]): Map<string, CklstTreeNode[]> {
|
||||
const grouped = new Map<string, CklstTreeNode[]>();
|
||||
|
||||
@@ -81,7 +46,7 @@ function getOrderedSelectableNodes(
|
||||
const grouped = groupCklstTreeByTaskSeComCd(cklstTree);
|
||||
|
||||
return taskSeComCdArray.flatMap((code) =>
|
||||
orderCklstTreeNodes(grouped.get(code) ?? []).filter((node) => node.bchckUseYn === "Y"),
|
||||
(grouped.get(code) ?? []).filter((node) => node.bchckUseYn === "Y"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,7 +63,7 @@ export function buildChecklistTabs(
|
||||
return orderedCodes.map((code) => ({
|
||||
code,
|
||||
label: taskCodeLabels[code] || code,
|
||||
items: orderCklstTreeNodes(grouped.get(code) ?? []).map((node) => ({
|
||||
items: (grouped.get(code) ?? []).map((node) => ({
|
||||
cklstSn: node.cklstSn,
|
||||
cklstArtclSn: node.cklstArtclSn,
|
||||
cklstRvwSn: node.cklstRvwSn,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
createUploadKey,
|
||||
type FieldPrefix,
|
||||
isEtcFileKey,
|
||||
toFileAccept,
|
||||
} from "../lib/attachments-form-utils";
|
||||
|
||||
import { SectionCollapsible } from "./section-collapsible";
|
||||
@@ -373,7 +374,8 @@ function AttachmentsFormSection({
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="60%" />
|
||||
<col width="50%" />
|
||||
<col width="10%" />
|
||||
<col width="40%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
@@ -381,6 +383,7 @@ function AttachmentsFormSection({
|
||||
<TableHead className="w-[30rem]" align="left">
|
||||
파일명
|
||||
</TableHead>
|
||||
<TableHead>파일 확장자</TableHead>
|
||||
<TableHead>업로드된 파일</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -401,6 +404,7 @@ function AttachmentsFormSection({
|
||||
>
|
||||
{file.esntlAtchFileNm}
|
||||
</TableHead>
|
||||
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
|
||||
<TableCell className="space-y-2">
|
||||
{uploadedFiles[file.esntlAtchFileId]?.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
@@ -432,6 +436,7 @@ function AttachmentsFormSection({
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
|
||||
onChange={(e) => {
|
||||
void handleEssentialFileUpload(
|
||||
file.esntlAtchFileId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
|
||||
import { Field, FieldLabel } from "@/shared/ui/field";
|
||||
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
@@ -196,31 +196,33 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
검토항목
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2}>주요 검토내용</TableHead>
|
||||
<TableHead className="w-[16rem]">검토</TableHead>
|
||||
<TableHead colSpan={2}>검토 결과</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "Y");
|
||||
}}
|
||||
>
|
||||
전체 예
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "N");
|
||||
}}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
</div>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "Y");
|
||||
}}
|
||||
>
|
||||
전체 예
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead className="w-[12rem]">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelectAll(tab.code, "N");
|
||||
}}
|
||||
className="!bg-destructive"
|
||||
>
|
||||
전체 아니오
|
||||
</Button>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -236,64 +238,72 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
style={{
|
||||
paddingLeft: `${Math.max(0, item.treeLevel - 1) * 16 + 8}px`,
|
||||
}}
|
||||
colSpan={showRadios ? 0 : 2}
|
||||
colSpan={showRadios ? 0 : 3}
|
||||
>
|
||||
{item.treeLevel > 1 ? "└ " : ""}
|
||||
{item.content}
|
||||
</TableCell>
|
||||
{showRadios && resultIdx >= 0 ? (
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={`cklstResultList.${resultIdx}.selected`}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FieldSet>
|
||||
<Controller
|
||||
name={`cklstResultList.${resultIdx}.selected`}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<>
|
||||
<TableCell className="text-center">
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value || undefined}
|
||||
value={field.value === "Y" ? "Y" : undefined}
|
||||
onValueChange={field.onChange}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<div className="flex w-full justify-center gap-6">
|
||||
<FieldLabel
|
||||
htmlFor={`check-${item.cklstRvwSn}-Y`}
|
||||
className="!w-auto border-none"
|
||||
<FieldLabel
|
||||
htmlFor={`check-${item.cklstRvwSn}-Y`}
|
||||
className="!w-auto justify-center border-none"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-auto"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-auto"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="Y"
|
||||
id={`check-${item.cklstRvwSn}-Y`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
<span className="font-normal">예</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel
|
||||
htmlFor={`check-${item.cklstRvwSn}-N`}
|
||||
className="!w-auto border-none"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-auto"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`check-${item.cklstRvwSn}-N`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
/>
|
||||
<span className="font-normal">아니오</span>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</div>
|
||||
<RadioGroupItem
|
||||
value="Y"
|
||||
id={`check-${item.cklstRvwSn}-Y`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-label="예"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value === "N" ? "N" : undefined}
|
||||
onValueChange={field.onChange}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<FieldLabel
|
||||
htmlFor={`check-${item.cklstRvwSn}-N`}
|
||||
className="!w-auto justify-center border-none"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-auto"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`check-${item.cklstRvwSn}-N`}
|
||||
aria-invalid={fieldState.invalid}
|
||||
aria-label="아니오"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -70,6 +70,8 @@ function ContactInfoFormSection({ fieldPrefix }: ContactInfoFormSectionProps) {
|
||||
id={field.name}
|
||||
placeholder="설계자 전화번호"
|
||||
aria-invalid={fieldState.invalid}
|
||||
maxLength={13}
|
||||
onChange={(e) => field.onChange(e.target.value.replace(/[^0-9-]/g, ""))}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
|
||||
Generated
+10
@@ -213,6 +213,9 @@ 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
|
||||
@@ -8114,8 +8117,13 @@ packages:
|
||||
utrie@1.0.2:
|
||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||
|
||||
uuid@14.0.1:
|
||||
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
@@ -15046,6 +15054,8 @@ snapshots:
|
||||
dependencies:
|
||||
base64-arraybuffer: 1.0.2
|
||||
|
||||
uuid@14.0.1: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
|
||||
Reference in New Issue
Block a user