st: feat: 로그 조회, 입력-하수관로 조회, 입력-하수도보급률 예외 제출
This commit is contained in:
@@ -72,6 +72,8 @@ const CompactDataGrid = ({
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
onRowClick,
|
||||
compactFullWidth = false,
|
||||
compactGridHeightClass = "h-[65vh]",
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
@@ -313,7 +315,7 @@ const CompactDataGrid = ({
|
||||
return () => {
|
||||
if (getGridDataRef) getGridDataRef.current = null;
|
||||
};
|
||||
}, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox, getGridDataRef]);
|
||||
}, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox, getGridDataRef, compactFullWidth, compactGridHeightClass]);
|
||||
|
||||
// 행 클릭 이벤트
|
||||
useEffect(() => {
|
||||
@@ -336,8 +338,8 @@ const CompactDataGrid = ({
|
||||
}, [onRowClick]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem] w-1/2">
|
||||
<div className="h-[65vh]">
|
||||
<div className={`flex flex-col gap-[0.6rem] ${compactFullWidth ? "w-full min-w-0" : "w-1/2"}`}>
|
||||
<div className={compactGridHeightClass}>
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
||||
|
||||
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
|
||||
|
||||
type GridTopActionButtonProps = {
|
||||
text: string;
|
||||
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
|
||||
/** 상위 화면 검색 통계연도 — 시설 조회 다이얼로그에 전달 */
|
||||
statsYr?: string;
|
||||
};
|
||||
|
||||
const GridTopActionButton = ({ text, onClick, statsYr }: GridTopActionButtonProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [title, setTitle] = useState(text);
|
||||
|
||||
const openDialog = (nextTitle?: string) => {
|
||||
setTitle(nextTitle ?? text);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => setIsOpen(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (onClick) {
|
||||
onClick({ openDialog, closeDialog });
|
||||
return;
|
||||
}
|
||||
openDialog(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button className="h-[3.2rem] px-6" color="white" type="button" onClick={handleClick}>
|
||||
<span>{text}</span>
|
||||
</Button>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent aria-describedby={undefined} className="max-w-[100rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SidoSggFacilityNameSearchForm statsYr={statsYr} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GridTopActionButton;
|
||||
@@ -233,7 +233,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button color="green" className="h-[3.2rem] w-[96px]" type="button" onClick={handleReset}>
|
||||
<Button color="white" className="h-[3.2rem] w-[96px]" type="button" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
LoadingWithAnimation
|
||||
} from "@common/components";
|
||||
import CompactDataGrid from "./CompactDataGrid";
|
||||
import SaveButton from "../button/SaveButton";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
|
||||
import type { ColumnInfo } from "../type/DataGrid.type";
|
||||
import type {
|
||||
SeweragePipeFacilitiesListRequest,
|
||||
SeweragePipeFacilityAdminAreaListRequest,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
|
||||
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
|
||||
|
||||
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
|
||||
const treatmentFacilityColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "fctNm", fieldName: "fctNm", header: { text: "시설명" }, editable: false },
|
||||
{
|
||||
name: "fctCpc",
|
||||
fieldName: "fctCpc",
|
||||
header: { text: "시설용량(m³/일)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
},
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "fctNm", "fctCpc"],
|
||||
};
|
||||
|
||||
/** 다이얼로그 우측: 행정구역 정보 (순번 없음, 헤더 체크박스는 showHeaderCheckbox) */
|
||||
const adminRegionColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "emdNm", fieldName: "emdNm", header: { text: "읍면동" }, editable: false },
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "emdNm"],
|
||||
};
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggAllListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggAllList(key[1]);
|
||||
};
|
||||
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
/** 상위(SewagePipeInp 등) 검색 조건의 통계연도 — facilitiesList 등 API 필수값 */
|
||||
statsYr?: string;
|
||||
};
|
||||
|
||||
const SidoSggFacilityNameSearchForm = ({ statsYr }: SidoSggFacilityNameSearchFormProps) => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>("sido-list", getSidoListFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시도 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
});
|
||||
|
||||
const [sido, setSido] = useState<string>("ALL");
|
||||
const [gugun, setGugun] = useState<string>("ALL");
|
||||
const [fctNm, setFctNm] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [treatmentFacilityRows, setTreatmentFacilityRows] = useState<Record<string, string | number>[]>([]);
|
||||
const [adminRegionRows, setAdminRegionRows] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
const sggKey: [string, string] | null = sido && sido !== "ALL" ? ["sgg-all", sido] : null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(sggKey, getSggAllListFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
revalidateIfStale: false,
|
||||
dedupingInterval: 2000,
|
||||
shouldRetryOnError: false,
|
||||
onSuccess: (data) => {
|
||||
if (data.status !== "success") {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert(`시군구 목록을 불러오는데 실패했습니다.\n새로고침 후 다시 시도해주세요.`);
|
||||
},
|
||||
});
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
/** 시설 목록·행정구역 API 공통 — 하수도 사업자코드(bsnmCd) */
|
||||
const resolveSewerBzmnCd = (): string => {
|
||||
const sidoItem = sidoList?.data?.sidoList?.find((v) => v.sido === sido);
|
||||
const raw =
|
||||
gugun !== "ALL" && gugun
|
||||
? sggOptions.find((s) => s.gugun === gugun)?.bsnmCd
|
||||
: sidoItem?.bsnmCd;
|
||||
return raw?.trim() ?? "";
|
||||
};
|
||||
|
||||
const handleSidoChange = (value: string) => {
|
||||
setSido(value);
|
||||
setGugun("ALL");
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!statsYr?.trim()) {
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
return;
|
||||
}
|
||||
if (!sido || sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const trimmedFclty = fctNm.trim();
|
||||
|
||||
const sewerBzmnCd = resolveSewerBzmnCd();
|
||||
|
||||
const params: SeweragePipeFacilitiesListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
...(sewerBzmnCd ? { sewerBzmnCd } : {}),
|
||||
ctpvNm: sido,
|
||||
...(gugun !== "ALL" && gugun ? { sggNm: gugun } : {}),
|
||||
...(trimmedFclty ? { fcltyNm: trimmedFclty } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputSewPipeApiService.getFacilitiesList(params);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "처리시설 목록 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
const list = response.data ?? [];
|
||||
setTreatmentFacilityRows(
|
||||
list.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
fctNm: item.bscFctNm,
|
||||
fctCpc: item.fctCpc,
|
||||
bscFctCd: item.bscFctCd,
|
||||
admdstCd: item.admdstCd,
|
||||
statsYr: item.statsYr,
|
||||
})),
|
||||
);
|
||||
setAdminRegionRows([]);
|
||||
} catch {
|
||||
alert("처리시설 목록 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTreatmentFacilityRowClick = async (row: Record<string, string | number>) => {
|
||||
if (!statsYr?.trim()) {
|
||||
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
|
||||
return;
|
||||
}
|
||||
const swtrmtFcltCd = String(row.bscFctCd ?? "").trim();
|
||||
if (!swtrmtFcltCd) {
|
||||
alert("처리시설 코드가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const ctpvNm = String(row.ctpvNm ?? sido);
|
||||
const sggNm = String(
|
||||
row.sggNm ?? (gugun !== "ALL" && gugun ? gugun : ""),
|
||||
);
|
||||
|
||||
const params: SeweragePipeFacilityAdminAreaListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd,
|
||||
sewerBzmnCd: String(row.admdstCd ?? "").trim(),
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputSewPipeApiService.getFacilityAdminAreaList(params);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "행정구역 목록 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
const list = response.data ?? [];
|
||||
setAdminRegionRows(
|
||||
list.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
emdNm: item.admdstNm,
|
||||
admdstCd: item.admdstCd,
|
||||
})),
|
||||
);
|
||||
} catch {
|
||||
alert("행정구역 목록 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveAdminRegions = () => {
|
||||
alert("저장 API 연동 예정입니다.");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-2">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label shrink-0">시도</Label>
|
||||
<Select value={sido} onValueChange={handleSidoChange}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sidoList?.data?.sidoList?.map((item) => (
|
||||
<SelectItem key={item.adstDstrCd} value={item.sido}>
|
||||
{item.sido}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label shrink-0">시군구</Label>
|
||||
<Select value={gugun} onValueChange={setGugun} disabled={!sido || sido === "ALL"}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggOptions?.map((sgg) => (
|
||||
<SelectItem key={sgg.adstDstrCd} value={sgg.gugun}>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-[20rem] flex-1">
|
||||
<Label className="text-label shrink-0">시설명</Label>
|
||||
<Input
|
||||
className="flex-1 min-w-[12rem]"
|
||||
value={fctNm}
|
||||
onChange={(e) => setFctNm(e.target.value)}
|
||||
placeholder="시설명 입력"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="h-[3.2rem] w-[96px]" color="primary">
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<SaveButton saveFunc={handleSaveAdminRegions} />
|
||||
</div>
|
||||
</form>
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<div className="flex gap-[1.2rem] items-stretch w-full min-w-0">
|
||||
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">처리시설 정보</h3>
|
||||
<CompactDataGrid
|
||||
columnInfo={treatmentFacilityColumnInfo}
|
||||
rowData={treatmentFacilityRows}
|
||||
setRowData={setTreatmentFacilityRows}
|
||||
showHeaderCheckbox={false}
|
||||
compactFullWidth
|
||||
onRowClick={handleTreatmentFacilityRowClick}
|
||||
// compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-[1.4rem] font-medium text-foreground">행정구역 정보</h3>
|
||||
</div>
|
||||
<CompactDataGrid
|
||||
columnInfo={adminRegionColumnInfo}
|
||||
rowData={adminRegionRows}
|
||||
setRowData={setAdminRegionRows}
|
||||
showHeaderCheckbox
|
||||
compactFullWidth
|
||||
// compactGridHeightClass="h-[min(40vh,28rem)] min-h-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidoSggFacilityNameSearchForm;
|
||||
@@ -14,7 +14,7 @@ const SplitDataGrid = ({
|
||||
enableSort = false,
|
||||
}: SplitDataGridProps) => {
|
||||
return (
|
||||
<div className="flex gap-[0.6rem] mx-[0.6rem]">
|
||||
<div className="flex gap-[1.2rem]">
|
||||
<CompactDataGrid
|
||||
columnInfo={leftColumnInfo}
|
||||
fixedColumns={leftFixedColumns}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useTabStore } from "@/store/tab-store"
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components";
|
||||
import { Button } from "@common/components";
|
||||
import SplitDataGrid from "./SplitDataGrid";
|
||||
import GridTopActionButton from "./GridTopActionButton";
|
||||
|
||||
// RealGrid 타입 확장
|
||||
declare module "realgrid" {
|
||||
@@ -214,7 +215,20 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
|
||||
};
|
||||
}
|
||||
|
||||
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange, getGridDataRef, enableSort = false }: TabDataGridProps) => {
|
||||
const TabDataGrid = ({
|
||||
columnInfo,
|
||||
isExcelImport = true,
|
||||
isExcelExport = true,
|
||||
rows = [],
|
||||
showHeaderCheckbox = false,
|
||||
rowDataMap,
|
||||
setRowDataMap,
|
||||
activeTab: activeTabProp,
|
||||
onActiveTabChange,
|
||||
getGridDataRef,
|
||||
enableSort = false,
|
||||
facilitySearchStatsYr,
|
||||
}: TabDataGridProps) => {
|
||||
// 각 탭별로 그리드 ref와 데이터를 관리
|
||||
const gridRefs = useRef<Map<string, RealGridReact>>(new Map());
|
||||
const activeTabRef = useRef<string>("");
|
||||
@@ -738,6 +752,11 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
if (info.isSplitGrid && info.left && info.right) {
|
||||
const leftRowData = rowDataMap.get(info.left.tabKey || `${tabKey}_left`) || [];
|
||||
const rightRowData = rowDataMap.get(info.right.tabKey || `${tabKey}_right`) || [];
|
||||
const splitTopButton = info.right.topButton;
|
||||
const showSplitTopButton =
|
||||
Boolean(splitTopButton?.text) &&
|
||||
(splitTopButton?.visible ?? true) &&
|
||||
isExcelImport;
|
||||
|
||||
// ColumnInfoList를 ColumnInfo로 변환
|
||||
const leftColumnInfo: ColumnInfo = {
|
||||
@@ -751,6 +770,15 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mb-[0.6rem]">
|
||||
{showSplitTopButton && (
|
||||
<GridTopActionButton
|
||||
text={splitTopButton?.text ?? ""}
|
||||
onClick={splitTopButton?.onClick}
|
||||
statsYr={facilitySearchStatsYr ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={leftColumnInfo}
|
||||
rightColumnInfo={rightColumnInfo}
|
||||
|
||||
Reference in New Issue
Block a user