예전 디자인

This commit is contained in:
ljpark
2025-12-09 10:47:28 +09:00
parent b0dbb1110f
commit 34b54c70de
11 changed files with 1174 additions and 111 deletions
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
import styles from "./KoreaBasinMap.module.css"; // 추가
import styles from "./korea-basin-map.module.css"; // 추가
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
@@ -0,0 +1,33 @@
"use client";
import React, { useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
const BasinSelect: React.FC = () => {
const [selectedBasin, setSelectedBasin] = useState<string>("한강유역");
const basins = ["한강유역", "금강유역", "낙동강유역", "영산강유역"];
return (
<Select value={selectedBasin} onValueChange={setSelectedBasin}>
<SelectTrigger className="w-[100px] h-8 bg-blue-500 text-white font-bold text-md border-none hover:bg-blue-600">
<SelectValue placeholder="유역 선택" />
</SelectTrigger>
<SelectContent>
{basins.map((basin) => (
<SelectItem key={basin} value={basin}>
{basin}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
export default BasinSelect;
+23 -110
View File
@@ -4,7 +4,7 @@ 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/KoreaBasinMap";
import { KoreaBasinMap } from "../lib/korea-basin-map";
import {
Chart as ChartJS,
CategoryScale,
@@ -17,7 +17,13 @@ import {
Tooltip,
ArcElement,
} from "chart.js";
import { Doughnut } from "react-chartjs-2";
// 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";
import FoWqltInflEffl from "./fo-wqlt-Infl-effl";
import ReuseChart from "./reuse-chart";
import ReuseStatus from "./reuse-status";
// Chart.js 등록
ChartJS.register(
@@ -108,121 +114,28 @@ function DashboardPage() {
<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>
{/* 전국 하수처리시설 개소수 및 가동률 */}
<FoFcltCntRt />
{/* 오수관로 및 우수관로 현황 */}
<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>
<SnPplStatus />
{/* 하수처리수 재이용 */}
<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>
{/* 하수처리수 재이용 (m³) */}
<ReuseChart />
</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>
{/* 수계별 유입ㆍ방류수질 분석 */}
<FoWqltInflEffl />
{/* 재이용시설(개소) */}
<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>
{/* 노후하수관로현황 */}
<SnOldPpl />
{/* 물재이용 현황 (재이용량, m³) */}
<ReuseStatus />
</div>
{/* 지도영역 */}
<div className="col-span-3 h-200 !p-0 relative z-10">
<div className="card card-border">
@@ -267,12 +180,12 @@ function DashboardPage() {
{/* 두번째 라인 */}
<div className="grid grid-cols-4 gap-1">
<div className="card card-border"id="grouping-statistics">
<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">
@@ -0,0 +1,303 @@
"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 };
@@ -0,0 +1,100 @@
/**
* 수계별 시설운영 개소수 및 가동률
*/
import React from 'react';
import BasinSelect from "./basIn-select";
import { Doughnut } from "react-chartjs-2";
const FoFcltCntRt: React.FC = () => {
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">수계별 시설운영 개소수 및 가동률</h3>
<BasinSelect />
</div>
<div className="h-60 p-4">
<div className="flex gap-2 h-full">
{/* 왼쪽: 개소수 카드 */}
<div className="flex-1">
<div className="card card-border h-full flex flex-col">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium mx-auto">개소수 (2024년도)</h3>
</div>
<div className="flex items-center justify-center flex-1">
<div style={{ fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a" }}>
3,567 <span style={{ fontSize: "14px", color: "#000000" }}>개소</span>
</div>
</div>
</div>
</div>
{/* 오른쪽: Doughnut 차트 */}
<div className="flex-1">
<div className="card card-border h-full flex flex-col">
<div className="card-header text-foreground text-center">
<h3 className="text-l font-medium mx-auto">가동률 (2024년도)</h3>
</div>
<div className="flex-1 flex items-start justify-center">
<div style={{ width: "100px", height: "100px", display: "flex", alignItems: "flex-start", justifyContent: "center" }}>
<Doughnut
data={{
datasets: [{
data: [75, 25], // 75% 가동률
backgroundColor: ['#00DFE5', '#f5f5f5'],
borderWidth: 0
}]
}}
options={{
responsive: true,
maintainAspectRatio: true,
cutout: '60%',
circumference: 180, // 반원
rotation: -90, // 왼쪽에서 시작
layout: {
padding: 0,
},
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 16px Arial';
ctx.fillStyle = '#00DFE5';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('75%', centerX, centerY + 10);
// 라벨 텍스트
ctx.font = '10px Arial';
ctx.fillStyle = '#666';
ctx.fillText('가동률', centerX, centerY + 30);
ctx.restore();
}
}]}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default FoFcltCntRt;
@@ -0,0 +1,228 @@
/**
* 수계별 유입ㆍ방류수질 분석
*/
import React, { useState } from 'react';
import { Line } from 'react-chartjs-2';
import BasinSelect from "./basIn-select";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from 'chart.js';
// Chart.js 등록
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN';
interface ChartData {
labels: string[];
inflowData: number[];
inflowAverage: number[];
effluentData: number[];
effluentAverage: number[];
}
const FoWqltInflEffl: React.FC = () => {
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
// 데이터 (실제로는 API에서 가져올 데이터)
const bodData: ChartData = {
labels: ['2020', '2021', '2022', '2023', '2024'],
inflowData: [2255.6, 2282.2, 2304.3, 5156.6, 4620.4],
inflowAverage: [2255.6, 2282.2, 2304.3, 2304.3, 2304.3],
effluentData: [55, 47.5, 52, 79.7, 83.9],
effluentAverage: [55, 47.5, 52, 52, 52],
};
const tabs: WaterQualityType[] = ['BOD', 'TOC', 'SS', 'TP', 'TN'];
// 차트 공통 옵션
const getChartOptions = (yAxisLabel: string): ChartOptions<'line'> => ({
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
usePointStyle: true,
boxWidth: 6,
color: '#000',
font: {
size: 8,
},
},
},
tooltip: {
enabled: true,
titleFont: { size: 10 },
bodyFont: { size: 10 },
},
},
scales: {
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)',
tickLength: 4, // 눈금 길이 축소
},
ticks: {
color: '#000',
font: {
size: 8, // 폰트 크기 축소
},
padding: 0,
},
},
y: {
grid: {
color: 'rgba(255, 255, 255, 0.1)',
},
ticks: {
color: '#000',
font: {
size: 8, // 폰트 크기 축소
},
maxTicksLimit: 4, // 눈금 개수 제한
padding: 2,
},
title: {
display: false,
},
},
},
layout: {
padding: {
top: 5,
bottom: 0,
left: 0,
right: 5
}
}
});
// 유입 데이터 차트 설정
const inflowChartData = {
labels: bodData.labels,
datasets: [
{
label: '유입BOD (mg/L)',
data: bodData.inflowData,
borderColor: '#00D4FF',
backgroundColor: '#00D4FF',
borderWidth: 1,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.4,
},
{
label: '유입 평균 (mg/L)',
data: bodData.inflowAverage,
borderColor: '#FF1493',
backgroundColor: '#FF1493',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 2,
pointHoverRadius: 4,
tension: 0,
},
],
};
// 발류 데이터 차트 설정
const effluentChartData = {
labels: bodData.labels,
datasets: [
{
label: '발류BOD (mg/L)',
data: bodData.effluentData,
borderColor: '#00FF88',
backgroundColor: '#00FF88',
borderWidth: 1,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.4,
},
{
label: '발류 평균 (mg/L)',
data: bodData.effluentAverage,
borderColor: '#FFB800',
backgroundColor: '#FFB800',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 1,
pointHoverRadius: 4,
tension: 0,
},
],
};
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">수계별 유입ㆍ방류수질 분석</h3>
<BasinSelect />
</div>
<div className="h-60">
<div style={{ padding: '4px', height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* 탭 메뉴 */}
<div style={{ display: 'flex', gap: '20px', borderBottom: '1px solid #000000', flexShrink: 0 }}>
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setSelectedTab(tab)}
style={{
background: 'none',
border: 'none',
color: selectedTab === tab ? '#00D4FF' : '#6b7c93',
fontSize: '8px',
fontWeight: selectedTab === tab ? 'bold' : 'normal',
cursor: 'pointer',
borderBottom: selectedTab === tab ? '3px solid #00D4FF' : 'none',
transition: 'all 0.3s',
}}
>
{tab}
</button>
))}
</div>
{/* 차트 영역 */}
<div style={{ display: 'flex', flexDirection: 'column',flex: 1, minHeight: 0 }}>
{/* 유입 차트 */}
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>
<Line data={inflowChartData} options={getChartOptions('mg/L')} />
</div>
{/* 발류 차트 */}
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>
<Line data={effluentChartData} options={getChartOptions('mg/L')} />
</div>
</div>
</div>
</div>
</div>
);
};
export default FoWqltInflEffl;
@@ -0,0 +1,124 @@
/**
* 물재이용 차트
*/
import React from 'react';
import { Line } from 'react-chartjs-2';
import { Button } from "@common/components";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler,
type ChartOptions
} from 'chart.js';
// Chart.js 플러그인 등록
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler // 영역 채우기를 위해 필요
);
const ReuseChart: React.FC = () => {
// 임시 데이터 (이미지 기반 추정치)
const data = {
labels: ['2021', '2022', '2023', '2024'],
datasets: [
{
label: '재이용량',
data: [1125000, 1138000, 1135000, 1168000], // 이미지의 대략적인 값
borderColor: '#00D4FF', // 선 색상 (밝은 하늘색)
backgroundColor: 'rgba(0, 212, 255, 0.1)', // 채우기 색상 (투명도 있는 하늘색)
borderWidth: 3,
pointBackgroundColor: '#00D4FF',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 6,
pointHoverRadius: 8,
fill: true, // 아래 영역 채우기 활성화
tension: 0.4, // 곡선 부드럽게
},
],
};
const options: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false, // 범례 숨김 (이미지에 없음)
},
tooltip: {
callbacks: {
label: (context) => {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('ko-KR').format(context.parsed.y) + '㎥';
}
return label;
}
}
}
},
scales: {
x: {
grid: {
display: true,
color: '#f0f0f0', // 연한 회색 그리드
},
ticks: {
color: '#666',
}
},
y: {
min: 1100000, // Y축 최소값 설정 (그래프 모양을 위해)
grid: {
color: '#f0f0f0',
},
ticks: {
color: '#666',
callback: function(value) {
return new Intl.NumberFormat('ko-KR').format(Number(value)) + '㎥';
}
}
}
},
interaction: {
mode: 'index',
intersect: false,
},
};
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">하수처리수 재이용량 (m³)</h3>
<Button variant="outline" size="sm" className="text-white h-8 py-0 ml-auto">더보기</Button>
</div>
<div className="h-60 p-4">
<div style={{ width: '100%', height: '100%' }}>
<Line data={data} options={options} />
</div>
</div>
</div>
);
};
export default ReuseChart;
@@ -0,0 +1,101 @@
"use client";
/**
* 물재이용 현황
*/
import React from 'react';
import { Button } from "@common/components";
interface ReuseData {
year: string;
reuseAmount: number;
wasteWaterAmount: number;
rainfallAmount: number;
}
const ReuseStatus: React.FC = () => {
// 임시 데이터
const data: ReuseData[] = [
{
year: "'21",
reuseAmount: 1126995,
wasteWaterAmount: 254805510,
rainfallAmount: 14049225,
},
{
year: "'22",
reuseAmount: 1136754,
wasteWaterAmount: 243457789,
rainfallAmount: 15128093,
},
{
year: "'23",
reuseAmount: 1137539,
wasteWaterAmount: 222475997,
rainfallAmount: 13944733,
},
{
year: "'24",
reuseAmount: 1168627,
wasteWaterAmount: 265625918,
rainfallAmount: 13305310,
},
];
// 숫자 포맷 함수 (천단위 쉼표)
const formatNumber = (num: number): string => {
return new Intl.NumberFormat('ko-KR').format(num);
};
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">물재이용 현황 (재이용량, m³)</h3>
<Button variant="outline" size="sm" className="text-white h-8 py-0 ml-auto">더보기</Button>
</div>
<div className="h-70 p-4">
<div className="w-full h-full flex items-center justify-center">
<table className="w-full border-collapse text-md">
<thead>
<tr>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
년도
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
하수재이용량
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
중수도이용량
</th>
<th className="bg-blue-500 text-white font-bold py-1 px-3 border border-gray-300">
빗물이용량
</th>
</tr>
</thead>
<tbody>
{data.map((row, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="bg-blue-100 font-semibold text-center py-1 px-3 border border-gray-300">
{row.year}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.reuseAmount)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.wasteWaterAmount)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.rainfallAmount)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default ReuseStatus;
@@ -0,0 +1,144 @@
"use client";
/**
* 노후 하수관로 현황
*/
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
// ChartJS 컴포넌트 등록
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
export default function SnOldPpl() {
// 차트 데이터
const labels = ["한강", "낙동강", "영산강", "금강"];
const datasets = [
{
label: '20-25년',
data: [3859, 1066, 1929, 2828],
backgroundColor: '#3B82F6', // 파란색
borderRadius: 4,
},
{
label: '25년-30년',
data: [3153, 1420, 1204, 2877],
backgroundColor: '#EF4444', // 빨간색
borderRadius: 4,
},
{
label: '30년이상',
data: [9404, 1835, 1821, 8115],
backgroundColor: '#F87171', // 연한 빨간색/분홍색
borderRadius: 4,
},
];
// 각 지역별 총 길이 계산
const totalLengths = [44251, 33786, 11888, 14878];
// 차트 옵션 설정
const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: {
display: true,
},
},
y: {
beginAtZero: true,
min: 0,
max: 10000,
ticks: {
stepSize: 2000,
callback: function (value) {
return value + 'km';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.3)',
}
}
},
plugins: {
legend: {
position: 'top',
align: 'center', // ⬅ 가운데 정렬
labels: {
boxWidth: 15,
padding: 20, // ⬅ 범례 항목 간 여백 확대
usePointStyle: false,
},
},
tooltip: {
callbacks: {
label: function (context) {
return `${context.dataset.label}: ${context.parsed.y}km`;
}
}
},
},
// 총 길이 데이터 표시를 위한 애니메이션 후 함수 설정
animation: {
onComplete: function (animation) {
const chart = animation.chart;
const ctx = chart.ctx;
// 각 데이터셋의 마지막 데이터셋의 인덱스
const lastDatasetIndex = datasets.length - 1;
console.log(lastDatasetIndex);
// 텍스트 스타일 설정
ctx.font = '11px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
// 각 지역별로 총 길이 표시
labels.forEach((_, i) => {
const xPos = chart.scales.x ? chart.scales.x.getPixelForTick(i) : 0;
const yPos = chart.chartArea.top; // 차트 영역 최상단 위에 표시
// 막대 위에 총 길이 텍스트 표시
if (totalLengths[i]) ctx.fillText(`총 ${totalLengths[i].toLocaleString()}km`, xPos, yPos);
});
}
}
};
const data = {
labels,
datasets,
};
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">노후 하수관로 현황</h3>
<Button variant="outline" size="sm" className="text-white h-8 py-0 ml-auto">더보기</Button>
</div>
<div className="h-70 p-4 flex items-center justify-center">
<div style={{ width: "100%", maxWidth: 400, height: 131 }}>
<Bar options={options} data={data} />
</div>
</div>
</div>
);
}
@@ -0,0 +1,117 @@
"use client";
/**
* 오수관로 및 우수관로 현황
*/
import React from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function SnPplStatus() {
const labels = ["한강", "낙동강", "영산강", "금강"];
// 데이터: [기타, 오수관로, 우수관로]
const datasets = [
{
label: "기타",
data: [15481, 6959, 1938, 1967],
backgroundColor: "rgba(255, 178, 150, 0.9)", // 연한 주황(파스텔톤)
borderColor: "rgba(255,178,150,1)",
},
{
label: "오수관로",
data: [15878, 16336, 5670, 8073],
backgroundColor: "rgba(180, 240, 190, 0.95)", // 연한 초록(파스텔톤)
borderColor: "rgba(180,240,190,1)",
},
{
label: "우수관로",
data: [12892, 10491, 4280, 4838],
backgroundColor: "rgba(170, 210, 245, 0.95)", // 연한 파랑(파스텔톤)
borderColor: "rgba(170,210,245,1)",
},
];
// 총합(라벨당) 계산
const totals = labels.map((_, i) =>
datasets.reduce((s, ds) => s + (ds.data[i] as number), 0),
);
const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
},
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${Number(ctx.parsed.y).toLocaleString()}km`,
},
},
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
beginAtZero: true,
ticks: {
callback: (value) => `${Number(value).toLocaleString()}km`,
},
},
},
// 각 스택 위에 총합 표시
animation: {
onComplete: function (animation) {
const chart = animation.chart;
const ctx = chart.ctx;
const lastMeta = chart.getDatasetMeta(datasets.length - 1);
ctx.save();
ctx.fillStyle = "#111";
ctx.font = "gray bold 12px Arial";
ctx.textAlign = "center";
totals.forEach((total, i) => {
const bar = lastMeta.data[i];
if (!bar) return;
const x = bar.x;
// y는 마지막 스택의 y 좌표(스택의 최상단)에서 조금 위로
const y = bar.y - 8;
ctx.fillText(`총 ${total.toLocaleString()}km`, x, y);
});
ctx.restore();
},
},
};
const data = { labels, datasets };
return (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
<Button variant="outline" size="sm" className="text-white h-8 py-0 ml-auto">더보기</Button>
</div>
<div className="h-60 p-4">
<div style={{ width: "100%", maxWidth: 400, height: 150 }}>
<Bar options={options} data={data} />
</div>
</div>
</div>
);
}