From 2a8c2ae542d58d52048a89e2972df3ec5678b0cb Mon Sep 17 00:00:00 2001 From: jangwoong Date: Tue, 2 Jun 2026 19:10:56 +0900 Subject: [PATCH] =?UTF-8?q?[as=20frontend]=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=85=EB=A0=A5=20=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=8B=9C=20validation=20=EB=B0=8F=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=20=EC=84=9C=EB=B2=84=20=EC=A0=80=EC=9E=A5=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 137 ++++++++++++------ 1 file changed, 93 insertions(+), 44 deletions(-) diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx index dd042ef29..1f377549c 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-regist-page.tsx @@ -159,7 +159,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { // userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) const userInfo = { ...userInfo2, - dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) + dtlInstCd: "ROLE_USER05", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) }; const alert = useAlertStore((state) => state.setMessage); const qc = useQueryClient(); @@ -203,17 +203,22 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }, [currentDet]); const [docSeComCd] = useState("INSTL"); - const [taskSeComCdNm, setTaskSeComCdNm] = useState("재이용수"); - const [taskSeComCd, setTaskSeComCd] = useState("RE"); + const [taskSeComCdNm, setTaskSeComCdNm] = useState( + () => (currentDet?.taskSeComCdNm as string) ?? "재이용수", + ); + const [taskSeComCd, setTaskSeComCd] = useState(() => (currentDet?.taskSeComCd as string) ?? "RE"); useEffect(() => { + // do not override server-provided initialData (modify mode) + if (currentDet) return; + const raw = sessionStorage.getItem(INSTL_SNCN_REGISTER_SELECTION_KEY); if (!raw) return; try { - const parsed = JSON.parse(raw) as { + const parsed = JSON.parse(raw) as { taskSeComCdList?: string[]; - taskSeComCdNmList?: string[]; + taskSeComCdNmList?: string[]; }; setTaskSeComCd(parsed.taskSeComCdList?.join(",") || "RE"); @@ -223,7 +228,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } finally { sessionStorage.removeItem(INSTL_SNCN_REGISTER_SELECTION_KEY); } - }, []); + }, [currentDet]); /* ── 기본 정보 ── */ const [bzNm, setBzNm] = useState(""); const [bzBgngYr, setBzBgngYr] = useState(""); @@ -429,6 +434,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } // console.log("etcFileGroupIdRef.current 초기화:", etcFileGroupIdRef.current); + // 수정모드인 경우 서버가 전달한 업무구분(단일 혹은 콤마로 결합된 복수)을 상태로 복원 + setTaskSeComCd(det.taskSeComCd); + setTaskSeComCdNm(det.taskSeComCdNm ); + // 매핑 완료 플래그 설정 — 이후 동일 컴포넌트 인스턴스에서 재매핑 방지 initialMappedRef.current = true; @@ -620,6 +629,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const [checkResults, setCheckResults] = useState>({}); const [checkComments, setCheckComments] = useState>({}); const cklstRowRefs = useRef>({}); + const [pendingInvalidKey, setPendingInvalidKey] = useState(null); const { data: cklstTree = [] } = useQuery({ queryKey: ["cklstTree", docSeComCd, taskSeComCd], @@ -699,8 +709,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const codes = Array.from( new Set((cklstTree ?? []).map((r) => (r as any).taskSeComCd).filter(Boolean)), ); - console.log("Available taskSeComCd codes in checklist tree:", codes); - + if (codes.length === 0) { if (selectedTaskCode !== null) setSelectedTaskCode(null); return; @@ -715,7 +724,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const cklstKey = (row: CklstTreeNode, rvwStpComCd?: string) => { const stepCode = rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD; - return `${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`; + const taskCode = (row as any).taskSeComCd ?? ""; + return `${taskCode}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${stepCode}`; }; useEffect(() => { @@ -733,9 +743,10 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }); }); - // 행이 없어졌을 때 stale key만 정리 (현재 화면에 노출되는 step 기준) + // 행이 없어졌을 때 stale key만 정리 + // build liveKeys from full cklstTree so inputs for other taskSeComCd are preserved const liveKeys = new Set(); - cklstRows.forEach((row) => { + (cklstTree ?? []).forEach((row) => { visibleReviewStepCodes.forEach((stepCode) => { liveKeys.add(cklstKey(row, stepCode)); }); @@ -770,7 +781,21 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return changed ? next : prev; }); - }, [cklstRows, visibleReviewStepCodes]); + }, [cklstTree, visibleReviewStepCodes]); + + // when a previously-missing invalid key is set (due to switching task code), + // try to scroll/focus it after the rows have re-rendered + useEffect(() => { + if (!pendingInvalidKey) return; + const el = cklstRowRefs.current[pendingInvalidKey]; + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + const firstRadio = el.querySelector(`input[name="chk_${pendingInvalidKey}"]`); + firstRadio?.focus(); + setPendingInvalidKey(null); + } + // otherwise, wait for cklstRows update (effect will re-run when pendingInvalidKey remains set) + }, [pendingInvalidKey, cklstRows]); // 수정 모드에서 서버의 체크리스트 결과값을 폼 상태로 반영 useEffect(() => { @@ -785,7 +810,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { 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}`; + const key = `${r.taskSeComCd ?? ""}_${r.cklstSn}_${r.cklstArtclSn ?? ""}_${r.cklstRvwSn}_${stepCode}`; if (r.rvwRsltAnmlyYn === "Y" || r.rvwRsltAnmlyYn === "N") lookup.set(key, r.rvwRsltAnmlyYn); if (r.picRvwCn) commentLookup.set(key, String(r.picRvwCn)); } @@ -839,28 +864,39 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }, [cklstRows, checkResults, selectedReviewStep]); - const toCklstSaveDtoList = () => - { - const persistedStepCodeSet = new Set(); - 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 toCklstSaveDtoList = () => { + const persistedStepCodeSet = new Set(); + normalizeChecklistResultRows(initialData).forEach((r: any) => { + const code = r?.rvwStpComCd as ReviewStepCode | undefined; + if (code && REVIEW_STEP_CODES.includes(code)) persistedStepCodeSet.add(code); + }); + // Always persist currently visible review steps as well + visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); + // only persist rows that are enabled for checkbox (`bchckUseYn === "Y") // only persist rows that are enabled for checkbox (`bchckUseYn === "Y") - const relevantRows = cklstRows.filter((row) => row.bchckUseYn === "Y"); - return Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) => - relevantRows.map((row) => ({ - cklstSn: row.cklstSn, - cklstArtclSn: row.cklstArtclSn, - cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: rvwStpComCd, - selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null, - picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null, - })), - ); - }; + // Use full cklstTree so items not currently visible (other task codes) are included + const relevantRows = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y"); + + // Ensure we generate DTOs grouped by all known task codes (TASK_CODE_LABELS keys) + const allTaskCodes = Object.keys(TASK_CODE_LABELS); + + return allTaskCodes.flatMap((taskCode) => + Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) => + relevantRows + .filter((row) => ((row as any).taskSeComCd ?? "") === taskCode) + .map((row) => ({ + taskSeComCd: taskCode, + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: rvwStpComCd, + selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null, + picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null, + })), + ), + ); + }; // removed map-shaped payload generation — server now expects `cklstResultList` only @@ -1053,19 +1089,32 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return false; } - // 저장 시: bchckUseYn =="Y" 일경우 검토 결과 라디오 박스가 반드시 선택되어야 함 - const invalidYesRow = cklstRows.find( - (row) => row.bchckUseYn === "Y" && !checkResults[cklstKey(row, editableReviewStep)], + // 저장 시: bchckUseYn =="Y" 인 경우, 화면에 표시되는 모든 review step에 대해 + // 검토 결과가 선택되어 있어야 함 (visibleReviewStepCodes 기준) + // check across all checklist items (not only currently visible rows) + const allRelevant = (cklstTree ?? []).filter((row) => row.bchckUseYn === "Y"); + const missingRow = allRelevant.find((row) => + visibleReviewStepCodes.some((step) => !checkResults[cklstKey(row, step)]), ); - if (invalidYesRow) { - const invalidKey = cklstKey(invalidYesRow, editableReviewStep); - console.warn("검토 결과 미선택 항목 발견:", invalidKey, invalidYesRow); + if (missingRow) { + const missingStep = visibleReviewStepCodes.find((step) => !checkResults[cklstKey(missingRow, step)])!; + const invalidKey = cklstKey(missingRow, missingStep); + console.warn("검토 결과 미선택 항목 발견:", invalidKey, missingRow, missingStep); + const invalidRowEl = cklstRowRefs.current[invalidKey]; - invalidRowEl?.scrollIntoView({ behavior: "smooth", block: "center" }); - // 라디오 그룹의 첫 입력에 포커스(예:Y 또는 아니오:N) - const firstRadio = invalidRowEl?.querySelector(`input[name="chk_${invalidKey}"]`); - firstRadio?.focus(); - alert(`체크리스트 항목 "${invalidYesRow.rvwCn}"의 검토결과를 선택하세요.`); + if (invalidRowEl) { + invalidRowEl.scrollIntoView({ behavior: "smooth", block: "center" }); + const firstRadio = invalidRowEl.querySelector(`input[name="chk_${invalidKey}"]`); + firstRadio?.focus(); + } else { + // row not visible: switch header filter to the missing row's task code, + // then focus after render via pendingInvalidKey + const taskCode = (missingRow as any).taskSeComCd ?? null; + if (taskCode) setSelectedTaskCode(taskCode); + setPendingInvalidKey(invalidKey); + } + + alert(`체크리스트 항목 "${missingRow.rvwCn}"의 검토결과를 선택하세요.`); return false; } }