Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
738b1f7735 |
@@ -21,8 +21,3 @@ apps/fo/.next/**/*
|
||||
# env
|
||||
#.env*
|
||||
.qodo
|
||||
|
||||
pnpm-lock.yaml
|
||||
settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
+6
-13
@@ -1,14 +1,7 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3001
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
|
||||
NEXT_PUBLIC_SITE_ID=CM
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000
|
||||
# NEXT_PUBLIC_SERVER_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
|
||||
NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083
|
||||
NEXT_PUBLIC_SERVER_API_URL3=https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_SN=https://devlion.hasuon.co.kr
|
||||
NEXT_PUBLIC_WR=https://devtiger.hasuon.co.kr
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
@@ -0,0 +1,10 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:13000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
|
||||
NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/
|
||||
@@ -0,0 +1,7 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
Binary file not shown.
@@ -10,8 +10,6 @@
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
pnpm_lock.yaml
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
|
||||
@@ -36,15 +36,9 @@ async function handler(
|
||||
}
|
||||
|
||||
try {
|
||||
// GET 요청이 아닐 때만 body 파싱
|
||||
if (method !== "GET" && !isRefreshHandler) {
|
||||
try {
|
||||
body = await request.json();
|
||||
console.log(`[Proxy] Request body:`, body);
|
||||
} catch {
|
||||
// body가 없을 수 있음
|
||||
body = {};
|
||||
}
|
||||
if (!isRefreshHandler) {
|
||||
body = await request.json();
|
||||
console.log(`[Proxy] Request body:`, body);
|
||||
}
|
||||
|
||||
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
|
||||
@@ -53,7 +47,7 @@ async function handler(
|
||||
const result = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: method !== "GET" ? JSON.stringify(body) : undefined,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
console.log(`[Proxy] Response status: ${result.status}`);
|
||||
@@ -61,14 +55,12 @@ async function handler(
|
||||
|
||||
// 2FA 관련 엔드포인트인 경우 응답 본문도 확인
|
||||
const is2FAEndpoint = params.url.includes("2fa") || params.url.includes("auth");
|
||||
// 배너 API는 인증 없이 호출 가능
|
||||
const isBannerEndpoint = params.url.includes("bnrs");
|
||||
let responseData = {};
|
||||
|
||||
if (is2FAEndpoint || isBannerEndpoint) {
|
||||
if (is2FAEndpoint) {
|
||||
try {
|
||||
responseData = await result.json();
|
||||
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} Response data:`, responseData);
|
||||
console.log(`[Proxy] 2FA Response data:`, responseData);
|
||||
} catch {
|
||||
// JSON 파싱 실패 시 빈 객체 사용
|
||||
console.log(`[Proxy] Failed to parse JSON response`);
|
||||
@@ -82,15 +74,15 @@ async function handler(
|
||||
console.log(`[Proxy] Tokens - accessToken: ${!!accessToken}, userId: ${!!userId}, refreshToken: ${!!refreshToken}`);
|
||||
console.log(`[Proxy] Available headers:`, Array.from(result.headers.keys()));
|
||||
|
||||
// 2FA 또는 배너 엔드포인트이고 실패한 경우 실제 오류 메시지 반환
|
||||
if (!result.ok && (is2FAEndpoint || isBannerEndpoint)) {
|
||||
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint failed, returning actual error`);
|
||||
// 2FA 엔드포인트이고 실패한 경우 실제 오류 메시지 반환
|
||||
if (!result.ok && is2FAEndpoint) {
|
||||
console.log(`[Proxy] 2FA endpoint failed, returning actual error`);
|
||||
return NextResponse.json(responseData, { status: result.status });
|
||||
}
|
||||
|
||||
// 2FA 또는 배너 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음)
|
||||
if (result.ok && (is2FAEndpoint || isBannerEndpoint)) {
|
||||
console.log(`[Proxy] ${is2FAEndpoint ? '2FA' : 'Banner'} endpoint succeeded, returning response data`);
|
||||
// 2FA 엔드포인트이고 성공한 경우 (토큰이 헤더에 없을 수 있음)
|
||||
if (result.ok && is2FAEndpoint) {
|
||||
console.log(`[Proxy] 2FA endpoint succeeded, returning response data`);
|
||||
return NextResponse.json(responseData, { status: result.status });
|
||||
}
|
||||
|
||||
@@ -151,11 +143,3 @@ export async function PUT(
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
props: { params: Promise<{ url: string[] }> },
|
||||
) {
|
||||
const params = await props.params;
|
||||
return handler(request, params, "GET");
|
||||
}
|
||||
|
||||
@@ -12,11 +12,6 @@ const compat = new FlatCompat({
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
},
|
||||
|
||||
+42
-54
@@ -17,8 +17,8 @@ const serverApiUrl =
|
||||
const serverApiUrl3 =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl;
|
||||
|
||||
// const serverApiUrl4 =
|
||||
// process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl;
|
||||
const serverApiUrl4 =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl;
|
||||
|
||||
const serverApiUrl5 =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL5 ?? serverApiUrl;
|
||||
@@ -33,6 +33,19 @@ export function middleware(req: NextRequest) {
|
||||
`[Request]\n Original Pathname: ${pathname}`,
|
||||
];
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
@@ -42,27 +55,6 @@ export function middleware(req: NextRequest) {
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
// console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (pathname.startsWith("/server/sewerage-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
// const decodedQuery = toDecodedQueryString(searchParams);
|
||||
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
@@ -72,7 +64,8 @@ export function middleware(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/reuse-service")) {
|
||||
|
||||
if (pathname.startsWith("/sewerage-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
@@ -90,12 +83,11 @@ export function middleware(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/server/operation-service")) {
|
||||
if (pathname.startsWith("/reuse-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
// const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`;
|
||||
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
|
||||
const newUrl = `${serverApiUrl3}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
@@ -109,12 +101,11 @@ export function middleware(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/server/ic-service")) {
|
||||
if (pathname.startsWith("/operation-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
|
||||
const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "/")}`;
|
||||
const newUrl = `${serverApiUrl4}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
@@ -128,12 +119,29 @@ export function middleware(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/server/dc-service")) {
|
||||
if (pathname.startsWith("/ic-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl5}${pathname}`;
|
||||
|
||||
const newUrl = `${serverApiUrl3}${pathname.replace("/server/", "/")}${searchParams ? `?${searchParams}` : ""}`;
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/dc-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl5}${pathname}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
@@ -155,25 +163,11 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
logMessages.push(` User Service Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
// console.log(logMessages.join("\n"));
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "/")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
logMessages.push(` Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
// console.log(logMessages.join("\n"));
|
||||
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
|
||||
if (pathname === "/") {
|
||||
return NextResponse.redirect(new URL("/dashboard", req.url));
|
||||
}
|
||||
@@ -184,11 +178,5 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*",
|
||||
"/attach-service/:path*",
|
||||
"/user-service/:path*",
|
||||
"/sewerage-service/:path*",
|
||||
"/reuse-service/:path*",
|
||||
"/operation-service/:path*",
|
||||
"/"],
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/user-service/:path*", "/"],
|
||||
};
|
||||
|
||||
+6
-14
@@ -3,22 +3,10 @@ import type { NextConfig } from "next";
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const backendUrl = process.env.NEXT_PUBLIC_SERVER_API_URL || "http://192.168.0.102:8000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
proxyTimeout: 600000,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
},
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
@@ -39,6 +27,10 @@ const nextConfig: NextConfig = {
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/user-service/:path*",
|
||||
destination: `${backendUrl}/user-service/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
|
||||
+10
-10
@@ -25,16 +25,16 @@
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@tinymce/tinymce-react": "^6.1.0",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.8",
|
||||
"next": "15.3.0",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
@@ -47,17 +47,17 @@
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.0.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"postcss": "^8.4.49",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"steiger": "^0.5.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.7.2"
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
@@ -73,32 +73,12 @@
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_paddingnone {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_nomargin {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_title {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card_title .leftArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card_title h4 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.topline {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
@@ -147,300 +127,3 @@
|
||||
border-bottom-color: #3f4042;
|
||||
}
|
||||
}
|
||||
|
||||
/* 자료현황 스타일 */
|
||||
.mid_info_area {
|
||||
padding: 10px 5px;
|
||||
height: 220px;
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mi_list_swiper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mi_list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ml_box {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 90%;
|
||||
height: 150px;
|
||||
padding: 12px;
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
border-radius: 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ml_box:hover {
|
||||
background: #e3f2fd;
|
||||
border-color: #2196f3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mb_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tit i {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.tt {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.info_row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info_item {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info_label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info_value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Swiper 네비게이션 스타일 */
|
||||
.mi_list_swiper .swiper-button-next,
|
||||
.mi_list_swiper .swiper-button-prev {
|
||||
color: #2196f3;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
.mi_list_swiper .swiper-button-next:after,
|
||||
.mi_list_swiper .swiper-button-prev:after {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mi_list_swiper .swiper-pagination {
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.mi_list_swiper .swiper-pagination-bullet {
|
||||
background: #2196f3;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.mi_list_swiper .swiper-pagination-bullet-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 특별시 스타일 */
|
||||
.ml_box.special-city {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
|
||||
border-color: #ff4757;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ml_box.special-city .tt {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ml_box.special-city .info_label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.ml_box.special-city .info_value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ml_box.special-city:hover {
|
||||
background: linear-gradient(135deg, #ff5252, #ff7979);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
/* 광역시 스타일 */
|
||||
.ml_box.metropolitan-city {
|
||||
background: linear-gradient(135deg, #4facfe, #00f2fe);
|
||||
border-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ml_box.metropolitan-city .tt {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml_box.metropolitan-city .info_label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.ml_box.metropolitan-city .info_value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ml_box.metropolitan-city:hover {
|
||||
background: linear-gradient(135deg, #2196f3, #21cbf3);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
/* 광역도 스타일 */
|
||||
.ml_box.province {
|
||||
background: linear-gradient(135deg, #a8edea, #fed6e3);
|
||||
border-color: #00bcd4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ml_box.province .tt {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml_box.province .info_label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ml_box.province .info_value {
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
.ml_box.province:hover {
|
||||
background: linear-gradient(135deg, #00bcd4, #26c6da);
|
||||
color: white;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 12px rgba(0, 188, 212, 0.3);
|
||||
}
|
||||
|
||||
.ml_box.province:hover .tt,
|
||||
.ml_box.province:hover .info_label,
|
||||
.ml_box.province:hover .info_value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 네이버 스타일 로그인 박스 */
|
||||
.login_info_box {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login_avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.login_avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.login_user_info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user_name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.user_status {
|
||||
font-size: 12px;
|
||||
color: #03c75a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login_actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login_btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
background: #03c75a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.login_btn:hover {
|
||||
background: #02b351;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.signup_btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
color: #03c75a;
|
||||
border: 1px solid #03c75a;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.signup_btn:hover {
|
||||
background: #03c75a;
|
||||
color: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/shared/config";
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
||||
|
||||
export class TokenManager {
|
||||
@@ -19,37 +19,14 @@ export class TokenManager {
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
// 환경에 따른 도메인 설정
|
||||
const domain = this.getCookieDomain();
|
||||
|
||||
setCookie(key, value, {
|
||||
secure: window.location.protocol === "https:",
|
||||
sameSite: "lax",
|
||||
domain: domain,
|
||||
path: "/", // 모든 경로에서 접근 가능
|
||||
});
|
||||
}
|
||||
|
||||
private getCookieDomain(): string | undefined {
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 개발 환경에서는 localhost 도메인 사용
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
||||
return undefined; // localhost에서는 도메인 설정하지 않음
|
||||
}
|
||||
|
||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
return ".hasuon.or.kr";
|
||||
}
|
||||
|
||||
// 기타 환경에서는 현재 호스트명 사용
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public removeToken(key: string): void {
|
||||
const domain = this.getCookieDomain();
|
||||
removeCookie(key, { domain, path: "/" });
|
||||
removeCookie(key);
|
||||
}
|
||||
|
||||
public setupRefreshTimer(
|
||||
@@ -76,8 +53,6 @@ export class TokenManager {
|
||||
|
||||
this.removeToken(CLAIM_NAME);
|
||||
this.removeToken(AUTH_USER_ID);
|
||||
this.removeToken(REFRESH_TOKEN);
|
||||
this.removeToken(ACCESS_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,9 @@ import {
|
||||
type MyPostList,
|
||||
type PostParams,
|
||||
type MyPostParams,
|
||||
type NoticePostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
type NoticePostList,
|
||||
} from "../model/post-interface";
|
||||
|
||||
export class PostApiService implements PostService {
|
||||
@@ -30,22 +28,11 @@ export class PostApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
public async getNoticePostList(
|
||||
params?: NoticePostParams,
|
||||
): Promise<NoticePostList> {
|
||||
const response = await client.get<NoticePostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getMyPostList(
|
||||
params?: MyPostParams,
|
||||
): Promise<MyPostList> {
|
||||
const response = await client.get<MyPostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.MY_POSTS}`,
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/mypost`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -4,13 +4,11 @@ export {
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type MyPost,
|
||||
type NoticePost,
|
||||
type MovePost,
|
||||
type DeletePost,
|
||||
type PostList,
|
||||
type PostParams,
|
||||
type MyPostParams,
|
||||
type NoticePostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
|
||||
@@ -12,20 +12,6 @@ export interface PostParams extends PageableParams {
|
||||
ansUseYn?: string;
|
||||
}
|
||||
|
||||
export interface NoticePostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number | null;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
}
|
||||
|
||||
export interface NoticePost extends Post {
|
||||
bbsNm: string;
|
||||
bbsTypeCd: string;
|
||||
}
|
||||
|
||||
export interface MyPostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
@@ -113,7 +99,6 @@ export const postUpdateFormSchema = postFormSchema.omit({
|
||||
|
||||
export type PostList = PageableList<Post>;
|
||||
export type MyPostList = PageableList<MyPost>;
|
||||
export type NoticePostList = PageableList<NoticePost>;
|
||||
export type PostFormData = z.infer<typeof postFormSchema>;
|
||||
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
export interface Banner {
|
||||
bnrSn: number;
|
||||
bnrTypeCd: string;
|
||||
bnrTypeNm: string;
|
||||
bnrTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
useYn: string;
|
||||
atflCd: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const getBannerList = async (siteId: string) => {
|
||||
const response = await client.get<Banner[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getImageUrlByAtflCd = async (atflCd: string): Promise<string> => {
|
||||
try {
|
||||
const response = await client.get<Blob>(
|
||||
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
|
||||
{
|
||||
responseType: "blob",
|
||||
},
|
||||
);
|
||||
const blob = response.data;
|
||||
const url = URL.createObjectURL(blob);
|
||||
return url;
|
||||
} catch (error: unknown) {
|
||||
const err = error as
|
||||
| { status?: number; message?: string; response?: { status?: number } }
|
||||
| Error
|
||||
| undefined;
|
||||
const status = err && typeof err === "object" && "status" in err
|
||||
? err.status
|
||||
: err && typeof err === "object" && "response" in err && err.response
|
||||
? err.response.status
|
||||
: undefined;
|
||||
const message = err instanceof Error
|
||||
? err.message
|
||||
: err && typeof err === "object" && "message" in err
|
||||
? String(err.message)
|
||||
: String(error);
|
||||
|
||||
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
|
||||
|
||||
export class DashboardApiService implements DashboardService {
|
||||
public async getDashboard(): Promise<Dashboard> {
|
||||
const response = await client.get<Dashboard>(
|
||||
ENDPOINT.CMS_SERVICE.DASHBOARD,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardApi = new DashboardApiService();
|
||||
@@ -1,48 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
export interface DataStatus {
|
||||
ctpvgnsvycnt: number;
|
||||
filecnt: number;
|
||||
ctpvnm: string;
|
||||
}
|
||||
|
||||
export const getDataStatusList = async (): Promise<PageableList<DataStatus>> => {
|
||||
const response = await client.get<DataStatus[]>(
|
||||
`${ENDPOINT.DC_SERVICE.DATA_STATUS}`,
|
||||
);
|
||||
const data = response.data;
|
||||
|
||||
console.log("getDataStatusList data:", data);
|
||||
|
||||
return {
|
||||
content: data as DataStatus[],
|
||||
pageable: {
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
offset: 0,
|
||||
pageNumber: 0,
|
||||
pageSize: data.length as number,
|
||||
paged: false,
|
||||
unpaged: true,
|
||||
},
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
totalPages: 1,
|
||||
totalElements: data.length,
|
||||
number: 0,
|
||||
numberOfElements: data.length,
|
||||
size: data.length,
|
||||
first: true,
|
||||
last: true,
|
||||
empty: data.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
// apps/cm/src/pages/dashboard/api/fo-service.ts
|
||||
import { client } from "@/shared/api";
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import { create } from "zustand";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { OperWqltContent, OperFcltContent, ReuseStatusContent, OperationService
|
||||
} from "../model/fo-interface";
|
||||
import type { mapOperFcltItem, mapOperFcltType } from "../model/fo-interface";
|
||||
import type { ReuseMapContent, mapReuseMapType } from "../model/fo-interface";
|
||||
|
||||
// 기준년도 조회
|
||||
export type YearState = {
|
||||
year: string | null;
|
||||
};
|
||||
export type YearStateActions = {
|
||||
setYear: (year: string | null) => void;
|
||||
};
|
||||
export type YearStore = YearState & YearStateActions;
|
||||
|
||||
export const defaultYearInitState: YearState = {
|
||||
year: null,
|
||||
};
|
||||
export const createYearStore = (initState: YearState = defaultYearInitState) =>
|
||||
createStore<YearStore>()((set) => ({
|
||||
...initState,
|
||||
setYear: (year) => set(() => ({ year })),
|
||||
}));
|
||||
export const yearStore = createYearStore();
|
||||
|
||||
// 유역
|
||||
export type BasinState = {
|
||||
selectedBasin: string;
|
||||
};
|
||||
export type BasinStateActions = {
|
||||
setSelectedBasin: (selectedBasin: string ) => void;
|
||||
};
|
||||
export type BasinStore = BasinState & BasinStateActions;
|
||||
export const defaultBasinInitState: BasinState = {
|
||||
selectedBasin: "한강",
|
||||
};
|
||||
export const createBasinStore = (initState: BasinState = defaultBasinInitState) =>
|
||||
createStore<BasinStore>()((set) => ({
|
||||
...initState,
|
||||
setSelectedBasin: (selectedBasin) => set(() => ({ selectedBasin })),
|
||||
}));
|
||||
export const basinStore = createBasinStore();
|
||||
|
||||
//수계별 시설운영 개소수 및 가동률
|
||||
export type OperFcltState = {
|
||||
operFcltStatus: OperFcltContent[] | null;
|
||||
};
|
||||
export type OperFcltStateActions = {
|
||||
setOperFcltStatus: (operFcltStatus: OperFcltContent[] | null) => void;
|
||||
};
|
||||
export type OperFcltStore = OperFcltState & OperFcltStateActions;
|
||||
// const initialOperFclt: OperFcltContent[]= [
|
||||
// {
|
||||
// "cnt": 710,
|
||||
// "fctCpc": 2911609.0,
|
||||
// "rvbsn": "금강",
|
||||
// "gubun": "R"
|
||||
// },
|
||||
// {
|
||||
// "cnt": 1032,
|
||||
// "fctCpc": 7506047.0,
|
||||
// "rvbsn": "낙동강",
|
||||
// "gubun": "R"
|
||||
// },
|
||||
// {
|
||||
// "cnt": 1261,
|
||||
// "fctCpc": 2061610.0,
|
||||
// "rvbsn": "영산강",
|
||||
// "gubun": "R"
|
||||
// },
|
||||
// {
|
||||
// "cnt": 983,
|
||||
// "fctCpc": 1.3229809E7,
|
||||
// "rvbsn": "한강",
|
||||
// "gubun": "R"
|
||||
// }
|
||||
// ];
|
||||
export const defaultInitOperFcltState: OperFcltState = {
|
||||
// operFcltStatus: initialOperFclt,
|
||||
operFcltStatus: null,
|
||||
};
|
||||
export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFcltState) =>
|
||||
createStore<OperFcltStore>()((set) => ({
|
||||
...initState,
|
||||
setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })),
|
||||
}));
|
||||
export const operFcltStore = createOperFcltStore();
|
||||
|
||||
export const initailMapOperFcltData: mapOperFcltType = {
|
||||
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
|
||||
data: [
|
||||
{name: "금강", cnt: 710, rt: 100,},
|
||||
{name: "한강", cnt: 983, rt: 100,},
|
||||
{name: "낙동강", cnt: 1032, rt: 100,},
|
||||
{name: "영산강", cnt: 1261, rt: 100,},
|
||||
]
|
||||
};
|
||||
type mapOperFcltState = {
|
||||
mapOperFclt: mapOperFcltItem[];
|
||||
setMapOperFclt: (mapOperFclt: mapOperFcltItem[]) => void;
|
||||
};
|
||||
export const useMapOperFcltStore = create<mapOperFcltState>((set) => ({
|
||||
mapOperFclt: initailMapOperFcltData.data,
|
||||
setMapOperFclt: (newMapOperFclt) => set({ mapOperFclt: newMapOperFclt }),
|
||||
}));
|
||||
|
||||
export const initailMapReuseMapData: mapReuseMapType = {
|
||||
colTitle: {reuseAmnt: "하수재이용량",cnt : "시설수" }, //헤더 영역
|
||||
data: [
|
||||
{name: "금강", reuseAmnt: 17099, cnt: 3181},
|
||||
{name: "한강", reuseAmnt: 17099, cnt: 3181},
|
||||
{name: "낙동강", reuseAmnt: 14745, cnt: 3100},
|
||||
{name: "영산강", reuseAmnt: 5073, cnt: 1662},
|
||||
]
|
||||
};
|
||||
type mapReuseMapState = {
|
||||
mapData: ReuseMapContent[];
|
||||
setMapData: (mapData: ReuseMapContent[]) => void;
|
||||
};
|
||||
export const useMapReuseMapStore = create<mapReuseMapState>((set) => ({
|
||||
mapData: initailMapReuseMapData.data,
|
||||
setMapData: (newMapOperFclt) => set({ mapData: newMapOperFclt }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* OperFcltContent[]를 지도 팝업 데이터 형태인 mapOperFcltItem[]으로 변환합니다.
|
||||
* @param data API 응답 데이터
|
||||
* @returns 유역별 개소수 및 가동률이 계산된 데이터
|
||||
*/
|
||||
function transformToMapData(data: OperFcltContent[]): mapOperFcltItem[] {
|
||||
const basinData = new Map<string, { operatingCpc: number; totalCpc: number; count: number }>();
|
||||
|
||||
// 1. 유역별로 가동/전체 용량 및 개소수 집계
|
||||
for (const item of data) {
|
||||
if (!item.rvbsn) continue;
|
||||
|
||||
const basin = basinData.get(item.rvbsn) || { operatingCpc: 0, totalCpc: 0, count: 0 };
|
||||
|
||||
basin.totalCpc += item.fctCpc;
|
||||
if (item.gubun === 'R') {
|
||||
basin.operatingCpc += item.fctCpc;
|
||||
basin.count = item.cnt; // 'R' 구분 데이터의 개소수를 사용
|
||||
}
|
||||
basinData.set(item.rvbsn, basin);
|
||||
}
|
||||
|
||||
// 2. mapOperFcltItem 형태로 변환
|
||||
const result: mapOperFcltItem[] = [];
|
||||
for (const [name, { operatingCpc, totalCpc, count }] of basinData.entries()) {
|
||||
const rt = totalCpc > 0 ? Math.round((operatingCpc / totalCpc) * 100) : 0;
|
||||
result.push({ name, cnt: count, rt });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//수계별 유입ㆍ방류수질 분석
|
||||
export type OperWqltState = {
|
||||
operWqltStatus: OperWqltContent[] | [];
|
||||
};
|
||||
export type OperWqltStateActions = {
|
||||
setOperWqltStatus: (operWqltStatus: OperWqltContent[] | []) => void;
|
||||
};
|
||||
export type OperWqltStore = OperWqltState & OperWqltStateActions;
|
||||
const initialWqltData: OperWqltContent[] = [
|
||||
{ rvbsn: '금강', yr: '2020', sumInflBod: 97975, sumInflToc: 59900, sumInflSs: 103564, sumInflTp: 2879, sumInflTn: 27062, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '금강', yr: '2021', sumInflBod: 97943, sumInflToc: 55043, sumInflSs: 102727, sumInflTp: 2866, sumInflTn: 26237, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '금강', yr: '2022', sumInflBod: 104513, sumInflToc: 58471, sumInflSs: 96534, sumInflTp: 2960, sumInflTn: 27089, sumEfflBod: 100143, sumEfflToc: 57805, sumEfflSs: 100942, sumEfflTp: 2901, sumEfflTn: 26796, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '낙동강', yr: '2020', sumInflBod: 127321, sumInflToc: 79241, sumInflSs: 120184, sumInflTp: 4004, sumInflTn: 35373, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '낙동강', yr: '2021', sumInflBod: 127792, sumInflToc: 61919, sumInflSs: 123625, sumInflTp: 4157, sumInflTn: 35270, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '낙동강', yr: '2022', sumInflBod: 131644, sumInflToc: 68350, sumInflSs: 127789, sumInflTp: 6159, sumInflTn: 39578, sumEfflBod: 128919, sumEfflToc: 69837, sumEfflSs: 123866, sumEfflTp: 4773, sumEfflTn: 36740, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '영산강', yr: '2020', sumInflBod: 141092, sumInflToc: 97019, sumInflSs: 140610, sumInflTp: 14032, sumInflTn: 45880, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '영산강', yr: '2021', sumInflBod: 131004, sumInflToc: 74582, sumInflSs: 141129, sumInflTp: 5682, sumInflTn: 45056, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '영산강', yr: '2022', sumInflBod: 124784, sumInflToc: 79326, sumInflSs: 140281, sumInflTp: 5543, sumInflTn: 47443, sumEfflBod: 132293, sumEfflToc: 83642, sumEfflSs: 140673, sumEfflTp: 8419, sumEfflTn: 46126, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '한강', yr: '2020', sumInflBod: 146258, sumInflToc: 92931, sumInflSs: 151875, sumInflTp: 4151, sumInflTn: 36537, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '한강', yr: '2021', sumInflBod: 150983, sumInflToc: 77755, sumInflSs: 153838, sumInflTp: 4098, sumInflTn: 34946, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '한강', yr: '2022', sumInflBod: 159057, sumInflToc: 82394, sumInflSs: 153619, sumInflTp: 17080, sumInflTn: 35231, sumEfflBod: 152099, sumEfflToc: 84360, sumEfflSs: 153111, sumEfflTp: 8443, sumEfflTn: 35571, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '', yr: '2020', sumInflBod: 55280, sumInflToc: 35027, sumInflSs: 55125, sumInflTp: 1695, sumInflTn: 15442, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '', yr: '2021', sumInflBod: 66809, sumInflToc: 33841, sumInflSs: 65743, sumInflTp: 2015, sumInflTn: 17674, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
{ rvbsn: '', yr: '2022', sumInflBod: 74505, sumInflToc: 40498, sumInflSs: 74899, sumInflTp: 4780, sumInflTn: 21756, sumEfflBod: 65532, sumEfflToc: 36455, sumEfflSs: 65256, sumEfflTp: 2830, sumEfflTn: 18291, avgInflBod: 0, avgInflToc: 0, avgInflSs: 0, avgInflTp: 0, avgInflTn: 0, avgEfflBod: 0, avgEfflToc: 0, avgEfflSs: 0, avgEfflTp: 0, avgEfflTn: 0 },
|
||||
];
|
||||
export const defaultInitOperWqltState: OperWqltState = {
|
||||
operWqltStatus: initialWqltData,
|
||||
};
|
||||
export const createOperWqltStore = (initState: OperWqltState = defaultInitOperWqltState) =>
|
||||
createStore<OperWqltStore>()((set) => ({
|
||||
...initState,
|
||||
setOperWqltStatus: (operWqltStatus) => set(() => ({ operWqltStatus })),
|
||||
}));
|
||||
export const operWqltStore = createOperWqltStore();
|
||||
|
||||
// 물재이용 현황
|
||||
export type ReuseState = {
|
||||
reuseStatus: ReuseStatusContent[] | null;
|
||||
};
|
||||
export type ReuseStateActions = {
|
||||
setReuseStatus: (reuseStatus: ReuseStatusContent[] | null) => void;
|
||||
};
|
||||
export type ReuseStore = ReuseState & ReuseStateActions;
|
||||
const initialReuseStatusData: ReuseStatusContent[] = [
|
||||
{ yr: '2021', sewage: 7335803, reclaimed: 254805510.3, rainwater: 14049224.5 },
|
||||
{ yr: '2022', sewage: 7378907, reclaimed: 243457788.8, rainwater: 15128093.4 },
|
||||
{ yr: '2023', sewage: 7537151, reclaimed: 222475997, rainwater: 13944732.5 },
|
||||
{ yr: '2024', sewage: 7479136, reclaimed: 265625917.7, rainwater: 13305309.5 },
|
||||
];
|
||||
export const defaultInitReuseState: ReuseState = {
|
||||
reuseStatus: initialReuseStatusData,
|
||||
};
|
||||
export const createReuseStore = (initState: ReuseState = defaultInitReuseState) =>
|
||||
createStore<ReuseStore>()((set) => ({
|
||||
...initState,
|
||||
setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })),
|
||||
}));
|
||||
export const reuseStore = createReuseStore();
|
||||
|
||||
/**
|
||||
* 두 객체 또는 배열의 깊은 비교를 수행합니다.
|
||||
* @param obj1 비교할 첫 번째 객체
|
||||
* @param obj2 비교할 두 번째 객체
|
||||
* @returns 두 객체의 내용이 같으면 true, 그렇지 않으면 false
|
||||
*/
|
||||
// function deepEqual(obj1: any, obj2: any): boolean {
|
||||
// if (obj1 === obj2) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// if (obj1 == null || typeof obj1 !== 'object' || obj2 == null || typeof obj2 !== 'object') {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// const keys1 = Object.keys(obj1);
|
||||
// const keys2 = Object.keys(obj2);
|
||||
|
||||
// if (keys1.length !== keys2.length) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// for (const key of keys1) {
|
||||
// if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 배열인 경우 순서와 내용이 모두 동일한지 확인
|
||||
// if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
// if (obj1.length !== obj2.length) return false;
|
||||
// for (let i = 0; i < obj1.length; i++) {
|
||||
// if (!deepEqual(obj1[i], obj2[i])) return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
|
||||
export class OperationApiService implements OperationService {
|
||||
|
||||
// 기준년도 조회
|
||||
public async getStandardYear(): Promise<string> {
|
||||
const { year, setYear } = yearStore.getState();
|
||||
if (year) {
|
||||
return year;
|
||||
}
|
||||
|
||||
const response = await client.get<{
|
||||
status: number;
|
||||
data: string;
|
||||
count: number;
|
||||
}>(ENDPOINT.OPERATION_SERVICE.MAIN_YEAR);
|
||||
|
||||
const newYear = response.data.data || '2024';
|
||||
setYear(newYear);
|
||||
|
||||
return newYear;
|
||||
}
|
||||
|
||||
// 수계별 시설운영 개소수 및 가동률
|
||||
public async getDashOperCntRt(year: string): Promise<OperFcltContent[]> {
|
||||
const { operFcltStatus, setOperFcltStatus } = operFcltStore.getState();
|
||||
const { setMapOperFclt } = useMapOperFcltStore.getState();
|
||||
// 간단한 캐싱: 데이터가 이미 있으면 API 호출 생략
|
||||
if (operFcltStatus && operFcltStatus.length > 0) {
|
||||
return operFcltStatus;
|
||||
}
|
||||
|
||||
const response = await client.get<{
|
||||
status: number;
|
||||
data: OperFcltContent[];
|
||||
count: number;
|
||||
}>(ENDPOINT.OPERATION_SERVICE.OPER_CNTRT, {
|
||||
params: {
|
||||
year,
|
||||
},
|
||||
});
|
||||
|
||||
const list = (response.data.data ?? []).map((item) => ({
|
||||
...item,
|
||||
cnt: Number(item.cnt),
|
||||
fctCpc: Number(item.fctCpc),
|
||||
}));
|
||||
|
||||
setOperFcltStatus(list);
|
||||
setMapOperFclt(transformToMapData(list));
|
||||
return list;
|
||||
}
|
||||
|
||||
// 수계별 유입ㆍ방류수질 분석
|
||||
public async getDashOperWqlt(year: string): Promise<OperWqltContent[]> {
|
||||
|
||||
const response = await client.get<{
|
||||
status: number;
|
||||
data: OperWqltContent[];
|
||||
count: number;
|
||||
}>(ENDPOINT.OPERATION_SERVICE.OPER_WQLT, {
|
||||
params: {
|
||||
year,
|
||||
},
|
||||
});
|
||||
|
||||
const list = (response.data.data ?? []).map((item) => ({
|
||||
...item,
|
||||
}));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// 물재이용 현황
|
||||
public async getReuseStatus(): Promise<ReuseStatusContent[]> {
|
||||
// API 호출 시에는 항상 최신 데이터를 가져오도록 캐싱 로직을 제거하거나 수정할 수 있습니다.
|
||||
// 여기서는 기존 캐싱 로직을 유지하되, 초기 데이터가 아닌 경우에만 API를 호출하도록 합니다.
|
||||
const { reuseStatus, setReuseStatus } = reuseStore.getState();
|
||||
if (reuseStatus && reuseStatus.length > 0 && reuseStatus !== defaultInitReuseState.reuseStatus) {
|
||||
return reuseStatus;
|
||||
}
|
||||
|
||||
const response = await client.get<{
|
||||
status: number;
|
||||
data: ReuseStatusContent[];
|
||||
count: number;
|
||||
}>(ENDPOINT.OPERATION_SERVICE.REUSE_STATUS);
|
||||
|
||||
const list = (response.data.data ?? []).map((item) => ({
|
||||
...item,
|
||||
reclaimed: Number(item.reclaimed),
|
||||
rainwater: Number(item.rainwater),
|
||||
}));
|
||||
|
||||
setReuseStatus(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
// 수계별 하수재이용량, 시설수
|
||||
public async getReuseMap(year: string): Promise<ReuseMapContent[]> {
|
||||
|
||||
const response = await client.get<{
|
||||
status: number;
|
||||
data: ReuseMapContent[];
|
||||
count: number;
|
||||
}>(ENDPOINT.OPERATION_SERVICE.REUSE_MAP, {
|
||||
params: {
|
||||
year,
|
||||
},
|
||||
});
|
||||
|
||||
const list = (response.data.data ?? []).map((item) => ({
|
||||
...item,
|
||||
}));
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
export const OperationApi = new OperationApiService();
|
||||
@@ -1,66 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
export interface ScheduleParams {
|
||||
siteId?: string;
|
||||
schdlTtl?: string;
|
||||
schdlSeCd?: string;
|
||||
schdlBgngYmd?: string;
|
||||
schdlEndYmd?: string;
|
||||
reptYn?: string;
|
||||
commSchdlYn?: string;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
schdlSn: number;
|
||||
siteId: string;
|
||||
schdlTtl: string;
|
||||
schdlSeCd: string;
|
||||
schdlSeNm?: string;
|
||||
schdlBgngDt: string;
|
||||
schdlEndDt: string;
|
||||
reptYn: string;
|
||||
commSchdlYn: string;
|
||||
schdlCn?: string;
|
||||
actualDate?: string;
|
||||
atflCd?: string | null;
|
||||
}
|
||||
|
||||
|
||||
export const getScheduleList = async (params?: ScheduleParams): Promise<PageableList<Schedule>> => {
|
||||
const response = await client.get<Schedule[]>(
|
||||
ENDPOINT.CMS_SERVICE.SCHEDULES,
|
||||
{ params },
|
||||
);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
content: data as Schedule[],
|
||||
pageable: {
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
offset: 0,
|
||||
pageNumber: 0,
|
||||
pageSize: data.length as number,
|
||||
paged: false,
|
||||
unpaged: true,
|
||||
},
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
totalPages: 1,
|
||||
totalElements: data.length,
|
||||
number: 0,
|
||||
numberOfElements: data.length,
|
||||
size: data.length,
|
||||
first: true,
|
||||
last: true,
|
||||
empty: data.length === 0,
|
||||
};
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
// apps/cm/src/pages/dashboard/api/sn-service.ts
|
||||
import { client } from "@/shared/api";
|
||||
import { create } from "zustand";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PplLengthContent, SewerageService } from "../model/sn-interface";
|
||||
import type { OldPplChartData, mapPplOldType, mapPplOldItem } from "../model/sn-interface";
|
||||
|
||||
export type SnState = {
|
||||
snStatus: PplLengthContent[] | null;
|
||||
};
|
||||
export type SnStateActions = {
|
||||
setSnStatus: (snStatus: PplLengthContent[] | null) => void;
|
||||
};
|
||||
export type SnStore = SnState & SnStateActions;
|
||||
|
||||
const initialUsePplChartData: OldPplChartData = {
|
||||
labels: ["금강", "한강", "낙동강", "영산강"],
|
||||
datasets: [
|
||||
{
|
||||
label: "기타",
|
||||
data: [1966, 15480, 6957, 1937],
|
||||
backgroundColor: "rgba(255, 178, 150, 0.9)"
|
||||
},
|
||||
{
|
||||
label: "오수관로",
|
||||
data: [8074, 15879, 16337, 5671],
|
||||
backgroundColor: "rgba(180, 240, 190, 0.95)"
|
||||
},
|
||||
{
|
||||
label: "우수관로",
|
||||
data: [4838, 12893, 10492, 4281],
|
||||
backgroundColor: "rgba(170, 210, 245, 0.95)"
|
||||
}
|
||||
]
|
||||
};
|
||||
const initialOldPplChartData: OldPplChartData = {
|
||||
labels: ["금강", "한강", "낙동강", "영산강"],
|
||||
datasets: [
|
||||
{
|
||||
label: "20-25년",
|
||||
data: [1140, 3181, 3100, 1662],
|
||||
backgroundColor: "#3B82F6",
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: "25-30년",
|
||||
data: [1391, 3615, 1814, 1411],
|
||||
backgroundColor: "#EF4444",
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: "30년이상",
|
||||
data: [2014, 10303, 9831, 2000],
|
||||
backgroundColor: "#F87171",
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
};
|
||||
type OldPplStoreState = {
|
||||
chartData: OldPplChartData;
|
||||
setChartData: (chartData: OldPplChartData) => void;
|
||||
};
|
||||
export const useOldPplStore = create<OldPplStoreState>((set) => ({
|
||||
chartData: initialOldPplChartData,
|
||||
setChartData: (chartData) => set(() => ({ chartData })),
|
||||
}));
|
||||
export const useUsePplStore = create<OldPplStoreState>((set) => ({
|
||||
chartData: initialUsePplChartData,
|
||||
setChartData: (chartData) => set(() => ({ chartData })),
|
||||
}));
|
||||
|
||||
export const initailMapOldPplData: mapPplOldType = {
|
||||
colTitle: { totalLen: "총길이", pplLen2025: "20~25년", pplLen2530: "25~30년", pplLen30: "30년 이상" }, //헤더 영역
|
||||
data: [
|
||||
{ name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30: 2014 },
|
||||
{ name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30: 10303 },
|
||||
{ name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30: 9831 },
|
||||
{ name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30: 2000 },
|
||||
]
|
||||
};
|
||||
type mapPplOldState = {
|
||||
mapData: mapPplOldItem[];
|
||||
setMapData: (mapData: mapPplOldItem[]) => void;
|
||||
};
|
||||
export const useMapOldPplStore = create<mapPplOldState>((set) => ({
|
||||
mapData: initailMapOldPplData.data,
|
||||
setMapData: (newMapData) => set({ mapData: newMapData }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* OldPplChartData를 지도 팝업 데이터 형태인 mapPplOldItem[]으로 변환합니다.
|
||||
* @param chartData 차트 데이터
|
||||
* @returns 유역별 노후관로 현황 데이터
|
||||
*/
|
||||
export function transformChartDataToMapData(chartData: OldPplChartData): mapPplOldItem[] {
|
||||
const mapData: mapPplOldItem[] = [];
|
||||
const { labels, datasets } = chartData;
|
||||
|
||||
const pplLen2025Data = datasets.find(d => d.label === '20-25년')?.data || [];
|
||||
const pplLen2530Data = datasets.find(d => d.label === '25-30년')?.data || [];
|
||||
const pplLen30Data = datasets.find(d => d.label === '30년이상')?.data || [];
|
||||
|
||||
labels.forEach((label, index) => {
|
||||
const pplLen2025 = pplLen2025Data[index] || 0;
|
||||
const pplLen2530 = pplLen2530Data[index] || 0;
|
||||
const pplLen30 = pplLen30Data[index] || 0;
|
||||
const totalLen = pplLen2025 + pplLen2530 + pplLen30;
|
||||
|
||||
mapData.push({
|
||||
name: label,
|
||||
totalLen,
|
||||
pplLen2025,
|
||||
pplLen2530,
|
||||
pplLen30,
|
||||
});
|
||||
});
|
||||
|
||||
return mapData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class SewerageApiService implements SewerageService {
|
||||
// 기간별 관로 총길이
|
||||
public async getPplLength(instlYmdFrom: string, instlYmdTo: string): Promise<PplLengthContent[]> {
|
||||
const response = await client.get<{
|
||||
PplLengthList: PplLengthContent[];
|
||||
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_LENGTH, {
|
||||
params: {
|
||||
instlYmdFrom,
|
||||
instlYmdTo,
|
||||
},
|
||||
});
|
||||
|
||||
const list = (response.data.PplLengthList ?? []).map((item) => ({
|
||||
...item,
|
||||
sumPrlgVl: Number(item.sumPrlgVl),
|
||||
}));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
// 기간별 용도별 관로 총길이 : useCd - SBA004 오수관, SBA003 우수관
|
||||
public async getPplUseLength(instlYmdFrom: string, instlYmdTo: string, useCd: string): Promise<PplLengthContent[]> {
|
||||
const response = await client.get<{
|
||||
PplLengthList: PplLengthContent[];
|
||||
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_USE_LENGTH, {
|
||||
params: {
|
||||
instlYmdFrom,
|
||||
instlYmdTo,
|
||||
useCd,
|
||||
},
|
||||
});
|
||||
|
||||
const list = (response.data.PplLengthList ?? []).map((item) => ({
|
||||
...item,
|
||||
sumPrlgVl: Number(item.sumPrlgVl),
|
||||
}));
|
||||
return list;
|
||||
}
|
||||
|
||||
// 유역별 용도별 관로 길이(km) MV조회 : usgCd - SBA004 오수관, SBA003 우수관
|
||||
public async getMvUsgPplLength(): Promise<PplLengthContent[]> {
|
||||
const response = await client.get<{
|
||||
PplLengthList: PplLengthContent[];
|
||||
}>(ENDPOINT.SEWERAGE_SERVICE.MV_USG_PPL_LENGTH);
|
||||
|
||||
const list = (response.data.PplLengthList ?? []).map((item) => ({
|
||||
...item,
|
||||
sumPrlgVl: Number(item.sumPrlgVl),
|
||||
}));
|
||||
return list;
|
||||
}
|
||||
// 유역별 노후도별 관로 길이(km) MV조회 : yearGroup - 20년~25년미만,25년~30년미만,30년이상
|
||||
public async getMvOldPplLength(): Promise<PplLengthContent[]> {
|
||||
const response = await client.get<{
|
||||
PplLengthList: PplLengthContent[];
|
||||
}>(ENDPOINT.SEWERAGE_SERVICE.MV_OLD_PPL_LENGTH);
|
||||
|
||||
const list = (response.data.PplLengthList ?? []).map((item) => ({
|
||||
...item,
|
||||
sumPrlgVl: Number(item.sumPrlgVl),
|
||||
}));
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const SewerageApi = new SewerageApiService();
|
||||
@@ -1,53 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
export interface Support {
|
||||
total: number;
|
||||
ongoing: number;
|
||||
han: number;
|
||||
youngsang: number;
|
||||
geum: number;
|
||||
nakdong: number;
|
||||
completed: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const getSupportList = async (): Promise<PageableList<Support>> => {
|
||||
const response = await client.get<Support[]>(
|
||||
`${ENDPOINT.IC_SERVICE.GROUPING_STATISTICS}`,
|
||||
);
|
||||
const data = response.data;
|
||||
|
||||
console.log("getSupportList data:", data);
|
||||
|
||||
return {
|
||||
content: data as Support[],
|
||||
pageable: {
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
offset: 0,
|
||||
pageNumber: 0,
|
||||
pageSize: data.length as number,
|
||||
paged: false,
|
||||
unpaged: true,
|
||||
},
|
||||
sort: {
|
||||
sorted: false,
|
||||
unsorted: true,
|
||||
empty: true,
|
||||
},
|
||||
totalPages: 1,
|
||||
totalElements: data.length,
|
||||
number: 0,
|
||||
numberOfElements: data.length,
|
||||
size: data.length,
|
||||
first: true,
|
||||
last: true,
|
||||
empty: data.length === 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import styles from "./KoreaBasinMap.module.css"; // 추가
|
||||
|
||||
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
|
||||
|
||||
const MANUAL_LABEL_POS = {
|
||||
han: { x: 0.52, y: 0.14 },
|
||||
geum: { x: 0.28, y: 0.47 },
|
||||
nakdong: { x: 0.64, y: 0.47 },
|
||||
yeongsan: { x: 0.18, y: 0.77 },
|
||||
};
|
||||
|
||||
const BASIN_DATA = {
|
||||
han: {
|
||||
name: "한강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
geum: {
|
||||
name: "금강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
nakdong: {
|
||||
name: "낙동강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
yeongsan: {
|
||||
name: "영산강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const BASIN_IDS = [
|
||||
{ key: "han", labels: ["한강", "han"] },
|
||||
{ key: "geum", labels: ["금강", "geum"] },
|
||||
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
|
||||
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
|
||||
];
|
||||
|
||||
function fmt(n: number) {
|
||||
return new Intl.NumberFormat("ko-KR").format(n);
|
||||
}
|
||||
|
||||
export function KoreaBasinMap() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const svgLayerRef = useRef<HTMLDivElement>(null);
|
||||
const labelsLayerRef = useRef<HTMLDivElement>(null);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const badgeRef = useRef<HTMLDivElement>(null);
|
||||
const rowsRef = useRef<HTMLTableSectionElement>(null);
|
||||
const colTitleRef = useRef<HTMLTableCellElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let svg: SVGSVGElement | null = null;
|
||||
let gs: SVGGElement[] = [];
|
||||
let labels: HTMLDivElement[] = [];
|
||||
|
||||
console.log("Loading SVG from:", SVG_URL);
|
||||
fetch(SVG_URL)
|
||||
.then((r) => {
|
||||
console.log("SVG fetch response:", r.status, r.statusText);
|
||||
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
|
||||
return r.text();
|
||||
})
|
||||
.then((txt) => {
|
||||
console.log("SVG content loaded, length:", txt.length);
|
||||
if (!svgLayerRef.current) {
|
||||
console.error("svgLayerRef.current is null");
|
||||
return;
|
||||
}
|
||||
svgLayerRef.current.innerHTML = txt;
|
||||
svg = svgLayerRef.current.querySelector("svg");
|
||||
if (!svg) {
|
||||
console.error("SVG element not found in loaded content");
|
||||
return;
|
||||
}
|
||||
console.log("SVG element found:", svg);
|
||||
|
||||
// viewBox/크기 세팅
|
||||
if (!svg.hasAttribute("viewBox")) {
|
||||
const bb = svg.getBBox();
|
||||
svg.setAttribute(
|
||||
"viewBox",
|
||||
[bb.x, bb.y, bb.width, bb.height].join(" "),
|
||||
);
|
||||
}
|
||||
const vb = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number);
|
||||
if (vb.length === 4 && vb[3]! > 0 && stageRef.current) {
|
||||
stageRef.current!.style.aspectRatio = String(vb[2]! / vb[3]!);
|
||||
}
|
||||
svg!.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
svg.style.width = "100%";
|
||||
svg.style.height = "100%";
|
||||
|
||||
// 1) 지도 그룹 수집
|
||||
gs = [];
|
||||
console.log("Looking for basin groups in SVG...");
|
||||
BASIN_IDS.forEach((item) => {
|
||||
let g: SVGGElement | null = null;
|
||||
for (const id of item.labels) {
|
||||
g = svg!.getElementById(id) as SVGGElement | null;
|
||||
if (g) {
|
||||
console.log(`Found group for ${item.key} with id '${id}':`, g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!g) {
|
||||
console.warn(`No group found for ${item.key} with labels:`, item.labels);
|
||||
return;
|
||||
}
|
||||
g.classList.add("basin");
|
||||
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key = item.key;
|
||||
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.label = item.labels[0] || '';
|
||||
|
||||
// 패스에 기본 스타일 적용
|
||||
const paths = g.querySelectorAll('path');
|
||||
paths.forEach((path) => {
|
||||
path.style.fill = 'rgba(0, 150, 255, 0.2)';
|
||||
path.style.stroke = '#0066cc';
|
||||
path.style.strokeWidth = '1';
|
||||
path.style.vectorEffect = 'non-scaling-stroke';
|
||||
});
|
||||
|
||||
gs.push(g);
|
||||
});
|
||||
console.log(`Found ${gs.length} basin groups`);
|
||||
|
||||
// 각 유역 그룹의 패스 정보 확인
|
||||
gs.forEach((g, index) => {
|
||||
const paths = g.querySelectorAll('path');
|
||||
console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
|
||||
element: g,
|
||||
pathCount: paths.length,
|
||||
paths: Array.from(paths).map(p => ({
|
||||
className: p.className.baseVal,
|
||||
d: p.getAttribute('d')?.substring(0, 50) + '...'
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
// 2) 함수들 먼저 정의
|
||||
const clear = () => {
|
||||
gs.forEach((el) => el.classList.remove("active"));
|
||||
if (popupRef.current) {
|
||||
if (styles.show) {
|
||||
popupRef.current.classList.remove(styles.show);
|
||||
}
|
||||
popupRef.current.style.opacity = "0";
|
||||
popupRef.current.style.visibility = "hidden";
|
||||
}
|
||||
if (badgeRef.current) {
|
||||
if (styles.show) {
|
||||
badgeRef.current.classList.remove(styles.show);
|
||||
}
|
||||
badgeRef.current.style.opacity = "0";
|
||||
badgeRef.current.style.visibility = "hidden";
|
||||
}
|
||||
labels.forEach((l) => l.classList.remove("active"));
|
||||
};
|
||||
|
||||
function showFor(g: SVGGElement) {
|
||||
console.log("showFor called for:", g);
|
||||
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
|
||||
const data = (BASIN_DATA as Record<string, { name: string; colTitle: string; rows: (string | number)[][] }>)[key] || {
|
||||
name: (g as SVGGElement & { dataset: { label: string } }).dataset.label,
|
||||
colTitle: "",
|
||||
rows: [],
|
||||
};
|
||||
console.log("Data for popup:", data);
|
||||
|
||||
if (badgeRef.current) badgeRef.current.textContent = data.name;
|
||||
if (colTitleRef.current)
|
||||
colTitleRef.current.textContent = data.colTitle || "???";
|
||||
if (rowsRef.current)
|
||||
rowsRef.current.innerHTML = data.rows
|
||||
.map(
|
||||
(r: (string | number)[]) =>
|
||||
`<tr><td>${r[0]}</td><td style="text-align:right">${fmt(Number(r[1]) || 0)}</td></tr>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
if (!stageRef.current) return;
|
||||
const sRect = stageRef.current.getBoundingClientRect(),
|
||||
r = g.getBoundingClientRect();
|
||||
const cx = (r.left + r.right) / 2 - sRect.left;
|
||||
const ty = r.top - sRect.top;
|
||||
|
||||
console.log("Popup position:", { cx, ty });
|
||||
|
||||
if (badgeRef.current) {
|
||||
badgeRef.current.style.left = cx + "px";
|
||||
badgeRef.current.style.top = ty + 24 + "px";
|
||||
}
|
||||
if (popupRef.current) {
|
||||
popupRef.current.style.left = cx + "px";
|
||||
popupRef.current.style.top = ty + 6 + "px";
|
||||
popupRef.current.style.opacity = "1";
|
||||
popupRef.current.style.visibility = "visible";
|
||||
if (styles.show) {
|
||||
popupRef.current.classList.add(styles.show);
|
||||
}
|
||||
console.log("Added show class to popup:", popupRef.current.classList.toString());
|
||||
}
|
||||
if (badgeRef.current && styles.show) {
|
||||
badgeRef.current.classList.add(styles.show);
|
||||
}
|
||||
|
||||
if (popupRef.current) {
|
||||
const pRect = popupRef.current.getBoundingClientRect();
|
||||
const x = Math.max(
|
||||
12 + pRect.width / 2,
|
||||
Math.min(cx, sRect.width - 12 - pRect.width / 2),
|
||||
);
|
||||
popupRef.current.style.left = x + "px";
|
||||
if (badgeRef.current) badgeRef.current.style.left = x + "px";
|
||||
}
|
||||
}
|
||||
|
||||
function showForWithLabel(g: SVGGElement) {
|
||||
console.log("showForWithLabel called for:", g);
|
||||
showFor(g);
|
||||
const idx = gs.indexOf(g);
|
||||
labels.forEach((l) => l.classList.remove("active"));
|
||||
if (idx >= 0) labels[idx]!.classList.add("active");
|
||||
}
|
||||
|
||||
// 3) 라벨 DOM 생성
|
||||
labels = [];
|
||||
if (labelsLayerRef.current) {
|
||||
labelsLayerRef.current.innerHTML = "";
|
||||
gs.forEach((g) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = styles.basinLabel || '';
|
||||
el.textContent = (g as SVGGElement & { dataset: { label: string } }).dataset.label || '';
|
||||
labelsLayerRef.current!.appendChild(el);
|
||||
labels.push(el);
|
||||
});
|
||||
}
|
||||
|
||||
// 3) 라벨 위치
|
||||
function positionLabels() {
|
||||
if (!stageRef.current || !svg) return;
|
||||
const sRect = stageRef.current.getBoundingClientRect();
|
||||
const inset =
|
||||
parseFloat(
|
||||
getComputedStyle(stageRef.current).getPropertyValue(
|
||||
"--map-inset",
|
||||
) || "0",
|
||||
) / 100;
|
||||
const left = sRect.width * inset,
|
||||
top = sRect.height * inset;
|
||||
const w = sRect.width * (1 - inset * 2),
|
||||
h = sRect.height * (1 - inset * 2);
|
||||
const [vx, vy, vw, vh] = (
|
||||
svg!.getAttribute("viewBox") || "0 0 100 100"
|
||||
)
|
||||
.split(/\s+/)
|
||||
.map(Number);
|
||||
|
||||
const svgRect = svg.getBoundingClientRect();
|
||||
const stageRect = stageRef.current.getBoundingClientRect();
|
||||
const offsetX = svgRect.left - stageRect.left - left;
|
||||
const offsetY = svgRect.top - stageRect.top - top;
|
||||
|
||||
gs.forEach((g, i) => {
|
||||
const key = (g as SVGGElement & { dataset: { key: string } }).dataset.key;
|
||||
const manual = (MANUAL_LABEL_POS as Record<string, { x?: number; y?: number; px?: number; py?: number }>)[key];
|
||||
let px, py;
|
||||
if (
|
||||
manual &&
|
||||
(("x" in manual && "y" in manual) ||
|
||||
("px" in manual && "py" in manual))
|
||||
) {
|
||||
if ("px" in manual) {
|
||||
px = left + (manual.px || 0) + offsetX;
|
||||
py = top + (manual.py || 0) + offsetY;
|
||||
} else {
|
||||
px = left + w * (manual.x || 0) + offsetX;
|
||||
py = top + h * (manual.y || 0) + offsetY;
|
||||
}
|
||||
} else {
|
||||
const b = g.getBBox();
|
||||
const cx = b.x + b.width / 2,
|
||||
cy = b.y + b.height / 2;
|
||||
px = ((cx - vx!) / vw!) * w + left + offsetX;
|
||||
py = ((cy - vy!) / vh!) * h + top + offsetY;
|
||||
}
|
||||
labels[i]!.style.left = `${px}px`;
|
||||
labels[i]!.style.top = `${py + 18}px`;
|
||||
});
|
||||
}
|
||||
|
||||
// 4) 상호작용 (함수들은 이미 위에서 정의됨)
|
||||
gs.forEach((g) => {
|
||||
g.addEventListener("mouseenter", () => {
|
||||
clear();
|
||||
g.classList.add("active");
|
||||
showForWithLabel(g);
|
||||
});
|
||||
g.addEventListener("mousemove", () => {
|
||||
if (g.classList.contains("active")) showForWithLabel(g);
|
||||
});
|
||||
g.addEventListener("mouseleave", clear);
|
||||
g.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
clear();
|
||||
g.classList.add("active");
|
||||
showForWithLabel(g);
|
||||
});
|
||||
});
|
||||
if (stageRef.current) {
|
||||
stageRef.current.addEventListener("mouseleave", clear);
|
||||
stageRef.current.addEventListener("click", () => clear());
|
||||
}
|
||||
|
||||
// 5) 실행
|
||||
positionLabels();
|
||||
window.addEventListener("resize", positionLabels);
|
||||
new window.ResizeObserver(positionLabels).observe(stageRef.current!);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading SVG:", error);
|
||||
if (svgLayerRef.current)
|
||||
svgLayerRef.current.innerHTML =
|
||||
`<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">지도를 불러올 수 없습니다. (${error.message})</div>`;
|
||||
});
|
||||
|
||||
// cleanup
|
||||
return () => {
|
||||
window.removeEventListener("resize", () => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
|
||||
return (
|
||||
<div
|
||||
ref={stageRef}
|
||||
id="stage"
|
||||
style={
|
||||
{
|
||||
width: "100%",
|
||||
height: "560px",
|
||||
position: "relative",
|
||||
zIndex: 20,
|
||||
"--map-inset": "0",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={styles.bgLayer}
|
||||
id="bgLayer"
|
||||
/>
|
||||
<div
|
||||
ref={svgLayerRef}
|
||||
id="svgLayer"
|
||||
className={`${styles.svgLayer} ${styles.svgContainer}`}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<div
|
||||
ref={labelsLayerRef}
|
||||
id="labelsLayer"
|
||||
className={styles.labelsLayer}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={badgeRef}
|
||||
id="badge"
|
||||
className={styles.badge}
|
||||
style={{ position: "absolute", zIndex: 10, display: "block" }}
|
||||
/>
|
||||
<div
|
||||
ref={popupRef}
|
||||
id="popup"
|
||||
className={styles.popup}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 20,
|
||||
background: "#fff",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
minWidth: 160,
|
||||
boxShadow: "0 2px 8px #0002",
|
||||
display: "block",
|
||||
opacity: 0,
|
||||
visibility: "hidden",
|
||||
}}
|
||||
>
|
||||
<table style={{ width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>구분</th>
|
||||
<th ref={colTitleRef} id="colTitle">
|
||||
시설수
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ref={rowsRef} id="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 유역 지도
|
||||
*/
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useStore } from "zustand";
|
||||
import styles from "./korea-basin-map.module.css"; // 추가
|
||||
import { basinStore } from "../api/fo-service";
|
||||
import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data";
|
||||
import { BASIN_DATA, BASIN_IDS } from "../model/map-data";
|
||||
import { svcInfo } from "../model/map-data";
|
||||
import type { mapOperFcltType, mapReuseMapType} from "../model/fo-interface";
|
||||
import type { mapPplOldType } from "../model/sn-interface";
|
||||
import { useMapOperFcltStore, OperationApi, useMapReuseMapStore } from "../api/fo-service";
|
||||
import { useMapOldPplStore } from "../api/sn-service";
|
||||
import {Button} from "@common/components";
|
||||
|
||||
const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요
|
||||
|
||||
function fmt(n: number) {
|
||||
return new Intl.NumberFormat("ko-KR").format(n);
|
||||
}
|
||||
|
||||
const mapOperFcltData: mapOperFcltType = {
|
||||
colTitle: {cnt:"개소수",rt:"가동률"}, //헤더 영역
|
||||
data: [
|
||||
{name: "금강", cnt: 710, rt: 100,},
|
||||
{name: "한강", cnt: 983, rt: 100,},
|
||||
{name: "낙동강", cnt: 1032, rt: 100,},
|
||||
{name: "영산강", cnt: 1261, rt: 100,},
|
||||
]
|
||||
};
|
||||
const mapPplOldData: mapPplOldType = {
|
||||
colTitle: {totalLen: "총길이",pplLen2025: "20~25년",pplLen2530: "25~30년",pplLen30 : "30년 이상" }, //헤더 영역
|
||||
data: [
|
||||
{name: "금강", totalLen: 4545, pplLen2025: 1140, pplLen2530: 1391, pplLen30 : 2014},
|
||||
{name: "한강", totalLen: 17099, pplLen2025: 3181, pplLen2530: 3615, pplLen30 : 10303},
|
||||
{name: "낙동강", totalLen: 14745, pplLen2025: 3100, pplLen2530: 1814, pplLen30 : 9831},
|
||||
{name: "영산강", totalLen: 5073, pplLen2025: 1662, pplLen2530: 1411, pplLen30 : 2000},
|
||||
]
|
||||
};
|
||||
const mapReuseMapData: mapReuseMapType = {
|
||||
colTitle: {reuseAmnt: "하수재이용량",cnt : "시설수" }, //헤더 영역
|
||||
data: [
|
||||
{name: "금강", reuseAmnt: 17099, cnt: 3181},
|
||||
{name: "한강", reuseAmnt: 17099, cnt: 3181},
|
||||
{name: "낙동강", reuseAmnt: 14745, cnt: 3100},
|
||||
{name: "영산강", reuseAmnt: 5073, cnt: 1662},
|
||||
]
|
||||
};
|
||||
|
||||
export function KoreaBasinMap() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const svgLayerRef = useRef<HTMLDivElement>(null);
|
||||
const labelsLayerRef = useRef<HTMLDivElement>(null);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const [services, setServices] = useState(svcInfo);
|
||||
const [popupContent, setPopupContent] = useState<{ title: string; items: { header: string; data: string }[] }>({
|
||||
title: '',
|
||||
items: [],
|
||||
});
|
||||
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
|
||||
const selectedBasinKey = basinKeyMap[selectedBasin];
|
||||
const {mapOperFclt} = useMapOperFcltStore();
|
||||
const {mapData: mapPplOld} = useMapOldPplStore();
|
||||
const {setMapData: setMapReuseMapData} = useMapReuseMapStore();
|
||||
mapOperFcltData.data = mapOperFclt;
|
||||
mapPplOldData.data = mapPplOld;
|
||||
// mapReuseMapData.data = mapReuseMap;
|
||||
let standardYear = '2024';
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
standardYear = await OperationApi.getStandardYear();
|
||||
const mapReuseMapRslt = await OperationApi.getReuseMap(standardYear);
|
||||
setMapReuseMapData(mapReuseMapRslt);
|
||||
mapReuseMapData.data = mapReuseMapRslt;
|
||||
} catch (e) {
|
||||
console.error("하수재이용량 map 데이터 조회 실패", e);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
// 유역정보
|
||||
const showFor = useCallback((g: SVGGElement) => {
|
||||
const activeService = services.find(s => s.active);
|
||||
if (!activeService) return;
|
||||
|
||||
const key = (g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key;
|
||||
const basinName = BASIN_DATA[key as keyof typeof BASIN_DATA]?.name || (g as SVGGElement & { dataset: { label: string } }).dataset.label;
|
||||
|
||||
const newPopupContent: { title: string; items: { header: string; data: string }[] } = { title: '', items: [] };
|
||||
|
||||
if (activeService.svcId === 'fo') { // 운영실태
|
||||
const item = mapOperFcltData.data.find(d => d.name === basinName);
|
||||
newPopupContent.title = `${standardYear}년도 ${basinName}유역 시설운영 현황`;
|
||||
newPopupContent.items.push({
|
||||
header: mapOperFcltData.colTitle.cnt,
|
||||
data: item ? `${fmt(item.cnt)}개소` : '-',
|
||||
});
|
||||
newPopupContent.items.push({
|
||||
header: mapOperFcltData.colTitle.rt,
|
||||
data: item ? `${item.rt}%` : '-',
|
||||
});
|
||||
} else if (activeService.svcId === 'sn') { // 하수관로
|
||||
const item = mapPplOldData.data.find(d => d.name === basinName);
|
||||
newPopupContent.title = `${standardYear}년도 ${basinName}유역 노후하수관로 현황`;
|
||||
const colTitles = mapPplOldData.colTitle;
|
||||
newPopupContent.items.push({
|
||||
header: colTitles.totalLen,
|
||||
data: item ? `${fmt(item.totalLen)}km` : '-',
|
||||
});
|
||||
newPopupContent.items.push({
|
||||
header: colTitles.pplLen2025,
|
||||
data: item ? `${fmt(item.pplLen2025)}km` : '-',
|
||||
});
|
||||
newPopupContent.items.push({
|
||||
header: colTitles.pplLen2530,
|
||||
data: item ? `${fmt(item.pplLen2530)}km` : '-',
|
||||
});
|
||||
newPopupContent.items.push({
|
||||
header: colTitles.pplLen30,
|
||||
data: item ? `${fmt(item.pplLen30)}km` : '-',
|
||||
});
|
||||
} else if (activeService.svcId === 'wr') { // 물재이용
|
||||
const item = mapReuseMapData.data.find(d => d.name === basinName);
|
||||
newPopupContent.title = `${standardYear}년도 ${basinName}유역 물재이용 현황`;
|
||||
newPopupContent.items.push({
|
||||
header: mapReuseMapData.colTitle.reuseAmnt,
|
||||
data: item ? `${fmt(item.reuseAmnt)}m³` : '-',
|
||||
});
|
||||
newPopupContent.items.push({
|
||||
header: mapReuseMapData.colTitle.cnt,
|
||||
data: item ? `${fmt(item.cnt)}개소` : '-',
|
||||
});
|
||||
}
|
||||
|
||||
setPopupContent(newPopupContent);
|
||||
|
||||
if (!stageRef.current) return;
|
||||
const sRect = stageRef.current.getBoundingClientRect(),
|
||||
r = g.getBoundingClientRect();
|
||||
const cx = (r.left + r.right) / 2 - sRect.left;
|
||||
const ty = r.top - sRect.top;
|
||||
if (popupRef.current) {
|
||||
popupRef.current.style.left = cx + "px";
|
||||
popupRef.current.style.top = ty + 6 + "px";
|
||||
popupRef.current.style.opacity = "1";
|
||||
popupRef.current.style.visibility = "visible";
|
||||
if (styles.show) {
|
||||
popupRef.current.classList.add(styles.show);
|
||||
}
|
||||
}
|
||||
|
||||
if (popupRef.current) {
|
||||
const pRect = popupRef.current.getBoundingClientRect();
|
||||
const x = Math.max(
|
||||
12 + pRect.width / 2,
|
||||
Math.min(cx, sRect.width - 12 - pRect.width / 2),
|
||||
);
|
||||
popupRef.current.style.left = x + "px";
|
||||
}
|
||||
}, [services, mapOperFclt]);
|
||||
|
||||
// selectedBasinKey prop이 변경될 때 지도를 업데이트하는 useEffect
|
||||
useEffect(() => {
|
||||
if (selectedBasinKey) {
|
||||
const svg = svgLayerRef.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
|
||||
// 모든 유역의 활성화 상태를 초기화합니다.
|
||||
svg.querySelectorAll('.basin.active').forEach(el => el.classList.remove('active'));
|
||||
|
||||
const targetGroup = svg.querySelector(`.basin[data-key="${selectedBasinKey}"]`) as SVGGElement | null;
|
||||
targetGroup?.classList.add('active');
|
||||
}
|
||||
}, [selectedBasinKey]);
|
||||
|
||||
useEffect(() => {
|
||||
let svg: SVGSVGElement | null = null;
|
||||
let gs: SVGGElement[] = [];
|
||||
let labels: HTMLDivElement[] = [];
|
||||
|
||||
const clear = () => {
|
||||
gs.forEach((el) => el.classList.remove("active"));
|
||||
if (popupRef.current) {
|
||||
if (styles.show) {
|
||||
popupRef.current.classList.remove(styles.show);
|
||||
}
|
||||
popupRef.current.style.opacity = "0";
|
||||
}
|
||||
labels.forEach((l) => l.classList.remove("active"));
|
||||
};
|
||||
|
||||
// const positionLabels = () => {
|
||||
// // This function's content is moved inside the fetch callback
|
||||
// };
|
||||
|
||||
// console.log("Loading SVG from:", SVG_URL);
|
||||
fetch(SVG_URL)
|
||||
.then((r) => {
|
||||
// console.log("SVG fetch response:", r.status, r.statusText);
|
||||
if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
|
||||
return r.text();
|
||||
})
|
||||
.then((txt) => {
|
||||
// console.log("SVG content loaded, length:", txt.length);
|
||||
if (!svgLayerRef.current) {
|
||||
console.error("svgLayerRef.current is null");
|
||||
return;
|
||||
}
|
||||
svgLayerRef.current.innerHTML = txt;
|
||||
svg = svgLayerRef.current.querySelector("svg");
|
||||
if (!svg) {
|
||||
console.error("SVG element not found in loaded content");
|
||||
return;
|
||||
}
|
||||
// console.log("SVG element found:", svg);
|
||||
|
||||
// viewBox/크기 세팅
|
||||
if (!svg.hasAttribute("viewBox")) {
|
||||
const bb = svg.getBBox();
|
||||
svg.setAttribute(
|
||||
"viewBox",
|
||||
[bb.x, bb.y, bb.width, bb.height].join(" "),
|
||||
);
|
||||
}
|
||||
const vb = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number);
|
||||
if (vb.length === 4 && vb[3]! > 0 && stageRef.current) {
|
||||
stageRef.current!.style.aspectRatio = String(vb[2]! / vb[3]!);
|
||||
}
|
||||
svg!.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
svg.style.width = "100%";
|
||||
svg.style.height = "100%";
|
||||
|
||||
// 1) 지도 그룹 수집
|
||||
gs = [];
|
||||
// console.log("Looking for basin groups in SVG...");
|
||||
BASIN_IDS.forEach((item) => {
|
||||
let g: SVGGElement | null = null;
|
||||
for (const id of item.labels) {
|
||||
g = svg!.getElementById(id) as SVGGElement | null;
|
||||
if (g) {
|
||||
// console.log(`Found group for ${item.key} with id '${id}':`, g);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!g) {
|
||||
console.warn(`No group found for ${item.key} with labels:`, item.labels);
|
||||
return;
|
||||
}
|
||||
g.classList.add("basin");
|
||||
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.key = item.key;
|
||||
(g as SVGGElement & { dataset: { key: string; label: string } }).dataset.label = item.labels[0] || '';
|
||||
|
||||
// 패스에 기본 스타일 적용
|
||||
const paths = g.querySelectorAll('path');
|
||||
paths.forEach((path) => {
|
||||
path.style.fill = 'rgba(0, 150, 255, 0.2)';
|
||||
path.style.stroke = '#0066cc';
|
||||
path.style.strokeWidth = '1';
|
||||
path.style.vectorEffect = 'non-scaling-stroke';
|
||||
});
|
||||
|
||||
gs.push(g);
|
||||
});
|
||||
// console.log(`Found ${gs.length} basin groups`);
|
||||
|
||||
// 각 유역 그룹의 패스 정보 확인
|
||||
// gs.forEach((g, index) => {
|
||||
// const paths = g.querySelectorAll('path');
|
||||
// // console.log(`Group ${index} (${(g as SVGGElement & { dataset: { key: string } }).dataset.key}):`, {
|
||||
// // element: g,
|
||||
// // pathCount: paths.length,
|
||||
// // paths: Array.from(paths).map(p => ({
|
||||
// // className: p.className.baseVal,
|
||||
// // d: p.getAttribute('d')?.substring(0, 50) + '...'
|
||||
// // }))
|
||||
// // });
|
||||
// });
|
||||
|
||||
function showForWithLabel(g: SVGGElement) {
|
||||
// console.log("showForWithLabel called for:", g);
|
||||
showFor(g);
|
||||
const idx = gs.indexOf(g);
|
||||
labels.forEach((l) => l.classList.remove("active"));
|
||||
if (idx >= 0) labels[idx]!.classList.add("active");
|
||||
}
|
||||
|
||||
// 3) 라벨 DOM 생성
|
||||
labels = [];
|
||||
if (labelsLayerRef.current) {
|
||||
labelsLayerRef.current.innerHTML = "";
|
||||
gs.forEach((g) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = styles.basinLabel || '';
|
||||
el.textContent = (g as SVGGElement & { dataset: { label: string } }).dataset.label || '';
|
||||
labelsLayerRef.current!.appendChild(el);
|
||||
labels.push(el);
|
||||
});
|
||||
}
|
||||
|
||||
// 3) 라벨 위치
|
||||
const positionLabels = () => {
|
||||
if (!stageRef.current || !svg) return;
|
||||
const sRect = stageRef.current.getBoundingClientRect();
|
||||
const inset =
|
||||
parseFloat(
|
||||
getComputedStyle(stageRef.current).getPropertyValue(
|
||||
"--map-inset",
|
||||
) || "0",
|
||||
) / 100;
|
||||
const left = sRect.width * inset,
|
||||
top = sRect.height * inset;
|
||||
const w = sRect.width * (1 - inset * 2),
|
||||
h = sRect.height * (1 - inset * 2);
|
||||
const [vx, vy, vw, vh] = (
|
||||
svg!.getAttribute("viewBox") || "0 0 100 100"
|
||||
)
|
||||
.split(/\s+/)
|
||||
.map(Number);
|
||||
|
||||
const svgRect = svg.getBoundingClientRect();
|
||||
const stageRect = stageRef.current.getBoundingClientRect();
|
||||
const offsetX = svgRect.left - stageRect.left - left;
|
||||
const offsetY = svgRect.top - stageRect.top - top;
|
||||
|
||||
gs.forEach((g, i) => {
|
||||
const key = (g as SVGGElement & { dataset: { key: string } }).dataset.key;
|
||||
const manual = (MANUAL_LABEL_POS as Record<string, { x?: number; y?: number; px?: number; py?: number }>)[key];
|
||||
let px, py;
|
||||
if (
|
||||
manual &&
|
||||
(("x" in manual && "y" in manual) ||
|
||||
("px" in manual && "py" in manual))
|
||||
) {
|
||||
if ("px" in manual) {
|
||||
px = left + (manual.px || 0) + offsetX;
|
||||
py = top + (manual.py || 0) + offsetY;
|
||||
} else {
|
||||
px = left + w * (manual.x || 0) + offsetX;
|
||||
py = top + h * (manual.y || 0) + offsetY;
|
||||
}
|
||||
} else {
|
||||
const b = g.getBBox();
|
||||
const cx = b.x + b.width / 2,
|
||||
cy = b.y + b.height / 2;
|
||||
px = ((cx - vx!) / vw!) * w + left + offsetX;
|
||||
py = ((cy - vy!) / vh!) * h + top + offsetY;
|
||||
}
|
||||
labels[i]!.style.left = `${px}px`;
|
||||
labels[i]!.style.top = `${py + 18}px`;
|
||||
});
|
||||
}
|
||||
|
||||
// 4) 상호작용 (함수들은 이미 위에서 정의됨)
|
||||
gs.forEach((g) => {
|
||||
g.addEventListener("mouseenter", () => {
|
||||
clear();
|
||||
g.classList.add("active");
|
||||
showForWithLabel(g);
|
||||
});
|
||||
g.addEventListener("mousemove", () => {
|
||||
if (g.classList.contains("active")) showForWithLabel(g);
|
||||
});
|
||||
g.addEventListener("mouseleave", clear);
|
||||
g.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
clear();
|
||||
g.classList.add("active");
|
||||
showForWithLabel(g);
|
||||
const basinKey = g.dataset.key as keyof typeof BASIN_DATA | undefined;
|
||||
if (basinKey) {
|
||||
const basinName = BASIN_DATA[basinKey].name;
|
||||
setSelectedBasin(basinName);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
if (stageRef.current) {
|
||||
stageRef.current.addEventListener("mouseleave", clear);
|
||||
stageRef.current.addEventListener("click", () => clear());
|
||||
}
|
||||
|
||||
// 5) 실행
|
||||
positionLabels();
|
||||
window.addEventListener("resize", positionLabels);
|
||||
new window.ResizeObserver(positionLabels).observe(stageRef.current!);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading SVG:", error);
|
||||
if (svgLayerRef.current)
|
||||
svgLayerRef.current.innerHTML =
|
||||
`<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 14px;">지도를 불러올 수 없습니다. (${error.message})</div>`;
|
||||
});
|
||||
|
||||
// cleanup
|
||||
return () => {
|
||||
window.removeEventListener("resize", () => {});
|
||||
};
|
||||
}, [selectedBasinKey, showFor]); // 의존성 배열에 mapOperFclt 추가 불필요
|
||||
|
||||
const handleSvcInfoClick = (clickedSvcId: string) => {
|
||||
setServices(prevServices =>
|
||||
prevServices.map(svc => ({
|
||||
...svc,
|
||||
active: svc.svcId === clickedSvcId,
|
||||
}))
|
||||
);
|
||||
};
|
||||
// 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요.
|
||||
return (
|
||||
<div className="col-span-3 !p-0 relative z-10" style={{ height: "765px", background: "rgba(255, 255, 255, 0.6)" }}>
|
||||
<div className="card card-border" style={{ height: "100%" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">유역별 정보</h3>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{services.map((svc) => (
|
||||
<Button
|
||||
key={svc.name}
|
||||
className="h-[3.2rem] float-right"
|
||||
color={svc.active ? "primary" : "white"}
|
||||
onClick={() => handleSvcInfoClick(svc.svcId)}
|
||||
>
|
||||
{svc.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="mapArea" style={{height: "calc(100% - 40px)"}}>
|
||||
<div
|
||||
ref={stageRef}
|
||||
id="stage"
|
||||
style={
|
||||
{
|
||||
aspectRatio: 0.72/1,
|
||||
width: "100%",
|
||||
maxHeight: "100%",
|
||||
position: "relative",
|
||||
margin: "auto",
|
||||
"--map-inset": "0%",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={styles.bgLayer}
|
||||
id="bgLayer"
|
||||
/>
|
||||
<div
|
||||
ref={svgLayerRef}
|
||||
id="svgLayer"
|
||||
className={`${styles.svgLayer} ${styles.svgContainer}`}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<div
|
||||
ref={labelsLayerRef}
|
||||
id="labelsLayer"
|
||||
className={styles.labelsLayer}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={popupRef}
|
||||
id="popup"
|
||||
className={`${styles.popup} absolute z-20 font-sans border border-blue-200 rounded-lg shadow-md overflow-hidden opacity-0 invisible bg-blue-50`}
|
||||
style={{
|
||||
minWidth: 160,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${Math.max(1, popupContent.items.length)}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{/* 제목 영역 */}
|
||||
<div className="bg-blue-100 text-center font-bold border-b-2 border-blue-200 flex items-center justify-center" style={{ gridColumn: `span ${Math.max(1, popupContent.items.length)}` }}>
|
||||
{popupContent.title}
|
||||
</div>
|
||||
|
||||
{/* 헤더 및 데이터 동적 렌더링 */}
|
||||
{popupContent.items.map((item, index) => (
|
||||
<div key={`header-${index}`} className={`text-center border-b-2 border-blue-200 flex items-center justify-center ${index < popupContent.items.length - 1 ? 'border-r-2' : ''}`}>
|
||||
{item.header}
|
||||
</div>
|
||||
))}
|
||||
{popupContent.items.map((item, index) => (
|
||||
<div key={`data-${index}`} className={`text-center flex items-center justify-center ${index < popupContent.items.length - 1 ? 'border-r-2' : ''}`}>
|
||||
{item.data}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface SiteStatisticsContent {
|
||||
siteId: string;
|
||||
siteNm: string;
|
||||
accessHour: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface MenuStatisticsContent {
|
||||
menuCd: number;
|
||||
menuNm: string;
|
||||
accessCnt: number;
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
memberTotCnt: number;
|
||||
memberTodayCnt: number;
|
||||
pstTotCnt: number;
|
||||
pstTodayCnt: number;
|
||||
accessTodayCnt: number;
|
||||
accessBefDayPer: number;
|
||||
apiTodayCnt: number;
|
||||
waitMemberCnt: number;
|
||||
waitCntnCnt: number;
|
||||
notiList: [];
|
||||
schdlList: [];
|
||||
siteAccessList: SiteStatisticsContent[];
|
||||
menuAccessList: MenuStatisticsContent[];
|
||||
}
|
||||
|
||||
export interface DashboardService {
|
||||
getDashboard(): Promise<Dashboard>;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
export interface OperFcltContent {
|
||||
rvbsn: string; // 유역
|
||||
cnt: number; // 개소수
|
||||
fctCpc: number; // 가동률 (용량)
|
||||
gubun: string; // 구분
|
||||
}
|
||||
|
||||
export interface OperWqltContent {
|
||||
rvbsn: string; // 유역명
|
||||
yr: string; // 연도
|
||||
|
||||
sumInflBod: number; // 유입수 BOD 합계
|
||||
sumInflToc: number; // 유입수 TOC 합계
|
||||
sumInflSs: number; // 유입수 SS 합계
|
||||
sumInflTp: number; // 유입수 TP 합계
|
||||
sumInflTn: number; // 유입수 TN 합계
|
||||
|
||||
sumEfflBod: number; // 유출수 BOD 합계
|
||||
sumEfflToc: number; // 유출수 TOC 합계
|
||||
sumEfflSs: number; // 유출수 SS 합계
|
||||
sumEfflTp: number; // 유출수 TP 합계
|
||||
sumEfflTn: number; // 유출수 TN 합계
|
||||
|
||||
avgInflBod: number; // 기간 평균 유입수 BOD
|
||||
avgInflToc: number; // 기간 평균 유입수 TOC
|
||||
avgInflSs: number; // 기간 평균 유입수 SS
|
||||
avgInflTp: number; // 기간 평균 유입수 TP
|
||||
avgInflTn: number; // 기간 평균 유입수 TN
|
||||
|
||||
avgEfflBod: number; // 기간 평균 유출수 BOD
|
||||
avgEfflToc: number; // 기간 평균 유출수 TOC
|
||||
avgEfflSs: number; // 기간 평균 유출수 SS
|
||||
avgEfflTp: number; // 기간 평균 유출수 TP
|
||||
avgEfflTn: number; // 기간 평균 유출수 TN
|
||||
}
|
||||
|
||||
export interface ReuseMapContent {
|
||||
name: string; // 유역명
|
||||
reuseAmnt: number; // 하수재이용량
|
||||
cnt: number; // 시설수
|
||||
}
|
||||
|
||||
export interface ReuseStatusContent {
|
||||
yr: string;
|
||||
sewage: number;
|
||||
reclaimed: number;
|
||||
rainwater: number;
|
||||
}
|
||||
|
||||
export interface OperationService {
|
||||
getStandardYear(): Promise<string> ;
|
||||
getDashOperCntRt(year: string): Promise<OperFcltContent[]>;
|
||||
getDashOperWqlt(year: string): Promise<OperWqltContent[]>;
|
||||
getReuseStatus(): Promise<ReuseStatusContent[]>;
|
||||
}
|
||||
|
||||
export type mapOperFcltItem = {
|
||||
name: string;
|
||||
cnt: number;
|
||||
rt: number;
|
||||
};
|
||||
|
||||
export type mapOperFcltType = {
|
||||
colTitle: {
|
||||
cnt: string; // 개소수
|
||||
rt: string; // 가동률
|
||||
};
|
||||
data: mapOperFcltItem[];
|
||||
};
|
||||
|
||||
export type mapReuseMapType = {
|
||||
colTitle: {
|
||||
reuseAmnt: string; // 하수재이용량
|
||||
cnt: string; // 시설수
|
||||
};
|
||||
data: ReuseMapContent[];
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
export const basinKeyMap: { [key: string]: string } = {
|
||||
"한강": "han",
|
||||
"금강": "geum",
|
||||
"낙동강": "nakdong",
|
||||
"영산강": "yeongsan",
|
||||
};
|
||||
|
||||
// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 ---
|
||||
// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리)
|
||||
// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준)
|
||||
export const MANUAL_LABEL_POS = {
|
||||
han: { x: 0.52, y: 0.14 },
|
||||
geum: { x: 0.28, y: 0.47 },
|
||||
nakdong: { x: 0.64, y: 0.47 },
|
||||
yeongsan: { x: 0.18, y: 0.77 },
|
||||
};
|
||||
|
||||
export const BASIN_DATA = {
|
||||
han: {
|
||||
name: "한강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
geum: {
|
||||
name: "금강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
nakdong: {
|
||||
name: "낙동강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
yeongsan: {
|
||||
name: "영산강",
|
||||
colTitle: "시설수",
|
||||
rows: [
|
||||
["500m³ 미만", 100000000],
|
||||
["500m³ 이상", 100000000],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const BASIN_IDS = [
|
||||
{ key: "han", labels: ["한강", "han"] },
|
||||
{ key: "geum", labels: ["금강", "geum"] },
|
||||
{ key: "nakdong", labels: ["낙동강", "nakdong"] },
|
||||
{ key: "yeongsan", labels: ["영산강", "yeongsan"] },
|
||||
];
|
||||
|
||||
export const svcInfo = [
|
||||
{svcId: "wr", active: true, name:"물재이용"},
|
||||
{svcId: "sn", active: false, name:"하수관로"},
|
||||
{svcId: "fo", active: false, name:"운영실태"},
|
||||
]
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { basinKeyMap } from "../model/map-data";
|
||||
export interface PplLengthContent {
|
||||
zoneNm: string;
|
||||
sumPrlgVl: number;
|
||||
usgCd?: string;
|
||||
yearGroup?: string;
|
||||
}
|
||||
|
||||
export interface SewerageService {
|
||||
getPplLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
|
||||
getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
|
||||
getMvUsgPplLength(): Promise<PplLengthContent[]>;
|
||||
getMvOldPplLength(): Promise<PplLengthContent[]>;
|
||||
}
|
||||
|
||||
export interface ChartDataSet {
|
||||
label: string;
|
||||
data: (number | undefined)[];
|
||||
backgroundColor: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
export interface OldPplChartData {
|
||||
labels: string[];
|
||||
datasets: ChartDataSet[];
|
||||
}
|
||||
|
||||
type SnOldPplItem = {
|
||||
name: string;
|
||||
colTitle: string[];
|
||||
rows: number[];
|
||||
};
|
||||
|
||||
type SnOldPplLength = {
|
||||
[key: string]: SnOldPplItem;
|
||||
};
|
||||
|
||||
export type mapPplOldItem = {
|
||||
name: string;
|
||||
totalLen: number;
|
||||
pplLen2025: number;
|
||||
pplLen2530: number;
|
||||
pplLen30: number;
|
||||
};
|
||||
|
||||
export type mapPplOldType = {
|
||||
colTitle: {
|
||||
totalLen: string; // 총길이
|
||||
pplLen2025: string; // 20~25년
|
||||
pplLen2530: string; // 25~30년
|
||||
pplLen30: string; // 30년 이상
|
||||
};
|
||||
data: mapPplOldItem[];
|
||||
};
|
||||
|
||||
|
||||
export function buildSnOldPplLength(
|
||||
chartData: {
|
||||
labels: string[];
|
||||
datasets: { label: string; data: number[] }[];
|
||||
},
|
||||
totalLengths: number[]
|
||||
): SnOldPplLength {
|
||||
const colTitle = ["총길이", ...chartData.datasets.map((ds) => ds.label)];
|
||||
|
||||
const result: SnOldPplLength = {};
|
||||
|
||||
chartData.labels.forEach((basinName, index) => {
|
||||
const key = basinKeyMap[basinName]; // geum, han, nakdong, yeongsan
|
||||
if (!key) return; // 매핑 안되면 스킵
|
||||
const total = totalLengths[index] ?? 0;
|
||||
const datasetValues = chartData.datasets.map(
|
||||
(ds) => ds.data[index] ?? 0,
|
||||
);
|
||||
result[key] = {
|
||||
name: basinName,
|
||||
colTitle,
|
||||
rows: [
|
||||
total, // 총길이 (undefined면 0)
|
||||
...datasetValues, // 각 연차 데이터 (undefined면 0)
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { Support } from "../api/support-service";
|
||||
|
||||
const typeLabelMap: Record<string, string> = {
|
||||
waterQuality: "수질",
|
||||
flood: "침수",
|
||||
technical: "기술지원",
|
||||
};
|
||||
|
||||
const supportTableColumns: ColumnDef<Support>[] = [
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: "구분",
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue() as string;
|
||||
return typeLabelMap[value] || value;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "total",
|
||||
header: "합계",
|
||||
},
|
||||
{
|
||||
accessorKey: "han",
|
||||
header: "한강",
|
||||
},
|
||||
{
|
||||
accessorKey: "nakdong",
|
||||
header: "낙동강",
|
||||
},
|
||||
{
|
||||
accessorKey: "geum",
|
||||
header: "금강",
|
||||
},
|
||||
{
|
||||
accessorKey: "youngsang",
|
||||
header: "영산강",
|
||||
},
|
||||
{
|
||||
accessorKey: "ongoing",
|
||||
header: "진행중",
|
||||
},
|
||||
{
|
||||
accessorKey: "completed",
|
||||
header: "완료",
|
||||
},
|
||||
];
|
||||
|
||||
export { supportTableColumns };
|
||||
@@ -1,9 +1,28 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { MyPost, NoticePost } from "@/entities/post";
|
||||
import type { Schedule } from "../api/schedule-service";
|
||||
import { Badge } from "@common/components";
|
||||
import type { Post, MyPost } from "@/entities/post";
|
||||
|
||||
const noticePostTableColumns: ColumnDef<NoticePost>[] = [
|
||||
const noticePostTableColumns: ColumnDef<Post>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.upndFixYn === "Y") {
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
@@ -24,35 +43,33 @@ const noticePostTableColumns: ColumnDef<NoticePost>[] = [
|
||||
];
|
||||
|
||||
const myPostTableColumns: ColumnDef<MyPost>[] = [
|
||||
{
|
||||
header: "번호",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
},
|
||||
{
|
||||
accessorKey: "wrtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "작성일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const scheduleTableColumns: ColumnDef<Schedule>[] = [
|
||||
{
|
||||
accessorKey: "schdlTtl",
|
||||
header: "주요일정",
|
||||
cell: ({ row }) => {
|
||||
const startDate = dayjs(row.original.schdlBgngDt).format("YYYY-MM-DD");
|
||||
const endDate = dayjs(row.original.schdlEndDt).format("YYYY-MM-DD");
|
||||
const schdlTtl = row.original.schdlTtl;
|
||||
return (
|
||||
<div className="text-left">
|
||||
[{startDate} ~ {endDate}]<br />{schdlTtl}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
export { noticePostTableColumns, myPostTableColumns, scheduleTableColumns };
|
||||
export { noticePostTableColumns, myPostTableColumns };
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner-service";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
Autoplay,
|
||||
} from "@common/components";
|
||||
|
||||
type BannerImageProps = {
|
||||
atflCd: string;
|
||||
lnkgUrlAddr: string;
|
||||
};
|
||||
|
||||
const BannerList: React.FC = () => {
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const BannerImage = ({ atflCd, lnkgUrlAddr }: BannerImageProps) => {
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => getImageUrlByAtflCd(atflCd),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (lnkgUrlAddr) {
|
||||
window.open(lnkgUrlAddr, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return (
|
||||
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">이미지 없음</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-[210px] w-full cursor-pointer overflow-hidden transition-opacity hover:opacity-80"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${imageUrl})` }}
|
||||
role="img"
|
||||
aria-label="배너 이미지"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
data: bannerList,
|
||||
error: bannerError,
|
||||
isLoading: isLoadingBanners,
|
||||
} = useQuery<Banner[]>({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, siteId],
|
||||
queryFn: () => getBannerList(siteId),
|
||||
enabled: !!siteId,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const plugin = useRef(Autoplay({ delay: 2000, stopOnInteraction: true }));
|
||||
|
||||
// MAIN 타입 배너만 필터링
|
||||
const mainBanners =
|
||||
bannerList?.filter((banner) => banner.bnrTypeCd === "BNRMAIN") ?? [];
|
||||
|
||||
// 에러 상태 판별 헬퍼 함수
|
||||
const getErrorStatus = (e: unknown): number | undefined => {
|
||||
if (!e || typeof e !== "object") return undefined;
|
||||
const maybe = e as { status?: number; response?: { status?: number } };
|
||||
return maybe.status ?? maybe.response?.status;
|
||||
};
|
||||
|
||||
// 401 에러 처리
|
||||
if (bannerError) {
|
||||
const is401Error =
|
||||
(bannerError instanceof Error && bannerError.message.includes("401")) ||
|
||||
getErrorStatus(bannerError) === 401;
|
||||
|
||||
if (is401Error) {
|
||||
console.error("[BannerList] 401 인증 오류:", bannerError);
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-red-500">
|
||||
인증이 필요합니다. 로그인해주세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.error("[BannerList] 배너 조회 오류:", bannerError);
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
배너를 불러올 수 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 로딩 중
|
||||
if (isLoadingBanners) {
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">배너 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// siteId가 없을 때
|
||||
if (!siteId) {
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
사이트 ID가 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "230px" }}>
|
||||
<Carousel
|
||||
plugins={[plugin.current]}
|
||||
opts={{ loop: true }}
|
||||
className="relative"
|
||||
>
|
||||
<CarouselContent className="-ml-0">
|
||||
{mainBanners.length > 0 ? (
|
||||
mainBanners.map((banner) => (
|
||||
<CarouselItem
|
||||
key={`banner-${banner.bnrSn}`}
|
||||
className="basis-full pl-0"
|
||||
>
|
||||
<BannerImage
|
||||
atflCd={banner.atflCd}
|
||||
lnkgUrlAddr={banner.lnkgUrlAddr}
|
||||
/>
|
||||
</CarouselItem>
|
||||
))
|
||||
) : (
|
||||
<CarouselItem className="basis-full pl-0">
|
||||
<div className="relative flex h-[210px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
배너가 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
)}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
|
||||
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
|
||||
</Carousel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BannerList;
|
||||
@@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
cn,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
interface BasinSelectProps {
|
||||
selectedBasin: string;
|
||||
onValueChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BasinSelect: React.FC<BasinSelectProps> = ({
|
||||
selectedBasin,
|
||||
onValueChange,
|
||||
className
|
||||
}) => {
|
||||
const basins = ["한강", "금강", "낙동강", "영산강"];
|
||||
|
||||
return (
|
||||
<Select value={selectedBasin} onValueChange={onValueChange}>
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder="유역 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{basins.map((basin) => (
|
||||
<SelectItem key={basin} value={basin}>
|
||||
{basin}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasinSelect;
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
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 { KoreaBasinMap } from "../lib/KoreaBasinMap";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
@@ -18,18 +17,7 @@ import {
|
||||
Tooltip,
|
||||
ArcElement,
|
||||
} from "chart.js";
|
||||
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";
|
||||
import NoticeList from "./notice-list";
|
||||
import BannerList from "./banner-list";
|
||||
import SupportList from "./support-list";
|
||||
import DataStatusList from "./datastatus-list";
|
||||
import MyPostList from "./mypost-list";
|
||||
import ScheduleList from "./schedule-list";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
|
||||
// Chart.js 등록
|
||||
ChartJS.register(
|
||||
@@ -54,7 +42,8 @@ interface UserInfo {
|
||||
function DashboardPage() {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserInfo = async () => {
|
||||
console.log("fetchUserInfo called");
|
||||
@@ -113,96 +102,197 @@ function DashboardPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-2 sm:p-4 md:p-6">
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* 첫번째 라인 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-8 gap-1">
|
||||
<div className="grid grid-cols-8 gap-1">
|
||||
{/* 첫번쨰 열 */}
|
||||
<div className="col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-2 h-250 !p-0">
|
||||
{/* 전국 하수처리시설 개소수 및 가동률 */}
|
||||
<FoFcltCntRt />
|
||||
<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>
|
||||
|
||||
{/* 오수관로 및 우수관로 현황 */}
|
||||
<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 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-1 md:col-span-1 lg:col-span-1 xl:col-span-2 h-250 !p-0">
|
||||
{/* 수계별 유입ㆍ방류수질 분석 */}
|
||||
<FoWqltInflEffl />
|
||||
|
||||
<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>
|
||||
|
||||
{/* 노후하수관로현황 */}
|
||||
<SnOldPpl />
|
||||
<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>
|
||||
|
||||
{/* 물재이용 현황 (재이용량, m³) */}
|
||||
<ReuseStatus />
|
||||
{/* 재이용시설(개소) */}
|
||||
<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-1 md:col-span-2 lg:col-span-1 xl:col-span-3">
|
||||
<KoreaBasinMap />
|
||||
<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="col-span-1 md:col-span-2 lg:col-span-1 xl:col-span-1 !p-0">
|
||||
<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="card card_border login_info_box" style={{ minHeight: "150px", height: "auto" }}>
|
||||
<div className="login_header" style={{ paddingLeft: "10px", paddingRight: "10px" }}>
|
||||
<div className="login_avatar">
|
||||
<Image
|
||||
src="/images/icon_loginID.png"
|
||||
alt="프로필"
|
||||
width={40}
|
||||
height={40}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E";
|
||||
}}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<b>아이디:</b> {userInfo.userId}
|
||||
</div>
|
||||
<div className="login_user_info">
|
||||
<div className="user_name">{userInfo.userName || "-"}</div>
|
||||
<div className="user_status">온라인</div>
|
||||
<div className="user_status text-xs sm:text-sm">
|
||||
<b>2FA 활성화:</b>{" "}
|
||||
{userInfo.twoFactorEnabled ? "예" : "아니오"}
|
||||
</div>
|
||||
<div>
|
||||
<b>이메일:</b> {userInfo.email || userInfo.userId}
|
||||
</div>
|
||||
<div>
|
||||
<b>이름:</b> {userInfo.userName || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<b>2FA 활성화:</b>{" "}
|
||||
{userInfo.twoFactorEnabled ? "예" : "아니오"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="login_actions" style={{ paddingLeft: "10px", paddingRight: "10px" }}>
|
||||
<button className="login_btn text-xs sm:text-sm" onClick={handleLogout}>로그아웃</button>
|
||||
<button className="signup_btn text-xs sm:text-sm" onClick={handleChangePassword}>마이페이지</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
!error && <div>사용자 정보를 불러오는 중...</div>
|
||||
)}
|
||||
|
||||
<ScheduleList />
|
||||
|
||||
<MyPostList />
|
||||
</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-1 sm:grid-cols-2 lg:grid-cols-4 gap-1">
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<SupportList />
|
||||
<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" style={{ height: "250px" }}>
|
||||
<DataStatusList />
|
||||
<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" style={{ height: "250px" }}>
|
||||
<NoticeList />
|
||||
<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" style={{ height: "250px" }}>
|
||||
<BannerList />
|
||||
<div className="card card-border">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { getDataStatusList, type DataStatus } from "../api/datastatus-service";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
Autoplay,
|
||||
} from "@common/components";
|
||||
|
||||
// 지역 타입에 따른 아이콘과 클래스 결정
|
||||
const getRegionInfo = (regionName: string) => {
|
||||
if (regionName.includes("서울특별시")) {
|
||||
return { icon: "🏛️", className: "special-city" };
|
||||
}
|
||||
if (
|
||||
regionName.includes("광역시") ||
|
||||
regionName.includes("부산") ||
|
||||
regionName.includes("대구") ||
|
||||
regionName.includes("인천") ||
|
||||
regionName.includes("광주") ||
|
||||
regionName.includes("대전") ||
|
||||
regionName.includes("울산")
|
||||
) {
|
||||
return { icon: "🏭", className: "metropolitan-city" };
|
||||
}
|
||||
return { icon: "🌊", className: "province" };
|
||||
};
|
||||
|
||||
const DataStatusList: React.FC = () => {
|
||||
const plugin = useRef(
|
||||
Autoplay({ delay: 3000, stopOnInteraction: true })
|
||||
);
|
||||
|
||||
const {
|
||||
data: dataStatusResponse,
|
||||
error: dataStatusError,
|
||||
isLoading: isLoadingDataStatus,
|
||||
} = useQuery<PageableList<DataStatus>>({
|
||||
queryKey: [ENDPOINT.DC_SERVICE.DATA_STATUS],
|
||||
queryFn: () => getDataStatusList(),
|
||||
enabled: true,
|
||||
retry: false,
|
||||
staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지
|
||||
refetchOnWindowFocus: false, // 윈도우 포커스 시 리페칭 방지
|
||||
refetchOnMount: false, // 마운트 시 리페칭 방지 (캐시된 데이터 사용)
|
||||
});
|
||||
|
||||
const dataStatusList = dataStatusResponse?.content || [];
|
||||
|
||||
// 에러 상태 판별 헬퍼 함수
|
||||
const getErrorStatus = (e: unknown): number | undefined => {
|
||||
if (!e || typeof e !== "object") return undefined;
|
||||
const maybe = e as { status?: number; response?: { status?: number } };
|
||||
return maybe.status ?? maybe.response?.status;
|
||||
};
|
||||
|
||||
// 401 에러 처리
|
||||
if (dataStatusError) {
|
||||
const is401Error =
|
||||
(dataStatusError instanceof Error && dataStatusError.message.includes("401")) ||
|
||||
getErrorStatus(dataStatusError) === 401;
|
||||
|
||||
if (is401Error) {
|
||||
console.error("[DataStatusList] 401 인증 오류:", dataStatusError);
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-red-500">
|
||||
인증이 필요합니다. 로그인해주세요.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
console.error("[DataStatusList] 데이터 조회 오류:", dataStatusError);
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[240px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
데이터를 불러올 수 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 로딩 중
|
||||
if (isLoadingDataStatus) {
|
||||
return (
|
||||
<div className="card card-border">
|
||||
<div className="relative flex h-[200px] w-full items-center justify-center bg-accent" >
|
||||
<div className="text-sm text-muted-foreground">데이터 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card_title bottomline">
|
||||
<div className="leftArea">
|
||||
<h4>자료현황</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card card_paddingnone">
|
||||
<div className="mid_info_area" style={{ width: "100%", padding: "5px" }}>
|
||||
<Carousel
|
||||
plugins={[plugin.current]}
|
||||
opts={{ loop: true }}
|
||||
className="relative mi_list_swiper"
|
||||
>
|
||||
<CarouselContent className="-ml-4 mi_list">
|
||||
{dataStatusList.length > 0 ? (
|
||||
dataStatusList.map((dataStatus) => {
|
||||
const regionInfo = getRegionInfo(dataStatus.ctpvnm);
|
||||
return (
|
||||
<CarouselItem
|
||||
key={`dataStatus-${dataStatus.ctpvnm}`}
|
||||
className={`basis-1/2 pl-4 ml_box ${regionInfo.className}`}
|
||||
style={{ width: "200px" }}
|
||||
>
|
||||
<div className="mb_section">
|
||||
<div className="tit">
|
||||
<i className="cloud-arrow-down-fill color_3">
|
||||
{regionInfo.icon}
|
||||
</i>
|
||||
<span className="tt">{dataStatus.ctpvnm}</span>
|
||||
</div>
|
||||
<div className="info_row">
|
||||
<div className="info_item">
|
||||
<div className="info_label">기본계획</div>
|
||||
<div className="info_value">
|
||||
{dataStatus.ctpvgnsvycnt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="info_item">
|
||||
<div className="info_label">파일수</div>
|
||||
<div className="info_value">{dataStatus.filecnt}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<CarouselItem className="basis-full pl-0" style={{ height: "210px" }}>
|
||||
<div className="relative flex h-[280px] w-full items-center justify-center bg-accent">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
데이터가 없습니다
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
)}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="absolute left-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
|
||||
<CarouselNext className="absolute right-2 top-1/2 z-10 !bg-[rgba(255,255,255,0.8)] text-accent-foreground" />
|
||||
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataStatusList;
|
||||
@@ -1,149 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 수계별 시설운영 개소수 및 가동률
|
||||
*/
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useStore } from "zustand";
|
||||
import BasinSelect from "./basIn-select";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import type { Chart as ChartJS } from "chart.js";
|
||||
// import type { mapOperFcltItem, mapBasinData } from "../model/fo-interface";
|
||||
import { OperationApi, operFcltStore, basinStore } from "../api/fo-service";
|
||||
|
||||
const FoFcltCntRt: React.FC = () => {
|
||||
const [year, setYear] = useState<string | null>(null);
|
||||
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
|
||||
const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus);
|
||||
|
||||
const onBasinChange = (value: string) => {
|
||||
setSelectedBasin(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const standardYear = await OperationApi.getStandardYear();
|
||||
setYear(standardYear);
|
||||
await OperationApi.getDashOperCntRt(standardYear);
|
||||
} catch (e) {
|
||||
console.error("시설운영 데이터 조회 실패", e);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const displayData = useMemo(() => {
|
||||
if (!operFcltStatus) {
|
||||
return { facilityCount: 0, operatingRate: 0, chartData: [0, 100] };
|
||||
}
|
||||
const basinData = operFcltStatus.filter(d => d.rvbsn === selectedBasin); // 선택된 유역의 데이터만 filter
|
||||
const operatingData = basinData.find(d => d.gubun === 'R'); // 선택 유역의 R 데이터만
|
||||
const nonOperatingData = basinData.find(d => d.gubun === 'A');
|
||||
|
||||
const facilityCount = operatingData?.cnt ?? 0;
|
||||
const operatingCapacity = operatingData?.fctCpc ?? 0;
|
||||
const nonOperatingCapacity = nonOperatingData?.fctCpc ?? 0;
|
||||
const totalCapacity = operatingCapacity + nonOperatingCapacity;
|
||||
|
||||
const operatingRate = totalCapacity > 0 ? Math.round((operatingCapacity / totalCapacity) * 100) : 0;
|
||||
const result = {
|
||||
facilityCount,
|
||||
operatingRate,
|
||||
chartData: [operatingCapacity, nonOperatingCapacity],
|
||||
};
|
||||
// console.log(result);
|
||||
return result;
|
||||
}, [operFcltStatus, selectedBasin]);
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">수계별 시설운영 개소수 및 가동률</h3>
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
|
||||
</div>
|
||||
<div className="h-70 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">개소수 ({year}년도)</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-center flex-1" style={{ height: "200px" }}>
|
||||
<div style={{ fontSize: "38px", textAlign: "center", fontWeight: "600", lineHeight: "1.2", color: "#0ec58a" }}>
|
||||
{displayData.facilityCount.toLocaleString()} <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">가동률 ({year}년도)</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: displayData.chartData,
|
||||
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
|
||||
}
|
||||
}
|
||||
}}
|
||||
key={displayData.operatingRate} // Key를 변경하여 차트 강제 업데이트
|
||||
plugins={[{
|
||||
id: 'centerText',
|
||||
afterDraw: (chart: ChartJS) => {
|
||||
const ctx = chart.ctx;
|
||||
const centerX = chart.width / 2;
|
||||
const centerY = chart.height / 2;
|
||||
const rate = displayData.operatingRate ?? 0;
|
||||
|
||||
// 가동률 텍스트
|
||||
ctx.save();
|
||||
ctx.font = 'bold 16px Arial';
|
||||
ctx.fillStyle = '#00DFE5';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`${rate}%`, centerX, centerY + 10);
|
||||
|
||||
// 라벨 텍스트
|
||||
ctx.font = '10px Arial';
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.fillText('가동률', centerX, centerY + 28);
|
||||
ctx.restore();
|
||||
}
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default FoFcltCntRt;
|
||||
@@ -1,246 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 수계별 유입ㆍ방류수질 분석
|
||||
*/
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useStore } from "zustand";
|
||||
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';
|
||||
import type { OperWqltContent } from "../model/fo-interface";
|
||||
import { OperationApi, basinStore } from "../api/fo-service";
|
||||
import { defaultInitOperWqltState } from "../api/fo-service";
|
||||
|
||||
// Chart.js 등록
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
|
||||
type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN';
|
||||
|
||||
const FoWqltInflEffl: React.FC = () => {
|
||||
const { selectedBasin, setSelectedBasin } = useStore(basinStore);
|
||||
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
|
||||
const [wqltData, setWqltData] = useState<OperWqltContent[]>([]);
|
||||
const tabs: WaterQualityType[] = ['BOD', 'TOC', 'SS', 'TP', 'TN'];
|
||||
|
||||
const onBasinChange = (value: string) => {
|
||||
setSelectedBasin(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setWqltData(defaultInitOperWqltState.operWqltStatus);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const standardYear = await OperationApi.getStandardYear();
|
||||
const data = await OperationApi.getDashOperWqlt(standardYear);
|
||||
setWqltData(data);
|
||||
// console.log(wqltData);
|
||||
} catch (e) {
|
||||
console.error("시설 유입ㆍ방류수질 데이터 조회 실패", e);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const filteredData = wqltData
|
||||
.filter(d => d.rvbsn === selectedBasin) // 해당 유역
|
||||
.sort((a, b) => Number(a.yr) - Number(b.yr));
|
||||
|
||||
const wqltKey = selectedTab.charAt(0).toUpperCase() + selectedTab.slice(1).toLowerCase();
|
||||
const inflowKey = `sumInfl${wqltKey}` as keyof OperWqltContent;
|
||||
const effluentKey = `sumEffl${wqltKey}` as keyof OperWqltContent;
|
||||
const inflowAvgKey = `avgInfl${wqltKey}` as keyof OperWqltContent;
|
||||
const effluentAvgKey = `avgEffl${wqltKey}` as keyof OperWqltContent;
|
||||
|
||||
|
||||
const labels = filteredData.map(d => d.yr ?? '');
|
||||
const inflowData = filteredData.map(d => d[inflowKey] as number);
|
||||
const effluentData = filteredData.map(d => d[effluentKey] as number);
|
||||
const inflowAvg = filteredData[0]?.[inflowAvgKey] as number ?? 0;
|
||||
const effluentAvg = filteredData[0]?.[effluentAvgKey] as number ?? 0;
|
||||
|
||||
return { labels, inflowData, effluentData, inflowAvg, effluentAvg };
|
||||
}, [wqltData, selectedBasin, selectedTab]);
|
||||
|
||||
// console.log(chartData);
|
||||
|
||||
// 차트 공통 옵션
|
||||
// const getChartOptions = (yAxisLabel: string): ChartOptions<'line'> => ({
|
||||
const getChartOptions = (): 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: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: `유입${selectedTab} (mg/L)`,
|
||||
data: chartData.inflowData,
|
||||
borderColor: '#00D4FF',
|
||||
backgroundColor: '#00D4FF',
|
||||
borderWidth: 1,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0.4,
|
||||
},
|
||||
{
|
||||
label: '유입 평균 (mg/L)',
|
||||
data: Array(chartData.labels.length).fill(chartData.inflowAvg),
|
||||
borderColor: '#FF1493',
|
||||
backgroundColor: '#FF1493',
|
||||
borderWidth: 1,
|
||||
borderDash: [10, 5],
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 방류 데이터 차트 설정
|
||||
const effluentChartData = {
|
||||
labels: chartData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: `방류${selectedTab} (mg/L)`,
|
||||
data: chartData.effluentData,
|
||||
borderColor: '#00FF88',
|
||||
backgroundColor: '#00FF88',
|
||||
borderWidth: 1,
|
||||
pointRadius: 2,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0.4,
|
||||
},
|
||||
{
|
||||
label: '방류 평균 (mg/L)',
|
||||
data: Array(chartData.labels.length).fill(chartData.effluentAvg),
|
||||
borderColor: '#FFB800',
|
||||
backgroundColor: '#FFB800',
|
||||
borderWidth: 1,
|
||||
borderDash: [10, 5],
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
tension: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">수계별 유입ㆍ방류수질 분석</h3>
|
||||
<BasinSelect selectedBasin={selectedBasin} onValueChange={onBasinChange} />
|
||||
</div>
|
||||
<div className="h-70 p-0 flex items-center justify-center">
|
||||
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
|
||||
{/* 탭 메뉴 */}
|
||||
<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: 'row', flex: 1, minHeight: 0 }}>
|
||||
{/* 유입 차트 */}
|
||||
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
|
||||
<Line data={inflowChartData} options={getChartOptions()} />
|
||||
</div>
|
||||
|
||||
{/* 방류 차트 */}
|
||||
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
|
||||
<Line data={effluentChartData} options={getChartOptions()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default FoWqltInflEffl;
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { PostApi, type MyPost, type NoticePost } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
myPostTableColumns,
|
||||
} from "../model/table-columns";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
|
||||
const MyPostList: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const myPostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.MY_POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getMyPostList({
|
||||
page: 0,
|
||||
size: 5,
|
||||
}),
|
||||
});
|
||||
|
||||
const handleMyPostRowClick = (row: MyPost) => {
|
||||
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
|
||||
};
|
||||
|
||||
const { tableComponent: myPostTableComponent } = useDataTable({
|
||||
query: myPostQuery,
|
||||
tableOptions: {
|
||||
columns: myPostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleMyPostRowClick
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !p-0" style={{ height: "300px" }}>
|
||||
<div className="card-header !m-0 !border-none text-foreground">
|
||||
<h3 className="text-2xl font-medium">내 게시글</h3>
|
||||
</div>
|
||||
{myPostTableComponent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyPostList;
|
||||
@@ -1,63 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { PostApi, type NoticePost } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
noticePostTableColumns,
|
||||
} from "../model/table-columns";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
|
||||
const NoticeList: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const noticePostQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS],
|
||||
queryFn: () =>
|
||||
PostApi.getNoticePostList({
|
||||
page: 0,
|
||||
size: 3,
|
||||
keywordType: "bbsTypeCd",
|
||||
keyword: "BBSNOTICE",
|
||||
}),
|
||||
});
|
||||
|
||||
const handleNoticePostRowClick = (row: NoticePost) => {
|
||||
router.push(`/cms/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`);
|
||||
};
|
||||
|
||||
const handleMoreClick = () => {
|
||||
try {
|
||||
router.push('/cms/bbsnotice/7');
|
||||
} catch (error) {
|
||||
console.error("공지사항 더보기 이동 실패", error);
|
||||
}
|
||||
};
|
||||
|
||||
const { tableComponent: noticePostTableComponent } = useDataTable({
|
||||
query: noticePostQuery,
|
||||
tableOptions: {
|
||||
columns: noticePostTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
onRowClick: handleNoticePostRowClick,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !p-0" style={{ height: "220px" }}>
|
||||
<div className="card-header !m-0 !border-none text-foreground">
|
||||
<h3 className="text-2xl font-medium">공지사항</h3>
|
||||
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
|
||||
</div>
|
||||
{noticePostTableComponent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeList;
|
||||
@@ -1,242 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 물재이용 차트
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useStore } from "zustand";
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { Button } from "@common/components";
|
||||
import type { ReuseStatusContent } from "../model/fo-interface";
|
||||
import { OperationApi, reuseStore } from "../api/fo-service";
|
||||
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 [activeIndex, setActiveIndex] = useState(0);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const reuseStatusData = useStore(
|
||||
reuseStore,
|
||||
(state) => state.reuseStatus) as ReuseStatusContent[];
|
||||
|
||||
useEffect(() => {
|
||||
OperationApi.getReuseStatus().catch((e) => {
|
||||
console.error("물재이용 차트 데이터 조회 실패", e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const labels = reuseStatusData?.map((r) => r.yr);
|
||||
const sewageData = reuseStatusData?.map((r) => r.sewage);
|
||||
const reclaimedData = reuseStatusData?.map((r) => r.reclaimed);
|
||||
const rainwaterData = reuseStatusData?.map((r) => r.rainwater);
|
||||
|
||||
const commonChartData = {
|
||||
datasets: [
|
||||
{
|
||||
label: '재이용량',
|
||||
data: sewageData, // 기본은 하수처리수
|
||||
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 commonChartOptions: ChartOptions<'line'> = {
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
const chartItems = [
|
||||
{
|
||||
title: "하수처리수 재이용량 (m³)",
|
||||
data: {
|
||||
...commonChartData,
|
||||
labels,
|
||||
datasets: [{ ...commonChartData.datasets[0], data: sewageData }],
|
||||
},
|
||||
options: commonChartOptions,
|
||||
},
|
||||
{
|
||||
title: "중수도 재이용량 (m³)",
|
||||
data: {
|
||||
...commonChartData,
|
||||
labels,
|
||||
datasets: [{ ...commonChartData.datasets[0], data: reclaimedData }],
|
||||
},
|
||||
options: commonChartOptions,
|
||||
},
|
||||
{
|
||||
title: "빗물 재이용량 (m³)",
|
||||
data: {
|
||||
...commonChartData,
|
||||
labels,
|
||||
datasets: [{ ...commonChartData.datasets[0], data: rainwaterData }],
|
||||
},
|
||||
options: commonChartOptions,
|
||||
},
|
||||
];
|
||||
|
||||
const startSlider = () => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setActiveIndex(prevIndex => (prevIndex + 1) % chartItems.length);
|
||||
}, 7000);
|
||||
};
|
||||
|
||||
const stopSlider = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startSlider();
|
||||
return () => stopSlider();
|
||||
}, []);
|
||||
|
||||
const handleIndicatorClick = (index: number) => {
|
||||
stopSlider();
|
||||
setActiveIndex(index);
|
||||
startSlider();
|
||||
};
|
||||
|
||||
const handleMoreClick = () => {
|
||||
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">{chartItems[activeIndex]?.title}</h3>
|
||||
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
|
||||
</div>
|
||||
<div className="relative" style={{ maxWidth: 400, height: 180, marginLeft: 20 }}>
|
||||
<div className="h-full">
|
||||
|
||||
{/* ✅ 좌표 기준 (절대 이동 X) */}
|
||||
<div
|
||||
ref={viewportRef}
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: 170}}
|
||||
>
|
||||
{/* ✅ 이동 대상 */}
|
||||
<div
|
||||
className="flex h-full transition-transform duration-500 ease-in-out"
|
||||
style={{
|
||||
width: `${chartItems.length * 100}%`,
|
||||
transform: `translateX(-${
|
||||
activeIndex * (viewportRef.current?.offsetWidth ?? 0)
|
||||
}px)`
|
||||
}}
|
||||
>
|
||||
{chartItems.map((chart, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-full flex-shrink-0"
|
||||
style={{ width: `${100 / chartItems.length}%` }}
|
||||
>
|
||||
<Line
|
||||
data={chart.data}
|
||||
options={chart.options as ChartOptions<'line'>}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 flex space-x-2" style={{ bottom: '-1px'}}>
|
||||
{chartItems.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleIndicatorClick(index)}
|
||||
className={`w-3 h-3 rounded-full cursor-pointer transition-colors ${
|
||||
activeIndex === index ? 'bg-blue-500' : 'bg-gray-300'
|
||||
}`}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReuseChart;
|
||||
@@ -1,85 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 물재이용 현황
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import { useStore } from "zustand";
|
||||
import { Button } from "@common/components";
|
||||
import type { ReuseStatusContent } from "../model/fo-interface";
|
||||
import { OperationApi, reuseStore} from "../api/fo-service";
|
||||
|
||||
const ReuseStatus: React.FC = () => {
|
||||
// const [reuseStatusData, setReuseStatusData] = useState<ReuseStatusContent[]>([]);
|
||||
const reuseStatusData = useStore(
|
||||
reuseStore,
|
||||
(state) => state.reuseStatus ) as ReuseStatusContent[];
|
||||
|
||||
useEffect(() => {
|
||||
OperationApi.getReuseStatus().catch((e) => {
|
||||
console.error("물재이용 현황 데이터 조회 실패", e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// 숫자 포맷 함수 (천단위 쉼표)
|
||||
const formatNumber = (num: number): string => {
|
||||
return new Intl.NumberFormat('ko-KR').format(num);
|
||||
};
|
||||
|
||||
const handleMoreClick = () => {
|
||||
window.open(process.env.NEXT_PUBLIC_WR, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">물재이용 현황 (재이용량, m³)</h3>
|
||||
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</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 style={{ height: "35px" }}>
|
||||
<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>
|
||||
{reuseStatusData && reuseStatusData.length > 0 && reuseStatusData?.map((row, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50" style={{ height: "35px" }}>
|
||||
<td className="bg-blue-100 font-semibold text-center py-1 px-3 border border-gray-300">
|
||||
{row.yr}
|
||||
</td>
|
||||
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
|
||||
{formatNumber(row.sewage)}
|
||||
</td>
|
||||
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
|
||||
{formatNumber(row.reclaimed)}
|
||||
</td>
|
||||
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
|
||||
{formatNumber(row.rainwater)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ReuseStatus;
|
||||
@@ -1,70 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
scheduleTableColumns,
|
||||
} from "../model/table-columns";
|
||||
import { getScheduleList } from "../api/schedule-service";
|
||||
|
||||
// 날짜를 YYYY-MM-DD 형식으로 포맷팅
|
||||
const formatDate = (date: Date): string => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// 오늘부터 한 달 후까지의 날짜 계산
|
||||
const getDateRange = () => {
|
||||
const today = new Date();
|
||||
const oneMonthLater = new Date();
|
||||
oneMonthLater.setMonth(today.getMonth() + 1);
|
||||
|
||||
return {
|
||||
startDate: formatDate(today),
|
||||
endDate: formatDate(oneMonthLater),
|
||||
};
|
||||
};
|
||||
|
||||
const ScheduleList: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { startDate, endDate } = getDateRange();
|
||||
|
||||
const scheduleQuery = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, startDate, endDate],
|
||||
queryFn: () =>
|
||||
getScheduleList({
|
||||
siteId: "",
|
||||
schdlTtl: "",
|
||||
schdlSeCd: "",
|
||||
schdlBgngYmd: startDate,
|
||||
schdlEndYmd: endDate,
|
||||
reptYn: "",
|
||||
commSchdlYn: "Y",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
const { tableComponent: scheduleTableComponent } = useDataTable({
|
||||
query: scheduleQuery,
|
||||
tableOptions: {
|
||||
columns: scheduleTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !p-0" style={{ height: "300px" }}>
|
||||
{scheduleTableComponent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleList;
|
||||
@@ -1,184 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 노후 하수관로 현황
|
||||
*/
|
||||
import React, { useEffect, useState } 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";
|
||||
import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface";
|
||||
import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
|
||||
|
||||
|
||||
// ChartJS 컴포넌트 등록
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
export default function SnOldPpl() {
|
||||
const [oldPplLenData, setOldPplLenData] = useState<PplLengthContent[]>([]);
|
||||
const {chartData, setChartData} = useOldPplStore();
|
||||
const {setMapData} = useMapOldPplStore();
|
||||
|
||||
useEffect(() => {
|
||||
// 유역별, 노후년도별 관로길이 데이터를 한번에 조회
|
||||
SewerageApi.getMvOldPplLength()
|
||||
.then((apiResult) => {
|
||||
setOldPplLenData(apiResult);
|
||||
})
|
||||
.catch((e) => console.error("노후관로 데이터 조회 실패", e));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (oldPplLenData.length === 0) return;
|
||||
|
||||
// 유역 목록 추출 (중복 제거)
|
||||
const allZones = [...new Set(oldPplLenData.map(d => d.zoneNm))];
|
||||
|
||||
// yearGroup 매핑: API 응답값 -> 차트 label
|
||||
const yearGroupMap: { [key: string]: { label: string; color: string } } = {
|
||||
"20년~25년미만": { label: "20-25년", color: "#3B82F6" },
|
||||
"25년~30년미만": { label: "25-30년", color: "#EF4444" },
|
||||
"30년이상": { label: "30년이상", color: "#F87171" },
|
||||
};
|
||||
|
||||
// 유역별, 노후년도별 데이터 구조화
|
||||
const dataByZone: { [zone: string]: { [yearGroup: string]: number } } = {};
|
||||
allZones.forEach(zone => {
|
||||
dataByZone[zone] = {};
|
||||
});
|
||||
|
||||
oldPplLenData.forEach(item => {
|
||||
let zoneData = dataByZone[item.zoneNm];
|
||||
if (!zoneData) {
|
||||
zoneData = {};
|
||||
dataByZone[item.zoneNm] = zoneData;
|
||||
}
|
||||
if (item.yearGroup) {
|
||||
zoneData[item.yearGroup] = item.sumPrlgVl;
|
||||
}
|
||||
});
|
||||
|
||||
// 데이터셋 생성
|
||||
const newDatasets: ChartDataSet[] = Object.entries(yearGroupMap).map(([yearGroup, { label, color }]) => ({
|
||||
label,
|
||||
data: allZones.map(zone => dataByZone[zone]?.[yearGroup] ?? 0),
|
||||
backgroundColor: color,
|
||||
borderRadius: 4,
|
||||
}));
|
||||
|
||||
const newChartData: OldPplChartData = {
|
||||
labels: allZones,
|
||||
datasets: newDatasets,
|
||||
};
|
||||
|
||||
setChartData(newChartData);
|
||||
|
||||
// 차트 데이터를 기반으로 지도 팝업 데이터 생성 및 상태 업데이트
|
||||
setMapData(transformChartDataToMapData(newChartData));
|
||||
}, [oldPplLenData]);
|
||||
|
||||
// 각 지역별 총 길이 계산
|
||||
const totalLengths = chartData.labels.map((label, i) => {
|
||||
return chartData.datasets.reduce((sum, dataset) => {
|
||||
const value = dataset.data[i];
|
||||
return sum + (typeof value === 'number' ? value : 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
|
||||
// 차트 옵션 설정
|
||||
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 Number(value).toLocaleString() + '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}: ${Number(context.parsed.y).toLocaleString()}km`;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// 총 길이 데이터 표시를 위한 애니메이션 후 함수 설정
|
||||
animation: {
|
||||
onComplete: function (animation) {
|
||||
const chart = animation.chart;
|
||||
const ctx = chart.ctx;
|
||||
|
||||
// 각 데이터셋의 마지막 데이터셋의 인덱스
|
||||
if (chartData.datasets.length === 0) return;
|
||||
// const lastMeta = chart.getDatasetMeta(chartData.datasets.length - 1);
|
||||
// 텍스트 스타일 설정
|
||||
ctx.font = '11px Arial';
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
// 각 지역별로 총 길이 표시
|
||||
chartData.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 handleMoreClick = () => {
|
||||
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">노후 하수관로 현황</h3>
|
||||
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
|
||||
</div>
|
||||
<div className="h-70 p-0 flex items-center justify-center">
|
||||
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
|
||||
<Bar options={options} data={chartData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 오수관로 및 우수관로 현황
|
||||
*/
|
||||
import React, { useEffect, useState } 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";
|
||||
import type { PplLengthContent } from "../model/sn-interface";
|
||||
import { SewerageApi,useUsePplStore } from "../api/sn-service";
|
||||
|
||||
interface ChartDataSet {
|
||||
label: string;
|
||||
data: (number | undefined)[];
|
||||
backgroundColor: string;
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
// const usgCd = { "SBA004": "오수관로", "SBA003": "우수관로", "ETC": "기타" }; // 용도
|
||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
|
||||
|
||||
export default function SnPplStatus() {
|
||||
const {chartData, setChartData} = useUsePplStore();
|
||||
const [usgPplLenData, setUsgPplLenData] = useState<PplLengthContent[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// 유역별, 용도별 관로길이 데이터를 한번에 조회
|
||||
SewerageApi.getMvUsgPplLength()
|
||||
.then((apiResult) => {
|
||||
setUsgPplLenData(apiResult);
|
||||
})
|
||||
.catch((e) => console.error("용도별 관로 데이터 조회 실패", e));
|
||||
}, []);
|
||||
|
||||
// 데이터가 변경될 때마다 차트를 다시 계산
|
||||
useEffect(() => {
|
||||
if (usgPplLenData.length === 0) return;
|
||||
|
||||
// 유역 목록 추출 (중복 제거)
|
||||
const allZones = [...new Set(usgPplLenData.map(d => d.zoneNm))];
|
||||
const labels = allZones;
|
||||
|
||||
// 유역별, 용도별 데이터 구조화
|
||||
const dataByZone: { [key: string]: { sewage: number; stormwater: number; etc: number } } = {};
|
||||
labels.forEach(zone => {
|
||||
dataByZone[zone] = { sewage: 0, stormwater: 0, etc: 0 };
|
||||
});
|
||||
|
||||
// usgCd별로 데이터 분류: SBA004(오수관로), SBA003(우수관로), ETC(기타)
|
||||
usgPplLenData.forEach(item => {
|
||||
if (!dataByZone[item.zoneNm]) {
|
||||
dataByZone[item.zoneNm] = { sewage: 0, stormwater: 0, etc: 0 };
|
||||
}
|
||||
if (item.usgCd === "SBA004") {
|
||||
dataByZone[item.zoneNm]!.sewage = item.sumPrlgVl;
|
||||
} else if (item.usgCd === "SBA003") {
|
||||
dataByZone[item.zoneNm]!.stormwater = item.sumPrlgVl;
|
||||
} else if (item.usgCd === "ETC") {
|
||||
dataByZone[item.zoneNm]!.etc = item.sumPrlgVl;
|
||||
}
|
||||
});
|
||||
|
||||
const datasets: ChartDataSet[] = [];
|
||||
|
||||
// 기타 데이터셋 (맨 아래에 쌓기 위해 먼저 추가)
|
||||
datasets.push({
|
||||
label: "기타",
|
||||
data: labels.map(zone => dataByZone[zone]?.etc),
|
||||
backgroundColor: "rgba(255, 178, 150, 0.9)",
|
||||
});
|
||||
|
||||
// 오수관로 데이터셋
|
||||
datasets.push({
|
||||
label: "오수관로",
|
||||
data: labels.map(zone => dataByZone[zone]?.sewage),
|
||||
backgroundColor: "rgba(180, 240, 190, 0.95)",
|
||||
});
|
||||
|
||||
// 우수관로 데이터셋
|
||||
datasets.push({
|
||||
label: "우수관로",
|
||||
data: labels.map(zone => dataByZone[zone]?.stormwater),
|
||||
backgroundColor: "rgba(170, 210, 245, 0.95)",
|
||||
});
|
||||
|
||||
setChartData({ labels, datasets });
|
||||
|
||||
}, [usgPplLenData]);
|
||||
|
||||
// 총합(라벨당) 계산
|
||||
const totals = chartData.labels.map((_, i) =>
|
||||
chartData.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(chartData.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 handleMoreClick = () => {
|
||||
window.open(process.env.NEXT_PUBLIC_SN, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
return (
|
||||
<div className="card card-border" style={{ height: "250px" }}>
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">오수관로 및 우수관로 현황</h3>
|
||||
|
||||
<Button className="h-[3.2rem]" onClick={handleMoreClick}>더보기</Button>
|
||||
</div>
|
||||
<div className="h-60 p-4">
|
||||
<div style={{ width: "100%", maxWidth: 400, height: 180 }}>
|
||||
<Bar options={options} data={chartData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import { supportTableColumns } from "../model/support-columns";
|
||||
import { getSupportList, type Support } from "../api/support-service";
|
||||
|
||||
const SupportList: React.FC = () => {
|
||||
const supportQuery = useQuery<PageableList<Support>>({
|
||||
queryKey: [ENDPOINT.IC_SERVICE.GROUPING_STATISTICS],
|
||||
queryFn: async () => {
|
||||
return await getSupportList();
|
||||
},
|
||||
});
|
||||
|
||||
const { tableComponent: supportTableComponent } = useDataTable({
|
||||
query: supportQuery,
|
||||
tableOptions: {
|
||||
columns: supportTableColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
variant: "line",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !p-0" style={{ height: "220px" }}>
|
||||
<div className="card-header !m-0 !border-none text-foreground">
|
||||
<h3 className="text-2xl font-medium">지원현황</h3>
|
||||
</div>
|
||||
{supportTableComponent}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportList;
|
||||
@@ -44,7 +44,7 @@ export const ENDPOINT = {
|
||||
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
|
||||
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
BANNERS: "/cms-service/api/v1/bnrs/user",
|
||||
BANNERS: "/cms-service/api/v1/bnrs",
|
||||
TEMPLATE: "/cms-service/api/v1/tmplt",
|
||||
TERMS: "/cms-service/api/v1/trms",
|
||||
LWPO_CODE: "/cms-service/api/v1/lwpo-cds",
|
||||
@@ -61,38 +61,23 @@ export const ENDPOINT = {
|
||||
APILOG: "/cms-service/api/v1/api-logs",
|
||||
SITE_AUTHORITY: "/cms-service/api/v1/site-authrt",
|
||||
CONNECTION_LOG: "/cms-service/api/v1/cntnlog",
|
||||
SCEHDULES: "/cms-service/api/v1/schdls",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt",
|
||||
POSTS: "/board-service/api/v1/posts",
|
||||
MY_POSTS: "/board-service/api/v1/posts/me",
|
||||
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
|
||||
POST_EVALUATION: "/board-service/api/v1/pstEvl",
|
||||
SITE_AUTHORITY_LIST: "/cms-service/api/v1/site-authrt/user",
|
||||
CONNECTION_LOG_LIST: "/cms-service/api/v1/cntnlog/user",
|
||||
},
|
||||
SEWERAGE_SERVICE: {
|
||||
PPL_LENGTH: "/sewerage-service/api/v1/ms/pplLength",
|
||||
PPL_USE_LENGTH: "/sewerage-service/api/v1/ms/pplUseLength",
|
||||
PPL_COUNTS: "/sewerage-service/api/v1/ms/pplCounts",
|
||||
OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts",
|
||||
MV_USG_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvUsgPplLength",
|
||||
MV_OLD_PPL_LENGTH: "/sewerage-service/api/v1/ms/mvOldPplLength",
|
||||
},
|
||||
OPERATION_SERVICE: {
|
||||
MAIN_YEAR: "/operation-service/api/v1/dashBoard/main/year",
|
||||
OPER_CNTRT: "/operation-service/api/v1/dashBoard/main/oper/cntrt",
|
||||
OPER_WQLT: "/operation-service/api/v1/dashBoard/main/oper/wqlt",
|
||||
REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status",
|
||||
REUSE_MAP: "/operation-service/api/v1/dashBoard/main/reuse/map",
|
||||
},
|
||||
},
|
||||
IC_SERVICE: {
|
||||
GROUPING_STATISTICS: "/ic-service/api/v1/support-status-summary",
|
||||
GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics",
|
||||
},
|
||||
DC_SERVICE: {
|
||||
DATA_STATUS: "/dc-service/api/v1/dataStatus",
|
||||
DATA_STATUS: "/dc-service/api/v1/dc/dataStatus",
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { type UseQueryResult } from "@tanstack/react-query";
|
||||
import type { PageableList } from "../model";
|
||||
|
||||
@@ -10,24 +9,24 @@ import type { PageableList } from "../model";
|
||||
export function matchQueryStatus<T>(
|
||||
query: UseQueryResult<PageableList<T>>,
|
||||
options: {
|
||||
Loading?: React.ReactElement;
|
||||
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
|
||||
Empty?: React.ReactElement;
|
||||
Loading?: JSX.Element;
|
||||
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
|
||||
Empty?: JSX.Element;
|
||||
Success: (
|
||||
data: UseQueryResult<PageableList<T>> & {
|
||||
data: NonNullable<UseQueryResult<T>["data"]>;
|
||||
},
|
||||
) => React.ReactElement;
|
||||
) => JSX.Element;
|
||||
},
|
||||
): React.ReactElement;
|
||||
): JSX.Element;
|
||||
export function matchQueryStatus<T>(
|
||||
query: UseQueryResult<PageableList<T>>,
|
||||
options: {
|
||||
Loading?: React.ReactElement;
|
||||
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
|
||||
Success: (data: UseQueryResult<PageableList<T>>) => React.ReactElement;
|
||||
Loading?: JSX.Element;
|
||||
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
|
||||
Success: (data: UseQueryResult<PageableList<T>>) => JSX.Element;
|
||||
},
|
||||
): React.ReactElement;
|
||||
): JSX.Element;
|
||||
export function matchQueryStatus<T>(
|
||||
query: UseQueryResult<PageableList<T>>,
|
||||
{
|
||||
@@ -36,12 +35,12 @@ export function matchQueryStatus<T>(
|
||||
Empty,
|
||||
Success,
|
||||
}: {
|
||||
Loading?: React.ReactElement;
|
||||
Errored?: React.ReactElement | ((error: unknown) => React.ReactElement);
|
||||
Empty?: React.ReactElement;
|
||||
Success: (data: UseQueryResult<PageableList<T>>) => React.ReactElement;
|
||||
Loading?: JSX.Element;
|
||||
Errored?: JSX.Element | ((error: unknown) => JSX.Element);
|
||||
Empty?: JSX.Element;
|
||||
Success: (data: UseQueryResult<PageableList<T>>) => JSX.Element;
|
||||
},
|
||||
): React.ReactElement {
|
||||
): JSX.Element {
|
||||
if (query.isLoading && Loading) {
|
||||
return Loading;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { PageTitle } from "@common/components";
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLayout };
|
||||
@@ -1,5 +1,22 @@
|
||||
import { PageTitle } from "@common/components";
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="h-auto min-h-full bg-container pb-2">{children}</div>;
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLayout };
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"@tinymce/tinymce-react": "^6.1.0",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "15.3.8",
|
||||
"next": "15.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
File diff suppressed because one or more lines are too long
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
/**
|
||||
* Jquery integration plugin.
|
||||
*
|
||||
* @class tinymce.core.JqueryIntegration
|
||||
* @private
|
||||
*/
|
||||
!function(){function f(){
|
||||
// Reference to tinymce needs to be lazily evaluated since tinymce
|
||||
// might be loaded through the compressor or other means
|
||||
return d.tinymce}var p,c,u,s=[],d="undefined"!=typeof global?global:window,m=d.jQuery;m.fn.tinymce=function(o){var e,t,i,n,l=this,r="";
|
||||
// No match then just ignore the call
|
||||
return l.length?
|
||||
// Get editor instance
|
||||
o?(l.css("visibility","hidden"),
|
||||
// Load TinyMCE on demand, if we need to
|
||||
d.tinymce||c||!(e=o.script_url)?
|
||||
// Delay the init call until tinymce is loaded
|
||||
1===c?s.push(a):a():(c=1,t=e.substring(0,e.lastIndexOf("/")),
|
||||
// Check if it's a dev/src version they want to load then
|
||||
// make sure that all plugins, themes etc are loaded in source mode as well
|
||||
-1!=e.indexOf(".min")&&(r=".min"),
|
||||
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
|
||||
// core script to locate other resources like CSS files, dialogs etc
|
||||
// You can also predefined a tinyMCEPreInit object and then it will use that instead
|
||||
d.tinymce=d.tinyMCEPreInit||{base:t,suffix:r},
|
||||
// url contains gzip then we assume it's a compressor
|
||||
-1!=e.indexOf("gzip")&&(i=o.language||"en",e=e+(/\?/.test(e)?"&":"?")+"js=true&core=true&suffix="+escape(r)+"&themes="+escape(o.theme||"modern")+"&plugins="+escape(o.plugins||"")+"&languages="+(i||""),
|
||||
// Check if compressor script is already loaded otherwise setup a basic one
|
||||
d.tinyMCE_GZ||(d.tinyMCE_GZ={start:function(){function n(e){f().ScriptLoader.markDone(f().baseURI.toAbsolute(e))}
|
||||
// Add core languages
|
||||
n("langs/"+i+".js"),
|
||||
// Add themes with languages
|
||||
n("themes/"+o.theme+"/theme"+r+".js"),n("themes/"+o.theme+"/langs/"+i+".js"),
|
||||
// Add plugins with languages
|
||||
m.each(o.plugins.split(","),function(e,t){t&&(n("plugins/"+t+"/plugin"+r+".js"),n("plugins/"+t+"/langs/"+i+".js"))})},end:function(){}})),(n=document.createElement("script")).type="text/javascript",n.onload=n.onreadystatechange=function(e){e=e||window.event,2===c||"load"!=e.type&&!/complete|loaded/.test(n.readyState)||(f().dom.Event.domLoaded=1,c=2,
|
||||
// Execute callback after mainscript has been loaded and before the initialization occurs
|
||||
o.script_loaded&&o.script_loaded(),a(),m.each(s,function(e,t){t()}))},n.src=e,document.body.appendChild(n)),l):f()?f().get(l[0].id):null:l;function a(){var a=[],c=0;
|
||||
// Apply patches to the jQuery object, only once
|
||||
u||(v(),u=!0),
|
||||
// Create an editor instance for each matched node
|
||||
l.each(function(e,t){var n,i=t.id,r=o.oninit;
|
||||
// Generate unique id for target element if needed
|
||||
i||(t.id=i=f().DOM.uniqueId()),
|
||||
// Only init the editor once
|
||||
f().get(i)||(
|
||||
// Create editor instance and render it
|
||||
n=f().createEditor(i,o),a.push(n),n.on("init",function(){var e,t=r;l.css("visibility",""),
|
||||
// Run this if the oninit setting is defined
|
||||
// this logic will fire the oninit callback ones each
|
||||
// matched editor instance is initialized
|
||||
r&&++c==a.length&&("string"==typeof t&&(e=-1===t.indexOf(".")?null:f().resolve(t.replace(/\.\w+$/,"")),t=f().resolve(t)),
|
||||
// Call the oninit function with the object
|
||||
t.apply(e||f(),a))}))}),
|
||||
// Render the editor instances in a separate loop since we
|
||||
// need to have the full editors array used in the onInit calls
|
||||
m.each(a,function(e,t){t.render()})}},
|
||||
// Add :tinymce pseudo selector this will select elements that has been converted into editor instances
|
||||
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
|
||||
m.extend(m.expr[":"],{tinymce:function(e){var t;return!!(e.id&&"tinymce"in d&&(t=f().get(e.id))&&t.editorManager===f())}});
|
||||
// This function patches internal jQuery functions so that if
|
||||
// you for example remove an div element containing an editor it's
|
||||
// automatically destroyed by the TinyMCE API
|
||||
var v=function(){function r(e){
|
||||
// If the function is remove
|
||||
"remove"===e&&this.each(function(e,t){var n=u(t);n&&n.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(e,t){var n=f().get(t.id.replace(/_parent$/,""));n&&n.remove()})}function o(i){var e,t=this;
|
||||
// Handle set value
|
||||
/*jshint eqnull:true */if(null!=i)r.call(t),
|
||||
// Saves the contents before get/set value of textarea/div
|
||||
t.each(function(e,t){var n;(n=f().get(t.id))&&n.setContent(i)});else if(0<t.length&&(e=f().get(t[0].id)))return e.getContent()}function l(e){return e&&e.length&&d.tinymce&&e.is(":tinymce")}
|
||||
// Removes any child editor instances by looking for editor wrapper elements
|
||||
var u=function(e){var t=null;return e&&e.id&&d.tinymce?f().get(e.id):t},s={};
|
||||
// Loads or saves contents from/to textarea if the value
|
||||
// argument is defined it will set the TinyMCE internal contents
|
||||
// Patch some setter/getter functions these will
|
||||
// now be able to set/get the contents of editor instances for
|
||||
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
|
||||
m.each(["text","html","val"],function(e,t){var a=s[t]=m.fn[t],c="text"===t;m.fn[t]=function(e){var t=this;if(!l(t))return a.apply(t,arguments);if(e!==p)return o.call(t.filter(":tinymce"),e),a.apply(t.not(":tinymce"),arguments),t;// return original set for chaining
|
||||
var i="",r=arguments;return(c?t:t.eq(0)).each(function(e,t){var n=u(t);i+=n?c?n.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):n.getContent({save:!0}):a.apply(m(t),r)}),i}}),
|
||||
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
|
||||
m.each(["append","prepend"],function(e,t){var n=s[t]=m.fn[t],r="prepend"===t;m.fn[t]=function(i){var e=this;return l(e)?i!==p?("string"==typeof i&&e.filter(":tinymce").each(function(e,t){var n=u(t);n&&n.setContent(r?i+n.getContent():n.getContent()+i)}),n.apply(e.not(":tinymce"),arguments),e):void 0:n.apply(e,arguments)}}),
|
||||
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
|
||||
m.each(["remove","replaceWith","replaceAll","empty"],function(e,t){var n=s[t]=m.fn[t];m.fn[t]=function(){return r.call(this,t),n.apply(this,arguments)}}),s.attr=m.fn.attr,
|
||||
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
|
||||
m.fn.attr=function(e,t){var n=this,i=arguments;if(!e||"value"!==e||!l(n))return s.attr.apply(n,i);if(t!==p)return o.call(n.filter(":tinymce"),t),s.attr.apply(n.not(":tinymce"),i),n;// return original set for chaining
|
||||
var r=n[0],a=u(r);return a?a.getContent({save:!0}):s.attr.apply(m(r),i)}}}();
|
||||
@@ -1,3 +0,0 @@
|
||||
This is where language files should be placed.
|
||||
|
||||
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
|
||||
File diff suppressed because one or more lines are too long
@@ -1,504 +0,0 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function f(t,e,n){t.execCommand("UL"===e?"InsertUnorderedList":"InsertOrderedList",!1,!1===n?null:{"list-style-type":n})}function i(t){return function(){return t}}function t(t){return t}function e(){return s}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),g=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=i(!1),l=i(!0),s={fold:function(t,e){return t()},isSome:u,isNone:l,getOr:t,getOrThunk:r,getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:i(null),getOrUndefined:i(void 0),or:t,orThunk:r,map:e,each:function(){},bind:e,exists:u,forall:l,filter:function(){return s},toArray:function(){return[]},toString:i("none()")};function r(t){return t()}function d(t){return t&&/^(TH|TD)$/.test(t.nodeName)}function m(c,a){return function(s){function t(t){var e,n,r,o,i,u,l;s.setActive((e=c,r=a,i=-1!==(o=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}((n=t).parents,d))?n.parents.slice(0,o):n.parents,0<(u=g.grep(i,(l=e,function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(e=t,l.$.contains(l.getBody(),e));var e}))).length&&u[0].nodeName===r))}return c.on("NodeChange",t),function(){return c.off("NodeChange",t)}}}function c(t,e,n,r,o,i){var u,l,s,c,a,d;1<i.length?(c=r,a=o,d=i,(s=t).ui.registry.addSplitButton(e,{tooltip:n,icon:"OL"===a?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:function(t){t(g.map(d,function(t){return{type:"choiceitem",value:"default"===t?"":t,icon:"list-"+("OL"===a?"num":"bull")+"-"+("disc"===t||"decimal"===t?"default":t),text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}}))},onAction:function(){return s.execCommand(c)},onItemAction:function(t,e){f(s,a,e)},select:function(e){var t,n=(t=s).dom.getParent(t.selection.getNode(),"ol,ul"),r=t.dom.getStyle(n,"listStyleType");return p(r).map(function(t){return e===t}).getOr(!1)},onSetup:m(s,a)})):(l=r,(u=t).ui.registry.addToggleButton(e,{active:!1,tooltip:n,icon:"OL"===o?"ordered-list":"unordered-list",onSetup:m(u,o),onAction:function(){return u.execCommand(l)}}))}var a=function(n){function t(){return o}function e(t){return t(n)}var r=i(n),o={fold:function(t,e){return e(n)},isSome:l,isNone:u,getOr:r,getOrThunk:r,getOrDie:r,getOrNull:r,getOrUndefined:r,or:t,orThunk:t,map:function(t){return a(t(n))},each:function(t){t(n)},bind:e,exists:e,forall:e,filter:function(t){return t(n)?o:s},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},p=function(t){return null==t?s:a(t)};n.add("advlist",function(t){var n,e,r,o;t.hasPlugin("lists")?(c(e=t,"numlist","Numbered list","InsertOrderedList","OL",(r=e.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman"))?r.split(/[ ,]/):[]),c(e,"bullist","Bullet list","InsertUnorderedList","UL",(o=e.getParam("advlist_bullet_styles","default,circle,square"))?o.split(/[ ,]/):[]),(n=t).addCommand("ApplyUnorderedListStyle",function(t,e){f(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){f(n,"OL",e["list-style-type"])})):console.error("Please use the Lists plugin together with the Advanced List plugin.")})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function a(e){return e.getAttribute("id")||e.getAttribute("name")||""}function c(e){return e&&"a"===e.nodeName.toLowerCase()&&!e.getAttribute("href")&&""!==a(e)}function d(e){return e.dom.getParent(e.selection.getStart(),l)}function r(e,t){var o,a,n,r,i,l=d(e);l?(n=e,r=t,(i=l).removeAttribute("name"),i.id=r,n.addVisual(),n.undoManager.add()):(a=t,(o=e).undoManager.transact(function(){var e,n;o.getParam("allow_html_in_named_anchor",!1,"boolean")||o.selection.collapse(!0),o.selection.isCollapsed()?o.insertContent(o.dom.createHTML("a",{id:a})):(n=(e=o).dom,u(n).walk(e.selection.getRng(),function(e){s.each(e,function(e){var t;c(t=e)&&!t.firstChild&&n.remove(e,!1)})}),o.formatter.remove("namedAnchor",null,null,!0),o.formatter.apply("namedAnchor",{value:a}),o.addVisual())})),e.focus()}function i(r){return function(e){for(var t,n=0;n<e.length;n++){var o=e[n],a=void 0;!(a=t=o)||a.attr("href")||!a.attr("id")&&!a.attr("name")||t.firstChild||o.attr("contenteditable",r)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),u=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),s=tinymce.util.Tools.resolve("tinymce.util.Tools"),l="a:not([href])";e.add("anchor",function(e){var t,n,o;(t=e).on("PreInit",function(){t.parser.addNodeFilter("a",i("false")),t.serializer.addNodeFilter("a",i(null))}),(n=e).addCommand("mceAnchor",function(){var o,e,t;t=(e=d(o=n))?a(e):"",o.windowManager.open({title:"Anchor",size:"normal",body:{type:"panel",items:[{name:"id",type:"input",label:"ID",placeholder:"example"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{id:t},onSubmit:function(e){var t=o,n=e.getData().id;(/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(n)?(r(t,n),0):(t.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))||e.close()}})}),(o=e).ui.registry.addToggleButton("anchor",{icon:"bookmark",tooltip:"Anchor",onAction:function(){return o.execCommand("mceAnchor")},onSetup:function(e){return o.selection.selectorChangedWithUnbind("a:not([href])",e.setActive).unbind}}),o.ui.registry.addMenuItem("anchor",{icon:"bookmark",text:"Anchor...",onAction:function(){return o.execCommand("mceAnchor")}}),e.on("PreInit",function(){e.formatter.register("namedAnchor",{inline:"a",selector:l,remove:"all",split:!0,deep:!0,attributes:{id:"%value"},onmatch:c})})})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function k(e){return/^[(\[{ \u00a0]$/.test(e)}function w(e){return 3===e.nodeType}function i(e){return 1===e.nodeType}function o(e,t){var n;return t<0&&(t=0),!w(e)||(n=e.data.length)<t&&(t=n),t}function y(e,t,n){!i(t)||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)}function v(e,t,n){!i(t)||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)}function r(e,t){var n,i,o,r,a,f=e.getParam("autolink_pattern",A),s=e.getParam("default_link_target",!1);if(null===e.dom.getParent(e.selection.getNode(),"a[href]")){var d=e.selection.getRng().cloneRange();if(d.startOffset<5){if(!(r=d.endContainer.previousSibling)){if(!d.endContainer.firstChild||!d.endContainer.firstChild.nextSibling)return;r=d.endContainer.firstChild.nextSibling}if(y(d,r,a=r.length),v(d,r,a),d.endOffset<5)return;n=d.endOffset,i=r}else{if(!w(i=d.endContainer)&&i.firstChild){for(;!w(i)&&i.firstChild;)i=i.firstChild;w(i)&&(y(d,i,0),v(d,i,i.nodeValue.length))}n=1===d.endOffset?2:d.endOffset-1-t}for(var l=n;y(d,i,2<=n?n-2:0),v(d,i,1<=n?n-1:0),--n,!k(d.toString())&&0<=n-2;);k(d.toString())?(y(d,i,n),v(d,i,l),n+=1):(0===d.startOffset?y(d,i,0):y(d,i,n),v(d,i,l)),u=d.toString(),/[?!,.;:]/.test(u.charAt(u.length-1))&&v(d,i,l-1);var u,c,g,h,C=(u=d.toString().trim()).match(f),m=e.getParam("link_default_protocol","http","string");C&&((g=c=C[0]).length>=(h="www.").length&&g.substr(0,0+h.length)===h?c=m+"://"+c:-1===c.indexOf("@")||/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(c)||(c="mailto:"+c),o=e.selection.getBookmark(),e.selection.setRng(d),e.execCommand("createlink",!1,c),!1!==s&&e.dom.setAttrib(e.selection.getNode(),"target",s),e.selection.moveToBookmark(o),e.nodeChanged())}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),A=new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i");e.add("autolink",function(e){var t,n;(t=e).on("keydown",function(e){if(13===e.keyCode)return r(t,-1)}),a.browser.isIE()?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){if(41===e.keyCode||93===e.keyCode||125===e.keyCode)return r(t,-1)}),t.on("keyup",function(e){if(32===e.keyCode)return r(t,0)}))})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function y(e){return e.getParam("min_height",e.getElement().offsetHeight,"number")}function p(e,t){var n=e.getBody();n&&(n.style.overflowY=t?"":"hidden",t||(n.scrollTop=0))}function v(e,t,n,i){var o=parseInt(e.getStyle(t,n,i),10);return isNaN(o)?0:o}var l=Object.hasOwnProperty,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),b=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),u=function(e,t,n,i,o){r.setEditorTimeout(e,function(){C(e,t),n--?u(e,t,n,i,o):o&&o()},i)},C=function(e,t,n){var i,o,r,s,a,l,u,g,c,m,f,d=e.dom,h=e.getDoc();h&&(e.plugins.fullscreen&&e.plugins.fullscreen.isFullscreen()?p(e,!0):(i=h.documentElement,o=e.getParam("autoresize_bottom_margin",50,"number"),r=y(e),s=v(d,i,"margin-top",!0),a=v(d,i,"margin-bottom",!0),(l=(l=i.offsetHeight+s+a+o)<0?0:l)+(u=e.getContainer().offsetHeight-e.getContentAreaContainer().offsetHeight)>y(e)&&(r=l+u),(g=e.getParam("max_height",0,"number"))&&g<r?(r=g,p(e,!0)):p(e,!1),r!==t.get()&&(c=r-t.get(),d.setStyle(e.getContainer(),"height",r+"px"),t.set(r),e.fire("ResizeEditor"),b.browser.isSafari()&&b.mac&&(m=e.getWin()).scrollTo(m.pageXOffset,m.pageYOffset),!e.hasFocus()||"setcontent"!==(null==(f=n)?void 0:f.type.toLowerCase())||!0!==f.selection&&!0!==f.paste||e.selection.scrollIntoView(),b.webkit&&c<0&&C(e,t,n))))};e.add("autoresize",function(e){var t,n,i,o,r,s,a=e.settings;l.call(a,"resize")||(e.settings.resize=!1),e.inline||(s=0,r=t={get:function(){return s},set:function(e){s=e}},(o=e).addCommand("mceAutoResize",function(){C(o,r)}),i=t,(n=e).on("init",function(){var e=n.getParam("autoresize_overflow_padding",1,"number"),t=n.dom;t.setStyles(n.getDoc().documentElement,{height:"auto"}),t.setStyles(n.getBody(),{paddingLeft:e,paddingRight:e,"min-height":0})}),n.on("NodeChange SetContent keyup FullscreenStateChanged ResizeContent",function(e){C(n,i,e)}),n.getParam("autoresize_on_init",!0,"boolean")&&n.on("init",function(){u(n,i,20,100,function(){u(n,i,5,1e3)})}))})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(t,e){var r=t||e,n=/^(\d+)([ms]?)$/.exec(""+r);return(n[2]?{s:1e3,m:6e4}[n[2]]:1)*parseInt(r,10)}function n(t){var e=document.location;return t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-").replace(/{path}/g,e.pathname).replace(/{query}/g,e.search).replace(/{hash}/g,e.hash).replace(/{id}/g,t.id)}function i(t,e){if(a(e))return t.dom.isEmpty(t.getBody());var r=d.trim(e);if(""===r)return!0;var n=(new DOMParser).parseFromString(r,"text/html");return t.dom.isEmpty(n)}function u(t){var e=parseInt(v.getItem(n(t)+"time"),10)||0;return!((new Date).getTime()-e>o(t.getParam("autosave_retention"),"20m")&&(g(t,!1),1))}function s(t){var e=n(t);!i(t)&&t.isDirty()&&(v.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),v.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))}function f(t){var e=n(t);u(t)&&(t.setContent(v.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))}function c(t){t.undoManager.transact(function(){f(t),g(t)}),t.focus()}function m(r){return function(t){function e(){return t.setDisabled(!u(r))}return t.setDisabled(!u(r)),r.on("StoreDraft RestoreDraft RemoveDraft",e),function(){return r.off("StoreDraft RestoreDraft RemoveDraft",e)}}}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return void 0===t},l=tinymce.util.Tools.resolve("tinymce.util.Delay"),v=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),d=tinymce.util.Tools.resolve("tinymce.util.Tools"),g=function(t,e){var r=n(t);v.removeItem(r+"draft"),v.removeItem(r+"time"),!1!==e&&t.fire("RemoveDraft")},y=tinymce.util.Tools.resolve("tinymce.EditorManager");t.add("autosave",function(t){var e,r,n,a;return t.editorManager.on("BeforeUnload",function(t){var e;d.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e&&(t.preventDefault(),t.returnValue=e)}),n=e=t,a=o(n.getParam("autosave_interval"),"30s"),l.setEditorInterval(n,function(){s(n)},a),e.ui.registry.addButton("restoredraft",{tooltip:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),e.ui.registry.addMenuItem("restoredraft",{text:"Restore last draft",icon:"restore-draft",onAction:function(){c(e)},onSetup:m(e)}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&f(t)}),r=t,{hasDraft:function(){return u(r)},storeDraft:function(){return s(r)},restoreDraft:function(){return f(r)},removeDraft:function(t){return g(r,t)},isEmpty:function(t){return i(r,t)}}})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function i(t){function o(o,e){t=t.replace(o,e)}return t=n.trim(t),o(/\n/gi,"<br />"),o(/\[b\]/gi,"<strong>"),o(/\[\/b\]/gi,"</strong>"),o(/\[i\]/gi,"<em>"),o(/\[\/i\]/gi,"</em>"),o(/\[u\]/gi,"<u>"),o(/\[\/u\]/gi,"</u>"),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),o(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),o(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),o(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span> '),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span> '),t}var o=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.util.Tools");o.add("bbcode",function(o){o.on("BeforeSetContent",function(o){o.content=i(o.content)}),o.on("PostProcess",function(o){function e(o,e){t=t.replace(o,e)}var t;o.set&&(o.content=i(o.content)),o.get&&(o.content=(t=o.content,t=n.trim(t),e(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),e(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),e(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),e(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),e(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),e(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),e(/<font>(.*?)<\/font>/gi,"$1"),e(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),e(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),e(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),e(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),e(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),e(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),e(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),e(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),e(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),e(/<\/(strong|b)>/gi,"[/b]"),e(/<(strong|b)>/gi,"[b]"),e(/<\/(em|i)>/gi,"[/i]"),e(/<(em|i)>/gi,"[i]"),e(/<\/u>/gi,"[/u]"),e(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),e(/<u>/gi,"[u]"),e(/<blockquote[^>]*>/gi,"[quote]"),e(/<\/blockquote>/gi,"[/quote]"),e(/<br \/>/gi,"\n"),e(/<br\/>/gi,"\n"),e(/<br>/gi,"\n"),e(/<p>/gi,""),e(/<\/p>/gi,"\n"),e(/ |\u00a0/gi," "),e(/"/gi,'"'),e(/</gi,"<"),e(/>/gi,">"),e(/&/gi,"&"),t))})})}();
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",function(e){var t,o;function n(){return o.execCommand("mceCodeEditor")}return(t=e).addCommand("mceCodeEditor",function(){var n,e;e=(n=t).getContent({source_view:!0}),n.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:e},onSubmit:function(e){var t=n,o=e.getData().code;t.focus(),t.undoManager.transact(function(){t.setContent(o)}),t.selection.setCursorLocation(),t.nodeChanged(),e.close()}})}),(o=e).ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:n}),o.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:n}),{}})}();
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("colorpicker",function(){})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("contextmenu",function(){})}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("hr",function(n){var o,t;function e(){return t.execCommand("InsertHorizontalRule")}(o=n).addCommand("InsertHorizontalRule",function(){o.execCommand("mceInsertContent",!1,"<hr />")}),(t=n).ui.registry.addButton("hr",{icon:"horizontal-rule",tooltip:"Horizontal line",onAction:e}),t.ui.registry.addMenuItem("hr",{icon:"horizontal-rule",text:"Horizontal line",onAction:e})})}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function t(n){return function(t){return r=typeof(e=t),(null===e?"null":"object"==r&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"==r&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":r)===n;var e,r}}function y(t){return t.getParam("importcss_selector_converter")}function u(e){return l(e)?function(t){return-1!==t.indexOf(e)}:e instanceof RegExp?function(t){return e.test(t)}:e}function h(t,e){var r,n=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(e);if(n){var o=n[1],i=n[2].substr(1).split(".").join(" "),c=x.makeMap("a,img");return n[1]?(r={title:e},t.schema.getTextBlockElements()[o]?r.block=o:t.schema.getBlockElements()[o]||c[o.toLowerCase()]?r.selector=o:r.inline=o):n[2]&&(r={inline:"span",title:e.substr(1),classes:i}),!1!==t.getParam("importcss_merge_classes")?r.classes=i:r.attributes={class:i},r}}function d(t,e){return null===e||!1!==t.getParam("importcss_exclusive")}function r(v){v.on("init",function(){function o(t,e){if(f=t,p=g,!(d(v,m=e)?f in p:f in m.selectors)){a=t,l=g,d(v,u=e)?l[a]=!0:u.selectors[a]=!0;var r=(i=(o=v).plugins.importcss,c=t,((s=e)&&s.selector_converter?s.selector_converter:y(o)?y(o):function(){return h(o,c)}).call(i,c,s));if(r){var n=r.name||_.DOM.uniqueId();return v.formatter.register(n,r),{title:r.title,format:n}}}var o,i,c,s,a,u,l,f,m,p;return null}var e,r,n,t,i=(e=[],r=[],n={},{addItemToGroup:function(t,e){n[t]?n[t].push(e):(r.push(t),n[t]=[e])},addItem:function(t){e.push(t)},toFormats:function(){return function(t){for(var e=[],r=0,n=t.length;r<n;++r){if(!f(t[r]))throw new Error("Arr.flatten item "+r+" was not an array, input: "+t);P.apply(e,t[r])}return e}(function(t,e){for(var r=t.length,n=new Array(r),o=0;o<r;o++){var i=t[o];n[o]=e(i,o)}return n}(r,function(t){var e=n[t];return 0===e.length?[]:[{title:t,items:e}]})).concat(e)}}),g={},c=u(v.getParam("importcss_selector_filter")),s=(t=v.getParam("importcss_groups"),x.map(t,function(t){return x.extend({},t,{original:t,selectors:{},filter:u(t.filter)})}));x.each(function(c,t,s){var a=[],r={},u=function(t,e){var r,n,o=n=t.href,i=p.cacheSuffix;if((n=o=l(o)?o.replace("?"+i,"").replace("&"+i,""):o)&&s(n,e)&&!function(t,e){var r,n=!1!==(r=t.getParam("skin"))&&(r||"oxide");if(n){var o=t.getParam("skin_url"),i=o?t.documentBaseURI.toAbsolute(o):m.baseURL+"/skins/ui/"+n,c=m.baseURL+"/skins/content/";return e===i+"/content"+(t.inline?".inline":"")+".min.css"||-1!==e.indexOf(c)}}(c,n)){x.each(t.imports,function(t){u(t,!0)});try{r=t.cssRules||t.rules}catch(t){}x.each(r,function(t){t.styleSheet?u(t.styleSheet,!0):t.selectorText&&x.each(t.selectorText.split(","),function(t){a.push(x.trim(t))})})}};x.each(c.contentCSS,function(t){r[t]=!0}),s=s||function(t,e){return e||r[t]};try{x.each(t.styleSheets,function(t){u(t)})}catch(t){}return a}(v,v.getDoc(),u(v.getParam("importcss_file_filter"))),function(r){var t,e,n;T.test(r)||c&&!c(r)||(n=r,0<(t=x.grep(s,function(t){return!t.filter||t.filter(n)})).length?x.each(t,function(t){var e=o(r,t);e&&i.addItemToGroup(t.title,e)}):(e=o(r,null))&&i.addItem(e))});var a=i.toFormats();v.fire("addStyleModifications",{items:a,replace:!v.getParam("importcss_append")})})}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=t("string"),f=t("array"),_=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=tinymce.util.Tools.resolve("tinymce.EditorManager"),p=tinymce.util.Tools.resolve("tinymce.Env"),x=tinymce.util.Tools.resolve("tinymce.util.Tools"),P=Array.prototype.push,T=/^\.(?:ephox|tiny-pageembed|mce)(?:[.-]+\w+)+$/;e.add("importcss",function(t){return r(t),e=t,{convertSelectorToFormat:function(t){return h(e,t)}};var e})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function l(e){return e.getParam("insertdatetime_timeformat",e.translate("%H:%M:%S"))}function s(e){return e.getParam("insertdatetime_formats",["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"])}function r(e,t){if((e=""+e).length<t)for(var n=0;n<t-e.length;n++)e="0"+e;return e}function d(e,t,n){return void 0===n&&(n=new Date),(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+n.getFullYear())).replace("%y",""+n.getYear())).replace("%m",r(n.getMonth()+1,2))).replace("%d",r(n.getDate(),2))).replace("%H",""+r(n.getHours(),2))).replace("%M",""+r(n.getMinutes(),2))).replace("%S",""+r(n.getSeconds(),2))).replace("%I",""+((n.getHours()+11)%12+1))).replace("%p",n.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(u[n.getMonth()]))).replace("%b",""+e.translate(o[n.getMonth()]))).replace("%A",""+e.translate(i[n.getDay()]))).replace("%a",""+e.translate(a[n.getDay()]))).replace("%%","%")}function p(e,t){var n,r,a,i,o,u;e.getParam("insertdatetime_element",!1)?(n=d(e,t),r=/%[HMSIp]/.test(t)?d(e,"%Y-%m-%dT%H:%M"):d(e,"%Y-%m-%d"),(a=e.dom.getParent(e.selection.getStart(),"time"))?(o=a,u=(i=e).dom.create("time",{datetime:r},n),o.parentNode.insertBefore(u,o),i.dom.remove(o),i.selection.select(u,!0),i.selection.collapse(!1)):e.insertContent('<time datetime="'+r+'">'+n+"</time>")):e.insertContent(d(e,t))}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),i="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),o="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),u="January February March April May June July August September October November December".split(" "),g=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("insertdatetime",function(e){var n,r,t,a,i,o,u,c;function m(e){return r.execCommand("mceInsertDate",!1,e)}(n=e).addCommand("mceInsertDate",function(e,t){p(n,null!=t?t:n.getParam("insertdatetime_dateformat",n.translate("%Y-%m-%d")))}),n.addCommand("mceInsertTime",function(e,t){p(n,null!=t?t:l(n))}),u=s(r=e),t=0<(o=s(i=r)).length?o[0]:l(i),a=t,c={get:function(){return a},set:function(e){a=e}},r.ui.registry.addSplitButton("insertdatetime",{icon:"insert-time",tooltip:"Insert date/time",select:function(e){return e===c.get()},fetch:function(e){e(g.map(u,function(e){return{type:"choiceitem",text:d(r,e),value:e}}))},onAction:function(e){m(c.get())},onItemAction:function(e,t){c.set(t),m(t)}}),r.ui.registry.addNestedMenuItem("insertdatetime",{icon:"insert-time",text:"Date/time",getSubmenuItems:function(){return g.map(u,function(e){return{type:"menuitem",text:d(r,e),onAction:(t=e,function(){c.set(t),m(t)})};var t})}})})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("legacyoutput",function(e){var s,t;(t=s=e).settings.inline_styles=!1,t.getParam("fontsize_formats")||(t.settings.fontsize_formats="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7"),t.getParam("font_formats")||(t.settings.font_formats="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),s.on("PreInit",function(){var e=s,t="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table",i=l.explode(e.getParam("font_size_style_values","xx-small,x-small,small,medium,large,x-large,xx-large")),a=e.schema;e.formatter.register({alignleft:{selector:t,attributes:{align:"left"}},aligncenter:{selector:t,attributes:{align:"center"}},alignright:{selector:t,attributes:{align:"right"}},alignjustify:{selector:t,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all",preserve_attributes:["class","style"]},{inline:"strong",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all",preserve_attributes:["class","style"]},{inline:"em",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all",preserve_attributes:["class","style"]},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",toggle:!1,attributes:{face:"%value"}},fontsize:{inline:"font",toggle:!1,attributes:{size:function(e){return String(l.inArray(i,e.value)+1)}}},forecolor:{inline:"font",attributes:{color:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0}}),l.each("b,i,u,strike".split(","),function(e){a.addValidElements(e+"[*]")}),a.getElementRule("font")||a.addValidElements("font[face|size|color|style]"),l.each(t.split(","),function(e){var t=a.getElementRule(e);t&&(t.attributes.align||(t.attributes.align={},t.attributesOrder.push("align")))})})})}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(n,e){for(var a="",o=0;o<e;o++)a+=n;return a}function s(n,e){var a=n.getParam("nonbreaking_wrap",!0,"boolean")||n.plugins.visualchars?'<span class="'+(n.plugins.visualchars&&n.plugins.visualchars.isEnabled()?"mce-nbsp-wrap mce-nbsp":"mce-nbsp-wrap")+'" contenteditable="false">'+o(" ",e)+"</span>":o(" ",e);n.undoManager.transact(function(){return n.insertContent(a)})}var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.VK");n.add("nonbreaking",function(n){var e,a,o,t,i;function r(){return a.execCommand("mceNonBreaking")}(e=n).addCommand("mceNonBreaking",function(){s(e,1)}),(a=n).ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:r}),a.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:r}),0<(i="boolean"==typeof(t=(o=n).getParam("nonbreaking_force_tab",0))?!0===t?3:0:t)&&o.on("keydown",function(n){n.keyCode!==c.TAB||n.isDefaultPrevented()||n.shiftKey||(n.preventDefault(),n.stopImmediatePropagation(),s(o,i))})})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function l(t){return t.getParam("noneditable_noneditable_class","mceNonEditable")}function u(e){return function(t){return-1!==(" "+t.attr("class")+" ").indexOf(e)}}function e(e){var t,r="contenteditable",n=" "+f.trim(e.getParam("noneditable_editable_class","mceEditable"))+" ",a=" "+f.trim(l(e))+" ",i=u(n),o=u(a),c=(t=e.getParam("noneditable_regexp",[]))&&t.constructor===RegExp?[t]:t;e.on("PreInit",function(){0<c.length&&e.on("BeforeSetContent",function(t){!function(t,e,n){var r=e.length,a=n.content;if("raw"!==n.format){for(;r--;)a=a.replace(e[r],function(i,o,c){return function(t){var e=arguments,n=e[e.length-2],r=0<n?o.charAt(n-1):"";if('"'===r)return t;if(">"===r){var a=o.lastIndexOf("<",n);if(-1!==a&&-1!==o.substring(a,n).indexOf('contenteditable="false"'))return t}return'<span class="'+c+'" data-mce-content="'+i.dom.encode(e[0])+'">'+i.dom.encode("string"==typeof e[1]?e[1]:e[0])+"</span>"}}(t,a,l(t)));n.content=a}}(e,c,t)}),e.parser.addAttributeFilter("class",function(t){for(var e,n=t.length;n--;)e=t[n],i(e)?e.attr(r,"true"):o(e)&&e.attr(r,"false")}),e.serializer.addAttributeFilter(r,function(t){for(var e,n=t.length;n--;)e=t[n],(i(e)||o(e))&&(0<c.length&&e.attr("data-mce-content")?(e.name="#text",e.type=3,e.raw=!0,e.value=e.attr("data-mce-content")):e.attr(r,null))})})}var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.util.Tools");t.add("noneditable",function(t){e(t)})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function u(e){return e.getParam("pagebreak_split_block",!1)}function l(e){var a='<img src="'+n.transparentSrc+'" class="'+m+'" data-mce-resize="false" data-mce-placeholder />';return e?"<p>"+a+"</p>":a}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.Env"),m="mce-pagebreak";e.add("pagebreak",function(e){var a,n,o,i,t,r;function c(){return n.execCommand("mcePageBreak")}function g(){return u(o)}(a=e).addCommand("mcePageBreak",function(){a.insertContent(l(u(a)))}),(n=e).ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:c}),n.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:c}),i=(o=e).getParam("pagebreak_separator","\x3c!-- pagebreak --\x3e"),t=new RegExp(i.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(e){return"\\"+e}),"gi"),o.on("BeforeSetContent",function(e){e.content=e.content.replace(t,l(g()))}),o.on("PreInit",function(){o.serializer.addNodeFilter("img",function(e){for(var a,n,t,r=e.length;r--;)(t=(n=e[r]).attr("class"))&&-1!==t.indexOf(m)&&(a=n.parent,o.schema.getBlockElements()[a.name]&&g()?(a.type=3,a.value=i,a.raw=!0,n.remove()):(n.type=3,n.value=i,n.raw=!0))})}),(r=e).on("ResolveName",function(e){"IMG"===e.target.nodeName&&r.dom.hasClass(e.target,m)&&(e.name="pagebreak")})})}();
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.Env"),w=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("preview",function(e){var n,t;function i(){return t.execCommand("mcePreview")}(n=e).addCommand("mcePreview",function(){var e,t;t=function(t){var n="",i=t.dom.encode,e=t.getParam("content_style","","string");n+='<base href="'+i(t.documentBaseURI.getURI())+'">';var o=t.getParam("content_css_cors",!1,"boolean")?' crossorigin="anonymous"':"";w.each(t.contentCSS,function(e){n+='<link type="text/css" rel="stylesheet" href="'+i(t.documentBaseURI.toAbsolute(e))+'"'+o+">"}),e&&(n+='<style type="text/css">'+e+"</style>");var a,r,s,c,d,l,m,y=-1===(c=(a=t).getParam("body_id","tinymce","string")).indexOf("=")?c:(s=(r=a).getParam("body_id","","hash"))[r.id]||s,u=-1===(m=(d=t).getParam("body_class","","string")).indexOf("=")?m:(l=d).getParam("body_class","","hash")[l.id]||"",v='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !('+(f.mac?"e.metaKey":"e.ctrlKey && !e.altKey")+")) {e.preventDefault();}}}, false);<\/script> ",g=t.getBody().dir,p=g?' dir="'+i(g)+'"':"";return"<!DOCTYPE html><html><head>"+n+'</head><body id="'+i(y)+'" class="mce-content-body '+i(u)+'"'+p+">"+t.getContent()+v+"</body></html>"}(e=n),e.windowManager.open({title:"Preview",size:"large",body:{type:"panel",items:[{name:"preview",type:"iframe",sandboxed:!0}]},buttons:[{type:"cancel",name:"close",text:"Close",primary:!0}],initialData:{preview:t}}).focus("close")}),(t=e).ui.registry.addButton("preview",{icon:"preview",tooltip:"Preview",onAction:i}),t.ui.registry.addMenuItem("preview",{icon:"preview",text:"Preview",onAction:i})})}();
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.Env");n.add("print",function(n){var t,i;function e(){return i.execCommand("mcePrint")}(t=n).addCommand("mcePrint",function(){r.browser.isIE()?t.getDoc().execCommand("print",!1,null):t.getWin().print()}),(i=n).ui.registry.addButton("print",{icon:"print",tooltip:"Print",onAction:e}),i.ui.registry.addMenuItem("print",{text:"Print...",icon:"print",onAction:e}),n.addShortcut("Meta+P","","mcePrint")})}();
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*
|
||||
* Version: 5.10.9 (2023-11-15)
|
||||
*/
|
||||
!function(){"use strict";function o(e){return e.getParam("save_enablewhendirty",!0)}function a(e,n){e.notificationManager.open({text:n,type:"error"})}function t(t){t.addCommand("mceSave",function(){!function(e){var n=c.DOM.getParent(e.id,"form");if(!o(e)||e.isDirty()){if(e.save(),e.getParam("save_onsavecallback"))return e.execCallback("save_onsavecallback",e),e.nodeChanged();n?(e.setDirty(!1),n.onsubmit&&!n.onsubmit()||("function"==typeof n.submit?n.submit():a(e,"Error: Form submit field collision.")),e.nodeChanged()):a(e,"Error: No form element found.")}}(t)}),t.addCommand("mceCancel",function(){var e=t,n=r.trim(e.startContent);e.getParam("save_oncancelcallback")?e.execCallback("save_oncancelcallback",e):e.resetContent(n)})}function i(t){return function(e){function n(){e.setDisabled(o(t)&&!t.isDirty())}return n(),t.on("NodeChange dirty",n),function(){return t.off("NodeChange dirty",n)}}}var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),r=tinymce.util.Tools.resolve("tinymce.util.Tools");e.add("save",function(e){var n;(n=e).ui.registry.addButton("save",{icon:"save",tooltip:"Save",disabled:!0,onAction:function(){return n.execCommand("mceSave")},onSetup:i(n)}),n.ui.registry.addButton("cancel",{icon:"cancel",tooltip:"Cancel",disabled:!0,onAction:function(){return n.execCommand("mceCancel")},onSetup:i(n)}),n.addShortcut("Meta+S","","mceSave"),t(e)})}();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user