[st frontend] 조회시 체크리스트 3개 부분 조회 처리로 수정함
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user