diff --git a/apps/as/app/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx b/apps/as/app/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx index 2ddd7ff21..72492a84c 100644 --- a/apps/as/app/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx +++ b/apps/as/app/(with-base-layout)/sewer-bsc-plan/[id]/edit/page.tsx @@ -2,9 +2,12 @@ import { SewerBscPlanEditPage } from "@/pages/sewer-bsc-plan"; interface PageProps { params: Promise<{ id: string }>; + searchParams: Promise<{ chgCycl?: string }>; } -export default async function Page({ params }: PageProps) { +export default async function Page({ params, searchParams }: PageProps) { const { id } = await params; - return ; + const { chgCycl } = await searchParams; + const propChgCycl = chgCycl ? Number(chgCycl) : undefined; + return ; } diff --git a/apps/as/src/entities/dc/api/dc-service.ts b/apps/as/src/entities/dc/api/dc-service.ts index bea49d554..423815cb7 100644 --- a/apps/as/src/entities/dc/api/dc-service.ts +++ b/apps/as/src/entities/dc/api/dc-service.ts @@ -2,6 +2,11 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + ClsfSystmRequestDto, + ClsfSystmResponseDto, + DcService, + LvNowDmgMtncGnsvyResponseDto, + LvNowDmgMtncGnsvySearchDto, NowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvySearchDto, } from "../model/dc-interface"; @@ -10,10 +15,31 @@ async function getNowDmgMtncGnsvy( params: NowDmgMtncGnsvySearchDto, ): Promise { const response = await client.get( - ENDPOINT.DC_SERVICE.NOW_DMG_MTNC_GNSVY, + `${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`, { params }, ); return response.data; } -export { getNowDmgMtncGnsvy }; +async function getLvNowDmgMtncGnsvy( + params: LvNowDmgMtncGnsvySearchDto, +): Promise { + const response = await client.get( + `${ENDPOINT.DC_SERVICE}/lvNowDmgMtncGnsvy`, + { params }, + ); + return response.data; +} + +async function getClsfSystm(params: ClsfSystmRequestDto): Promise { + const response = await client.get(`${ENDPOINT.DC_SERVICE}/clsfSystm`, { + params, + }); + return response.data; +} + +export const dcService = { + getNowDmgMtncGnsvy, + getLvNowDmgMtncGnsvy, + getClsfSystm, +} satisfies DcService; diff --git a/apps/as/src/entities/dc/api/queries.ts b/apps/as/src/entities/dc/api/queries.ts index 440b66b09..9f28fcd95 100644 --- a/apps/as/src/entities/dc/api/queries.ts +++ b/apps/as/src/entities/dc/api/queries.ts @@ -2,16 +2,35 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import type { NowDmgMtncGnsvySearchDto } from "../model/dc-interface"; +import type { + ClsfSystmRequestDto, + LvNowDmgMtncGnsvySearchDto, + NowDmgMtncGnsvySearchDto, +} from "../model/dc-interface"; -import { getNowDmgMtncGnsvy } from "./dc-service"; +import { dcService } from "./dc-service"; -export const nowDmgMtncGnsvyBaseKey = [ENDPOINT.DC_SERVICE.NOW_DMG_MTNC_GNSVY] as const; +export const baseKey = [ENDPOINT.DC_SERVICE] as const; export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto) { return useQuery({ - queryKey: [...nowDmgMtncGnsvyBaseKey, params], - queryFn: () => getNowDmgMtncGnsvy(params), + queryKey: [...baseKey, "nowDmgMtncGnsvy", params], + queryFn: () => dcService.getNowDmgMtncGnsvy(params), enabled: Boolean(params.swtrmtFcltCd && params.ctpvNm), }); } + +export function useLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto) { + return useQuery({ + queryKey: [...baseKey, "lvNowDmgMtncGnsvy", params], + queryFn: () => dcService.getLvNowDmgMtncGnsvy(params), + enabled: Boolean(params.level && params.ctpvNm && params.sggNm && params.gnsvyYr), + }); +} + +export function useClsfSystm(params: ClsfSystmRequestDto) { + return useQuery({ + queryKey: [...baseKey, "clsfSystm", params], + queryFn: () => dcService.getClsfSystm(params), + }); +} diff --git a/apps/as/src/entities/dc/index.ts b/apps/as/src/entities/dc/index.ts index be8a12d0b..916014d96 100644 --- a/apps/as/src/entities/dc/index.ts +++ b/apps/as/src/entities/dc/index.ts @@ -1,6 +1,3 @@ -export { getNowDmgMtncGnsvy } from "./api/dc-service"; -export { nowDmgMtncGnsvyBaseKey, useNowDmgMtncGnsvy } from "./api/queries"; -export type { - NowDmgMtncGnsvyResponseDto, - NowDmgMtncGnsvySearchDto, -} from "./model/dc-interface"; +export { dcService } from "./api/dc-service"; +export { baseKey as nowDmgMtncGnsvyBaseKey, useNowDmgMtncGnsvy } from "./api/queries"; +export type { NowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvySearchDto } from "./model/dc-interface"; diff --git a/apps/as/src/entities/dc/model/dc-interface.ts b/apps/as/src/entities/dc/model/dc-interface.ts index cffab992d..83303afde 100644 --- a/apps/as/src/entities/dc/model/dc-interface.ts +++ b/apps/as/src/entities/dc/model/dc-interface.ts @@ -5,6 +5,18 @@ export interface NowDmgMtncGnsvySearchDto { targetYear?: string; } +export interface LvNowDmgMtncGnsvySearchDto { + level?: string; + ctpvNm?: string; + sggNm?: string; + gnsvyYr?: string; +} + +export interface ClsfSystmRequestDto { + clsfSystmNm?: string; + upClsfSystmSn?: string; +} + type NullableNumber = number | null; type NullableString = string | null; @@ -46,7 +58,6 @@ type StagedMetricFields = { [K in keyof NowDmgMtncGnsvyMetricFields as `${K & string}${StageSuffix}`]?: NowDmgMtncGnsvyMetricFields[K]; }; -/** DC nowDmgMtncGnsvy 응답 DTO */ export interface NowDmgMtncGnsvyResponseDto extends NowDmgMtncGnsvyMetricFields, StagedMetricFields { swtrmtFcltCd?: NullableString; @@ -55,3 +66,28 @@ export interface NowDmgMtncGnsvyResponseDto ctpvNm?: NullableString; sggNm?: NullableString; } + +export interface LvNowDmgMtncGnsvyResponseDto { + level: number; + ctpvNm: string; + sggNm: string; + gnsvyYr: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + children: LvNowDmgMtncGnsvyResponseDto[]; +} + +export interface ClsfSystmResponseDto { + clsfSystmSn: string; + upClsfSystmSn: string; + clsfSystmNm: string; + sortSeq: string; + droppAble: boolean; + path: string; +} + +export interface DcService { + getNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto): Promise; + getLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto): Promise; + getClsfSystm(params: ClsfSystmRequestDto): Promise; +} diff --git a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts index eab17428e..013b1d384 100644 --- a/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts +++ b/apps/as/src/entities/sewer-bsc-plan/api/sewer-bsc-plan-service.ts @@ -4,6 +4,7 @@ import type { PageResponse } from "@/shared/model"; import type { SewerBscPlanApplySaveRequestDto, + SewerBscPlanApplySaveResponseDto, SewerBscPlanDetResponseDto, SewerBscPlanResponseDto, SewerBscPlanSaveRequestDto, @@ -45,8 +46,10 @@ async function deleteFn(sewerBscPlanDocNo: string): Promise { await client.delete(`${BASE}/${sewerBscPlanDocNo}`); } -async function saveApply(body: SewerBscPlanApplySaveRequestDto): Promise { - const response = await client.post(`${BASE}/save-apply`, body); +async function saveApply( + body: SewerBscPlanApplySaveRequestDto, +): Promise { + const response = await client.post(`${BASE}/save-apply`, body); return response.data; } diff --git a/apps/as/src/entities/sewer-bsc-plan/index.ts b/apps/as/src/entities/sewer-bsc-plan/index.ts index 08dec87a7..eecf354cd 100644 --- a/apps/as/src/entities/sewer-bsc-plan/index.ts +++ b/apps/as/src/entities/sewer-bsc-plan/index.ts @@ -19,6 +19,7 @@ export type { EsntlAtchFileItem, EsntlAtchFileMngTreeResponseDto, SewerBscPlanApplySaveRequestDto, + SewerBscPlanApplySaveResponseDto, SewerBscPlanBizCstItem, SewerBscPlanBizCstResponseDto, SewerBscPlanDetResponseDto, diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts index 13485f5e4..cc5f533d8 100644 --- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-interface.ts @@ -307,6 +307,12 @@ export interface SewerBscPlanApplySaveRequestDto extends PbadmsApplySaveHeader { tmplntTmprStrgList?: SewerBscPlanTmplntTmprStrgItem[]; } +/** 백엔드 SewerBscPlanApplySaveResponseDto */ +export interface SewerBscPlanApplySaveResponseDto { + sewerBscPlanDocNo: string; + chgCycl: number; +} + /** 백엔드 SewerBscPlanDetResponseDto */ export interface SewerBscPlanDetResponseDto { detDto?: SewerBscPlanResponseDto; @@ -333,7 +339,7 @@ export interface SewerBscPlanService { body: SewerBscPlanUpdateRequestDto, ): Promise; delete(sewerBscPlanDocNo: string): Promise; - saveApply(body: SewerBscPlanApplySaveRequestDto): Promise; + saveApply(body: SewerBscPlanApplySaveRequestDto): Promise; findDetails(sewerBscPlanDocNo: string, chgCycl: number): Promise; submitSupplement(id: string, body: SewerBscPlanApplySaveRequestDto): Promise; submitReqSupplement( diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts index 53c4e3f10..7a6b1f0ac 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/form-data-mapper.ts @@ -9,6 +9,8 @@ import { recalcFnrsCstSumAmt } from "./fnrs-cst-form-utils"; import { type SewerBscPlanRegistFormData } from "./sewer-bsc-plan-regist-schema"; const PLAN_STEPS = [1, 2, 3, 4] as const; +/** 처리시설 상세: 0=현재, 1~4=변경 단계 */ +const TMPLNT_DTL_STEPS = [0, 1, 2, 3, 4] as const; function createEmptyOtlnList() { return PLAN_STEPS.map((planStpNo) => ({ planStpNo })); @@ -38,7 +40,7 @@ function mapEsntlAtchFileList(esntlFileList: SewerBscPlanDetResponseDto["esntlFi } function createEmptyTmplntDtlList(swtrmtFcltCd: string) { - return PLAN_STEPS.map((planStpNo) => ({ + return TMPLNT_DTL_STEPS.map((planStpNo) => ({ planStpNo, swtrmtFcltCd, prcsZoneArea: 0, @@ -183,6 +185,7 @@ export function mapDetailToFormData(data: SewerBscPlanDetResponseDto): SewerBscP swtrmtFcltCd: code, swtrmtFcltNm: item.swtrmtFcltNm, atchFileGroupId: item.atchFileGroupId, + atchFileNm: item.atchFileInfo?.orgnlFileNm, sewerBscPlanTmplntDtlList: createEmptyTmplntDtlList(code).map((empty) => { const dtlItem = dtlMap.get(empty.planStpNo); if (!dtlItem) return empty; @@ -257,11 +260,14 @@ export function buildApplyPayload( sewerBscPlanOtlnList: data.sewerBscPlanOtlnList, bizCstList: data.bizCstList, fnrsCstList: data.fnrsCstList.map((item) => recalcFnrsCstSumAmt(item)), - sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map((item) => ({ - swtrmtFcltCd: item.swtrmtFcltCd, - atchFileGroupId: item.atchFileGroupId, - sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList, - })), + sewerBscPlanTmplntList: data.sewerBscPlanTmplntList.map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ atchFileNm, swtrmtFcltNm, ...item }) => ({ + swtrmtFcltCd: item.swtrmtFcltCd, + atchFileGroupId: item.atchFileGroupId, + sewerBscPlanTmplntDtlList: item.sewerBscPlanTmplntDtlList, + }), + ), cklstResultList: data.cklstResultList.map((item) => ({ ...item, selected: item.selected ?? undefined, diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts index e49524a31..f789a41c9 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts @@ -86,6 +86,7 @@ export const sewerBscPlanRegistSchema = z.object({ swtrmtFcltCd: z.string(), swtrmtFcltNm: z.string().optional(), atchFileGroupId: z.string().optional(), + atchFileNm: z.string().optional(), sewerBscPlanTmplntDtlList: z.array(tmplntDtlSchema).optional(), }), ), diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/tmplnt-original-mapper.ts b/apps/as/src/pages/sewer-bsc-plan/lib/tmplnt-original-mapper.ts new file mode 100644 index 000000000..50deb704d --- /dev/null +++ b/apps/as/src/pages/sewer-bsc-plan/lib/tmplnt-original-mapper.ts @@ -0,0 +1,70 @@ +import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc"; + +/** + * 폼 매트릭스 행 키 ↔ DC 당초(Dc) 필드 prefix. + * API에 대응 필드가 없으면 null (당초 셀 `-`). + */ +const TMPLNT_DC_FIELD_PREFIX: Record = { + prcsZoneArea: "swtrmtZoneArea", + swtrmtPpltnCnt: "swtrmtPpltnCnt", + planSwqty: "planSwageQntSum", + fcltCpct: "fcltCpctSum", + sprsysDrtwPplLen: "brcPplSum", + sprsysExlnPplLen: "exclnPplSum", + cbsysPplLen: null, + icptgPplLen: "icptgPplSum", + drgCnt: "mltvFclSum", + drtwRlyPumpCnt: null, + drtwPumpCnt: "pmstnSum", + exlnPumpCnt: null, +}; + +export type TmplntFieldKey = + | "prcsZoneArea" + | "swtrmtPpltnCnt" + | "planSwqty" + | "fcltCpct" + | "sprsysDrtwPplLen" + | "sprsysExlnPplLen" + | "cbsysPplLen" + | "icptgPplLen" + | "drgCnt" + | "drtwRlyPumpCnt" + | "drtwPumpCnt" + | "exlnPumpCnt"; + +export const PLAN_STP_NOS = [1, 2, 3, 4] as const; + +export type PlanStpNo = (typeof PLAN_STP_NOS)[number]; + +function formatOriginalValue(value: string | number | null | undefined): string { + if (value === undefined || value === null || value === "") return "-"; + const num = Number(value); + if (!Number.isNaN(num)) return num.toLocaleString(); + return String(value); +} + +function getOriginalYear( + data: NowDmgMtncGnsvyResponseDto | null | undefined, + planStpNo: PlanStpNo, +): string { + if (!data) return "-"; + const key = `planYrDc${planStpNo}` as keyof NowDmgMtncGnsvyResponseDto; + const value = data[key]; + if (value === undefined || value === null || value === "") return "-"; + return String(value); +} + +function getOriginalValue( + data: NowDmgMtncGnsvyResponseDto | null | undefined, + fieldKey: TmplntFieldKey, + planStpNo: PlanStpNo, +): string { + if (!data) return "-"; + const prefix = TMPLNT_DC_FIELD_PREFIX[fieldKey]; + if (!prefix) return "-"; + const key = `${prefix}Dc${planStpNo}` as keyof NowDmgMtncGnsvyResponseDto; + return formatOriginalValue(data[key] as string | number | null | undefined); +} + +export { getOriginalValue, getOriginalYear }; diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx index c241806b2..d600d8701 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sections/tmplnt-form-section.tsx @@ -1,14 +1,19 @@ "use client"; -import { useMemo, useState } from "react"; -import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; +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 { useFacilityList } from "@/entities/bns-if"; +import { useNowDmgMtncGnsvy } from "@/entities/dc"; import { SectionCollapsible } from "@/widgets/pbadms-sprt"; import { + Button, Checkbox, Input, Table, @@ -25,66 +30,386 @@ import { import { createEmptyTmplntDtlList } from "../../lib/form-data-mapper"; import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema"; +import { + getOriginalValue, + getOriginalYear, + PLAN_STP_NOS, + type TmplntFieldKey, +} from "../../lib/tmplnt-original-mapper"; interface FacilityOption { code: string; name: string; } -const TMPLNT_FIELD_ROWS = [ - { key: "prcsZoneArea" as const, label: "처리구역면적(km²)" }, - { key: "swtrmtPpltnCnt" as const, label: "하수처리인구(명)" }, - { key: "planSwqty" as const, label: "계획하수량(㎥/일)" }, - { key: "fcltCpct" as const, label: "시설용량(㎥/일)" }, - { key: "sprsysDrtwPplLen" as const, label: "분류식 오수(km)" }, - { key: "sprsysExlnPplLen" as const, label: "분류식 우수(km)" }, - { key: "cbsysPplLen" as const, label: "합류식(km)" }, - { key: "icptgPplLen" as const, label: "차집관로(km)" }, - { key: "drgCnt" as const, label: "배수설비(개소)" }, - { key: "drtwRlyPumpCnt" as const, label: "오수 중계 펌프(개소)" }, - { key: "drtwPumpCnt" as const, label: "오수 일반 펌프(개소)" }, - { key: "exlnPumpCnt" as const, label: "우수 펌프(개소)" }, +interface ChangeStepHeader { + planStpNo: (typeof PLAN_STP_NOS)[number]; + year: string | undefined; +} + +interface UploadedTmplntFile { + fileName: string; + fileKey: string; + atflCd?: string; + atflId?: string; +} + +const TMPLNT_ATCH_ACCEPT = ".pdf,.png,.jpg,.jpeg"; +const TMPLNT_ATCH_FORMAT_LABEL = ".pdf, .png, .jpg, .jpeg"; + +const TMPLNT_FIELD_ROWS: { key: TmplntFieldKey; label: string }[] = [ + { key: "prcsZoneArea", label: "처리구역면적(km²)" }, + { key: "swtrmtPpltnCnt", label: "하수처리인구(명)" }, + { key: "planSwqty", label: "계획하수량(㎥/일)" }, + { key: "fcltCpct", label: "시설용량(㎥/일)" }, + { key: "sprsysDrtwPplLen", label: "분류식 오수(km)" }, + { key: "sprsysExlnPplLen", label: "분류식 우수(km)" }, + { key: "cbsysPplLen", label: "합류식(km)" }, + { key: "icptgPplLen", label: "차집관로(km)" }, + { key: "drgCnt", label: "배수설비(개소)" }, + { key: "drtwRlyPumpCnt", label: "오수 중계 펌프(개소)" }, + { key: "drtwPumpCnt", label: "오수 일반 펌프(개소)" }, + { key: "exlnPumpCnt", label: "우수 펌프(개소)" }, ]; +interface TmplntDtlInputProps { + facilityIndex: number; + planStpNo: number; + fieldKey: TmplntFieldKey; +} + +function TmplntDtlInput({ facilityIndex, planStpNo, fieldKey }: TmplntDtlInputProps) { + const { control } = useFormContext(); + const fieldName = + `sewerBscPlanTmplntList.${facilityIndex}.sewerBscPlanTmplntDtlList.${planStpNo}.${fieldKey}` as const; + + return ( + ( + + { + const numValue = Number(e.target.value.replace(/,/g, "")) || 0; + field.onChange(numValue); + }} + /> + + )} + /> + ); +} + +interface TmplntFacilityMatrixProps { + swtrmtFcltCd: string; + sido: string; + gugun: string; + facilityIndex: number; + crtrYr: string | undefined; + changeStepHeaders: ChangeStepHeader[]; +} + +function TmplntFacilityMatrix({ + swtrmtFcltCd, + sido, + gugun, + facilityIndex, + crtrYr, + changeStepHeaders, +}: TmplntFacilityMatrixProps) { + const searchParams = useMemo( + () => ({ + swtrmtFcltCd, + ctpvNm: sido, + sggNm: gugun, + }), + [swtrmtFcltCd, sido, gugun], + ); + const { data } = useNowDmgMtncGnsvy(searchParams); + const originalData = Array.isArray(data) ? (data[0] ?? null) : null; + + return ( + + + + + + + + + + + + + + + + + 구분 + + + 당초 + + + 현재 +
+ ({crtrYr || "-"}년) +
+ + 변경 + +
+ + {PLAN_STP_NOS.map((planStpNo) => ( + + {planStpNo}단계 +
+ + ({getOriginalYear(originalData, planStpNo)}) + +
+ ))} + {changeStepHeaders.map((step) => ( + + {step.planStpNo}단계 +
+ ({step.year || "-"}) +
+ ))} +
+
+ + {TMPLNT_FIELD_ROWS.map((row) => ( + + {row.label} + {PLAN_STP_NOS.map((planStpNo) => ( + + {getOriginalValue(originalData, row.key, planStpNo)} + + ))} + + + + {changeStepHeaders.map((step) => ( + + + + ))} + + ))} + +
+ ); +} + +interface TmplntFacilityAttachmentsProps { + facilityIndex: number; + facilityName?: string; + atchFileGroupId?: string; + atchFileNm?: string; +} + +function TmplntFacilityAttachments({ + facilityIndex, + facilityName, + atchFileGroupId, + atchFileNm, +}: TmplntFacilityAttachmentsProps) { + const formId = useId(); + const { setValue } = useFormContext(); + const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd(); + const [uploadedFile, setUploadedFile] = useState(() => + atchFileGroupId && atchFileNm + ? { + fileName: atchFileNm, + fileKey: `server::${atchFileGroupId}`, + atflCd: atchFileGroupId, + } + : null, + ); + + const { uploadFileAsync, isUploading, uploads } = useTus({ + accept: TMPLNT_ATCH_ACCEPT, + onSuccess: (atflCd, atflId, _file, fileKey) => { + if (!atflCd || !atflId) return; + setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileGroupId`, atflCd, { + shouldDirty: true, + }); + setUploadedFile((prev) => (prev?.fileKey === fileKey ? { ...prev, atflCd, atflId } : prev)); + }, + onError: (_error, _file, fileKey) => { + setUploadedFile((prev) => (prev?.fileKey === fileKey ? null : prev)); + }, + }); + + 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) { + if (!files || files.length === 0) return; + + const file = files[0]; + const fileKey = getDefaultFileKey(file); + const existingAtflCd = atchFileGroupId || uploadedFile?.atflCd; + + setUploadedFile({ fileName: file.name, fileKey }); + setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileNm`, file.name, { + shouldDirty: true, + }); + + try { + await uploadFileAsync(file, { + fileKey, + metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined, + }); + } catch { + // onError callback handles cleanup + } + } + + async function handleFileRemove() { + if (!uploadedFile) return; + + uploads[uploadedFile.fileKey]?.abort(); + + if (uploadedFile.atflId) { + try { + await attachService.deleteAttachmentByAtflId(uploadedFile.atflId); + } catch (error) { + console.error("처리시설 첨부파일 삭제 실패:", error); + } + } + + setUploadedFile(null); + setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileGroupId`, "", { shouldDirty: true }); + setValue(`sewerBscPlanTmplntList.${facilityIndex}.atchFileNm`, "", { shouldDirty: true }); + } + + return ( +
+

처리구역 필수 첨부파일

+ + + + + + + + + 파일명 + 파일 확장자 + 파일첨부 + + + + + {facilityName} 계획평면도 + {TMPLNT_ATCH_FORMAT_LABEL} + + {hasFile ? ( +
+ {downloadAtflCd ? ( + downloadAttachment(downloadAtflCd)} + > + + {displayFileName} + + ) : ( + {displayFileName} + )} + +
+ ) : ( + + )} +
+
+
+
+
+ ); +} + function TmplntFormSection() { const { confirm } = useConfirm(); - const { control, setValue, getValues } = useFormContext(); + const { control, getValues } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, name: "sewerBscPlanTmplntList", }); const tmplntList = useWatch({ control, name: "sewerBscPlanTmplntList" }) ?? []; - const sido = useWatch({ control, name: "sewerBscPlan.sido" }) ?? ""; - const gugun = useWatch({ control, name: "sewerBscPlan.gugun" }) ?? ""; - const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" }); - const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" }); - const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" }); - const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" }); - - const { data: facilityListData } = useFacilityList({ sido, gugun }, { enabled: Boolean(sido) }); - - const facilityOptions = useMemo( - () => - (facilityListData?.fctList ?? []) - .filter((facility) => facility.bscFctCd) - .map((facility) => ({ - code: facility.bscFctCd!, - name: facility.bscFctNm ?? facility.bscFctCd!, - })), - [facilityListData?.fctList], + const [sido, gugun] = useWatch({ + control, + name: ["sewerBscPlan.sido", "sewerBscPlan.gugun"], + }); + const [frlvlGoalYr, sclvlGoalYr, thlvlGoalYr, folvlGoalYr, crtrYr] = useWatch({ + control, + name: [ + "sewerBscPlanDtl.frlvlGoalYr", + "sewerBscPlanDtl.sclvlGoalYr", + "sewerBscPlanDtl.thlvlGoalYr", + "sewerBscPlanDtl.folvlGoalYr", + "sewerBscPlanDtl.crtrYr", + ], + }); + const { data: facilityListData } = useFacilityList( + { sido: sido ?? "", gugun: gugun ?? "" }, + { enabled: Boolean(sido) }, ); + + const facilityOptions = + facilityListData?.fctList + ?.filter((facility) => facility.bscFctCd) + .map((facility) => ({ + code: facility.bscFctCd!, + name: facility.bscFctNm ?? facility.bscFctCd!, + })) ?? []; + const [activeTab, setActiveTab] = useState(""); - const selectedCodes = useMemo( - () => new Set(tmplntList.map((item) => item.swtrmtFcltCd)), - [tmplntList], - ); + const selectedCodes = new Set(tmplntList.map((item) => item.swtrmtFcltCd)); - const stepHeaders = [ - { planStpNo: 1, label: "변경 1단계", year: frlvlGoalYr }, - { planStpNo: 2, label: "변경 2단계", year: sclvlGoalYr }, - { planStpNo: 3, label: "변경 3단계", year: thlvlGoalYr }, - { planStpNo: 4, label: "변경 4단계", year: folvlGoalYr }, + const changeStepHeaders: ChangeStepHeader[] = [ + { planStpNo: 1, year: frlvlGoalYr }, + { planStpNo: 2, year: sclvlGoalYr }, + { planStpNo: 3, year: thlvlGoalYr }, + { planStpNo: 4, year: folvlGoalYr }, ]; async function handleFacilityToggle(option: FacilityOption, checked: boolean) { @@ -93,13 +418,14 @@ function TmplntFormSection() { swtrmtFcltCd: option.code, swtrmtFcltNm: option.name, atchFileGroupId: "", + atchFileNm: "", sewerBscPlanTmplntDtlList: createEmptyTmplntDtlList(option.code), }); setActiveTab(option.code); return; } - if (!(await confirm(`「${option.name}」을(를) 제거하시겠습니까?`))) return; + if (!(await confirm(`${option.name}을(를) 제거하시겠습니까?`))) return; const index = getValues("sewerBscPlanTmplntList").findIndex( (item) => item.swtrmtFcltCd === option.code, @@ -111,26 +437,6 @@ function TmplntFormSection() { setActiveTab(remaining[0]?.swtrmtFcltCd ?? ""); } - function handleDtlChange( - facilityIndex: number, - planStpNo: number, - fieldKey: (typeof TMPLNT_FIELD_ROWS)[number]["key"], - value: string, - ) { - const numValue = Number(value.replace(/,/g, "")) || 0; - const list = [...getValues("sewerBscPlanTmplntList")]; - const facility = list[facilityIndex]; - if (!facility) return; - - const dtlList = [...(facility.sewerBscPlanTmplntDtlList ?? [])]; - const dtlIndex = dtlList.findIndex((item) => item.planStpNo === planStpNo); - if (dtlIndex < 0) return; - - dtlList[dtlIndex] = { ...dtlList[dtlIndex], [fieldKey]: numValue }; - list[facilityIndex] = { ...facility, sewerBscPlanTmplntDtlList: dtlList }; - setValue("sewerBscPlanTmplntList", list, { shouldDirty: true }); - } - const resolvedActiveTab = activeTab && selectedCodes.has(activeTab) ? activeTab : (tmplntList[0]?.swtrmtFcltCd ?? ""); @@ -142,7 +448,7 @@ function TmplntFormSection() {

조회된 처리시설이 없습니다.

) : ( facilityOptions.map((option) => ( -