From 6ec9e51fb47fa4b80732726c239419142b52bfce Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 12 Dec 2025 10:44:06 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20pnpm=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(10.25.0=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f3833d7e..188fb1e29 100644 --- a/package.json +++ b/package.json @@ -49,5 +49,5 @@ } }, "license": "ISC", - "packageManager": "pnpm@9.12.3" + "packageManager": "pnpm@10.25.0" } \ No newline at end of file From e34776ccc21b1c36a915b3d576080deb9a09fab9 Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 12 Dec 2025 14:13:53 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/src/shared/config/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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: { From a42353467b0d3b9b4943792dcf00136ea5b92b3c Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 12 Dec 2025 14:31:52 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EA=B0=9C=EB=B0=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/middleware.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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; From 1bd5da63096c9f4e70323351609eefd612f7bd75 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 12 Dec 2025 14:51:35 +0900 Subject: [PATCH 4/5] =?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 = () => {

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

- +
From d117e1c6511c3c9a4bf21f4b388a1e57b248bd72 Mon Sep 17 00:00:00 2001 From: ljpark Date: Fri, 12 Dec 2025 15:28:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?cm:=EC=A7=80=EB=8F=84=EC=9C=A0=EC=97=AD?= =?UTF-8?q?=EC=84=A0=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx | 8 +++++++- apps/cm/src/pages/dashboard/ui/dashboard-page.tsx | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) 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 8aab93966..1f1ec1ab7 100644 --- a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -78,7 +78,7 @@ export function KoreaBasinMap() { const rowsRef = useRef(null); const colTitleRef = useRef(null); - const { selectedBasin } = useStore(basinStore); + const { selectedBasin, setSelectedBasin } = useStore(basinStore); const selectedBasinKey = basinKeyMap[selectedBasin]; // selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect useEffect(() => { @@ -351,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 37d43248e..96d98ade4 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -1,11 +1,10 @@ "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, basinKeyMap } from "../lib/korea-basin-map"; +import { KoreaBasinMap } from "../lib/korea-basin-map"; import { Chart as ChartJS, CategoryScale,