Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
+1
-1
@@ -9,4 +9,4 @@ 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_URL3=https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_SERVER_API_URL3=https://apigateway.apps.rsonesoft.com
|
||||
|
||||
@@ -4,4 +4,8 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000
|
||||
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
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -59,7 +59,6 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
// const decodedQuery = toDecodedQueryString(searchParams);
|
||||
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
|
||||
// const newUrl = `${serverApiUrl2}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
@@ -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,31 +1,69 @@
|
||||
// apps/cm/src/pages/dashboard/api/sn-service.ts
|
||||
import { client } from "@/shared/api";
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import { create } from "zustand";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PplLengthContent, SewerageService } from "../model/sn-interface";
|
||||
import type { OldPplChartData } from "../model/sn-interface";
|
||||
|
||||
export type SnState = {
|
||||
snStatus: PplLengthContent[] | null;
|
||||
};
|
||||
|
||||
export type SnStateActions = {
|
||||
setSnStatus: (snStatus: PplLengthContent[] | null) => void;
|
||||
};
|
||||
|
||||
export type SnStore = SnState & SnStateActions;
|
||||
|
||||
export const defaultInitState: SnState = {
|
||||
snStatus: null,
|
||||
const initialOldPplChartData: OldPplChartData={
|
||||
labels: [
|
||||
"금강",
|
||||
"한강",
|
||||
"낙동강",
|
||||
"영산강"
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: "20-25년",
|
||||
data: [
|
||||
1140,
|
||||
3181,
|
||||
3100,
|
||||
1662
|
||||
],
|
||||
backgroundColor: "#3B82F6",
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: "25-30년",
|
||||
data: [
|
||||
1391,
|
||||
3615,
|
||||
1814,
|
||||
1411
|
||||
],
|
||||
backgroundColor: "#EF4444",
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: "30년이상",
|
||||
data: [
|
||||
2014,
|
||||
10303,
|
||||
9831,
|
||||
2000
|
||||
],
|
||||
backgroundColor: "#F87171",
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const createSnStore = (initState: SnState = defaultInitState) =>
|
||||
createStore<SnStore>()((set) => ({
|
||||
...initState,
|
||||
setSnStatus: (snStatus) => set(() => ({ snStatus })),
|
||||
}));
|
||||
|
||||
// 전역으로 공유할 store 인스턴스
|
||||
// export const snStore = createSnStore();
|
||||
type OldPplStoreState = {
|
||||
chartData: OldPplChartData;
|
||||
setChartData: (chartData: OldPplChartData) => void;
|
||||
};
|
||||
export const useOldPplStore = create<OldPplStoreState>((set) => ({
|
||||
chartData: initialOldPplChartData,
|
||||
setChartData: (chartData) => set(() => ({ chartData })),
|
||||
}));
|
||||
|
||||
export class SewerageApiService implements SewerageService {
|
||||
// 기간별 관로 총길이
|
||||
@@ -43,6 +81,7 @@ export class SewerageApiService implements SewerageService {
|
||||
...item,
|
||||
sumPrlgVl: Number(item.sumPrlgVl),
|
||||
}));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,22 @@
|
||||
"use client";
|
||||
/**
|
||||
* 유역 지도
|
||||
*/
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useStore } from "zustand";
|
||||
import styles from "./korea-basin-map.module.css"; // 추가
|
||||
import { basinStore } from "../api/fo-service";
|
||||
import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data";
|
||||
import { BASIN_DATA, BASIN_IDS } from "../model/map-data";
|
||||
// import { operFcltStore } from "../api/fo-service";
|
||||
// import {Button} from "@common/components";
|
||||
|
||||
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 },
|
||||
nakdong: { x: 0.64, y: 0.47 },
|
||||
yeongsan: { x: 0.18, y: 0.77 },
|
||||
};
|
||||
|
||||
const BASIN_DATA = {
|
||||
han: {
|
||||
name: "한강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
geum: {
|
||||
name: "금강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
nakdong: {
|
||||
name: "낙동강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
yeongsan: {
|
||||
name: "영산강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const BASIN_IDS = [
|
||||
{ key: "han", labels: ["한강", "han"] },
|
||||
{ key: "geum", labels: ["금강", "geum"] },
|
||||
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
|
||||
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
|
||||
];
|
||||
|
||||
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);
|
||||
@@ -76,10 +24,12 @@ export function KoreaBasinMap() {
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const badgeRef = useRef<HTMLDivElement>(null);
|
||||
const rowsRef = useRef<HTMLTableSectionElement>(null);
|
||||
const colTitleRef = useRef<HTMLTableCellElement>(null);
|
||||
|
||||
const colTitleRef = useRef<HTMLTableCellElement>(null);
|
||||
// const [services, setServices] = useState(svcInfo);
|
||||
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
|
||||
// const { operFcltStatus, setSelectedBasin } = useStore(operFcltStore);
|
||||
const selectedBasinKey = basinKeyMap[selectedBasin];
|
||||
|
||||
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
|
||||
useEffect(() => {
|
||||
if (selectedBasinKey) {
|
||||
@@ -102,12 +52,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 +68,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 +88,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,20 +117,20 @@ 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) + '...'
|
||||
}))
|
||||
});
|
||||
});
|
||||
// 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) + '...'
|
||||
// // }))
|
||||
// // });
|
||||
// });
|
||||
|
||||
// 2) 함수들 먼저 정의
|
||||
const clear = () => {
|
||||
@@ -204,7 +154,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 +180,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 +194,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 +212,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"));
|
||||
@@ -382,81 +332,110 @@ export function KoreaBasinMap() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// const handleSvcInfoClick = (clickedSvcId: string) => {
|
||||
// setServices(prevServices =>
|
||||
// prevServices.map(svc => ({
|
||||
// ...svc,
|
||||
// active: svc.svcId === clickedSvcId,
|
||||
// }))
|
||||
// );
|
||||
// };
|
||||
// 스타일은 필요에 따라 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>
|
||||
{/*
|
||||
{services.map((svc) => (
|
||||
<Button
|
||||
key={svc.name}
|
||||
className="h-[3.2rem]"
|
||||
color={svc.active ? "primary" : "white"}
|
||||
onClick={() => handleSvcInfoClick(svc.svcId)}
|
||||
>
|
||||
{svc.name}
|
||||
</Button>
|
||||
))}
|
||||
*/}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
export const basinKeyMap: { [key: string]: string } = {
|
||||
"한강": "han",
|
||||
"금강": "geum",
|
||||
"낙동강": "nakdong",
|
||||
"영산강": "yeongsan",
|
||||
};
|
||||
|
||||
// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 ---
|
||||
// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리)
|
||||
// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준)
|
||||
export const MANUAL_LABEL_POS = {
|
||||
han: { x: 0.52, y: 0.14 },
|
||||
geum: { x: 0.28, y: 0.47 },
|
||||
nakdong: { x: 0.64, y: 0.47 },
|
||||
yeongsan: { x: 0.18, y: 0.77 },
|
||||
};
|
||||
|
||||
export const BASIN_DATA = {
|
||||
han: {
|
||||
name: "한강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
geum: {
|
||||
name: "금강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
nakdong: {
|
||||
name: "낙동강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
yeongsan: {
|
||||
name: "영산강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const BASIN_IDS = [
|
||||
{ key: "han", labels: ["한강", "han"] },
|
||||
{ key: "geum", labels: ["금강", "geum"] },
|
||||
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
|
||||
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
|
||||
];
|
||||
|
||||
export const svcInfo = [
|
||||
{svcId: "wr", active: true, name:"물재이용"},
|
||||
{svcId: "sn", active: false, name:"하수관로"},
|
||||
{svcId: "fo", active: false, name:"운영실태"},
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { basinKeyMap } from "../model/map-data";
|
||||
export interface PplLengthContent {
|
||||
zoneNm: string;
|
||||
sumPrlgVl: number;
|
||||
@@ -6,4 +7,56 @@ export interface PplLengthContent {
|
||||
export interface SewerageService {
|
||||
getPplLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
|
||||
getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
|
||||
}
|
||||
|
||||
export interface ChartDataSet {
|
||||
label: string;
|
||||
data: (number | undefined)[];
|
||||
backgroundColor: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
export interface OldPplChartData {
|
||||
labels: string[];
|
||||
datasets: ChartDataSet[];
|
||||
}
|
||||
|
||||
type SnOldPplItem = {
|
||||
name: string;
|
||||
colTitle: string[];
|
||||
rows: number[];
|
||||
};
|
||||
|
||||
type SnOldPplLength = {
|
||||
[key: string]: SnOldPplItem;
|
||||
};
|
||||
|
||||
export function buildSnOldPplLength(
|
||||
chartData: {
|
||||
labels: string[];
|
||||
datasets: { label: string; data: number[] }[];
|
||||
},
|
||||
totalLengths: number[]
|
||||
): SnOldPplLength {
|
||||
const colTitle = ["총길이", ...chartData.datasets.map((ds) => ds.label)];
|
||||
|
||||
const result: SnOldPplLength = {};
|
||||
|
||||
chartData.labels.forEach((basinName, index) => {
|
||||
const key = basinKeyMap[basinName]; // geum, han, nakdong, yeongsan
|
||||
if (!key) return; // 매핑 안되면 스킵
|
||||
const total = totalLengths[index] ?? 0;
|
||||
const datasetValues = chartData.datasets.map(
|
||||
(ds) => ds.data[index] ?? 0,
|
||||
);
|
||||
result[key] = {
|
||||
name: basinName,
|
||||
colTitle,
|
||||
rows: [
|
||||
total, // 총길이 (undefined면 0)
|
||||
...datasetValues, // 각 연차 데이터 (undefined면 0)
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
Tooltip,
|
||||
ArcElement,
|
||||
} from "chart.js";
|
||||
// import { Doughnut } from "react-chartjs-2";
|
||||
import SnPplStatus from "./sn-ppl-status";
|
||||
import SnOldPpl from "./sn-old-ppl";
|
||||
import FoFcltCntRt from "./fo-fclt-cnt-rt";
|
||||
@@ -139,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">
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
ArcElement,
|
||||
} from "chart.js";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
|
||||
// Chart.js 등록
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Filler,
|
||||
Legend,
|
||||
Tooltip,
|
||||
ArcElement
|
||||
);
|
||||
|
||||
interface UserInfo {
|
||||
email?: string;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
twoFactorEnabled?: boolean;
|
||||
}
|
||||
|
||||
function DashboardPage() {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserInfo = async () => {
|
||||
console.log("fetchUserInfo called");
|
||||
|
||||
try {
|
||||
// tokenManager를 통해 토큰 확인
|
||||
const accessToken = tokenManager.getToken("Authorization");
|
||||
const userId = tokenManager.getToken("token-id");
|
||||
|
||||
if (!accessToken || !userId) {
|
||||
console.log("No tokens found, redirecting to login");
|
||||
setError("로그인이 필요합니다.");
|
||||
router.push("/auth/login");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Fetching user info with userId:", userId);
|
||||
|
||||
// client를 사용하여 API 호출 (자동 토큰 처리)
|
||||
const response = await client.get(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/${userId}`,
|
||||
);
|
||||
|
||||
if (response.data) {
|
||||
console.log("User info received:", response.data);
|
||||
setUserInfo(response.data);
|
||||
} else {
|
||||
setError("사용자 정보를 불러올 수 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching user info:", error);
|
||||
if (error instanceof Error && error.message.includes("401")) {
|
||||
setError("인증이 만료되었습니다. 다시 로그인해주세요.");
|
||||
tokenManager.clearAuthToken();
|
||||
router.push("/auth/login");
|
||||
} else {
|
||||
setError("네트워크 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserInfo();
|
||||
}, [router]);
|
||||
|
||||
const handleLogout = () => {
|
||||
console.log("Logging out...");
|
||||
// localStorage 정리
|
||||
localStorage.removeItem("tempToken");
|
||||
// tokenManager를 통해 쿠키 정리
|
||||
tokenManager.clearAuthToken();
|
||||
router.push("/auth/logout");
|
||||
};
|
||||
|
||||
const handleChangePassword = () => {
|
||||
router.push("/otp?next=change-password");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* 첫번째 라인 */}
|
||||
<div className="grid grid-cols-8 gap-1">
|
||||
{/* 첫번쨰 열 */}
|
||||
<div className="col-span-2 h-250 !p-0">
|
||||
{/* 전국 하수처리시설 가동률 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">전국 하수처리시설 가동률</h3>
|
||||
</div>
|
||||
<div className="h-60 p-4">
|
||||
<Doughnut
|
||||
data={{
|
||||
datasets: [{
|
||||
data: [75, 25], // 75% 가동률
|
||||
backgroundColor: ['#00DFE5', '#f5f5f5'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
}}
|
||||
options={{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '70%',
|
||||
circumference: 180, // 반원
|
||||
rotation: -90, // 왼쪽에서 시작
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}}
|
||||
plugins={[{
|
||||
id: 'centerText',
|
||||
afterDraw: (chart) => {
|
||||
const ctx = chart.ctx;
|
||||
const centerX = chart.width / 2;
|
||||
const centerY = chart.height / 2;
|
||||
|
||||
// 가동률 텍스트
|
||||
ctx.save();
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.fillStyle = '#00DFE5';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('75%', centerX, centerY + 10);
|
||||
|
||||
// 라벨 텍스트
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText('가동률', centerX, centerY + 30);
|
||||
ctx.restore();
|
||||
}
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오수관로 및 우수관로 현황 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
|
||||
</div>
|
||||
<div className="h-60 p-4"></div>
|
||||
</div>
|
||||
|
||||
{/* 하수처리수 재이용 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">하수처리수 재이용</h3>
|
||||
</div>
|
||||
<div className="h-60 p-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 두번쨰 열 */}
|
||||
<div className="col-span-2 h-250 !p-0">
|
||||
{/* 하수처리시설(개소) */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">하수처리시설(개소)</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1 h-60 !p-0">
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground text-center">
|
||||
<h3 className="text-l font-medium" style={{textAlign: "center", width: "100%"}}>500㎥ 미만(개소)</h3>
|
||||
</div>
|
||||
<div style={{fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a", padding: "2px 0"}}>3,567 <span style={{fontSize: "14px", color: "#000000"}}>개소</span></div>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground text-center">
|
||||
<h3 className="text-l font-medium" style={{textAlign: "center", width: "100%"}}>500㎥ 이상(개소)</h3>
|
||||
</div>
|
||||
<div style={{fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a", padding: "2px 0"}}>767 <span style={{fontSize: "14px", color: "#000000"}}>개소</span></div>
|
||||
</div>
|
||||
<div className="col-span-2" style={{width: "100%", textAlign: "right"}}>기준년월 : 2025.09 </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 노후하수관로현황 */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">노후하수관로현황</h3>
|
||||
</div>
|
||||
<div className="h-70 p-4">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 재이용시설(개소) */}
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">재이용시설(개소)</h3>
|
||||
</div>
|
||||
<div className="h-70 p-4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 지도영역 */}
|
||||
<div className="col-span-3 h-200 !p-0 relative z-10">
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">유역별 정보</h3>
|
||||
</div>
|
||||
<KoreaBasinMap />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 사용자 정보 카드 */}
|
||||
<div className="card card-border col-span-1 h-80 p-10">
|
||||
<h2 className="mb-6 text-2xl">사용자 정보</h2>
|
||||
{error && <div className="mb-4 text-red-500">{error}</div>}
|
||||
{userInfo ? (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<b>아이디:</b> {userInfo.userId}
|
||||
</div>
|
||||
<div>
|
||||
<b>이메일:</b> {userInfo.email || userInfo.userId}
|
||||
</div>
|
||||
<div>
|
||||
<b>이름:</b> {userInfo.userName || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<b>2FA 활성화:</b>{" "}
|
||||
{userInfo.twoFactorEnabled ? "예" : "아니오"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
!error && <div>사용자 정보를 불러오는 중...</div>
|
||||
)}
|
||||
<button className="btn mb-2 w-full" onClick={handleLogout}>
|
||||
로그아웃
|
||||
</button>
|
||||
<button className="btn w-full" onClick={handleChangePassword}>
|
||||
패스워드 변경
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 두번째 라인 */}
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
<div className="card card-border"id="grouping-statistics">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">지원현황</h3>
|
||||
</div>
|
||||
<div className="h-70 p-4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">자료관 자료현황</h3>
|
||||
</div>
|
||||
<div className="h-70 p-4">
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">공지사항</h3>
|
||||
</div>
|
||||
<div className="h-70 p-4">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 배너 영역 */}
|
||||
<div className="card card-border">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
@@ -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">
|
||||
|
||||
@@ -15,15 +15,10 @@ import {
|
||||
} from "chart.js";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { Button } from "@common/components";
|
||||
import type { PplLengthContent } from "../model/sn-interface";
|
||||
import { SewerageApi } from "../api/sn-service";
|
||||
import type { PplLengthContent, ChartDataSet } from "../model/sn-interface";
|
||||
import { SewerageApi, useOldPplStore } from "../api/sn-service";
|
||||
|
||||
|
||||
interface ChartDataSet {
|
||||
label: string;
|
||||
data: (number | undefined)[];
|
||||
backgroundColor: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
// ChartJS 컴포넌트 등록
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
@@ -31,13 +26,11 @@ export default function SnOldPpl() {
|
||||
const [oldPpl2025Data, setOldPpl2025Data] = useState<PplLengthContent[]>([]);
|
||||
const [oldPpl2530Data, setOldPpl2530Data] = useState<PplLengthContent[]>([]);
|
||||
const [oldPpl30Data, setOldPpl30Data] = useState<PplLengthContent[]>([]);
|
||||
const [chartData, setChartData] = useState<{
|
||||
labels: string[];
|
||||
datasets: ChartDataSet[];
|
||||
}>({ labels: [], datasets: [] });
|
||||
const {chartData, setChartData} = useOldPplStore();
|
||||
|
||||
useEffect(() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const ranges = {
|
||||
"20-25년": { from: `${currentYear - 25 + 1}0101`, to: `${currentYear - 20}1231`, setter: setOldPpl2025Data },
|
||||
"25-30년": { from: `${currentYear - 30 + 1}0101`, to: `${currentYear - 25}1231`, setter: setOldPpl2530Data },
|
||||
@@ -164,11 +157,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 }}>
|
||||
|
||||
Reference in New Issue
Block a user