285 lines
14 KiB
TypeScript
285 lines
14 KiB
TypeScript
import { useRef, useState } from "react";
|
|
|
|
import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
|
import SearchForm from "@/components/module/SearchForm";
|
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
|
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
|
|
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
|
|
|
|
import { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
|
|
import { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
|
|
import { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
|
|
import { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
|
|
|
|
import DomesticPopulationTab from "./DomesticPopulationTab";
|
|
import ForeignPopulationTab from "./ForeignPopulationTab";
|
|
import AdmdstRenewalTab from "./AdmdstRenewalTab";
|
|
import PopulationStatusTab from "./PopulationStatusTab";
|
|
import PopulationVizTab from "./PopulationVizTab";
|
|
|
|
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
|
|
|
|
const TAB_LIST = [
|
|
{ key: "domestic", label: "행정인구등록" },
|
|
{ key: "foreign", label: "외국인인구등록" },
|
|
{ key: "renewal", label: "행정동갱신" },
|
|
{ key: "status", label: "행정인구현황" },
|
|
{ key: "statusViz", label: "행정인구현황 시각화" },
|
|
];
|
|
|
|
const StatisticsPopulationManagement = () => {
|
|
const [activeTab, setActiveTab] = useState("domestic");
|
|
|
|
/** 검색폼은 모든 탭이 공유. 마지막 조회 조건만 ref로 기억해뒀다가 탭 전환 시 재사용 */
|
|
const lastSearchedValuesRef = useRef<SearchFormValues | null>(null);
|
|
|
|
/* ── 탭별 상태/로직은 전부 훅에서 가져온다. 탭이 화면에 보이지 않아도 훅은 항상 호출되므로
|
|
탭을 왔다갔다 해도 데이터가 유지된다 (원본 컴포넌트와 동일한 동작) ── */
|
|
const domesticTab = usePopulationMappingTab<DomesticPopulationItem>({
|
|
kind: "domestic",
|
|
getList: statisticsPpltnApi.getDomesticPopulationList,
|
|
uploadExcel: statisticsPpltnApi.uploadDomesticPopulationExcel,
|
|
saveFinal: statisticsPpltnApi.saveDomesticPopulationFinal,
|
|
getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName,
|
|
requiredHeaderKeywords: ["행정기관코드", "행정기관"],
|
|
invalidFileMessage: "행정인구(내국인) 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.",
|
|
});
|
|
|
|
const foreignTab = usePopulationMappingTab<ForeignPopulationItem>({
|
|
kind: "foreign",
|
|
getList: async (statsYr, opt1) => {
|
|
const res = await statisticsPpltnApi.getForeignPopulationList(statsYr, opt1);
|
|
// res.data를 확실하게 ForeignPopulationItem[]로 캐스팅
|
|
return { ...res, data: res.data as ForeignPopulationItem[] };
|
|
},
|
|
uploadExcel: statisticsPpltnApi.uploadForeignPopulationExcel,
|
|
saveFinal: statisticsPpltnApi.saveForeignPopulationFinal,
|
|
getUpperCodesByName: statisticsPpltnApi.getUpperCodesByName,
|
|
requiredHeaderKeywords: ["행정동코드", "행정동명", "외국인수"],
|
|
invalidFileMessage: "외국인 인구 전용 파일만 업로드 가능합니다.\n올바른 파일을 선택해주세요.",
|
|
});
|
|
|
|
const renewalTab = useAdmdstRenewalTab();
|
|
const statusTab = usePopulationStatusTab();
|
|
const vizTab = usePopulationVizTab();
|
|
|
|
const statsYr = lastSearchedValuesRef.current?.year ?? "";
|
|
|
|
const isLoading =
|
|
(activeTab === "domestic" && domesticTab.isLoading) ||
|
|
(activeTab === "foreign" && foreignTab.isLoading) ||
|
|
(activeTab === "renewal" && renewalTab.isLoading) ||
|
|
(activeTab === "status" && statusTab.isLoading) ||
|
|
(activeTab === "statusViz" && vizTab.isLoading);
|
|
|
|
/* ════════════════ 공통 ════════════════ */
|
|
const handleSearch = async (values: SearchFormValues) => {
|
|
lastSearchedValuesRef.current = values;
|
|
const yr = values.year ?? "";
|
|
|
|
if (activeTab === "domestic") await domesticTab.search(yr);
|
|
else if (activeTab === "foreign") await foreignTab.search(yr);
|
|
else if (activeTab === "renewal") await renewalTab.loadAll(yr);
|
|
else if (activeTab === "status") await statusTab.search(yr);
|
|
else if (activeTab === "statusViz") await vizTab.search(yr);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
domesticTab.reset();
|
|
foreignTab.reset();
|
|
renewalTab.reset();
|
|
statusTab.reset();
|
|
vizTab.reset();
|
|
setActiveTab("domestic");
|
|
lastSearchedValuesRef.current = null;
|
|
};
|
|
|
|
const handleActiveTabChange = async (tabKey: string) => {
|
|
setActiveTab(tabKey);
|
|
|
|
const values = lastSearchedValuesRef.current;
|
|
if (!values) return;
|
|
const yr = values.year ?? "";
|
|
|
|
if (tabKey === "domestic") await domesticTab.loadOnActivate(yr);
|
|
else if (tabKey === "foreign") await foreignTab.loadOnActivate(yr);
|
|
else if (tabKey === "renewal") await renewalTab.loadOnActivate(yr);
|
|
// status / statusViz는 원본에서도 탭 전환 시 자동 재조회하지 않음 (그대로 유지)
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
<input
|
|
ref={domesticTab.fileInputRef}
|
|
type="file"
|
|
accept=".xlsx,.xls,.csv"
|
|
className="hidden"
|
|
onChange={(e) => void domesticTab.handleFileChange(e, statsYr)}
|
|
/>
|
|
<input
|
|
ref={foreignTab.fileInputRef}
|
|
type="file"
|
|
accept=".xlsx,.xls,.csv"
|
|
className="hidden"
|
|
onChange={(e) => void foreignTab.handleFileChange(e, statsYr)}
|
|
/>
|
|
|
|
<div className="relative">
|
|
<SearchForm sido={false} gugun={false} onSubmit={handleSearch} onReset={handleReset} />
|
|
|
|
{activeTab === "status" && (
|
|
<div className="absolute top-0 left-[16rem] h-full flex items-center gap-6 px-4">
|
|
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={statusTab.includeOriginMale}
|
|
onChange={(e) => statusTab.setIncludeOriginMale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
내국인 남성 포함
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={statusTab.includeOriginFemale}
|
|
onChange={(e) => statusTab.setIncludeOriginFemale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
내국인 여성 포함
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={statusTab.includeForeignMale}
|
|
onChange={(e) => statusTab.setIncludeForeignMale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
외국인 남성 포함
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-label cursor-pointer flex items-center gap-1.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={statusTab.includeForeignFemale}
|
|
onChange={(e) => statusTab.setIncludeForeignFemale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
외국인 여성 포함
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === "statusViz" && (
|
|
<div className="absolute top-0 left-[16rem] h-full flex items-center gap-5 px-4 flex-nowrap">
|
|
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={vizTab.includeOriginMale}
|
|
onChange={(e) => vizTab.setIncludeOriginMale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
내국인 남성
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={vizTab.includeOriginFemale}
|
|
onChange={(e) => vizTab.setIncludeOriginFemale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
내국인 여성
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={vizTab.includeForeignMale}
|
|
onChange={(e) => vizTab.setIncludeForeignMale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
외국인 남성
|
|
</label>
|
|
<label className="text-[1.3rem] font-medium leading-none text-label cursor-pointer flex items-center gap-1.5 shrink-0 whitespace-nowrap">
|
|
<input
|
|
type="checkbox"
|
|
checked={vizTab.includeForeignFemale}
|
|
onChange={(e) => vizTab.setIncludeForeignFemale(e.target.checked)}
|
|
className="size-4"
|
|
/>
|
|
외국인 여성
|
|
</label>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
onClick={vizTab.selectAll}
|
|
className="px-[1rem] py-[0.5rem] text-[1.2rem] shrink-0 whitespace-nowrap"
|
|
>
|
|
전체선택
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
onClick={vizTab.deselectAll}
|
|
className="px-[1rem] py-[0.5rem] text-[1.2rem] shrink-0 whitespace-nowrap"
|
|
>
|
|
전체해제(총 인구만 조회)
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{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">
|
|
<DomesticPopulationTab
|
|
tab={domesticTab}
|
|
statsYr={statsYr}
|
|
onFileButtonClick={() => domesticTab.fileInputRef.current?.click()}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="foreign">
|
|
<ForeignPopulationTab
|
|
tab={foreignTab}
|
|
statsYr={statsYr}
|
|
onFileButtonClick={() => foreignTab.fileInputRef.current?.click()}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="renewal">
|
|
<AdmdstRenewalTab tab={renewalTab} statsYr={statsYr} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="status">
|
|
<PopulationStatusTab tab={statusTab} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="statusViz">
|
|
<PopulationVizTab tab={vizTab} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StatisticsPopulationManagement; |