From 70faadb27eaa3a7a7d0ec2dbb95cce38ec3f83e6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 15 Jul 2026 17:52:43 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EB=B3=B4=EC=99=84=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../instl-sncn/api/instl-sncn-service.ts | 9 + .../as/src/entities/instl-sncn/api/queries.ts | 13 + apps/as/src/entities/instl-sncn/index.ts | 1 + .../instl-sncn/model/instl-sncn-interface.ts | 16 +- .../pages/fnrs-cnsltn/lib/button-config.ts | 5 - .../fnrs-cnsltn/lib/save-payload-utils.ts | 318 +++++++++-------- .../ui/components/floating-action-buttons.tsx | 20 +- .../src/pages/instl-sncn/lib/button-config.ts | 5 - .../lib/instl-sncn-regist-schema.ts | 26 +- .../instl-sncn/lib/save-payload-utils.ts | 235 +++++++------ .../ui/components/floating-action-buttons.tsx | 39 ++- .../pages/instl-sncn/ui/instl-form-page.tsx | 42 ++- apps/as/src/widgets/pbadms-sprt/index.ts | 6 +- .../pbadms-sprt/ui/splmnt-dmnd-dialog.tsx | 329 ++++++++++++++++++ .../pbadms-sprt/ui/text-input-dialog.tsx | 77 ---- 15 files changed, 735 insertions(+), 406 deletions(-) create mode 100644 apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx delete mode 100644 apps/as/src/widgets/pbadms-sprt/ui/text-input-dialog.tsx 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 38ce8e69a..5739b82bb 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 @@ -65,6 +65,14 @@ async function techReview(id: string, payload: InstlSncnApplySaveRequestDto): Pr await client.post(`${BASE}/${id}/tech-review`, payload); } +async function submitReqSupplement( + id: string, + payload: InstlSncnApplySaveRequestDto, +): Promise { + const response = await client.post(`${BASE}/${id}/reqsupplement`, payload); + return response.data; +} + async function remove(id: string): Promise { await client.delete(`${BASE}/${id}`); } @@ -80,6 +88,7 @@ export const instlSncnService = { saveApply, update, techReview, + submitReqSupplement, remove, changePrgrsSttsComCd, } satisfies InstlSncnMngService; diff --git a/apps/as/src/entities/instl-sncn/api/queries.ts b/apps/as/src/entities/instl-sncn/api/queries.ts index ac651be47..6044db955 100644 --- a/apps/as/src/entities/instl-sncn/api/queries.ts +++ b/apps/as/src/entities/instl-sncn/api/queries.ts @@ -99,6 +99,19 @@ export function useInstlSncnTechReview() { }); } +export function useInstlSncnSubmitReqSupplement() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: InstlSncnApplySaveRequestDto }) => + instlSncnService.submitReqSupplement(id, payload), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: instlSncnKeys.all }); + queryClient.invalidateQueries({ queryKey: instlSncnKeys.detail(variables.id) }); + }, + }); +} + export function useInstlSncnRemove() { const queryClient = useQueryClient(); diff --git a/apps/as/src/entities/instl-sncn/index.ts b/apps/as/src/entities/instl-sncn/index.ts index 9547dd6db..c528eca07 100644 --- a/apps/as/src/entities/instl-sncn/index.ts +++ b/apps/as/src/entities/instl-sncn/index.ts @@ -5,6 +5,7 @@ export { useInstlSncnList, useInstlSncnSidoCombo, useInstlSncnSigunguCombo, + useInstlSncnSubmitReqSupplement, useInstlSncnTechReview, } from "./api/queries"; export { 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 500cfb842..0f6fc36ee 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 @@ -190,15 +190,15 @@ export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader { /** 설치인가관리 정보 */ instlSncnMng: InstlSncnMngSaveRequestDto; /** 사업규모 목록 */ - bzScaleList: InstlSncnBzScaleSaveDto[]; + bzScaleList?: InstlSncnBzScaleSaveDto[]; /** 총사업비 목록 */ - bizCstList: InstlSncnBizCstSaveRequestDto[]; + bizCstList?: InstlSncnBizCstSaveRequestDto[]; /** 체크리스트 검토결과 목록 */ - cklstResultList: InstlSncnCklstResultSaveDto[]; + cklstResultList?: InstlSncnCklstResultSaveDto[]; /** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */ cklstResultMap?: Record; /** 필수첨부파일 목록 */ - esntlAtchFileList: InstlSncnEsntlAtchFileSaveDto[]; + esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[]; /** 검토의견 목록 */ rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[]; /** 보완요구 목록 */ @@ -222,8 +222,11 @@ export type InstlSncnRvwOpnnResponseDto = InstlSncnRvwOpnnSaveRequestDto & }; /** 설치인가관리 상세 응답 DTO */ -export interface InstlSncnMngDetResponse - extends PbadmsMngDetResponseBase { +export interface InstlSncnMngDetResponse extends PbadmsMngDetResponseBase< + InstlSncnMngResponseDto, + InstlSncnRvwOpnnResponseDto, + InstlSncnSplmntDmndSaveDto +> { detDto: InstlSncnMngResponseDto; bizSclList: BizSclTreeDto[]; bizCstList: BizCstTreeDto[]; @@ -239,6 +242,7 @@ export interface InstlSncnMngService { saveApply: (payload: InstlSncnApplySaveRequestDto) => Promise; update: (id: string, payload: InstlSncnMngUpdateRequestDto) => Promise; techReview: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; + submitReqSupplement: (id: string, payload: InstlSncnApplySaveRequestDto) => Promise; remove: (id: string) => Promise; changePrgrsSttsComCd: (payload: DocPrgrsUpdateRequestDto) => Promise; } diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index cc6d8d4b9..55ec0e0ae 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -15,7 +15,6 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { dmndSplmnt: "dmndSplmnt", atrzAprv: "atrzAprv", atrzRply: "atrzRply", - grndsChck: "grndsChck", } as const; export type FnrsCnsltnViewButtonAction = @@ -44,10 +43,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record node.useYn === "Y") - .map((node) => ({ - cnstpSeCd: node.cnstpSeCd, - bscPl: node.bscPlanQty, - budgetReflct: node.bgtRfltQty, - thtiDsgn: node.tstmDesignQty, - })); -} - -export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { - return bizCstList - .filter((node) => node.useYn === "Y") - .map((node) => ({ - acntgSeCd: node.acntgSeCd, - bscPlanAmt: node.bscPlanAmt, - bgtRfltAmt: node.bgtRfltAmt, - tstmDesignAmt: node.tstmDesignAmt, - })); -} - -export function toEsntlAtchFileSaveDto( - esntlFileList: EsntlAtchFileMngTreeResponseDto[], -): EsntlAtchFileItem[] { - const esntlMap = new Map(); - - for (const item of esntlFileList) { - if (!item.esntlAtchFileId || !item.atflCd) continue; - esntlMap.set(item.esntlAtchFileId, item.atflCd); - } - - return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ - esntlAtchFileId, - atchFileGroupId, - })); -} - -export function buildSavePayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, -): FnrsCnsltnApplySaveRequestDto { - const detDto = data.detDto; - if (!detDto) { - throw new Error("상세 정보(detDto)가 없습니다."); - } - - const bizSclList = data.bizSclList ?? []; - const bizCstList = data.bizCstList ?? []; - const cklstResultList = data.cklstResultList ?? []; - const esntlFileList = data.esntlFileList ?? []; - - return { - fnrsCnsltnMng: { ...detDto, prgrsSttsComCd }, - bzScaleList: toBzScaleSaveDto(bizSclList), - bizCstList: toBizCstSaveDto(bizCstList), - cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), - esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), - fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])), - }; -} - -export function buildTechReviewReplyPayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, - techReViewSaveCd: string, - reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null, - opnrNm?: string, -): FnrsCnsltnApplySaveRequestDto { - return { - ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), - techReViewSaveCd, - rvwStpCd: reviewStep, - rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), - }; -} - -export function buildSupplementReqPayload( - data: FnrsCnsltnMngDetResponseDto, - splmntDmndCn: string, -): FnrsCnsltnApplySaveRequestDto { - const detDto = data.detDto; - if (!detDto) { - throw new Error("상세 정보(detDto)가 없습니다."); - } - - const nextSn = - (data.splmntDmndList?.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) ?? 0) + 1; - - const splmntDmndList: FnrsCnsltnSplmntDmndItem[] = [ - ...(data.splmntDmndList ?? []), - { - splmntDmndSn: nextSn, - splmntDmndCn, - cmptnYn: "N", - }, - ]; - - return { - fnrsCnsltnMng: detDto, - splmntDmndList, - }; -} - -export function buildSupplementSubmitPayload( - data: FnrsCnsltnMngDetResponseDto, - checklistFormState: ChecklistFormState, -): FnrsCnsltnApplySaveRequestDto { - return { - ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), - splmntDmndList: data.splmntDmndList, - }; -} - -export function buildTechReviewPayload( - det: FnrsCnsltnMngResponseDto, - cklstResultList: CklstResultItem[], -): FnrsCnsltnApplySaveRequestDto { - return { - fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" }, - bzScaleList: [], - bizCstList: [], - cklstResultList, - esntlAtchFileList: [], - }; -} +import type { + BizCstItem, + BizCstTreeDto, + BizSclTreeDto, + BzScaleItem, + CklstResultItem, + EsntlAtchFileItem, + EsntlAtchFileMngTreeResponseDto, + FnrsCnsltnApplySaveRequestDto, + FnrsCnsltnMngDetResponseDto, + FnrsCnsltnMngResponseDto, + FnrsCnsltnSplmntDmndItem, +} from "@/entities/fnrs-cnsltn"; +import { + mapFnrsSharDetToFormList, + type SplmntDmndDialogConfirmPayload, + toFnrsSharSaveList, +} from "@/widgets/pbadms-sprt"; + +import type { ChecklistFormState, ReviewStepCode } from "../model/view-types"; + +import { toCklstSaveDto } from "./checklist-utils"; +import { type ReviewOpinionGetFormDataResult, toRvwOpnnSaveDto } from "./review-opinion-utils"; + +export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): BzScaleItem[] { + return bizSclList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + cnstpSeCd: node.cnstpSeCd, + bscPl: node.bscPlanQty, + budgetReflct: node.bgtRfltQty, + thtiDsgn: node.tstmDesignQty, + })); +} + +export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): BizCstItem[] { + return bizCstList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + acntgSeCd: node.acntgSeCd, + bscPlanAmt: node.bscPlanAmt, + bgtRfltAmt: node.bgtRfltAmt, + tstmDesignAmt: node.tstmDesignAmt, + })); +} + +export function toEsntlAtchFileSaveDto( + esntlFileList: EsntlAtchFileMngTreeResponseDto[], +): EsntlAtchFileItem[] { + const esntlMap = new Map(); + + for (const item of esntlFileList) { + if (!item.esntlAtchFileId || !item.atflCd) continue; + esntlMap.set(item.esntlAtchFileId, item.atflCd); + } + + return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ + esntlAtchFileId, + atchFileGroupId, + })); +} + +export function buildSavePayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, +): FnrsCnsltnApplySaveRequestDto { + const detDto = data.detDto; + if (!detDto) { + throw new Error("상세 정보(detDto)가 없습니다."); + } + + const bizSclList = data.bizSclList ?? []; + const bizCstList = data.bizCstList ?? []; + const cklstResultList = data.cklstResultList ?? []; + const esntlFileList = data.esntlFileList ?? []; + + return { + fnrsCnsltnMng: { ...detDto, prgrsSttsComCd }, + bzScaleList: toBzScaleSaveDto(bizSclList), + bizCstList: toBizCstSaveDto(bizCstList), + cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), + esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), + fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])), + }; +} + +export function buildTechReviewReplyPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, + techReViewSaveCd: string, + reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null, + opnrNm?: string, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), + techReViewSaveCd, + rvwStpCd: reviewStep, + rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), + }; +} + +export function buildSupplementReqPayload( + data: FnrsCnsltnMngDetResponseDto, + payload: SplmntDmndDialogConfirmPayload, + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const nextSn = + existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1; + + const newItems: FnrsCnsltnSplmntDmndItem[] = payload.items.map((item, index) => ({ + splmntDmndSn: nextSn + index, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + +function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem { + return { + splmntDmndSn: item.splmntDmndSn, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndDt: item.splmntDmndDt, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + splmntCn: item.splmntCn, + splmntPicNm: item.splmntPicNm, + splmntCmptnDt: item.splmntCmptnDt, + splmntAtchFileGroupId: item.splmntAtchFileGroupId, + cmptnYn: item.cmptnYn, + }; +} + +export function buildSupplementSubmitPayload( + data: FnrsCnsltnMngDetResponseDto, + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: data.splmntDmndList, + }; +} + +export function buildTechReviewPayload( + det: FnrsCnsltnMngResponseDto, + cklstResultList: CklstResultItem[], +): FnrsCnsltnApplySaveRequestDto { + return { + fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" }, + bzScaleList: [], + bizCstList: [], + cklstResultList, + esntlAtchFileList: [], + }; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index 47c222dfc..46d8f9eee 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -27,7 +27,7 @@ import { fnrsCnsltnMngService, } from "@/entities/fnrs-cnsltn"; import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw"; -import { TextInputDialog } from "@/widgets/pbadms-sprt"; +import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; import { @@ -402,12 +402,12 @@ function FloatingActionButtons({ setActiveDialog("dmndSplmnt"); } - async function handleDmndSplmntConfirm(splmntDmndCn: string) { + async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) { setActiveDialog(null); - const payload = buildSupplementReqPayload(data, splmntDmndCn); + const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState); await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, - request: payload, + request: requestPayload, successMessage: "보완요청이 처리되었습니다.", }); } @@ -514,13 +514,11 @@ function FloatingActionButtons({ onConfirm={handleElaprTmpltConfirm} /> - setActiveDialog(null)} - title="보완요청 내용 입력" - placeholder="보완요청 내용을 입력하세요." isSubmitting={isProcessing} - onConfirm={handleDmndSplmntConfirm} + onConfirm={(payload) => void handleDmndSplmntConfirm(payload)} /> { context.onSuccess?.(); } -async function handleGrndsChck(context: ActionHandlerContext): Promise { - // TODO: 현장점검 등록 API 호출 - context.onSuccess?.(); -} - const buttonActionHandlers: Record = { strg: handleSaveApply, aply: handleSaveApply, @@ -654,7 +647,6 @@ const buttonActionHandlers: Record = dmndSplmnt: handleDmndSplmnt, atrzAprv: handleAtrzAprv, atrzRply: handleAtrzRply, - grndsChck: handleGrndsChck, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/as/src/pages/instl-sncn/lib/button-config.ts b/apps/as/src/pages/instl-sncn/lib/button-config.ts index 83ad6c54a..28ce89f9e 100644 --- a/apps/as/src/pages/instl-sncn/lib/button-config.ts +++ b/apps/as/src/pages/instl-sncn/lib/button-config.ts @@ -15,7 +15,6 @@ export const INSTL_SNCN_VIEW_BUTTON_ACTION = { dmndSplmnt: "dmndSplmnt", atrzAprv: "atrzAprv", atrzRply: "atrzRply", - grndsChck: "grndsChck", } as const; export type InstlSncnViewButtonAction = @@ -44,10 +43,6 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record node.useYn === "Y") - .map((node) => ({ - cnstpSeCd: node.cnstpSeCd, - bscPl: node.bscPlanQty, - budgetReflct: node.bgtRfltQty, - thtiDsgn: node.tstmDesignQty, - diff: node.tstmDesignQty - node.bgtRfltQty, - })); -} - -export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] { - return bizCstList - .filter((node) => node.useYn === "Y") - .map((node) => ({ - acntgSeCd: node.acntgSeCd, - bscPlanAmt: node.bscPlanAmt, - bgtRfltAmt: node.bgtRfltAmt, - tstmDesignAmt: node.tstmDesignAmt, - })); -} - -export function toEsntlAtchFileSaveDto( - esntlFileList: EsntlAtchFileMngTreeResponseDto[], -): InstlSncnEsntlAtchFileSaveDto[] { - const esntlMap = new Map(); - - for (const item of esntlFileList) { - if (!item.esntlAtchFileId || !item.atflCd) continue; - esntlMap.set(item.esntlAtchFileId, item.atflCd); - } - - return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ - esntlAtchFileId, - atchFileGroupId, - })); -} - -export function buildSavePayload( - data: InstlSncnMngDetResponse, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, -): InstlSncnApplySaveRequestDto { - const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data; - - return { - instlSncnMng: { ...detDto, prgrsSttsComCd }, - bzScaleList: toBzScaleSaveDto(bizSclList), - bizCstList: toBizCstSaveDto(bizCstList), - cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), - esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), - }; -} - -export function buildTechReviewReplyPayload( - data: InstlSncnMngDetResponse, - checklistFormState: ChecklistFormState, - reviewStep: ReviewStepCode, - prgrsSttsComCd: string, - techReViewSaveCd: string, - reviewOpinionFormData?: ReviewOpinionFormData | null, - opnrNm?: string, -): InstlSncnApplySaveRequestDto { - return { - ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), - techReViewSaveCd, - rvwStpCd: reviewStep, - rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), - }; -} - -export function buildTechReviewPayload( - det: InstlSncnMngResponseDto, - cklstResultList: InstlSncnCklstResultSaveDto[], -): InstlSncnApplySaveRequestDto { - return { - instlSncnMng: { ...det, prgrsSttsComCd: "ER" }, - bzScaleList: [], - bizCstList: [], - cklstResultList, - esntlAtchFileList: [], - }; -} +import type { + BizCstTreeDto, + BizSclTreeDto, + EsntlAtchFileMngTreeResponseDto, + InstlSncnApplySaveRequestDto, + InstlSncnBizCstSaveRequestDto, + InstlSncnBzScaleSaveDto, + InstlSncnEsntlAtchFileSaveDto, + InstlSncnMngDetResponse, + InstlSncnSplmntDmndSaveDto, + ReviewStepCode, +} from "@/entities/instl-sncn"; +import type { SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; + +import type { ChecklistFormState } from "../model/view-types"; + +import { toCklstSaveDto } from "./checklist-utils"; +import { type ReviewOpinionFormData, toRvwOpnnSaveDto } from "./review-opinion-utils"; + +export function toBzScaleSaveDto(bizSclList: BizSclTreeDto[]): InstlSncnBzScaleSaveDto[] { + return bizSclList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + cnstpSeCd: node.cnstpSeCd, + bscPl: node.bscPlanQty, + budgetReflct: node.bgtRfltQty, + thtiDsgn: node.tstmDesignQty, + diff: node.tstmDesignQty - node.bgtRfltQty, + })); +} + +export function toBizCstSaveDto(bizCstList: BizCstTreeDto[]): InstlSncnBizCstSaveRequestDto[] { + return bizCstList + .filter((node) => node.useYn === "Y") + .map((node) => ({ + acntgSeCd: node.acntgSeCd, + bscPlanAmt: node.bscPlanAmt, + bgtRfltAmt: node.bgtRfltAmt, + tstmDesignAmt: node.tstmDesignAmt, + })); +} + +export function toEsntlAtchFileSaveDto( + esntlFileList: EsntlAtchFileMngTreeResponseDto[], +): InstlSncnEsntlAtchFileSaveDto[] { + const esntlMap = new Map(); + + for (const item of esntlFileList) { + if (!item.esntlAtchFileId || !item.atflCd) continue; + esntlMap.set(item.esntlAtchFileId, item.atflCd); + } + + return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({ + esntlAtchFileId, + atchFileGroupId, + })); +} + +export function buildSavePayload( + data: InstlSncnMngDetResponse, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, +): InstlSncnApplySaveRequestDto { + const { detDto, bizSclList, bizCstList, cklstResultList = [], esntlFileList } = data; + + return { + instlSncnMng: { ...detDto, prgrsSttsComCd }, + bzScaleList: toBzScaleSaveDto(bizSclList), + bizCstList: toBizCstSaveDto(bizCstList), + cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep), + esntlAtchFileList: toEsntlAtchFileSaveDto(esntlFileList), + }; +} + +export function buildTechReviewReplyPayload( + data: InstlSncnMngDetResponse, + checklistFormState: ChecklistFormState, + reviewStep: ReviewStepCode, + prgrsSttsComCd: string, + techReViewSaveCd: string, + reviewOpinionFormData?: ReviewOpinionFormData | null, + opnrNm?: string, +): InstlSncnApplySaveRequestDto { + return { + ...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd), + techReViewSaveCd, + rvwStpCd: reviewStep, + rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm), + }; +} + +export function buildSupplementReqPayload( + data: InstlSncnMngDetResponse, + payload: SplmntDmndDialogConfirmPayload, + checklistFormState: ChecklistFormState, +): InstlSncnApplySaveRequestDto { + const existingItems: InstlSncnSplmntDmndSaveDto[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1; + + const newItems: InstlSncnSplmntDmndSaveDto[] = payload.items.map((item, index) => ({ + splmntDmndSn: nextSn + index, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + +function toSplmntDmndSaveItem(item: InstlSncnSplmntDmndSaveDto): InstlSncnSplmntDmndSaveDto { + return { + splmntDmndSn: item.splmntDmndSn, + splmntArtclComCd: item.splmntArtclComCd, + splmntDmndCn: item.splmntDmndCn, + splmntDmndDt: item.splmntDmndDt, + splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, + splmntCn: item.splmntCn, + splmntPicNm: item.splmntPicNm, + splmntCmptnDt: item.splmntCmptnDt, + splmntAtchFileGroupId: item.splmntAtchFileGroupId, + cmptnYn: item.cmptnYn, + }; +} diff --git a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx index eaed1e8c5..a65ddc291 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx @@ -27,7 +27,7 @@ import { instlSncnService, } from "@/entities/instl-sncn"; import { useRvwMnpwMngLoginUserList } from "@/entities/rvw-mnpw"; -import { TextInputDialog } from "@/widgets/pbadms-sprt"; +import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; import { @@ -37,7 +37,11 @@ import { needsRoleUser01AtrzResolve, } from "../../lib/button-config"; import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils"; -import { buildSavePayload, buildTechReviewReplyPayload } from "../../lib/save-payload-utils"; +import { + buildSavePayload, + buildSupplementReqPayload, + buildTechReviewReplyPayload, +} from "../../lib/save-payload-utils"; import { type ChecklistFormState } from "../../model/view-types"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; @@ -401,11 +405,12 @@ function FloatingActionButtons({ setActiveDialog("dmndSplmnt"); } - async function handleDmndSplmntConfirm(splmntCn: string) { + async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) { setActiveDialog(null); + const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState); await executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, - request: splmntCn, + request: requestPayload, successMessage: "보완요청이 처리되었습니다.", }); } @@ -517,13 +522,11 @@ function FloatingActionButtons({ onConfirm={handleElaprTmpltConfirm} /> - setActiveDialog(null)} - title="보완요청 내용 입력" - placeholder="보완요청 내용을 입력하세요." isSubmitting={isProcessing} - onConfirm={(value) => void handleDmndSplmntConfirm(value)} + onConfirm={handleDmndSplmntConfirm} /> { - // TODO: 보완요청 API 호출 - context.onSuccess?.(); +async function handleDmndSplmnt( + context: ActionHandlerContext, +): Promise { + try { + const docNo = context.data.detDto.instlSncnDocNo; + if (!docNo) throw new Error("문서번호가 없습니다."); + await instlSncnService.submitReqSupplement(docNo, context.request); + context.onSuccess?.(); + } catch (error) { + context.onError?.(error as Error); + } } async function handleAtrzAprv(context: ActionHandlerContext): Promise { @@ -616,11 +627,6 @@ async function handleAtrzRply(context: ActionHandlerContext): Promise { context.onSuccess?.(); } -async function handleGrndsChck(context: ActionHandlerContext): Promise { - // TODO: 현장점검 등록 API 호출 - context.onSuccess?.(); -} - const buttonActionHandlers: Record = { strg: handleSaveApply, aply: handleSaveApply, @@ -638,7 +644,6 @@ const buttonActionHandlers: Record = { dmndSplmnt: handleDmndSplmnt, atrzAprv: handleAtrzAprv, atrzRply: handleAtrzRply, - grndsChck: handleGrndsChck, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx index e7b482c67..1e5230384 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx @@ -48,6 +48,7 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp const alert = useAlertStore((state) => state.setMessage); const [isSubmitting, setIsSubmitting] = useState(false); const isEditMode = mode === "edit"; + const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL"; const form = useForm({ resolver: zodResolver(instlSncnRegistSchema), @@ -186,23 +187,36 @@ function InstlFormPage({ id, mode, initialData, taskSeComCd }: InstlFormPageProp > 목록 - - {isEditMode && ( + {!isSplmntPrgrsStts ? ( <> - - + {isEditMode && ( + <> + + + + )} + ) : ( + )} diff --git a/apps/as/src/widgets/pbadms-sprt/index.ts b/apps/as/src/widgets/pbadms-sprt/index.ts index aeee59a58..bec3bc4c6 100644 --- a/apps/as/src/widgets/pbadms-sprt/index.ts +++ b/apps/as/src/widgets/pbadms-sprt/index.ts @@ -30,5 +30,9 @@ export { ContactInfoFormSection } from "./ui/contact-info-form-section"; export { FnrsSharFormSection } from "./ui/fnrs-shar-form-section"; export { PrivacyConsentSection } from "./ui/privacy-consent-section"; export { SectionCollapsible } from "./ui/section-collapsible"; +export { + SplmntDmndDialog, + type SplmntDmndDialogConfirmItem, + type SplmntDmndDialogConfirmPayload, +} from "./ui/splmnt-dmnd-dialog"; export { TaskSePickerDialog } from "./ui/task-se-picker-dialog"; -export { TextInputDialog } from "./ui/text-input-dialog"; diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx new file mode 100644 index 000000000..9373285c2 --- /dev/null +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx @@ -0,0 +1,329 @@ +"use client"; + +import { useEffect, useId, useState } from "react"; + +import { getDefaultFileKey, useTus } from "@/shared/lib"; +import { attachService } from "@/entities/attach"; +import { CodeSelect } from "@/entities/code"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Textarea, +} from "@common/components"; + +const SPLMNT_ARTCL_UP_CD = "SPLMNTARTCL"; +const SPLMNT_DMND_FILE_KEY_PREFIX = "splmnt-dmnd::"; + +interface UploadedSplmntFile { + fileName: string; + fileKey: string; + atflId?: string; + atflCd?: string; +} + +interface SplmntDmndRow { + rowKey: string; + splmntArtclComCd: string; + splmntDmndCn: string; + splmntDmndAtchFileGroupId: string; + uploadedFile: UploadedSplmntFile | null; +} + +interface SplmntDmndDialogConfirmItem { + splmntArtclComCd: string; + splmntDmndCn: string; + splmntDmndAtchFileGroupId?: string; +} + +interface SplmntDmndDialogConfirmPayload { + items: SplmntDmndDialogConfirmItem[]; +} + +interface SplmntDmndDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + isSubmitting?: boolean; + onConfirm: (payload: SplmntDmndDialogConfirmPayload) => void; +} + +function createRowKey() { + return `splmnt-row-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + +function createEmptyRow(): SplmntDmndRow { + return { + rowKey: createRowKey(), + splmntArtclComCd: "", + splmntDmndCn: "", + splmntDmndAtchFileGroupId: "", + uploadedFile: null, + }; +} + +function createUploadKey(rowKey: string, file: File) { + return `${SPLMNT_DMND_FILE_KEY_PREFIX}${rowKey}::${getDefaultFileKey(file)}`; +} + +function getRowKeyFromFileKey(fileKey: string): string | null { + if (!fileKey.startsWith(SPLMNT_DMND_FILE_KEY_PREFIX)) return null; + const rest = fileKey.slice(SPLMNT_DMND_FILE_KEY_PREFIX.length); + const separatorIndex = rest.indexOf("::"); + if (separatorIndex < 0) return null; + return rest.slice(0, separatorIndex); +} + +function isRowsValid(rows: SplmntDmndRow[]) { + return rows.every((row) => row.splmntArtclComCd.trim() && row.splmntDmndCn.trim()); +} + +function SplmntDmndDialog({ open, onOpenChange, isSubmitting, onConfirm }: SplmntDmndDialogProps) { + const formId = useId(); + const [rows, setRows] = useState([createEmptyRow()]); + + const { uploadFileAsync, isUploading, uploads } = useTus({ + onSuccess: (atflCd, atflId, _file, fileKey) => { + if (!atflCd || !atflId) return; + const rowKey = getRowKeyFromFileKey(fileKey); + if (!rowKey) return; + + setRows((prev) => + prev.map((row) => { + if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row; + return { + ...row, + splmntDmndAtchFileGroupId: atflCd, + uploadedFile: { ...row.uploadedFile, atflCd, atflId }, + }; + }), + ); + }, + onError: (_error, _file, fileKey) => { + const rowKey = getRowKeyFromFileKey(fileKey); + if (!rowKey) return; + + setRows((prev) => + prev.map((row) => { + if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row; + return { ...row, uploadedFile: null }; + }), + ); + }, + }); + + useEffect(() => { + if (!open) return; + setRows([createEmptyRow()]); + }, [open]); + + const isAnyUploading = rows.some( + (row) => row.uploadedFile != null && (isUploading[row.uploadedFile.fileKey] ?? false), + ); + const canSubmit = isRowsValid(rows) && !isAnyUploading && !isSubmitting; + + function updateRow(rowKey: string, patch: Partial) { + setRows((prev) => prev.map((row) => (row.rowKey === rowKey ? { ...row, ...patch } : row))); + } + + function handleAddRow() { + setRows((prev) => [...prev, createEmptyRow()]); + } + + function handleRemoveRow(rowKey: string) { + setRows((prev) => (prev.length <= 1 ? prev : prev.filter((row) => row.rowKey !== rowKey))); + } + + async function handleFileUpload(rowKey: string, files: FileList | null) { + if (!files || files.length === 0) return; + + const file = files[0]; + const fileKey = createUploadKey(rowKey, file); + const targetRow = rows.find((row) => row.rowKey === rowKey); + const existingAtflCd = targetRow?.splmntDmndAtchFileGroupId; + + updateRow(rowKey, { + uploadedFile: { fileName: file.name, fileKey }, + }); + + try { + await uploadFileAsync(file, { + fileKey, + metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, + }); + } catch { + // onError callback handles cleanup + } + } + + async function handleFileRemove(rowKey: string) { + const targetRow = rows.find((row) => row.rowKey === rowKey); + if (!targetRow?.uploadedFile) return; + + const { fileKey, atflId } = targetRow.uploadedFile; + uploads[fileKey]?.abort(); + + if (atflId) { + try { + await attachService.deleteAttachmentByAtflId(atflId); + } catch (error) { + console.error("보완요청 첨부파일 삭제 실패:", error); + } + } + + updateRow(rowKey, { + uploadedFile: null, + splmntDmndAtchFileGroupId: "", + }); + } + + function handleConfirm() { + if (!canSubmit) return; + + onConfirm({ + items: rows.map((row) => ({ + splmntArtclComCd: row.splmntArtclComCd.trim(), + splmntDmndCn: row.splmntDmndCn.trim(), + splmntDmndAtchFileGroupId: row.splmntDmndAtchFileGroupId || undefined, + })), + }); + } + + return ( + + + + 보완요청 + + +
+
+

보완 내용

+ +
+ +
+ + + + + + {rows.length > 1 ? : null} + + + + 보완 항목 + 보완 의견 + 첨부파일 + {rows.length > 1 ? 관리 : null} + + + + {rows.map((row) => { + const uploading = row.uploadedFile + ? (isUploading[row.uploadedFile.fileKey] ?? false) + : false; + const fileInputId = `${formId}-${row.rowKey}-file`; + + return ( + + + + updateRow(row.rowKey, { splmntArtclComCd: value }) + } + placeholder="보완항목 선택" + className="w-full min-w-[12rem]" + /> + + +