Merge branch 'iipbs/as/dev'

This commit is contained in:
2026-09-23 09:50:30 +09:00
52 changed files with 851 additions and 768 deletions
@@ -2,35 +2,35 @@ import { z } from "zod";
/** 백엔드 DcRagFileInfoDto */
export const dcRagFileInfoSchema = z.object({
fileId: z.string().optional(),
fileGroupId: z.string().optional(),
fileName: z.string().optional(),
physicalPath: z.string().optional(),
downloadUrl: z.string().optional(),
fileSize: z.number().optional(),
esntlAtchFileId: z.string().optional(),
esntlAtchFileNm: z.string().optional(),
fileId: z.string().nullish(),
fileGroupId: z.string().nullish(),
fileName: z.string().nullish(),
physicalPath: z.string().nullish(),
downloadUrl: z.string().nullish(),
fileSize: z.number().nullish(),
esntlAtchFileId: z.string().nullish(),
esntlAtchFileNm: z.string().nullish(),
});
/** 백엔드 DcRagDocumentTransferRequestDto */
export const dcRagDocumentTransferRequestSchema = z.object({
fnrsCnsltnDocNo: z.string(),
taskSeComCd: z.string().optional(),
taskSeCdNm: z.string().optional(),
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
etcFiles: z.array(dcRagFileInfoSchema).optional(),
callbackUrl: z.string().optional(),
taskSeComCd: z.string().nullish(),
taskSeCdNm: z.string().nullish(),
essentialFiles: z.array(dcRagFileInfoSchema).nullish(),
etcFiles: z.array(dcRagFileInfoSchema).nullish(),
callbackUrl: z.string().nullish(),
});
/** 백엔드 DcRagDraftGenerateRequestDto */
export const dcRagDraftGenerateRequestSchema = z.object({
fnrsCnsltnDocNo: z.string(),
pwrkCnsltnDocNo: z.string(),
crtDmndUuid: z.string().optional(),
essentialFiles: z.array(dcRagFileInfoSchema).optional(),
etcFiles: z.array(dcRagFileInfoSchema).optional(),
rqstrUuid: z.string().optional(),
callbackUrl: z.string().optional(),
crtDmndUuid: z.string().nullish(),
essentialFiles: z.array(dcRagFileInfoSchema).nullish(),
etcFiles: z.array(dcRagFileInfoSchema).nullish(),
rqstrUuid: z.string().nullish(),
callbackUrl: z.string().nullish(),
});
/** 백엔드 DcRagCostVerifyRequestDto */
@@ -2,68 +2,68 @@ import { z } from "zod";
/** 백엔드 DesignCnstnMngSaveRequestDto */
export const designCnstnMngSaveSchema = z.object({
designCnstnDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
taskSeComCd: z.string().optional(),
bizNm: z.string().optional(),
bizBgngYr: z.string().optional(),
bizEndYr: z.string().optional(),
bizCn: z.string().optional(),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
dsgnrNm: z.string().optional(),
dsgnrTelno: z.string().optional(),
dsgnrOgdpNm: z.string().optional(),
dsgnrEmlAddr: z.string().optional(),
atchFileGroupId: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
designCnstnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
export const designCnstnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().optional(),
bscPl: z.number().optional(),
budgetReflct: z.number().optional(),
thtiDsgn: z.number().optional(),
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
export const designCnstnBizCstItemSchema = z.object({
acntgSeCd: z.string().optional(),
bscPlanAmt: z.number().optional(),
bgtRfltAmt: z.number().optional(),
tstmDesignAmt: z.number().optional(),
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
export const designCnstnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
export const designCnstnRvwOpnnSaveSchema = z.object({
designCnstnDocNo: z.string(),
rvwOpnnSn: z.number().optional(),
opnrSeComCd: z.string().optional(),
opnrNm: z.string().optional(),
rvwRsltCn: z.string().optional(),
rvwRsltAnmlyYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 DesignCnstnApplySaveRequestDto */
export const designCnstnApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
designCnstnMng: designCnstnMngSaveSchema,
bzScaleList: z.array(designCnstnBzScaleItemSchema).optional(),
bizCstList: z.array(designCnstnBizCstItemSchema).optional(),
esntlAtchFileList: z.array(designCnstnEsntlAtchFileItemSchema).optional(),
rvwOpnnList: z.array(designCnstnRvwOpnnSaveSchema).optional(),
bzScaleList: z.array(designCnstnBzScaleItemSchema).nullish(),
bizCstList: z.array(designCnstnBizCstItemSchema).nullish(),
esntlAtchFileList: z.array(designCnstnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(designCnstnRvwOpnnSaveSchema).nullish(),
});
export type DesignCnstnMngSaveRequestDto = z.infer<typeof designCnstnMngSaveSchema>;
+1
View File
@@ -41,6 +41,7 @@ export {
validateAllRowsForSave,
validateAplnRow,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "./lib/apln-validators";
export {
@@ -76,3 +76,16 @@ export function validateUniqueSeqNo(rows: ElaprAplnRow[]): string | null {
}
return null;
}
export function validateUniqueAtrzAprvr(rows: ElaprAplnRow[]): string | null {
const pairSet = new Set<string>();
for (const row of getVisibleRows(rows)) {
const atrz = row.atrzSeComCd?.trim();
const aprvr = row.aprvrUuid?.trim();
if (!atrz || !aprvr) continue;
const key = `${atrz}::${aprvr}`;
if (pairSet.has(key)) return "동일한 결재구분과 결재자가 중복됩니다.";
pairSet.add(key);
}
return null;
}
@@ -7,8 +7,8 @@ export interface ElaprDmndSaveRequestDto {
elaprDocNo: string;
dmndTtl: string;
dmndCn: string;
elaprCycl?: number;
docPrgrsSttsComCd?: string;
elaprCycl?: number | null;
docPrgrsSttsComCd?: string | null;
elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[];
}
@@ -17,7 +17,7 @@ import {
} from "@common/components";
import { useElaprAplnPage } from "../api/queries";
import { validateAplnRow } from "../lib/apln-validators";
import { validateAplnRow, validateUniqueAtrzAprvr } from "../lib/apln-validators";
import { useAplnRows } from "../lib/use-apln-rows";
import { useDndSensors } from "../lib/use-dnd-sensors";
import type { ElaprApln } from "../model/elapr-apln-interface";
@@ -110,6 +110,12 @@ function ElaprAplnPickerGrid({
return;
}
const duplicateError = validateUniqueAtrzAprvr(rows);
if (duplicateError) {
alert(duplicateError);
return;
}
commitEdit(row.rowKey);
}
@@ -17,6 +17,7 @@ import {
getVisibleRows,
validateAllRowsForSave,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "../lib/apln-validators";
import { mapDocPrgrsToAplnRows } from "../lib/doc-prgrs-to-apln-rows";
@@ -90,6 +91,12 @@ function ElaprDocPrgrsPickerDialog({
return;
}
const atrzAprvrError = validateUniqueAtrzAprvr(aplnRows);
if (atrzAprvrError) {
alert(atrzAprvrError);
return;
}
const visibleRows = getVisibleRows(aplnRows);
onConfirm?.({ aplnRows: visibleRows });
@@ -17,6 +17,7 @@ import {
getVisibleRows,
validateAllRowsForSave,
validateAtrzOrder,
validateUniqueAtrzAprvr,
validateUniqueSeqNo,
} from "../lib/apln-validators";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
@@ -106,6 +107,12 @@ function ElaprTmpltPickerDialog({
return;
}
const atrzAprvrError = validateUniqueAtrzAprvr(aplnRows);
if (atrzAprvrError) {
alert(atrzAprvrError);
return;
}
const visibleRows = getVisibleRows(aplnRows);
const payload: ElaprTmpltPickerConfirmPayload = {
@@ -2,52 +2,52 @@ import { z } from "zod";
/** 백엔드 FnrsCnsltnMngSaveRequestDto */
export const fnrsCnsltnMngSaveSchema = z.object({
fnrsCnsltnDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
taskSeComCd: z.string().optional(),
cfmtnBgtBizNo: z.string().optional(),
bgtAplyYr: z.string().optional(),
bizNm: z.string().optional(),
bizBgngYr: z.string().optional(),
bizEndYr: z.string().optional(),
bizCn: z.string().optional(),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
dsgnrNm: z.string().optional(),
dsgnrTelno: z.string().optional(),
dsgnrOgdpNm: z.string().optional(),
dsgnrEmlAddr: z.string().optional(),
atchFileGroupId: z.string().optional(),
sharAtchFileGroupId: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
rtlztnFcltSeComCd: z.string().optional(),
rtlztnUsgSeComCd: z.string().optional(),
fnrsCnsltnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
cfmtnBgtBizNo: z.string().nullish(),
bgtAplyYr: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
sharAtchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
rtlztnFcltSeComCd: z.string().nullish(),
rtlztnUsgSeComCd: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.BzScaleItem */
export const fnrsCnsltnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().optional(),
bscPl: z.number().optional(),
budgetReflct: z.number().optional(),
thtiDsgn: z.number().optional(),
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.BizCstItem */
export const fnrsCnsltnBizCstItemSchema = z.object({
acntgSeCd: z.string().optional(),
bscPlanAmt: z.number().optional(),
bgtRfltAmt: z.number().optional(),
tstmDesignAmt: z.number().optional(),
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.CklstResultItem */
export const fnrsCnsltnCklstResultItemSchema = z.object({
cklstRvwSn: z.number().optional(),
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string().nullish(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
@@ -55,58 +55,58 @@ export const fnrsCnsltnCklstResultItemSchema = z.object({
/** 백엔드 FnrsCnsltnApplySaveRequestDto.EsntlAtchFileItem */
export const fnrsCnsltnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsCnsltnSplmntDmndItem */
export const fnrsCnsltnSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
splmntDmndAtchFileGroupId: z.string().optional(),
splmntCn: z.string().optional(),
splmntPicNm: z.string().optional(),
splmntCmptnDt: z.string().optional(),
splmntAtchFileGroupId: z.string().optional(),
cmptnYn: z.string().optional(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
export const fnrsCnsltnRvwOpnnSaveSchema = z.object({
fnrsCnsltnDocNo: z.string(),
rvwOpnnSn: z.number().optional(),
opnrSeComCd: z.string().optional(),
opnrNm: z.string().optional(),
rvwRsltCn: z.string().optional(),
rvwRsltAnmlyYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
export const fnrsSharItemSchema = z.object({
fnrsSharSeCd: z.string().optional(),
exstnAmt: z.number().optional(),
chgRqtAmt: z.number().optional(),
ajmtAmt: z.number().optional(),
rmrkCn: z.string().optional(),
envoRmrkCn: z.string().optional(),
fnrsSharSeCd: z.string().nullish(),
exstnAmt: z.number().nullish(),
chgRqtAmt: z.number().nullish(),
ajmtAmt: z.number().nullish(),
rmrkCn: z.string().nullish(),
envoRmrkCn: z.string().nullish(),
});
/** 백엔드 FnrsCnsltnApplySaveRequestDto */
export const fnrsCnsltnApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
fnrsCnsltnMng: fnrsCnsltnMngSaveSchema,
bzScaleList: z.array(fnrsCnsltnBzScaleItemSchema).optional(),
bizCstList: z.array(fnrsCnsltnBizCstItemSchema).optional(),
cklstResultList: z.array(fnrsCnsltnCklstResultItemSchema).optional(),
cklstResultMap: z.record(z.array(fnrsCnsltnCklstResultItemSchema)).optional(),
esntlAtchFileList: z.array(fnrsCnsltnEsntlAtchFileItemSchema).optional(),
rvwOpnnList: z.array(fnrsCnsltnRvwOpnnSaveSchema).optional(),
splmntDmndList: z.array(fnrsCnsltnSplmntDmndItemSchema).optional(),
fnrsSharList: z.array(fnrsSharItemSchema).optional(),
bzScaleList: z.array(fnrsCnsltnBzScaleItemSchema).nullish(),
bizCstList: z.array(fnrsCnsltnBizCstItemSchema).nullish(),
cklstResultList: z.array(fnrsCnsltnCklstResultItemSchema).nullish(),
cklstResultMap: z.record(z.array(fnrsCnsltnCklstResultItemSchema)).nullish(),
esntlAtchFileList: z.array(fnrsCnsltnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(fnrsCnsltnRvwOpnnSaveSchema).nullish(),
splmntDmndList: z.array(fnrsCnsltnSplmntDmndItemSchema).nullish(),
fnrsSharList: z.array(fnrsSharItemSchema).nullish(),
});
export type FnrsCnsltnMngSaveRequestDto = z.infer<typeof fnrsCnsltnMngSaveSchema>;
@@ -2,12 +2,12 @@ import { z } from "zod";
/** 백엔드 FnrsExamineRequestDto */
export const fnrsExamineRequestSchema = z.object({
prgrsSttsComCd: z.string().optional(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 EnvoActnReviewReqDto */
export const envoActnReviewReqSchema = z.object({
specialRqstCn: z.string().optional(),
specialRqstCn: z.string().nullish(),
});
export type FnrsExamineRequestDto = z.infer<typeof fnrsExamineRequestSchema>;
@@ -2,52 +2,52 @@ import { z } from "zod";
/** 백엔드 InstlSncnMngSaveRequestDto */
export const instlSncnMngSaveSchema = z.object({
instlSncnDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
sncnSeComCd: z.string().optional(),
taskSeComCd: z.string().optional(),
bgtBizCd: z.string().optional(),
bgtAplyYr: z.string().optional(),
bizNm: z.string().optional(),
bizBgngYr: z.string().optional(),
bizEndYr: z.string().optional(),
bizCn: z.string().optional(),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
dsgnrNm: z.string().optional(),
dsgnrTelno: z.string().optional(),
dsgnrOgdpNm: z.string().optional(),
dsgnrEmlAddr: z.string().optional(),
atchFileGroupId: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
rtlztnFcltSeComCd: z.string().optional(),
rtlztnUsgSeComCd: z.string().optional(),
instlSncnDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
sncnSeComCd: z.string().nullish(),
taskSeComCd: z.string().nullish(),
bgtBizCd: z.string().nullish(),
bgtAplyYr: z.string().nullish(),
bizNm: z.string().nullish(),
bizBgngYr: z.string().nullish(),
bizEndYr: z.string().nullish(),
bizCn: z.string().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
rtlztnFcltSeComCd: z.string().nullish(),
rtlztnUsgSeComCd: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.BzScaleItem (diff는 API 미전송) */
export const instlSncnBzScaleItemSchema = z.object({
cnstpSeCd: z.string().optional(),
bscPl: z.number().optional(),
budgetReflct: z.number().optional(),
thtiDsgn: z.number().optional(),
cnstpSeCd: z.string().nullish(),
bscPl: z.number().nullish(),
budgetReflct: z.number().nullish(),
thtiDsgn: z.number().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.BizCstItem */
export const instlSncnBizCstItemSchema = z.object({
acntgSeCd: z.string().optional(),
bscPlanAmt: z.number().optional(),
bgtRfltAmt: z.number().optional(),
tstmDesignAmt: z.number().optional(),
acntgSeCd: z.string().nullish(),
bscPlanAmt: z.number().nullish(),
bgtRfltAmt: z.number().nullish(),
tstmDesignAmt: z.number().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.CklstResultItem */
export const instlSncnCklstResultItemSchema = z.object({
cklstRvwSn: z.number().optional(),
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string().nullish(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
@@ -55,47 +55,47 @@ export const instlSncnCklstResultItemSchema = z.object({
/** 백엔드 InstlSncnApplySaveRequestDto.EsntlAtchFileItem */
export const instlSncnEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 InstlSncnRvwOpnnSaveRequestDto */
export const instlSncnRvwOpnnSaveSchema = z.object({
instlSncnDocNo: z.string(),
rvwOpnnSn: z.number().optional(),
opnrSeComCd: z.string().optional(),
opnrNm: z.string().optional(),
rvwRsltCn: z.string().optional(),
rvwRsltAnmlyYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto.InstlSncnSplmntDmndItem */
export const instlSncnSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
splmntDmndAtchFileGroupId: z.string().optional(),
splmntCn: z.string().optional(),
splmntPicNm: z.string().optional(),
splmntCmptnDt: z.string().optional(),
splmntAtchFileGroupId: z.string().optional(),
cmptnYn: z.string().optional(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 InstlSncnApplySaveRequestDto */
export const instlSncnApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
instlSncnMng: instlSncnMngSaveSchema,
bzScaleList: z.array(instlSncnBzScaleItemSchema).optional(),
bizCstList: z.array(instlSncnBizCstItemSchema).optional(),
cklstResultList: z.array(instlSncnCklstResultItemSchema).optional(),
cklstResultMap: z.record(z.array(instlSncnCklstResultItemSchema)).optional(),
esntlAtchFileList: z.array(instlSncnEsntlAtchFileItemSchema).optional(),
rvwOpnnList: z.array(instlSncnRvwOpnnSaveSchema).optional(),
splmntDmndList: z.array(instlSncnSplmntDmndItemSchema).optional(),
bzScaleList: z.array(instlSncnBzScaleItemSchema).nullish(),
bizCstList: z.array(instlSncnBizCstItemSchema).nullish(),
cklstResultList: z.array(instlSncnCklstResultItemSchema).nullish(),
cklstResultMap: z.record(z.array(instlSncnCklstResultItemSchema)).nullish(),
esntlAtchFileList: z.array(instlSncnEsntlAtchFileItemSchema).nullish(),
rvwOpnnList: z.array(instlSncnRvwOpnnSaveSchema).nullish(),
splmntDmndList: z.array(instlSncnSplmntDmndItemSchema).nullish(),
});
export type InstlSncnMngSaveRequestDto = z.infer<typeof instlSncnMngSaveSchema>;
@@ -4,7 +4,7 @@ import { z } from "zod";
export const pwrkCnsltnDocCrtRsltSaveRequestSchema = z.object({
crtDmndUuid: z.string().uuid(),
pwrkCnsltnDocNo: z.string().min(1).max(100),
atchFileGroupId: z.string().max(30).optional(),
atchFileGroupId: z.string().max(30).nullish(),
});
export type PwrkCnsltnDocCrtRsltSaveRequestDto = z.infer<
@@ -3,10 +3,10 @@ import { z } from "zod";
/** 백엔드 PwrkCnsltnDocEvlSaveRequestDto */
export const pwrkCnsltnDocEvlSaveRequestSchema = z.object({
pwrkCnsltnDocNo: z.string().min(1),
pwrkCnsltnDocEvlUuid: z.string().uuid().optional(),
pwrkCnsltnDocEvlCn: z.string().max(2000).optional(),
pwrkCnsltnDocEvlScr: z.number().optional(),
pwrkCnsltnDocEvlDt: z.string().optional(),
pwrkCnsltnDocEvlUuid: z.string().uuid().nullish(),
pwrkCnsltnDocEvlCn: z.string().max(2000).nullish(),
pwrkCnsltnDocEvlScr: z.number().nullish(),
pwrkCnsltnDocEvlDt: z.string().nullish(),
});
export type PwrkCnsltnDocEvlSaveRequestDto = z.infer<typeof pwrkCnsltnDocEvlSaveRequestSchema>;
@@ -3,18 +3,18 @@ import { z } from "zod";
/** 백엔드 PwrkActnResultSubmitDto.ActnItemDto */
export const pwrkActnResultActnItemSchema = z.object({
rvwOpnnSn: z.number(),
actnRsltCn: z.string().optional(),
rfltYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
actnRsltCn: z.string().nullish(),
rfltYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 PwrkActnResultSubmitDto */
export const pwrkActnResultSubmitSchema = z.object({
isSubmit: z.boolean(),
actnList: z.array(pwrkActnResultActnItemSchema).optional(),
totActnSummary: z.string().optional(),
muniDocNo: z.string().optional(),
pwrkAtchFileGroupId: z.string().optional(),
actnList: z.array(pwrkActnResultActnItemSchema).nullish(),
totActnSummary: z.string().nullish(),
muniDocNo: z.string().nullish(),
pwrkAtchFileGroupId: z.string().nullish(),
});
export type PwrkActnResultSubmitDto = z.infer<typeof pwrkActnResultSubmitSchema>;
@@ -2,50 +2,50 @@ import { z } from "zod";
/** 백엔드 SewerBscPlanSaveRequestDto */
export const sewerBscPlanSaveSchema = z.object({
sewerBscPlanDocNo: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
sewerBscPlanSeComCd: z.string().optional(),
sewerBscPlanNm: z.string().optional(),
taskSeComCd: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
sewerBscPlanDocNo: z.string().nullish(),
admdstCd: z.string().nullish(),
envoSeComCd: z.string().nullish(),
sewerBscPlanSeComCd: z.string().nullish(),
sewerBscPlanNm: z.string().nullish(),
taskSeComCd: z.string().nullish(),
prgrsSttsComCd: z.string().nullish(),
});
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
export const sewerBscPlanDtlSaveSchema = z.object({
sewerBscPlanDocNo: z.string().optional(),
chgCycl: z.number().optional(),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
dsgnrNm: z.string().optional(),
dsgnrTelno: z.string().optional(),
dsgnrOgdpNm: z.string().optional(),
dsgnrEmlAddr: z.string().optional(),
planYr: z.string().optional(),
crtrYr: z.string().optional(),
bizOtlnCn: z.string().optional(),
frlvlGoalYr: z.string().optional(),
sclvlGoalYr: z.string().optional(),
thlvlGoalYr: z.string().optional(),
folvlGoalYr: z.string().optional(),
atchFileGroupId: z.string().optional(),
sewerBscPlanDocNo: z.string().nullish(),
chgCycl: z.number().nullish(),
aplcntNm: z.string().nullish(),
aplcntMblTelno: z.string().nullish(),
aplcntOgdpNm: z.string().nullish(),
aplcntEmlAddr: z.string().nullish(),
dsgnrNm: z.string().nullish(),
dsgnrTelno: z.string().nullish(),
dsgnrOgdpNm: z.string().nullish(),
dsgnrEmlAddr: z.string().nullish(),
planYr: z.string().nullish(),
crtrYr: z.string().nullish(),
bizOtlnCn: z.string().nullish(),
frlvlGoalYr: z.string().nullish(),
sclvlGoalYr: z.string().nullish(),
thlvlGoalYr: z.string().nullish(),
folvlGoalYr: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
export const sewerBscPlanOtlnItemSchema = z.object({
planStpNo: z.number().optional(),
sewerPlanPpltnCnt: z.number().optional(),
sewerPrcsPpltnCnt: z.number().optional(),
sewerPsplyRt: z.number().optional(),
planStpNo: z.number().nullish(),
sewerPlanPpltnCnt: z.number().nullish(),
sewerPrcsPpltnCnt: z.number().nullish(),
sewerPsplyRt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
export const sewerBscPlanCklstResultItemSchema = z.object({
cklstRvwSn: z.number().optional(),
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
cklstRvwSn: z.number().nullish(),
cklstArtclSn: z.number().nullish(),
cklstSn: z.number().nullish(),
rvwStpComCd: z.string(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
@@ -53,109 +53,109 @@ export const sewerBscPlanCklstResultItemSchema = z.object({
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
export const sewerBscPlanFnrsCstItemSchema = z.object({
planStpNo: z.number().optional(),
gvexpAmt: z.number().optional(),
lcexpAmt: z.number().optional(),
orgprBdchgAmt: z.number().optional(),
etcAmt: z.number().optional(),
sumAmt: z.number().optional(),
planStpNo: z.number().nullish(),
gvexpAmt: z.number().nullish(),
lcexpAmt: z.number().nullish(),
orgprBdchgAmt: z.number().nullish(),
etcAmt: z.number().nullish(),
sumAmt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
export const sewerBscPlanSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
splmntDmndAtchFileGroupId: z.string().optional(),
splmntCn: z.string().optional(),
splmntPicNm: z.string().optional(),
splmntCmptnDt: z.string().optional(),
splmntAtchFileGroupId: z.string().optional(),
cmptnYn: z.string().optional(),
splmntDmndCn: z.string().nullish(),
splmntDmndDt: z.string().nullish(),
splmntDmndAtchFileGroupId: z.string().nullish(),
splmntCn: z.string().nullish(),
splmntPicNm: z.string().nullish(),
splmntCmptnDt: z.string().nullish(),
splmntAtchFileGroupId: z.string().nullish(),
cmptnYn: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
export const sewerBscPlanRvwOpnnItemSchema = z.object({
rvwOpnnSn: z.number().optional(),
opnrSeComCd: z.string().optional(),
opnrNm: z.string().optional(),
rvwRsltCn: z.string().optional(),
rvwRsltAnmlyYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
rvwOpnnSn: z.number().nullish(),
opnrSeComCd: z.string().nullish(),
opnrNm: z.string().nullish(),
rvwRsltCn: z.string().nullish(),
rvwRsltAnmlyYn: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
export const sewerBscPlanEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
esntlAtchFileId: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
export const sewerBscPlanBizCstItemSchema = z.object({
planStpNo: z.number().optional(),
acntgSeCd: z.string().optional(),
cstrnAmt: z.number().optional(),
planStpNo: z.number().nullish(),
acntgSeCd: z.string().nullish(),
cstrnAmt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
export const sewerBscPlanTmplntDtlItemSchema = z.object({
planStpNo: z.number().optional(),
swtrmtFcltCd: z.string().optional(),
prcsZoneArea: z.number().optional(),
swtrmtPpltnCnt: z.number().optional(),
planSwqty: z.number().optional(),
fcltCpct: z.number().optional(),
sprsysDrtwPplLen: z.number().optional(),
sprsysExlnPplLen: z.number().optional(),
cbsysPplLen: z.number().optional(),
icptgPplLen: z.number().optional(),
drgCnt: z.number().optional(),
drtwRlyPumpCnt: z.number().optional(),
drtwPumpCnt: z.number().optional(),
exlnPumpCnt: z.number().optional(),
planStpNo: z.number().nullish(),
swtrmtFcltCd: z.string().nullish(),
prcsZoneArea: z.number().nullish(),
swtrmtPpltnCnt: z.number().nullish(),
planSwqty: z.number().nullish(),
fcltCpct: z.number().nullish(),
sprsysDrtwPplLen: z.number().nullish(),
sprsysExlnPplLen: z.number().nullish(),
cbsysPplLen: z.number().nullish(),
icptgPplLen: z.number().nullish(),
drgCnt: z.number().nullish(),
drtwRlyPumpCnt: z.number().nullish(),
drtwPumpCnt: z.number().nullish(),
exlnPumpCnt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
export const sewerBscPlanTmplntItemSchema = z.object({
swtrmtFcltCd: z.string().optional(),
atchFileGroupId: z.string().optional(),
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).optional(),
swtrmtFcltCd: z.string().nullish(),
atchFileGroupId: z.string().nullish(),
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
export const sewerBscPlanTmplntTmprStrgItemSchema = z.object({
dtStrgDt: z.string().optional(),
swtrmtFcltCd: z.string().optional(),
prcsZoneArea: z.number().optional(),
swtrmtPpltnCnt: z.number().optional(),
planSwqty: z.number().optional(),
fcltCpct: z.number().optional(),
sprsysDrtwPplLen: z.number().optional(),
sprsysExlnPplLen: z.number().optional(),
cbsysPplLen: z.number().optional(),
icptgPplLen: z.number().optional(),
drgCnt: z.number().optional(),
drtwRlyPumpCnt: z.number().optional(),
drtwPumpCnt: z.number().optional(),
exlnPumpCnt: z.number().optional(),
dtStrgDt: z.string().nullish(),
swtrmtFcltCd: z.string().nullish(),
prcsZoneArea: z.number().nullish(),
swtrmtPpltnCnt: z.number().nullish(),
planSwqty: z.number().nullish(),
fcltCpct: z.number().nullish(),
sprsysDrtwPplLen: z.number().nullish(),
sprsysExlnPplLen: z.number().nullish(),
cbsysPplLen: z.number().nullish(),
icptgPplLen: z.number().nullish(),
drgCnt: z.number().nullish(),
drtwRlyPumpCnt: z.number().nullish(),
drtwPumpCnt: z.number().nullish(),
exlnPumpCnt: z.number().nullish(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto */
export const sewerBscPlanApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
techReViewSaveCd: z.string().nullish(),
rvwStpCd: z.string().nullish(),
sewerBscPlan: sewerBscPlanSaveSchema,
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema,
sewerBscPlanOtlnList: z.array(sewerBscPlanOtlnItemSchema).optional(),
cklstResultList: z.array(sewerBscPlanCklstResultItemSchema).optional(),
fnrsCstList: z.array(sewerBscPlanFnrsCstItemSchema).optional(),
splmntDmndList: z.array(sewerBscPlanSplmntDmndItemSchema).optional(),
rvwOpnnList: z.array(sewerBscPlanRvwOpnnItemSchema).optional(),
esntlAtchFileList: z.array(sewerBscPlanEsntlAtchFileItemSchema).optional(),
bizCstList: z.array(sewerBscPlanBizCstItemSchema).optional(),
sewerBscPlanTmplntList: z.array(sewerBscPlanTmplntItemSchema).optional(),
tmplntTmprStrgList: z.array(sewerBscPlanTmplntTmprStrgItemSchema).optional(),
sewerBscPlanOtlnList: z.array(sewerBscPlanOtlnItemSchema).nullish(),
cklstResultList: z.array(sewerBscPlanCklstResultItemSchema).nullish(),
fnrsCstList: z.array(sewerBscPlanFnrsCstItemSchema).nullish(),
splmntDmndList: z.array(sewerBscPlanSplmntDmndItemSchema).nullish(),
rvwOpnnList: z.array(sewerBscPlanRvwOpnnItemSchema).nullish(),
esntlAtchFileList: z.array(sewerBscPlanEsntlAtchFileItemSchema).nullish(),
bizCstList: z.array(sewerBscPlanBizCstItemSchema).nullish(),
sewerBscPlanTmplntList: z.array(sewerBscPlanTmplntItemSchema).nullish(),
tmplntTmprStrgList: z.array(sewerBscPlanTmplntTmprStrgItemSchema).nullish(),
});
export type SewerBscPlanSaveRequestDto = z.infer<typeof sewerBscPlanSaveSchema>;
@@ -23,8 +23,8 @@ export const designCnstnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
sido: z.string().optional(),
gugun: z.string().optional(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
bzScaleList: z.array(
designCnstnBzScaleItemSchema.extend({
@@ -32,7 +32,7 @@ export const designCnstnRegistSchema = z.object({
bscPl: z.number(),
budgetReflct: z.number(),
thtiDsgn: z.number(),
diff: z.number().optional(),
diff: z.number().nullish(),
}),
),
esntlAtchFileList: z.array(
@@ -52,7 +52,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -72,7 +72,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -51,14 +51,19 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
LE: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw],
TP: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
],
RB: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RA: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
],
AV: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
EV: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RK: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
RR: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply],
EP: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
};
@@ -4,7 +4,6 @@ import {
type ChecklistFormState,
type ChecklistProgress,
type ChecklistRowKey,
type ChecklistStepMap,
REVIEW_STEP_CODES,
type ReviewStepCode,
} from "../model/view-types";
@@ -17,15 +16,6 @@ export function buildChecklistRowKey(
return `${row.taskSeComCd}_${row.cklstSn}_${row.cklstArtclSn}_${row.cklstRvwSn}_${step}`;
}
export function groupByReviewStep(rows: CklstRvwRsltTreeDto[]): ChecklistStepMap {
const base: ChecklistStepMap = { LOCAL: [], ENV: [], KECO: [] };
rows.forEach((row) => {
const step = (row.rvwStpComCd ?? "LOCAL") as ReviewStepCode;
if (REVIEW_STEP_CODES.includes(step)) base[step].push(row);
});
return base;
}
export function extractTaskCodes(rows: CklstRvwRsltTreeDto[]): string[] {
return Array.from(new Set(rows.map((row) => row.taskSeComCd).filter(Boolean)));
}
@@ -70,13 +60,6 @@ export function computeChecklistProgress(
return { total, completed, percent };
}
export function filterChecklistRowsByReviewStep(
rows: CklstRvwRsltTreeDto[],
reviewStep: ReviewStepCode,
): CklstRvwRsltTreeDto[] {
return rows.filter((row) => row.rvwStpComCd === reviewStep);
}
export function computeReviewStepChecklistProgress(
rows: CklstRvwRsltTreeDto[],
results: ChecklistFormState["results"],
@@ -93,23 +76,6 @@ export function computeReviewStepChecklistProgress(
return { total, completed, percent };
}
export function buildChecklistFormState(rows: CklstRvwRsltTreeDto[]): ChecklistFormState {
const results: ChecklistFormState["results"] = {};
const comments: ChecklistFormState["comments"] = {};
rows.forEach((row) => {
const key = buildChecklistRowKey(row);
if (row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N") {
results[key] = row.rvwRsltAnmlyYn;
}
if (row.picRvwCn) comments[key] = row.picRvwCn;
});
return {
results,
comments,
progress: computeChecklistProgress(rows, results),
};
}
export function mergeChecklistFormStateFromServer(
prev: ChecklistFormState,
allRows: CklstRvwRsltTreeDto[],
@@ -27,8 +27,8 @@ export const fnrsCnsltnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
sido: z.string().optional(),
gugun: z.string().optional(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
bzScaleList: z.array(
fnrsCnsltnBzScaleItemSchema.extend({
@@ -36,7 +36,7 @@ export const fnrsCnsltnRegistSchema = z.object({
bscPl: z.number(),
budgetReflct: z.number(),
thtiDsgn: z.number(),
diff: z.number().optional(),
diff: z.number().nullish(),
}),
),
bizCstList: z.array(
@@ -52,8 +52,8 @@ export const fnrsCnsltnRegistSchema = z.object({
fnrsSharSeCd: z.string(),
exstnAmt: z.number(),
chgRqtAmt: z.number(),
ajmtAmt: z.number().optional(),
rmrkCn: z.string().optional(),
ajmtAmt: z.number().nullish(),
rmrkCn: z.string().nullish(),
}),
),
cklstResultList: z.array(
@@ -74,7 +74,7 @@ export const fnrsCnsltnRegistSchema = z.object({
splmntDmndList: z.array(
fnrsCnsltnSplmntDmndItemSchema.extend({
splmntArtclCdNm: z.string().nullish(),
splmntDmndAtchFileNm: z.string().optional(),
splmntDmndAtchFileNm: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -88,7 +88,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -15,7 +15,7 @@ export const pwrkCnsltnDocEvlFormSchema = z.object({
required_error: "만족도를 선택해 주세요.",
invalid_type_error: "만족도를 선택해 주세요.",
}),
pwrkCnsltnDocEvlCn: z.string().max(2000).optional(),
pwrkCnsltnDocEvlCn: z.string().max(2000).nullish(),
});
export type PwrkCnsltnDocEvlFormValues = z.infer<typeof pwrkCnsltnDocEvlFormSchema>;
@@ -1,3 +1,5 @@
import type { CklstRvwRsltTreeDto } from "@/entities/fnrs-cnsltn";
import {
PBADMS_PROGRESS_ORDER,
REVIEW_STEP_CODES,
@@ -28,6 +30,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -113,7 +113,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -44,10 +44,14 @@ export interface ChecklistFormState {
progress: ChecklistProgress;
}
export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -180,7 +180,9 @@ function FloatingActionButtons({
userInfo?.userUnqId,
);
const viewOnlyPwrkCnsltnDoc =
prgrsSttsComCd === "SL" || (prgrsSttsComCd === "RB" && dtlInstCd !== "ROLE_USER05");
prgrsSttsComCd === "SL" ||
(prgrsSttsComCd === "RB" && dtlInstCd !== "ROLE_USER05") ||
(prgrsSttsComCd === "RK" && dtlInstCd === "ROLE_USER03");
const actionButtons = useMemo(() => {
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
@@ -113,7 +113,8 @@ function PwrkCnsltnDocEvlDialog({ open, pwrkCnsltnDocNo, onClose }: PwrkCnsltnDo
<Textarea
id="pwrkCnsltnDocEvlCn"
rows={4}
placeholder="의견을 입력해 주세요 (선택)"
placeholder="의견을 입력해 주세요"
className="!text-xl"
{...register("pwrkCnsltnDocEvlCn")}
/>
</div>
@@ -18,6 +18,7 @@ import {
fnrsCnsltnMngService,
} from "@/entities/fnrs-cnsltn";
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng";
import {
AttachmentsFormSection,
buildAcntgCostItems,
@@ -35,7 +36,10 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
import { type FnrsCnsltnRegistFormData, fnrsCnsltnRegistSchema } from "../lib/fnrs-cnsltn-regist-schema";
import {
type FnrsCnsltnRegistFormData,
fnrsCnsltnRegistSchema,
} from "../lib/fnrs-cnsltn-regist-schema";
import {
buildFormPayload,
getCreateDefaultValues,
@@ -68,6 +72,12 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
const fnrsCnsltnDocNo = initialData?.detDto?.fnrsCnsltnDocNo;
const { data: pwrkCnsltnData } = usePwrkCnsltnMngByUpDocNo(
fnrsCnsltnDocNo,
isSplmntPrgrsStts && Boolean(fnrsCnsltnDocNo),
);
const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
@@ -126,7 +136,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
(item) => !item.splmntCn || !item.splmntCn.trim(),
);
if (hasEmptySplmntCn) {
alert("보완요청내용이 있습니다. 보완 완료내용을 입력해 주세요.");
alert("보완 요청 내용이 있습니다.\n보완 완료 내용을 입력해 주세요.");
return;
}
@@ -174,6 +184,11 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
async function onApply(data: FnrsCnsltnRegistFormData) {
if (isSubmitting) return;
if (data.privacyConsent !== "Y") {
alert("개인정보 수집/이용에 동의해야 신청이 가능합니다.");
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return;
@@ -260,7 +275,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
/>
<SplmntDmndFormSection readOnly={!isSplmntPrgrsStts} />
<SplmntDmndFormSection
initialData={initialData?.splmntDmndList}
readOnly={!isSplmntPrgrsStts}
/>
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
<div className="mx-4 flex flex-wrap justify-center gap-3 sm:gap-4">
@@ -300,7 +318,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
</>
) : (
<>
{fnrsCnsltnDocNo ? (
{hasCompletedDraft ? (
<Button
color="orange"
type="button"
@@ -324,7 +342,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
</div>
</SectionCollapsibleGroup>
</form>
{fnrsCnsltnDocNo ? (
{hasCompletedDraft && fnrsCnsltnDocNo ? (
<PwrkCnsltnDocMngDialog
open={isPwrkCnsltnDocMngOpen}
fnrsCnsltnDocNo={fnrsCnsltnDocNo}
@@ -14,7 +14,11 @@ import {
} from "@/widgets/pbadms-sprt";
import { Skeleton } from "@common/components";
import { getAiPendingMessage, isAiPendingStatus, isRagIndexingInProgress } from "../lib/ai-pending-utils";
import {
getAiPendingMessage,
isAiPendingStatus,
isRagIndexingInProgress,
} from "../lib/ai-pending-utils";
import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
@@ -11,11 +11,11 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import type { ReviewStepCode } from "../../model/view-types";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -37,8 +37,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -87,7 +87,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -68,7 +68,7 @@ export const instlSncnRegistSchema = z.object({
splmntDmndList: z.array(
instlSncnSplmntDmndItemSchema.extend({
splmntArtclCdNm: z.string().nullish(),
splmntDmndAtchFileNm: z.string().optional(),
splmntDmndAtchFileNm: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -82,7 +82,7 @@ export function formatTaskSeComCdForApi(taskSeComCd: string[]): string {
return taskSeComCd.join(",");
}
export function parseTaskSeComCdFromApi(taskSeComCd?: string): string[] {
export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] {
if (!taskSeComCd) return [];
return taskSeComCd.split(",").filter(Boolean);
}
@@ -1,4 +1,4 @@
import type { ReviewStepCode } from "@/entities/instl-sncn";
import type { CklstRvwRsltTreeDto, ReviewStepCode } from "@/entities/instl-sncn";
import {
INSTL_PROGRESS_ORDER,
@@ -27,6 +27,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -9,7 +9,10 @@ import type {
ReviewStepCode,
} from "@/entities/instl-sncn";
import { instlSncnApplySaveSchema } from "@/entities/instl-sncn";
import { type SplmntDmndDialogConfirmPayload, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import {
type SplmntDmndDialogConfirmPayload,
toEsntlAtchFileSaveList,
} from "@/widgets/pbadms-sprt";
import type { ChecklistFormState } from "../model/view-types";
@@ -114,7 +114,7 @@ function getBadgeColor(state: BadgeState) {
}
}
function formatOptional(value: string | undefined) {
function formatOptional(value: string | null | undefined) {
return value?.trim() ? value : "-";
}
@@ -48,6 +48,12 @@ export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -11,10 +11,10 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -36,8 +36,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -86,7 +86,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -1,14 +1,14 @@
import { type CostItemDef, roundAmount } from "@/widgets/pbadms-sprt";
import { type BizCstMatrixItem,getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
import { type BizCstMatrixItem, getAmt, PLAN_STEPS } from "./biz-cst-matrix-utils";
export interface FnrsCstItem {
planStpNo: number;
gvexpAmt?: number;
lcexpAmt?: number;
orgprBdchgAmt?: number;
etcAmt?: number;
sumAmt?: number;
gvexpAmt?: number | null;
lcexpAmt?: number | null;
orgprBdchgAmt?: number | null;
etcAmt?: number | null;
sumAmt?: number | null;
}
export interface FnrsCstStepRemaining {
@@ -32,10 +32,7 @@ function getBizCstStepTotal(
function sumFnrsCstLeaf(item: FnrsCstItem): number {
return roundAmount(
(item.gvexpAmt ?? 0) +
(item.lcexpAmt ?? 0) +
(item.orgprBdchgAmt ?? 0) +
(item.etcAmt ?? 0),
(item.gvexpAmt ?? 0) + (item.lcexpAmt ?? 0) + (item.orgprBdchgAmt ?? 0) + (item.etcAmt ?? 0),
);
}
@@ -1,3 +1,5 @@
import type { CklstRvwRsltTreeDto } from "@/entities/sewer-bsc-plan";
import {
PBADMS_PROGRESS_ORDER,
REVIEW_STEP_CODES,
@@ -28,6 +30,25 @@ export function getVisibleReviewSteps(
return REVIEW_STEP_CODES.slice(0, count);
}
export function resolveVisibleReviewSteps(
prgrsSttsComCd: string,
userRoleCode: string | undefined,
cklstResultList: CklstRvwRsltTreeDto[],
): ReviewStepCode[] {
const byProgressAndRole = getVisibleReviewSteps(prgrsSttsComCd, userRoleCode);
const currentStep = byProgressAndRole.at(-1);
const stepsWithData = new Set<ReviewStepCode>();
for (const row of cklstResultList) {
const step = row.rvwStpComCd as ReviewStepCode | undefined;
if (step && REVIEW_STEP_CODES.includes(step)) stepsWithData.add(step);
}
return REVIEW_STEP_CODES.filter(
(step) => stepsWithData.has(step) || step === currentStep,
);
}
export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
@@ -16,9 +16,9 @@ import {
export const sewerBscPlanRegistSchema = z.object({
sewerBscPlan: sewerBscPlanSaveSchema.extend({
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
sewerBscPlanSeCdNm: z.string().optional(),
taskSeCdNm: z.string().optional(),
sido: z.string().optional(),
sewerBscPlanSeCdNm: z.string().nullish(),
taskSeCdNm: z.string().nullish(),
sido: z.string().nullish(),
gugun: z.string().nullish(),
}),
sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({
@@ -61,7 +61,7 @@ export const sewerBscPlanRegistSchema = z.object({
sewerBscPlanTmplntItemSchema.extend({
swtrmtFcltCd: z.string(),
swtrmtFcltNm: z.string().nullish(),
atchFileNm: z.string().optional(),
atchFileNm: z.string().nullish(),
sewerBscPlanTmplntDtlList: z
.array(
sewerBscPlanTmplntDtlItemSchema.extend({
@@ -69,7 +69,7 @@ export const sewerBscPlanRegistSchema = z.object({
swtrmtFcltCd: z.string(),
}),
)
.optional(),
.nullish(),
}),
),
cklstResultList: z.array(
@@ -78,7 +78,7 @@ export const sewerBscPlanRegistSchema = z.object({
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullable().optional(),
picRvwCn: z.string().nullish(),
}),
),
esntlAtchFileList: z.array(
@@ -90,9 +90,9 @@ export const sewerBscPlanRegistSchema = z.object({
splmntDmndList: z.array(
sewerBscPlanSplmntDmndItemSchema.extend({
splmntArtclCdNm: z.string().nullish(),
splmntDmndAtchFileNm: z.string().optional(),
splmntAtchFileNm: z.string().optional(),
splmntAtchFileAtflId: z.string().optional(),
splmntDmndAtchFileNm: z.string().nullish(),
splmntAtchFileNm: z.string().nullish(),
splmntAtchFileAtflId: z.string().nullish(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
@@ -42,6 +42,12 @@ export type ChecklistStepMap = Record<ReviewStepCode, CklstRvwRsltTreeDto[]>;
export const REVIEW_STEP_CODES: ReviewStepCode[] = ["LOCAL", "ENV", "KECO"];
export const CHECKLIST_TAB_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "자가체크",
ENV: "기술검토",
KECO: "환경청검토",
};
export const REVIEW_STEP_LABELS: Record<ReviewStepCode, string> = {
LOCAL: "지자체",
ENV: "환경청",
@@ -31,7 +31,7 @@ interface PreviousPlanLookupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
initialSidoNm?: string;
initialSidoNm?: string | null;
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
initialGugunNm?: string | null;
}
@@ -11,11 +11,11 @@ import {
extractTaskCodes,
getChecklistDisplayRows,
} from "../../lib/checklist-utils";
import { getVisibleReviewSteps } from "../../lib/review-step-utils";
import { resolveVisibleReviewSteps } from "../../lib/review-step-utils";
import type { ReviewStepCode } from "../../model/view-types";
import {
CHECKLIST_TAB_LABELS,
type ChecklistFormState,
REVIEW_STEP_LABELS,
ROLE_TO_REVIEW_STEP,
TASK_CODE_LABELS,
} from "../../model/view-types";
@@ -37,8 +37,8 @@ function ChecklistSection({
setFormState,
}: ChecklistSectionProps) {
const visibleReviewSteps = useMemo(
() => getVisibleReviewSteps(prgrsSttsComCd, userRoleCode),
[prgrsSttsComCd, userRoleCode],
() => resolveVisibleReviewSteps(prgrsSttsComCd, userRoleCode, cklstResultList),
[prgrsSttsComCd, userRoleCode, cklstResultList],
);
const taskCodes = useMemo(() => extractTaskCodes(cklstResultList), [cklstResultList]);
@@ -87,7 +87,7 @@ function ChecklistSection({
<TabsList>
{visibleReviewSteps.map((stepCode) => (
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
{REVIEW_STEP_LABELS[stepCode]}
{CHECKLIST_TAB_LABELS[stepCode]}
</TabsTrigger>
))}
</TabsList>
@@ -214,8 +214,8 @@ function TmplntFacilityMatrix({
interface TmplntFacilityAttachmentsProps {
facilityIndex: number;
facilityName?: string;
atchFileGroupId?: string;
atchFileNm?: string;
atchFileGroupId?: string | null;
atchFileNm?: string | null;
}
function TmplntFacilityAttachments({
+8 -8
View File
@@ -205,14 +205,14 @@ export interface EsntlAtchFileSaveBase {
export interface SplmntDmndSaveBase {
splmntDmndSn?: number | null;
splmntArtclComCd?: string | null;
splmntDmndCn?: string;
splmntDmndDt?: string;
splmntDmndAtchFileGroupId?: string;
splmntCn?: string;
splmntPicNm?: string;
splmntCmptnDt?: string;
splmntAtchFileGroupId?: string;
cmptnYn?: string;
splmntDmndCn?: string | null;
splmntDmndDt?: string | null;
splmntDmndAtchFileGroupId?: string | null;
splmntCn?: string | null;
splmntPicNm?: string | null;
splmntCmptnDt?: string | null;
splmntAtchFileGroupId?: string | null;
cmptnYn?: string | null;
}
/** 검토의견 저장 공통 필드 (문서번호 제외) */
@@ -1,8 +1,8 @@
interface CanEditPbadmsDocParams {
docSeComCd: string;
prgrsSttsComCd: string | undefined;
rgtr: string | undefined;
loggedInUserUnqId: string | undefined;
prgrsSttsComCd?: string | null;
rgtr?: string | null;
loggedInUserUnqId?: string | null;
}
function canEditPbadmsDoc({
@@ -17,8 +17,8 @@ export interface FnrsSharListItem {
fnrsSharSeCd: string;
exstnAmt: number;
chgRqtAmt: number;
ajmtAmt?: number;
rmrkCn?: string;
ajmtAmt?: number | null;
rmrkCn?: string | null;
}
export type FnrsSharTreeNode = FnrsSharSeMngTreeDto | FnrsSharBizCstTreeDto;
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useFormContext } from "react-hook-form";
import { useQueryClient } from "@tanstack/react-query";
import { Download } from "lucide-react";
@@ -78,7 +78,7 @@ function AttachmentsFormSection({
initialEsntlFiles,
initialEtcFiles,
}: AttachmentsFormSectionProps) {
const { control, getValues, setValue } = useFormContext<AttachmentsFormData>();
const { getValues, setValue } = useFormContext<AttachmentsFormData>();
const queryClient = useQueryClient();
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedFileItem[]>>({});
const [etcFiles, setEtcFiles] = useState<EtcFileItem[]>([]);
@@ -373,89 +373,83 @@ function AttachmentsFormSection({
return (
<SectionCollapsible title="첨부파일">
<div className="space-y-6">
<Controller
name="esntlAtchFileList"
control={control}
render={() => (
<div className="px-2">
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="44%" />
<col width="12%" />
<col width="44%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]" align="left">
파일명
</TableHead>
<TableHead>파일 확장자</TableHead>
<TableHead>파일첨부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{essentialFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
조회된 필수 첨부파일 항목이 없습니다.
<div className="px-2">
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
<Table variant="secondary" className="table-fixed">
<colgroup>
<col width="44%" />
<col width="12%" />
<col width="44%" />
</colgroup>
<TableHeader>
<TableRow>
<TableHead className="w-[30rem]" align="left">
파일명
</TableHead>
<TableHead>파일 확장자</TableHead>
<TableHead>파일첨부</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{essentialFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="py-12 text-center text-muted-foreground">
조회된 필수 첨부파일 항목이 없습니다.
</TableCell>
</TableRow>
) : (
essentialFiles.map((file) => {
const atflCd = file.atchFileGroupId ?? "";
return (
<TableRow key={file.esntlAtchFileId}>
<TableHead
className="group break-keep font-medium"
data-required={file.esntlAtchYn === "Y"}
align="left"
>
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
{file.esntlAtchFileNm}
{atflCd ? (
<Button
size="sm"
color="white"
onClick={() => downloadTemplate(atflCd)}
>
<Download className="!size-5 shrink-0" />
양식내려받기
</Button>
) : null}
</div>
</TableHead>
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
<TableCell className="space-y-2">
<AttachmentFileField
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
fileKey: item.fileKey,
fileName: item.file?.name ?? item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={`${file.esntlAtchFileId}-file-input`}
isUploadingByKey={isUploading}
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
onAdd={(fileList) =>
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
}
onRemove={(index) =>
void removeEssentialFile(file.esntlAtchFileId, index)
}
/>
</TableCell>
</TableRow>
) : (
essentialFiles.map((file) => {
const atflCd = file.atchFileGroupId ?? "";
return (
<TableRow key={file.esntlAtchFileId}>
<TableHead
className="group break-keep font-medium"
data-required={file.esntlAtchYn === "Y"}
align="left"
>
<div className="inline-flex w-[calc(100%-1rem)] items-center justify-between gap-10">
{file.esntlAtchFileNm}
{atflCd ? (
<Button
size="sm"
color="white"
onClick={() => downloadTemplate(atflCd)}
>
<Download className="!size-5 shrink-0" />
양식내려받기
</Button>
) : null}
</div>
</TableHead>
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
<TableCell className="space-y-2">
<AttachmentFileField
files={(uploadedFiles[file.esntlAtchFileId] ?? []).map((item) => ({
fileKey: item.fileKey,
fileName: item.file?.name ?? item.fileName,
onDownload: item.atflId
? () => downloadByAtflId(item.atflId!)
: undefined,
}))}
inputId={`${file.esntlAtchFileId}-file-input`}
isUploadingByKey={isUploading}
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
onAdd={(fileList) =>
void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
}
onRemove={(index) =>
void removeEssentialFile(file.esntlAtchFileId, index)
}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
)}
/>
);
})
)}
</TableBody>
</Table>
</div>
<div className="px-2">
<div className="mb-4 flex items-center justify-between">
@@ -43,12 +43,13 @@ function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewS
) : (
<div className="space-y-2">
{files.map((file) => (
<AttachmentDownloadLink
key={file.atflId}
atflId={file.atflId}
fileName={file.orgnlFileNm || "다운로드"}
className="items-center"
/>
<div key={file.atflId}>
<AttachmentDownloadLink
atflId={file.atflId}
fileName={file.orgnlFileNm || "다운로드"}
className="items-center"
/>
</div>
))}
</div>
)}
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
@@ -31,6 +31,16 @@ interface BusinessCostFormData {
type CostFieldKey = "bscPlanAmt" | "bgtRfltAmt" | "tstmDesignAmt";
function isSameBizCstCodes(
current: BusinessCostFormData["bizCstList"],
next: BusinessCostFormData["bizCstList"],
) {
return (
current.length === next.length &&
current.every((item, index) => item.acntgSeCd === next[index]?.acntgSeCd)
);
}
function BusinessCostFormSection() {
const { control, setValue, getValues } = useFormContext<BusinessCostFormData>();
const bizCstList = useWatch({ control, name: "bizCstList" }) ?? [];
@@ -42,22 +52,22 @@ function BusinessCostFormSection() {
useEffect(() => {
if (costItems.length === 0) return;
const existingMap = new Map(getValues("bizCstList").map((item) => [item.acntgSeCd, item]));
const current = getValues("bizCstList") ?? [];
const existingMap = new Map(current.map((item) => [item.acntgSeCd, item]));
const next = costItems.map((item) => {
const existing = existingMap.get(item.code);
if (existing) return existing;
setValue(
"bizCstList",
costItems.map((item) => {
const existing = existingMap.get(item.code);
if (existing) return existing;
return {
acntgSeCd: item.code,
bscPlanAmt: 0,
bgtRfltAmt: 0,
tstmDesignAmt: 0,
};
});
return {
acntgSeCd: item.code,
bscPlanAmt: 0,
bgtRfltAmt: 0,
tstmDesignAmt: 0,
};
}),
);
if (isSameBizCstCodes(current, next)) return;
setValue("bizCstList", next);
}, [costItems, getValues, setValue]);
function handleCostChange(index: number, fieldKey: CostFieldKey, value: string) {
@@ -119,58 +129,37 @@ function BusinessCostFormSection() {
</div>
</TableHead>
<TableCell>
<Controller
name={`bizCstList.${idx}.bgtRfltAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bgtRfltAmt ?? ""}
onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bizCstList.${idx}.bscPlanAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bscPlanAmt ?? ""}
onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bizCstList.${idx}.tstmDesignAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.tstmDesignAmt ?? ""}
onChange={(e) => handleCostChange(idx, "tstmDesignAmt", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
@@ -17,7 +17,7 @@ import {
TableRow,
} from "@common/components";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
import { buildCnstpScaleItems, recalculateBzScaleTotals } from "../lib/business-scale-form-utils";
interface BusinessScaleFormData {
bzScaleList: Array<{
@@ -31,6 +31,16 @@ interface BusinessScaleFormData {
type ScaleFieldKey = "bscPl" | "budgetReflct" | "thtiDsgn";
function isSameBzScaleCodes(
current: BusinessScaleFormData["bzScaleList"],
next: BusinessScaleFormData["bzScaleList"],
) {
return (
current.length === next.length &&
current.every((item, index) => item.cnstpSeCd === next[index]?.cnstpSeCd)
);
}
function BusinessScaleFormSection() {
const { control, setValue, getValues } = useFormContext<BusinessScaleFormData>();
const bzScaleList = useWatch({ control, name: "bzScaleList" }) ?? [];
@@ -42,7 +52,8 @@ function BusinessScaleFormSection() {
useEffect(() => {
if (scaleItems.length === 0) return;
const existingMap = new Map(getValues("bzScaleList").map((item) => [item.cnstpSeCd, item]));
const current = getValues("bzScaleList") ?? [];
const existingMap = new Map(current.map((item) => [item.cnstpSeCd, item]));
const next = scaleItems.map((item) => {
const existing = existingMap.get(item.code);
@@ -57,6 +68,7 @@ function BusinessScaleFormSection() {
};
});
if (isSameBzScaleCodes(current, next)) return;
setValue("bzScaleList", recalculateBzScaleTotals(next, scaleItems));
}, [scaleItems, getValues, setValue]);
@@ -112,58 +124,37 @@ function BusinessScaleFormSection() {
</div>
</TableHead>
<TableCell>
<Controller
name={`bzScaleList.${idx}.bscPl`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.bscPl ?? ""}
onChange={(e) => handleScaleChange(idx, "bscPl", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bzScaleList.${idx}.budgetReflct`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.budgetReflct ?? ""}
onChange={(e) => handleScaleChange(idx, "budgetReflct", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`bzScaleList.${idx}.thtiDsgn`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value || ""}
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.thtiDsgn ?? ""}
onChange={(e) => handleScaleChange(idx, "thtiDsgn", e.target.value)}
readOnly={item.readonly}
className={`text-right ${item.readonly ? "bg-muted" : ""}`}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -1,18 +1,15 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { useQuery } from "@tanstack/react-query";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel } from "@/shared/ui/field";
import { cklstMngService, type CklstTreeNode } from "@/entities/cklst";
import { useCodeList } from "@/entities/code";
import {
Button,
RadioGroup,
RadioGroupItem,
Skeleton,
Table,
TableBody,
@@ -125,11 +122,21 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
}
}, [checklistTabs, activeTab]);
function handleSelectChange(resultIdx: number, value: "Y" | "N") {
const currentList = getValues("cklstResultList") as CklstResultListItem[];
const row = currentList[resultIdx];
if (!row) return;
const newList = [...currentList];
newList[resultIdx] = { ...row, selected: value };
setValue("cklstResultList", newList, { shouldDirty: true });
}
function handleSelectAll(tabCode: string, value: "Y" | "N") {
const tab = checklistTabs.find((t) => t.code === tabCode);
if (!tab) return;
const currentList = getValues("cklstResultList");
const currentList = getValues("cklstResultList") as CklstResultListItem[];
const newList = [...currentList];
tab.items
@@ -141,7 +148,7 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
}
});
setValue("cklstResultList", newList);
setValue("cklstResultList", newList, { shouldDirty: true });
}
const hasSelectedTaskCodes = Boolean(taskSeComCdArray?.length);
@@ -234,6 +241,8 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
{tab.items.map((item) => {
const resultIdx = cklstRvwSnIndexMap.get(item.cklstRvwSn) ?? -1;
const showRadios = item.bchckUseYn === "Y";
const selected = cklstResultList[resultIdx]?.selected;
const radioName = `check-${item.cklstRvwSn}`;
return (
<TableRow key={item.cklstRvwSn}>
@@ -248,66 +257,30 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
{item.content}
</TableCell>
{showRadios && resultIdx >= 0 ? (
<Controller
name={`cklstResultList.${resultIdx}.selected`}
control={control}
render={({ field, fieldState }) => (
<>
<TableCell className="text-center">
<RadioGroup
name={field.name}
value={field.value === "Y" ? "Y" : undefined}
onValueChange={field.onChange}
className="flex justify-center"
>
<FieldLabel
htmlFor={`check-${item.cklstRvwSn}-Y`}
className="!w-auto justify-center border-none"
>
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
className="w-auto"
>
<RadioGroupItem
value="Y"
id={`check-${item.cklstRvwSn}-Y`}
aria-invalid={fieldState.invalid}
aria-label="예"
/>
</Field>
</FieldLabel>
</RadioGroup>
</TableCell>
<TableCell className="text-center">
<RadioGroup
name={field.name}
value={field.value === "N" ? "N" : undefined}
onValueChange={field.onChange}
className="flex justify-center"
>
<FieldLabel
htmlFor={`check-${item.cklstRvwSn}-N`}
className="!w-auto justify-center border-none"
>
<Field
orientation="horizontal"
data-invalid={fieldState.invalid}
className="w-auto"
>
<RadioGroupItem
value="N"
id={`check-${item.cklstRvwSn}-N`}
aria-invalid={fieldState.invalid}
aria-label="아니오"
/>
</Field>
</FieldLabel>
</RadioGroup>
</TableCell>
</>
)}
/>
<>
<TableCell className="text-center">
<input
type="radio"
name={radioName}
value="Y"
checked={selected === "Y"}
onChange={() => handleSelectChange(resultIdx, "Y")}
aria-label="예"
className="cursor-pointer"
/>
</TableCell>
<TableCell className="text-center">
<input
type="radio"
name={radioName}
value="N"
checked={selected === "N"}
onChange={() => handleSelectChange(resultIdx, "N")}
aria-label="아니오"
className="cursor-pointer"
/>
</TableCell>
</>
) : null}
</TableRow>
);
@@ -1,18 +1,17 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { useAcntgSeMngTree } from "@/entities/acntg";
import { useDownloadAttachmentByAtflId } from "@/entities/attach";
import type { AtflMngClientResponseDto } from "@/entities/fnrs-cnsltn";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import { useFnrsSharSeMngTree } from "@/entities/fnrs-shar-se";
import {
Button,
cn,
Input,
Skeleton,
@@ -37,6 +36,7 @@ import { calcDiffPercent } from "../lib/format-utils";
import { BizCstTotalDisplayTable } from "./biz-cst-total-display-table";
const SHAR_FILE_KEY_PREFIX = "fnrs-shar::";
const SHAR_FILE_ACCEPT = ".hwp,.hwpx";
interface FnrsSharFormData {
fnrsSharList: FnrsSharListItem[];
@@ -53,6 +53,12 @@ interface FnrsSharFormData {
type SharFieldKey = "exstnAmt" | "chgRqtAmt" | "rmrkCn";
interface SharUploadedFile {
fileKey: string;
fileName: string;
atflId?: string;
}
interface FnrsSharFormSectionProps {
initialSharTree?: FnrsSharBizCstTreeDto[];
initialSharAtchFiles?: AtflMngClientResponseDto[];
@@ -62,12 +68,30 @@ function createSharUploadKey(file: File) {
return `${SHAR_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`;
}
function toInitialSharFile(file?: AtflMngClientResponseDto): SharUploadedFile | null {
if (!file?.orgnlFileNm) return null;
return {
fileKey: file.atflId ? `server::${file.atflId}` : `${SHAR_FILE_KEY_PREFIX}${file.atflCd}`,
fileName: file.orgnlFileNm,
atflId: file.atflId,
};
}
function isSameFnrsSharCodes(current: FnrsSharListItem[], next: FnrsSharListItem[]) {
return (
current.length === next.length &&
current.every((item, index) => item.fnrsSharSeCd === next[index]?.fnrsSharSeCd)
);
}
function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsSharFormSectionProps) {
const { control, setValue, getValues } = useFormContext<FnrsSharFormData>();
const fnrsSharList = useWatch({ control, name: "fnrsSharList" }) ?? [];
const [uploadedFileName, setUploadedFileName] = useState(
() => initialSharAtchFiles?.[0]?.orgnlFileNm ?? "",
const [uploadedFile, setUploadedFile] = useState<SharUploadedFile | null>(() =>
toInitialSharFile(initialSharAtchFiles?.[0]),
);
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { data: masterTree = [], isLoading: isMasterLoading } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree();
@@ -78,9 +102,13 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
const isLoading = initialSharTree?.length ? false : isMasterLoading;
const { uploadFileAsync, isUploading } = useTus({
onSuccess: (atflCd) => {
onSuccess: (atflCd, atflId, _file, fileKey) => {
if (!atflCd) return;
setValue("fnrsCnsltnMng.sharAtchFileGroupId", atflCd, { shouldDirty: true });
setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflId } : prev));
},
onError: (_error, _file, fileKey) => {
setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev));
},
});
const isSharUploading = Object.values(isUploading).some(Boolean);
@@ -96,6 +124,7 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
: mergeFnrsSharTreeWithForm(sharTree, []);
const withTotals = recalculateFnrsSharTotals(merged, sharItems);
if (isSameFnrsSharCodes(existing, withTotals)) return;
setValue("fnrsSharList", withTotals);
}, [sharItems, sharTree, initialSharTree, getValues, setValue]);
@@ -120,16 +149,32 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
applySharList(draft);
}
async function handleSharFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
async function handleSharFileAdd(files: FileList | null) {
const file = files?.[0];
if (!file) return;
const fileKey = createSharUploadKey(file);
setUploadedFileName(file.name);
await uploadFileAsync(file, { fileKey });
event.target.value = "";
setUploadedFile({ fileName: file.name, fileKey });
try {
await uploadFileAsync(file, { fileKey });
} catch {
// onError callback handles cleanup
}
}
const sharAtchFiles = uploadedFile
? [
{
fileKey: uploadedFile.fileKey,
fileName: uploadedFile.fileName,
onDownload: uploadedFile.atflId
? () => downloadByAtflId(uploadedFile.atflId!)
: undefined,
},
]
: [];
if (isLoading) {
return (
<SectionCollapsible title="재원분담">
@@ -189,40 +234,26 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
{item.label}
</TableHead>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.exstnAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.exstnAmt ?? ""}
onChange={(e) => handleSharChange(idx, "exstnAmt", e.target.value)}
readOnly={item.readonly}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
</TableCell>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.chgRqtAmt`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
type="number"
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
readOnly={item.readonly}
aria-invalid={fieldState.invalid}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
)}
/>
<Field>
<Input
type="number"
value={data.chgRqtAmt ?? ""}
onChange={(e) => handleSharChange(idx, "chgRqtAmt", e.target.value)}
readOnly={item.readonly}
className={cn("text-right", item.readonly && "bg-muted")}
/>
</Field>
</TableCell>
<TableCell>
<div className="text-right">
@@ -233,19 +264,12 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<div className="text-right">{calcDiffPercent(diff, data.exstnAmt || 0)}</div>
</TableCell>
<TableCell>
<Controller
name={`fnrsSharList.${idx}.rmrkCn`}
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
value={field.value ?? ""}
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
aria-invalid={fieldState.invalid}
/>
</Field>
)}
/>
<Field>
<Input
value={data.rmrkCn ?? ""}
onChange={(e) => handleSharChange(idx, "rmrkCn", e.target.value)}
/>
</Field>
</TableCell>
</TableRow>
);
@@ -257,31 +281,20 @@ function FnrsSharFormSection({ initialSharTree, initialSharAtchFiles }: FnrsShar
<div className="mt-6 px-2 pb-2">
<h3 className="mb-2 text-xl font-semibold">첨부파일</h3>
<div className="flex flex-wrap items-center gap-2">
{isSharUploading && uploadedFileName ? (
<TextShimmer className="max-w-lg text-xl" active>
{uploadedFileName}
</TextShimmer>
) : (
<Input
value={uploadedFileName}
readOnly
placeholder="총사업비 신청서(.hwp) 파일을 등록해주세요."
className="max-w-lg rounded-sm"
/>
)}
<Button type="button" color="white" disabled={isSharUploading} asChild>
<label className="cursor-pointer">
첨부파일
<input
type="file"
accept=".hwp,.hwpx"
className="hidden"
onChange={handleSharFileChange}
/>
</label>
</Button>
</div>
{!uploadedFile ? (
<p className="mb-2 px-1 text-xl text-muted-foreground">
총사업비 신청서(.hwp) 파일을 등록해주세요.
</p>
) : null}
<AttachmentFileField
files={sharAtchFiles}
inputId="fnrs-shar-atch-file-input"
isUploadingByKey={isUploading}
accept={SHAR_FILE_ACCEPT}
multiple={false}
addDisabled={isSharUploading}
onAdd={(fileList) => void handleSharFileAdd(fileList)}
/>
</div>
</SectionCollapsible>
);
@@ -5,7 +5,12 @@ import { Controller, useFormContext, useWatch } from "react-hook-form";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { AttachmentDownloadLink, attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import {
AttachmentDownloadLink,
attachService,
useDownloadAttachmentByAtflId,
} from "@/entities/attach";
import { FnrsCnsltnSplmntDmndResponseDto } from "@/entities/fnrs-cnsltn";
import {
Badge,
Table,
@@ -46,6 +51,7 @@ interface SplmntDmndFormData {
}
interface SplmntDmndFormSectionProps {
initialData?: FnrsCnsltnSplmntDmndResponseDto[];
readOnly?: boolean;
}
@@ -65,7 +71,7 @@ function getRowKey(item: SplmntDmndFormItem, index: number) {
return item.splmntDmndSn != null ? String(item.splmntDmndSn) : `idx-${index}`;
}
function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) {
function SplmntDmndFormSection({ initialData, readOnly = false }: SplmntDmndFormSectionProps) {
const formId = useId();
const { control, setValue, getValues } = useFormContext<SplmntDmndFormData>();
const splmntDmndList = useWatch({ control, name: "splmntDmndList" }) ?? [];
@@ -260,6 +266,7 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
const isRowUploading = rowFiles.some((file) => isUploading[file.fileKey] ?? false);
const fileInputId = `${formId}-${rowKey}-file`;
const isRowReadOnly = readOnly || item.cmptnYn === "Y";
const splmntAtchFileInfo = initialData?.[index]?.splmntAtchFileInfo;
return (
<TableRow key={rowKey}>
@@ -282,7 +289,15 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
</TableCell>
<TableCell className="align-top">
{isRowReadOnly ? (
<span className="whitespace-pre-wrap">{item.splmntCn || "-"}</span>
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntCn || "-"}</div>
{splmntAtchFileInfo?.atflCd ? (
<AttachmentDownloadLink
atflCd={splmntAtchFileInfo?.atflCd}
fileName={splmntAtchFileInfo?.orgnlFileNm}
/>
) : null}
</div>
) : (
<Controller
control={control}
@@ -292,7 +307,7 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
value={field.value ?? ""}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="보완완료 내용을 입력하세요."
placeholder="보완 완료 내용을 입력하세요."
className="min-h-[6rem] !text-xl"
/>
)}