[as] feat: 하수도기본계획 > 이전기본계획조회 다이얼로그 구현
This commit is contained in:
@@ -34,7 +34,7 @@ async function getLvNowDmgMtncGnsvy(
|
||||
}
|
||||
|
||||
async function getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]> {
|
||||
const response = await client.get<ClsfSystmResponseDto[]>(`${ENDPOINT.DC_SERVICE}/clsfSystm`, {
|
||||
const response = await client.get<ClsfSystmResponseDto[]>(`${ENDPOINT.DC_SERVICE}/clsf`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
@@ -54,9 +54,10 @@ async function getFilePrcs(fileSn: string): Promise<unknown> {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fileDownload(params: FileDownloadParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileDownload`, {
|
||||
async function fileDownload(params: FileDownloadParamsDto): Promise<Blob> {
|
||||
const response = await client.get<Blob>(`${ENDPOINT.DC_SERVICE}/fileDownload`, {
|
||||
params,
|
||||
responseType: "blob",
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import { dcService } from "./dc-service";
|
||||
|
||||
export const baseKey = [ENDPOINT.DC_SERVICE] as const;
|
||||
|
||||
export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto) {
|
||||
export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto, enabled?: boolean) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "nowDmgMtncGnsvy", params],
|
||||
queryFn: () => dcService.getNowDmgMtncGnsvy(params),
|
||||
enabled: Boolean(params.swtrmtFcltCd && params.ctpvNm),
|
||||
enabled: enabled ?? Boolean(params.swtrmtFcltCd && params.ctpvNm),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ export interface ClsfSystmResponseDto {
|
||||
clsfSystmNm: string;
|
||||
sortSeq: string;
|
||||
droppAble: boolean;
|
||||
path: string;
|
||||
path: string | null;
|
||||
children: ClsfSystmResponseDto[];
|
||||
}
|
||||
|
||||
export interface DcService {
|
||||
@@ -110,5 +111,5 @@ export interface DcService {
|
||||
getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getFile(params: FileParamsDto): Promise<unknown>;
|
||||
getFilePrcs(fileSn: string): Promise<unknown>;
|
||||
fileDownload(params: FileDownloadParamsDto): Promise<unknown>;
|
||||
fileDownload(params: FileDownloadParamsDto): Promise<Blob>;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { dcService, type NowDmgMtncGnsvyResponseDto, useLvNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { useLvNowDmgMtncGnsvy, useNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
@@ -50,9 +50,8 @@ function PreviousPlanLookupDialog({
|
||||
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 [searchedClsfSystmNm, setSearchedClsfSystmNm] = useState("");
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
@@ -63,11 +62,15 @@ function PreviousPlanLookupDialog({
|
||||
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 searchParams = {
|
||||
swtrmtFcltCd,
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
targetYear: gnsvyYr,
|
||||
};
|
||||
|
||||
const { data, isFetching, refetch } = useNowDmgMtncGnsvy(searchParams, false);
|
||||
const planData = gnsvyYr && swtrmtFcltCd ? (data?.[0] ?? null) : null;
|
||||
|
||||
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "2",
|
||||
@@ -94,7 +97,7 @@ function PreviousPlanLookupDialog({
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
setSearchedClsfSystmNm("");
|
||||
setHasPrefillApplied(false);
|
||||
return;
|
||||
}
|
||||
@@ -124,65 +127,36 @@ function PreviousPlanLookupDialog({
|
||||
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 {
|
||||
const { data: list, error } = await refetch();
|
||||
if (error) {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setPlanData(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
if (!list?.length) {
|
||||
alert("조회된 데이터가 없습니다.");
|
||||
setSearchedClsfSystmNm("");
|
||||
return;
|
||||
}
|
||||
setSearchedClsfSystmNm(sggNm || ctpvNm);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -197,7 +171,9 @@ function PreviousPlanLookupDialog({
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
{isSidoPending ? (
|
||||
<Skeleton className="h-[6rem] w-full" />
|
||||
<div className="mx-4">
|
||||
<Skeleton className="h-[6rem] w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<PreviousPlanSearchFilters
|
||||
sidoComCd={sidoComCd}
|
||||
@@ -211,7 +187,7 @@ function PreviousPlanLookupDialog({
|
||||
isGugunPending={isGugunPending}
|
||||
isYearPending={Boolean(sggNm) && isYearPending}
|
||||
isFacilityPending={Boolean(gnsvyYr) && isFacilityPending}
|
||||
isSearching={isSearching}
|
||||
isSearching={isFetching}
|
||||
onSidoChange={handleSidoChange}
|
||||
onGugunChange={handleGugunChange}
|
||||
onYearChange={handleYearChange}
|
||||
@@ -220,15 +196,15 @@ function PreviousPlanLookupDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
<SectionCollapsible
|
||||
title="기본계획파일(기본계획의 보고서를 트리에서 클릭하시면 파일을 다운받으실수 있습니다.)"
|
||||
defaultOpen
|
||||
>
|
||||
<PreviousPlanFileSection key={sggNm || "empty"} gugunNm={sggNm} />
|
||||
<SectionCollapsible title="기본계획파일" defaultOpen>
|
||||
<PreviousPlanFileSection
|
||||
key={searchedClsfSystmNm || "empty"}
|
||||
clsfSystmNm={searchedClsfSystmNm || undefined}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="총설" defaultOpen>
|
||||
{isSearching ? (
|
||||
{isFetching ? (
|
||||
<Skeleton className="h-[8rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSummaryTable data={planData} />
|
||||
@@ -236,7 +212,7 @@ function PreviousPlanLookupDialog({
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="처리구역별 현황" defaultOpen>
|
||||
{isSearching ? (
|
||||
{isFetching ? (
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanAreaTable data={planData} />
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { ChevronRight, File, Folder } from "lucide-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[];
|
||||
}
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
interface LegacyFileItem {
|
||||
no?: number | string;
|
||||
@@ -19,30 +29,7 @@ interface LegacyFileItem {
|
||||
}
|
||||
|
||||
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;
|
||||
clsfSystmNm?: string;
|
||||
}
|
||||
|
||||
// TODO: T2 — getFile Response DTO 확정 후 정규화 로직 교체 (레거시: no, fileTitle, regDt, fileSn)
|
||||
@@ -66,55 +53,77 @@ function ClsfTreeItem({
|
||||
selectedSn,
|
||||
onSelect,
|
||||
}: {
|
||||
item: ClsfTreeNode;
|
||||
item: ClsfSystmResponseDto;
|
||||
depth: number;
|
||||
selectedSn: string;
|
||||
onSelect: (sn: string) => void;
|
||||
}) {
|
||||
const isSelected = selectedSn === item.node.clsfSystmSn;
|
||||
const children = item.children ?? [];
|
||||
const isFolder = children.length > 0;
|
||||
|
||||
if (isFolder) {
|
||||
return (
|
||||
<li>
|
||||
<Collapsible defaultOpen={depth < 1} className="group/collapsible">
|
||||
<CollapsibleTrigger
|
||||
className="flex w-full items-center gap-1.5 rounded px-3 py-1.5 text-left text-xl hover:bg-muted"
|
||||
style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}
|
||||
>
|
||||
<ChevronRight className="size-4 shrink-0 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
<Folder className="size-6 shrink-0" />
|
||||
<span className="truncate">{item.clsfSystmNm}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="m-0 ml-3 list-none border-l border-border p-0 pl-1">
|
||||
{children.map((child) => (
|
||||
<ClsfTreeItem
|
||||
key={child.clsfSystmSn}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
selectedSn={selectedSn}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const isSelected = selectedSn === item.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)}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-1.5 rounded px-3 py-1.5 text-left text-xl hover:bg-muted",
|
||||
isSelected && "bg-muted font-semibold",
|
||||
)}
|
||||
style={{ paddingLeft: `${0.5 + depth * 1}rem` }}
|
||||
onClick={() => onSelect(item.clsfSystmSn)}
|
||||
>
|
||||
{item.node.clsfSystmNm}
|
||||
<File className="size-6 shrink-0" />
|
||||
<span className="truncate">{item.clsfSystmNm}</span>
|
||||
</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) {
|
||||
function PreviousPlanFileSection({ clsfSystmNm }: 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: clsfTree, isPending: isClsfPending } = useClsfSystm({
|
||||
clsfSystmNm,
|
||||
});
|
||||
const { data: fileData, isPending: isFilePending } = useFile({
|
||||
clsfSystmSn: selectedClsfSn || undefined,
|
||||
});
|
||||
|
||||
const tree = useMemo(() => buildClsfTree(clsfList ?? []), [clsfList]);
|
||||
const tree = clsfTree ?? [];
|
||||
const files = useMemo(() => normalizeFileList(fileData), [fileData]);
|
||||
|
||||
async function handleDownload(fileSn?: string, fallbackName?: string) {
|
||||
@@ -138,9 +147,15 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: T3 — blob / Content-Disposition / 게이트웨이 다운로드 UX 확정
|
||||
await dcService.fileDownload({ filePath, fileName });
|
||||
alert("다운로드 요청을 전송했습니다. (다운로드 UX TBD)");
|
||||
const blob = await dcService.fileDownload({ filePath, fileName });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
@@ -148,9 +163,11 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!gugunNm) {
|
||||
if (!clsfSystmNm) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">시도와 구군을 선택하세요.</p>
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
시도 및 구군을 선택 후 조회하세요.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,12 +177,12 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
{isClsfPending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : tree.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">파일이 없습니다.</p>
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{tree.map((item) => (
|
||||
<ClsfTreeItem
|
||||
key={item.node.clsfSystmSn}
|
||||
key={item.clsfSystmSn}
|
||||
item={item}
|
||||
depth={0}
|
||||
selectedSn={selectedClsfSn}
|
||||
@@ -177,30 +194,30 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
</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 className="py-8 text-center text-lg 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>
|
||||
<p className="py-8 text-center text-lg 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>
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="!py-2 !text-xl">번호</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">파일명</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">등록일</TableHead>
|
||||
<TableHead className="!py-2 !text-xl">다운로드</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{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">
|
||||
<TableRow key={file.fileSn ?? `${file.fileTitle}-${index}`}>
|
||||
<TableCell className="!py-2 text-center">{file.no ?? index + 1}</TableCell>
|
||||
<TableCell className="!py-2 text-center">{file.fileTitle ?? "-"}</TableCell>
|
||||
<TableCell className="!py-2 text-center">{file.regDt ?? "-"}</TableCell>
|
||||
<TableCell className="!py-2 text-center">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
@@ -210,11 +227,11 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+91
-98
@@ -3,16 +3,12 @@
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { FacilityOption } from "../../lib/previous-plan-table-utils";
|
||||
@@ -57,99 +53,96 @@ function PreviousPlanSearchFilters({
|
||||
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>
|
||||
<section className="card card-border mx-4 bg-background">
|
||||
<div className="flex flex-wrap items-center gap-4 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-sidoComCd" className="whitespace-nowrap">
|
||||
시도
|
||||
</Label>
|
||||
<Select value={sidoComCd || undefined} onValueChange={onSidoChange}>
|
||||
<SelectTrigger id="previous-plan-sidoComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-sggComCd" className="whitespace-nowrap">
|
||||
구군
|
||||
</Label>
|
||||
<Select
|
||||
key={sidoComCd || "gugun"}
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={onGugunChange}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-sggComCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-gnsvyYr" className="whitespace-nowrap">
|
||||
연도
|
||||
</Label>
|
||||
<Select
|
||||
key={sggComCd || "year"}
|
||||
value={gnsvyYr || undefined}
|
||||
onValueChange={onYearChange}
|
||||
disabled={!sggComCd || isYearPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-gnsvyYr" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem key={year} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="previous-plan-swtrmtFcltCd" className="whitespace-nowrap">
|
||||
처리시설
|
||||
</Label>
|
||||
<Select
|
||||
key={gnsvyYr || "facility"}
|
||||
value={swtrmtFcltCd || undefined}
|
||||
onValueChange={onFacilityChange}
|
||||
disabled={!gnsvyYr || isFacilityPending}
|
||||
>
|
||||
<SelectTrigger id="previous-plan-swtrmtFcltCd" className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{facilityOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="button" onClick={onSearch} disabled={isSearching}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ function OutlineFormSection() {
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
{/* TODO: T6 — 부모 폼 수치 반영은 요구 없음(레거시 동일) */}
|
||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user