From bb7cce47ac7670d64bbba9118ecafb35ac1ad8bc Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 4 Jun 2026 11:02:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[as=20frontend]=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B7=B0=EC=8B=9C=20rvw=5Fstp=5F?= =?UTF-8?q?com=5Fcd.=EB=A6=AC=EB=B7=B0=EB=8B=A8=EA=B3=84=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=B0=8F,=20=EC=97=85=EB=AC=B4=EA=B5=AC=EB=B6=84=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B7=B0=EB=8B=A8=EC=97=90=EC=84=9C=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8A=94=20=EB=B6=80=EB=B6=84=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/instl-sncn/ui/cnfm-view-page.tsx | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx index 606c9c31c..1f5d867d0 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx @@ -55,6 +55,13 @@ const getRoleStepCount = (dtlInstCd?: string): number => { return mapped ? REVIEW_STEP_ROLE_RANK[mapped] : 0; }; +// 업무구분 코드 -> 표시명 매핑 (체크리스트 그룹 표시용) +const TASK_CODE_LABELS: Record = { + RE: "재이용수", + SP: "하수관로", + SF: "하수처리시설", +}; + const normalizeChecklistResultMap = ( raw: unknown, ): Record => { @@ -122,6 +129,13 @@ function CnfmViewPage({ id }: { id: string }) { [visibleStepCount], ); const [selectedReviewStep, setSelectedReviewStep] = useState("LOCAL"); + // selected task code for header filtering (null = none selected) + const [selectedTaskCode, setSelectedTaskCode] = useState(null); + + // derive available task codes from checklist results + const taskCodes = useMemo(() => { + return Array.from(new Set((cklstAll ?? []).map((r: any) => (r as any).taskSeComCd).filter(Boolean))); + }, [cklstAll]); useEffect(() => { if (visibleReviewStepCodes.length === 0) return; @@ -130,10 +144,28 @@ function CnfmViewPage({ id }: { id: string }) { } }, [selectedReviewStep, visibleReviewStepCodes]); + // when checklist result task codes change, default selectedTaskCode to first available code + useEffect(() => { + if (!taskCodes || taskCodes.length === 0) { + if (selectedTaskCode !== null) setSelectedTaskCode(null); + return; + } + if (!selectedTaskCode || !taskCodes.includes(selectedTaskCode)) { + setSelectedTaskCode(taskCodes[0]); + } + }, [taskCodes, selectedTaskCode]); + const selectedChecklistRows = useMemo(() => { if (visibleReviewStepCodes.length === 0) return []; - return cklstResultMap[selectedReviewStep] ?? []; - }, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length]); + const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) => + selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true, + ); + return rows + .slice() + .sort((a: any, b: any) => + (a.cklstArtclSn ?? 0) - (b.cklstArtclSn ?? 0) || (a.sortSeq ?? 0) - (b.sortSeq ?? 0) || (a.cklstRvwSn ?? 0) - (b.cklstRvwSn ?? 0), + ); + }, [cklstResultMap, selectedReviewStep, visibleReviewStepCodes.length, selectedTaskCode]); const [viewCheckResults, setViewCheckResults] = useState>({}); const [viewCheckComments, setViewCheckComments] = useState>({}); @@ -142,7 +174,7 @@ function CnfmViewPage({ id }: { id: string }) { const results: Record = {}; const comments: Record = {}; (selectedChecklistRows ?? []).forEach((row: any) => { - const key = `${row.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`; + const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`; if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") results[key] = row.rvwRsltAnmlyYn; if (row.picRvwCn) comments[key] = String(row.picRvwCn); }); @@ -387,6 +419,24 @@ function CnfmViewPage({ id }: { id: string }) { {/* 체크리스트 */}

체크리스트

+
+ {Array.from(new Set(taskCodes)).map((code) => { + const name = TASK_CODE_LABELS[code] ?? code; + const active = selectedTaskCode === code; + return ( + + ); + })} +
{visibleReviewStepCodes.length === 0 ? ( @@ -446,7 +496,7 @@ function CnfmViewPage({ id }: { id: string }) { ) : ( selectedChecklistRows.map((row: any) => { - const key = `${row.cklstSn}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`; + const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${row.rvwStpComCd ?? "LOCAL"}`; const hasResult = row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N"; const showRadios = row.bchckUseYn === "Y" ; From e9caa1b84d94c80bcb319e35472fb578802d23db Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 4 Jun 2026 13:27:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[as=20frontend]=20tech=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instl-sncn/api/instl-sncn-service.ts | 7 + .../pages/instl-sncn/ui/cnfm-regist-page.tsx | 38 ++++-- .../pages/instl-sncn/ui/cnfm-view-page.tsx | 125 ++++++++++++++---- 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts index 434a77918..208e0172d 100644 --- a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts +++ b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts @@ -29,6 +29,13 @@ export class InstlSncnApiService implements InstlSncnMngService { ); return response.data; } + public async techReview(payload: InstlSncnApplySaveRequestDto): Promise { + const response = await client.post( + `${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/tech-review`, + payload, + ); + return response.data; + } } export const InstlSncnApi = new InstlSncnApiService(); 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 1f377549c..5bcd8925c 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_USER05", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) + dtlInstCd: "ROLE_USER03", //ROLE_USER03, ROLE_USER02, ROLE_USER01 KECO 권한 테스트용 (실제 권한에 맞게 변경 필요) }; const alert = useAlertStore((state) => state.setMessage); const qc = useQueryClient(); @@ -826,7 +826,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { targetStepCodes.forEach((stepCode) => { const k = cklstKey(row, stepCode); const v = lookup.get(k); - if (v && next[k] !== v) { + // only apply server-provided value when we don't already have a user-entered value + if (v && (next[k] === null || next[k] === undefined)) { next[k] = v as CheckResultVal; changed = true; } @@ -839,7 +840,8 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { const next: Record = { ...prev }; let changed = false; for (const [k, v] of commentLookup.entries()) { - if (next[k] !== v) { + // only set comment if none exists yet (preserve user edits) + if (!next[k]) { next[k] = v; changed = true; } @@ -1122,7 +1124,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { return true; } - const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"): InstlSncnMng => ({ + const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"| "ER"): InstlSncnMng => ({ instlSncnDocNo: docId ?? "", // 수정 시 기존 문서번호 유지, 신규 저장 시 빈값 admdstCd: "", sidoNm: "", @@ -1180,16 +1182,28 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) { } } - function handleSubmit() { - if (!fnValidation("PRESENTN")) return; - const payload = { - instlSncnMng: toInstlSncnMngPayload("ST"), + async function handleSubmit() { + if (!fnValidation("SAVE")) return; + + const payload: InstlSncnApplySaveRequestDto = { + instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장) bzScaleList: toBzScaleSaveDtoList(), bizCstList: toBizCstSaveDtoList(), - cklstRvwRsltList: toCklstSaveDtoList(), - }; - void payload; - alert("제출 API 연동 후 progrsSttus=ST 전송"); + cklstResultList: toCklstSaveDtoList(), + esntlAtchFileList: toEsntlAtchFileSaveDtoList(), + }; + + try { + const issuedDocNo = await InstlSncnApi.saveApply(payload); + alert(`제출 되었습니다. 문서번호: ${issuedDocNo}`); + } catch (error) { + console.error("저장 API 호출 실패", error); + const serverMessage = + axios.isAxiosError(error) && error.response?.data?.message + ? (error.response.data as { message: string }).message + : "저장 중 오류가 발생했습니다."; + alert(serverMessage); + } } /* ---------------------------------------------------------------- */ diff --git a/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx b/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx index 1f5d867d0..316d466e1 100644 --- a/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/cnfm-view-page.tsx @@ -7,10 +7,12 @@ import { Button } from "@common/components"; import { useQuery } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; -import { InstlSncnApi } from "@/entities/instl-sncn"; +// InstlSncnApi imported below with types import { PROXY_HOST } from "@/shared/config"; import { formatFileSize } from "@/shared/lib/file-utils"; -import { dayjs, useUserStore } from "@/shared/lib"; +import axios from "axios"; +import { dayjs, useUserStore, useAlertStore } from "@/shared/lib"; +import { InstlSncnApi, type InstlSncnApplySaveRequestDto } from "@/entities/instl-sncn"; import "./cnfm-page.css"; @@ -92,6 +94,8 @@ function CnfmViewPage({ id }: { id: string }) { const router = useRouter(); const userInfo = useUserStore((state) => state.userInfo); + const alert = useAlertStore((state) => state.setMessage); + const query = useQuery({ queryKey: ["instl-sncn-detail", id], queryFn: () => InstlSncnApi.getById(id), @@ -170,6 +174,66 @@ function CnfmViewPage({ id }: { id: string }) { const [viewCheckResults, setViewCheckResults] = useState>({}); const [viewCheckComments, setViewCheckComments] = useState>({}); + const cklstStats = useMemo(() => { + if (!selectedReviewStep) return { total: 0, completed: 0, percent: 0 }; + const rows = (cklstResultMap[selectedReviewStep] ?? []).filter((r: any) => r.bchckUseYn === "Y" && (selectedTaskCode ? (r.taskSeComCd ?? "") === selectedTaskCode : true)); + const total = rows.length; + const completed = rows.reduce((acc: number, row: any) => { + const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`; + const val = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? ""; + return acc + (val ? 1 : 0); + }, 0); + const percent = total > 0 ? Math.round((completed / total) * 100) : 0; + return { total, completed, percent }; + }, [cklstResultMap, viewCheckResults, selectedReviewStep, selectedTaskCode]); + + const fnValidation = (mode: "SAVE" | "SUBMIT") => { + // 기본 검증: 필요하면 확장 + return true; + }; + + const hadleTechReviewSubmit = async () => { + if (!fnValidation("SAVE")) return; + + const cklstResultList = (cklstAll ?? []) + .filter((r: any) => r.bchckUseYn === "Y") + .map((row: any) => { + const key = `${row.taskSeComCd ?? ""}_${row.cklstSn}_${row.cklstArtclSn ?? ""}_${row.cklstRvwSn}_${selectedReviewStep}`; + const rvwRsltAnmlyYn = (viewCheckResults[key] ?? row.rvwRsltAnmlyYn) ?? null; + const picRvwCn = (viewCheckComments[key] ?? row.picRvwCn) ?? null; + return { + taskSeComCd: row.taskSeComCd ?? "", + cklstSn: row.cklstSn, + cklstArtclSn: row.cklstArtclSn, + cklstRvwSn: row.cklstRvwSn, + rvwStpComCd: selectedReviewStep, + rvwRsltAnmlyYn, + picRvwCn, + }; + }); + + const payload: InstlSncnApplySaveRequestDto = { + // only map core management DTO and checklist results; other lists empty per request + instlSncnMng: { ...(det ?? {}), prgrsSttsComCd: "ER" } as any, + bzScaleList: [], + bizCstList: [], + cklstResultList, + esntlAtchFileList: [], + } as unknown as InstlSncnApplySaveRequestDto; + + try { + const issuedDocNo = await InstlSncnApi.techReview(payload); + alert(`기술검토 등록이 완료되었습니다. 문서번호: ${issuedDocNo}`); + } catch (error) { + console.error("기술검토 등록 API 호출 실패", error); + const serverMessage = + axios.isAxiosError(error) && error.response?.data?.message + ? (error.response.data as { message: string }).message + : "저장 중 오류가 발생했습니다."; + alert(serverMessage); + } + }; + useEffect(() => { const results: Record = {}; const comments: Record = {}; @@ -417,27 +481,32 @@ function CnfmViewPage({ id }: { id: string }) {
{/* 체크리스트 */} -
-

체크리스트

-
- {Array.from(new Set(taskCodes)).map((code) => { - const name = TASK_CODE_LABELS[code] ?? code; - const active = selectedTaskCode === code; - return ( - - ); - })} -
-
+
+
+

체크리스트

+
+
+ {Array.from(new Set(taskCodes)).map((code) => { + const name = TASK_CODE_LABELS[code] ?? code; + const active = selectedTaskCode === code; + return ( + + ); + })} +
+
{`(${cklstStats.completed}/${cklstStats.total})`}
+
+
+
{visibleReviewStepCodes.length === 0 ? ( 현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다. @@ -459,20 +528,21 @@ function CnfmViewPage({ id }: { id: string }) { )) )}
+
+ 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · 완료: {cklstStats.completed} · 진행률: {cklstStats.percent}% +
- - + + {selectedReviewStep !== "LOCAL" ? ( ) : null} -
검토항목주요 검토내용검토항목주요 검토내용 검토 결과(✓)기술검토 의견
- + );