cm: 지도 수정
This commit is contained in:
@@ -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 = {};
|
||||
|
||||
|
||||
// }
|
||||
Reference in New Issue
Block a user