Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
+2
-2
@@ -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/
|
||||
@@ -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<YearStore>()((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<BasinStore>()((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<YearStore>()((set) => ({
|
||||
...initState,
|
||||
setYear: (year) => set(() => ({ year })),
|
||||
}));
|
||||
|
||||
// 전역으로 공유할 store 인스턴스
|
||||
export const operFcltStore = createOperFcltStore();
|
||||
export const reuseStore = createReuseStore();
|
||||
export const yearStore = createYearStore();
|
||||
|
||||
|
||||
export class OperationApiService implements OperationService {
|
||||
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const svgLayerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -65,12 +78,28 @@ export function KoreaBasinMap() {
|
||||
const rowsRef = useRef<HTMLTableSectionElement>(null);
|
||||
const colTitleRef = useRef<HTMLTableCellElement>(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);
|
||||
|
||||
@@ -50,8 +50,7 @@ interface UserInfo {
|
||||
function DashboardPage() {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const fetchUserInfo = async () => {
|
||||
console.log("fetchUserInfo called");
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [selectedBasin, setSelectedBasin] = useState<string>("한강");
|
||||
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 = () => {
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">수계별 시설운영 개소수 및 가동률</h3>
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={setSelectedBasin} />
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
|
||||
</div>
|
||||
<div className="h-60 p-4">
|
||||
<div className="flex gap-2 h-full">
|
||||
|
||||
@@ -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<string>("한강");
|
||||
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
|
||||
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
|
||||
const [wqltData, setWqltData] = useState<OperWqltContent[]>([]);
|
||||
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 = () => {
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">수계별 유입ㆍ방류수질 분석</h3>
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={setSelectedBasin} />
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
|
||||
</div>
|
||||
<div className="h-60">
|
||||
<div style={{ padding: '4px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user