feat: 통계인구관리 내국인/외국인/행정동갱신 탭 구현"
This commit is contained in:
@@ -1,11 +1,696 @@
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { useRef, useState } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import {
|
||||
DomesticPopulationLeftColumnConfig,
|
||||
DomesticPopulationRightColumnConfig,
|
||||
} from "@/components/util/column/management/DomesticPopulationColumn";
|
||||
import {
|
||||
ForeignPopulationLeftColumnConfig,
|
||||
ForeignPopulationRightColumnConfig,
|
||||
} from "@/components/util/column/management/ForeignPopulationColumn";
|
||||
import {
|
||||
AdmdstRenewalReducedColumnConfig,
|
||||
AdmdstRenewalAddedColumnConfig,
|
||||
} from "@/components/util/column/management/AdmdstRenewalColumn";
|
||||
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
||||
import { UpperCodeSelectDialog } from "./UpperCodeSelectDialog";
|
||||
|
||||
const TAB_LIST = [
|
||||
{ key: "domestic", label: "행정인구등록" },
|
||||
{ key: "foreign", label: "외국인인구등록" },
|
||||
{ key: "renewal", label: "행정동갱신" },
|
||||
{ key: "status", label: "행정인구현황" },
|
||||
{ key: "statusViz", label: "행정인구현황 시각화" },
|
||||
];
|
||||
|
||||
/* ── 컬럼 설정 → ColumnInfo 변환 ── */
|
||||
const domesticLeftColumnInfo: ColumnInfo = {
|
||||
columns: DomesticPopulationLeftColumnConfig.columns ?? [],
|
||||
columnLayout: DomesticPopulationLeftColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const domesticRightColumnInfo: ColumnInfo = {
|
||||
columns: DomesticPopulationRightColumnConfig.columns ?? [],
|
||||
columnLayout: DomesticPopulationRightColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const foreignLeftColumnInfo: ColumnInfo = {
|
||||
columns: ForeignPopulationLeftColumnConfig.columns ?? [],
|
||||
columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const foreignRightColumnInfo: ColumnInfo = {
|
||||
columns: ForeignPopulationRightColumnConfig.columns ?? [],
|
||||
columnLayout: ForeignPopulationRightColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const renewalReducedColumnInfo: ColumnInfo = {
|
||||
columns: AdmdstRenewalReducedColumnConfig.columns ?? [],
|
||||
columnLayout: AdmdstRenewalReducedColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
const renewalAddedColumnInfo: ColumnInfo = {
|
||||
columns: AdmdstRenewalAddedColumnConfig.columns ?? [],
|
||||
columnLayout: AdmdstRenewalAddedColumnConfig.columnLayout ?? [],
|
||||
};
|
||||
|
||||
/* ── 엑셀 헤더 검증 ── */
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════
|
||||
컴포넌트
|
||||
══════════════════════════════════════════ */
|
||||
const StatisticsPopulationManagement = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [activeTab, setActiveTab] = useState("domestic");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/* ── 내국인 데이터 ── */
|
||||
const [domesticLeftRowData, setDomesticLeftRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [domesticRightRowData, setDomesticRightRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
/* ── 외국인 데이터 ── */
|
||||
const [foreignLeftRowData, setForeignLeftRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [foreignRightRowData, setForeignRightRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
/* ── 행정동갱신 데이터 ── */
|
||||
const [renewalReducedRowData, setRenewalReducedRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [renewalAddedRowData, setRenewalAddedRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
/* ── 상위 코드 다이얼로그 ── */
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [candidates, setCandidates] = useState<UpperCodeCandidateItem[]>([]);
|
||||
const selectedRowIndexRef = useRef<number | null>(null);
|
||||
const selectedTabRef = useRef<"domestic" | "foreign" | "renewal">("domestic");
|
||||
|
||||
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
||||
const domesticFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const foreignFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
/* ════════════════ 내국인 ════════════════ */
|
||||
const loadDomesticData = async (values: SearchFormValues, reloadRight = true): Promise<boolean> => {
|
||||
const yr = values.year ?? "";
|
||||
if (!yr) { alert("연도를 선택해주세요."); return false; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const leftRes = await statisticsPpltnApi.getDomesticPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setDomesticLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
if (reloadRight) {
|
||||
const rightRes = await statisticsPpltnApi.getDomesticPopulationList(yr, "2");
|
||||
if (rightRes.status === "success") setDomesticRightRowData(toGridRows((rightRes.data as object[]) ?? []));
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDomesticFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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, ["행정기관코드", "행정기관"]);
|
||||
if (!isValidHeader) { alert("행정인구(내국인) 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요."); e.target.value = ""; return; }
|
||||
const yr = lastSearchedValuesRef.current?.year ?? "";
|
||||
if (!yr) { alert("연도를 선택해주세요."); e.target.value = ""; return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsPpltnApi.uploadDomesticPopulationExcel(file, yr);
|
||||
if (res.status === "success") {
|
||||
await loadDomesticData(lastSearchedValuesRef.current!, true);
|
||||
alert("업로드가 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "업로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("업로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDomesticFinal = async () => {
|
||||
const values = lastSearchedValuesRef.current;
|
||||
const yr = values?.year ?? "";
|
||||
if (!yr) { alert("먼저 조회를 진행해주세요."); return; }
|
||||
if (domesticRightRowData.length === 0) { alert("반영할 데이터가 없습니다."); return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const regList = domesticRightRowData
|
||||
.filter((row) => String(row.mpinCd ?? "").trim() !== "")
|
||||
.map((row) => ({
|
||||
statsYr: yr,
|
||||
admdstCd: String(row.admdstCd ?? ""),
|
||||
admdstNm: String(row.admdstNm ?? ""),
|
||||
subAdmdstCd: String(row.mpinCd ?? ""),
|
||||
}));
|
||||
const res = await statisticsPpltnApi.saveDomesticPopulationFinal(yr, regList);
|
||||
if (res.status === "success") {
|
||||
const leftRes = await statisticsPpltnApi.getDomesticPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setDomesticLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
alert("반영이 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "반영 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("반영 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ════════════════ 외국인 ════════════════ */
|
||||
const loadForeignData = async (values: SearchFormValues, reloadRight = true): Promise<boolean> => {
|
||||
const yr = values.year ?? "";
|
||||
if (!yr) { alert("연도를 선택해주세요."); return false; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const leftRes = await statisticsPpltnApi.getForeignPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setForeignLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
if (reloadRight) {
|
||||
const rightRes = await statisticsPpltnApi.getForeignPopulationList(yr, "2");
|
||||
if (rightRes.status === "success") setForeignRightRowData(toGridRows((rightRes.data as object[]) ?? []));
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForeignFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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, ["행정동코드", "행정동명", "외국인수"]);
|
||||
if (!isValidHeader) { alert("외국인 인구 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요."); e.target.value = ""; return; }
|
||||
const yr = lastSearchedValuesRef.current?.year ?? "";
|
||||
if (!yr) { alert("연도를 선택해주세요."); e.target.value = ""; return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsPpltnApi.uploadForeignPopulationExcel(file, yr);
|
||||
if (res.status === "success") {
|
||||
await loadForeignData(lastSearchedValuesRef.current!, true);
|
||||
alert("업로드가 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "업로드 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("업로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleForeignFinal = async () => {
|
||||
const values = lastSearchedValuesRef.current;
|
||||
const yr = values?.year ?? "";
|
||||
if (!yr) { alert("먼저 조회를 진행해주세요."); return; }
|
||||
if (foreignRightRowData.length === 0) { alert("반영할 데이터가 없습니다."); return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const regList = foreignRightRowData
|
||||
.filter((row) => String(row.mpinCd ?? "").trim() !== "")
|
||||
.map((row) => ({
|
||||
statsYr: yr,
|
||||
admdstCd: String(row.admdstCd ?? ""),
|
||||
admdstNm: String(row.admdstNm ?? ""),
|
||||
subAdmdstCd: String(row.mpinCd ?? ""),
|
||||
}));
|
||||
const res = await statisticsPpltnApi.saveForeignPopulationFinal(yr, regList);
|
||||
if (res.status === "success") {
|
||||
const leftRes = await statisticsPpltnApi.getForeignPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setForeignLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
alert("반영이 완료되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "반영 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("반영 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ════════════════ 행정동갱신 ════════════════ */
|
||||
const loadRenewalData = async (values: SearchFormValues): Promise<boolean> => {
|
||||
const yr = values.year ?? "";
|
||||
if (!yr) { alert("연도를 선택해주세요."); return false; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// OPT_1: "1" = 추가된 행정구역, "2" = 감축된 행정구역
|
||||
const [addedRes, reducedRes] = await Promise.all([
|
||||
statisticsPpltnApi.searchChangedArea(yr, "1"),
|
||||
statisticsPpltnApi.searchChangedArea(yr, "2"),
|
||||
]);
|
||||
if (addedRes.status === "success") setRenewalAddedRowData(toGridRows((addedRes.data as object[]) ?? []));
|
||||
if (reducedRes.status === "success") setRenewalReducedRowData(toGridRows((reducedRes.data as object[]) ?? []));
|
||||
return true;
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenewalFinal = async () => {
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values?.year) { alert("먼저 조회를 진행해주세요."); return; }
|
||||
if (renewalReducedRowData.length === 0 && renewalAddedRowData.length === 0) {
|
||||
alert("반영할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
// adstDstrNm("시도 구군 동" 형태)을 sido/gugun/adstDong으로 분리
|
||||
const addedList = renewalAddedRowData.map((row) => {
|
||||
const fullNm = String(row.adstDstrNm ?? "").trim();
|
||||
const parts = fullNm.split(/\s+/);
|
||||
const sido = parts[0] ?? "";
|
||||
const gugun = parts.length >= 2 ? parts[1] : "";
|
||||
const adstDong = parts.length >= 3 ? parts.slice(2).join(" ") : "";
|
||||
|
||||
return {
|
||||
adstCd: String(row.adstCd ?? ""),
|
||||
lawCd: String(row.lawCd ?? ""),
|
||||
sido,
|
||||
gugun,
|
||||
adstDong,
|
||||
userId: userInfo.userUnqId,
|
||||
};
|
||||
});
|
||||
|
||||
const reducedList = renewalReducedRowData.map((row) => ({
|
||||
adstCd: String(row.adstCd ?? ""),
|
||||
}));
|
||||
|
||||
const res = await statisticsPpltnApi.saveChangedArea(reducedList, addedList);
|
||||
if (res.status === "success") {
|
||||
await loadRenewalData(values);
|
||||
alert("행정구역 감축 및 추가사항이 성공적으로 반영되었습니다.");
|
||||
} else {
|
||||
alert(res.message || "반영 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("반영 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ════════════════ 공통 ════════════════ */
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
lastSearchedValuesRef.current = values;
|
||||
if (activeTab === "domestic") {
|
||||
const yr = values.year ?? "";
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const leftRes = await statisticsPpltnApi.getDomesticPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setDomesticLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
if (domesticRightRowData.length === 0) {
|
||||
const rightRes = await statisticsPpltnApi.getDomesticPopulationList(yr, "2");
|
||||
if (rightRes.status === "success") setDomesticRightRowData(toGridRows((rightRes.data as object[]) ?? []));
|
||||
}
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else if (activeTab === "foreign") {
|
||||
const yr = values.year ?? "";
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const leftRes = await statisticsPpltnApi.getForeignPopulationList(yr, "1");
|
||||
if (leftRes.status === "success") setForeignLeftRowData(toGridRows((leftRes.data as object[]) ?? []));
|
||||
if (foreignRightRowData.length === 0) {
|
||||
const rightRes = await statisticsPpltnApi.getForeignPopulationList(yr, "2");
|
||||
if (rightRes.status === "success") setForeignRightRowData(toGridRows((rightRes.data as object[]) ?? []));
|
||||
}
|
||||
} catch {
|
||||
alert("서버 연결 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else if (activeTab === "renewal") {
|
||||
await loadRenewalData(values);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setDomesticLeftRowData([]);
|
||||
setDomesticRightRowData([]);
|
||||
setForeignLeftRowData([]);
|
||||
setForeignRightRowData([]);
|
||||
setRenewalReducedRowData([]);
|
||||
setRenewalAddedRowData([]);
|
||||
setActiveTab("domestic");
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
const values = lastSearchedValuesRef.current;
|
||||
if (!values) return;
|
||||
if (tabKey === "domestic" && domesticLeftRowData.length === 0) await loadDomesticData(values);
|
||||
if (tabKey === "foreign" && foreignLeftRowData.length === 0) await loadForeignData(values);
|
||||
if (tabKey === "renewal" && renewalReducedRowData.length === 0 && renewalAddedRowData.length === 0) await loadRenewalData(values);
|
||||
};
|
||||
|
||||
/* ── 우측 행 클릭 → 상위 코드 후보 조회 ── */
|
||||
const handleRightRowClick = async (
|
||||
rowData: Record<string, string | number>,
|
||||
rowIndex: number,
|
||||
tab: "domestic" | "foreign",
|
||||
) => {
|
||||
const yr = lastSearchedValuesRef.current?.year ?? "";
|
||||
const admdstNm = String(rowData.admdstNm ?? "");
|
||||
if (!admdstNm) { alert("행정구역명이 없습니다."); return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsPpltnApi.getUpperCodesByName(yr, admdstNm);
|
||||
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
|
||||
setCandidates(res.data);
|
||||
selectedRowIndexRef.current = rowIndex;
|
||||
selectedTabRef.current = tab;
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
alert("해당 지역명에 대한 상위 코드 후보가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("상위 코드 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ── 행정동갱신 우측 행 클릭 → 법정동코드 후보 조회 ── */
|
||||
const handleRenewalRowClick = async (
|
||||
rowData: Record<string, string | number>,
|
||||
rowIndex: number,
|
||||
) => {
|
||||
const yr = lastSearchedValuesRef.current?.year ?? "";
|
||||
const adstDstrNm = String(rowData.adstDstrNm ?? "");
|
||||
if (!adstDstrNm) { alert("행정구역명이 없습니다."); return; }
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsPpltnApi.getRenewalUpperCodesByName(yr, adstDstrNm);
|
||||
if (res.status === "success" && Array.isArray(res.data) && res.data.length > 0) {
|
||||
setCandidates(res.data);
|
||||
selectedRowIndexRef.current = rowIndex;
|
||||
selectedTabRef.current = "renewal";
|
||||
setDialogOpen(true);
|
||||
} else {
|
||||
alert("해당 지역명에 대한 법정동코드 후보가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("법정동코드 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ── 다이얼로그 코드 선택 → 업데이트 ── */
|
||||
const handleCodeSelect = (item: UpperCodeCandidateItem) => {
|
||||
const rowIndex = selectedRowIndexRef.current;
|
||||
if (rowIndex === null) return;
|
||||
|
||||
if (selectedTabRef.current === "renewal") {
|
||||
// 행정동갱신: 법정동코드/법정동명 업데이트
|
||||
setRenewalAddedRowData((prev) =>
|
||||
prev.map((row, idx) =>
|
||||
idx === rowIndex
|
||||
? { ...row, lawCd: item.adstCd, lawNm: item.adstDstrNm }
|
||||
: row
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// 내국인/외국인: mpinCd 업데이트
|
||||
const setter = selectedTabRef.current === "domestic" ? setDomesticRightRowData : setForeignRightRowData;
|
||||
setter((prev) =>
|
||||
prev.map((row, idx) =>
|
||||
idx === rowIndex ? { ...row, mpinCd: item.adstCd } : row
|
||||
)
|
||||
);
|
||||
}
|
||||
selectedRowIndexRef.current = null;
|
||||
};
|
||||
|
||||
/* ══════════════════════ 렌더 ══════════════════════ */
|
||||
return (
|
||||
<>
|
||||
<SearchForm sido={false} />
|
||||
</>
|
||||
<div className="relative">
|
||||
<input ref={domesticFileInputRef} type="file" accept=".xlsx,.xls,.csv" className="hidden" onChange={handleDomesticFileChange} />
|
||||
<input ref={foreignFileInputRef} type="file" accept=".xlsx,.xls,.csv" className="hidden" onChange={handleForeignFileChange} />
|
||||
|
||||
<SearchForm sido={false} gugun={false} onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{TAB_LIST.map((tab) => (
|
||||
<TabsTrigger key={tab.key} value={tab.key}>{tab.label}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{/* ── 내국인 탭 ── */}
|
||||
<TabsContent value="domestic">
|
||||
<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 mb-[0.4rem]">
|
||||
<Button type="button" color="red" onClick={() => domesticFileInputRef.current?.click()}
|
||||
className="mt-[0.5rem] px-[3rem] py-[0.8rem] text-[1.4rem]">
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={domesticLeftColumnInfo}
|
||||
rowStartNum={DomesticPopulationLeftColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="내국인전체리스트"
|
||||
rowData={domesticLeftRowData}
|
||||
setRowData={setDomesticLeftRowData}
|
||||
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={isLoading || domesticRightRowData.length === 0}
|
||||
onClick={() => void handleDomesticFinal()}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]">
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={domesticRightColumnInfo}
|
||||
rowStartNum={DomesticPopulationRightColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="미매핑리스트"
|
||||
rowData={domesticRightRowData}
|
||||
setRowData={setDomesticRightRowData}
|
||||
isExcelImport={false} isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={DomesticPopulationRightColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
onRowClick={(rowData) => {
|
||||
const rowIndex = domesticRightRowData.findIndex((r) => r.admdstCd === rowData.admdstCd);
|
||||
void handleRightRowClick(rowData, rowIndex, "domestic");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* ── 외국인 탭 ── */}
|
||||
<TabsContent value="foreign">
|
||||
<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 mb-[0.4rem]">
|
||||
<Button type="button" color="red" onClick={() => foreignFileInputRef.current?.click()}
|
||||
className="mt-[0.5rem] px-[3rem] py-[0.8rem] text-[1.4rem]">
|
||||
엑셀 업로드
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={foreignLeftColumnInfo}
|
||||
rowStartNum={ForeignPopulationLeftColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="외국인전체리스트"
|
||||
rowData={foreignLeftRowData}
|
||||
setRowData={setForeignLeftRowData}
|
||||
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={isLoading || foreignRightRowData.length === 0}
|
||||
onClick={() => void handleForeignFinal()}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]">
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={foreignRightColumnInfo}
|
||||
rowStartNum={ForeignPopulationRightColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="외국인미매핑리스트"
|
||||
rowData={foreignRightRowData}
|
||||
setRowData={setForeignRightRowData}
|
||||
isExcelImport={false} isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={ForeignPopulationRightColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
onRowClick={(rowData) => {
|
||||
const rowIndex = foreignRightRowData.findIndex((r) => r.admdstCd === rowData.admdstCd);
|
||||
void handleRightRowClick(rowData, rowIndex, "foreign");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="renewal">
|
||||
<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="h-[3.9rem]" />
|
||||
<DataGrid
|
||||
columnInfo={renewalReducedColumnInfo}
|
||||
rowStartNum={AdmdstRenewalReducedColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="감축된행정구역"
|
||||
rowData={renewalReducedRowData}
|
||||
setRowData={setRenewalReducedRowData}
|
||||
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={isLoading || (renewalReducedRowData.length === 0 && renewalAddedRowData.length === 0)}
|
||||
onClick={() => void handleRenewalFinal()}
|
||||
className="mt-[0.5rem] px-[4rem] py-[0.8rem] text-[1.4rem]">
|
||||
반영
|
||||
</Button>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={renewalAddedColumnInfo}
|
||||
rowStartNum={AdmdstRenewalAddedColumnConfig.rowStartNum}
|
||||
excelExportFileSuffix="추가된행정구역"
|
||||
rowData={renewalAddedRowData}
|
||||
setRowData={setRenewalAddedRowData}
|
||||
isExcelImport={false} isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={AdmdstRenewalAddedColumnConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
onRowClick={(rowData) => {
|
||||
const rowIndex = renewalAddedRowData.findIndex((r) => r.adstCd === rowData.adstCd);
|
||||
void handleRenewalRowClick(rowData, rowIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="status">
|
||||
<div className="p-6 text-sm text-gray-400">행정인구현황 구현 예정</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="statusViz">
|
||||
<div className="p-6 text-sm text-gray-400">행정인구현황 시각화 구현 예정</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<UpperCodeSelectDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
candidates={candidates}
|
||||
onSelect={handleCodeSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticsPopulationManagement;
|
||||
export default StatisticsPopulationManagement;
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
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;
|
||||
}
|
||||
|
||||
export const UpperCodeSelectDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
candidates,
|
||||
onSelect,
|
||||
}: UpperCodeSelectDialogProps) => {
|
||||
const [selectedCode, setSelectedCode] = useState<string | null>(null);
|
||||
|
||||
const handleSelect = () => {
|
||||
const item = candidates.find((c) => c.adstCd === selectedCode);
|
||||
if (!item) return;
|
||||
onSelect(item);
|
||||
onOpenChange(false);
|
||||
setSelectedCode(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
setSelectedCode(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-[50rem]" aria-describedby={undefined}>
|
||||
<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>
|
||||
|
||||
<DialogFooter className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
disabled={!selectedCode}
|
||||
onClick={handleSelect}
|
||||
className="w-full py-[1rem] text-[1.4rem]"
|
||||
>
|
||||
선택
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={handleClose}
|
||||
className="w-full py-[1rem] text-[1.4rem]"
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
16. 통계인구관리 > 행정동갱신 탭 컬럼 설정
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
/** 좌측: 감축된 행정구역(범위:동읍면) 자료 */
|
||||
export const AdmdstRenewalReducedColumnConfig: ColumnInfoList = {
|
||||
tabKey: "RENEWAL_REDUCED",
|
||||
tabLabel: "감축된 행정구역(범위:동읍면) 자료",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 1,
|
||||
columns: [
|
||||
{
|
||||
name: "adstDstrNm",
|
||||
fieldName: "adstDstrNm",
|
||||
header: { text: "행정동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "adstCd",
|
||||
fieldName: "adstCd",
|
||||
header: { text: "행정동코드" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: ["adstDstrNm", "adstCd"],
|
||||
};
|
||||
|
||||
/** 우측: 추가된 행정구역 자료 */
|
||||
export const AdmdstRenewalAddedColumnConfig: ColumnInfoList = {
|
||||
tabKey: "RENEWAL_ADDED",
|
||||
tabLabel: "추가된 행정구역 자료",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 1,
|
||||
columns: [
|
||||
{
|
||||
name: "adstDstrNm",
|
||||
fieldName: "adstDstrNm",
|
||||
header: { text: "행정동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "adstCd",
|
||||
fieldName: "adstCd",
|
||||
header: { text: "행정동코드" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "lawCd",
|
||||
fieldName: "lawCd",
|
||||
header: { text: "법정동코드" },
|
||||
width: 150,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "lawNm",
|
||||
fieldName: "lawNm",
|
||||
header: { text: "법정동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: ["adstDstrNm", "adstCd", "lawCd", "lawNm"],
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
16. 통계인구관리 > 내국인 인구 탭 컬럼 설정
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
/** 좌측: 엑셀 업로드 자료 (내국인 전체 리스트) */
|
||||
export const DomesticPopulationLeftColumnConfig: ColumnInfoList = {
|
||||
tabKey: "DOMESTIC_LEFT",
|
||||
tabLabel: "엑셀 업로드 자료 (내국인 전체 리스트)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
fieldName: "admdstCd",
|
||||
header: { text: "코드" },
|
||||
width: 120,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplWholPpltnCnt",
|
||||
fieldName: "domplWholPpltnCnt",
|
||||
header: { text: "합계" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplMlPpltnCnt",
|
||||
fieldName: "domplMlPpltnCnt",
|
||||
header: { text: "남성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplFmlPpltnCnt",
|
||||
fieldName: "domplFmlPpltnCnt",
|
||||
header: { text: "여성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"admdstCd",
|
||||
"admdstNm",
|
||||
{
|
||||
name: "populationGroup",
|
||||
header: { text: "인구수" },
|
||||
items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 우측: 미매핑 리스트 (작년 대비) */
|
||||
export const DomesticPopulationRightColumnConfig: ColumnInfoList = {
|
||||
tabKey: "DOMESTIC_RIGHT",
|
||||
tabLabel: "미매핑 리스트 (작년 대비)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
fieldName: "admdstCd",
|
||||
header: { text: "업로드코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "mpinCd",
|
||||
fieldName: "mpinCd",
|
||||
header: { text: "맵핑코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplWholPpltnCnt",
|
||||
fieldName: "domplWholPpltnCnt",
|
||||
header: { text: "합계" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplMlPpltnCnt",
|
||||
fieldName: "domplMlPpltnCnt",
|
||||
header: { text: "남성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "domplFmlPpltnCnt",
|
||||
fieldName: "domplFmlPpltnCnt",
|
||||
header: { text: "여성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"admdstCd",
|
||||
"admdstNm",
|
||||
"mpinCd",
|
||||
{
|
||||
name: "populationGroup",
|
||||
header: { text: "인구수" },
|
||||
items: ["domplWholPpltnCnt", "domplMlPpltnCnt", "domplFmlPpltnCnt"],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
16. 통계인구관리 > 외국인 인구 탭 컬럼 설정
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
/** 좌측: 엑셀 업로드 자료 (외국인 전체 리스트) */
|
||||
export const ForeignPopulationLeftColumnConfig: ColumnInfoList = {
|
||||
tabKey: "FOREIGN_LEFT",
|
||||
tabLabel: "엑셀 업로드 자료 (외국인 전체 리스트)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
fieldName: "admdstCd",
|
||||
header: { text: "코드" },
|
||||
width: 120,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrWholPpltnCnt",
|
||||
fieldName: "frgnrWholPpltnCnt",
|
||||
header: { text: "합계" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrMlPpltnCnt",
|
||||
fieldName: "frgnrMlPpltnCnt",
|
||||
header: { text: "남성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrFmlPpltnCnt",
|
||||
fieldName: "frgnrFmlPpltnCnt",
|
||||
header: { text: "여성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"admdstCd",
|
||||
"admdstNm",
|
||||
{
|
||||
name: "populationGroup",
|
||||
header: { text: "외국인수" },
|
||||
items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** 우측: 미매핑 리스트 (작년 대비) */
|
||||
export const ForeignPopulationRightColumnConfig: ColumnInfoList = {
|
||||
tabKey: "FOREIGN_RIGHT",
|
||||
tabLabel: "미매핑 리스트 (작년 대비)",
|
||||
rowStartNum: 1,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: "admdstCd",
|
||||
fieldName: "admdstCd",
|
||||
header: { text: "업로드코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "동명" },
|
||||
width: 200,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "mpinCd",
|
||||
fieldName: "mpinCd",
|
||||
header: { text: "맵핑코드" },
|
||||
width: 130,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrWholPpltnCnt",
|
||||
fieldName: "frgnrWholPpltnCnt",
|
||||
header: { text: "합계" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrMlPpltnCnt",
|
||||
fieldName: "frgnrMlPpltnCnt",
|
||||
header: { text: "남성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
{
|
||||
name: "frgnrFmlPpltnCnt",
|
||||
fieldName: "frgnrFmlPpltnCnt",
|
||||
header: { text: "여성" },
|
||||
width: 110,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly",
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"admdstCd",
|
||||
"admdstNm",
|
||||
"mpinCd",
|
||||
{
|
||||
name: "populationGroup",
|
||||
header: { text: "외국인수" },
|
||||
items: ["frgnrWholPpltnCnt", "frgnrMlPpltnCnt", "frgnrFmlPpltnCnt"],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stClient } from "@/shared/api/client";
|
||||
|
||||
import type {
|
||||
DomesticPopulationListResponse,
|
||||
DomesticPopulationFinalResponse,
|
||||
UpperCodesByNameResponse,
|
||||
DomesticPopulationMappingItem,
|
||||
} from "../model/ppltn/statistics-domestic-population-interface";
|
||||
|
||||
export class StatisticsPpltnApiService {
|
||||
|
||||
/** 내국인 엑셀 임시 업로드 */
|
||||
public async uploadDomesticPopulationExcel(
|
||||
file: File,
|
||||
statsYr: string,
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("statsYr", statsYr);
|
||||
|
||||
const response = await stClient.post<{ status: string; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-excel`,
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 내국인 임시 적재 리스트 조회
|
||||
* @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측)
|
||||
*/
|
||||
public async getDomesticPopulationList(
|
||||
statsYr: string,
|
||||
opt1: "1" | "2",
|
||||
): Promise<DomesticPopulationListResponse> {
|
||||
const response = await stClient.get<DomesticPopulationListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-list`,
|
||||
{ params: { statsYr, opt1 } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 내국인 최종 마스터 반영 */
|
||||
public async saveDomesticPopulationFinal(
|
||||
statsYr: string,
|
||||
regList: DomesticPopulationMappingItem[],
|
||||
): Promise<DomesticPopulationFinalResponse> {
|
||||
const response = await stClient.post<DomesticPopulationFinalResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/domestic-population-final`,
|
||||
{ YR: statsYr, regList },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 미매핑 행 클릭 시 상위 코드 후보 조회 */
|
||||
public async getUpperCodesByName(
|
||||
statsYr: string,
|
||||
admdstNm: string,
|
||||
): Promise<UpperCodesByNameResponse> {
|
||||
const response = await stClient.post<UpperCodesByNameResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/upper-codes-by-name`,
|
||||
{ statsYr, admdstNm },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
/* ════════════════════════════════════
|
||||
외국인
|
||||
════════════════════════════════════ */
|
||||
|
||||
/** 외국인 엑셀 임시 업로드 */
|
||||
public async uploadForeignPopulationExcel(
|
||||
file: File,
|
||||
statsYr: string,
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("statsYr", statsYr);
|
||||
const response = await stClient.post<{ status: string; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-excel`,
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 외국인 임시 적재 리스트 조회
|
||||
* @param opt1 "1" = 전체 리스트(좌측), "2" = 미매핑 리스트(우측)
|
||||
*/
|
||||
public async getForeignPopulationList(
|
||||
statsYr: string,
|
||||
opt1: "1" | "2",
|
||||
): Promise<{ status: string; data: object[]; message?: string }> {
|
||||
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-list`,
|
||||
{ params: { statsYr, opt1 } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 외국인 최종 마스터 반영 */
|
||||
public async saveForeignPopulationFinal(
|
||||
statsYr: string,
|
||||
regList: DomesticPopulationMappingItem[],
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const response = await stClient.post<{ status: string; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/foreign-population-final`,
|
||||
{ statsYr, regList },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
/* ════════════════════════════════════
|
||||
행정동갱신
|
||||
════════════════════════════════════ */
|
||||
|
||||
/** 행정동 갱신 — 감축/추가 행정구역 조회
|
||||
* @param opt1 "1" = 추가된 행정구역, "2" = 감축된 행정구역
|
||||
*/
|
||||
public async searchChangedArea(
|
||||
statsYr: string,
|
||||
opt1: "1" | "2",
|
||||
): Promise<{ status: string; data: object[]; message?: string }> {
|
||||
const response = await stClient.get<{ status: string; data: object[]; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/search`,
|
||||
{ params: { statsYr, opt1 } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 행정동 갱신 — 감축/추가 반영 */
|
||||
public async saveChangedArea(
|
||||
reducedList: object[],
|
||||
addedList: object[],
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const response = await stClient.post<{ status: string; message?: string }>(
|
||||
`${ENDPOINT.ST_SERVICE.PPLTN}/admdst-renewal/save`,
|
||||
{ reducedList, addedList },
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const statisticsPpltnApi = new StatisticsPpltnApiService();
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
/* ══════════════════════════════════════════════════
|
||||
16. 통계인구관리 > 내국인 인구 관련 인터페이스
|
||||
══════════════════════════════════════════════════ */
|
||||
|
||||
/** 내국인 인구 임시 적재 행 (좌측 전체 리스트 / 우측 미매핑 리스트 공통) */
|
||||
export interface DomesticPopulationItem {
|
||||
statsYr: string; // 통계연도
|
||||
admdstCd: string; // 행정구역코드
|
||||
admdstNm: string; // 행정구역명
|
||||
domplWholPpltnCnt: number; // 내국인 합계 인구수
|
||||
domplMlPpltnCnt: number; // 내국인 남성 인구수
|
||||
domplFmlPpltnCnt: number; // 내국인 여성 인구수
|
||||
/** 미매핑 리스트 전용 - 매핑 대상 행정구역코드 (상위코드) */
|
||||
mpinCd: string;
|
||||
}
|
||||
|
||||
/** 엑셀 업로드 요청 파라미터 */
|
||||
export interface DomesticPopulationUploadParams {
|
||||
file: File;
|
||||
statsYr: string;
|
||||
}
|
||||
|
||||
/** 임시 적재 리스트 조회 요청 파라미터 */
|
||||
export interface DomesticPopulationListParams {
|
||||
statsYr: string;
|
||||
/** 1: 전체 리스트(좌측), 2: 미매핑 리스트(우측) */
|
||||
opt1: '1' | '2';
|
||||
}
|
||||
|
||||
/** 수동 매핑 후 최종 반영 요청 파라미터 */
|
||||
export interface DomesticPopulationFinalParams {
|
||||
YR: string;
|
||||
/** 수동 매핑된 행 목록 (admdstCd → subAdmdstCd 매핑 정보) */
|
||||
regList: DomesticPopulationMappingItem[];
|
||||
}
|
||||
|
||||
/** 수동 매핑 단건 아이템 */
|
||||
export interface DomesticPopulationMappingItem {
|
||||
statsYr: string;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
subAdmdstCd: string;
|
||||
}
|
||||
|
||||
/** 상위 코드 후보 조회 요청 */
|
||||
export interface UpperCodesByNameParams {
|
||||
statsYr: string;
|
||||
admdstNm: string;
|
||||
}
|
||||
|
||||
/** 상위 코드 후보 단건 아이템 */
|
||||
export interface UpperCodeCandidateItem {
|
||||
adstCd: string; // 행정구역코드
|
||||
adstDstrNm: string; // 행정구역명
|
||||
}
|
||||
|
||||
/* ── API 응답 타입 ── */
|
||||
export type DomesticPopulationListResponse = ApiResponse<DomesticPopulationItem[]>;
|
||||
export type DomesticPopulationFinalResponse = ApiResponse<string>;
|
||||
export type UpperCodesByNameResponse = ApiResponse<UpperCodeCandidateItem[]>;
|
||||
@@ -71,6 +71,7 @@ export const ENDPOINT = {
|
||||
INPUT: "/st-input-service/api/v1/input",
|
||||
REVIEW: "/st-management-service/api/v1/review",
|
||||
STATS: "/st-management-service/api/stats",
|
||||
PPLTN: "/st-management-service/api/v1/ppltn",
|
||||
INPUTCOMMON: "/st-input-service/api/v1/common",
|
||||
INPUTVERIFY: "/st-input-service/api/v1/verify",
|
||||
TOTAL: "/st-management-service/api/v1/stats",
|
||||
|
||||
Reference in New Issue
Block a user