diff --git a/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.module.css b/apps/cm/src/pages/dashboard/lib/korea-basin-map.module.css similarity index 100% rename from apps/cm/src/pages/dashboard/lib/KoreaBasinMap.module.css rename to apps/cm/src/pages/dashboard/lib/korea-basin-map.module.css diff --git a/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx similarity index 99% rename from apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx rename to apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx index 139f9f872..637f88e9d 100644 --- a/apps/cm/src/pages/dashboard/lib/KoreaBasinMap.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -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 위치 필요 diff --git a/apps/cm/src/pages/dashboard/ui/basIn-select.tsx b/apps/cm/src/pages/dashboard/ui/basIn-select.tsx new file mode 100644 index 000000000..52b2e58a3 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/basIn-select.tsx @@ -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("한강유역"); + + const basins = ["한강유역", "금강유역", "낙동강유역", "영산강유역"]; + + return ( + + ); +}; + +export default BasinSelect; \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 76b6876fb..d0cc508b6 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -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() {
{/* 첫번쨰 열 */}
- {/* 전국 하수처리시설 가동률 */} -
-
-

전국 하수처리시설 가동률

-
-
- { - 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(); - } - }]} - /> -
-
+ {/* 전국 하수처리시설 개소수 및 가동률 */} + {/* 오수관로 및 우수관로 현황 */} -
-
-

오수관로 및 우수관로 현황

-
-
-
+ - {/* 하수처리수 재이용 */} -
-
-

하수처리수 재이용

-
-
-
+ {/* 하수처리수 재이용 (m³) */} +
{/* 두번쨰 열 */}
- {/* 하수처리시설(개소) */} -
-
-

하수처리시설(개소)

-
-
-
-
-

500㎥ 미만(개소)

-
-
3,567 개소
-
-
-
-

500㎥ 이상(개소)

-
-
767 개소
-
-
기준년월 : 2025.09
-
-
- - {/* 노후하수관로현황 */} -
-
-

노후하수관로현황

-
-
-
-
+ {/* 수계별 유입ㆍ방류수질 분석 */} + - {/* 재이용시설(개소) */} -
-
-

재이용시설(개소)

-
-
-
-
+ {/* 노후하수관로현황 */} + + + {/* 물재이용 현황 (재이용량, m³) */} +
- + {/* 지도영역 */}
@@ -267,12 +180,12 @@ function DashboardPage() { {/* 두번째 라인 */}
-
+

지원현황

- +
diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx new file mode 100644 index 000000000..0bcce9a6a --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx @@ -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(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 ( +
+
+ {/* 첫번째 라인 */} +
+ {/* 첫번쨰 열 */} +
+ {/* 전국 하수처리시설 가동률 */} +
+
+

전국 하수처리시설 가동률

+
+
+ { + 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(); + } + }]} + /> +
+
+ + {/* 오수관로 및 우수관로 현황 */} +
+
+

오수관로 및 우수관로 현황

+
+
+
+ + {/* 하수처리수 재이용 */} +
+
+

하수처리수 재이용

+
+
+
+
+ + {/* 두번쨰 열 */} +
+ {/* 하수처리시설(개소) */} +
+
+

하수처리시설(개소)

+
+
+
+
+

500㎥ 미만(개소)

+
+
3,567 개소
+
+
+
+

500㎥ 이상(개소)

+
+
767 개소
+
+
기준년월 : 2025.09
+
+
+ + {/* 노후하수관로현황 */} +
+
+

노후하수관로현황

+
+
+
+
+ + {/* 재이용시설(개소) */} +
+
+

재이용시설(개소)

+
+
+
+
+
+ + {/* 지도영역 */} +
+
+
+

유역별 정보

+
+ +
+
+ + {/* 사용자 정보 카드 */} +
+

사용자 정보

+ {error &&
{error}
} + {userInfo ? ( +
+
+ 아이디: {userInfo.userId} +
+
+ 이메일: {userInfo.email || userInfo.userId} +
+
+ 이름: {userInfo.userName || "-"} +
+
+ 2FA 활성화:{" "} + {userInfo.twoFactorEnabled ? "예" : "아니오"} +
+
+ ) : ( + !error &&
사용자 정보를 불러오는 중...
+ )} + + +
+
+ + {/* 두번째 라인 */} +
+
+
+

지원현황

+
+
+ +
+
+
+
+

자료관 자료현황

+
+
+
+
+
+
+

공지사항

+
+
+
+
+ + {/* 배너 영역 */} +
+ +
+
+
+
+ ); +} + +export { DashboardPage }; diff --git a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx new file mode 100644 index 000000000..3f4e7410d --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx @@ -0,0 +1,100 @@ +/** + * 수계별 시설운영 개소수 및 가동률 + */ +import React from 'react'; +import BasinSelect from "./basIn-select"; +import { Doughnut } from "react-chartjs-2"; + +const FoFcltCntRt: React.FC = () => { + + + return ( +
+
+

수계별 시설운영 개소수 및 가동률

+ +
+
+
+ {/* 왼쪽: 개소수 카드 */} +
+
+
+

개소수 (2024년도)

+
+
+
+ 3,567 개소 +
+
+
+
+ {/* 오른쪽: Doughnut 차트 */} +
+
+
+

가동률 (2024년도)

+
+
+
+ { + 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(); + } + }]} + /> +
+
+
+
+
+
+
+ ); + +}; + +export default FoFcltCntRt; \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx new file mode 100644 index 000000000..723080648 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx @@ -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('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 ( +
+
+

수계별 유입ㆍ방류수질 분석

+ +
+
+
+ {/* 탭 메뉴 */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* 차트 영역 */} +
+ {/* 유입 차트 */} +
+ +
+ + {/* 발류 차트 */} +
+ +
+
+
+
+
+ + + ); +}; + +export default FoWqltInflEffl; diff --git a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx new file mode 100644 index 000000000..2541b955e --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx @@ -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 ( +
+
+

하수처리수 재이용량 (m³)

+ +
+
+
+ +
+
+
+ ); +}; + +export default ReuseChart; \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx new file mode 100644 index 000000000..835bf3170 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx @@ -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 ( +
+
+

물재이용 현황 (재이용량, m³)

+ +
+
+
+ + + + + + + + + + + {data.map((row, index) => ( + + + + + + + ))} + +
+ 년도 + + 하수재이용량 + + 중수도이용량 + + 빗물이용량 +
+ {row.year} + + {formatNumber(row.reuseAmount)} + + {formatNumber(row.wasteWaterAmount)} + + {formatNumber(row.rainfallAmount)} +
+
+
+
+ + + ); +}; + +export default ReuseStatus; \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx new file mode 100644 index 000000000..acd73d7c2 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx @@ -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 ( +
+
+

노후 하수관로 현황

+ +
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx new file mode 100644 index 000000000..afc2d3166 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx @@ -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 ( +
+
+

오수관로 및 우수관로 현황

+ +
+
+
+ +
+
+
+ ); +} \ No newline at end of file