feat: 행정동인구 탭 추가
This commit is contained in:
@@ -625,6 +625,7 @@ const DataGrid = ({
|
||||
onCellEdited,
|
||||
onRowClick,
|
||||
onDataCellClick,
|
||||
onCellDblClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
onScrollToBottom,
|
||||
@@ -633,6 +634,7 @@ const DataGrid = ({
|
||||
idFieldName = "id",
|
||||
enableSort = false,
|
||||
gridHeight = 55, // vh 단위
|
||||
onRowDblClick,
|
||||
headerHeight = 60,
|
||||
fitStyle = GridFitStyle.EVEN,
|
||||
}: DataGridProps) => {
|
||||
@@ -671,6 +673,7 @@ const DataGrid = ({
|
||||
onCellEditedRef.current = onCellEdited;
|
||||
onDataCellClickRef.current = onDataCellClick;
|
||||
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
if (!gridRef.current?.gridView || highlightedRowId == null) return;
|
||||
@@ -686,7 +689,27 @@ const DataGrid = ({
|
||||
gridView.setFocus();
|
||||
}
|
||||
}, [highlightedRowId, rowData, idFieldName]);
|
||||
// 행 더블클릭 이벤트
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || !onCellDblClick) return;
|
||||
|
||||
(gridView as any).onCellDblClicked = (_grid: unknown, clickData: { cellType?: string; itemIndex?: number }) => {
|
||||
if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return;
|
||||
const row = rowDataRef.current[clickData.itemIndex];
|
||||
const cellIndex = (gridView as any).getCurrent?.();
|
||||
const fieldName = cellIndex?.fieldName ?? cellIndex?.column ?? "";
|
||||
if (row) onCellDblClick({ // ← 여기는 Props 콜백 호출이라 그대로
|
||||
row: row as Record<string, string | number>,
|
||||
fieldName,
|
||||
itemIndex: clickData.itemIndex
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
(gridView as any).onCellDblClicked = undefined;
|
||||
};
|
||||
}, [onCellDblClick]);
|
||||
useLayoutEffect(() => {
|
||||
if (!cellStyleCallback || !gridRef.current?.gridView) return;
|
||||
gridRef.current.gridView.refresh();
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Button } from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import {
|
||||
AdmdstRenewalReducedColumnConfig,
|
||||
AdmdstRenewalAddedColumnConfig,
|
||||
} from "@/components/util/column/management/AdmdstRenewalColumn";
|
||||
|
||||
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
|
||||
import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
|
||||
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
|
||||
import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types";
|
||||
|
||||
const reducedColumnInfo: ColumnInfo = {
|
||||
columns: AdmdstRenewalReducedColumnConfig.columns ?? [],
|
||||
columnLayout: AdmdstRenewalReducedColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const addedColumnInfo: ColumnInfo = {
|
||||
columns: AdmdstRenewalAddedColumnConfig.columns ?? [],
|
||||
columnLayout: AdmdstRenewalAddedColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tab: ReturnType<typeof useAdmdstRenewalTab>;
|
||||
statsYr: string;
|
||||
};
|
||||
|
||||
const AdmdstRenewalTab = ({ tab, statsYr }: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[1.1rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
감축된 행정구역(범위:동읍면) 자료
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-[3.4rem]" />
|
||||
<DataGrid
|
||||
columnInfo={reducedColumnInfo}
|
||||
rowStartNum={AdmdstRenewalReducedColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="감축된행정구역"
|
||||
rowData={asGridRowData(tab.reducedRowData)}
|
||||
setRowData={asGridRowSetter(tab.setReducedRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={AdmdstRenewalReducedColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[0.4rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
추가된 행정구역 자료
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={tab.isLoading || (tab.reducedRowData.length === 0 && tab.addedRowData.length === 0)}
|
||||
onClick={() => void tab.handleFinal(statsYr)}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
|
||||
>
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={addedColumnInfo}
|
||||
rowStartNum={AdmdstRenewalAddedColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="추가된행정구역"
|
||||
rowData={asGridRowData(tab.addedRowData)}
|
||||
setRowData={asGridRowSetter(tab.setAddedRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={AdmdstRenewalAddedColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
onCellDblClick={({ row, fieldName }) => {
|
||||
if (fieldName !== "lawCd") return; // lawCd 셀만!
|
||||
void tab.handleRowClick(fromGridRow<AdmdstRenewalAddedItem>(row), statsYr);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UpperCodeSelectDialog
|
||||
open={tab.dialogOpen}
|
||||
onOpenChange={tab.setDialogOpen}
|
||||
candidates={tab.candidates}
|
||||
codeField="lawCd"
|
||||
nameField="lawNm"
|
||||
extraFields={[
|
||||
{ field: "rgnCd", header: "지역코드" },
|
||||
{ field: "lv", header: "레벨" },
|
||||
]}
|
||||
onSelect={tab.handleCodeSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdmdstRenewalTab;
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Button } from "@common/components";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import {
|
||||
DomesticPopulationLeftColumnConfig,
|
||||
DomesticPopulationRightColumnConfig,
|
||||
} from "@/components/util/column/management/DomesticPopulationColumn";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
|
||||
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
|
||||
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
|
||||
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
|
||||
|
||||
const leftColumnInfo: ColumnInfo = {
|
||||
columns: DomesticPopulationLeftColumnConfig.columns ?? [],
|
||||
columnLayout: DomesticPopulationLeftColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const rightColumnInfo: ColumnInfo = {
|
||||
columns: DomesticPopulationRightColumnConfig.columns ?? [],
|
||||
columnLayout: DomesticPopulationRightColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
|
||||
type DomesticTab = ReturnType<typeof usePopulationMappingTab<DomesticPopulationItem>>;
|
||||
|
||||
type Props = {
|
||||
tab: DomesticTab;
|
||||
statsYr: string;
|
||||
onFileButtonClick: () => void;
|
||||
};
|
||||
|
||||
const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
|
||||
const rightGridRef = useRef<RealGridReact | null>(null);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[0.4rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
엑셀 업로드 자료 (내국인 전체 리스트)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end pr-2 mb-[0.4rem]">
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={onFileButtonClick}
|
||||
className="mt-[0.5rem] px-[3rem] py-[0.8rem] text-[1.4rem]"
|
||||
>
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={leftColumnInfo}
|
||||
rowStartNum={DomesticPopulationLeftColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="내국인전체리스트"
|
||||
rowData={asGridRowData(tab.leftRowData)}
|
||||
setRowData={asGridRowSetter(tab.setLeftRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={DomesticPopulationLeftColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[0.4rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
미매핑 리스트 (작년 대비)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={tab.isLoading || tab.rightRowData.length === 0}
|
||||
onClick={() => void tab.handleFinal(statsYr)}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
|
||||
>
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={rightColumnInfo}
|
||||
rowStartNum={DomesticPopulationRightColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="미매핑리스트"
|
||||
rowData={asGridRowData(tab.rightRowData)}
|
||||
setRowData={asGridRowSetter(tab.setRightRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={DomesticPopulationRightColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
getGridRef={rightGridRef} // ← 추가
|
||||
onDataCellClick={({ row, fieldName }) => {
|
||||
if (fieldName === "mpinCd") return;
|
||||
if (fieldName === "admdstCd") {
|
||||
(rightGridRef.current?.gridView as any)?.commit?.(); // ← commit 먼저
|
||||
setTimeout(() => {
|
||||
tab.setRightRowData((prev) =>
|
||||
prev.map((r) =>
|
||||
r.admdstCd === row.admdstCd
|
||||
? { ...r, mpinCd: String(row.admdstCd ?? "") }
|
||||
: r,
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
onCellDblClick={({ row, fieldName }) => {
|
||||
if (fieldName !== "mpinCd") return;
|
||||
void tab.handleRightRowClick(fromGridRow<DomesticPopulationItem>(row), statsYr);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UpperCodeSelectDialog
|
||||
open={tab.dialogOpen}
|
||||
onOpenChange={tab.setDialogOpen}
|
||||
candidates={tab.candidates}
|
||||
codeField="admdstCd"
|
||||
nameField="admdstNm"
|
||||
onSelect={tab.handleCodeSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomesticPopulationTab;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Button } from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { useRef } from "react";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
import {
|
||||
ForeignPopulationLeftColumnConfig,
|
||||
ForeignPopulationRightColumnConfig,
|
||||
} from "@/components/util/column/management/ForeignPopulationColumn";
|
||||
|
||||
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
|
||||
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
|
||||
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
|
||||
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
|
||||
|
||||
const leftColumnInfo: ColumnInfo = {
|
||||
columns: ForeignPopulationLeftColumnConfig.columns ?? [],
|
||||
columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const rightColumnInfo: ColumnInfo = {
|
||||
columns: ForeignPopulationRightColumnConfig.columns ?? [],
|
||||
columnLayout: ForeignPopulationRightColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
|
||||
type ForeignTab = ReturnType<typeof usePopulationMappingTab<ForeignPopulationItem>>;
|
||||
|
||||
type Props = {
|
||||
tab: ForeignTab;
|
||||
statsYr: string;
|
||||
onFileButtonClick: () => void;
|
||||
};
|
||||
|
||||
const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
|
||||
const rightGridRef = useRef<RealGridReact | null>(null);
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[0.4rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
엑셀 업로드 자료 (외국인 전체 리스트)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end pr-2 mb-[0.4rem]">
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={onFileButtonClick}
|
||||
className="mt-[0.5rem] px-[3rem] py-[0.8rem] text-[1.4rem]"
|
||||
>
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={leftColumnInfo}
|
||||
rowStartNum={ForeignPopulationLeftColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="외국인전체리스트"
|
||||
rowData={asGridRowData(tab.leftRowData)}
|
||||
setRowData={asGridRowSetter(tab.setLeftRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={ForeignPopulationLeftColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center mb-[0.4rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
미매핑 리스트 (작년 대비)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-end pr-[1rem] mb-[0.4rem]">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={tab.isLoading || tab.rightRowData.length === 0}
|
||||
onClick={() => void tab.handleFinal(statsYr)}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]"
|
||||
>
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columnInfo={rightColumnInfo}
|
||||
rowStartNum={ForeignPopulationRightColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="외국인미매핑리스트"
|
||||
rowData={asGridRowData(tab.rightRowData)}
|
||||
setRowData={asGridRowSetter(tab.setRightRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={ForeignPopulationRightColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
getGridRef={rightGridRef}
|
||||
onDataCellClick={({ row, fieldName }) => {
|
||||
if (fieldName === "mpinCd") return;
|
||||
if (fieldName === "admdstCd") {
|
||||
(rightGridRef.current?.gridView as any)?.commit?.();
|
||||
setTimeout(() => {
|
||||
tab.setRightRowData((prev) =>
|
||||
prev.map((r) =>
|
||||
r.admdstCd === row.admdstCd
|
||||
? { ...r, mpinCd: String(row.admdstCd ?? "") }
|
||||
: r,
|
||||
),
|
||||
);
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
}}
|
||||
onCellDblClick={({ row, fieldName }) => {
|
||||
if (fieldName !== "mpinCd") return;
|
||||
void tab.handleRightRowClick(fromGridRow<ForeignPopulationItem>(row), statsYr);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UpperCodeSelectDialog
|
||||
open={tab.dialogOpen}
|
||||
onOpenChange={tab.setDialogOpen}
|
||||
candidates={tab.candidates}
|
||||
codeField="admdstCd"
|
||||
nameField="admdstNm"
|
||||
onSelect={tab.handleCodeSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForeignPopulationTab;
|
||||
@@ -0,0 +1,42 @@
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { PopulationStatusColumnConfig } from "@/components/util/column/management/PopulationStatusColumn";
|
||||
|
||||
import type { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
|
||||
import { asGridRowData, asGridRowSetter } from "@/components/statistics/management/lib/gridCast";
|
||||
|
||||
const columnInfo: ColumnInfo = {
|
||||
columns: PopulationStatusColumnConfig.columns ?? [],
|
||||
columnLayout: PopulationStatusColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tab: ReturnType<typeof usePopulationStatusTab>;
|
||||
};
|
||||
|
||||
const PopulationStatusTab = ({ tab }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center mb-[1rem]">
|
||||
<span className="text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500">
|
||||
행정 인구 현황
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowStartNum={PopulationStatusColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="행정인구현황"
|
||||
rowData={asGridRowData(tab.rowData)}
|
||||
setRowData={asGridRowSetter(tab.setRowData)}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={PopulationStatusColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopulationStatusTab;
|
||||
@@ -0,0 +1,22 @@
|
||||
import PopulationStatusChart from "./PopulationStatusChart";
|
||||
import type { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
|
||||
|
||||
type Props = {
|
||||
tab: ReturnType<typeof usePopulationVizTab>;
|
||||
};
|
||||
|
||||
const PopulationVizTab = ({ tab }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<span className="inline-block text-[1.8rem] font-semibold text-gray-700 before:content-['●'] before:mr-1 before:text-blue-500 mb-[1.5rem]">
|
||||
행정인구현황 시각화 (기준연도 ~ 과거 자료(최대 5년) / *단위: 명)
|
||||
</span>
|
||||
|
||||
<div className="w-full h-[77rem]">
|
||||
<PopulationStatusChart chartData={tab.chartData} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopulationVizTab;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,25 +7,30 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
||||
|
||||
interface UpperCodeSelectDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
candidates: UpperCodeCandidateItem[];
|
||||
onSelect: (item: UpperCodeCandidateItem) => void;
|
||||
candidates: Record<string, string>[];
|
||||
codeField: string;
|
||||
nameField: string;
|
||||
onSelect: (item: Record<string, string>) => void;
|
||||
extraFields?: { field: string; header: string }[];
|
||||
}
|
||||
|
||||
export const UpperCodeSelectDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
candidates,
|
||||
codeField,
|
||||
nameField,
|
||||
extraFields,
|
||||
onSelect,
|
||||
}: UpperCodeSelectDialogProps) => {
|
||||
const [selectedCode, setSelectedCode] = useState<string | null>(null);
|
||||
|
||||
const handleSelect = () => {
|
||||
const item = candidates.find((c) => c.adstCd === selectedCode);
|
||||
const item = candidates.find((c) => c[codeField] === selectedCode);
|
||||
if (!item) return;
|
||||
onSelect(item);
|
||||
onOpenChange(false);
|
||||
@@ -43,56 +48,62 @@ export const UpperCodeSelectDialog = ({
|
||||
<DialogHeader>
|
||||
<DialogTitle>상위 코드 리스트</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="border border-gray-300 rounded overflow-hidden">
|
||||
<table className="w-full text-[1.3rem]">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="bg-blue-600 text-white">
|
||||
<th className="py-[0.8rem] px-[1rem] text-center font-semibold border-r border-blue-500 w-[40%]">
|
||||
코드
|
||||
</th>
|
||||
<th className="py-[0.8rem] px-[1rem] text-center font-semibold">
|
||||
동명
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
{/* 스크롤 영역 분리 */}
|
||||
<div className="max-h-[40rem] overflow-y-auto">
|
||||
<table className="w-full text-[1.3rem]">
|
||||
<tbody>
|
||||
{candidates.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={2} className="py-[1rem] text-center text-gray-400">
|
||||
후보 코드가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
candidates.map((item) => (
|
||||
<tr
|
||||
key={item.adstCd}
|
||||
onClick={() => setSelectedCode(item.adstCd)}
|
||||
className={`cursor-pointer border-b border-gray-200 transition-colors
|
||||
${selectedCode === item.adstCd
|
||||
? "bg-blue-100"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<td className="py-[0.7rem] px-[1rem] text-center border-r border-gray-200 w-[40%]">
|
||||
{item.adstCd}
|
||||
</td>
|
||||
<td className="py-[0.7rem] px-[1rem] text-center">
|
||||
{item.adstDstrNm}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-300 rounded overflow-hidden">
|
||||
<table className="w-full text-[1.3rem]">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="bg-blue-600 text-white">
|
||||
<th className="py-[0.8rem] px-[1rem] text-center font-semibold border-r border-blue-500 w-[40%]">
|
||||
코드
|
||||
</th>
|
||||
<th className="py-[0.8rem] px-[1rem] text-center font-semibold">
|
||||
동명
|
||||
</th>
|
||||
{extraFields?.map((f) => (
|
||||
<th key={f.field} className="py-[0.8rem] px-[1rem] text-center font-semibold border-l border-blue-500">
|
||||
{f.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div className="max-h-[40rem] overflow-y-auto">
|
||||
<table className="w-full text-[1.3rem]">
|
||||
<tbody>
|
||||
{candidates.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={2} className="py-[1rem] text-center text-gray-400">
|
||||
후보 코드가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
candidates.map((item,index) => (
|
||||
<tr
|
||||
key={`${item[codeField]}-${index}`} // ← index 추가
|
||||
onClick={() => setSelectedCode(item[codeField] ?? "")}
|
||||
className={`cursor-pointer border-b border-gray-200 transition-colors
|
||||
${selectedCode === item[codeField]
|
||||
? "bg-blue-100"
|
||||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<td className="py-[0.7rem] px-[1rem] text-center border-r border-gray-200 w-[40%]">
|
||||
{item[codeField]}
|
||||
</td>
|
||||
<td className="py-[0.7rem] px-[1rem] text-center">
|
||||
{item[nameField]}
|
||||
</td>
|
||||
{extraFields?.map((f) => (
|
||||
<td key={f.field} className="py-[0.7rem] px-[1rem] text-center border-l border-gray-200">
|
||||
{item[f.field]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
// TODO: 원본 파일은 `../../../../../../packages/common/components/src/api` (6단계 상위) 였음.
|
||||
// 이 훅 파일은 원본 컴포넌트보다 한 단계 더 깊은 폴더(hooks/)에 위치하므로
|
||||
// 실제 프로젝트에 옮길 때 상대경로 깊이를 다시 맞춰줘야 함 (혹은 @common/components에 getUserInfo가
|
||||
// re-export 되어 있다면 그쪽으로 교체).
|
||||
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
|
||||
|
||||
import { useAsyncAction } from "./useAsyncAction";
|
||||
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
||||
import type {
|
||||
AdmdstRenewalAddedItem,
|
||||
AdmdstRenewalAddedSaveItem,
|
||||
AdmdstRenewalReducedItem,
|
||||
AdmdstRenewalReducedSaveItem,
|
||||
} from "@/components/statistics/management/types/population-tab-types";
|
||||
|
||||
export function useAdmdstRenewalTab() {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { isLoading, run } = useAsyncAction(alert);
|
||||
|
||||
const [reducedRowData, setReducedRowData] = useState<AdmdstRenewalReducedItem[]>([]);
|
||||
const [addedRowData, setAddedRowData] = useState<AdmdstRenewalAddedItem[]>([]);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [candidates, setCandidates] = useState<UpperCodeCandidateItem[]>([]);
|
||||
const selectedRowIndexRef = useRef<number | null>(null);
|
||||
|
||||
/** OPT_1: "1" = 추가된 행정구역, "2" = 감축된 행정구역 (원본 그대로) */
|
||||
const loadAll = async (statsYr: string): Promise<boolean> => {
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
|
||||
const result = await run(async () => {
|
||||
const [addedRes, reducedRes] = await Promise.all([
|
||||
statisticsPpltnApi.searchChangedArea(statsYr, "1"),
|
||||
statisticsPpltnApi.searchChangedArea(statsYr, "2"),
|
||||
]);
|
||||
if (addedRes.status === "success") {
|
||||
setAddedRowData(toGridRows((addedRes.data as object[]) ?? []) as unknown as AdmdstRenewalAddedItem[]);
|
||||
}
|
||||
if (reducedRes.status === "success") {
|
||||
setReducedRowData(toGridRows((reducedRes.data as object[]) ?? []) as unknown as AdmdstRenewalReducedItem[]);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
};
|
||||
|
||||
/** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */
|
||||
const loadOnActivate = async (statsYr: string) => {
|
||||
if (reducedRowData.length === 0 && addedRowData.length === 0) await loadAll(statsYr);
|
||||
};
|
||||
|
||||
const handleFinal = async (statsYr: string) => {
|
||||
if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; }
|
||||
if (reducedRowData.length === 0 && addedRowData.length === 0) {
|
||||
alert("반영할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
await run(async () => {
|
||||
const userInfo = await getUserInfo();
|
||||
const addedList: AdmdstRenewalAddedSaveItem[] = addedRowData
|
||||
.filter((row) => {
|
||||
const lawCd = String(row.lawCd ?? "").trim();
|
||||
return lawCd !== "" && lawCd !== "null";
|
||||
})
|
||||
.map((row) => ({
|
||||
adstCd: String(row.adstCd ?? ""),
|
||||
lawCd: String(row.lawCd ?? ""),
|
||||
adstDstrNm: String(row.adstDstrNm ?? ""),
|
||||
rgnCd: String(row.rgnCd ?? ""),
|
||||
lv: String(row.lv ?? ""),
|
||||
sido: String(row.sido ?? ""),
|
||||
gugun: String(row.gugun ?? ""),
|
||||
adstDong: String(row.adstDong ?? ""),
|
||||
userId: userInfo.userUnqId,
|
||||
}));
|
||||
|
||||
const reducedList: AdmdstRenewalReducedSaveItem[] = reducedRowData.map((row) => ({
|
||||
adstCd: String(row.adstCd ?? ""),
|
||||
}));
|
||||
|
||||
console.log("reducedList:", reducedList);
|
||||
console.log("addedList:", addedList);
|
||||
console.log("reducedList 이상한 것:", reducedList.filter(r => r.adstCd.length !== 10));
|
||||
const res = await statisticsPpltnApi.saveChangedArea(reducedList, addedList);
|
||||
if (res.status === "success") {
|
||||
await loadAll(statsYr);
|
||||
alert("행정구역 감축 및 추가사항이 성공적으로 반영되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "반영 중 오류가 발생했습니다.");
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const handleRowClick = async (rowData: AdmdstRenewalAddedItem, statsYr: string) => {
|
||||
const adstCd = String(rowData.adstCd ?? "");
|
||||
if (!adstCd) { alert("행정동코드가 없습니다."); return; }
|
||||
|
||||
const parts = adstCd.trim().split(/\s+/);
|
||||
const dongNm = parts[parts.length - 1] ?? adstCd;
|
||||
|
||||
await run(
|
||||
async () => {
|
||||
const res = await statisticsPpltnApi.getLawCdsByName(dongNm);
|
||||
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
|
||||
setCandidates(res.data);
|
||||
const rowIndex = addedRowData.findIndex((r) => r.adstCd === rowData.adstCd);
|
||||
console.log("rowData.adstCd:", rowData.adstCd); // ← 여기
|
||||
console.log("rowIndex:", rowIndex); // ← 여기
|
||||
selectedRowIndexRef.current = rowIndex;
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
alert("해당 지역명에 대한 법정동코드 후보가 없습니다.");
|
||||
}
|
||||
},
|
||||
{ errorMessage: "법정동코드 조회 중 오류가 발생했습니다." },
|
||||
);
|
||||
};
|
||||
const handleCodeSelect = (item: Record<string, string>) => {
|
||||
const rowIndex = selectedRowIndexRef.current;
|
||||
if (rowIndex === null) return;
|
||||
setAddedRowData((prev) =>
|
||||
prev.map((row, idx) =>
|
||||
idx === rowIndex
|
||||
? { ...row, lawCd: item["lawCd"] ?? "", lawNm: item["lawNm"] ?? "", rgnCd: item["rgnCd"] ?? "", lv: item["lv"] ?? "" }
|
||||
: row,
|
||||
),
|
||||
);
|
||||
selectedRowIndexRef.current = null;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setReducedRowData([]);
|
||||
setAddedRowData([]);
|
||||
setDialogOpen(false);
|
||||
setCandidates([]);
|
||||
selectedRowIndexRef.current = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
reducedRowData,
|
||||
addedRowData,
|
||||
setReducedRowData,
|
||||
setAddedRowData,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
candidates,
|
||||
loadAll,
|
||||
loadOnActivate,
|
||||
handleFinal,
|
||||
handleRowClick,
|
||||
handleCodeSelect,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
type AsyncActionOptions = {
|
||||
/** onError가 없을 때 사용할 기본 에러 메시지 */
|
||||
errorMessage?: string;
|
||||
/** 커스텀 에러 처리가 필요할 때 - 지정하면 errorMessage 대신 이 콜백이 호출됨 */
|
||||
onError?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 탭별 hook(usePopulationMappingTab, useAdmdstRenewalTab 등)에서
|
||||
* 반복되던 `setIsLoading(true) → try → catch(alert) → finally(setIsLoading(false))`
|
||||
* 패턴을 공통화한 훅.
|
||||
*
|
||||
* notify: 실패 시 사용자에게 보여줄 알럿 함수 (useAlertStore의 setMessage를 그대로 넘기면 됨)
|
||||
*/
|
||||
export function useAsyncAction(notify: (message: string) => void) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const run = useCallback(
|
||||
async <T>(action: () => Promise<T>, options: AsyncActionOptions = {}): Promise<T | undefined> => {
|
||||
const { errorMessage = "서버 연결 중 오류가 발생했습니다.", onError } = options;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
return await action();
|
||||
} catch (error) {
|
||||
if (onError) {
|
||||
onError(error);
|
||||
} else {
|
||||
notify(errorMessage);
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[notify],
|
||||
);
|
||||
|
||||
return { isLoading, run };
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { useRef, useState } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import { useAsyncAction } from "./useAsyncAction";
|
||||
import type {
|
||||
PopulationMappingApiConfig,
|
||||
PopulationMappingRow,
|
||||
UpperCodeCandidateItem,
|
||||
} from "@/components/statistics/management/types/population-tab-types";
|
||||
|
||||
/* ── 엑셀 헤더 검증 (원본 컴포넌트의 validateExcelHeader를 그대로 이동) ── */
|
||||
const validateExcelHeader = (file: File, requiredKeywords: string[]): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = e.target?.result;
|
||||
const workbook = XLSX.read(data, { type: "binary" });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
if (!sheetName) { resolve(false); return; }
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
if (!sheet) { resolve(false); return; }
|
||||
const rows = XLSX.utils.sheet_to_json<string[]>(sheet, { header: 1 });
|
||||
const headerRow = (rows[0] as string[]).map((h) => String(h ?? ""));
|
||||
const isValid = requiredKeywords.every((kw) => headerRow.some((h) => h.includes(kw)));
|
||||
resolve(isValid);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 내국인/외국인 탭이 공유하는 "좌측 전체 리스트 + 우측 미매핑 리스트" 패턴 훅.
|
||||
* 두 탭은 API 함수와 필드명만 다를 뿐 동작이 완전히 동일해서 config로 주입받는다.
|
||||
*/
|
||||
export function usePopulationMappingTab<TRow extends PopulationMappingRow>(
|
||||
config: PopulationMappingApiConfig<TRow>,
|
||||
) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { isLoading, run } = useAsyncAction(alert);
|
||||
|
||||
const [leftRowData, setLeftRowData] = useState<TRow[]>([]);
|
||||
const [rightRowData, setRightRowData] = useState<TRow[]>([]);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [candidates, setCandidates] = useState<UpperCodeCandidateItem[]>([]);
|
||||
const selectedRowIndexRef = useRef<number | null>(null);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const loadLeft = async (statsYr: string) => {
|
||||
const res = await config.getList(statsYr, "1");
|
||||
if (res.status === "success") setLeftRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]);
|
||||
};
|
||||
|
||||
const loadRight = async (statsYr: string) => {
|
||||
const res = await config.getList(statsYr, "2");
|
||||
if (res.status === "success") setRightRowData(toGridRows((res.data as object[]) ?? []) as unknown as TRow[]);
|
||||
};
|
||||
|
||||
/** 탭 최초 진입/활성화 시 - 좌/우 모두 갱신 (업로드 직후에도 사용) */
|
||||
const loadAll = async (statsYr: string): Promise<boolean> => {
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
|
||||
const result = await run(async () => {
|
||||
await loadLeft(statsYr);
|
||||
await loadRight(statsYr);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 검색 버튼 클릭 시 - 좌측은 항상 갱신하고, 우측은 비어있을 때만 갱신한다.
|
||||
* (사용자가 우측에서 수동으로 매핑한 값을 재검색 시 날려버리지 않기 위함 - 원본 동작 그대로 유지)
|
||||
*/
|
||||
const search = async (statsYr: string): Promise<boolean> => {
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
|
||||
const result = await run(async () => {
|
||||
await loadLeft(statsYr);
|
||||
if (rightRowData.length === 0) await loadRight(statsYr);
|
||||
return true;
|
||||
});
|
||||
return result ?? false;
|
||||
};
|
||||
|
||||
/** 탭 전환 시 - 아직 한 번도 로드되지 않았을 때만 로드 */
|
||||
const loadOnActivate = async (statsYr: string) => {
|
||||
if (leftRowData.length === 0) await loadAll(statsYr);
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>, statsYr: string) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const isValidExt = [".xlsx", ".xls", ".csv"].some((ext) => file.name.toLowerCase().endsWith(ext));
|
||||
if (!isValidExt) { alert("xlsx, xls, csv 파일만 업로드 가능합니다."); e.target.value = ""; return; }
|
||||
|
||||
const isValidHeader = await validateExcelHeader(file, config.requiredHeaderKeywords);
|
||||
if (!isValidHeader) { alert(config.invalidFileMessage); e.target.value = ""; return; }
|
||||
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); e.target.value = ""; return; }
|
||||
|
||||
await run(async () => {
|
||||
const res = await config.uploadExcel(file, statsYr);
|
||||
if (res.status === "success") {
|
||||
await loadAll(statsYr);
|
||||
alert("업로드가 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "업로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
});
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleFinal = async (statsYr: string) => {
|
||||
if (!statsYr) { alert("먼저 조회를 진행해주세요."); return; }
|
||||
if (rightRowData.length === 0) { alert("반영할 데이터가 없습니다."); return; }
|
||||
|
||||
await run(async () => {
|
||||
const regList = rightRowData
|
||||
.filter((row) => String(row.mpinCd ?? "").trim() !== "")
|
||||
.map((row) => ({
|
||||
statsYr,
|
||||
admdstCd: String(row.admdstCd ?? ""),
|
||||
admdstNm: String(row.admdstNm ?? ""),
|
||||
subAdmdstCd: String(row.mpinCd ?? ""),
|
||||
}));
|
||||
|
||||
const res = await config.saveFinal(statsYr, regList);
|
||||
if (res.status === "success") {
|
||||
await loadLeft(statsYr);
|
||||
alert("반영이 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "반영 중 오류가 발생했습니다.");
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleRightRowClick = async (rowData: TRow, statsYr: string) => {
|
||||
const admdstCd = String(rowData.admdstCd ?? "");
|
||||
if (!admdstCd) { alert("행정구역코드가 없습니다."); return; }
|
||||
|
||||
await run(
|
||||
async () => {
|
||||
const res = await config.getUpperCodesByName(statsYr, "", admdstCd); // ← admdstCd 추가
|
||||
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
|
||||
setCandidates(res.data);
|
||||
const rowIndex = rightRowData.findIndex((r) => r.admdstCd === rowData.admdstCd);
|
||||
selectedRowIndexRef.current = rowIndex;
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
alert("해당 지역명에 대한 상위 코드 후보가 없습니다.");
|
||||
}
|
||||
},
|
||||
{ errorMessage: "상위 코드 조회 중 오류가 발생했습니다." },
|
||||
);
|
||||
};
|
||||
|
||||
const handleCodeSelect = (item: Record<string, string>) => {
|
||||
const rowIndex = selectedRowIndexRef.current;
|
||||
if (rowIndex === null) return;
|
||||
setRightRowData((prev) =>
|
||||
prev.map((row, idx) => (idx === rowIndex ? { ...row, mpinCd: item["admdstCd"] ?? "" } : row)),
|
||||
);
|
||||
selectedRowIndexRef.current = null;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setLeftRowData([]);
|
||||
setRightRowData([]);
|
||||
setDialogOpen(false);
|
||||
setCandidates([]);
|
||||
selectedRowIndexRef.current = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
leftRowData,
|
||||
rightRowData,
|
||||
setLeftRowData,
|
||||
setRightRowData,
|
||||
fileInputRef,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
candidates,
|
||||
loadAll,
|
||||
search,
|
||||
loadOnActivate,
|
||||
handleFileChange,
|
||||
handleFinal,
|
||||
handleRightRowClick,
|
||||
handleCodeSelect,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import { useAsyncAction } from "./useAsyncAction";
|
||||
import type { PopulationStatusItem } from "../types/population-tab-types";
|
||||
|
||||
export function usePopulationStatusTab() {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { isLoading, run } = useAsyncAction(alert);
|
||||
|
||||
const [rowData, setRowData] = useState<PopulationStatusItem[]>([]);
|
||||
|
||||
const [includeOriginMale, setIncludeOriginMale] = useState(true);
|
||||
const [includeOriginFemale, setIncludeOriginFemale] = useState(true);
|
||||
const [includeForeignMale, setIncludeForeignMale] = useState(true);
|
||||
const [includeForeignFemale, setIncludeForeignFemale] = useState(true);
|
||||
|
||||
const search = async (statsYr: string): Promise<boolean> => {
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
|
||||
if (!includeOriginMale && !includeOriginFemale && !includeForeignMale && !includeForeignFemale) {
|
||||
alert(
|
||||
"체크 박스를 모두 해제하신 상태에서는\n조회가 불가능합니다.\n(최소 1개 이상의 체크 박스 선택 후\n조회를 진행해주세요.)",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await run(async () => {
|
||||
const res = await statisticsPpltnApi.searchPopByEachCondition(
|
||||
statsYr,
|
||||
includeOriginMale,
|
||||
includeOriginFemale,
|
||||
includeForeignMale,
|
||||
includeForeignFemale,
|
||||
);
|
||||
if (res.status === "success") {
|
||||
setRowData(toGridRows((res.data as object[]) ?? []) as unknown as PopulationStatusItem[]);
|
||||
return true;
|
||||
}
|
||||
alert(res.message || "조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
});
|
||||
return result ?? false;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setRowData([]);
|
||||
setIncludeOriginMale(true);
|
||||
setIncludeOriginFemale(true);
|
||||
setIncludeForeignMale(true);
|
||||
setIncludeForeignFemale(true);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
rowData,
|
||||
setRowData,
|
||||
includeOriginMale,
|
||||
setIncludeOriginMale,
|
||||
includeOriginFemale,
|
||||
setIncludeOriginFemale,
|
||||
includeForeignMale,
|
||||
setIncludeForeignMale,
|
||||
includeForeignFemale,
|
||||
setIncludeForeignFemale,
|
||||
search,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
|
||||
|
||||
import { useAsyncAction } from "./useAsyncAction";
|
||||
import type { PopulationChartData } from "../PopulationStatusChart";
|
||||
|
||||
export function usePopulationVizTab() {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { isLoading, run } = useAsyncAction(alert);
|
||||
|
||||
const [year, setYear] = useState(String(new Date().getFullYear() - 1));
|
||||
|
||||
const [includeOriginMale, setIncludeOriginMale] = useState(true);
|
||||
const [includeOriginFemale, setIncludeOriginFemale] = useState(true);
|
||||
const [includeForeignMale, setIncludeForeignMale] = useState(true);
|
||||
const [includeForeignFemale, setIncludeForeignFemale] = useState(true);
|
||||
|
||||
const [chartData, setChartData] = useState<PopulationChartData | null>(null);
|
||||
|
||||
const search = async (statsYr: string): Promise<boolean> => {
|
||||
if (!statsYr) { alert("연도를 선택해주세요."); return false; }
|
||||
setYear(statsYr);
|
||||
|
||||
const result = await run(async () => {
|
||||
const res = await statisticsPpltnApi.getChartDatas1(
|
||||
statsYr,
|
||||
includeOriginMale,
|
||||
includeOriginFemale,
|
||||
includeForeignMale,
|
||||
includeForeignFemale,
|
||||
);
|
||||
if (res.status === "success") {
|
||||
setChartData(res.data as PopulationChartData);
|
||||
return true;
|
||||
}
|
||||
alert(res.message || "조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
});
|
||||
return result ?? false;
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
setIncludeOriginMale(true);
|
||||
setIncludeOriginFemale(true);
|
||||
setIncludeForeignMale(true);
|
||||
setIncludeForeignFemale(true);
|
||||
};
|
||||
|
||||
const deselectAll = () => {
|
||||
setIncludeOriginMale(false);
|
||||
setIncludeOriginFemale(false);
|
||||
setIncludeForeignMale(false);
|
||||
setIncludeForeignFemale(false);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setYear(String(new Date().getFullYear() - 1));
|
||||
setIncludeOriginMale(true);
|
||||
setIncludeOriginFemale(true);
|
||||
setIncludeForeignMale(true);
|
||||
setIncludeForeignFemale(true);
|
||||
setChartData(null);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
year,
|
||||
chartData,
|
||||
includeOriginMale,
|
||||
setIncludeOriginMale,
|
||||
includeOriginFemale,
|
||||
setIncludeOriginFemale,
|
||||
includeForeignMale,
|
||||
setIncludeForeignMale,
|
||||
includeForeignFemale,
|
||||
setIncludeForeignFemale,
|
||||
search,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export type GridRow = Record<string, string | number>;
|
||||
|
||||
/**
|
||||
* DataGrid가 제네릭이 아니라 rowData: Record<string, string|number>[] 를 그대로 요구함
|
||||
* (실제 타입 에러로 확인됨: ts(2322), "missing properties from PopulationStatusItem").
|
||||
*
|
||||
* 훅/탭 내부에서는 명시적 도메인 타입(PopulationStatusItem 등)을 그대로 쓰고,
|
||||
* DataGrid에 넘기는 이 경계에서만 캐스팅한다. 내부 로직의 타입 안정성은 유지하면서
|
||||
* 외부 라이브러리(DataGrid)의 느슨한 타입과의 경계만 명시적으로 표시하는 용도.
|
||||
*/
|
||||
export function asGridRowData<T>(data: T[]): GridRow[] {
|
||||
return data as unknown as GridRow[];
|
||||
}
|
||||
|
||||
export function asGridRowSetter<T>(setter: Dispatch<SetStateAction<T[]>>): Dispatch<SetStateAction<GridRow[]>> {
|
||||
return setter as unknown as Dispatch<SetStateAction<GridRow[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* DataGrid의 onRowClick 등에서 넘어오는 GridRow(Record<string,string|number>)를
|
||||
* 도메인 타입으로 되돌릴 때 사용. `as unknown as T`를 매번 직접 쓰지 않도록 모아둠.
|
||||
*/
|
||||
export function fromGridRow<T>(row: GridRow): T {
|
||||
return row as unknown as T;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
|
||||
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
외국인 인구 관련 타입
|
||||
(내국인 쪽 DomesticPopulationItem과 필드명만 다르고 구조는 동일)
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
export interface ForeignPopulationItem {
|
||||
statsYr: string;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
frgnrWholPpltnCnt: number;
|
||||
frgnrMlPpltnCnt: number;
|
||||
frgnrFmlPpltnCnt: number;
|
||||
/** 미매핑 리스트(우측) 전용 - 매핑 대상 행정구역코드(상위코드) */
|
||||
mpinCd: string;
|
||||
}
|
||||
|
||||
export type ForeignPopulationListResponse = ApiResponse<ForeignPopulationItem[]>;
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
행정동갱신 관련 타입
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
export interface AdmdstRenewalReducedItem {
|
||||
adstDstrNm: string;
|
||||
adstCd: string;
|
||||
}
|
||||
|
||||
export interface AdmdstRenewalAddedItem {
|
||||
adstDstrNm: string;
|
||||
adstCd: string;
|
||||
lawCd: string;
|
||||
lawNm: string;
|
||||
rgnCd?: string;
|
||||
lv?: string;
|
||||
sido?: string; // ← 추가
|
||||
gugun?: string; // ← 추가
|
||||
adstDong?: string; // ← 추가
|
||||
}
|
||||
|
||||
/** 추가된 행정구역 반영 시 서버로 보내는 단건 페이로드 */
|
||||
export interface AdmdstRenewalAddedSaveItem {
|
||||
adstCd: string;
|
||||
lawCd: string;
|
||||
adstDstrNm: string;
|
||||
sido: string;
|
||||
gugun: string;
|
||||
adstDong: string;
|
||||
userId: string;
|
||||
rgnCd: string;
|
||||
lv: string;
|
||||
}
|
||||
|
||||
/** 감축된 행정구역 반영 시 서버로 보내는 단건 페이로드 */
|
||||
export interface AdmdstRenewalReducedSaveItem {
|
||||
adstCd: string;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
행정인구현황 관련 타입
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
export interface PopulationStatusItem {
|
||||
statsYr: string;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
wholPpltnCnt: number;
|
||||
domplWholPpltnCnt: number;
|
||||
domplMlPpltnCnt: number;
|
||||
domplFmlPpltnCnt: number;
|
||||
frgnrWholPpltnCnt: number;
|
||||
frgnrMlPpltnCnt: number;
|
||||
frgnrFmlPpltnCnt: number;
|
||||
}
|
||||
|
||||
export type PopulationStatusListResponse = ApiResponse<PopulationStatusItem[]>;
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
내국인/외국인 공용 매핑 탭(좌:전체 리스트, 우:미매핑 리스트) 공통 타입
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
export type MappingTabKind = "domestic" | "foreign";
|
||||
|
||||
/** 반영(최종 매핑 저장) 요청 시 서버로 보내는 단건 페이로드 - 내국인/외국인 공통 */
|
||||
export interface PopulationMappingRegItem {
|
||||
statsYr: string;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
subAdmdstCd: string;
|
||||
}
|
||||
|
||||
/** 좌/우 그리드 행이 공통으로 가져야 하는 최소 필드 */
|
||||
export interface PopulationMappingRow {
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
/** 우측(미매핑 리스트) 전용 필드. 좌측 데이터에는 존재하지 않을 수 있음 */
|
||||
mpinCd?: string;
|
||||
}
|
||||
|
||||
/** usePopulationMappingTab에 주입하는 API/검증 설정 - 내국인/외국인 탭이 이 설정만 다르게 넘김 */
|
||||
export interface PopulationMappingApiConfig<TRow extends PopulationMappingRow> {
|
||||
kind: MappingTabKind;
|
||||
/** opt1: "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측) */
|
||||
getList: (statsYr: string, opt1: "1" | "2") => Promise<ApiResponse<TRow[]>>;
|
||||
uploadExcel: (file: File, statsYr: string) => Promise<ApiResponse<unknown>>;
|
||||
saveFinal: (statsYr: string, regList: PopulationMappingRegItem[]) => Promise<ApiResponse<unknown>>;
|
||||
getUpperCodesByName: (statsYr: string, admdstNm: string, admdstCd: string) => Promise<ApiResponse<UpperCodeCandidateItem[]>>;
|
||||
/** 업로드된 엑셀 헤더에 반드시 포함되어야 하는 키워드 목록 */
|
||||
requiredHeaderKeywords: string[];
|
||||
/** 헤더 검증 실패 시 노출할 안내 메시지 */
|
||||
invalidFileMessage: string;
|
||||
}
|
||||
|
||||
export type { UpperCodeCandidateItem };
|
||||
@@ -367,6 +367,8 @@ export type DataGridProps = {
|
||||
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
|
||||
getGridRef?: React.MutableRefObject<RealGridReact | null>;
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
onRowDblClick?: (rowData: Record<string, string | number>) => void; // ← 추가
|
||||
onCellDblClick?: DataCellClickCallback;
|
||||
/** 데이터 셀 클릭 시 행 + 클릭한 필드명 전달 (`onRowClick`과 함께 쓸 수 있음) */
|
||||
onDataCellClick?: DataCellClickCallback;
|
||||
/** CheckBar 행 체크 변경 */
|
||||
|
||||
@@ -60,7 +60,7 @@ export const AdmdstRenewalAddedColumnConfig: ColumnInfoList = {
|
||||
header: { text: "법정동코드" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "lawNm",
|
||||
|
||||
@@ -75,7 +75,6 @@ export const DomesticPopulationRightColumnConfig: ColumnInfoList = {
|
||||
tabKey: "DOMESTIC_RIGHT",
|
||||
tabLabel: "미매핑 리스트 (작년 대비)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
@@ -98,8 +97,7 @@ export const DomesticPopulationRightColumnConfig: ColumnInfoList = {
|
||||
fieldName: "mpinCd",
|
||||
header: { text: "맵핑코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "domplWholPpltnCnt",
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
|
||||
tabKey: "FOREIGN_RIGHT",
|
||||
tabLabel: "미매핑 리스트 (작년 대비)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
fixedColumns: 3,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
@@ -84,6 +84,22 @@ export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "sido",
|
||||
fieldName: "sido",
|
||||
header: { text: "시도" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "gugun",
|
||||
fieldName: "gugun",
|
||||
header: { text: "구군" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
@@ -98,8 +114,7 @@ export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
|
||||
fieldName: "mpinCd",
|
||||
header: { text: "맵핑코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
editable: true
|
||||
},
|
||||
{
|
||||
name: "frgnrWholPpltnCnt",
|
||||
@@ -134,6 +149,8 @@ export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
|
||||
],
|
||||
columnLayout: [
|
||||
"admdstCd",
|
||||
"sido",
|
||||
"gugun",
|
||||
"admdstNm",
|
||||
"mpinCd",
|
||||
{
|
||||
|
||||
@@ -66,20 +66,23 @@ export class StatisticsPpltnApiService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 미매핑 행 클릭 시 상위 코드 후보 조회 */
|
||||
|
||||
public async getUpperCodesByName(
|
||||
statsYr: string,
|
||||
admdstNm: string,
|
||||
admdstCd: string
|
||||
): Promise<UpperCodesByNameResponse> {
|
||||
const response = await stClient.post<UpperCodesByNameResponse>(
|
||||
// GET 요청으로 변경하고 params 객체를 추가합니다.
|
||||
const response = await stClient.get<UpperCodesByNameResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/upper-codes-by-name`,
|
||||
{ statsYr, admdstNm },
|
||||
{
|
||||
params: { statsYr, admdstNm,admdstCd }
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ════════════════════════════════════
|
||||
외국인
|
||||
════════════════════════════════════ */
|
||||
@@ -157,18 +160,32 @@ export class StatisticsPpltnApiService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 행정동 갱신 — 법정동코드 후보 조회 (행정동명 클릭 시) */
|
||||
public async getRenewalUpperCodesByName(
|
||||
statsYr: string,
|
||||
admdstNm: string,
|
||||
): Promise<UpperCodesByNameResponse> {
|
||||
const response = await stClient.post<UpperCodesByNameResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/upper-codes-by-name`,
|
||||
{ statsYr, admdstNm },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
// /** 행정동 갱신 — 법정동코드 후보 조회 (행정동명 클릭 시) */
|
||||
// public async getRenewalUpperCodesByName(
|
||||
// statsYr: string,
|
||||
// admdstNm: string,
|
||||
// ): Promise<UpperCodesByNameResponse> {
|
||||
// const response = await stClient.get<UpperCodesByNameResponse>(
|
||||
// `${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/upper-codes-by-name`,
|
||||
// {
|
||||
// params: { statsYr, admdstNm }
|
||||
// },
|
||||
// );
|
||||
// return response.data;
|
||||
// }
|
||||
|
||||
/** 행정동 갱신 — 법정동코드 후보 조회 */
|
||||
public async getLawCdsByName(
|
||||
adstCd : string,
|
||||
): Promise<UpperCodesByNameResponse> {
|
||||
const response = await stClient.get<UpperCodesByNameResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/law-codes-by-name`,
|
||||
{
|
||||
params: { adstCd }
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
/* ════════════════════════════════════
|
||||
행정인구현황
|
||||
════════════════════════════════════ */
|
||||
@@ -181,20 +198,22 @@ export class StatisticsPpltnApiService {
|
||||
fMale: boolean,
|
||||
fFemale: boolean,
|
||||
): Promise<{ status: string; data: object[]; message?: string }> {
|
||||
const response = await stClient.post<{ status: string; data: object[]; message?: string }>(
|
||||
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/pop-by-condition`,
|
||||
{
|
||||
statsYr,
|
||||
oMale: oMale ? "TRUE" : "FALSE",
|
||||
oFemale: oFemale ? "TRUE" : "FALSE",
|
||||
fMale: fMale ? "TRUE" : "FALSE",
|
||||
fFemale: fFemale ? "TRUE" : "FALSE",
|
||||
params: {
|
||||
statsYr,
|
||||
oMale: oMale ? "TRUE" : "FALSE",
|
||||
oFemale: oFemale ? "TRUE" : "FALSE",
|
||||
fMale: fMale ? "TRUE" : "FALSE",
|
||||
fFemale: fFemale ? "TRUE" : "FALSE",
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 행정인구현황 시각화 — 기준연도 기준 최대 5년치 차트 데이터 조회 */
|
||||
/** 행정인구현황 시각화 — 기준연도 기준 최대 5년치 차트 데이터 조회 */
|
||||
public async getChartDatas1(
|
||||
statsYr: string,
|
||||
oMale: boolean,
|
||||
@@ -202,14 +221,16 @@ export class StatisticsPpltnApiService {
|
||||
fMale: boolean,
|
||||
fFemale: boolean,
|
||||
): Promise<ChartDataResponse> {
|
||||
const response = await stClient.post<ChartDataResponse>(
|
||||
const response = await stClient.get<ChartDataResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/chart-datas`,
|
||||
{
|
||||
statsYr,
|
||||
oMale: oMale ? "TRUE" : "FALSE",
|
||||
oFemale: oFemale ? "TRUE" : "FALSE",
|
||||
fMale: fMale ? "TRUE" : "FALSE",
|
||||
fFemale: fFemale ? "TRUE" : "FALSE",
|
||||
params: {
|
||||
statsYr,
|
||||
oMale: oMale ? "TRUE" : "FALSE",
|
||||
oFemale: oFemale ? "TRUE" : "FALSE",
|
||||
fMale: fMale ? "TRUE" : "FALSE",
|
||||
fFemale: fFemale ? "TRUE" : "FALSE",
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
|
||||
+12
-4
@@ -44,18 +44,26 @@ export interface DomesticPopulationMappingItem {
|
||||
subAdmdstCd: string;
|
||||
}
|
||||
|
||||
/** 상위 코드 후보 조회 요청 */
|
||||
/** 내국인/외국인 상위 코드 후보 조회 요청 */
|
||||
export interface UpperCodesByNameParams {
|
||||
statsYr: string;
|
||||
admdstNm: string;
|
||||
}
|
||||
|
||||
/** 상위 코드 후보 단건 아이템 */
|
||||
/** 내국인/외국인 상위 코드 후보 단건 아이템 */
|
||||
export interface UpperCodeCandidateItem {
|
||||
adstCd: string; // 행정구역코드
|
||||
adstDstrNm: string; // 행정구역명
|
||||
admdstCd: string; // 행정동코드
|
||||
admdstNm: string; // 행정동명
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
/** 행정동 갱신 법정동코드 후보 단건 아이템 */
|
||||
export interface LawCodeCandidateItem {
|
||||
lawCd: string; // 법정동코드
|
||||
lawNm: string; // 법정동명
|
||||
[key: string]: string;
|
||||
|
||||
}
|
||||
/* ── API 응답 타입 ── */
|
||||
export type DomesticPopulationListResponse = ApiResponse<DomesticPopulationItem[]>;
|
||||
export type DomesticPopulationFinalResponse = ApiResponse<string>;
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface cancelSubmitResponse {
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
status: string;
|
||||
data: T;
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user