기본계획 조회 전체 적용
Deploy Frontend / deploy (push) Successful in 1m40s

This commit is contained in:
2026-10-15 15:53:38 +09:00
parent f284c67988
commit 5234448ae5
18 changed files with 122 additions and 50 deletions
+5 -1
View File
@@ -13,12 +13,16 @@ import type {
NowDmgMtncGnsvySearchDto,
} from "../model/dc-interface";
/** 빈 문자열 파라미터를 보내면 서버 오류가 나므로 값이 있는 항목만 전송한다. */
async function getNowDmgMtncGnsvy(
params: NowDmgMtncGnsvySearchDto,
): Promise<NowDmgMtncGnsvyResponseDto[]> {
const filledParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined && value !== ""),
);
const response = await client.get<NowDmgMtncGnsvyResponseDto[]>(
`${ENDPOINT.DC_SERVICE}/nowDmgMtncGnsvy`,
{ params },
{ params: filledParams },
);
return response.data;
}
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Input, Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import type { DesignCnstnRegistFormData } from "../../lib/design-cnstn-regist-schema";
@@ -16,9 +17,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<DesignCnstnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("designCnstnMng.taskSeComCd");
const localGovLabel = [watch("designCnstnMng.sido"), watch("designCnstnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("designCnstnMng.sido");
const gugun = watch("designCnstnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
const { data: taskOptions } = useCodeList("CNFM");
function handleBnsSelect(data: BnsApplyData) {
@@ -36,6 +37,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { DesignCnstnMngResponseDto } from "@/entities/design-cnstn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Checkbox,
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<FnrsCnsltnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("fnrsCnsltnMng.taskSeComCd");
const localGovLabel = [watch("fnrsCnsltnMng.sido"), watch("fnrsCnsltnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("fnrsCnsltnMng.sido");
const gugun = watch("fnrsCnsltnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
const { data: taskOptions } = useCodeList("FNRS");
const { data: rtlztnFcltOptions } = useCodeList("RTLZTNFCLTSE");
const { data: rtlztnUsgOptions } = useCodeList("RTLZTNUSGSE");
@@ -49,6 +50,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 시설종류, 재이용용도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { FnrsCnsltnMngResponseDto } from "@/entities/fnrs-cnsltn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 환경청구분, 구분, 신청지자체, 확정예산사업번호, 예산신청연도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -8,6 +8,7 @@ import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldError, FieldGroup, FieldLabel, FieldSet } from "@/shared/ui/field";
import { findCodeLabel, useCodeList } from "@/entities/code";
import { type BnsApplyData, BnsPickerDialog } from "@/widgets/bns-picker";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Checkbox,
@@ -27,9 +28,9 @@ function BasicInfoFormSection() {
const { control, watch, setValue } = useFormContext<InstlSncnRegistFormData>();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const taskSeComCd = watch("instlSncnMng.taskSeComCd");
const localGovLabel = [watch("instlSncnMng.sido"), watch("instlSncnMng.gugun")]
.filter(Boolean)
.join(" ");
const sido = watch("instlSncnMng.sido");
const gugun = watch("instlSncnMng.gugun");
const localGovLabel = [sido, gugun].filter(Boolean).join(" ");
function handleBnsSelect(data: BnsApplyData) {
setValue("instlSncnMng.bizNm", data.bizNm);
@@ -53,6 +54,7 @@ function BasicInfoFormSection() {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="기본 정보 입력으로 사업명, 사업기간, 사업내용, 구분, 신청 지자체, 사업구분, 시설종류, 재이용용도 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -2,6 +2,7 @@
import { SectionCollapsible } from "@/shared/ui";
import type { InstlSncnMngResponseDto } from "@/entities/instl-sncn";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface BasicInfoSectionProps {
@@ -11,6 +12,7 @@ interface BasicInfoSectionProps {
function BasicInfoSection({ det }: BasicInfoSectionProps) {
return (
<SectionCollapsible title="기본 정보">
<PreviousPlanLookupButton sidoNm={det.sido} gugunNm={det.gugun} />
<Table caption="기본 정보로 사업명, 사업기간, 사업내용, 구분, 신청지자체, 사업구분 정보 제공" variant="secondary">
<colgroup>
<col width="15%" />
@@ -1,15 +1,13 @@
"use client";
import { useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import dynamic from "next/dynamic";
import { NON_NEGATIVE_INPUT_PROPS, sanitizeNumberInput } from "@/shared/lib";
import { SectionCollapsible } from "@/shared/ui";
import { Field } from "@/shared/ui/field";
import { truncateDecimals } from "@/widgets/pbadms-sprt";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import {
Button,
Input,
Table,
TableBody,
@@ -21,14 +19,6 @@ import {
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
const PreviousPlanLookupDialog = dynamic(
() =>
import("../previous-plan-lookup-dialog").then((mod) => ({
default: mod.PreviousPlanLookupDialog,
})),
{ ssr: false },
);
const OTLN_ROWS = [
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구", decimals: 0 },
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구", decimals: 0 },
@@ -37,7 +27,6 @@ const OTLN_ROWS = [
function OutlineFormSection() {
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
const [isLookupOpen, setIsLookupOpen] = useState(false);
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
@@ -74,11 +63,7 @@ function OutlineFormSection() {
return (
<SectionCollapsible title="사업개요">
<div className="mb-2 flex justify-end px-2">
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
이전기본계획조회
</Button>
</div>
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
<colgroup>
<col width="12%" />
@@ -126,13 +111,6 @@ function OutlineFormSection() {
))}
</TableBody>
</Table>
<PreviousPlanLookupDialog
open={isLookupOpen}
onOpenChange={setIsLookupOpen}
initialSidoNm={sido}
initialGugunNm={gugun}
/>
</SectionCollapsible>
);
}
@@ -6,11 +6,14 @@ import type {
SewerBscPlanOtlnResponseDto,
} from "@/entities/sewer-bsc-plan";
import { fmtNumber } from "@/widgets/pbadms-sprt";
import { PreviousPlanLookupButton } from "@/widgets/previous-plan-lookup";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
interface OutlineSectionProps {
otlnList: SewerBscPlanOtlnResponseDto[];
dtl?: SewerBscPlanDtlResponseDto;
sido?: string | null;
gugun?: string | null;
}
const OTLN_ROWS = [
@@ -19,7 +22,7 @@ const OTLN_ROWS = [
{ key: "sewerPsplyRt" as const, label: "하수도보급율" },
];
function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
function OutlineSection({ otlnList, dtl, sido, gugun }: OutlineSectionProps) {
const stepHeaders = [
{ planStpNo: 1, label: "1단계", year: dtl?.frlvlGoalYr },
{ planStpNo: 2, label: "2단계", year: dtl?.sclvlGoalYr },
@@ -29,6 +32,7 @@ function OutlineSection({ otlnList, dtl }: OutlineSectionProps) {
return (
<SectionCollapsible title="사업개요">
<PreviousPlanLookupButton sidoNm={sido} gugunNm={gugun} />
<Table caption="사업개요 목록으로 구분, 1단계, 2단계, 3단계, 4단계 정보 제공" variant="secondary">
<colgroup>
<col width="12%" />
@@ -114,7 +114,12 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) {
<BasicInfoSection det={detDto} dtl={activeDtlDto} />
<ContactInfoSection dtl={activeDtlDto} />
<ElaprDocPrgrsStatus elaprDocNo={detDto.sewerBscPlanDocNo} />
<OutlineSection otlnList={otlnList} dtl={activeDtlDto} />
<OutlineSection
otlnList={otlnList}
dtl={activeDtlDto}
sido={detDto.sido}
gugun={detDto.gugun}
/>
<BizCstMatrixSection bizCstList={bizCstList} dtl={activeDtlDto} />
<FnrsCstSection fnrsCstList={fnrsCstList} dtl={activeDtlDto} />
<TmplntSection tmplntList={tmplntList} det={detDto} dtl={activeDtlDto} />
@@ -0,0 +1,4 @@
export {
PreviousPlanLookupButton,
type PreviousPlanLookupButtonProps,
} from "./ui/previous-plan-lookup-button";
@@ -0,0 +1,46 @@
"use client";
import { useState } from "react";
import dynamic from "next/dynamic";
import { Button } from "@common/components";
const PreviousPlanLookupDialog = dynamic(
() =>
import("./previous-plan-lookup-dialog").then((mod) => ({
default: mod.PreviousPlanLookupDialog,
})),
{ ssr: false },
);
interface PreviousPlanLookupButtonProps {
/** 조회 다이얼로그 시도 초기값(표시명) */
sidoNm?: string | null;
/** 조회 다이얼로그 시군구 초기값(표시명) */
gugunNm?: string | null;
}
/** 섹션 상단 우측에 [이전기본계획조회] 버튼과 조회 다이얼로그를 배치한다. */
function PreviousPlanLookupButton({ sidoNm, gugunNm }: PreviousPlanLookupButtonProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div className="mb-2 flex justify-end px-2">
<Button type="button" color="white" onClick={() => setIsOpen(true)}>
이전기본계획조회
</Button>
</div>
<PreviousPlanLookupDialog
open={isOpen}
onOpenChange={setIsOpen}
initialSidoNm={sidoNm}
initialGugunNm={gugunNm}
/>
</>
);
}
export { PreviousPlanLookupButton };
export type { PreviousPlanLookupButtonProps };
@@ -51,7 +51,7 @@ function PreviousPlanLookupDialog({
const [gnsvyYr, setGnsvyYr] = useState("");
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
const [searchedClsfSystmNm, setSearchedClsfSystmNm] = useState("");
const [hasGugunPrefillApplied, setHasGugunPrefillApplied] = useState(false);
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
@@ -62,15 +62,19 @@ function PreviousPlanLookupDialog({
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
// 창 오픈 시 시도/시군구로 조회하고, 콤보 선택에 따라 파라미터가 추가되며 재조회된다.
// 빈 값은 서비스에서 제외되며, 시도가 없으면 조회하지 않는다.
const searchParams = {
swtrmtFcltCd,
ctpvNm,
sggNm,
targetYear: gnsvyYr,
swtrmtFcltCd,
};
const isSearchEnabled = open && hasGugunPrefillApplied && Boolean(ctpvNm);
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, false);
const planData = gnsvyYr && swtrmtFcltCd ? (data?.[0] ?? null) : null;
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, isSearchEnabled);
const planData = isSearchEnabled ? (data?.[0] ?? null) : null;
const clsfSystmNm = planData ? sggNm || ctpvNm : "";
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
level: "2",
@@ -97,8 +101,8 @@ function PreviousPlanLookupDialog({
setSggComCd("");
setGnsvyYr("");
setSwtrmtFcltCd("");
setSearchedClsfSystmNm("");
setHasPrefillApplied(false);
setHasGugunPrefillApplied(false);
return;
}
}, [open]);
@@ -115,12 +119,27 @@ function PreviousPlanLookupDialog({
setHasPrefillApplied(true);
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
// 시군구 초기값 적용이 끝난 뒤에 최초 조회가 나가도록 완료 여부를 표시한다.
useEffect(() => {
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
if (sggComCd) return;
if (!open || !hasPrefillApplied || hasGugunPrefillApplied) return;
if (!sidoComCd || !initialGugunNm) {
setHasGugunPrefillApplied(true);
return;
}
if (isGugunPending) return;
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
setHasGugunPrefillApplied(true);
}, [
open,
hasPrefillApplied,
hasGugunPrefillApplied,
sidoComCd,
isGugunPending,
gugunOptions,
initialGugunNm,
]);
function handleSidoChange(value: string) {
setSidoComCd(value);
@@ -145,18 +164,18 @@ function PreviousPlanLookupDialog({
}
async function handleSearch() {
if (!ctpvNm) {
alert("시도를 선택하세요.");
return;
}
const { data: list, error } = await refetch();
if (error) {
alert("데이터 조회 중 오류가 발생했습니다.");
setSearchedClsfSystmNm("");
return;
}
if (!list?.length) {
alert("조회된 데이터가 없습니다.");
setSearchedClsfSystmNm("");
return;
}
setSearchedClsfSystmNm(sggNm || ctpvNm);
}
return (
@@ -198,8 +217,8 @@ function PreviousPlanLookupDialog({
<SectionCollapsible title="기본계획파일" defaultOpen>
<PreviousPlanFileSection
key={searchedClsfSystmNm || "empty"}
clsfSystmNm={searchedClsfSystmNm || undefined}
key={clsfSystmNm || "empty"}
clsfSystmNm={clsfSystmNm || undefined}
/>
</SectionCollapsible>