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