From 3c2ca24dead5e8761b409219fa981e531f471828 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 31 Jul 2026 16:58:46 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=ED=95=98=EC=88=98=EB=8F=84?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=20>=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/entities/dc/api/dc-service.ts | 7 +- apps/as/src/entities/dc/api/queries.ts | 4 +- apps/as/src/entities/dc/model/dc-interface.ts | 5 +- .../ui/previous-plan-lookup-dialog.tsx | 90 +++------ .../previous-plan-file-section.tsx | 183 +++++++++-------- .../previous-plan-search-filters.tsx | 189 +++++++++--------- .../ui/sections/outline-form-section.tsx | 1 - 7 files changed, 233 insertions(+), 246 deletions(-) diff --git a/apps/as/src/entities/dc/api/dc-service.ts b/apps/as/src/entities/dc/api/dc-service.ts index ed8fa4f4a..650e81b23 100644 --- a/apps/as/src/entities/dc/api/dc-service.ts +++ b/apps/as/src/entities/dc/api/dc-service.ts @@ -34,7 +34,7 @@ async function getLvNowDmgMtncGnsvy( } async function getClsfSystm(params: ClsfSystmParamsDto): Promise { - const response = await client.get(`${ENDPOINT.DC_SERVICE}/clsfSystm`, { + const response = await client.get(`${ENDPOINT.DC_SERVICE}/clsf`, { params, }); return response.data; @@ -54,9 +54,10 @@ async function getFilePrcs(fileSn: string): Promise { return response.data; } -async function fileDownload(params: FileDownloadParamsDto): Promise { - const response = await client.get(`${ENDPOINT.DC_SERVICE}/fileDownload`, { +async function fileDownload(params: FileDownloadParamsDto): Promise { + const response = await client.get(`${ENDPOINT.DC_SERVICE}/fileDownload`, { params, + responseType: "blob", }); return response.data; } diff --git a/apps/as/src/entities/dc/api/queries.ts b/apps/as/src/entities/dc/api/queries.ts index 8790d691d..043006305 100644 --- a/apps/as/src/entities/dc/api/queries.ts +++ b/apps/as/src/entities/dc/api/queries.ts @@ -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), }); } diff --git a/apps/as/src/entities/dc/model/dc-interface.ts b/apps/as/src/entities/dc/model/dc-interface.ts index 6a02a2b1c..59f0a7be2 100644 --- a/apps/as/src/entities/dc/model/dc-interface.ts +++ b/apps/as/src/entities/dc/model/dc-interface.ts @@ -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; getFile(params: FileParamsDto): Promise; getFilePrcs(fileSn: string): Promise; - fileDownload(params: FileDownloadParamsDto): Promise; + fileDownload(params: FileDownloadParamsDto): Promise; } diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx index 5c8c5ca11..843f0251b 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx @@ -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(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({
{isSidoPending ? ( - +
+ +
) : ( )} - - + + - {isSearching ? ( + {isFetching ? ( ) : ( @@ -236,7 +212,7 @@ function PreviousPlanLookupDialog({ - {isSearching ? ( + {isFetching ? ( ) : ( diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx index 084ddf4b1..c2d6cc457 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx @@ -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(); - 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 ( +
  • + + + + + {item.clsfSystmNm} + + +
      + {children.map((child) => ( + + ))} +
    +
    +
    +
  • + ); + } + + const isSelected = selectedSn === item.clsfSystmSn; return (
  • - {item.children.length > 0 && ( -
      - {item.children.map((child) => ( - - ))} -
    - )}
  • ); } -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 ( -

    시도와 구군을 선택하세요.

    +

    + 시도 및 구군을 선택 후 조회하세요. +

    ); } @@ -160,12 +177,12 @@ function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) { {isClsfPending ? ( ) : tree.length === 0 ? ( -

    파일이 없습니다.

    +

    파일이 없습니다.

    ) : (
      {tree.map((item) => (
      {!selectedClsfSn ? ( -

      +

      트리에서 항목을 선택하세요.

      ) : isFilePending ? ( ) : files.length === 0 ? ( -

      파일이 없습니다.

      +

      파일이 없습니다.

      ) : ( - - - - - - - - - - +
      번호파일명등록일다운로드
      + + + 번호 + 파일명 + 등록일 + 다운로드 + + + {files.map((file, index) => ( - - - - - - + + ))} - -
      {file.no ?? index + 1}{file.fileTitle ?? "-"}{file.regDt ?? "-"} + + {file.no ?? index + 1} + {file.fileTitle ?? "-"} + {file.regDt ?? "-"} + -
      + + )}
    diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx index e0f39085a..5ff87d01e 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx @@ -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 ( -
    - - - - - - - - - - - - - - 시도 - - - - 구군 - - - - 연도 - - - - 처리시설 - - - - - -
    - -
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    ); } diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx index fcb466cff..97674abbc 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx @@ -70,7 +70,6 @@ function OutlineFormSection() { return (
    - {/* TODO: T6 — 부모 폼 수치 반영은 요구 없음(레거시 동일) */}