diff --git a/apps/cm/.env.local b/apps/cm/.env.local index 8b33b38e8..af93107b4 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -7,6 +7,6 @@ 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/ +NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ +# NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ NEXT_PUBLIC_SERVER_API_URL_RSONE=https://apigateway.apps.rsonesoft.com/ \ 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 989839187..1f1ec1ab7 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, setSelectedBasin } = 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; @@ -321,7 +351,13 @@ export function KoreaBasinMap() { clear(); g.classList.add("active"); showForWithLabel(g); + const basinKey = g.dataset.key as keyof typeof BASIN_DATA | undefined; + if (basinKey) { + const basinName = BASIN_DATA[basinKey].name; + setSelectedBasin(basinName); + } }); + }); if (stageRef.current) { stageRef.current.addEventListener("mouseleave", clear); diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index a1b61ddb2..e83f81c7e 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -50,8 +50,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 d31bc97fc..753edfc81 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 09c2db7d4..d265d2efc 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 = () => {

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

- +
diff --git a/apps/sn/middleware.ts b/apps/sn/middleware.ts index 97f4fcce7..c4da97544 100644 --- a/apps/sn/middleware.ts +++ b/apps/sn/middleware.ts @@ -44,8 +44,10 @@ export function middleware(req: NextRequest) { if (pathname.startsWith("/server")) { const decodedQuery = toDecodedQueryString(searchParams); - const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${decodedQuery ? `?${decodedQuery}` : "" - }`; + const replacedPath = pathname.replace("/server", ""); + // '/server//cms-service/..' 같은 케이스에서 '//' 정리 + const normalizedPath = replacedPath.replace(/^\/+/g, "/"); + const newUrl = `${SERVER_API_URL}${normalizedPath}${decodedQuery ? `?${decodedQuery}` : ""}`; console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`); const rewriteRequest = rewriteWith(newUrl, req, true); return rewriteRequest; diff --git a/apps/sn/src/shared/config/api.ts b/apps/sn/src/shared/config/api.ts index 3eba9fa91..fe2e39682 100644 --- a/apps/sn/src/shared/config/api.ts +++ b/apps/sn/src/shared/config/api.ts @@ -1,6 +1,9 @@ import { PROXY_HOST } from "./env"; -export const BASE_URL = `${PROXY_HOST}/server`; +// 브라우저에서는 동일 오리진으로 상대경로 호출 → CORS/localhost 고정 문제 방지 +// (Next middleware가 /server/* 요청을 백엔드로 rewrite) +// 서버 환경(SSR 등)에서는 절대 URL이 필요할 수 있어 기존 PROXY_HOST 기반 유지 +export const BASE_URL = typeof window === "undefined" ? `${PROXY_HOST}/server` : "/server"; export const ENDPOINT = { ATTACH_SERVICE: {