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 606c9c31c..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"; @@ -55,6 +57,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 => { @@ -85,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), @@ -122,6 +133,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,19 +148,97 @@ 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>({}); + 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 = {}; (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); }); @@ -385,9 +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 ( + + ); + })} +
+
{`(${cklstStats.completed}/${cklstStats.total})`}
+
+
+
{visibleReviewStepCodes.length === 0 ? ( 현재 사용자 권한/진행상태 조합에서 표시 가능한 체크리스트 단계가 없습니다. @@ -409,20 +528,21 @@ function CnfmViewPage({ id }: { id: string }) { )) )}
+
+ 체크리스트 진행상황 [{selectedReviewStep}] — 총합: {cklstStats.total} · 완료: {cklstStats.completed} · 진행률: {cklstStats.percent}% +
- - + + {selectedReviewStep !== "LOCAL" ? ( ) : null} -
검토항목주요 검토내용검토항목주요 검토내용 검토 결과(✓)기술검토 의견
- + );