From 72ddb5ad9e30b721fd5e17b199cd3631638258ce Mon Sep 17 00:00:00 2001 From: dongsilove Date: Sun, 14 Dec 2025 21:25:50 +0900 Subject: [PATCH] =?UTF-8?q?cm:=20=EC=A7=80=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/src/pages/dashboard/api/fo-service.ts | 54 +++++++- .../pages/dashboard/lib/korea-basin-map.tsx | 120 ++++++++---------- .../src/pages/dashboard/model/fo-interface.ts | 24 +++- 3 files changed, 132 insertions(+), 66 deletions(-) diff --git a/apps/cm/src/pages/dashboard/api/fo-service.ts b/apps/cm/src/pages/dashboard/api/fo-service.ts index a35fedd51..d85ae2d6d 100644 --- a/apps/cm/src/pages/dashboard/api/fo-service.ts +++ b/apps/cm/src/pages/dashboard/api/fo-service.ts @@ -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((set) => ({ + mapOperFclt: initailMapOperFcltData.data, + setMapOperFclt: (newMapOperFclt) => set({ mapOperFclt: newMapOperFclt }), +})); + +/** + * OperFcltContent[]를 지도 팝업 데이터 형태인 mapOperFcltItem[]으로 변환합니다. + * @param data API 응답 데이터 + * @returns 유역별 개소수 및 가동률이 계산된 데이터 + */ +function transformToMapData(data: OperFcltContent[]): mapOperFcltItem[] { + const basinData = new Map(); + + // 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 { 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(); - diff --git a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx index 00014cfa7..714e8ef35 100644 --- a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -8,7 +8,8 @@ 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 type { mapOperFcltType } from "../model/fo-interface"; +import { useMapOperFcltStore } from "../api/fo-service"; // import {Button} from "@common/components"; const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요 @@ -17,18 +18,31 @@ function fmt(n: number) { return new Intl.NumberFormat("ko-KR").format(n); } +const mapOperFcltData: 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,}, + ] +}; export function KoreaBasinMap() { const stageRef = useRef(null); const svgLayerRef = useRef(null); const labelsLayerRef = useRef(null); const popupRef = useRef(null); - const badgeRef = useRef(null); - const rowsRef = useRef(null); - const colTitleRef = useRef(null); + const popupTitleRef = useRef(null); + const popupHeader1Ref = useRef(null); + const popupHeader2Ref = useRef(null); + const popupData1Ref = useRef(null); + const popupData2Ref = useRef(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 +154,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 +162,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)[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)[]) => - `${r[0]}${fmt(Number(r[1]) || 0)}`, - ) - .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 +189,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 +199,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 +207,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 +396,34 @@ export function KoreaBasinMap() { pointerEvents: "none", }} /> -
diff --git a/apps/cm/src/pages/dashboard/model/fo-interface.ts b/apps/cm/src/pages/dashboard/model/fo-interface.ts index 7aa61ff8d..9ec3a312c 100644 --- a/apps/cm/src/pages/dashboard/model/fo-interface.ts +++ b/apps/cm/src/pages/dashboard/model/fo-interface.ts @@ -47,4 +47,26 @@ export interface OperationService { getDashOperCntRt(year: string): Promise; getDashOperWqlt(year: string): Promise; getReuseStatus(): Promise; -} \ No newline at end of file +} + +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 = {}; + + +// } \ No newline at end of file