cm: 지도 수정

This commit is contained in:
ljpark
2025-12-14 18:31:51 +09:00
parent dc764bbe0d
commit 6ab3d7b1a9
3 changed files with 131 additions and 66 deletions
+53 -1
View File
@@ -1,9 +1,11 @@
// apps/cm/src/pages/dashboard/api/fo-service.ts
import { client } from "@/shared/api";
import { createStore } from "zustand/vanilla";
import { create } from "zustand";
import { ENDPOINT } from "@/shared/config";
import type { OperWqltContent, OperFcltContent, ReuseStatusContent, OperationService
} from "../model/fo-interface";
import type { mapOperFcltItem, mapOperFcltType } from "../model/fo-interface";
// 기준년도 조회
export type YearState = {
@@ -86,6 +88,55 @@ export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFc
}));
export const operFcltStore = createOperFcltStore();
export const initailMapOperFcltData: mapOperFcltType = {
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
data: [
{name: "금강", cnt: 710, rt: 100,},
{name: "한강", cnt: 983, rt: 100,},
{name: "낙동강", cnt: 1032, rt: 100,},
{name: "영산강", cnt: 1261, rt: 100,},
]
};
type mapOperFcltState = {
mapOperFclt: mapOperFcltItem[];
setMapOperFclt: (mapOperFclt: mapOperFcltItem[]) => void;
};
export const useMapOperFcltStore = create<mapOperFcltState>((set) => ({
mapOperFclt: initailMapOperFcltData.data,
setMapOperFclt: (newMapOperFclt) => set({ mapOperFclt: newMapOperFclt }),
}));
/**
* OperFcltContent[]를 지도 팝업 데이터 형태인 mapOperFcltItem[]으로 변환합니다.
* @param data API 응답 데이터
* @returns 유역별 개소수 및 가동률이 계산된 데이터
*/
function transformToMapData(data: OperFcltContent[]): mapOperFcltItem[] {
const basinData = new Map<string, { operatingCpc: number; totalCpc: number; count: number }>();
// 1. 유역별로 가동/전체 용량 및 개소수 집계
for (const item of data) {
if (!item.rvbsn) continue;
const basin = basinData.get(item.rvbsn) || { operatingCpc: 0, totalCpc: 0, count: 0 };
basin.totalCpc += item.fctCpc;
if (item.gubun === 'R') {
basin.operatingCpc += item.fctCpc;
basin.count = item.cnt; // 'R' 구분 데이터의 개소수를 사용
}
basinData.set(item.rvbsn, basin);
}
// 2. mapOperFcltItem 형태로 변환
const result: mapOperFcltItem[] = [];
for (const [name, { operatingCpc, totalCpc, count }] of basinData.entries()) {
const rt = totalCpc > 0 ? Math.round((operatingCpc / totalCpc) * 100) : 0;
result.push({ name, cnt: count, rt });
}
return result;
}
//수계별 유입ㆍ방류수질 분석
export type OperWqltState = {
operWqltStatus: OperWqltContent[] | [];
@@ -170,6 +221,7 @@ export class OperationApiService implements OperationService {
// 수계별 시설운영 개소수 및 가동률
public async getDashOperCntRt(year: string): Promise<OperFcltContent[]> {
const { operFcltStatus, setOperFcltStatus } = operFcltStore.getState();
const { setMapOperFclt } = useMapOperFcltStore.getState();
// 간단한 캐싱: 데이터가 이미 있으면 API 호출 생략
if (operFcltStatus && operFcltStatus.length > 0) {
return operFcltStatus;
@@ -192,6 +244,7 @@ export class OperationApiService implements OperationService {
}));
setOperFcltStatus(list);
setMapOperFclt(transformToMapData(list));
return list;
}
@@ -244,4 +297,3 @@ export class OperationApiService implements OperationService {
}
export const OperationApi = new OperationApiService();
@@ -8,7 +8,7 @@ import styles from "./korea-basin-map.module.css"; // 추가
import { basinStore } from "../api/fo-service";
import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data";
import { BASIN_DATA, BASIN_IDS } from "../model/map-data";
// import { operFcltStore } from "../api/fo-service";
import { useMapOperFcltStore } from "../api/fo-service";
// import {Button} from "@common/components";
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
@@ -17,18 +17,31 @@ function fmt(n: number) {
return new Intl.NumberFormat("ko-KR").format(n);
}
const mapOperFcltData = {
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
data: [
// {name: "금강", cnt: 710, rt: 100,},
// {name: "한강", cnt: 983, rt: 100,},
// {name: "낙동강", cnt: 1032, rt: 100,},
// {name: "영산강", cnt: 1261, rt: 100,},
]
};
export function KoreaBasinMap() {
const stageRef = useRef<HTMLDivElement>(null);
const svgLayerRef = useRef<HTMLDivElement>(null);
const labelsLayerRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const badgeRef = useRef<HTMLDivElement>(null);
const rowsRef = useRef<HTMLTableSectionElement>(null);
const colTitleRef = useRef<HTMLTableCellElement>(null);
const popupTitleRef = useRef<HTMLDivElement>(null);
const popupHeader1Ref = useRef<HTMLDivElement>(null);
const popupHeader2Ref = useRef<HTMLDivElement>(null);
const popupData1Ref = useRef<HTMLDivElement>(null);
const popupData2Ref = useRef<HTMLDivElement>(null);
// const [services, setServices] = useState(svcInfo);
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
// const { operFcltStatus, setSelectedBasin } = useStore(operFcltStore);
const selectedBasinKey = basinKeyMap[selectedBasin];
const {mapOperFclt} = useMapOperFcltStore();
mapOperFcltData.data = mapOperFclt;
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
useEffect(() => {
@@ -140,14 +153,6 @@ export function KoreaBasinMap() {
popupRef.current.classList.remove(styles.show);
}
popupRef.current.style.opacity = "0";
popupRef.current.style.visibility = "hidden";
}
if (badgeRef.current) {
if (styles.show) {
badgeRef.current.classList.remove(styles.show);
}
badgeRef.current.style.opacity = "0";
badgeRef.current.style.visibility = "hidden";
}
labels.forEach((l) => l.classList.remove("active"));
};
@@ -156,23 +161,24 @@ export function KoreaBasinMap() {
function showFor(g: SVGGElement) {
// console.log("showFor called for:", g);
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
const data = (BASIN_DATA as Record<string, { name: string; colTitle: string; rows: (string | number)[][] }>)[key] || {
name: (g as SVGGElement & { dataset: { label: string } }).dataset.label,
colTitle: "",
rows: [],
};
console.log("Data for popup:", data);
const basinName = BASIN_DATA[key as keyof typeof BASIN_DATA]?.name || (g as SVGGElement & { dataset: { label: string } }).dataset.label;
const fcltItem = mapOperFcltData.data.find(d => d.name === basinName);
if (badgeRef.current) badgeRef.current.textContent = data.name;
if (colTitleRef.current)
colTitleRef.current.textContent = data.colTitle || "???";
if (rowsRef.current)
rowsRef.current.innerHTML = data.rows
.map(
(r: (string | number)[]) =>
`<tr><td>${r[0]}</td><td style="text-align:right">${fmt(Number(r[1]) || 0)}</td></tr>`,
)
.join("");
if (popupTitleRef.current) {
popupTitleRef.current.textContent = `2024년도 ${basinName}유역 시설운영 현황`;
}
if (popupHeader1Ref.current) {
popupHeader1Ref.current.textContent = mapOperFcltData.colTitle.cnt;
}
if (popupHeader2Ref.current) {
popupHeader2Ref.current.textContent = mapOperFcltData.colTitle.rt;
}
if (popupData1Ref.current) {
popupData1Ref.current.textContent = fcltItem ? `${fmt(fcltItem.cnt)}개소` : "-";
}
if (popupData2Ref.current) {
popupData2Ref.current.textContent = fcltItem ? `${fcltItem.rt}%` : "-";
}
if (!stageRef.current) return;
const sRect = stageRef.current.getBoundingClientRect(),
@@ -182,10 +188,6 @@ export function KoreaBasinMap() {
// console.log("Popup position:", { cx, ty });
if (badgeRef.current) {
badgeRef.current.style.left = cx + "px";
badgeRef.current.style.top = ty + 24 + "px";
}
if (popupRef.current) {
popupRef.current.style.left = cx + "px";
popupRef.current.style.top = ty + 6 + "px";
@@ -196,9 +198,6 @@ export function KoreaBasinMap() {
}
// console.log("Added show class to popup:", popupRef.current.classList.toString());
}
if (badgeRef.current && styles.show) {
badgeRef.current.classList.add(styles.show);
}
if (popupRef.current) {
const pRect = popupRef.current.getBoundingClientRect();
@@ -207,7 +206,6 @@ export function KoreaBasinMap() {
Math.min(cx, sRect.width - 12 - pRect.width / 2),
);
popupRef.current.style.left = x + "px";
if (badgeRef.current) badgeRef.current.style.left = x + "px";
}
}
@@ -397,41 +395,34 @@ export function KoreaBasinMap() {
pointerEvents: "none",
}}
/>
<div
ref={badgeRef}
id="badge"
className={styles.badge}
style={{ position: "absolute", zIndex: 10, display: "block" }}
/>
<div
ref={popupRef}
id="popup"
className={styles.popup}
className={`${styles.popup} absolute z-20 grid w-full font-sans border border-blue-200 rounded-lg shadow-md overflow-hidden opacity-0 invisible bg-blue-50`}
style={{
position: "absolute",
zIndex: 20,
background: "#fff",
border: "1px solid #ccc",
borderRadius: 8,
padding: 12,
minWidth: 160,
boxShadow: "0 2px 8px #0002",
display: "block",
opacity: 0,
visibility: "hidden",
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th>구분</th>
<th ref={colTitleRef} id="colTitle">
시설수
</th>
</tr>
</thead>
<tbody ref={rowsRef} id="rows"></tbody>
</table>
{/* 제목 영역 */}
<div ref={popupTitleRef} className="col-span-2 bg-blue-100 text-center font-bold border-b-2 border-blue-200 flex items-center justify-center">
2024년도 000유역 시설운영 현황
</div>
{/* 헤더 영역 */}
<div ref={popupHeader1Ref} className="text-center border-b-2 border-r-2 border-blue-200 flex items-center justify-center">
개소수
</div>
<div ref={popupHeader2Ref} className="text-center border-b-2 border-blue-200 flex items-center justify-center">
가동률
</div>
{/* 데이터 영역 */}
<div ref={popupData1Ref} className="text-center border-r-2 border-blue-200 flex items-center justify-center">
2,134개소
</div>
<div ref={popupData2Ref} className="text-center flex items-center justify-center">
75%
</div>
</div>
</div>
</div>
@@ -47,4 +47,26 @@ export interface OperationService {
getDashOperCntRt(year: string): Promise<OperFcltContent[]>;
getDashOperWqlt(year: string): Promise<OperWqltContent[]>;
getReuseStatus(): Promise<ReuseStatusContent[]>;
}
}
export type mapOperFcltItem = {
name: string;
cnt: number;
rt: number;
};
export type mapOperFcltType = {
colTitle: {
cnt: string; // 개소수
rt: string; // 가동률
};
data: mapOperFcltItem[];
};
// function buildCntRt( operFcltStatus: OperFcltContent[]) : mapBasinData => {
// const colTitle = {cnt:"개소수",rt:"가동률"};
// const result: mapBasinData = {};
// }