178 lines
6.0 KiB
TypeScript
178 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import { useRef } from "react";
|
|
import { Button, Input } from "@common/components";
|
|
import { UiTable } from "@common/business_components/ui";
|
|
import LoadingOverlay from "@/components/ui/LoadingBar";
|
|
import useStore from "@/store/useStore";
|
|
import {
|
|
AGED_PIPE_LIST_HEADLIST,
|
|
type AgedPipeFilters,
|
|
type AgedPipeRow,
|
|
} from "./agedPipeUtils";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
filters: AgedPipeFilters;
|
|
onFiltersChange: (filters: AgedPipeFilters) => void;
|
|
rows: AgedPipeRow[];
|
|
total: number;
|
|
currentPage: number;
|
|
isLoading: boolean;
|
|
onSearch: (page?: number) => void;
|
|
onDownload: () => void;
|
|
onRowDoubleClick: (row: AgedPipeRow) => void;
|
|
};
|
|
|
|
export default function AgedPipeListModal({
|
|
open,
|
|
onOpenChange,
|
|
filters,
|
|
onFiltersChange,
|
|
rows,
|
|
total,
|
|
currentPage,
|
|
isLoading,
|
|
onSearch,
|
|
onDownload,
|
|
onRowDoubleClick,
|
|
}: Props) {
|
|
const { ctpv, sgg } = useStore();
|
|
const lastClickRef = useRef<{ index: number; clickedAt: number } | null>(null);
|
|
const sggOptions = filters.ctpvCd === "0"
|
|
? []
|
|
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
|
|
|
|
const handleRowClick = (index?: number) => {
|
|
if (typeof index !== "number") return;
|
|
const row = rows[index];
|
|
if (!row) return;
|
|
|
|
const now = Date.now();
|
|
const lastClick = lastClickRef.current;
|
|
if (lastClick && lastClick.index === index && now - lastClick.clickedAt < 300) {
|
|
lastClickRef.current = null;
|
|
onRowDoubleClick(row);
|
|
return;
|
|
}
|
|
|
|
lastClickRef.current = { index, clickedAt: now };
|
|
};
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center px-6 py-8">
|
|
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden border border-[#dbe2e6] bg-white shadow-[0_0_12px_rgba(0,0,0,0.25)]">
|
|
<LoadingOverlay isLoading={isLoading} />
|
|
<div className="flex items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
|
<div>
|
|
<div className="text-[1.45rem] font-semibold text-white">노후관로 목록</div>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
color="white"
|
|
size="xs"
|
|
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
닫기
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="relative overflow-y-auto p-4">
|
|
<div className="grid grid-cols-1 gap-3 bg-slate-50 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
|
<Input
|
|
className="h-10 !text-[1.1rem]"
|
|
inputMode="numeric"
|
|
value={String(filters.elapsedYears)}
|
|
onChange={(event) => {
|
|
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
|
onFiltersChange({ ...filters, elapsedYears: next });
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter") onSearch(1);
|
|
}}
|
|
/>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
|
<select
|
|
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
|
value={filters.ctpvCd}
|
|
onChange={(event) =>
|
|
onFiltersChange({
|
|
...filters,
|
|
ctpvCd: event.target.value,
|
|
sggCd: "0",
|
|
})
|
|
}
|
|
>
|
|
<option value="0">전체</option>
|
|
{ctpv.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
|
<select
|
|
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
|
value={filters.sggCd}
|
|
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
|
|
>
|
|
<option value="0">전체</option>
|
|
{sggOptions.map((item) => (
|
|
<option key={item.val} value={String(item.val)}>
|
|
{item.text}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
|
<Button type="button" size="sm" color="primary" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
|
조회
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
color="green"
|
|
className="h-10 px-5 !text-[1.1rem]"
|
|
onClick={onDownload}
|
|
>
|
|
다운로드
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-200 pt-3">
|
|
<UiTable<AgedPipeRow>
|
|
headName={<>노후관로 조회 결과</>}
|
|
tableData={rows}
|
|
headlist={AGED_PIPE_LIST_HEADLIST}
|
|
pageSize={10}
|
|
total={total}
|
|
cellClick={(index) => handleRowClick(index)}
|
|
onPageChange={(page) => onSearch(page)}
|
|
curPage={currentPage}
|
|
widthFixed={true}
|
|
/>
|
|
</div>
|
|
|
|
<div className="px-[1.2rem] pb-1 text-[1rem] text-slate-500">
|
|
목록 행을 더블클릭하면 지도 이동 후 상세 패널이 열립니다.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|