From 1bd5da63096c9f4e70323351609eefd612f7bd75 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 12 Dec 2025 14:51:35 +0900 Subject: [PATCH] =?UTF-8?q?cm:=EC=9C=A0=EC=97=AD=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/.env.local | 4 +- apps/cm/src/pages/dashboard/api/fo-service.ts | 61 ++++++++++++------- .../pages/dashboard/lib/korea-basin-map.tsx | 32 +++++++++- .../src/pages/dashboard/ui/dashboard-page.tsx | 6 +- .../src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx | 10 ++- .../pages/dashboard/ui/fo-wqlt-Infl-effl.tsx | 11 +++- 6 files changed, 90 insertions(+), 34 deletions(-) diff --git a/apps/cm/.env.local b/apps/cm/.env.local index da63ebdfd..a042ab706 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -7,5 +7,5 @@ NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 -# NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ -NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ \ No newline at end of file +NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ +# NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/api/fo-service.ts b/apps/cm/src/pages/dashboard/api/fo-service.ts index 3dc64dc5f..8dc163ffd 100644 --- a/apps/cm/src/pages/dashboard/api/fo-service.ts +++ b/apps/cm/src/pages/dashboard/api/fo-service.ts @@ -5,6 +5,43 @@ import { ENDPOINT } from "@/shared/config"; import type { OperWqltContent, OperFcltContent, ReuseStatusContent, OperationService } from "../model/fo-interface"; +// 기준년도 조회 +export type YearState = { + year: string | null; +}; +export type YearStateActions = { + setYear: (year: string | null) => void; +}; +export type YearStore = YearState & YearStateActions; + +export const defaultYearInitState: YearState = { + year: null, +}; +export const createYearStore = (initState: YearState = defaultYearInitState) => + createStore()((set) => ({ + ...initState, + setYear: (year) => set(() => ({ year })), + })); +export const yearStore = createYearStore(); + +// 유역 +export type BasinState = { + selectedBasin: string; +}; +export type BasinStateActions = { + setSelectedBasin: (selectedBasin: string ) => void; +}; +export type BasinStore = BasinState & BasinStateActions; +export const defaultBasinInitState: BasinState = { + selectedBasin: "한강", +}; +export const createBasinStore = (initState: BasinState = defaultBasinInitState) => + createStore()((set) => ({ + ...initState, + setSelectedBasin: (selectedBasin) => set(() => ({ selectedBasin })), + })); +export const basinStore = createBasinStore(); + //수계별 시설운영 개소수 및 가동률 export type OperFcltState = { operFcltStatus: OperFcltContent[] | null; @@ -50,6 +87,7 @@ const initialWqltData: OperWqltContent[] = [ export const defaultInitOperWqltState: OperWqltState = { operWqltStatus: initialWqltData, }; +export const operFcltStore = createOperFcltStore(); // 물재이용 현황 export type ReuseState = { @@ -73,29 +111,8 @@ export const createReuseStore = (initState: ReuseState = defaultInitReuseState) ...initState, setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })), })); - -// 기준년도 조회 -export type YearState = { - year: string | null; -}; -export type YearStateActions = { - setYear: (year: string | null) => void; -}; -export type YearStore = YearState & YearStateActions; - -export const defaultYearInitState: YearState = { - year: null, -}; -export const createYearStore = (initState: YearState = defaultYearInitState) => - createStore()((set) => ({ - ...initState, - setYear: (year) => set(() => ({ year })), - })); - -// 전역으로 공유할 store 인스턴스 -export const operFcltStore = createOperFcltStore(); export const reuseStore = createReuseStore(); -export const yearStore = createYearStore(); + export class OperationApiService implements OperationService { 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 eacf1a2e1..bce1879be 100644 --- a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -1,8 +1,20 @@ import React, { useEffect, useRef } from "react"; +import { useStore } from "zustand"; import styles from "./korea-basin-map.module.css"; // 추가 +import { basinStore } from "../api/fo-service"; const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요 + // 2. 유역 키를 매핑하는 로직 (예: "한강" -> "han") +export const basinKeyMap: { [key: string]: string } = { + "한강": "han", + "금강": "geum", + "낙동강": "nakdong", + "영산강": "yeongsan", +}; +// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 --- +// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리) +// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준) const MANUAL_LABEL_POS = { han: { x: 0.52, y: 0.14 }, geum: { x: 0.28, y: 0.47 }, @@ -56,6 +68,7 @@ function fmt(n: number) { return new Intl.NumberFormat("ko-KR").format(n); } + export function KoreaBasinMap() { const stageRef = useRef(null); const svgLayerRef = useRef(null); @@ -65,12 +78,28 @@ export function KoreaBasinMap() { const rowsRef = useRef(null); const colTitleRef = useRef(null); + const { selectedBasin } = useStore(basinStore); + const selectedBasinKey = basinKeyMap[selectedBasin]; + // selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect + useEffect(() => { + if (selectedBasinKey) { + const svg = svgLayerRef.current?.querySelector("svg"); + if (!svg) return; + + // 모든 유역의 활성화 상태를 초기화합니다. + svg.querySelectorAll('.basin.active').forEach(el => el.classList.remove('active')); + + const targetGroup = svg.querySelector(`.basin[data-key="${selectedBasinKey}"]`) as SVGGElement | null; + targetGroup?.classList.add('active'); + } + }, [selectedBasinKey]); + useEffect(() => { let svg: SVGSVGElement | null = null; let gs: SVGGElement[] = []; let labels: HTMLDivElement[] = []; - console.log("Loading SVG from:", SVG_URL); + // console.log("Loading SVG from:", SVG_URL); fetch(SVG_URL) .then((r) => { console.log("SVG fetch response:", r.status, r.statusText); @@ -173,6 +202,7 @@ export function KoreaBasinMap() { labels.forEach((l) => l.classList.remove("active")); }; + // 유역정보 function showFor(g: SVGGElement) { console.log("showFor called for:", g); const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 0ace9730a..be477c98c 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -1,10 +1,11 @@ "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { useStore } from "zustand"; import { tokenManager } from "@/entities/auth/lib/token-manager"; import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; -import { KoreaBasinMap } from "../lib/korea-basin-map"; +import { KoreaBasinMap, basinKeyMap } from "../lib/korea-basin-map"; import { Chart as ChartJS, CategoryScale, @@ -48,8 +49,7 @@ interface UserInfo { function DashboardPage() { const [userInfo, setUserInfo] = useState(null); const [error, setError] = useState(""); - const router = useRouter(); - + const router = useRouter(); useEffect(() => { const fetchUserInfo = async () => { console.log("fetchUserInfo called"); diff --git a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx index 7003cc130..2615a75e2 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx @@ -8,13 +8,17 @@ import BasinSelect from "./basIn-select"; import { Doughnut } from "react-chartjs-2"; import type { Chart as ChartJS } from "chart.js"; // import type { OperFcltContent } from "../model/fo-interface"; -import { OperationApi, operFcltStore } from "../api/fo-service"; +import { OperationApi, operFcltStore, basinStore } from "../api/fo-service"; const FoFcltCntRt: React.FC = () => { const [year, setYear] = useState(null); - const [selectedBasin, setSelectedBasin] = useState("한강"); + const { selectedBasin, setSelectedBasin } = useStore(basinStore); const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus); + const onBasinChange = (value: string) => { + setSelectedBasin(value); + }; + useEffect(() => { const fetchData = async () => { try { @@ -55,7 +59,7 @@ const FoFcltCntRt: React.FC = () => {

수계별 시설운영 개소수 및 가동률

- +
diff --git a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx index a2cc12168..09ef77ba3 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx @@ -3,6 +3,7 @@ * 수계별 유입ㆍ방류수질 분석 */ import React, { useEffect, useState, useMemo } from 'react'; +import { useStore } from "zustand"; import { Line } from 'react-chartjs-2'; import BasinSelect from "./basIn-select"; import { @@ -11,7 +12,7 @@ import { Title, Tooltip, Legend, type ChartOptions, } from 'chart.js'; import type { OperWqltContent } from "../model/fo-interface"; -import { OperationApi } from "../api/fo-service"; +import { OperationApi, basinStore } from "../api/fo-service"; import { defaultInitOperWqltState } from "../api/fo-service"; // Chart.js 등록 @@ -20,11 +21,15 @@ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, T type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN'; const FoWqltInflEffl: React.FC = () => { - const [selectedBasin, setSelectedBasin] = useState("한강"); + const { selectedBasin, setSelectedBasin } = useStore(basinStore); const [selectedTab, setSelectedTab] = useState('BOD'); const [wqltData, setWqltData] = useState([]); const tabs: WaterQualityType[] = ['BOD', 'TOC', 'SS', 'TP', 'TN']; + const onBasinChange = (value: string) => { + setSelectedBasin(value); + }; + useEffect(() => { setWqltData(defaultInitOperWqltState.operWqltStatus); const fetchData = async () => { @@ -192,7 +197,7 @@ const FoWqltInflEffl: React.FC = () => {

수계별 유입ㆍ방류수질 분석

- +