Compare commits

...
8 changed files with 414 additions and 5 deletions
+1
View File
@@ -10,4 +10,5 @@ NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083
+3 -4
View File
@@ -4,16 +4,15 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_PORT = 3002
NEXT_PUBLIC_PROXY_HOST = http://localhost:3002
# NEXT_PUBLIC_PROXY_HOST = http://localhost:3002
NEXT_PUBLIC_PROXY_HOST = http://sn.onasys.co.kr:3002
NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL = http://localhost:3000/auth/login
NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3000/auth/login
NEXT_PUBLIC_VDI_DEV_CM_LOGIN_PAGE_URL = http://localhost:3000/auth/login
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
+1
View File
@@ -11,3 +11,4 @@ NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BE
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
+14
View File
@@ -0,0 +1,14 @@
NEXT_PUBLIC_SITE_ID = SN
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_PORT = 3000
NEXT_PUBLIC_PROXY_HOST = https://sn.hasuon.or.kr
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
@@ -0,0 +1,105 @@
"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";
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function Page() {
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 style={{ width: "100%", maxWidth: 800, height: 360 }}>
<Bar options={options} data={data} />
</div>
);
}
@@ -0,0 +1,143 @@
"use client";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
type ChartOptions,
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Card, CardHeader, CardTitle, CardContent } from "@common/business_components/ui/chart/lib/card";
import { Button } from "@common/components";
// ChartJS 컴포넌트 등록
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
export default function TestPage() {
// 차트 데이터
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: 'start',
labels: {
boxWidth: 15,
padding: 15,
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;
// 텍스트 스타일 설정
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 (
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-base">노후하수관로현황</CardTitle>
<Button size="sm">더보기</Button>
</CardHeader>
<CardContent className="pb-4">
{/* 너비 400px, 높이 150px로 조정 */}
<div className="w-[400px] h-[150px] mx-auto">
<Bar options={options} data={data} />
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,146 @@
// ...existing code...
"use client";
import React from "react";
import { Chart as ChartJS, ArcElement, Tooltip, Legend, type ChartOptions } from "chart.js";
import { Pie } from "react-chartjs-2";
ChartJS.register(ArcElement, Tooltip, Legend);
export default function Page() {
const labels = ["한강", "낙동강", "영산강", "금강"];
const datasets = [
{ label: "20년 미만", data: [27835, 29465, 6934, 1058], color: "rgba(169, 169, 169, 0.8)" },
{ label: "20-25년", data: [3859, 1066, 1929, 2828], color: "rgba(255, 160, 122, 0.8)" },
{ label: "25년-30년", data: [3153, 1420, 1204, 2877], color: "rgba(152, 251, 152, 0.8)" },
{ label: "30년이상", data: [9404, 1835, 1821, 8115], color: "rgba(135, 206, 235, 0.8)" },
];
// 각 지역별 총 길이 계산
const totalLengths = [44251, 33786, 11888, 14878];
// 외부 툴팁을 사용하여 컨테이너 넘침과 관계없이 전체 툴팁을 보여주고,
// 각 항목의 전체에서 차지하는 비율(%)을 표시합니다.
const pieOptions: ChartOptions<"pie"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
enabled: false, // 기본 캔버스 툴팁 비활성화 (외부 툴팁 사용)
external: function (context) {
const { chart, tooltip } = context;
let tooltipEl = document.getElementById("chartjs-external-tooltip");
// tooltip DOM이 없으면 생성
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.id = "chartjs-external-tooltip";
tooltipEl.style.position = "absolute";
tooltipEl.style.pointerEvents = "auto";
tooltipEl.style.background = "rgba(0,0,0,0.75)";
tooltipEl.style.color = "#fff";
tooltipEl.style.padding = "6px 8px";
tooltipEl.style.borderRadius = "6px";
tooltipEl.style.fontSize = "12px";
tooltipEl.style.zIndex = "9999";
tooltipEl.style.transform = "translate(-50%, -120%)";
document.body.appendChild(tooltipEl);
}
// 사라짐 처리
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = "0";
return;
}
// 툴팁 내용 구성 (파이의 전체 합 대비 비율 표시)
const data = chart.data;
const dataset = data.datasets && data.datasets[0] && data.datasets[0].data
? (data.datasets[0].data as number[])
: [];
const total = dataset.reduce((s, n) => s + (Number(n) || 0), 0);
const innerHtmlParts: string[] = [];
if (tooltip.dataPoints) {
tooltip.dataPoints.forEach((dp) => {
const idx = dp.dataIndex;
const label = (data.labels && data.labels[idx]) || dp.label || "";
const value = Number(dp.raw ?? dp.parsed ?? 0);
const pct = total > 0 ? (value / total) * 100 : 0;
innerHtmlParts.push(
`<div style="white-space:nowrap">
<strong style="display:inline-block;width:8px;height:8px;border-radius:2px;background:${(dp as any).element?.options?.backgroundColor || (dataset as any)[idx] || "#999"};margin-right:6px;vertical-align:middle"></strong>
<span style="vertical-align:middle">${label}: ${value.toLocaleString()}km</span>
<span style="margin-left:8px;color:#ddd">(${pct.toFixed(1)}%)</span>
</div>`
);
});
}
tooltipEl.innerHTML = innerHtmlParts.join("");
tooltipEl.style.opacity = "1";
// 위치: 차트 캔버스 기준 툴팁 caret 위치에 맞춤 (윈도우 스크롤 보정 포함)
const canvasRect = chart.canvas.getBoundingClientRect();
const caretX = tooltip.caretX ?? (canvasRect.left + canvasRect.width / 2);
const caretY = tooltip.caretY ?? (canvasRect.top + canvasRect.height / 2);
tooltipEl.style.left = `${canvasRect.left + window.pageXOffset + caretX}px`;
tooltipEl.style.top = `${canvasRect.top + window.pageYOffset + caretY}px`;
},
},
},
};
// 상단에 공통 범례(연도) 가로로 표시
const SharedLegend = () => (
<div className="flex gap-4 items-center justify-center">
{datasets.map((d) => (
<div key={d.label} className="flex items-center gap-2">
<span style={{ width: 12, height: 12, backgroundColor: d.color, borderRadius: 3, display: "inline-block" }} />
<span className="text-xl text-gray-600">{d.label}</span>
</div>
))}
</div>
);
// 레이블(강) 당 pie 데이터 생성 (레이블은 아래로 이동)
const Charts = () =>
labels.map((river, i) => {
const data = {
labels: datasets.map((d) => d.label),
datasets: [
{
data: datasets.map((d) => d.data[i]),
backgroundColor: datasets.map((d) => d.color),
borderColor: datasets.map(() => "rgba(255,255,255,0.8)"),
borderWidth: 1,
},
],
};
return (
<div key={river} className="flex flex-col items-center gap-2">
<div style={{ width: 80, height: 80 }}>
<Pie data={data} options={pieOptions} />
</div>
{/* 레이블을 차트 하단으로 이동 */}
<div className="text-lg font-medium text-gray-600">{totalLengths[i]?.toLocaleString()} km</div>
<div className="text-xl font-bold text-gray-600">{river}</div>
</div>
);
});
return (
<div className="p-4 w-[400px]">
{/* 상단 공통 범례 */}
<div className="mb-3">
<SharedLegend />
</div>
<div className="flex items-start justify-between gap-6">
<Charts />
</div>
</div>
);
}
// ...existing code...
@@ -10,7 +10,7 @@ import { getCookie } from "@/shared/lib";
import { LoginApi } from "../api/login-service";
const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_DEV_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL
const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://dev.onasys.co.kr:3000/auth/login"; // 로그인 페이지 URL
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();