[as] WIP: 기본계획조회 다이얼로그 구현
This commit is contained in:
@@ -2,9 +2,11 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmRequestDto,
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
DcService,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
@@ -31,15 +33,39 @@ async function getLvNowDmgMtncGnsvy(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getClsfSystm(params: ClsfSystmRequestDto): Promise<ClsfSystmResponseDto[]> {
|
||||
async function getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]> {
|
||||
const response = await client.get<ClsfSystmResponseDto[]>(`${ENDPOINT.DC_SERVICE}/clsfSystm`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFile(params: FileParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/file`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFilePrcs(fileSn: string): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileprcs`, {
|
||||
params: { fileSn },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fileDownload(params: FileDownloadParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileDownload`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dcService = {
|
||||
getNowDmgMtncGnsvy,
|
||||
getLvNowDmgMtncGnsvy,
|
||||
getClsfSystm,
|
||||
getFile,
|
||||
getFilePrcs,
|
||||
fileDownload,
|
||||
} satisfies DcService;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmRequestDto,
|
||||
ClsfSystmParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "../model/dc-interface";
|
||||
@@ -20,17 +21,45 @@ export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto) {
|
||||
});
|
||||
}
|
||||
|
||||
function isLvNowDmgMtncGnsvyEnabled(params: LvNowDmgMtncGnsvySearchDto): boolean {
|
||||
if (!params.level || !params.ctpvNm) return false;
|
||||
if (params.level === "1") return true;
|
||||
if (params.level === "2") return Boolean(params.sggNm);
|
||||
if (params.level === "3") return Boolean(params.sggNm && params.gnsvyYr);
|
||||
return false;
|
||||
}
|
||||
|
||||
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),
|
||||
enabled: isLvNowDmgMtncGnsvyEnabled(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useClsfSystm(params: ClsfSystmRequestDto) {
|
||||
export function useClsfSystm(params: ClsfSystmParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "clsfSystm", params],
|
||||
queryFn: () => dcService.getClsfSystm(params),
|
||||
// TODO: T4 — upClsfSystmSn 점진 로드 여부 확정 후 enabled/파라미터 조정
|
||||
enabled: Boolean(params.clsfSystmNm),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFile(params: FileParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "file", params],
|
||||
queryFn: () => dcService.getFile(params),
|
||||
// TODO: T2 — getFile Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(params.clsfSystmSn),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFilePrcs(fileSn: string) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "filePrcs", fileSn],
|
||||
queryFn: () => dcService.getFilePrcs(fileSn),
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(fileSn),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export { dcService } from "./api/dc-service";
|
||||
export { baseKey as nowDmgMtncGnsvyBaseKey, useNowDmgMtncGnsvy } from "./api/queries";
|
||||
export type { NowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvySearchDto } from "./model/dc-interface";
|
||||
export {
|
||||
baseKey as nowDmgMtncGnsvyBaseKey,
|
||||
useClsfSystm,
|
||||
useFile,
|
||||
useFilePrcs,
|
||||
useLvNowDmgMtncGnsvy,
|
||||
useNowDmgMtncGnsvy,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "./model/dc-interface";
|
||||
|
||||
@@ -12,11 +12,22 @@ export interface LvNowDmgMtncGnsvySearchDto {
|
||||
gnsvyYr?: string;
|
||||
}
|
||||
|
||||
export interface ClsfSystmRequestDto {
|
||||
export interface ClsfSystmParamsDto {
|
||||
clsfSystmNm?: string;
|
||||
upClsfSystmSn?: string;
|
||||
}
|
||||
|
||||
export interface FileParamsDto {
|
||||
sort?: string;
|
||||
clsfSystmSn?: string;
|
||||
vetPrcsYn?: string;
|
||||
}
|
||||
|
||||
export interface FileDownloadParamsDto {
|
||||
filePath?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
type NullableNumber = number | null;
|
||||
type NullableString = string | null;
|
||||
|
||||
@@ -67,14 +78,21 @@ export interface NowDmgMtncGnsvyResponseDto
|
||||
sggNm?: NullableString;
|
||||
}
|
||||
|
||||
/**
|
||||
* lvNowDmgMtncGnsvy 계층 노드.
|
||||
* - level 1: ctpvNm + children(구군)
|
||||
* - level 2: sggNm + children(연도)
|
||||
* - level 3: gnsvyYr + children(처리시설)
|
||||
* - 시설 리프: swtrmtFcltCd / swtrmtFcltNm (level 없음)
|
||||
*/
|
||||
export interface LvNowDmgMtncGnsvyResponseDto {
|
||||
level: number;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
gnsvyYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
children: LvNowDmgMtncGnsvyResponseDto[];
|
||||
level?: number;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
gnsvyYr?: string;
|
||||
swtrmtFcltCd?: string;
|
||||
swtrmtFcltNm?: string;
|
||||
children?: LvNowDmgMtncGnsvyResponseDto[];
|
||||
}
|
||||
|
||||
export interface ClsfSystmResponseDto {
|
||||
@@ -89,5 +107,8 @@ export interface ClsfSystmResponseDto {
|
||||
export interface DcService {
|
||||
getNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto): Promise<NowDmgMtncGnsvyResponseDto[]>;
|
||||
getLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto): Promise<LvNowDmgMtncGnsvyResponseDto[]>;
|
||||
getClsfSystm(params: ClsfSystmRequestDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getFile(params: FileParamsDto): Promise<unknown>;
|
||||
getFilePrcs(fileSn: string): Promise<unknown>;
|
||||
fileDownload(params: FileDownloadParamsDto): Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { LvNowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
|
||||
type StageKey = "Dt1" | "Dt2" | "Dt3" | "Dt4" | "" | "Dc1" | "Dc2" | "Dc3" | "Dc4";
|
||||
|
||||
const STAGE_KEYS: StageKey[] = ["Dt1", "Dt2", "Dt3", "Dt4", "", "Dc1", "Dc2", "Dc3", "Dc4"];
|
||||
|
||||
function formatPlanCell(value: string | number | null | undefined, asYear = false): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (asYear) return `${value}년`;
|
||||
if (typeof value === "number") return value.toLocaleString();
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num) && value.trim() !== "" && /^-?\d+(\.\d+)?$/.test(value.trim()))
|
||||
return num.toLocaleString();
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getStagedValue(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
stage: StageKey,
|
||||
): string | number | null | undefined {
|
||||
if (!data) return undefined;
|
||||
const key = `${fieldPrefix}${stage}` as keyof NowDmgMtncGnsvyResponseDto;
|
||||
return data[key] as string | number | null | undefined;
|
||||
}
|
||||
|
||||
function getPlanYearCells(data: NowDmgMtncGnsvyResponseDto | null | undefined): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, "planYr", stage), true));
|
||||
}
|
||||
|
||||
function getMetricCells(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, fieldPrefix, stage)));
|
||||
}
|
||||
|
||||
function collectNodesByLevel(
|
||||
nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
targetLevel: number,
|
||||
acc: LvNowDmgMtncGnsvyResponseDto[] = [],
|
||||
): LvNowDmgMtncGnsvyResponseDto[] {
|
||||
if (!nodes) return acc;
|
||||
for (const node of nodes) {
|
||||
if (node.level === targetLevel) acc.push(node);
|
||||
if (node.children?.length) collectNodesByLevel(node.children, targetLevel, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function extractYearOptions(tree: LvNowDmgMtncGnsvyResponseDto[] | undefined): string[] {
|
||||
const years = collectNodesByLevel(tree, 3)
|
||||
.map((node) => node.gnsvyYr)
|
||||
.filter((year): year is string => Boolean(year));
|
||||
return [...new Set(years)];
|
||||
}
|
||||
|
||||
interface FacilityOption {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function extractFacilityOptions(
|
||||
tree: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
gnsvyYr: string,
|
||||
): FacilityOption[] {
|
||||
const yearNodes = collectNodesByLevel(tree, 3).filter((node) => node.gnsvyYr === gnsvyYr);
|
||||
const options: FacilityOption[] = [];
|
||||
|
||||
function pushFacility(node: LvNowDmgMtncGnsvyResponseDto) {
|
||||
if (!node.swtrmtFcltCd) return;
|
||||
options.push({
|
||||
code: node.swtrmtFcltCd,
|
||||
name: node.swtrmtFcltNm ?? node.swtrmtFcltCd,
|
||||
});
|
||||
}
|
||||
|
||||
if (yearNodes.length > 0) {
|
||||
for (const yearNode of yearNodes) {
|
||||
for (const child of yearNode.children ?? []) pushFacility(child);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
// level=3 응답이 시설 리프만 내려주는 경우
|
||||
function walk(nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined) {
|
||||
if (!nodes) return;
|
||||
for (const node of nodes) {
|
||||
pushFacility(node);
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
walk(tree);
|
||||
return options;
|
||||
}
|
||||
|
||||
export {
|
||||
extractFacilityOptions,
|
||||
extractYearOptions,
|
||||
formatPlanCell,
|
||||
getMetricCells,
|
||||
getPlanYearCells,
|
||||
STAGE_KEYS,
|
||||
};
|
||||
export type { FacilityOption };
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { dcService, type NowDmgMtncGnsvyResponseDto, useLvNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
|
||||
import { extractFacilityOptions, extractYearOptions } from "../lib/previous-plan-table-utils";
|
||||
|
||||
import { PreviousPlanAreaTable } from "./previous-plan/previous-plan-area-table";
|
||||
import { PreviousPlanFileSection } from "./previous-plan/previous-plan-file-section";
|
||||
import { PreviousPlanSearchFilters } from "./previous-plan/previous-plan-search-filters";
|
||||
import { PreviousPlanSummaryTable } from "./previous-plan/previous-plan-summary-table";
|
||||
|
||||
interface PreviousPlanLookupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
|
||||
initialSidoNm?: string;
|
||||
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
|
||||
initialGugunNm?: string;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
function PreviousPlanLookupDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialSidoNm = "",
|
||||
initialGugunNm = "",
|
||||
}: PreviousPlanLookupDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [gnsvyYr, setGnsvyYr] = useState("");
|
||||
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
|
||||
const [planData, setPlanData] = useState<NowDmgMtncGnsvyResponseDto | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
|
||||
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
|
||||
|
||||
// TODO: T7 — level=1 응답 UI 활용 여부(현재는 캐시·호출만, 구군은 adstdstr)
|
||||
useLvNowDmgMtncGnsvy({
|
||||
level: "1",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
});
|
||||
|
||||
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "2",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
});
|
||||
|
||||
const { data: facilityTree, isPending: isFacilityPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "3",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
gnsvyYr: gnsvyYr || undefined,
|
||||
});
|
||||
|
||||
const yearOptions = useMemo(() => extractYearOptions(yearTree), [yearTree]);
|
||||
const facilityOptions = useMemo(
|
||||
() => extractFacilityOptions(facilityTree, gnsvyYr),
|
||||
[facilityTree, gnsvyYr],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSidoComCd("");
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
setHasPrefillApplied(false);
|
||||
return;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || hasPrefillApplied || isSidoPending || !sidoOptions.length) return;
|
||||
if (!initialSidoNm) {
|
||||
setHasPrefillApplied(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedSido = sidoOptions.find((option) => option.adstDstrNm === initialSidoNm);
|
||||
if (matchedSido) setSidoComCd(matchedSido.adstDstrCd);
|
||||
setHasPrefillApplied(true);
|
||||
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
|
||||
if (sggComCd) return;
|
||||
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
|
||||
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
|
||||
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
|
||||
|
||||
function handleSidoChange(value: string) {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleGugunChange(value: string) {
|
||||
setSggComCd(value);
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleYearChange(value: string) {
|
||||
setGnsvyYr(value);
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleFacilityChange(value: string) {
|
||||
setSwtrmtFcltCd(value);
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (!ctpvNm) {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!sggNm) {
|
||||
alert("구군을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!gnsvyYr) {
|
||||
alert("연도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!swtrmtFcltCd) {
|
||||
alert("공공하수처리시설을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const list = await dcService.getNowDmgMtncGnsvy({
|
||||
swtrmtFcltCd,
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
targetYear: gnsvyYr,
|
||||
});
|
||||
if (!list.length) {
|
||||
alert("조회된 데이터가 없습니다.");
|
||||
setPlanData(null);
|
||||
return;
|
||||
}
|
||||
setPlanData(list[0] ?? null);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setPlanData(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="flex max-h-[90vh] flex-col overflow-hidden sm:max-w-[90rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>기본 계획 찾기</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
{isSidoPending ? (
|
||||
<Skeleton className="h-[6rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSearchFilters
|
||||
sidoComCd={sidoComCd}
|
||||
sggComCd={sggComCd}
|
||||
gnsvyYr={gnsvyYr}
|
||||
swtrmtFcltCd={swtrmtFcltCd}
|
||||
sidoOptions={sidoOptions}
|
||||
gugunOptions={gugunOptions}
|
||||
yearOptions={yearOptions}
|
||||
facilityOptions={facilityOptions}
|
||||
isGugunPending={isGugunPending}
|
||||
isYearPending={Boolean(sggNm) && isYearPending}
|
||||
isFacilityPending={Boolean(gnsvyYr) && isFacilityPending}
|
||||
isSearching={isSearching}
|
||||
onSidoChange={handleSidoChange}
|
||||
onGugunChange={handleGugunChange}
|
||||
onYearChange={handleYearChange}
|
||||
onFacilityChange={handleFacilityChange}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SectionCollapsible
|
||||
title="기본계획파일(기본계획의 보고서를 트리에서 클릭하시면 파일을 다운받으실수 있습니다.)"
|
||||
defaultOpen
|
||||
>
|
||||
<PreviousPlanFileSection key={sggNm || "empty"} gugunNm={sggNm} />
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="총설" defaultOpen>
|
||||
{isSearching ? (
|
||||
<Skeleton className="h-[8rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSummaryTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="처리구역별 현황" defaultOpen>
|
||||
{isSearching ? (
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanAreaTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanLookupDialog };
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getMetricCells, getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
const AREA_ROWS = [
|
||||
{ label: "하수처리인구(인)", field: "swtrmtPpltnCnt" },
|
||||
{ label: "자연적하수처리인구수(인)", field: "natrSwtrmtPpltnCnt" },
|
||||
{ label: "사회적하수처리인구수(인)", field: "scilSwtrmtPpltnCnt" },
|
||||
{ label: "하수처리구역면적", field: "swtrmtZoneArea" },
|
||||
{ label: "물사용량원단위", field: "wtrUsqtyOrgnUnit" },
|
||||
{ label: "오수전환율(%)", field: "drtwSwtcRt" },
|
||||
{ label: "변동부하율(%)", field: "flctnLodRt" },
|
||||
{ label: "오수원단위", field: "drtwOrgnUnit" },
|
||||
{ label: "계획하수량합계(㎥/일)", field: "planSwageQntSum" },
|
||||
{ label: "생활오수(㎥/일)", field: "lvngDrtwSum" },
|
||||
{ label: "지하수(㎥/일)", field: "ugwtSum" },
|
||||
{ label: "공장폐수(㎥/일)", field: "fctryWwtSum" },
|
||||
{ label: "관광오수(㎥/일)", field: "toursmDrtwSum" },
|
||||
{ label: "군부대(㎥/일)", field: "mtbsSum" },
|
||||
{ label: "연계처리수(㎥/일)", field: "linkPrcsCntSum" },
|
||||
{ label: "기타오수(㎥/일)", field: "etcDrtwSum" },
|
||||
{ label: "지하수사용량(㎥/일)", field: "ugwtUseSum" },
|
||||
{ label: "시설용량(㎥/일)", field: "fcltCpctSum" },
|
||||
{ label: "배제방식", field: "exclsMethExpln" },
|
||||
] as const;
|
||||
|
||||
interface PreviousPlanAreaTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={3} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableHead key={index} className="text-center font-normal">
|
||||
{cell}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{AREA_ROWS.map((row) => {
|
||||
const cells = getMetricCells(data, row.field);
|
||||
return (
|
||||
<TableRow key={row.field}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{cells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-right">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanAreaTable };
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { type ClsfSystmResponseDto, dcService, useClsfSystm, useFile } from "@/entities/dc";
|
||||
import { Button, Skeleton } from "@common/components";
|
||||
|
||||
interface ClsfTreeNode {
|
||||
node: ClsfSystmResponseDto;
|
||||
children: ClsfTreeNode[];
|
||||
}
|
||||
|
||||
interface LegacyFileItem {
|
||||
no?: number | string;
|
||||
fileTitle?: string;
|
||||
regDt?: string;
|
||||
fileSn?: string;
|
||||
}
|
||||
|
||||
interface PreviousPlanFileSectionProps {
|
||||
gugunNm: string;
|
||||
}
|
||||
|
||||
function buildClsfTree(list: ClsfSystmResponseDto[]): ClsfTreeNode[] {
|
||||
const byId = new Map<string, ClsfTreeNode>();
|
||||
for (const item of list) {
|
||||
byId.set(item.clsfSystmSn, { node: item, children: [] });
|
||||
}
|
||||
|
||||
const roots: ClsfTreeNode[] = [];
|
||||
for (const item of list) {
|
||||
const current = byId.get(item.clsfSystmSn);
|
||||
if (!current) continue;
|
||||
const parent = item.upClsfSystmSn ? byId.get(item.upClsfSystmSn) : undefined;
|
||||
if (parent) parent.children.push(current);
|
||||
else roots.push(current);
|
||||
}
|
||||
|
||||
function sortNodes(nodes: ClsfTreeNode[]) {
|
||||
nodes.sort((a, b) => Number(a.node.sortSeq) - Number(b.node.sortSeq));
|
||||
nodes.forEach((node) => sortNodes(node.children));
|
||||
}
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
// TODO: T2 — getFile Response DTO 확정 후 정규화 로직 교체 (레거시: no, fileTitle, regDt, fileSn)
|
||||
function normalizeFileList(data: unknown): LegacyFileItem[] {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map((item) => {
|
||||
if (!item || typeof item !== "object") return {};
|
||||
const row = item as Record<string, unknown>;
|
||||
return {
|
||||
no: row.no as number | string | undefined,
|
||||
fileTitle: (row.fileTitle ?? row.fileName ?? row.fileNm) as string | undefined,
|
||||
regDt: row.regDt as string | undefined,
|
||||
fileSn: (row.fileSn ?? row.fileId) as string | undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function ClsfTreeItem({
|
||||
item,
|
||||
depth,
|
||||
selectedSn,
|
||||
onSelect,
|
||||
}: {
|
||||
item: ClsfTreeNode;
|
||||
depth: number;
|
||||
selectedSn: string;
|
||||
onSelect: (sn: string) => void;
|
||||
}) {
|
||||
const isSelected = selectedSn === item.node.clsfSystmSn;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className={`w-full rounded px-2 py-1 text-left text-sm hover:bg-muted ${
|
||||
isSelected ? "bg-muted font-semibold" : ""
|
||||
}`}
|
||||
style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}
|
||||
onClick={() => onSelect(item.node.clsfSystmSn)}
|
||||
>
|
||||
{item.node.clsfSystmNm}
|
||||
</button>
|
||||
{item.children.length > 0 && (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{item.children.map((child) => (
|
||||
<ClsfTreeItem
|
||||
key={child.node.clsfSystmSn}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
selectedSn={selectedSn}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [selectedClsfSn, setSelectedClsfSn] = useState("");
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const { data: clsfList, isPending: isClsfPending } = useClsfSystm({
|
||||
clsfSystmNm: gugunNm || undefined,
|
||||
});
|
||||
const { data: fileData, isPending: isFilePending } = useFile({
|
||||
clsfSystmSn: selectedClsfSn || undefined,
|
||||
});
|
||||
|
||||
const tree = useMemo(() => buildClsfTree(clsfList ?? []), [clsfList]);
|
||||
const files = useMemo(() => normalizeFileList(fileData), [fileData]);
|
||||
|
||||
async function handleDownload(fileSn?: string, fallbackName?: string) {
|
||||
if (!fileSn) {
|
||||
alert("파일 식별자가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 가드
|
||||
const prcsRaw = await dcService.getFilePrcs(fileSn);
|
||||
const fileInfo = (Array.isArray(prcsRaw) ? prcsRaw[0] : prcsRaw) as
|
||||
| { filePath?: string; fileTitle?: string; fileName?: string }
|
||||
| undefined;
|
||||
const filePath = fileInfo?.filePath;
|
||||
const fileName = fileInfo?.fileTitle ?? fileInfo?.fileName ?? fallbackName;
|
||||
|
||||
if (!filePath || !fileName) {
|
||||
alert("파일 정보를 가져올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: T3 — blob / Content-Disposition / 게이트웨이 다운로드 UX 확정
|
||||
await dcService.fileDownload({ filePath, fileName });
|
||||
alert("다운로드 요청을 전송했습니다. (다운로드 UX TBD)");
|
||||
} catch {
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!gugunNm) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">시도와 구군을 선택하세요.</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[40rem] gap-2">
|
||||
<div className="w-[40%] overflow-y-auto rounded border p-2">
|
||||
{isClsfPending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : tree.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{tree.map((item) => (
|
||||
<ClsfTreeItem
|
||||
key={item.node.clsfSystmSn}
|
||||
item={item}
|
||||
depth={0}
|
||||
selectedSn={selectedClsfSn}
|
||||
onSelect={(sn) => setSelectedClsfSn(sn)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[60%] overflow-y-auto rounded border p-2">
|
||||
{!selectedClsfSn ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
트리에서 항목을 선택하세요.
|
||||
</p>
|
||||
) : isFilePending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/40">
|
||||
<th className="border px-2 py-1">번호</th>
|
||||
<th className="border px-2 py-1 text-left">파일명</th>
|
||||
<th className="border px-2 py-1">등록일</th>
|
||||
<th className="border px-2 py-1">다운로드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file, index) => (
|
||||
<tr key={file.fileSn ?? `${file.fileTitle}-${index}`}>
|
||||
<td className="border px-2 py-1 text-center">{file.no ?? index + 1}</td>
|
||||
<td className="border px-2 py-1">{file.fileTitle ?? "-"}</td>
|
||||
<td className="border px-2 py-1 text-center">{file.regDt ?? "-"}</td>
|
||||
<td className="border px-2 py-1 text-center">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="sm"
|
||||
disabled={isDownloading}
|
||||
onClick={() => handleDownload(file.fileSn, file.fileTitle)}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanFileSection };
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { FacilityOption } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSearchFiltersProps {
|
||||
sidoComCd: string;
|
||||
sggComCd: string;
|
||||
gnsvyYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
sidoOptions: CmCdAdstDstrComboResponseDto[];
|
||||
gugunOptions: CmCdAdstDstrComboResponseDto[];
|
||||
yearOptions: string[];
|
||||
facilityOptions: FacilityOption[];
|
||||
isGugunPending: boolean;
|
||||
isYearPending: boolean;
|
||||
isFacilityPending: boolean;
|
||||
isSearching: boolean;
|
||||
onSidoChange: (value: string) => void;
|
||||
onGugunChange: (value: string) => void;
|
||||
onYearChange: (value: string) => void;
|
||||
onFacilityChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
function PreviousPlanSearchFilters({
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
gnsvyYr,
|
||||
swtrmtFcltCd,
|
||||
sidoOptions,
|
||||
gugunOptions,
|
||||
yearOptions,
|
||||
facilityOptions,
|
||||
isGugunPending,
|
||||
isYearPending,
|
||||
isFacilityPending,
|
||||
isSearching,
|
||||
onSidoChange,
|
||||
onGugunChange,
|
||||
onYearChange,
|
||||
onFacilityChange,
|
||||
onSearch,
|
||||
}: PreviousPlanSearchFiltersProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||
<Table variant="secondary" className="flex-1">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select value={sidoComCd || undefined} onValueChange={onSidoChange}>
|
||||
<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 data-required>구군</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={onGugunChange}
|
||||
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>
|
||||
<TableHead data-required>연도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={gnsvyYr || undefined}
|
||||
onValueChange={onYearChange}
|
||||
disabled={!sggComCd || isYearPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem key={year} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead data-required>처리시설</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={swtrmtFcltCd || undefined}
|
||||
onValueChange={onFacilityChange}
|
||||
disabled={!gnsvyYr || isFacilityPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{facilityOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
<Button type="button" className="shrink-0" onClick={onSearch} disabled={isSearching}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSearchFilters };
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSummaryTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>계획목표년도</TableHead>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-center">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSummaryTable };
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
@@ -17,6 +19,14 @@ 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: "계획인구" },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구" },
|
||||
@@ -25,11 +35,17 @@ 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" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
const [sido, gugun] = useWatch({
|
||||
control,
|
||||
name: ["sewerBscPlan.sido", "sewerBscPlan.gugun"],
|
||||
});
|
||||
|
||||
const stepHeaders = [
|
||||
{ label: "1단계", year: frlvlGoalYr },
|
||||
@@ -54,7 +70,8 @@ function OutlineFormSection() {
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
<Button type="button" color="white" disabled>
|
||||
{/* TODO: T6 — 부모 폼 수치 반영은 요구 없음(레거시 동일) */}
|
||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
@@ -102,6 +119,13 @@ function OutlineFormSection() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<PreviousPlanLookupDialog
|
||||
open={isLookupOpen}
|
||||
onOpenChange={setIsLookupOpen}
|
||||
initialSidoNm={sido}
|
||||
initialGugunNm={gugun}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user