[as] feat: 행정지원 메뉴(설치인가, 재원협의, 설계자문, 하수도기본계획) 수정

This commit is contained in:
2026-08-05 15:58:48 +09:00
parent e105f52fea
commit 1d8d9c7b3d
38 changed files with 803 additions and 679 deletions
@@ -51,7 +51,6 @@ export interface ElaprPrgrsSttsMyDocSearchParams extends ElaprPrgrsSttsSearchDto
export interface ElaprProcessRequestDto { export interface ElaprProcessRequestDto {
atrzPrgrsSttsComCd: string; atrzPrgrsSttsComCd: string;
rjctRsn?: string; rjctRsn?: string;
finalApprovalYn?: boolean;
} }
/** 백엔드 ElaprApproverChangeRequestDto */ /** 백엔드 ElaprApproverChangeRequestDto */
@@ -5,6 +5,8 @@ import { ENDPOINT } from "@/shared/config";
import type { import type {
SewerBscPlanApplySaveRequestDto, SewerBscPlanApplySaveRequestDto,
SewerBscPlanSaveRequestDto, SewerBscPlanSaveRequestDto,
} from "../model/sewer-bsc-plan-apply-payload-schema";
import type {
SewerBscPlanSearchRequestDto, SewerBscPlanSearchRequestDto,
SewerBscPlanUpdateRequestDto, SewerBscPlanUpdateRequestDto,
} from "../model/sewer-bsc-plan-interface"; } from "../model/sewer-bsc-plan-interface";
@@ -4,10 +4,12 @@ import type { PageResponse } from "@/shared/model";
import type { import type {
SewerBscPlanApplySaveRequestDto, SewerBscPlanApplySaveRequestDto,
SewerBscPlanSaveRequestDto,
} from "../model/sewer-bsc-plan-apply-payload-schema";
import type {
SewerBscPlanApplySaveResponseDto, SewerBscPlanApplySaveResponseDto,
SewerBscPlanDetResponseDto, SewerBscPlanDetResponseDto,
SewerBscPlanResponseDto, SewerBscPlanResponseDto,
SewerBscPlanSaveRequestDto,
SewerBscPlanSearchRequestDto, SewerBscPlanSearchRequestDto,
SewerBscPlanService, SewerBscPlanService,
SewerBscPlanUpdateRequestDto, SewerBscPlanUpdateRequestDto,
+30 -13
View File
@@ -13,35 +13,52 @@ export {
} from "./api/queries"; } from "./api/queries";
export { sewerBscPlanService } from "./api/sewer-bsc-plan-service"; export { sewerBscPlanService } from "./api/sewer-bsc-plan-service";
export type { export type {
AtflMngClientResponseDto,
CklstResultItem, CklstResultItem,
CklstRvwRsltTreeDto,
EsntlAtchFileItem, EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
SewerBscPlanApplySaveRequestDto, SewerBscPlanApplySaveRequestDto,
SewerBscPlanApplySaveResponseDto,
SewerBscPlanBizCstItem, SewerBscPlanBizCstItem,
SewerBscPlanDtlSaveRequestDto,
SewerBscPlanFnrsCstItem,
SewerBscPlanOtlnItem,
SewerBscPlanRvwOpnnItem,
SewerBscPlanSaveRequestDto,
SewerBscPlanSplmntDmndItem,
SewerBscPlanTmplntDtlItem,
SewerBscPlanTmplntItem,
SewerBscPlanTmplntTmprStrgItem,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export {
sewerBscPlanApplySaveSchema,
sewerBscPlanBizCstItemSchema,
sewerBscPlanCklstResultItemSchema,
sewerBscPlanDtlSaveSchema,
sewerBscPlanEsntlAtchFileItemSchema,
sewerBscPlanFnrsCstItemSchema,
sewerBscPlanOtlnItemSchema,
sewerBscPlanRvwOpnnItemSchema,
sewerBscPlanSaveSchema,
sewerBscPlanSplmntDmndItemSchema,
sewerBscPlanTmplntDtlItemSchema,
sewerBscPlanTmplntItemSchema,
sewerBscPlanTmplntTmprStrgItemSchema,
} from "./model/sewer-bsc-plan-apply-payload-schema";
export type {
AtflMngClientResponseDto,
CklstRvwRsltTreeDto,
EsntlAtchFileMngTreeResponseDto,
SewerBscPlanApplySaveResponseDto,
SewerBscPlanBizCstResponseDto, SewerBscPlanBizCstResponseDto,
SewerBscPlanDetResponseDto, SewerBscPlanDetResponseDto,
SewerBscPlanDtlResponseDto, SewerBscPlanDtlResponseDto,
SewerBscPlanDtlSaveRequestDto,
SewerBscPlanFnrsCstItem,
SewerBscPlanFnrsCstResponseDto, SewerBscPlanFnrsCstResponseDto,
SewerBscPlanOtlnItem,
SewerBscPlanOtlnResponseDto, SewerBscPlanOtlnResponseDto,
SewerBscPlanResponseDto, SewerBscPlanResponseDto,
SewerBscPlanRvwOpnnItem,
SewerBscPlanRvwOpnnResponseDto, SewerBscPlanRvwOpnnResponseDto,
SewerBscPlanSaveRequestDto,
SewerBscPlanSearchRequestDto, SewerBscPlanSearchRequestDto,
SewerBscPlanService, SewerBscPlanService,
SewerBscPlanSplmntDmndItem,
SewerBscPlanSplmntDmndResponseDto, SewerBscPlanSplmntDmndResponseDto,
SewerBscPlanTmplntDtlItem,
SewerBscPlanTmplntDtlResponseDto, SewerBscPlanTmplntDtlResponseDto,
SewerBscPlanTmplntItem,
SewerBscPlanTmplntResponseDto, SewerBscPlanTmplntResponseDto,
SewerBscPlanTmplntTmprStrgItem,
SewerBscPlanTmplntTmprStrgResponseDto, SewerBscPlanTmplntTmprStrgResponseDto,
SewerBscPlanUpdateRequestDto, SewerBscPlanUpdateRequestDto,
} from "./model/sewer-bsc-plan-interface"; } from "./model/sewer-bsc-plan-interface";
@@ -0,0 +1,173 @@
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(),
});
/** 백엔드 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(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
export const sewerBscPlanOtlnItemSchema = z.object({
planStpNo: z.number().optional(),
sewerPlanPpltnCnt: z.number().optional(),
sewerPrcsPpltnCnt: z.number().optional(),
sewerPsplyRt: z.number().optional(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
export const sewerBscPlanCklstResultItemSchema = z.object({
cklstRvwSn: z.number().optional(),
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
rvwStpComCd: z.string(),
selected: z.string().optional(),
picRvwCn: z.string().nullish(),
});
/** 백엔드 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(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
export const sewerBscPlanSplmntDmndItemSchema = z.object({
splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().optional(),
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(),
});
/** 백엔드 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(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
export const sewerBscPlanEsntlAtchFileItemSchema = z.object({
esntlAtchFileId: z.string().optional(),
atchFileGroupId: z.string().optional(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
export const sewerBscPlanBizCstItemSchema = z.object({
planStpNo: z.number().optional(),
acntgSeCd: z.string().optional(),
cstrnAmt: z.number().optional(),
});
/** 백엔드 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(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
export const sewerBscPlanTmplntItemSchema = z.object({
swtrmtFcltCd: z.string().optional(),
atchFileGroupId: z.string().optional(),
sewerBscPlanTmplntDtlList: z.array(sewerBscPlanTmplntDtlItemSchema).optional(),
});
/** 백엔드 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(),
});
/** 백엔드 SewerBscPlanApplySaveRequestDto */
export const sewerBscPlanApplySaveSchema = z.object({
techReViewSaveCd: z.string().optional(),
rvwStpCd: z.string().optional(),
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(),
});
export type SewerBscPlanSaveRequestDto = z.infer<typeof sewerBscPlanSaveSchema>;
export type SewerBscPlanDtlSaveRequestDto = z.infer<typeof sewerBscPlanDtlSaveSchema>;
export type SewerBscPlanOtlnItem = z.infer<typeof sewerBscPlanOtlnItemSchema>;
export type CklstResultItem = z.infer<typeof sewerBscPlanCklstResultItemSchema>;
export type SewerBscPlanFnrsCstItem = z.infer<typeof sewerBscPlanFnrsCstItemSchema>;
export type SewerBscPlanSplmntDmndItem = z.infer<typeof sewerBscPlanSplmntDmndItemSchema>;
export type SewerBscPlanRvwOpnnItem = z.infer<typeof sewerBscPlanRvwOpnnItemSchema>;
export type EsntlAtchFileItem = z.infer<typeof sewerBscPlanEsntlAtchFileItemSchema>;
export type SewerBscPlanBizCstItem = z.infer<typeof sewerBscPlanBizCstItemSchema>;
export type SewerBscPlanTmplntDtlItem = z.infer<typeof sewerBscPlanTmplntDtlItemSchema>;
export type SewerBscPlanTmplntItem = z.infer<typeof sewerBscPlanTmplntItemSchema>;
export type SewerBscPlanTmplntTmprStrgItem = z.infer<typeof sewerBscPlanTmplntTmprStrgItemSchema>;
export type SewerBscPlanApplySaveRequestDto = z.infer<typeof sewerBscPlanApplySaveSchema>;
@@ -1,35 +1,26 @@
import type { import type {
AtflMngClientResponseDto, AtflMngClientResponseDto,
CklstResultSaveBase,
CklstRvwRsltTreeDto, CklstRvwRsltTreeDto,
EsntlAtchFileMngTreeResponseDto, EsntlAtchFileMngTreeResponseDto,
EsntlAtchFileSaveBase,
PageableParams, PageableParams,
PageResponse, PageResponse,
PbadmsApplySaveHeader,
RvwOpnnResponseExtension, RvwOpnnResponseExtension,
RvwOpnnSaveBase, RvwOpnnSaveBase,
SplmntDmndResponseExtension, SplmntDmndResponseExtension,
SplmntDmndSaveBase, SplmntDmndSaveBase,
} from "@/shared/model"; } from "@/shared/model";
import type {
SewerBscPlanApplySaveRequestDto,
SewerBscPlanSaveRequestDto,
} from "./sewer-bsc-plan-apply-payload-schema";
export type { export type {
AtflMngClientResponseDto, AtflMngClientResponseDto,
CklstRvwRsltTreeDto, CklstRvwRsltTreeDto,
EsntlAtchFileMngTreeResponseDto, EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model"; } from "@/shared/model";
/** 백엔드 SewerBscPlanSaveRequestDto */
export interface SewerBscPlanSaveRequestDto {
sewerBscPlanDocNo?: string;
admdstCd?: string;
envoSeComCd?: string;
sewerBscPlanSeComCd?: string;
sewerBscPlanNm?: string;
taskSeComCd?: string;
prgrsSttsComCd?: string;
}
/** 백엔드 SewerBscPlanUpdateRequestDto */ /** 백엔드 SewerBscPlanUpdateRequestDto */
export interface SewerBscPlanUpdateRequestDto { export interface SewerBscPlanUpdateRequestDto {
admdstCd?: string; admdstCd?: string;
@@ -72,28 +63,6 @@ export interface SewerBscPlanResponseDto {
mdfcnDt?: string; mdfcnDt?: string;
} }
/** 백엔드 SewerBscPlanDtlSaveRequestDto */
export interface SewerBscPlanDtlSaveRequestDto {
sewerBscPlanDocNo?: string;
chgCycl?: number;
aplcntNm?: string;
aplcntMblTelno?: string;
aplcntOgdpNm?: string;
aplcntEmlAddr?: string;
dsgnrNm?: string;
dsgnrTelno?: string;
dsgnrOgdpNm?: string;
dsgnrEmlAddr?: string;
planYr?: string;
crtrYr?: string;
bizOtlnCn?: string;
frlvlGoalYr?: string;
sclvlGoalYr?: string;
thlvlGoalYr?: string;
folvlGoalYr?: string;
atchFileGroupId?: string;
}
/** 백엔드 SewerBscPlanDtlResponseDto */ /** 백엔드 SewerBscPlanDtlResponseDto */
export interface SewerBscPlanDtlResponseDto { export interface SewerBscPlanDtlResponseDto {
sewerBscPlanDocNo?: string; sewerBscPlanDocNo?: string;
@@ -210,103 +179,6 @@ export interface SewerBscPlanTmplntTmprStrgResponseDto {
exlnPumpCnt?: number; exlnPumpCnt?: number;
} }
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanOtlnItem */
export interface SewerBscPlanOtlnItem {
planStpNo?: number;
sewerPlanPpltnCnt?: number;
sewerPrcsPpltnCnt?: number;
sewerPsplyRt?: number;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.CklstResultItem */
export interface CklstResultItem extends CklstResultSaveBase {
selected?: string;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanFnrsCstItem */
export interface SewerBscPlanFnrsCstItem {
planStpNo?: number;
gvexpAmt?: number;
lcexpAmt?: number;
orgprBdchgAmt?: number;
etcAmt?: number;
sumAmt?: number;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */
export type SewerBscPlanSplmntDmndItem = SplmntDmndSaveBase;
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanRvwOpnnItem */
export type SewerBscPlanRvwOpnnItem = RvwOpnnSaveBase;
/** 백엔드 SewerBscPlanApplySaveRequestDto.EsntlAtchFileItem */
export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanBizCstItem */
export interface SewerBscPlanBizCstItem {
planStpNo?: number;
acntgSeCd?: string;
cstrnAmt?: number;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntDtlItem */
export interface SewerBscPlanTmplntDtlItem {
planStpNo?: number;
swtrmtFcltCd?: string;
prcsZoneArea?: number;
swtrmtPpltnCnt?: number;
planSwqty?: number;
fcltCpct?: number;
sprsysDrtwPplLen?: number;
sprsysExlnPplLen?: number;
cbsysPplLen?: number;
icptgPplLen?: number;
drgCnt?: number;
drtwRlyPumpCnt?: number;
drtwPumpCnt?: number;
exlnPumpCnt?: number;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntItem */
export interface SewerBscPlanTmplntItem {
swtrmtFcltCd?: string;
atchFileGroupId?: string;
sewerBscPlanTmplntDtlList?: SewerBscPlanTmplntDtlItem[];
}
/** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanTmplntTmprStrgItem */
export interface SewerBscPlanTmplntTmprStrgItem {
dtStrgDt?: string;
swtrmtFcltCd?: string;
prcsZoneArea?: number;
swtrmtPpltnCnt?: number;
planSwqty?: number;
fcltCpct?: number;
sprsysDrtwPplLen?: number;
sprsysExlnPplLen?: number;
cbsysPplLen?: number;
icptgPplLen?: number;
drgCnt?: number;
drtwRlyPumpCnt?: number;
drtwPumpCnt?: number;
exlnPumpCnt?: number;
}
/** 백엔드 SewerBscPlanApplySaveRequestDto */
export interface SewerBscPlanApplySaveRequestDto extends PbadmsApplySaveHeader {
sewerBscPlan: SewerBscPlanSaveRequestDto;
sewerBscPlanDtl: SewerBscPlanDtlSaveRequestDto;
sewerBscPlanOtlnList?: SewerBscPlanOtlnItem[];
cklstResultList?: CklstResultItem[];
fnrsCstList?: SewerBscPlanFnrsCstItem[];
splmntDmndList?: SewerBscPlanSplmntDmndItem[];
rvwOpnnList?: SewerBscPlanRvwOpnnItem[];
esntlAtchFileList?: EsntlAtchFileItem[];
bizCstList?: SewerBscPlanBizCstItem[];
sewerBscPlanTmplntList?: SewerBscPlanTmplntItem[];
tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgItem[];
}
/** 백엔드 SewerBscPlanApplySaveResponseDto */ /** 백엔드 SewerBscPlanApplySaveResponseDto */
export interface SewerBscPlanApplySaveResponseDto { export interface SewerBscPlanApplySaveResponseDto {
sewerBscPlanDocNo: string; sewerBscPlanDocNo: string;
@@ -15,10 +15,12 @@ import {
type DesignCnstnMngDetResponseDto, type DesignCnstnMngDetResponseDto,
designCnstnMngService, designCnstnMngService,
} from "@/entities/design-cnstn"; } from "@/entities/design-cnstn";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import { import {
AttachmentsFormSection, AttachmentsFormSection,
BusinessScaleFormSection, BusinessScaleFormSection,
ContactInfoFormSection, ContactInfoFormSection,
isFormEsntlAtchComplete,
PrivacyConsentSection, PrivacyConsentSection,
} from "@/widgets/pbadms-sprt"; } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components"; import { Button } from "@common/components";
@@ -53,6 +55,8 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const isEditMode = mode === "edit"; const isEditMode = mode === "edit";
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "CNFM" });
const form = useForm<DesignCnstnRegistFormData>({ const form = useForm<DesignCnstnRegistFormData>({
resolver: zodResolver(designCnstnRegistSchema), resolver: zodResolver(designCnstnRegistSchema),
mode: "onChange", mode: "onChange",
@@ -113,6 +117,11 @@ function DesignFormPage({ id, mode, initialData, createDefaults }: DesignFormPag
async function onApply(data: DesignCnstnRegistFormData) { async function onApply(data: DesignCnstnRegistFormData) {
if (isSubmitting) return; if (isSubmitting) return;
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
}
try { try {
setIsSubmitting(true); setIsSubmitting(true);
const designCnstnDocNo = await submitForm(data, "CT"); const designCnstnDocNo = await submitForm(data, "CT");
@@ -62,49 +62,51 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
</SectionCollapsible> </SectionCollapsible>
)} )}
<SectionCollapsible title="기타 첨부파일"> {etcFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="기타 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{etcFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground"> <TableHead>파일명</TableHead>
등록된 기타첨부파일이 없습니다. <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableCell>
</TableRow> </TableRow>
) : ( </TableHeader>
etcFileList.map((file) => ( <TableBody>
<TableRow key={file.atflId}> {etcFileList.length === 0 ? (
<TableCell className="text-left"> <TableRow>
{file.atflCd ? ( <TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
<a 등록된 기타첨부파일이 없습니다.
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ) : (
)} etcFileList.map((file) => (
</TableBody> <TableRow key={file.atflId}>
</Table> <TableCell className="text-left">
</div> {file.atflCd ? (
</SectionCollapsible> <a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
</> </>
); );
} }
@@ -2,6 +2,8 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config"; import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
@@ -71,12 +73,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
const TechReviewOpinionSection = forwardRef< const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle, TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps TechReviewOpinionSectionProps
>(function TechReviewOpinionSection({ >(function TechReviewOpinionSection(
rvwList, { rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
editableReviewStep, ref,
atrzSeComCdList = [], ) {
isAtrzLoading = false,
}, ref) {
const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02"); const canWriteOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({ const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA, ...EMPTY_FORM_DATA,
@@ -317,11 +317,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
<TableCell align="center"> <TableCell align="center">
{item.atchFileInfo?.atflCd ? ( {item.atchFileInfo?.atflCd ? (
<a <a
className="text-blue-600 hover:underline" className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`} href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"} {item.atchFileInfo.orgnlFileNm || "다운로드"}
</a> </a>
) : ( ) : (
@@ -85,7 +85,7 @@ export const fnrsCnsltnRegistSchema = z.object({
), ),
splmntDmndList: z.array( splmntDmndList: z.array(
z.object({ z.object({
splmntDmndSn: z.number().optional(), splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().optional(), splmntArtclComCd: z.string().optional(),
splmntArtclCdNm: z.string().nullish(), splmntArtclCdNm: z.string().nullish(),
splmntDmndCn: z.string().optional(), splmntDmndCn: z.string().optional(),
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { useAcntgSeMngTree } from "@/entities/acntg"; import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import { import {
type FnrsCnsltnApplySaveRequestDto, type FnrsCnsltnApplySaveRequestDto,
fnrsCnsltnMngBaseKey, fnrsCnsltnMngBaseKey,
@@ -27,6 +28,7 @@ import {
ContactInfoFormSection, ContactInfoFormSection,
FnrsSharFormSection, FnrsSharFormSection,
isFormChecklistComplete, isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection, PrivacyConsentSection,
recalculateFnrsSharTotals, recalculateFnrsSharTotals,
SplmntDmndFormSection, SplmntDmndFormSection,
@@ -67,6 +69,7 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
const { data: masterSharTree = [] } = useFnrsSharSeMngTree(); const { data: masterSharTree = [] } = useFnrsSharSeMngTree();
const { data: acntgTree = [] } = useAcntgSeMngTree(); const { data: acntgTree = [] } = useAcntgSeMngTree();
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "FNRS" });
const sharTree = const sharTree =
isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree; isEditMode && initialData?.fnrsSharList?.length ? initialData.fnrsSharList : masterSharTree;
const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]); const sharItems = useMemo(() => buildFnrsSharItems(sharTree), [sharTree]);
@@ -210,6 +213,11 @@ function FnrsFormPage({ id, mode, initialData, createDefaults }: FnrsFormPagePro
return; return;
} }
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
}
try { try {
setIsSubmitting(true); setIsSubmitting(true);
const fnrsCnsltnDocNo = await submitForm(data, "ST"); const fnrsCnsltnDocNo = await submitForm(data, "ST");
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
return ( return (
<> <>
<SectionCollapsible title="필수 첨부파일"> {esntlFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="필수 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>필수 자료명</TableHead>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{esntlFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground"> <TableHead>필수 자료명</TableHead>
조회된 필수 첨부파일 항목이 없습니다. <TableHead>파일명</TableHead>
</TableCell> <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow> </TableRow>
) : ( </TableHeader>
esntlFileList.map((file) => ( <TableBody>
<TableRow key={file.esntlAtchFileId}> {esntlFileList.length === 0 ? (
<TableCell className="pl-2 text-left"> <TableRow>
{file.esntlAtchFileNm} <TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
{file.esntlAtchYn === "Y" ? ( 조회된 필수 첨부파일 항목이 없습니다.
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell> </TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
</TableRow> </TableRow>
)) ) : (
)} esntlFileList.map((file) => (
</TableBody> <TableRow key={file.esntlAtchFileId}>
</Table> <TableCell className="pl-2 text-left">
</div> {file.esntlAtchFileNm}
</SectionCollapsible> {file.esntlAtchYn === "Y" ? (
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
<SectionCollapsible title="기타 첨부파일"> {etcFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="기타 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{etcFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground"> <TableHead>파일명</TableHead>
등록된 기타첨부파일이 없습니다. <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableCell>
</TableRow> </TableRow>
) : ( </TableHeader>
etcFileList.map((file) => ( <TableBody>
<TableRow key={file.atflId}> {etcFileList.length === 0 ? (
<TableCell className="text-left"> <TableRow>
{file.atflCd ? ( <TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
<a 등록된 기타첨부파일이 없습니다.
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ) : (
)} etcFileList.map((file) => (
</TableBody> <TableRow key={file.atflId}>
</Table> <TableCell className="text-left">
</div> {file.atflCd ? (
</SectionCollapsible> <a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
</> </>
); );
} }
@@ -71,6 +71,10 @@ function ChecklistSection({
const { total, completed, percent } = progress; const { total, completed, percent } = progress;
if (cklstResultList.length === 0) {
return null;
}
return ( return (
<SectionCollapsible title="체크리스트"> <SectionCollapsible title="체크리스트">
<div className="flex flex-col gap-4 p-2"> <div className="flex flex-col gap-4 p-2">
@@ -2,6 +2,8 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config"; import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
@@ -71,12 +73,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
const TechReviewOpinionSection = forwardRef< const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle, TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps TechReviewOpinionSectionProps
>(function TechReviewOpinionSection({ >(function TechReviewOpinionSection(
rvwList, { rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
editableReviewStep, ref,
atrzSeComCdList = [], ) {
isAtrzLoading = false,
}, ref) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02"); const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({ const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA, ...EMPTY_FORM_DATA,
@@ -336,11 +336,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
<TableCell align="center"> <TableCell align="center">
{item.atchFileInfo?.atflCd ? ( {item.atchFileInfo?.atflCd ? (
<a <a
className="text-blue-600 hover:underline" className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`} href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"} {item.atchFileInfo.orgnlFileNm || "다운로드"}
</a> </a>
) : ( ) : (
@@ -77,7 +77,7 @@ export const instlSncnRegistSchema = z.object({
), ),
splmntDmndList: z.array( splmntDmndList: z.array(
z.object({ z.object({
splmntDmndSn: z.number().optional(), splmntDmndSn: z.number().nullish(),
splmntArtclComCd: z.string().optional(), splmntArtclComCd: z.string().optional(),
splmntArtclCdNm: z.string().nullish(), splmntArtclCdNm: z.string().nullish(),
splmntDmndCn: z.string().optional(), splmntDmndCn: z.string().optional(),
@@ -9,6 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import { import {
type InstlSncnApplySaveRequestDto, type InstlSncnApplySaveRequestDto,
instlSncnKeys, instlSncnKeys,
@@ -22,6 +23,7 @@ import {
ChecklistFormSection, ChecklistFormSection,
ContactInfoFormSection, ContactInfoFormSection,
isFormChecklistComplete, isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection, PrivacyConsentSection,
SplmntDmndFormSection, SplmntDmndFormSection,
} from "@/widgets/pbadms-sprt"; } from "@/widgets/pbadms-sprt";
@@ -57,6 +59,8 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
const isEditMode = mode === "edit"; const isEditMode = mode === "edit";
const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL"; const isSplmntPrgrsStts = initialData?.detDto.prgrsSttsComCd === "SL";
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "INSTL" });
const form = useForm<InstlSncnRegistFormData>({ const form = useForm<InstlSncnRegistFormData>({
resolver: zodResolver(instlSncnRegistSchema), resolver: zodResolver(instlSncnRegistSchema),
mode: "onChange", mode: "onChange",
@@ -172,6 +176,11 @@ function InstlFormPage({ id, mode, initialData, createDefaults }: InstlFormPageP
return; return;
} }
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
}
try { try {
setIsSubmitting(true); setIsSubmitting(true);
const instlSncnDocNo = await submitForm(data, "ST"); const instlSncnDocNo = await submitForm(data, "ST");
@@ -19,98 +19,104 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
return ( return (
<> <>
<SectionCollapsible title="필수 첨부파일"> {esntlFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="필수 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>필수 자료명</TableHead>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{esntlFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground"> <TableHead>필수 자료명</TableHead>
조회된 필수 첨부파일 항목이 없습니다. <TableHead>파일명</TableHead>
</TableCell> <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow> </TableRow>
) : ( </TableHeader>
esntlFileList.map((file) => ( <TableBody>
<TableRow key={file.esntlAtchFileId}> {esntlFileList.length === 0 ? (
<TableCell className="pl-2 text-left"> <TableRow>
{file.esntlAtchFileNm} <TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
{file.esntlAtchYn === "Y" ? ( 조회된 필수 첨부파일 항목이 없습니다.
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell> </TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
</TableRow> </TableRow>
)) ) : (
)} esntlFileList.map((file) => (
</TableBody> <TableRow key={file.esntlAtchFileId}>
</Table> <TableCell className="pl-2 text-left">
</div> {file.esntlAtchFileNm}
</SectionCollapsible> {file.esntlAtchYn === "Y" ? (
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
<SectionCollapsible title="기타 첨부파일"> {etcFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="기타 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{etcFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground"> <TableHead>파일명</TableHead>
등록된 기타첨부파일이 없습니다. <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableCell>
</TableRow> </TableRow>
) : ( </TableHeader>
etcFileList.map((file) => ( <TableBody>
<TableRow key={file.atflId}> {etcFileList.length === 0 ? (
<TableCell className="text-left"> <TableRow>
{file.atflCd ? ( <TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
<a 등록된 기타첨부파일이 없습니다.
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ) : (
)} etcFileList.map((file) => (
</TableBody> <TableRow key={file.atflId}>
</Table> <TableCell className="text-left">
</div> {file.atflCd ? (
</SectionCollapsible> <a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
</> </>
); );
} }
@@ -70,6 +70,10 @@ function ChecklistSection({
const { total, completed, percent } = progress; const { total, completed, percent } = progress;
if (cklstResultList.length === 0) {
return null;
}
return ( return (
<SectionCollapsible title="체크리스트"> <SectionCollapsible title="체크리스트">
<div className="flex flex-col gap-4 p-2"> <div className="flex flex-col gap-4 p-2">
@@ -2,6 +2,8 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config"; import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
@@ -67,12 +69,10 @@ function hasFormContent(formData: ReviewOpinionFormData) {
const TechReviewOpinionSection = forwardRef< const TechReviewOpinionSection = forwardRef<
TechReviewOpinionSectionHandle, TechReviewOpinionSectionHandle,
TechReviewOpinionSectionProps TechReviewOpinionSectionProps
>(function TechReviewOpinionSection({ >(function TechReviewOpinionSection(
rvwList, { rvwList, editableReviewStep, atrzSeComCdList = [], isAtrzLoading = false },
editableReviewStep, ref,
atrzSeComCdList = [], ) {
isAtrzLoading = false,
}, ref) {
const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02"); const canWriteKecoOpinion = !isAtrzLoading && atrzSeComCdList.includes("ATRZ02");
const [formData, setFormData] = useState<ReviewOpinionFormData>({ const [formData, setFormData] = useState<ReviewOpinionFormData>({
...EMPTY_FORM_DATA, ...EMPTY_FORM_DATA,
@@ -329,11 +329,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
<TableCell align="center"> <TableCell align="center">
{item.atchFileInfo?.atflCd ? ( {item.atchFileInfo?.atflCd ? (
<a <a
className="text-blue-600 hover:underline" className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`} href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"} {item.atchFileInfo.orgnlFileNm || "다운로드"}
</a> </a>
) : ( ) : (
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { IUserInfo } from "@/shared/store"; import type { IUserInfo } from "@/shared/store";
import type { import type {
SewerBscPlanApplySaveRequestDto, SewerBscPlanApplySaveRequestDto,
SewerBscPlanDetResponseDto, SewerBscPlanDetResponseDto,
} from "@/entities/sewer-bsc-plan"; } from "@/entities/sewer-bsc-plan";
import { sewerBscPlanApplySaveSchema } from "@/entities/sewer-bsc-plan";
import type { BizCstMatrixItem } from "./biz-cst-matrix-utils"; import type { BizCstMatrixItem } from "./biz-cst-matrix-utils";
import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils"; import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils";
@@ -245,47 +245,33 @@ export function buildApplyPayload(
prgrsSttsComCd: "SV" | "LE" | "TE", prgrsSttsComCd: "SV" | "LE" | "TE",
options?: { isEditMode?: boolean; docNo?: string }, options?: { isEditMode?: boolean; docNo?: string },
): SewerBscPlanApplySaveRequestDto { ): SewerBscPlanApplySaveRequestDto {
const { taskSeComCd, ...sewerBscPlanRest } = data.sewerBscPlan; const docNo = options?.isEditMode ? options.docNo : undefined;
return { return sewerBscPlanApplySaveSchema.parse({
sewerBscPlan: { sewerBscPlan: {
...sewerBscPlanRest, ...data.sewerBscPlan,
taskSeComCd: taskSeComCd ?? "", sewerBscPlanDocNo: docNo,
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined, taskSeComCd: data.sewerBscPlan.taskSeComCd ?? "",
prgrsSttsComCd, prgrsSttsComCd,
}, },
sewerBscPlanDtl: { sewerBscPlanDtl: {
...data.sewerBscPlanDtl, ...data.sewerBscPlanDtl,
sewerBscPlanDocNo: options?.isEditMode ? options.docNo : undefined, sewerBscPlanDocNo: docNo,
}, },
sewerBscPlanOtlnList: data.sewerBscPlanOtlnList, sewerBscPlanOtlnList: data.sewerBscPlanOtlnList,
bizCstList: data.bizCstList, bizCstList: data.bizCstList,
fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)), fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)),
sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map( sewerBscPlanTmplntList: data.sewerBscPlanTmplntList,
({ atchFileNm, swtrmtFcltNm, ...item }) => ({
swtrmtFcltCd: item.swtrmtFcltCd,
atchFileGroupId: item.atchFileGroupId,
sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList,
}),
),
cklstResultList: data.cklstResultList.map((item) => ({ cklstResultList: data.cklstResultList.map((item) => ({
...item, ...item,
selected: item.selected ?? undefined, selected: item.selected ?? undefined,
})), })),
esntlAtchFileList: data.esntlAtchFileList, esntlAtchFileList: data.esntlAtchFileList,
splmntDmndList: data.splmntDmndList.map( splmntDmndList: data.splmntDmndList.map((item) => ({
({ ...item,
splmntArtclCdNm, cmptnYn: prgrsSttsComCd === "TE" ? "Y" : item.cmptnYn,
splmntAtchFileNm, })),
splmntAtchFileAtflId, });
splmntDmndAtchFileNm,
...item
}) => ({
...item,
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
}),
),
};
} }
export { createEmptyTmplntDtlList }; export { createEmptyTmplntDtlList };
@@ -146,10 +146,8 @@ export function buildSupplementReqPayload(
toSplmntDmndSaveItem(item), toSplmntDmndSaveItem(item),
); );
const nextSn = existingItems.reduce((max, item) => Math.max(max, item.splmntDmndSn ?? 0), 0) + 1; const newItems: SewerBscPlanSplmntDmndItem[] = payload.items.map((item) => ({
splmntDmndSn: null,
const newItems: SewerBscPlanSplmntDmndItem[] = payload.items.map((item, index) => ({
splmntDmndSn: nextSn + index,
splmntArtclComCd: item.splmntArtclComCd, splmntArtclComCd: item.splmntArtclComCd,
splmntDmndCn: item.splmntDmndCn, splmntDmndCn: item.splmntDmndCn,
splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId, splmntDmndAtchFileGroupId: item.splmntDmndAtchFileGroupId,
@@ -1,38 +1,27 @@
import { z } from "zod"; import { z } from "zod";
const tmplntDtlSchema = z.object({ import {
planStpNo: z.number(), sewerBscPlanBizCstItemSchema,
swtrmtFcltCd: z.string(), sewerBscPlanCklstResultItemSchema,
prcsZoneArea: z.number().optional(), sewerBscPlanDtlSaveSchema,
swtrmtPpltnCnt: z.number().optional(), sewerBscPlanEsntlAtchFileItemSchema,
planSwqty: z.number().optional(), sewerBscPlanFnrsCstItemSchema,
fcltCpct: z.number().optional(), sewerBscPlanOtlnItemSchema,
sprsysDrtwPplLen: z.number().optional(), sewerBscPlanSaveSchema,
sprsysExlnPplLen: z.number().optional(), sewerBscPlanSplmntDmndItemSchema,
cbsysPplLen: z.number().optional(), sewerBscPlanTmplntDtlItemSchema,
icptgPplLen: z.number().optional(), sewerBscPlanTmplntItemSchema,
drgCnt: z.number().optional(), } from "@/entities/sewer-bsc-plan";
drtwRlyPumpCnt: z.number().optional(),
drtwPumpCnt: z.number().optional(),
exlnPumpCnt: z.number().optional(),
});
export const sewerBscPlanRegistSchema = z.object({ export const sewerBscPlanRegistSchema = z.object({
sewerBscPlan: z.object({ sewerBscPlan: sewerBscPlanSaveSchema.extend({
sewerBscPlanDocNo: z.string().optional(),
sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"), sewerBscPlanNm: z.string().min(1, "기본계획명은 필수입니다"),
sewerBscPlanSeComCd: z.string().optional(),
sewerBscPlanSeCdNm: z.string().optional(), sewerBscPlanSeCdNm: z.string().optional(),
taskSeComCd: z.string().optional(),
taskSeCdNm: z.string().optional(), taskSeCdNm: z.string().optional(),
admdstCd: z.string().optional(),
envoSeComCd: z.string().optional(),
prgrsSttsComCd: z.string().optional(),
sido: z.string().optional(), sido: z.string().optional(),
gugun: z.string().optional(), gugun: z.string().nullish(),
}), }),
sewerBscPlanDtl: z.object({ sewerBscPlanDtl: sewerBscPlanDtlSaveSchema.extend({
chgCycl: z.number().optional(),
planYr: z.string().min(1, "계획년도는 필수입니다"), planYr: z.string().min(1, "계획년도는 필수입니다"),
crtrYr: z.string().min(1, "기준년도는 필수입니다"), crtrYr: z.string().min(1, "기준년도는 필수입니다"),
frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"), frlvlGoalYr: z.string().min(1, "1단계 목표연도는 필수입니다"),
@@ -40,10 +29,6 @@ export const sewerBscPlanRegistSchema = z.object({
thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"), thlvlGoalYr: z.string().min(1, "3단계 목표연도는 필수입니다"),
folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"), folvlGoalYr: z.string().min(1, "4단계 목표연도는 필수입니다"),
bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"), bizOtlnCn: z.string().min(1, "계획개요는 필수입니다"),
aplcntNm: z.string().optional(),
aplcntMblTelno: z.string().optional(),
aplcntOgdpNm: z.string().optional(),
aplcntEmlAddr: z.string().optional(),
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"), dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
dsgnrTelno: z dsgnrTelno: z
.string() .string()
@@ -54,74 +39,60 @@ export const sewerBscPlanRegistSchema = z.object({
.string() .string()
.min(1, "설계자 이메일은 필수입니다") .min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"), .email("올바른 이메일 형식이 아닙니다"),
atchFileGroupId: z.string().optional(),
}), }),
sewerBscPlanOtlnList: z.array( sewerBscPlanOtlnList: z.array(
z.object({ sewerBscPlanOtlnItemSchema.extend({
planStpNo: z.number(), planStpNo: z.number(),
sewerPlanPpltnCnt: z.number().optional(),
sewerPrcsPpltnCnt: z.number().optional(),
sewerPsplyRt: z.number().optional(),
}), }),
), ),
bizCstList: z.array( bizCstList: z.array(
z.object({ sewerBscPlanBizCstItemSchema.extend({
planStpNo: z.number(), planStpNo: z.number(),
acntgSeCd: z.string(), acntgSeCd: z.string(),
cstrnAmt: z.number(), cstrnAmt: z.number(),
}), }),
), ),
fnrsCstList: z.array( fnrsCstList: z.array(
z.object({ sewerBscPlanFnrsCstItemSchema.extend({
planStpNo: z.number(), planStpNo: z.number(),
gvexpAmt: z.number().optional(),
lcexpAmt: z.number().optional(),
orgprBdchgAmt: z.number().optional(),
etcAmt: z.number().optional(),
sumAmt: z.number().optional(),
}), }),
), ),
sewerBscPlanTmplntList: z.array( sewerBscPlanTmplntList: z.array(
z.object({ sewerBscPlanTmplntItemSchema.extend({
swtrmtFcltCd: z.string(), swtrmtFcltCd: z.string(),
swtrmtFcltNm: z.string().nullish(), swtrmtFcltNm: z.string().nullish(),
atchFileGroupId: z.string().optional(),
atchFileNm: z.string().optional(), atchFileNm: z.string().optional(),
sewerBscPlanTmplntDtlList: z.array(tmplntDtlSchema).optional(), sewerBscPlanTmplntDtlList: z
.array(
sewerBscPlanTmplntDtlItemSchema.extend({
planStpNo: z.number(),
swtrmtFcltCd: z.string(),
}),
)
.optional(),
}), }),
), ),
cklstResultList: z.array( cklstResultList: z.array(
z.object({ sewerBscPlanCklstResultItemSchema.extend({
cklstSn: z.number(), cklstSn: z.number(),
cklstArtclSn: z.number(), cklstArtclSn: z.number(),
cklstRvwSn: z.number(), cklstRvwSn: z.number(),
rvwStpComCd: z.string(),
selected: z.enum(["Y", "N"]).nullable(), selected: z.enum(["Y", "N"]).nullable(),
picRvwCn: z.string().nullable().optional(), picRvwCn: z.string().nullable().optional(),
}), }),
), ),
esntlAtchFileList: z.array( esntlAtchFileList: z.array(
z.object({ sewerBscPlanEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(), esntlAtchFileId: z.string(),
atchFileGroupId: z.string(), atchFileGroupId: z.string(),
}), }),
), ),
splmntDmndList: z.array( splmntDmndList: z.array(
z.object({ sewerBscPlanSplmntDmndItemSchema.extend({
splmntDmndSn: z.number().optional(),
splmntArtclComCd: z.string().optional(),
splmntArtclCdNm: z.string().nullish(), splmntArtclCdNm: z.string().nullish(),
splmntDmndCn: z.string().optional(),
splmntDmndDt: z.string().optional(),
splmntDmndAtchFileGroupId: z.string().optional(),
splmntDmndAtchFileNm: z.string().optional(), splmntDmndAtchFileNm: z.string().optional(),
splmntCn: z.string().optional(),
splmntPicNm: z.string().optional(),
splmntCmptnDt: z.string().optional(),
splmntAtchFileGroupId: z.string().optional(),
splmntAtchFileNm: z.string().optional(), splmntAtchFileNm: z.string().optional(),
splmntAtchFileAtflId: z.string().optional(), splmntAtchFileAtflId: z.string().optional(),
cmptnYn: z.string().optional(),
}), }),
), ),
privacyConsent: z.enum(["Y", "N"], { privacyConsent: z.enum(["Y", "N"], {
@@ -130,6 +101,3 @@ export const sewerBscPlanRegistSchema = z.object({
}); });
export type SewerBscPlanRegistFormData = z.infer<typeof sewerBscPlanRegistSchema>; export type SewerBscPlanRegistFormData = z.infer<typeof sewerBscPlanRegistSchema>;
/** 체크리스트·필수첨부 docSeComCd — CMS 합의 전 임시값 */
export const SEWER_BSC_PLAN_DOC_SE_COM_CD = "SEWER";
@@ -46,8 +46,9 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
return [ return [
{ label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" }, { label: MAIN_STEP_LABELS.envoReview, state: "action" },
{ label: MAIN_STEP_LABELS.reply, state: "action" }, { label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
]; ];
} }
@@ -56,8 +57,8 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
{ label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" }, { label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" }, { label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "pending" },
{ label: MAIN_STEP_LABELS.supplement, state: "action" }, { label: MAIN_STEP_LABELS.supplement, state: "action" },
{ label: MAIN_STEP_LABELS.reply, state: "pending" },
]; ];
} }
@@ -71,16 +72,6 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
]; ];
} }
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: replyLabel, state: "action" },
];
}
if (prgrsSttsComCd === "TA") { if (prgrsSttsComCd === "TA") {
return [ return [
{ label: MAIN_STEP_LABELS.save, state: "completed" }, { label: MAIN_STEP_LABELS.save, state: "completed" },
@@ -91,6 +82,16 @@ function buildProgressBadges(prgrsSttsComCd: string, prgrsSttsCdNm: string): Pro
]; ];
} }
if (prgrsSttsComCd === "EP") {
return [
{ label: MAIN_STEP_LABELS.save, state: "completed" },
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
{ label: MAIN_STEP_LABELS.techReview, state: "completed" },
{ label: replyLabel, state: "completed" },
];
}
return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }]; return [{ label: prgrsSttsCdNm || prgrsSttsComCd || "-", state: "current" }];
} }
@@ -41,7 +41,7 @@ import {
buildSupplementReqPayload, buildSupplementReqPayload,
buildTechReviewReplyPayload, buildTechReviewReplyPayload,
toSplmntDmndSaveItem, toSplmntDmndSaveItem,
} from "../../lib/save-payload-utils"; } from "../../lib/payload-utils";
import { type ChecklistFormState } from "../../model/view-types"; import { type ChecklistFormState } from "../../model/view-types";
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
@@ -1,5 +1,7 @@
"use client"; "use client";
import { Paperclip } from "lucide-react";
import { formatFileSize } from "@/shared/lib/file-utils"; import { formatFileSize } from "@/shared/lib/file-utils";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach"; import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type { import type {
@@ -19,98 +21,105 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
return ( return (
<> <>
<SectionCollapsible title="필수 첨부파일"> {esntlFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="필수 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>필수 자료명</TableHead>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{esntlFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={3} className="py-12 text-center text-muted-foreground"> <TableHead>필수 자료명</TableHead>
조회된 필수 첨부파일 항목이 없습니다. <TableHead>파일명</TableHead>
</TableCell> <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow> </TableRow>
) : ( </TableHeader>
esntlFileList.map((file) => ( <TableBody>
<TableRow key={file.esntlAtchFileId}> {esntlFileList.length === 0 ? (
<TableCell className="pl-2 text-left"> <TableRow>
{file.esntlAtchFileNm} <TableCell colSpan={3} className="py-12 text-center text-muted-foreground">
{file.esntlAtchYn === "Y" ? ( 조회된 필수 첨부파일 항목이 없습니다.
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell> </TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">{formatFileSize(file.atflFlieSz)}</TableCell>
</TableRow> </TableRow>
)) ) : (
)} esntlFileList.map((file) => (
</TableBody> <TableRow key={file.esntlAtchFileId}>
</Table> <TableCell className="pl-2 text-left">
</div> {file.esntlAtchFileNm}
</SectionCollapsible> {file.esntlAtchYn === "Y" ? (
<span className="ml-1 text-red-500">*</span>
) : null}
</TableCell>
<TableCell className="text-left">
{file.atflCd ? (
<a
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
<Paperclip className="size-5 shrink-0" />
{file.orgnlFileNm || "다운로드"}
</a>
) : (
<span className="text-gray-500">첨부없음</span>
)}
</TableCell>
<TableCell className="text-right">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
<SectionCollapsible title="기타 첨부파일"> {etcFileList.length > 0 && (
<div className="overflow-x-auto"> <SectionCollapsible title="기타 첨부파일">
<Table variant="secondary"> <div className="overflow-x-auto">
<TableHeader> <Table variant="secondary">
<TableRow> <TableHeader>
<TableHead>파일명</TableHead>
<TableHead className="w-[8rem] text-right">크기</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{etcFileList.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={2} className="py-12 text-center text-muted-foreground"> <TableHead>파일명</TableHead>
등록된 기타첨부파일이 없습니다. <TableHead className="w-[8rem] text-right">크기</TableHead>
</TableCell>
</TableRow> </TableRow>
) : ( </TableHeader>
etcFileList.map((file) => ( <TableBody>
<TableRow key={file.atflId}> {etcFileList.length === 0 ? (
<TableCell className="text-left"> <TableRow>
{file.atflCd ? ( <TableCell colSpan={2} className="py-12 text-center text-muted-foreground">
<a 등록된 기타첨부파일이 없습니다.
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ) : (
)} etcFileList.map((file) => (
</TableBody> <TableRow key={file.atflId}>
</Table> <TableCell className="text-left">
</div> {file.atflCd ? (
</SectionCollapsible> <a
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
{file.orgnlFileNm || file.physFileNm || file.atflId}
</a>
) : (
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
)}
</TableCell>
<TableCell className="text-right text-gray-500">
{formatFileSize(file.atflFlieSz)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</SectionCollapsible>
)}
</> </>
); );
} }
@@ -2,6 +2,7 @@
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { useUserStore } from "@/shared/lib";
import { Field, FieldError } from "@/shared/ui/field"; import { Field, FieldError } from "@/shared/ui/field";
import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { SectionCollapsible } from "@/widgets/pbadms-sprt";
import { import {
@@ -23,6 +24,7 @@ import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist
import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options"; import { getGoalYearOptions, getLinkedGoalYears, getPlanYearOptions } from "../../lib/year-options";
function BasicInfoFormSection() { function BasicInfoFormSection() {
const userInfo = useUserStore((state) => state.userInfo);
const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>(); const { control, setValue, watch } = useFormContext<SewerBscPlanRegistFormData>();
const planYearOptions = getPlanYearOptions(); const planYearOptions = getPlanYearOptions();
const goalYearOptions = getGoalYearOptions(); const goalYearOptions = getGoalYearOptions();
@@ -265,7 +267,7 @@ function BasicInfoFormSection() {
<TableHead>구분</TableHead> <TableHead>구분</TableHead>
<TableCell>{sewerBscPlanSeLabel}</TableCell> <TableCell>{sewerBscPlanSeLabel}</TableCell>
<TableHead>신청 지자체</TableHead> <TableHead>신청 지자체</TableHead>
<TableCell>{localGovLabel}</TableCell> <TableCell>{localGovLabel || userInfo?.laeSeNm}</TableCell>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
</Table> </Table>
@@ -71,6 +71,10 @@ function ChecklistSection({
const { total, completed, percent } = progress; const { total, completed, percent } = progress;
if (cklstResultList.length === 0) {
return null;
}
return ( return (
<SectionCollapsible title="체크리스트"> <SectionCollapsible title="체크리스트">
<div className="flex flex-col gap-4 p-2"> <div className="flex flex-col gap-4 p-2">
@@ -2,6 +2,8 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config"; import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
@@ -334,11 +336,12 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
<TableCell align="center"> <TableCell align="center">
{item.atchFileInfo?.atflCd ? ( {item.atchFileInfo?.atflCd ? (
<a <a
className="text-blue-600 hover:underline" className="inline-flex cursor-pointer items-start gap-1 text-left text-blue-600 hover:underline"
href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`} href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${item.atchFileInfo.atflCd}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<Paperclip className="my-1 size-5 shrink-0" />
{item.atchFileInfo.orgnlFileNm || "다운로드"} {item.atchFileInfo.orgnlFileNm || "다운로드"}
</a> </a>
) : ( ) : (
@@ -4,11 +4,10 @@ import { useId, useMemo, useState } from "react";
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Paperclip } from "lucide-react";
import { getDefaultFileKey, useTus } from "@/shared/lib"; import { getDefaultFileKey, useTus } from "@/shared/lib";
import { Field } from "@/shared/ui/field"; import { Field } from "@/shared/ui/field";
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach"; import { attachService } from "@/entities/attach";
import { useFacilityList } from "@/entities/bns-if"; import { useFacilityList } from "@/entities/bns-if";
import { useNowDmgMtncGnsvy } from "@/entities/dc"; import { useNowDmgMtncGnsvy } from "@/entities/dc";
import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { SectionCollapsible } from "@/widgets/pbadms-sprt";
@@ -226,7 +225,6 @@ function TmplntFacilityAttachments({
}: TmplntFacilityAttachmentsProps) { }: TmplntFacilityAttachmentsProps) {
const formId = useId(); const formId = useId();
const { setValue } = useFormContext<SewerBscPlanRegistFormData>(); const { setValue } = useFormContext<SewerBscPlanRegistFormData>();
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() => const [uploadedFile, setUploadedFile] = useState<UploadedTmplntFile | null>(() =>
atchFileGroupId && atchFileNm atchFileGroupId && atchFileNm
? { ? {
@@ -254,7 +252,6 @@ function TmplntFacilityAttachments({
const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`; const fileInputId = `${formId}-tmplnt-atch-${facilityIndex}`;
const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false; const uploading = uploadedFile ? (isUploading[uploadedFile.fileKey] ?? false) : false;
const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? ""; const displayFileName = uploadedFile?.fileName ?? atchFileNm ?? "";
const downloadAtflCd = uploadedFile?.atflCd || atchFileGroupId;
const hasFile = Boolean(uploadedFile || atchFileNm); const hasFile = Boolean(uploadedFile || atchFileNm);
async function handleFileUpload(files: FileList | null) { async function handleFileUpload(files: FileList | null) {
@@ -315,22 +312,14 @@ function TmplntFacilityAttachments({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow> <TableRow>
<TableCell>{facilityName} 계획평면도</TableCell> <TableHead data-required align="left">
{facilityName} 계획평면도
</TableHead>
<TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell> <TableCell className="text-center">{TMPLNT_ATCH_FORMAT_LABEL}</TableCell>
<TableCell> <TableCell>
{hasFile ? ( {hasFile ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{downloadAtflCd ? ( <span className="text-xl">{displayFileName}</span>
<a
className="inline-flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(downloadAtflCd)}
>
<Paperclip className="size-5 shrink-0" />
{displayFileName}
</a>
) : (
<span className="text-xl">{displayFileName}</span>
)}
<Button <Button
type="button" type="button"
size="icon" size="icon"
@@ -245,6 +245,10 @@ function TmplntSection({ tmplntList, det, dtl }: TmplntSectionProps) {
<div className="mt-6 space-y-2"> <div className="mt-6 space-y-2">
<h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3> <h3 className="mb-4 text-xl font-semibold">처리구역 필수 첨부파일</h3>
<Table variant="secondary"> <Table variant="secondary">
<colgroup>
<col width="35%" />
<col width="65%" />
</colgroup>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead align="left">파일명</TableHead> <TableHead align="left">파일명</TableHead>
@@ -79,7 +79,9 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
setTaskSeComCd(""); setTaskSeComCd("");
}, [open]); }, [open]);
const canConfirm = Boolean(sidoComCd && sewerBscPlanSeComCd && taskSeComCd); const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
const canConfirm =
!!sewerBscPlanSeComCd && !!taskSeComCd && (!canSidoGugunSelectable || !!sidoComCd);
function handleConfirm() { function handleConfirm() {
if (!canConfirm) return; if (!canConfirm) return;
@@ -118,48 +120,50 @@ function SewerBscPlanApplyDialog({ open, onOpenChange, onConfirm }: SewerBscPlan
<col width="35%" /> <col width="35%" />
</colgroup> </colgroup>
<TableHeader> <TableHeader>
<TableRow> {canSidoGugunSelectable && (
<TableHead data-required>시도</TableHead> <TableRow>
<TableCell> <TableHead data-required>시도</TableHead>
<Select <TableCell>
value={sidoComCd || undefined} <Select
onValueChange={(value) => { value={sidoComCd || undefined}
setSidoComCd(value); onValueChange={(value) => {
setSggComCd(""); setSidoComCd(value);
}} setSggComCd("");
> }}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
{sidoOptions.map((option) => ( <SelectContent>
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}> {sidoOptions.map((option) => (
{option.adstDstrNm} <SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
</SelectItem> {option.adstDstrNm}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</TableCell> </Select>
<TableHead>시군구</TableHead> </TableCell>
<TableCell> <TableHead>시군구</TableHead>
<Select <TableCell>
value={sggComCd || undefined} <Select
onValueChange={setSggComCd} value={sggComCd || undefined}
disabled={!sidoComCd || isGugunPending} onValueChange={setSggComCd}
> disabled={!sidoComCd || isGugunPending}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
{gugunOptions.map((option) => ( <SelectContent>
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}> {gugunOptions.map((option) => (
{option.adstDstrNm} <SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
</SelectItem> {option.adstDstrNm}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</TableCell> </Select>
</TableRow> </TableCell>
</TableRow>
)}
<TableRow> <TableRow>
<TableHead data-required>계획구분</TableHead> <TableHead data-required>계획구분</TableHead>
<TableCell colSpan={3}> <TableCell colSpan={3}>
@@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { useAcntgSeMngTree } from "@/entities/acntg"; import { useAcntgSeMngTree } from "@/entities/acntg";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import { import {
SewerBscPlanApplySaveResponseDto, SewerBscPlanApplySaveResponseDto,
sewerBscPlanBaseKey, sewerBscPlanBaseKey,
@@ -22,6 +23,7 @@ import {
ChecklistFormSection, ChecklistFormSection,
ContactInfoFormSection, ContactInfoFormSection,
isFormChecklistComplete, isFormChecklistComplete,
isFormEsntlAtchComplete,
PrivacyConsentSection, PrivacyConsentSection,
SplmntDmndFormSection, SplmntDmndFormSection,
} from "@/widgets/pbadms-sprt"; } from "@/widgets/pbadms-sprt";
@@ -34,7 +36,6 @@ import {
mapDetailToFormData, mapDetailToFormData,
} from "../lib/form-data-mapper"; } from "../lib/form-data-mapper";
import { import {
SEWER_BSC_PLAN_DOC_SE_COM_CD,
type SewerBscPlanRegistFormData, type SewerBscPlanRegistFormData,
sewerBscPlanRegistSchema, sewerBscPlanRegistSchema,
} from "../lib/sewer-bsc-plan-regist-schema"; } from "../lib/sewer-bsc-plan-regist-schema";
@@ -76,6 +77,7 @@ function SewerBscPlanFormPage({
const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL"; const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL";
const { data: acntgTree = [] } = useAcntgSeMngTree(); const { data: acntgTree = [] } = useAcntgSeMngTree();
const { data: essentialFiles = [] } = useEsntlAtchFileMngTree({ docSeComCd: "SEWER" });
const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]); const costItems = useMemo(() => buildAcntgCostItems(acntgTree), [acntgTree]);
const form = useForm<SewerBscPlanRegistFormData>({ const form = useForm<SewerBscPlanRegistFormData>({
@@ -140,11 +142,21 @@ function SewerBscPlanFormPage({
return; return;
} }
if (data.sewerBscPlanTmplntList.some((item) => !item.atchFileGroupId?.trim())) {
alert("처리구역 필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
}
if (!isFormChecklistComplete(data.cklstResultList)) { if (!isFormChecklistComplete(data.cklstResultList)) {
alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다."); alert("모든 체크리스트 항목을 체크해야 신청이 가능합니다.");
return; return;
} }
if (!isFormEsntlAtchComplete(essentialFiles, data.esntlAtchFileList)) {
alert("필수 첨부파일을 모두 등록해야 신청이 가능합니다.");
return;
}
try { try {
setIsSubmitting(true); setIsSubmitting(true);
const result = await submitForm(data, "LE"); const result = await submitForm(data, "LE");
@@ -237,12 +249,9 @@ function SewerBscPlanFormPage({
<BizCstMatrixFormSection /> <BizCstMatrixFormSection />
<FnrsCstFormSection /> <FnrsCstFormSection />
<TmplntFormSection /> <TmplntFormSection />
<ChecklistFormSection <ChecklistFormSection docSeComCd="SEWER" fieldPrefix="sewerBscPlan" />
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD}
fieldPrefix="sewerBscPlan"
/>
<AttachmentsFormSection <AttachmentsFormSection
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD} docSeComCd="SEWER"
fieldPrefix="sewerBscPlanDtl" fieldPrefix="sewerBscPlanDtl"
initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined} initialEsntlFiles={isEditMode ? initialData?.esntlFileList : undefined}
initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined} initialEtcFiles={isEditMode ? initialData?.etcFileList : undefined}
@@ -11,7 +11,6 @@ import { Skeleton } from "@common/components";
import { needsRoleUser01AtrzResolve } from "../lib/button-config"; import { needsRoleUser01AtrzResolve } from "../lib/button-config";
import { getEditableReviewStep } from "../lib/review-step-utils"; import { getEditableReviewStep } from "../lib/review-step-utils";
import { SEWER_BSC_PLAN_DOC_SE_COM_CD } from "../lib/sewer-bsc-plan-regist-schema";
import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state"; import { EMPTY_CKLST_RESULT_LIST, useChecklistFormState } from "../lib/use-checklist-form-state";
import { FloatingActionButtons } from "./components/floating-action-buttons"; import { FloatingActionButtons } from "./components/floating-action-buttons";
@@ -121,7 +120,7 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
/> />
<FloatingActionButtons <FloatingActionButtons
data={data} data={data}
docSeComCd={SEWER_BSC_PLAN_DOC_SE_COM_CD} docSeComCd="SEWER"
sewerBscPlanDocNo={detDto.sewerBscPlanDocNo} sewerBscPlanDocNo={detDto.sewerBscPlanDocNo}
chgCycl={chgCycl} chgCycl={chgCycl}
prgrsSttsComCd={detDto.prgrsSttsComCd || ""} prgrsSttsComCd={detDto.prgrsSttsComCd || ""}
+1 -1
View File
@@ -188,7 +188,7 @@ export interface EsntlAtchFileSaveBase {
/** 보완요구 저장 공통 필드 */ /** 보완요구 저장 공통 필드 */
export interface SplmntDmndSaveBase { export interface SplmntDmndSaveBase {
splmntDmndSn?: number; splmntDmndSn?: number | null;
splmntArtclComCd?: string; splmntArtclComCd?: string;
splmntDmndCn?: string; splmntDmndCn?: string;
splmntDmndDt?: string; splmntDmndDt?: string;
+1
View File
@@ -3,6 +3,7 @@ export {
createEtcUploadKey, createEtcUploadKey,
createUploadKey, createUploadKey,
isEtcFileKey, isEtcFileKey,
isFormEsntlAtchComplete,
} from "./lib/attachments-form-utils"; } from "./lib/attachments-form-utils";
export { export {
buildAcntgCostItems, buildAcntgCostItems,
@@ -1,4 +1,5 @@
import { getDefaultFileKey } from "@/shared/lib"; import { getDefaultFileKey } from "@/shared/lib";
import type { EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
export type FieldPrefix = export type FieldPrefix =
| "fnrsCnsltnMng" | "fnrsCnsltnMng"
@@ -9,6 +10,11 @@ export type FieldPrefix =
export const ETC_FILE_KEY_PREFIX = "etc::"; export const ETC_FILE_KEY_PREFIX = "etc::";
interface EsntlAtchFileListItem {
esntlAtchFileId: string;
atchFileGroupId: string;
}
export function atchFileGroupIdPath(fieldPrefix: FieldPrefix): string { export function atchFileGroupIdPath(fieldPrefix: FieldPrefix): string {
return `${fieldPrefix}.atchFileGroupId`; return `${fieldPrefix}.atchFileGroupId`;
} }
@@ -35,3 +41,21 @@ export function toFileAccept(rmrk: string | null | undefined): string {
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)) .map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
.join(","); .join(",");
} }
export function isFormEsntlAtchComplete(
essentialFiles: EsntlAtchFileMngTreeResponseDto[],
esntlAtchFileList: EsntlAtchFileListItem[],
): boolean {
if (essentialFiles.length === 0) return true;
const requiredFiles = essentialFiles.filter((file) => file.esntlAtchYn === "Y");
if (requiredFiles.length === 0) return true;
const attachedIds = new Set(
esntlAtchFileList
.filter((item) => Boolean(item.atchFileGroupId))
.map((item) => item.esntlAtchFileId),
);
return requiredFiles.every((file) => attachedIds.has(file.esntlAtchFileId));
}
@@ -72,7 +72,8 @@ function PbadmsTaskApplyDialog({
const sidoOptions = sidoData ?? EMPTY_ADST_LIST; const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST; const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
const isPending = isSidoPending || isTaskSePending; const isPending = isSidoPending || isTaskSePending;
const canConfirm = Boolean(sidoComCd && selectedCodes.length > 0); const canSidoGugunSelectable = userInfo?.dtlInstCd !== "ROLE_USER05";
const canConfirm = selectedCodes.length > 0 && (!canSidoGugunSelectable || !!sidoComCd);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
@@ -121,48 +122,50 @@ function PbadmsTaskApplyDialog({
<col width="35%" /> <col width="35%" />
</colgroup> </colgroup>
<TableHeader> <TableHeader>
<TableRow> {canSidoGugunSelectable && (
<TableHead data-required>시도</TableHead> <TableRow>
<TableCell> <TableHead data-required>시도</TableHead>
<Select <TableCell>
value={sidoComCd || undefined} <Select
onValueChange={(value) => { value={sidoComCd || undefined}
setSidoComCd(value); onValueChange={(value) => {
setSggComCd(""); setSidoComCd(value);
}} setSggComCd("");
> }}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
{sidoOptions.map((option) => ( <SelectContent>
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}> {sidoOptions.map((option) => (
{option.adstDstrNm} <SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
</SelectItem> {option.adstDstrNm}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</TableCell> </Select>
<TableHead>시군구</TableHead> </TableCell>
<TableCell> <TableHead>시군구</TableHead>
<Select <TableCell>
value={sggComCd || undefined} <Select
onValueChange={setSggComCd} value={sggComCd || undefined}
disabled={!sidoComCd || isGugunPending} onValueChange={setSggComCd}
> disabled={!sidoComCd || isGugunPending}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="선택" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="선택" />
<SelectContent> </SelectTrigger>
{gugunOptions.map((option) => ( <SelectContent>
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}> {gugunOptions.map((option) => (
{option.adstDstrNm} <SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
</SelectItem> {option.adstDstrNm}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</TableCell> </Select>
</TableRow> </TableCell>
</TableRow>
)}
<TableRow> <TableRow>
<TableHead data-required>업무구분</TableHead> <TableHead data-required>업무구분</TableHead>
<TableCell colSpan={3}> <TableCell colSpan={3}>
@@ -26,6 +26,10 @@ interface SupplementDemandSectionProps {
function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) { function SupplementDemandSection({ splmntDmndList }: SupplementDemandSectionProps) {
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd(); const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
if (splmntDmndList.length === 0) {
return null;
}
return ( return (
<SectionCollapsible title="보완요청 및 완료 현황"> <SectionCollapsible title="보완요청 및 완료 현황">
<div className="overflow-x-auto"> <div className="overflow-x-auto">