cm: 더보기, eslint 수정

This commit is contained in:
ljpark
2025-12-12 18:14:53 +09:00
parent d117e1c651
commit 38e41feef2
13 changed files with 202 additions and 119 deletions
+3 -2
View File
@@ -7,5 +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_SN=https://devlion.hasuon.co.kr
NEXT_PUBLIC_WR=https://devtiger.hasuon.co.kr
+5 -1
View File
@@ -4,4 +4,8 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
NEXT_PUBLIC_SITE_ID=CS
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
NEXT_PUBLIC_SERVER_API_URL2=http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_SN=https://lion.hasuon.co.kr
NEXT_PUBLIC_WR=https://tiger.hasuon.co.kr
+5
View File
@@ -12,6 +12,11 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
languageOptions: {
parserOptions: {
tsconfigRootDir: __dirname,
},
},
rules: {
"react-hooks/exhaustive-deps": "off",
},
+2 -2
View File
@@ -58,7 +58,7 @@ export function middleware(req: NextRequest) {
const userId = req.cookies.get(AUTH_USER_ID)?.value;
// const decodedQuery = toDecodedQueryString(searchParams);
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`;
const newUrl = `${serverApiUrl2}${pathname.replace("/server", "")}${searchParams ? `?${searchParams}` : ""}`;
// const newUrl = `${serverApiUrl2}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
@@ -96,7 +96,7 @@ export function middleware(req: NextRequest) {
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
// const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`;
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`;
const newUrl = `${serverApiUrl2}${pathname.replace("/server", "")}${searchParams ? `?${searchParams}` : ""}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
+34 -2
View File
@@ -50,14 +50,41 @@ export type OperFcltStateActions = {
setOperFcltStatus: (operFcltStatus: OperFcltContent[] | null) => void;
};
export type OperFcltStore = OperFcltState & OperFcltStateActions;
const initialOperFclt: OperFcltContent[]= [
{
"cnt": 710,
"fctCpc": 2911609.0,
"rvbsn": "금강",
"gubun": "R"
},
{
"cnt": 1032,
"fctCpc": 7506047.0,
"rvbsn": "낙동강",
"gubun": "R"
},
{
"cnt": 1261,
"fctCpc": 2061610.0,
"rvbsn": "영산강",
"gubun": "R"
},
{
"cnt": 983,
"fctCpc": 1.3229809E7,
"rvbsn": "한강",
"gubun": "R"
}
];
export const defaultInitOperFcltState: OperFcltState = {
operFcltStatus: null,
operFcltStatus: initialOperFclt,
};
export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFcltState) =>
createStore<OperFcltStore>()((set) => ({
...initState,
setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })),
}));
export const operFcltStore = createOperFcltStore();
//수계별 유입ㆍ방류수질 분석
export type OperWqltState = {
@@ -87,7 +114,12 @@ const initialWqltData: OperWqltContent[] = [
export const defaultInitOperWqltState: OperWqltState = {
operWqltStatus: initialWqltData,
};
export const operFcltStore = createOperFcltStore();
export const createOperWqltStore = (initState: OperWqltState = defaultInitOperWqltState) =>
createStore<OperWqltStore>()((set) => ({
...initState,
setOperWqltStatus: (operWqltStatus) => set(() => ({ operWqltStatus })),
}));
export const operWqltStore = createOperWqltStore();
// 물재이용 현황
export type ReuseState = {
@@ -1,7 +1,18 @@
import React, { useEffect, useRef } from "react";
"use client";
/**
* 유역 지도
*/
import React, { useEffect, useRef, useState } from "react";
import { useStore } from "zustand";
import styles from "./korea-basin-map.module.css"; // 추가
import { basinStore } from "../api/fo-service";
// import { operFcltStore } from "../api/fo-service";
import {
Tabs,
// TabsContent,
TabsList,
TabsTrigger,
} from "@common/components";
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
@@ -77,9 +88,11 @@ export function KoreaBasinMap() {
const badgeRef = useRef<HTMLDivElement>(null);
const rowsRef = useRef<HTMLTableSectionElement>(null);
const colTitleRef = useRef<HTMLTableCellElement>(null);
const [tab, setTab] = useState("물재이용");
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
// const { operFcltStatus, setSelectedBasin } = useStore(operFcltStore);
const selectedBasinKey = basinKeyMap[selectedBasin];
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
useEffect(() => {
if (selectedBasinKey) {
@@ -102,12 +115,12 @@ export function KoreaBasinMap() {
// console.log("Loading SVG from:", SVG_URL);
fetch(SVG_URL)
.then((r) => {
console.log("SVG fetch response:", r.status, r.statusText);
// console.log("SVG fetch response:", r.status, r.statusText);
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
return r.text();
})
.then((txt) => {
console.log("SVG content loaded, length:", txt.length);
// console.log("SVG content loaded, length:", txt.length);
if (!svgLayerRef.current) {
console.error("svgLayerRef.current is null");
return;
@@ -118,7 +131,7 @@ export function KoreaBasinMap() {
console.error("SVG element not found in loaded content");
return;
}
console.log("SVG element found:", svg);
// console.log("SVG element found:", svg);
// viewBox/크기 세팅
if (!svg.hasAttribute("viewBox")) {
@@ -138,13 +151,13 @@ export function KoreaBasinMap() {
// 1) 지도 그룹 수집
gs = [];
console.log("Looking for basin groups in SVG...");
// console.log("Looking for basin groups in SVG...");
BASIN_IDS.forEach((item) => {
let g: SVGGElement | null = null;
for (const id of item.labels) {
g = svg!.getElementById(id) as SVGGElement | null;
if (g) {
console.log(`Found group for ${item.key} with id '${id}':`, g);
// console.log(`Found group for ${item.key} with id '${id}':`, g);
break;
}
}
@@ -167,19 +180,19 @@ export function KoreaBasinMap() {
gs.push(g);
});
console.log(`Found ${gs.length} basin groups`);
// console.log(`Found ${gs.length} basin groups`);
// 각 유역 그룹의 패스 정보 확인
gs.forEach((g, index) => {
const paths = g.querySelectorAll('path');
console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
element: g,
pathCount: paths.length,
paths: Array.from(paths).map(p => ({
className: p.className.baseVal,
d: p.getAttribute('d')?.substring(0, 50) + '...'
}))
});
// console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
// element: g,
// pathCount: paths.length,
// paths: Array.from(paths).map(p => ({
// className: p.className.baseVal,
// d: p.getAttribute('d')?.substring(0, 50) + '...'
// }))
// });
});
// 2) 함수들 먼저 정의
@@ -204,7 +217,7 @@ export function KoreaBasinMap() {
// 유역정보
function showFor(g: SVGGElement) {
console.log("showFor called for:", g);
// console.log("showFor called for:", g);
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
const data = (BASIN_DATA as Record<string, { name: string; colTitle: string; rows: (string | number)[][] }>)[key] || {
name: (g as SVGGElement & { dataset: { label: string } }).dataset.label,
@@ -230,7 +243,7 @@ export function KoreaBasinMap() {
const cx = (r.left + r.right) / 2 - sRect.left;
const ty = r.top - sRect.top;
console.log("Popup position:", { cx, ty });
// console.log("Popup position:", { cx, ty });
if (badgeRef.current) {
badgeRef.current.style.left = cx + "px";
@@ -244,7 +257,7 @@ export function KoreaBasinMap() {
if (styles.show) {
popupRef.current.classList.add(styles.show);
}
console.log("Added show class to popup:", popupRef.current.classList.toString());
// console.log("Added show class to popup:", popupRef.current.classList.toString());
}
if (badgeRef.current && styles.show) {
badgeRef.current.classList.add(styles.show);
@@ -262,7 +275,7 @@ export function KoreaBasinMap() {
}
function showForWithLabel(g: SVGGElement) {
console.log("showForWithLabel called for:", g);
// console.log("showForWithLabel called for:", g);
showFor(g);
const idx = gs.indexOf(g);
labels.forEach((l) => l.classList.remove("active"));
@@ -384,79 +397,103 @@ export function KoreaBasinMap() {
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
return (
<div
ref={stageRef}
id="stage"
style={
{
aspectRatio: 0.72/1,
width: "100%",
maxHeight: "100%",
position: "relative",
margin: "auto",
"--map-inset": "0%",
} as React.CSSProperties
}
>
<div
className={styles.bgLayer}
id="bgLayer"
/>
<div
ref={svgLayerRef}
id="svgLayer"
className={`${styles.svgLayer} ${styles.svgContainer}`}
style={{ width: "100%", height: "100%" }}
/>
<div
ref={labelsLayerRef}
id="labelsLayer"
className={styles.labelsLayer}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
}}
/>
<div
ref={badgeRef}
id="badge"
className={styles.badge}
style={{ position: "absolute", zIndex: 10, display: "block" }}
/>
<div
ref={popupRef}
id="popup"
className={styles.popup}
style={{
position: "absolute",
zIndex: 20,
background: "#fff",
border: "1px solid #ccc",
borderRadius: 8,
padding: 12,
minWidth: 160,
boxShadow: "0 2px 8px #0002",
display: "block",
opacity: 0,
visibility: "hidden",
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th>구분</th>
<th ref={colTitleRef} id="colTitle">
시설수
</th>
</tr>
</thead>
<tbody ref={rowsRef} id="rows"></tbody>
</table>
<div className="col-span-3 !p-0 relative z-10" style={{ height: "765px", background: "rgba(255, 255, 255, 0.6)" }}>
<div className="card card-border" style={{ height: "100%" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">유역별 정보</h3>
<Tabs value={tab} onValueChange={setTab} className="border-0">
<div className="flex justify-center">
<TabsList className="mx-auto border-0 pt-0 pb-0 pl-0 pr-0">
<TabsTrigger value="물재이용">
물재이용
</TabsTrigger>
<TabsTrigger value="하수관로">
하수관로
</TabsTrigger>
<TabsTrigger value="운영실태">
운영실태
</TabsTrigger>
</TabsList>
</div>
</Tabs>
</div>
<div id="mapArea" style={{height: "calc(100% - 40px)"}}>
<div
ref={stageRef}
id="stage"
style={
{
aspectRatio: 0.72/1,
width: "100%",
maxHeight: "100%",
position: "relative",
margin: "auto",
"--map-inset": "0%",
} as React.CSSProperties
}
>
<div
className={styles.bgLayer}
id="bgLayer"
/>
<div
ref={svgLayerRef}
id="svgLayer"
className={`${styles.svgLayer} ${styles.svgContainer}`}
style={{ width: "100%", height: "100%" }}
/>
<div
ref={labelsLayerRef}
id="labelsLayer"
className={styles.labelsLayer}
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
}}
/>
<div
ref={badgeRef}
id="badge"
className={styles.badge}
style={{ position: "absolute", zIndex: 10, display: "block" }}
/>
<div
ref={popupRef}
id="popup"
className={styles.popup}
style={{
position: "absolute",
zIndex: 20,
background: "#fff",
border: "1px solid #ccc",
borderRadius: 8,
padding: 12,
minWidth: 160,
boxShadow: "0 2px 8px #0002",
display: "block",
opacity: 0,
visibility: "hidden",
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th>구분</th>
<th ref={colTitleRef} id="colTitle">
시설수
</th>
</tr>
</thead>
<tbody ref={rowsRef} id="rows"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -138,16 +138,7 @@ function DashboardPage() {
</div>
{/* 지도영역 */}
<div className="col-span-3 !p-0 relative z-10" style={{ height: "765px", background: "rgba(255, 255, 255, 0.6)" }}>
<div className="card card-border" style={{ height: "100%" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">유역별 정보</h3>
</div>
<div id="mapArea" style={{height: "calc(100% - 40px)"}}>
<KoreaBasinMap />
</div>
</div>
</div>
<KoreaBasinMap />
{/* 사용자 정보 카드 */}
<div className="card card-border col-span-1 h-80 p-10">
@@ -51,7 +51,7 @@ const FoFcltCntRt: React.FC = () => {
operatingRate,
chartData: [operatingCapacity, nonOperatingCapacity],
};
console.log(result);
// console.log(result);
return result;
}, [operFcltStatus, selectedBasin]);
@@ -37,7 +37,7 @@ const FoWqltInflEffl: React.FC = () => {
const standardYear = await OperationApi.getStandardYear();
const data = await OperationApi.getDashOperWqlt(standardYear);
setWqltData(data);
console.log(wqltData);
// console.log(wqltData);
} catch (e) {
console.error("시설 유입ㆍ방류수질 데이터 조회 실패", e);
}
@@ -66,7 +66,7 @@ const FoWqltInflEffl: React.FC = () => {
return { labels, inflowData, effluentData, inflowAvg, effluentAvg };
}, [wqltData, selectedBasin, selectedTab]);
console.log(chartData);
// console.log(chartData);
// 차트 공통 옵션
// const getChartOptions = (yAxisLabel: string): ChartOptions<'line'> => ({
@@ -175,11 +175,15 @@ const ReuseChart: React.FC = () => {
startSlider();
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">{chartItems[activeIndex]?.title}</h3>
<Button className="h-[3.2rem]">더보기</Button>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-60 p-4 relative overflow-hidden">
<div
@@ -26,11 +26,15 @@ const ReuseStatus: React.FC = () => {
return new Intl.NumberFormat('ko-KR').format(num);
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">물재이용 현황 (재이용량, m³)</h3>
<Button className="h-[3.2rem]">더보기</Button>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-70 p-4">
<div className="w-full h-full flex items-center justify-center">
@@ -164,11 +164,15 @@ export default function SnOldPpl() {
}
};
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">노후 하수관로 현황</h3>
<Button className="h-[3.2rem]">더보기</Button>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-70 p-0 flex items-center justify-center">
<div style={{ width: "100%", maxWidth: 400, height: 131 }}>
@@ -184,14 +184,15 @@ export default function SnPplStatus() {
},
};
//const data = { labels, datasets };
const handleMoreClick = () => {
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
};
return (
<div className="card card-border" style={{ height: "250px" }}>
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
<Button className="h-[3.2rem]">더보기</Button>
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
</div>
<div className="h-60 p-4">
<div style={{ width: "100%", maxWidth: 400, height: 150 }}>