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 29d15fcf0..dd7dccd7a 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 @@ -1,6 +1,7 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + InstlSncnApplySaveRequestDto, InstlSncnMng, InstlSncnMngSearchParams, InstlSncnMngService, @@ -19,6 +20,14 @@ export class InstlSncnApiService implements InstlSncnMngService { const response = await client.get(`${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/${id}`); return response.data; } + + public async saveApply(payload: InstlSncnApplySaveRequestDto): Promise { + const response = await client.post( + `${ENDPOINT.AS_INSTL_SERVICE.INSTL_SNCN}/save-apply`, + payload, + ); + return response.data; + } } export const InstlSncnApi = new InstlSncnApiService(); diff --git a/apps/as/src/entities/instl-sncn/index.ts b/apps/as/src/entities/instl-sncn/index.ts index a85a145b5..5da39b459 100644 --- a/apps/as/src/entities/instl-sncn/index.ts +++ b/apps/as/src/entities/instl-sncn/index.ts @@ -1,7 +1,10 @@ export { InstlSncnApi } from "./api/instl-sncn-service"; export { INSTL_SNCN_REGISTER_SELECTION_KEY, + type InstlSncnApplySaveRequestDto, + type InstlSncnBizCstSaveRequestDto, type InstlSncnBzScaleSaveDto, + type InstlSncnCklstResultSaveDto, type InstlSncnMng, type InstlSncnMngSearchParams, type SpringPage, 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 1bf43482f..0209bfef3 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 @@ -80,7 +80,24 @@ export interface InstlSncnBizCstSaveRequestDto { tstmDesignAmt: number; } +/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */ +export interface InstlSncnCklstResultSaveDto { + cklstSn: number; + cklstArtclSn: number; + cklstRvwSn: number; + selected: "Y" | "N" | null; +} + +/** 설치인가 신청 - 통합 저장 요청 DTO */ +export interface InstlSncnApplySaveRequestDto { + instlSncnMng: InstlSncnMng; + bzScaleList: InstlSncnBzScaleSaveDto[]; + bizCstList: InstlSncnBizCstSaveRequestDto[]; + cklstResultList: InstlSncnCklstResultSaveDto[]; +} + export interface InstlSncnMngService { getPage: (params: InstlSncnMngSearchParams) => Promise>; getById: (id: string) => Promise; + saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise; } 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 128cae223..97fc032e7 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 @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; @@ -13,7 +13,11 @@ import type { CnstpSeMngTreeDto } from "@/entities/cnstp"; import { cnstpService } from "@/entities/cnstp"; import { INSTL_SNCN_REGISTER_SELECTION_KEY, + InstlSncnApi, + type InstlSncnApplySaveRequestDto, + type InstlSncnBizCstSaveRequestDto, type InstlSncnBzScaleSaveDto, + type InstlSncnMng, } from "@/entities/instl-sncn"; import { Button, Input } from "@common/components"; @@ -103,8 +107,9 @@ function CnfmRegistPage() { const [etcNoticeOpen, setEtcNoticeOpen] = useState(true); /* ── 체크리스트 ── */ - type CheckResultVal = "Y" | "N" | ""; + type CheckResultVal = "Y" | "N" | null; const [checkResults, setCheckResults] = useState>({}); + const cklstRowRefs = useRef>({}); const { data: cklstTree = [] } = useQuery({ queryKey: ["cklstTree", "INSTL", taskSeComCd], @@ -159,7 +164,7 @@ function CnfmRegistPage() { const next: Record = {}; cklstRows.forEach((row) => { const key = cklstKey(row); - next[key] = prev[key] ?? ""; + next[key] = prev[key] ?? null; }); return next; }); @@ -181,9 +186,8 @@ function CnfmRegistPage() { cklstSn: row.cklstSn, cklstArtclSn: row.cklstArtclSn, cklstRvwSn: row.cklstRvwSn, - selected: checkResults[cklstKey(row)] ?? "", - })) - .filter((row) => row.selected !== ""); + selected: checkResults[cklstKey(row)] ?? null, + })); /* ── 사업규모 (공종구분 트리 연동) ── */ // cnstpSeCd → { bscPl, budgetReflct, thtiDsgn } 단일 map @@ -277,6 +281,20 @@ function CnfmRegistPage() { [acntgSeCd]: { ...(prev[acntgSeCd] ?? emptyAmt()), [col]: val }, })); + /** 총사업비 맵 -> 백엔드 저장용 배열 DTO 변환 */ + const toBizCstSaveDtoList = (): InstlSncnBizCstSaveRequestDto[] => + acntgTree + .filter((node) => leafSet.has(node.acntgSeCd)) + .map((node) => { + const row = amtMap[node.acntgSeCd] ?? emptyAmt(); + return { + acntgSeCd: node.acntgSeCd, + bscPlanAmt: num(row.bscPl), + bgtRfltAmt: num(row.budgetReflct), + tstmDesignAmt: num(row.thtiDsgn), + }; + }); + /** * indAcntgSeCdNm 의 앞 공백 수를 파싱해 padding px 값으로 반환 * (공백 4칸 = 1 depth = 1rem) @@ -306,6 +324,7 @@ function CnfmRegistPage() { /* ── 검증 ── */ function fnValidation(mode: "SAVE" | "PRESENTN") { + /* if (!bzNm.trim()) { alert("사업명을 입력하세요."); return false; @@ -322,26 +341,96 @@ function CnfmRegistPage() { alert("개인정보 제공에 동의하셔야 제출이 가능합니다."); return false; } + + + // 체크리스트 사용 항목(Y) 중 미선택 항목이 있으면 진행 불가 + const hasUncheckedChecklist = cklstRows.some( + (row) => !checkResults[cklstKey(row)], + ); + if (hasUncheckedChecklist) { + alert("체크리스트를 모두 체크하세요."); + return false; + } + // 저장 시: 사용 항목(Y)은 검토 결과가 반드시 예(Y)여야 함 + */ + if (mode === "SAVE") { + const invalidYesRow = cklstRows.find( + (row) => row.bchckUseYn === "Y" && checkResults[cklstKey(row)] !== "Y", + ); + if (invalidYesRow) { + const invalidKey = cklstKey(invalidYesRow); + const invalidRowEl = cklstRowRefs.current[invalidKey]; + invalidRowEl?.scrollIntoView({ behavior: "smooth", block: "center" }); + const yesRadio = invalidRowEl?.querySelector( + `input[name="chk_${invalidKey}"][value="Y"]`, + ); + yesRadio?.focus(); + alert(`${invalidYesRow.rvwCn} 검토내용은 예(Y)여야 합니다.`); + return false; + } + } + return true; } - function handleSave() { + const toInstlSncnMngPayload = (prgrsSttsComCd: "SV" | "ST"): InstlSncnMng => ({ + instlSncnDocNo: "", + dongId: "", + sidoNm: "", + sggNm: "", + chrgrNm: "", + instNm: "", + envoSeComCd: instlCnfmApr, + envoSeCdNm: "", + taskSeComCd, + taskSeCdNm: taskSeComCdNm, + bgtBizCd: "", + bgtAplyYr: "", + bizNm: bzNm, + bizBgngYr: bzBgngYr, + bizEndYr: bzEndYr, + bizCn: bzCn, + aplcntNm: "", + aplcntMblTelno: "", + aplcntOgdpNm: "", + aplcntEmlAddr: "", + dsgnrNm, + dsgnrTelno, + dsgnrOgdpNm: dsgnrPsitn, + dsgnrEmlAddr, + atchFileGroupId: "", + prgrsSttsComCd, + prgrsSttsCdNm: "", + regDt: "", + mdfcnDt: "", + rgtrNm: "", + }); + + async function handleSave() { if (!fnValidation("SAVE")) return; - const payload = { - taskSeComCd, + const payload: InstlSncnApplySaveRequestDto = { + instlSncnMng: toInstlSncnMngPayload("SV"), // 저장 시 진행상태는 SV (임시저장) bzScaleList: toBzScaleSaveDtoList(), + bizCstList: toBizCstSaveDtoList(), cklstResultList: toCklstSaveDtoList(), - }; - void payload; - alert("저장(임시) API 연동 후 progrsSttus=SV 전송",JSON.stringify(payload, null, 2)); + }; + + try { + const issuedDocNo = await InstlSncnApi.saveApply(payload); + alert(`저장되었습니다. 문서번호: ${issuedDocNo}`); + } catch (error) { + console.error("저장 API 호출 실패", error); + alert("저장 중 오류가 발생했습니다."); + } } function handleSubmit() { if (!fnValidation("PRESENTN")) return; const payload = { - taskSeComCd, + instlSncnMng: toInstlSncnMngPayload("ST"), bzScaleList: toBzScaleSaveDtoList(), - cklstResultList: toCklstSaveDtoList(), + bizCstList: toBizCstSaveDtoList(), + cklstRvwRsltList: toCklstSaveDtoList(), }; void payload; alert("제출 API 연동 후 progrsSttus=ST 전송"); @@ -812,7 +901,12 @@ function CnfmRegistPage() { const rvwPaddingLeft = `${Math.max(0, row.treeLevel - 1) * 16 + 8}px`; return ( - + { + cklstRowRefs.current[key] = el; + }} + > {row.cklstArtclNm} {row.treeLevel > 1 ? "└ " : ""}