예전 디자인
This commit is contained in:
+1
-1
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user