From b704db99b1a691bd742a901e49e98a942fb968bc Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 1 Jun 2026 17:22:32 +0900 Subject: [PATCH 01/78] =?UTF-8?q?[st=20frontend]=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=8B=9C=20picRvwCn=20=EA=B2=80=ED=86=A0=20=EB=82=B4=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instl-sncn/model/instl-sncn-interface.ts | 2 + .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 86 +++++++++++++------ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts index b2872fea7..98713e4f5 100644 --- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts +++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts @@ -88,6 +88,7 @@ export interface InstlSncnCklstResultSaveDto { cklstRvwSn: number; rvwStpComCd: string; selected: "Y" | "N" | null; + picRvwCn?: string | null; } /** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */ @@ -102,6 +103,7 @@ export interface InstlSncnApplySaveRequestDto { bzScaleList: InstlSncnBzScaleSaveDto[]; bizCstList: InstlSncnBizCstSaveRequestDto[]; cklstResultList: InstlSncnCklstResultSaveDto[]; + cklstResultMap?: Record; esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[]; } 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 058bdd368..76503e541 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 @@ -102,7 +102,7 @@ const INSTL_PROGRESS_ORDER = [ 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; + if (idx <= INSTL_PROGRESS_ORDER.indexOf("ER")) return 2; return 3; }; @@ -152,7 +152,12 @@ const normalizeChecklistResultRows = (payload: any): any[] => { */ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const router = useRouter(); - const userInfo = useUserStore((state) => state.userInfo); + const userInfo2 = useUserStore((state) => state.userInfo); + // userInfo?.dtlInstCd 테스트 코드 ENV.ROLE_USER03 , KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) + const userInfo = { + ...userInfo2, + dtlInstCd: "ROLE_USER03", + }; const alert = useAlertStore((state) => state.setMessage); const qc = useQueryClient(); const currentDet = useMemo(() => initialData?.detDto ?? initialData, [initialData]); @@ -177,14 +182,17 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { 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]); + // Only set default/initial selection when visible steps or editable step change. + // Do NOT include `selectedReviewStep` in deps to avoid overwriting user clicks. + setSelectedReviewStep((current) => { + // keep current if still visible + if (current && visibleReviewStepCodes.includes(current)) return current; + // prefer editable step if available + if (editableReviewStep) return editableReviewStep; + // otherwise pick the last visible step + return visibleReviewStepCodes[visibleReviewStepCodes.length - 1]; + }); + }, [editableReviewStep, visibleReviewStepCodes]); const docId = useMemo(() => { return currentDet?.instlSncnDocNo ?? ""; @@ -594,6 +602,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { /* ── 체크리스트 ── */ type CheckResultVal = "Y" | "N" | null; const [checkResults, setCheckResults] = useState>({}); + const [checkComments, setCheckComments] = useState>({}); const cklstRowRefs = useRef>({}); const { data: cklstTree = [] } = useQuery({ @@ -707,6 +716,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { if (!Array.isArray(results) || results.length === 0) return; const lookup = new Map(); + const commentLookup = new Map(); const stepCodesInResult = new Set(); for (const r of results) { const stepCode = (r.rvwStpComCd ?? DEFAULT_RVW_STP_COM_CD) as ReviewStepCode; @@ -714,6 +724,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { stepCodesInResult.add(stepCode); const key = `${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)); } setCheckResults((prev) => { @@ -735,6 +746,18 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } return changed ? next : prev; }); + // set comment state separately to avoid rerendering conflicts + setCheckComments((prev) => { + const next: Record = { ...prev }; + let changed = false; + for (const [k, v] of commentLookup.entries()) { + if (next[k] !== v) { + next[k] = v; + changed = true; + } + } + return changed ? next : prev; + }); }, [initialData, cklstRows, visibleReviewStepCodes]); const cklstStats = useMemo(() => { @@ -759,24 +782,19 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { }); visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); - return Array.from(persistedStepCodeSet).flatMap((stepCode) => + return Array.from(persistedStepCodeSet).flatMap((rvwStpComCd) => cklstRows.map((row) => ({ cklstSn: row.cklstSn, cklstArtclSn: row.cklstArtclSn, cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: stepCode, - selected: checkResults[cklstKey(row, stepCode)] ?? null, + rvwStpComCd: rvwStpComCd, + selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null, + picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null, })), ); }; - // 맵 형식으로 저장할 경우(업서트 처리용) - 키는 상세 코드(EX: LOCAL.지자체) 포맷 - const REVIEW_STEP_FULL: Record = { - LOCAL: "LOCAL.지자체", - ENV: "ENV.환경청", - KECO: "KECO.환경공단", - }; - + // 맵 형식으로 저장할 경우(업서트 처리용) - 키는 검토단계 코드(LOCAL/ENV/KECO) 사용 const toCklstSaveDtoMap = () => { const persistedStepCodeSet = new Set(); normalizeChecklistResultRows(initialData).forEach((r: any) => { @@ -786,14 +804,15 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { visibleReviewStepCodes.forEach((code) => persistedStepCodeSet.add(code)); const result: Record = {}; - Array.from(persistedStepCodeSet).forEach((stepCode) => { - const fullKey = REVIEW_STEP_FULL[stepCode]; - result[fullKey] = cklstRows.map((row) => ({ + Array.from(persistedStepCodeSet).forEach((rvwStpComCd) => { + // use code as key (e.g., "LOCAL") and set rvwStpComCd to the same code + result[rvwStpComCd] = cklstRows.map((row) => ({ cklstSn: row.cklstSn, cklstArtclSn: row.cklstArtclSn, cklstRvwSn: row.cklstRvwSn, - rvwStpComCd: fullKey, - selected: checkResults[cklstKey(row, stepCode)] ?? null, + rvwStpComCd: rvwStpComCd, + selected: checkResults[cklstKey(row, rvwStpComCd)] ?? null, + picRvwCn: checkComments[cklstKey(row, rvwStpComCd)] ?? null, })); }); @@ -1525,6 +1544,11 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { 주요 검토내용 검토 결과(-) + {selectedReviewStep !== "LOCAL" ? ( + + 기술검토 의견 + + ) : null} @@ -1611,6 +1635,18 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { /> + {selectedReviewStep !== "LOCAL" ? ( + +