[st frontend] 조회시 체크리스트 3개 부분 조회 처리로 수정함

This commit is contained in:
2026-06-01 14:18:28 +09:00
parent 6432b9f44a
commit d4302550b4
5 changed files with 385 additions and 59 deletions
@@ -1,3 +1,5 @@
import { CnfmRegistPage } from "@/pages/instl-sncn";
export default CnfmRegistPage;
export default function Page(props: any) {
return <CnfmRegistPage {...props} />;
}
@@ -6,6 +6,7 @@ import type {
InstlSncnMngSearchParams,
InstlSncnMngService,
SpringPage,
InstlSncnMngDetResponse,
} from "../model/instl-sncn-interface";
export class InstlSncnApiService implements InstlSncnMngService {
@@ -16,8 +17,8 @@ export class InstlSncnApiService implements InstlSncnMngService {
return response.data;
}
public async getById(id: string): Promise<InstlSncnMng> {
const response = await client.get<InstlSncnMng>(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`);
public async getById(id: string): Promise<InstlSncnMngDetResponse> {
const response = await client.get<InstlSncnMngDetResponse>(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`);
return response.data;
}
@@ -105,8 +105,19 @@ export interface InstlSncnApplySaveRequestDto {
esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[];
}
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
export interface InstlSncnMngDetResponse {
detDto: InstlSncnMng;
bizSclList: any[];
bizCstList: any[];
cklstResultMap?: Record<ReviewStepCode, any[]>;
esntlFileList: any[];
etcFileList: any[];
}
export interface InstlSncnMngService {
getPage: (params: InstlSncnMngSearchParams) => Promise<SpringPage<InstlSncnMng>>;
getById: (id: string) => Promise<InstlSncnMng>;
getById: (id: string) => Promise<InstlSncnMngDetResponse>;
saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise<string>;
}
@@ -70,6 +70,56 @@ const num = (v: string) => parseFloat(v.replace(/,/g, "") || "0");
const fmt = (v: number) => (isNaN(v) ? "" : v.toLocaleString());
const DEFAULT_RVW_STP_COM_CD = "LOCAL"; //검토단계공통코드: RVWSTP ( 상세 코드 : LOCAL.지자체,ENV.환경청,KECO.환경공단)
const DEFAULT_SNCN_SE_COM_CD = "CNFM";
type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
ROLE_USER05: "LOCAL",
ROLE_USER04: "LOCAL",
ROLE_USER03: "ENV",
ROLE_USER02: "KECO",
ROLE_USER01: "KECO",
};
const REVIEW_STEP_ROLE_RANK: Record<ReviewStepCode, number> = {
LOCAL: 1,
ENV: 2,
KECO: 3,
};
const INSTL_PROGRESS_ORDER = [
"SV",
"ST",
"LE",
"ER",
"EE",
"TE",
"TS",
"TR",
"TP",
"EP",
"TA",
"TB",
];
const getProgressStepCount = (prgrsSttsComCd?: string): number => {
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd ?? "ST");
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
return 3;
};
const getRoleStepCount = (dtlInstCd?: string): number => {
const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined;
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
};
const normalizeChecklistResultRows = (payload: any): any[] => {
// support new API field `cklstResultMap` while keeping backward-compatibility
const raw = payload?.cklstResultMap ?? payload?.cklstResultList;
if (Array.isArray(raw)) return raw;
if (raw && typeof raw === "object") {
return Object.values(raw).flatMap((rows) => (Array.isArray(rows) ? rows : []));
}
return [];
};
/* ------------------------------------------------------------------ */
/* 메인 컴포넌트 */
@@ -106,6 +156,35 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const alert = useAlertStore((state) => state.setMessage);
const qc = useQueryClient();
const currentDet = useMemo(() => initialData?.detDto ?? initialData, [initialData]);
const isModifyMode = Boolean(currentDet?.instlSncnDocNo);
const currentPrgrsSttsComCd = (currentDet?.prgrsSttsComCd as string | undefined) ?? "ST";
const roleBasedMaxStepCount = getRoleStepCount(userInfo?.dtlInstCd);
const progressBasedMaxStepCount = getProgressStepCount(currentPrgrsSttsComCd);
const visibleStepCount = isModifyMode
? Math.min(progressBasedMaxStepCount, roleBasedMaxStepCount)
: 1;
const visibleReviewStepCodes = useMemo(
() => REVIEW_STEP_CODES.slice(0, visibleStepCount),
[visibleStepCount],
);
const editableReviewStep = useMemo(() => {
if (!isModifyMode) return "LOCAL" as ReviewStepCode;
const step = userInfo?.dtlInstCd ? ROLE_TO_REVIEW_STEP[userInfo.dtlInstCd] : undefined;
if (!step) return null;
return visibleReviewStepCodes.includes(step) ? step : null;
}, [isModifyMode, userInfo?.dtlInstCd, visibleReviewStepCodes]);
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
useEffect(() => {
if (visibleReviewStepCodes.length === 0) return;
if (editableReviewStep && selectedReviewStep !== editableReviewStep) {
setSelectedReviewStep(editableReviewStep);
return;
}
if (!visibleReviewStepCodes.includes(selectedReviewStep)) {
setSelectedReviewStep(visibleReviewStepCodes[visibleReviewStepCodes.length - 1]);
}
}, [editableReviewStep, selectedReviewStep, visibleReviewStepCodes]);
const docId = useMemo(() => {
return currentDet?.instlSncnDocNo ?? "";
@@ -569,27 +648,47 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
useEffect(() => {
setCheckResults((prev) => {
const next: Record<string, CheckResultVal> = {};
const next: Record<string, CheckResultVal> = { ...prev };
let changed = false;
cklstRows.forEach((row) => {
const key = cklstKey(row);
if (Object.prototype.hasOwnProperty.call(prev, key)) {
next[key] = prev[key];
} else {
next[key] = null;
visibleReviewStepCodes.forEach((stepCode) => {
const key = cklstKey(row, stepCode);
if (!Object.prototype.hasOwnProperty.call(next, key)) {
next[key] = null;
changed = true;
}
});
});
// 행이 없어졌을 때 stale key만 정리 (현재 화면에 노출되는 step 기준)
const liveKeys = new Set<string>();
cklstRows.forEach((row) => {
visibleReviewStepCodes.forEach((stepCode) => {
liveKeys.add(cklstKey(row, stepCode));
});
});
Object.keys(next).forEach((key) => {
const keyStep = key.split("_").pop();
if (
keyStep &&
visibleReviewStepCodes.includes(keyStep as ReviewStepCode) &&
!liveKeys.has(key)
) {
delete next[key];
changed = true;
}
});
// 기존 key가 사라졌거나 값이 달라진 경우만 상태 업데이트
if (!changed) {
const prevKeys = Object.keys(prev);
if (prevKeys.length !== Object.keys(next).length) {
const nextKeys = Object.keys(next);
if (prevKeys.length !== nextKeys.length) {
changed = true;
} else {
for (const key of prevKeys) {
if (!(key in next) || prev[key] !== next[key]) {
for (const key of nextKeys) {
if (prev[key] !== next[key]) {
changed = true;
break;
}
@@ -599,17 +698,20 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
return changed ? next : prev;
});
}, [cklstRows]);
}, [cklstRows, visibleReviewStepCodes]);
// 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영
useEffect(() => {
if (!initialData || cklstRows.length === 0) return;
const results = initialData.cklstResultList ?? [];
const results = normalizeChecklistResultRows(initialData);
if (!Array.isArray(results) || results.length === 0) return;
const lookup = new Map<string, string>();
const stepCodesInResult = new Set<ReviewStepCode>();
for (const r of results) {
const stepCode = r.rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD;
const stepCode = (r.rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD) as ReviewStepCode;
if (!REVIEW_STEP_CODES.includes(stepCode)) continue;
stepCodesInResult.add(stepCode);
const key = `${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`;
if (r.rvwRsltAnmlyYn === "Y" || r.rvwRsltAnmlyYn === "N") lookup.set(key, r.rvwRsltAnmlyYn);
}
@@ -617,37 +719,86 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
setCheckResults((prev) => {
const next = { ...prev };
let changed = false;
const targetStepCodes = Array.from(
new Set<ReviewStepCode>([...visibleReviewStepCodes, ...stepCodesInResult]),
);
for (const row of cklstRows) {
const k = cklstKey(row);
const v = lookup.get(k);
if (v && next[k] !== v) {
next[k] = v as CheckResultVal;
changed = true;
}
targetStepCodes.forEach((stepCode) => {
const k = cklstKey(row, stepCode);
const v = lookup.get(k);
if (v && next[k] !== v) {
next[k] = v as CheckResultVal;
changed = true;
}
});
}
return changed ? next : prev;
});
}, [initialData, cklstRows]);
}, [initialData, cklstRows, visibleReviewStepCodes]);
const cklstStats = useMemo(() => {
if (!selectedReviewStep) {
return { total: 0, completed: 0, percent: 0 };
}
const total = cklstRows.length;
const completed = cklstRows.reduce((acc, row) => {
const val = checkResults[cklstKey(row)] ?? "";
const val = checkResults[cklstKey(row, selectedReviewStep)] ?? "";
return acc + (val ? 1 : 0);
}, 0);
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, completed, percent };
}, [cklstRows, checkResults]);
}, [cklstRows, checkResults, selectedReviewStep]);
const toCklstSaveDtoList = () =>
cklstRows
.map((row) => ({
{
const persistedStepCodeSet = new Set<ReviewStepCode>();
normalizeChecklistResultRows(initialData).forEach((r: any) => {
const code = r?.rvwStpComCd as ReviewStepCode | undefined;
if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code);
});
visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code));
return Array.from(persistedStepCodeSet).flatMap((stepCode) =>
cklstRows.map((row) => ({
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: stepCode,
selected: checkResults[cklstKey(row, stepCode)] ?? null,
})),
);
};
// 맵 형식으로 저장할 경우(업서트 처리용) - 키는 상세 코드(EX: LOCAL.지자체) 포맷
const REVIEW_STEP_FULL: Record<ReviewStepCode, string> = {
LOCAL: "LOCAL.지자체",
ENV: "ENV.환경청",
KECO: "KECO.환경공단",
};
const toCklstSaveDtoMap = () => {
const persistedStepCodeSet = new Set<ReviewStepCode>();
normalizeChecklistResultRows(initialData).forEach((r: any) => {
const code = r?.rvwStpComCd as ReviewStepCode | undefined;
if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code);
});
visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code));
const result: Record<string, any[]> = {};
Array.from(persistedStepCodeSet).forEach((stepCode) => {
const fullKey = REVIEW_STEP_FULL[stepCode];
result[fullKey] = cklstRows.map((row) => ({
cklstSn: row.cklstSn,
cklstArtclSn: row.cklstArtclSn,
cklstRvwSn: row.cklstRvwSn,
rvwStpComCd: DEFAULT_RVW_STP_COM_CD,
selected: checkResults[cklstKey(row)] ?? null,
rvwStpComCd: fullKey,
selected: checkResults[cklstKey(row, stepCode)] ?? null,
}));
});
return result;
};
const toEsntlAtchFileSaveDtoList = () =>
Object.values(requiredFiles)
@@ -811,9 +962,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
}
// 체크리스트 사용 항목(Y) 중 미선택 항목이 있으면 진행 불가
if (!editableReviewStep) {
alert("현재 사용자 권한/진행상태 조합에서 입력 가능한 체크리스트 단계가 없습니다.");
return false;
}
// 현재 입력 단계의 체크리스트 미선택 항목이 있으면 진행 불가
const hasUncheckedChecklist = cklstRows.some(
(row) => !checkResults[cklstKey(row)],
(row) => !checkResults[cklstKey(row, editableReviewStep)],
);
if (hasUncheckedChecklist) {
alert("체크리스트를 모두 체크하세요.");
@@ -833,10 +989,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
// 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함
const invalidYesRow = cklstRows.find(
(row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row)] !== "Y",
(row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row, editableReviewStep)] !== "Y",
);
if (invalidYesRow) {
const invalidKey = cklstKey(invalidYesRow);
const invalidKey = cklstKey(invalidYesRow, editableReviewStep);
const invalidRowEl = cklstRowRefs.current[invalidKey];
invalidRowEl?.scrollIntoView({ behavior: "smooth", block: "center" });
const yesRadio = invalidRowEl?.querySelector<HTMLInputElement>(
@@ -887,12 +1043,15 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
async function handleSave() {
if (!fnValidation("SAVE")) return;
const payload: InstlSncnApplySaveRequestDto = {
// 저장 시에는 하위 호환성을 위해 기존 배열 필드도 함께 전송하되,
// 추가로 서버에서 upsert 처리할 수 있도록 `cklstResultMap` 형태로도 전송합니다.
const payload: InstlSncnApplySaveRequestDto & { cklstResultMap?: Record<string, any[]> } = {
instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장)
bzScaleList: toBzScaleSaveDtoList(),
bizCstList: toBizCstSaveDtoList(),
cklstResultList: toCklstSaveDtoList(),
esntlAtchFileList: toEsntlAtchFileSaveDtoList(),
cklstResultMap: toCklstSaveDtoMap(),
};
try {
@@ -1326,8 +1485,35 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
{(cklstRows[0]?.cklstNm ?? taskSeComCdNm) + `(${cklstStats.completed}/${cklstStats.total})`}
</button>
</div>
<div className="mb-3 flex flex-wrap gap-2">
{visibleReviewStepCodes.length === 0 ? (
<span className="text-[1.2rem] text-gray-500">
현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다.
</span>
) : (
visibleReviewStepCodes.map((stepCode) => {
const isSelected = selectedReviewStep === stepCode;
const isEditable = editableReviewStep === stepCode;
return (
<button
key={stepCode}
type="button"
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
isSelected
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 bg-white text-gray-600"
}`}
onClick={() => setSelectedReviewStep(stepCode)}
>
{stepCode}
{isEditable ? " (입력)" : " (참고)"}
</button>
);
})
)}
</div>
<div className="mb-2 text-[1.25rem] text-gray-500">
체크리스트 진행상황 — 총합: <strong>{cklstStats.total}</strong> · 완료:{" "}
체크리스트 진행상황 [{selectedReviewStep}] — 총합: <strong>{cklstStats.total}</strong> · 완료: {" "}
<strong>{cklstStats.completed}</strong> · 진행률: <strong>{cklstStats.percent}%</strong>
</div>
<div className="cnfm-table-wrap">
@@ -1345,10 +1531,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
<button
type="button"
className="w-full rounded border border-green-300 bg-green-500 px-3 py-1 text-[1.1rem] font-semibold text-white hover:bg-green-600"
disabled={editableReviewStep !== selectedReviewStep}
onClick={() =>
setCheckResults(
Object.fromEntries(cklstRows.map((row) => [cklstKey(row), "Y" as const])),
)
setCheckResults((prev) => ({
...prev,
...Object.fromEntries(
cklstRows.map((row) => [cklstKey(row, selectedReviewStep), "Y" as const]),
),
}))
}
>
전체 예
@@ -1358,10 +1548,14 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
<button
type="button"
className="w-full rounded border border-red-300 bg-red-500 px-3 py-1 text-[1.1rem] font-semibold text-white hover:bg-red-600"
disabled={editableReviewStep !== selectedReviewStep}
onClick={() =>
setCheckResults(
Object.fromEntries(cklstRows.map((row) => [cklstKey(row), "N" as const])),
)
setCheckResults((prev) => ({
...prev,
...Object.fromEntries(
cklstRows.map((row) => [cklstKey(row, selectedReviewStep), "N" as const]),
),
}))
}
>
전체 아니오
@@ -1378,7 +1572,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
</tr>
) : (
cklstRows.map((row) => {
const key = cklstKey(row);
const key = cklstKey(row, selectedReviewStep);
const rvwPaddingLeft = `${Math.max(0, row.treeLevel - 1) * 16 + 8}px`;
return (
@@ -1400,6 +1594,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
name={`chk_${key}`}
value="Y"
checked={checkResults[key] === "Y"}
disabled={editableReviewStep !== selectedReviewStep}
onChange={() => setCheckResults((prev) => ({ ...prev, [key]: "Y" }))}
/>
</label>
@@ -1411,6 +1606,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
name={`chk_${key}`}
value="N"
checked={checkResults[key] === "N"}
disabled={editableReviewStep !== selectedReviewStep}
onChange={() => setCheckResults((prev) => ({ ...prev, [key]: "N" }))}
/>
</label>
@@ -1,26 +1,130 @@
"use client";
/* eslint-disable @typescript-eslint/no-explicit-any, import/order */
/* eslint-disable @typescript-eslint/no-explicit-any, simple-import-sort/imports */
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useState } from "react";
import { Button } from "@common/components";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { InstlSncnApi } from "@/entities/instl-sncn";
import { dayjs } from "@/shared/lib";
import { PROXY_HOST } from "@/shared/config";
import { formatFileSize } from "@/shared/lib/file-utils";
import { dayjs, useUserStore } from "@/shared/lib";
import "./cnfm-page.css";
type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
const ROLE_TO_REVIEW_STEP: Record<string, ReviewStepCode> = {
ROLE_USER05: "LOCAL",
ROLE_USER04: "LOCAL",
ROLE_USER03: "ENV",
ROLE_USER02: "KECO",
ROLE_USER01: "KECO",
};
const REVIEW_STEP_ROLE_RANK: Record<ReviewStepCode, number> = {
LOCAL: 1,
ENV: 2,
KECO: 3,
};
const INSTL_PROGRESS_ORDER = [
"SV",
"ST",
"LE",
"ER",
"EE",
"TE",
"TS",
"TR",
"TP",
"EP",
"TA",
"TB",
];
const getProgressStepCount = (prgrsSttsComCd?: string): number => {
const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd ?? "ST");
if (idx <= INSTL_PROGRESS_ORDER.indexOf("ST")) return 1;
if (idx <= INSTL_PROGRESS_ORDER.indexOf("TP")) return 2;
return 3;
};
const getRoleStepCount = (dtlInstCd?: string): number => {
const mapped = dtlInstCd ? ROLE_TO_REVIEW_STEP[dtlInstCd] : undefined;
return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0;
};
const normalizeChecklistResultMap = (
raw: unknown,
): Record<ReviewStepCode, any[]> => {
const base: Record<ReviewStepCode, any[]> = { LOCAL: [], ENV: [], KECO: [] };
if (!raw) return base;
if (Array.isArray(raw)) {
raw.forEach((row: any) => {
const step = (row?.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
if (REVIEW_STEP_CODES.includes(step)) {
base[step].push(row);
}
});
return base;
}
if (typeof raw === "object") {
REVIEW_STEP_CODES.forEach((step) => {
const rows = (raw as Record<string, unknown>)[step];
base[step] = Array.isArray(rows) ? (rows as any[]) : [];
});
}
return base;
};
function CnfmViewPage({ id }: { id: string }) {
const router = useRouter();
const userInfo = useUserStore((state) => state.userInfo);
const query = useQuery({
queryKey: ["instl-sncn-detail", id],
queryFn: () => InstlSncnApi.getById(id),
});
const data = (query.data ?? {}) as any;
// support API shape where payload includes detDto + lists
const det = data.detDto ?? data;
const bizSclList = data.bizSclList ?? [];
const bizCstList = data.bizCstList ?? [];
// prefer new API field `cklstResultMap`, fallback to legacy `cklstResultList`
const cklstResultMap = useMemo(
() => normalizeChecklistResultMap(data.cklstResultMap ?? data.cklstResultList),
[data.cklstResultMap, data.cklstResultList],
);
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const currentPrgrsSttsComCd = (det.prgrsSttsComCd as string | undefined) ?? "ST";
const roleBasedMaxStepCount = getRoleStepCount(userInfo?.dtlInstCd);
const progressBasedMaxStepCount = getProgressStepCount(currentPrgrsSttsComCd);
const visibleStepCount = Math.min(progressBasedMaxStepCount, roleBasedMaxStepCount);
const visibleReviewStepCodes = useMemo(
() => REVIEW_STEP_CODES.slice(0, visibleStepCount),
[visibleStepCount],
);
const [selectedReviewStep, setSelectedReviewStep] = useState<ReviewStepCode>("LOCAL");
useEffect(() => {
if (visibleReviewStepCodes.length === 0) return;
if (!visibleReviewStepCodes.includes(selectedReviewStep)) {
setSelectedReviewStep(visibleReviewStepCodes[visibleReviewStepCodes.length - 1]);
}
}, [selectedReviewStep, visibleReviewStepCodes]);
const selectedChecklistRows = useMemo(() => {
if (visibleReviewStepCodes.length === 0) return [];
return cklstResultMap[selectedReviewStep] ?? [];
}, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length]);
if (query.isLoading) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 불러오는 중입니다...</div>;
}
@@ -29,16 +133,6 @@ function CnfmViewPage({ id }: { id: string }) {
return <div className="mt-6 text-[1.4rem]">상세 정보를 찾을 수 없습니다.</div>;
}
const data = query.data as any;
// support API shape where payload includes detDto + lists
const det = data.detDto ?? data;
const bizSclList = data.bizSclList ?? [];
const bizCstList = data.bizCstList ?? [];
const cklstResultList = data.cklstResultList ?? [];
const esntlFileList = data.esntlFileList ?? [];
const etcFileList = data.etcFileList ?? [];
const fmt = (v: number | null | undefined) =>
typeof v === "number" ? v.toLocaleString() : v === null || v === undefined ? "" : String(v);
@@ -268,6 +362,28 @@ function CnfmViewPage({ id }: { id: string }) {
{/* 체크리스트 */}
<div className="cnfm-card p-4">
<h3 className="text-[1.5rem] font-bold text-gray-700">체크리스트</h3>
<div className="mb-3 mt-2 flex flex-wrap gap-2">
{visibleReviewStepCodes.length === 0 ? (
<span className="text-[1.2rem] text-gray-500">
현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다.
</span>
) : (
visibleReviewStepCodes.map((stepCode) => (
<button
key={stepCode}
type="button"
className={`rounded border px-3 py-1 text-[1.2rem] font-semibold ${
selectedReviewStep === stepCode
? "border-blue-500 bg-blue-50 text-blue-700"
: "border-gray-300 bg-white text-gray-600"
}`}
onClick={() => setSelectedReviewStep(stepCode)}
>
{stepCode}
</button>
))
)}
</div>
<div className="cnfm-table-wrap mt-3">
<table className="cnfm-table">
<thead>
@@ -296,12 +412,12 @@ function CnfmViewPage({ id }: { id: string }) {
</tr>
</thead>
<tbody>
{cklstResultList.length === 0 ? (
{selectedChecklistRows.length === 0 ? (
<tr>
<td colSpan={3} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
</tr>
) : (
cklstResultList.map((row: any) => {
selectedChecklistRows.map((row: any) => {
const key = `${row.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "STEP01"}`;
const hasResult = row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N";
const showRadios = row.bchckUseYn === "Y" || hasResult;