feat: 행정동인구 탭 추가

This commit is contained in:
yangsein
2026-06-30 14:36:25 +09:00
parent 82ab1376cc
commit b15cbeb568
22 changed files with 1486 additions and 852 deletions
@@ -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;
@@ -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;
}