Merge branch 'iipbs/dm/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/dm/dev
This commit is contained in:
+7
-5
@@ -4,10 +4,7 @@
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# build
|
||||
.next/
|
||||
dist/
|
||||
.idea/
|
||||
|
||||
# build
|
||||
.next
|
||||
@@ -22,9 +19,14 @@ apps/fo/.next/**/*
|
||||
#.env*
|
||||
.qodo
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
pnpm-lock.yaml
|
||||
settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
logs/
|
||||
|
||||
@@ -3,4 +3,8 @@ BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Branch: $BRANCH"
|
||||
echo "Changed packages (dry-run):"
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]" --dry-run
|
||||
echo "Starting build..."
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]"
|
||||
|
||||
@@ -3,3 +3,6 @@ NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
NEXT_PUBLIC_SERVER_API_URL=
|
||||
|
||||
# 401 시 브라우저 리다이렉트 대상
|
||||
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -5,16 +5,15 @@ import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
// 로그인 페이지에서는 인증 체크를 건너뜀
|
||||
if (window.location.pathname === '/auth/login') {
|
||||
if (isAtLoginRedirectUrl("/auth/login")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,13 +34,8 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl("/auth/login")) {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,11 @@ function LoginPage() {
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
유역하수도 통합플랫폼
|
||||
유역하수도통합플랫폼
|
||||
<br />
|
||||
<span>통합관리시스템</span>
|
||||
<span>하수도 포탈</span>
|
||||
</h1>
|
||||
<p>유역하수도 통합플랫폼에 오신 것을 환영합니다.</p>
|
||||
<p>유역하수도통합플랫폼 하수도 포탈에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
proxyTimeout: 600000,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
},
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -4,13 +4,17 @@ import { useEffect } from "react";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { getAuthLoginRedirectUrl, isAtLoginRedirectUrl } from "@common/network";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(getAuthLoginRedirectUrl())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -28,8 +32,6 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
+2
-1
@@ -38,4 +38,5 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.example
|
||||
/start_cs.log
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import {
|
||||
ENDPOINT,
|
||||
AUTH_USER_ID,
|
||||
AUTH_LOGIN_REDIRECT_URL,
|
||||
} from "@/shared/config";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -30,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -39,10 +39,10 @@ function LoginlogPage() {
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("ALL");
|
||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||
userId: "",
|
||||
errOcrnYn: "N",
|
||||
errOcrnYn: "",
|
||||
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,desc",
|
||||
@@ -87,7 +87,7 @@ function LoginlogPage() {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
userId,
|
||||
errOcrnYn,
|
||||
errOcrnYn: errOcrnYn === "ALL" ? "" : errOcrnYn,
|
||||
searchDateFrom: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
@@ -102,12 +102,12 @@ function LoginlogPage() {
|
||||
|
||||
const handleReset = () => {
|
||||
setUserId("");
|
||||
setErrOcrnYn("N");
|
||||
setErrOcrnYn("ALL");
|
||||
setDateRange(defaultDateRange);
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
userId: "",
|
||||
errOcrnYn: "N",
|
||||
errOcrnYn: "",
|
||||
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
@@ -180,6 +180,7 @@ function LoginlogPage() {
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="N">정상</SelectItem>
|
||||
<SelectItem value="Y">오류</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -2,6 +2,8 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN =
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import {
|
||||
ENDPOINT,
|
||||
AUTH_USER_ID,
|
||||
AUTH_LOGIN_REDIRECT_URL,
|
||||
} from "@/shared/config";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -32,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -3,10 +3,14 @@ export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const NEXT_PUBLIC_DC_SERVICE_API_URL = process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`;
|
||||
export const NEXT_PUBLIC_DC_SERVICE_API_URL =
|
||||
process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
|
||||
// Flask API URL
|
||||
export const FLASK_API_URL = process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000";
|
||||
export const FLASK_API_URL =
|
||||
process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000";
|
||||
|
||||
//export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DC";
|
||||
export const SITE_ID = "DC";
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import SubPageHeader from "../../_components/SubPageHeader";
|
||||
//import { useDmStore } from "@/app/store";
|
||||
|
||||
type WaterDetail = {
|
||||
toc: string;
|
||||
@@ -114,12 +115,17 @@ type PairField = { label: string; value: string };
|
||||
export default function DemandStatusPage() {
|
||||
const rows = useMemo(() => MOCK, []);
|
||||
|
||||
// ✅ rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
// rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
const [selectedId, setSelectedId] = useState<number | null>(rows[0]?.id ?? null);
|
||||
|
||||
// ✅ 상세영역 스크롤 이동용
|
||||
// 상세영역 스크롤 이동용
|
||||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// update
|
||||
/* const updateSdmInfoData = useDmStore(
|
||||
(state) => state.updateSdmInfoData
|
||||
); */
|
||||
|
||||
// ✅ selected 계산 안정화
|
||||
const selected = useMemo(() => {
|
||||
if (!rows.length) return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import "@/app/commonOrigin.css";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { mapService } from "../service";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
|
||||
@@ -13,9 +13,10 @@ import { useRouter } from "next/navigation";
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import WaterQualityRefModal from "../modal/WaterQualityRefModal";
|
||||
import CandidateSiteModal, { type CandidateSite } from "@/components/CandidateSiteModal";
|
||||
import { useDmStore} from "@/app/store";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { SdmInfoFormData } from "@/app/store/sdmMng";
|
||||
import SimilarCaseModal, { type SimilarCaseDetail, type SimilarCaseRow } from "@/app/(landing)/reuse/apply/modal/SimilarCaseModal";
|
||||
import type { SubsInfo } from "@/app/store/map/type";
|
||||
|
||||
type PHRange = { min: string; max: string };
|
||||
|
||||
@@ -60,6 +61,7 @@ export default function DemandRegisterPage() {
|
||||
const [cand1, setCand1] = useState("");
|
||||
const [cand2, setCand2] = useState("");
|
||||
const [cand3, setCand3] = useState("");
|
||||
const [candidateList, setCandidateList] = useState<SubsInfo[]>([]);
|
||||
|
||||
const useTypesData = useDmStore((state) => state.Sdm.useTypesData);
|
||||
const getUseTypesData = useDmStore((state) => state.getUseTypesData);
|
||||
@@ -121,16 +123,38 @@ export default function DemandRegisterPage() {
|
||||
getUseTypesData();
|
||||
}, [getUseTypesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (!event.data) return;
|
||||
|
||||
if (event.data.type === "SET_SUBS_INFO") {
|
||||
const list = event.data.payload || [];
|
||||
|
||||
// 최대 3개
|
||||
setCand1(list[0]?.ruseUtztnFcltNm || "");
|
||||
setCand2(list[1]?.ruseUtztnFcltNm || "");
|
||||
setCand3(list[2]?.ruseUtztnFcltNm || "");
|
||||
setCandidateList(list);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// option
|
||||
// 'req':필수 / 'num' : 숫자 / 'num+[숫자] : 자릿수
|
||||
const validation = (val, text, option :string[]=[] ) => {
|
||||
if(option.includes('req')){
|
||||
if(val.trim() === "" || val ===undefined) {
|
||||
alert(`${text} 항목은 필수 입력값입니다.`);
|
||||
const validation = (val, text, option: string[] = []) => {
|
||||
if (option.includes('req')) {
|
||||
if (val.trim() === "" || val === undefined) {
|
||||
alert(`${text} 항목은 필수 입력값입니다.`);
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
if(val.trim()===""){
|
||||
} else {
|
||||
if (val.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -144,8 +168,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
if (!regex.test(val)) {
|
||||
alert(
|
||||
`${text}은(는) ${
|
||||
isDot ? "숫자 또는 소수" : "숫자"
|
||||
`${text}은(는) ${isDot ? "숫자 또는 소수" : "숫자"
|
||||
}만 입력 가능합니다.`
|
||||
);
|
||||
return false;
|
||||
@@ -230,29 +253,29 @@ export default function DemandRegisterPage() {
|
||||
});
|
||||
|
||||
// 데이터 저장
|
||||
const saveInfo = async() => {
|
||||
if(!validation(reqQty, '요구수량', ['req', 'numDot'])) return false;
|
||||
if(!validation(usePurpose, '사용용도', ['req'])) return false;
|
||||
if(!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false;
|
||||
/* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false;
|
||||
if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false;
|
||||
if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false;
|
||||
if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false;
|
||||
if(!validation(odor, '냄새', ['req'])) return false; */
|
||||
|
||||
const saveInfo = async () => {
|
||||
if (!validation(reqQty, '요구수량', ['req', 'numDot'])) return false;
|
||||
if (!validation(usePurpose, '사용용도', ['req'])) return false;
|
||||
if (!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false;
|
||||
/* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false;
|
||||
if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false;
|
||||
if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false;
|
||||
if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false;
|
||||
if(!validation(odor, '냄새', ['req'])) return false; */
|
||||
|
||||
// 필수는 아니지만 숫자로는 들어와야함.
|
||||
/* if(!validation(tn, '총질소', ['numDot'])) return false;
|
||||
if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false;
|
||||
if(!validation(tp, '총인', ['numDot'])) return false;
|
||||
if(!validation(ph.min, 'ph', ['numDot'])) return false;
|
||||
if(!validation(ph.max, 'ph', ['numDot'])) return false;
|
||||
if(!validation(chloride, '염화물', ['numDot'])) return false;
|
||||
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
|
||||
|
||||
/* if(!validation(tn, '총질소', ['numDot'])) return false;
|
||||
if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false;
|
||||
if(!validation(tp, '총인', ['numDot'])) return false;
|
||||
if(!validation(ph.min, 'ph', ['numDot'])) return false;
|
||||
if(!validation(ph.max, 'ph', ['numDot'])) return false;
|
||||
if(!validation(chloride, '염화물', ['numDot'])) return false;
|
||||
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
|
||||
|
||||
const userStr = sessionStorage.getItem("user");
|
||||
const user = userStr ? JSON.parse(userStr) : null;
|
||||
|
||||
if(user){
|
||||
if (user) {
|
||||
// 후보지 목록 (candsRef에 코드까지 보관됨)
|
||||
const splyCndtList = candsRef.current
|
||||
.filter((x) => x.name)
|
||||
@@ -261,25 +284,25 @@ export default function DemandRegisterPage() {
|
||||
ruseUtztnFcltCd: x.code,
|
||||
}));
|
||||
|
||||
const payload : SdmInfoFormData = {
|
||||
dmndTmng : new Date(reqDate),
|
||||
dmdInfoId : user.dmdInfoId,
|
||||
dmndQty : Number(reqQty),
|
||||
wntUntprc : Number(hopePrice),
|
||||
useUsgCd : usePurpose,
|
||||
const payload: SdmInfoFormData = {
|
||||
dmndTmng: new Date(reqDate),
|
||||
dmdInfoId: user.dmdInfoId,
|
||||
dmndQty: Number(reqQty),
|
||||
wntUntprc: Number(hopePrice),
|
||||
useUsgCd: usePurpose,
|
||||
// 수질
|
||||
grssClfg : Number(coliform),
|
||||
tn : Number(tn)||null,
|
||||
combResdch: Number(residualCl)||null,
|
||||
tph : Number(tp)||null,
|
||||
trbt : Number(turbidity),
|
||||
ph : ph.min||''+((ph.min||ph.max)?'~':'')+ph.max||'',
|
||||
blgcOxygDmndQnt : Number(bod),
|
||||
clIonQnt : Number(chloride)||null,
|
||||
smll : odor,
|
||||
cdtnv : Number(ec)||null,
|
||||
crmty : Number(color)||null,
|
||||
rmrk : remark,
|
||||
grssClfg: Number(coliform),
|
||||
tn: Number(tn) || null,
|
||||
combResdch: Number(residualCl) || null,
|
||||
tph: Number(tp) || null,
|
||||
trbt: Number(turbidity),
|
||||
ph: ph.min || '' + ((ph.min || ph.max) ? '~' : '') + ph.max || '',
|
||||
blgcOxygDmndQnt: Number(bod),
|
||||
clIonQnt: Number(chloride) || null,
|
||||
smll: odor,
|
||||
cdtnv: Number(ec) || null,
|
||||
crmty: Number(color) || null,
|
||||
rmrk: remark,
|
||||
splyCndtList, // 후보지 목록 추가
|
||||
}
|
||||
const success = await insertSdmInfoData(payload);
|
||||
@@ -344,13 +367,13 @@ export default function DemandRegisterPage() {
|
||||
const fncSetOptionValue = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
initQualityForm();
|
||||
|
||||
|
||||
switch (value) {
|
||||
// 청소화장실용도
|
||||
case '01':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setColor('20');
|
||||
break;
|
||||
@@ -358,14 +381,14 @@ export default function DemandRegisterPage() {
|
||||
case '02':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 조경용도
|
||||
case '03':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('250');
|
||||
break;
|
||||
@@ -373,318 +396,318 @@ export default function DemandRegisterPage() {
|
||||
case '05':
|
||||
break;
|
||||
// 농업용도
|
||||
case '06' :
|
||||
case '06':
|
||||
setTurbidity('5');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('8');
|
||||
setEc('2000');
|
||||
setFarmText("이하 (** 간접:2000,직접:700 )");
|
||||
break;
|
||||
// 하천 등 유지보수
|
||||
case '07' :
|
||||
case '07':
|
||||
setColiform('1000');
|
||||
setTn('10');
|
||||
setTp('0.5');
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 공업
|
||||
case '08':
|
||||
setPh({min:'5.8', max:'8.5'});
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
break;
|
||||
default:
|
||||
console.log('잘못된 선택입니다.');
|
||||
}
|
||||
|
||||
|
||||
setUsePurpose(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
|
||||
<Divider />
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<Divider />
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="사용용도" required>
|
||||
<select
|
||||
value={usePurpose}
|
||||
onChange={(e) => fncSetOptionValue(e)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<Divider />
|
||||
<FormRow label="사용용도" required>
|
||||
<select
|
||||
value={usePurpose}
|
||||
onChange={(e) => fncSetOptionValue(e)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
{/* 요구수질 섹션 타이틀 바 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-6 py-6 text-center">
|
||||
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenRef(true)}
|
||||
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
|
||||
>
|
||||
수질기준 참고
|
||||
</button>
|
||||
{/* 수질기준참고 모달 */}
|
||||
<WaterQualityRefModal
|
||||
open={openRef}
|
||||
onClose={() => setOpenRef(false)}
|
||||
imageSrc="/images/landing/water_quality_ref_1.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={coliform}
|
||||
onChange={setColiform}
|
||||
disabled={noDetect}
|
||||
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noDetect}
|
||||
onChange={(e) => {
|
||||
setNoDetect(e.target.checked);
|
||||
if (e.target.checked) setColiform("");
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
rightLabel="총질소(T-N)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 2행 */}
|
||||
<QualityRow
|
||||
leftLabel="결합잔류염소(mg/L)"
|
||||
/* leftRequired */
|
||||
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
|
||||
rightLabel="총인(T-P)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 3행 */}
|
||||
<QualityRow
|
||||
leftLabel="탁도(NTU)"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
|
||||
}
|
||||
rightLabel="수소이온농도(pH)"
|
||||
rightControl={
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={ph.min}
|
||||
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
<span className="text-neutral-500">~</span>
|
||||
<TextInput
|
||||
value={ph.max}
|
||||
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 4행 */}
|
||||
<QualityRow
|
||||
leftLabel={
|
||||
<>
|
||||
생물화학적산소요구량
|
||||
<br />
|
||||
(BOD)(mg/L)
|
||||
</>
|
||||
}
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
|
||||
rightLabel="염화물(mgCL/L)"
|
||||
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 5행 */}
|
||||
<QualityRow
|
||||
leftLabel="냄새"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<select
|
||||
value={odor}
|
||||
onChange={(e) => setOdor(e.target.value)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
}
|
||||
rightLabel="전기전도도(µs/cm)"
|
||||
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix={farmText} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 6행 */}
|
||||
<QualityRow
|
||||
leftLabel="색도(도)"
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
|
||||
rightLabel="비고"
|
||||
rightControl={
|
||||
<textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 후보지 선택 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell>
|
||||
공급처 후보지 선택
|
||||
<br />
|
||||
(필수아님)
|
||||
</LabelCell>
|
||||
|
||||
<div className="col-span-12 md:col-span-10 p-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
|
||||
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
|
||||
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
|
||||
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
|
||||
|
||||
<div className="flex items-center gap-3 xl:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={(e) => {
|
||||
// TODO: 유사사례 연결
|
||||
searchSimilarCase(e);
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
onClick={() => {
|
||||
// TODO: 저장 처리
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
{/* 요구수질 섹션 타이틀 바 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-6 py-6 text-center">
|
||||
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenRef(true)}
|
||||
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
|
||||
>
|
||||
수질기준 참고
|
||||
</button>
|
||||
{/* 수질기준참고 모달 */}
|
||||
<WaterQualityRefModal
|
||||
open={openRef}
|
||||
onClose={() => setOpenRef(false)}
|
||||
imageSrc="/images/landing/water_quality_ref_1.png"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows} // 실제 데이터
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
const code = getCriterionCode(criterion);
|
||||
if (!code) return;
|
||||
// 값은 상황에 맞게 (예시: usePurpose)
|
||||
await getSimilarCaseList(code, code=='S'?reqQty:code=='U'?usePurpose:'');
|
||||
}}
|
||||
/>
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={coliform}
|
||||
onChange={setColiform}
|
||||
disabled={noDetect}
|
||||
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noDetect}
|
||||
onChange={(e) => {
|
||||
setNoDetect(e.target.checked);
|
||||
if (e.target.checked) setColiform("");
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
rightLabel="총질소(T-N)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
// ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전)
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
{/* 2행 */}
|
||||
<QualityRow
|
||||
leftLabel="결합잔류염소(mg/L)"
|
||||
/* leftRequired */
|
||||
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
|
||||
rightLabel="총인(T-P)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
setOpenCand(false);
|
||||
}}
|
||||
/>
|
||||
{/* 3행 */}
|
||||
<QualityRow
|
||||
leftLabel="탁도(NTU)"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
|
||||
}
|
||||
rightLabel="수소이온농도(pH)"
|
||||
rightControl={
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={ph.min}
|
||||
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
<span className="text-neutral-500">~</span>
|
||||
<TextInput
|
||||
value={ph.max}
|
||||
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 4행 */}
|
||||
<QualityRow
|
||||
leftLabel={
|
||||
<>
|
||||
생물화학적산소요구량
|
||||
<br />
|
||||
(BOD)(mg/L)
|
||||
</>
|
||||
}
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
|
||||
rightLabel="염화물(mgCL/L)"
|
||||
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 5행 */}
|
||||
<QualityRow
|
||||
leftLabel="냄새"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<select
|
||||
value={odor}
|
||||
onChange={(e) => setOdor(e.target.value)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
}
|
||||
rightLabel="전기전도도(µs/cm)"
|
||||
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix={farmText} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 6행 */}
|
||||
<QualityRow
|
||||
leftLabel="색도(도)"
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
|
||||
rightLabel="비고"
|
||||
rightControl={
|
||||
<textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 후보지 선택 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell>
|
||||
공급처 후보지 선택
|
||||
<br />
|
||||
(필수아님)
|
||||
</LabelCell>
|
||||
|
||||
<div className="col-span-12 md:col-span-10 p-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
|
||||
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
|
||||
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
|
||||
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
|
||||
|
||||
<div className="flex items-center gap-3 xl:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={(e) => {
|
||||
// TODO: 유사사례 연결
|
||||
searchSimilarCase(e);
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
onClick={() => {
|
||||
// TODO: 저장 처리
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows} // 실제 데이터
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
const code = getCriterionCode(criterion);
|
||||
if (!code) return;
|
||||
// 값은 상황에 맞게 (예시: usePurpose)
|
||||
await getSimilarCaseList(code, code == 'S' ? reqQty : code == 'U' ? usePurpose : '');
|
||||
}}
|
||||
/>
|
||||
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
|
||||
// ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전)
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
|
||||
setOpenCand(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -819,7 +842,7 @@ function DateInput({
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const openPicker = () => {
|
||||
@@ -863,7 +886,7 @@ function DateInput({
|
||||
aria-label="달력 열기"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z" stroke="#1A1A1A" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z" stroke="#1A1A1A" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
@import "../../../packages/common/components/src/base.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "NotoSansKR";
|
||||
src: url("/fonts/NotoSansKR-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "NotoSansKR", sans-serif;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL;
|
||||
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
|
||||
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
|
||||
const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL || "";
|
||||
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
|
||||
const BUSINESS_JUSO_API_URL = process.env.NEXT_PUBLIC_FINDADRESS_URL || "";
|
||||
const AdressKey = process.env.NEXT_PRIVATE_WR_ADDRESS_KEY || "";
|
||||
|
||||
@@ -72,6 +73,8 @@ export function middleware(req: NextRequest) {
|
||||
if (pathname.startsWith("/geoserver")) {
|
||||
let queryParams = searchParams.toString();
|
||||
const typename = searchParams.get("TYPENAME");
|
||||
const startYmd = searchParams.get("START_YMD") ?? null;
|
||||
const endYmd = searchParams.get("END_YMD") ?? null;
|
||||
console.log("typename ", typename);
|
||||
|
||||
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
@@ -102,6 +105,8 @@ export function middleware(req: NextRequest) {
|
||||
) {
|
||||
const url = req.nextUrl.clone();
|
||||
url.searchParams.set("TYPENAME", typename.replace("hasudo:", ""));
|
||||
if (startYmd) url.searchParams.set("startYmd", startYmd);
|
||||
if (endYmd) url.searchParams.set("endYmd", endYmd);
|
||||
queryParams = url.searchParams.toString();
|
||||
newUrl = `${GATEWAY_URL}/sewerage-service/api/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service # 개발 서버 API
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11300
|
||||
NEXT_PUBLIC_SUB_DOMAIN = http://con.mind-one.co.kr:13000
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11082
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
@@ -17,3 +16,4 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
@@ -17,3 +17,4 @@ NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-s
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service
|
||||
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
@@ -37,7 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/src/shared/api";
|
||||
import type { IUserInfo } from "@/src/shared/store";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { useUserStore, getCookie } from "@/src/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/src/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://localhost:3001/auth/login"; // 개발 환경 URL
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
// const pathname = usePathname();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -33,24 +35,29 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
|
||||
// 개발 환경에서는 localhost 도메인 사용
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr")
|
||||
router.push("/"); // 개발시 홈으로 돌아가기
|
||||
|
||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
// router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용
|
||||
if (hostname.includes("hasuon.or.kr")) window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트
|
||||
setUserInfo(null);
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// const hostname = window.location.hostname;
|
||||
|
||||
// // 개발 환경에서는 localhost 도메인 사용
|
||||
// if (
|
||||
// hostname === "localhost" ||
|
||||
// hostname === "127.0.0.1" ||
|
||||
// hostname === "con.mind-one.co.kr"
|
||||
// ) {
|
||||
// router.push("/"); // 개발시 홈으로 돌아가기
|
||||
// }
|
||||
|
||||
// // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
// // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용
|
||||
// if (hostname.includes("hasuon.or.kr"))
|
||||
// window.location.assign(LOGIN_PAGE_URL); // 세션 만료 시 CM 로그인 페이지로 리다이렉트
|
||||
|
||||
// 개발/운영 환경에서 로그인 URL 분기는 환경 변수로 처리 (.env.*)
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
@@ -58,4 +65,4 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { AuthWrapper };
|
||||
export { AuthWrapper };
|
||||
|
||||
+2
-3
@@ -14,7 +14,6 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
|
||||
|
||||
@@ -12,4 +12,5 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
@@ -15,4 +15,4 @@ NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
|
||||
const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://dev.onasys.co.kr:3000/auth/login"; // 로그인 페이지 URL
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -35,24 +38,27 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUserInfo(null);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
// const hostname = window.location.hostname;
|
||||
|
||||
// 개발 환경에서는 localhost 도메인 사용
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr")
|
||||
window.location.href = "/";
|
||||
// // 개발 환경에서는 localhost 도메인 사용
|
||||
// if (
|
||||
// hostname === "localhost" ||
|
||||
// hostname === "127.0.0.1" ||
|
||||
// hostname === "con.mind-one.co.kr"
|
||||
// )
|
||||
// window.location.href = "/";
|
||||
|
||||
// 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
// router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용
|
||||
if (hostname.includes("hasuon.or.kr")) window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트
|
||||
}
|
||||
// // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용
|
||||
// // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용
|
||||
// if (hostname.includes("hasuon.or.kr"))
|
||||
// window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트
|
||||
|
||||
// 개발/운영 환경에서 로그인 URL 분기는 환경 변수로 처리 (.env.*)
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
})();
|
||||
}, [pathname]);
|
||||
|
||||
+4
-1
@@ -39,4 +39,7 @@ next-env.d.ts
|
||||
|
||||
.env*
|
||||
!.env.example
|
||||
*.md
|
||||
*.md
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import "@/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ko">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import {
|
||||
getTabStoreState as getGridTabStoreState,
|
||||
useTabStore as gridTabStore,
|
||||
} from "@rsone/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
const appSiteId = getCookie("site-id-key") ?? "";
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
);
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
const setMenuList = useTabStore((state) => state.setMenuList);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const menuList = useTabStore((state) => state.menuList);
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => MenuApi.getMenuRoles(appSiteId),
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (menuListData) {
|
||||
setMenuList(menuListData);
|
||||
setMenuListForGrid(menuListData);
|
||||
}
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
|
||||
useEffect(() => {
|
||||
if (!menuList) return;
|
||||
const appTabs = getTabStoreState().tabs;
|
||||
if (appTabs.length === 0) return;
|
||||
if (getGridTabStoreState().tabs.length > 0) return;
|
||||
|
||||
for (const tab of appTabs) {
|
||||
getGridTabStoreState().addTab(tab.menuItem);
|
||||
}
|
||||
|
||||
const activeId = getTabStoreState().activeTabId;
|
||||
if (activeId) {
|
||||
const activeTab = appTabs.find((t) => t.id === activeId);
|
||||
const gridActiveId = activeTab?.menuItem.menuEngNm;
|
||||
if (gridActiveId) {
|
||||
getGridTabStoreState().setActiveTab(gridActiveId);
|
||||
}
|
||||
}
|
||||
}, [menuList, tabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
// pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const tab = currentTabs.find((t) => t.path === pathname);
|
||||
if (tab) {
|
||||
setActiveTab(tab.id);
|
||||
const gridActiveId = tab.menuItem.menuEngNm;
|
||||
if (gridActiveId) {
|
||||
setActiveTabForGrid(gridActiveId);
|
||||
}
|
||||
}
|
||||
}, [pathname, setActiveTab, setActiveTabForGrid]);
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
const currentPath = useMemo(() => {
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.urlPath === activeTabId) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findMenuByEngNm(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
return activeMenu.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
if (activeTabId) {
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
return activeTab.menuItem.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
return currentMenuInfo.urlPath;
|
||||
}
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
return pathname ?? "";
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
if (addTabResult) {
|
||||
addTabForGrid(item);
|
||||
}
|
||||
|
||||
if (!addTabResult) {
|
||||
alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다.
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
}
|
||||
},
|
||||
[setCurrentMenuInfo, addTab, addTabForGrid, alert],
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
-1
@@ -1,3 +1,2 @@
|
||||
import { StatisticsPage } from "@/pages/statistics";
|
||||
|
||||
export default StatisticsPage;
|
||||
@@ -1,139 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useQueryClient, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore, useAlertStore } from "@/shared/lib";
|
||||
import { useTabStore, getTabStoreState } from "@/store/tab-store";
|
||||
import { useTabStore as gridTabStore } from "@rsone/components";
|
||||
import { getConfig } from "@common/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { getMenuList } from "../../../../packages/common/components/src/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
const appSiteId = getCookie("site-id-key") ?? "";
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
const setMenuList = useTabStore((state) => state.setMenuList);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const menuList = useTabStore((state) => state.menuList);
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => getMenuList(appSiteId),
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (menuListData) {
|
||||
setMenuList(menuListData);
|
||||
setMenuListForGrid(menuListData);
|
||||
}
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
// pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const tab = currentTabs.find((t) => t.path === pathname);
|
||||
if (tab) {
|
||||
setActiveTab(tab.id);
|
||||
setActiveTabForGrid(tab.id);
|
||||
}
|
||||
}, [pathname, setActiveTab, setActiveTabForGrid]);
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
const currentPath = useMemo(() => {
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.menuEngNm === activeTabId) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findMenuByEngNm(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
return activeMenu.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
if (activeTabId) {
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
return activeTab.menuItem.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
return currentMenuInfo.urlPath;
|
||||
}
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
return pathname ?? "";
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
const handleMenuClick = useCallback((item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
if (addTabResult) {
|
||||
addTabForGrid(item);
|
||||
}
|
||||
|
||||
if (!addTabResult) {
|
||||
alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다.
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
}
|
||||
}, [setCurrentMenuInfo, addTab, addTabForGrid, alert]);
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +1,106 @@
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
async function handler(
|
||||
request: Request,
|
||||
params: { url: string[] },
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
request: Request,
|
||||
params: { url: string[] },
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
) {
|
||||
const cookieStore = await cookies();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
const cookieStore = await cookies();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
|
||||
// silent refresh
|
||||
if (isRefreshHandler) {
|
||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||
// silent refresh
|
||||
if (isRefreshHandler) {
|
||||
const refreshToken = cookieStore.get(REFRESH_TOKEN);
|
||||
headers[CLAIM_NAME] = refreshToken?.value ?? "";
|
||||
|
||||
if (!headers[CLAIM_NAME]) {
|
||||
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isRefreshHandler) {
|
||||
body = await request.json();
|
||||
if (!headers[CLAIM_NAME]) {
|
||||
return NextResponse.json(
|
||||
{ message: "Can't refresh" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
try {
|
||||
if (!isRefreshHandler) {
|
||||
body = await request.json();
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
const result = await fetch(
|
||||
`${SERVER_API_URL}/${params.url.join("/")}`,
|
||||
{
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
|
||||
if (!result.ok) {
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = result.headers.get(ACCESS_TOKEN);
|
||||
const refreshToken = result.headers.get(REFRESH_TOKEN);
|
||||
const userId = result.headers.get(AUTH_USER_ID);
|
||||
|
||||
if (accessToken && userId) {
|
||||
const response = NextResponse.json({
|
||||
[ACCESS_TOKEN]: accessToken,
|
||||
[AUTH_USER_ID]: userId,
|
||||
});
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Server Error", error },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = result.headers.get(ACCESS_TOKEN);
|
||||
const refreshToken = result.headers.get(REFRESH_TOKEN);
|
||||
const userId = result.headers.get(AUTH_USER_ID);
|
||||
|
||||
if (accessToken && userId) {
|
||||
const response = NextResponse.json({
|
||||
[ACCESS_TOKEN]: accessToken,
|
||||
[AUTH_USER_ID]: userId,
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (refreshToken) {
|
||||
cookieStore.set(REFRESH_TOKEN, refreshToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
// domain: domain,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ message: "Server Error", error },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
props: { params: Promise<{ url: string[] }> },
|
||||
request: Request,
|
||||
props: { params: Promise<{ url: string[] }> },
|
||||
) {
|
||||
const params = await props.params;
|
||||
return handler(request, params, "POST");
|
||||
const params = await props.params;
|
||||
return handler(request, params, "POST");
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
props: { params: Promise<{ url: string[] }> },
|
||||
request: Request,
|
||||
props: { params: Promise<{ url: string[] }> },
|
||||
) {
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
const params = await props.params;
|
||||
return handler(request, params, "PUT");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import baseEsLintConfig from "../../eslint.config.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -11,6 +12,7 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...baseEsLintConfig,
|
||||
{
|
||||
rules: {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
||||
+19
-14
@@ -12,10 +12,10 @@ import {
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
|
||||
const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
|
||||
const stServiceUrl = ST_SERVICE_URL;
|
||||
|
||||
const linkBackendApiHost = process.env.NEXT_PUBLIC_LINK_BACKEND_API_HOST ?? "http://localhost:8080";
|
||||
|
||||
// CSP 헤더 설정 함수 (img-src만)
|
||||
function addSecurityHeaders(response: NextResponse): NextResponse {
|
||||
response.headers.set("Content-Security-Policy", "img-src 'self' data: https: blob:;");
|
||||
@@ -28,41 +28,46 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
const logMessages: string[] = [
|
||||
`[Middleware Request] ${new Date().toISOString()}`,
|
||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}`,
|
||||
`[Environment]\n Mode: ${process.env.NODE_ENV}\n Port: ${PORT}\n Proxy Host: ${PROXY_HOST}\n Server API URL: ${SERVER_API_URL}\n Site ID: ${SITE_ID}\n ST Service URL: ${stServiceUrl}`,
|
||||
`[Request]\n Original Pathname: ${pathname}`,
|
||||
];
|
||||
|
||||
if (pathname.startsWith("/st-management-service") || pathname.startsWith("/st-input-service")) {
|
||||
// ST Service 처리 (dc의 /server/dc-service 패턴과 동일)
|
||||
if (pathname.startsWith("/server/st-management-service") || pathname.startsWith("/server/st-input-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 queryParams = searchParams.toString();
|
||||
const newUrl = `${stServiceUrl}${pathname}${
|
||||
const newUrl = `${stServiceUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
// 디버깅을 위한 토큰 정보 로그
|
||||
logMessages.push(` Token Debug: ${token ? 'EXISTS' : 'MISSING'}`);
|
||||
logMessages.push(` UserId Debug: ${userId ? 'EXISTS' : 'MISSING'}`);
|
||||
|
||||
logMessages.push(` Token Debug: ${token ? "EXISTS" : "MISSING"}`);
|
||||
logMessages.push(` UserId Debug: ${userId ? "EXISTS" : "MISSING"}`);
|
||||
logMessages.push(` ST Service Rewritten URL: ${newUrl}`);
|
||||
logMessages.push("\n--------------------------------\n");
|
||||
console.log(logMessages.join("\n"));
|
||||
|
||||
|
||||
const response = NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
return addSecurityHeaders(response);
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
// 일반 Server API 처리 (ST 서비스 제외)
|
||||
if (
|
||||
pathname.startsWith("/server") &&
|
||||
!pathname.startsWith("/server/st-management-service") &&
|
||||
!pathname.startsWith("/server/st-input-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 queryParams = searchParams.toString();
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
@@ -138,5 +143,5 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/st-management-service/:path*", "/st-input-service/:path*", "/"],
|
||||
matcher: ["/server/:path*", "/attach-service/:path*", "/api/link/:path*", "/"],
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -61,7 +62,8 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
.card {
|
||||
position: relative;
|
||||
margin: 0.6rem;
|
||||
box-sizing: border-box;
|
||||
min-height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
font-size: 1.3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card_width10 {
|
||||
width: calc(10% - 12px);
|
||||
}
|
||||
|
||||
.card_width20 {
|
||||
width: calc(20% - 12px);
|
||||
}
|
||||
|
||||
.card_width25 {
|
||||
width: calc(25% - 12px);
|
||||
}
|
||||
|
||||
.card_width30 {
|
||||
width: calc(30% - 12px);
|
||||
}
|
||||
|
||||
.card_width33 {
|
||||
width: calc(33.333% - 12px);
|
||||
}
|
||||
|
||||
.card_width40 {
|
||||
width: calc(40% - 12px);
|
||||
}
|
||||
|
||||
.card_width45 {
|
||||
width: calc(45% - 12px);
|
||||
}
|
||||
|
||||
.card_width50 {
|
||||
width: calc(50% - 12px);
|
||||
}
|
||||
|
||||
.card_width60 {
|
||||
width: calc(60% - 12px);
|
||||
}
|
||||
|
||||
.card_width66 {
|
||||
width: calc(66.666% - 12px);
|
||||
}
|
||||
|
||||
.card_width70 {
|
||||
width: calc(70% - 12px);
|
||||
}
|
||||
|
||||
.card_width75 {
|
||||
width: calc(75% - 12px);
|
||||
}
|
||||
|
||||
.card_width80 {
|
||||
width: calc(80% - 12px);
|
||||
}
|
||||
|
||||
.card_width90 {
|
||||
width: calc(90% - 12px);
|
||||
}
|
||||
|
||||
.card_width100 {
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
|
||||
.card_nopadding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card_nomargin {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.topline {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.bottomline {
|
||||
border-bottom: 1px solid #e6eef3;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-border {
|
||||
border: 1px solid #dbe2e6;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-top: -0.6rem;
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e1e7eb;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-left: -0.6rem;
|
||||
margin-right: -0.6rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e1e7eb;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 블랙 테마 */
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
background-color: rgba(255, 255, 255, 0.065);
|
||||
border-color: #38383a;
|
||||
}
|
||||
.card-header {
|
||||
border-bottom-color: #3f4042;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface CancelSubmitButtonProps {
|
||||
cnacelSubmitFunc: () => void;
|
||||
}
|
||||
|
||||
const CancelSubmitButton = ({ cnacelSubmitFunc }: CancelSubmitButtonProps) => {
|
||||
return (
|
||||
<Button className="h-[3.2rem] w-[96px]" color="red" onClick={cnacelSubmitFunc}>
|
||||
<i className="ri-arrow-reset"></i>
|
||||
<span>제출 취소</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancelSubmitButton;
|
||||
@@ -4,9 +4,24 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#466995] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
.rg-grid .rg-header .rg-table {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
/* 상태바·체크바 헤더 */
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
.rg-grid .rg-header .rg-table tr td,
|
||||
.rg-grid .rg-head .rg-table tr td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-white;
|
||||
}
|
||||
@@ -17,9 +32,30 @@
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
|
||||
@apply !bg-[#ffe6e6] !text-[#c00];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
|
||||
@apply !bg-[#fff7bf] !text-[#b45309];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total {
|
||||
@apply !bg-[#e3eaf2];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-biz-sum {
|
||||
@apply !bg-[#ffe9cf];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-other {
|
||||
@apply !bg-[#fff7bf];
|
||||
}
|
||||
|
||||
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
|
||||
.data-grid-status-bubble {
|
||||
@apply rounded-lg border border-slate-200/80 bg-slate-50/90 px-4 py-2.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
.data-grid-status-bubble span {
|
||||
@apply text-base text-slate-700;
|
||||
}
|
||||
.data-grid-status-bubble span + span {
|
||||
@apply ml-4 border-l border-slate-200/70 pl-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@@ -28,6 +64,12 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-[#545556] !text-[#fff];
|
||||
}
|
||||
@@ -40,6 +82,12 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
@@ -49,5 +97,15 @@
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
margin-top: 0.6rem !important;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useRef, useEffect } from "react"
|
||||
import { useRef, useEffect, useLayoutEffect } from "react"
|
||||
import { ValueType } from "realgrid"
|
||||
|
||||
import RealGrid, { GridFitStyle } from "realgrid"
|
||||
import { RealGridReact, RGDataColumn } from "realgrid-react"
|
||||
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback } from "../type/DataGrid.type"
|
||||
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback } from "../type/DataGrid.type"
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||
|
||||
// RealGrid 타입 확장
|
||||
@@ -62,15 +62,37 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
|
||||
};
|
||||
}
|
||||
|
||||
const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false, rowData, setRowData }: DataGridProps) => {
|
||||
const CompactDataGrid = ({
|
||||
columnInfo,
|
||||
fixedColumns,
|
||||
showHeaderCheckbox = false,
|
||||
rowData,
|
||||
enableSort = false,
|
||||
// setRowData,
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
onRowClick,
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
|
||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!cellStyleCallback || !gridRef.current?.gridView) return;
|
||||
gridRef.current.gridView.refresh();
|
||||
}, [cellStyleCallback, rowData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gridRef.current?.gridView || !gridRef.current?.dataProvider) return;
|
||||
|
||||
const gridView = gridRef.current.gridView;
|
||||
const dataProvider = gridRef.current.dataProvider;
|
||||
if ((dataProvider as { setRows?: (r: unknown[]) => void })?.setRows && rowData) {
|
||||
(dataProvider as { setRows: (r: unknown[]) => void }).setRows(rowData);
|
||||
}
|
||||
|
||||
// setDataComparer로 숫자 정렬 (setFields 미사용 → 데이터 유실·필드 중복 오류 없음)
|
||||
const dp = dataProvider as RealGridDataProviderComparer;
|
||||
@@ -231,11 +253,11 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
|
||||
// footer가 단일 객체이고 valueCallback이 있는 경우
|
||||
columns.forEach(col => {
|
||||
if (!col.footer || Array.isArray(col.footer)) return;
|
||||
|
||||
|
||||
try {
|
||||
const column = currentGridView.columnByName(col.name);
|
||||
if (!column || !column.footer) return;
|
||||
|
||||
|
||||
if (col.footer.valueCallback) {
|
||||
(column.footer as { valueCallback?: typeof col.footer.valueCallback }).valueCallback = col.footer.valueCallback;
|
||||
}
|
||||
@@ -243,9 +265,75 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
|
||||
console.warn(`Error setting footer valueCallback for column ${col.name}:`, e);
|
||||
}
|
||||
});
|
||||
|
||||
// cellStyleCallback이 있으면 컬럼별 styleCallback 설정
|
||||
if (cellStyleCallbackRef.current) {
|
||||
columns.forEach((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
const gridCol = currentGridView.columnByName(column.name) as {
|
||||
styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string;
|
||||
} | null;
|
||||
if (gridCol) {
|
||||
gridCol.styleCallback = (_g, dataCell) => {
|
||||
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
|
||||
const fieldName = (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? '';
|
||||
const data = rowDataRef.current;
|
||||
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
|
||||
const row = data[itemIndex] as Record<string, string | number>;
|
||||
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
|
||||
if (!extra) return baseStyleName ?? '';
|
||||
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
currentGridView.refresh?.();
|
||||
}, 200);
|
||||
|
||||
}, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox]);
|
||||
if (getGridDataRef) {
|
||||
const fieldNames = columns.map((c) => c.fieldName).filter(Boolean) as string[];
|
||||
getGridDataRef.current = () => {
|
||||
const gv = gridRef.current?.gridView as {
|
||||
getDataSource?: () => { getRowCount(): number; getValue(r: number, f: string): unknown };
|
||||
} | undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
if (!ds) return [];
|
||||
const cnt = ds.getRowCount();
|
||||
const rows: Record<string, string | number>[] = [];
|
||||
for (let i = 0; i < cnt; i++) {
|
||||
const r: Record<string, string | number> = {};
|
||||
for (const f of fieldNames) r[f] = ds.getValue(i, f) as string | number;
|
||||
rows.push(r);
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (getGridDataRef) getGridDataRef.current = null;
|
||||
};
|
||||
}, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox, getGridDataRef]);
|
||||
|
||||
// 행 클릭 이벤트
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || !onRowClick) return;
|
||||
|
||||
gridView.onCellClicked = (grid, clickData) => {
|
||||
if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return;
|
||||
try {
|
||||
const row = rowDataRef.current[clickData.itemIndex];
|
||||
onRowClick((row ?? {}) as Record<string, string | number>);
|
||||
} catch (error) {
|
||||
console.error("행 데이터 가져오기 오류:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
gridView.onCellClicked = undefined;
|
||||
};
|
||||
}, [onRowClick]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem] w-1/2">
|
||||
@@ -255,11 +343,16 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined;
|
||||
onInitialized={() => {
|
||||
const dp = gridRef.current?.dataProvider as
|
||||
| { setFields?(f: unknown[]): void; setRows?(r: Record<string, string | number>[]): void }
|
||||
| undefined;
|
||||
if (dp?.setFields) {
|
||||
dp.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
if (dp?.setRows && rowData?.length) {
|
||||
dp.setRows(rowData);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
@@ -283,7 +376,7 @@ const CompactDataGrid = ({ columnInfo, fixedColumns, showHeaderCheckbox = false,
|
||||
numberFormat: column.footer.numberFormat,
|
||||
} : undefined}
|
||||
mergeRule={column.mergeCells ? { criteria: 'value' } : undefined}
|
||||
sortable={!column.sortdisable}
|
||||
sortable={enableSort && !column.sortdisable}
|
||||
/>
|
||||
))}
|
||||
</RealGridReact>
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import { useRef, useEffect, useLayoutEffect } from "react";
|
||||
import { ValueType } from "realgrid";
|
||||
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||
|
||||
import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid";
|
||||
import { RealGridReact, RGDataColumn } from "realgrid-react";
|
||||
import type {
|
||||
DataGridProps,
|
||||
RealGridColumnInfo,
|
||||
GridViewWithSetColumns,
|
||||
FooterItemWithCallback,
|
||||
ColumnLayoutItem,
|
||||
CellStyleCallback,
|
||||
} from "../type/DataGrid.type";
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
|
||||
import JSZip from "jszip";
|
||||
import { ValueType } from "realgrid";
|
||||
import RealGrid, { type DisplayOptions, GridFitStyle } from "realgrid";
|
||||
import { RealGridReact, RGDataColumn } from "realgrid-react";
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import type {
|
||||
CellStyleCallback,
|
||||
CellTooltipCallback,
|
||||
ColumnLayoutItem,
|
||||
DataGridProps,
|
||||
FooterItemWithCallback,
|
||||
GridViewWithSetColumns,
|
||||
RealGridColumnInfo,
|
||||
} from "../type/DataGrid.type";
|
||||
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
|
||||
// RealGrid 타입 확장
|
||||
@@ -86,110 +88,6 @@ function toNum(v: unknown): number {
|
||||
return parseFloat(String(v).replace(/,/g, "")) || NaN;
|
||||
}
|
||||
|
||||
/** 헤더에 정렬 화살표 표시 (CSS 방식) */
|
||||
function updateHeaderSortIndicator(
|
||||
gridView: {
|
||||
columnByIndex?: (
|
||||
i: number,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
columnByName?: (
|
||||
name: string,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
getColumn?: (
|
||||
i: number,
|
||||
) => { fieldName?: string; name?: string } | undefined;
|
||||
},
|
||||
columns: { fieldName?: string; name: string }[],
|
||||
sortDir: Record<string, "asc" | "desc" | "none">,
|
||||
) {
|
||||
try {
|
||||
columns.forEach((col) => {
|
||||
if (!col.fieldName && !col.name) return;
|
||||
|
||||
// 컬럼 찾기
|
||||
let gridCol: { fieldName?: string; name?: string } | undefined;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const c =
|
||||
gridView.columnByIndex?.(i) ?? gridView.getColumn?.(i);
|
||||
if (!c) break;
|
||||
if (
|
||||
c.fieldName === (col.fieldName || col.name) ||
|
||||
c.name === (col.fieldName || col.name)
|
||||
) {
|
||||
gridCol = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!gridCol) {
|
||||
gridCol = gridView.columnByName?.(col.name);
|
||||
}
|
||||
|
||||
if (!gridCol) return;
|
||||
|
||||
const fieldName =
|
||||
gridCol.fieldName || gridCol.name || col.fieldName || col.name;
|
||||
const direction = sortDir[fieldName] || sortDir[col.name] || "none";
|
||||
|
||||
// DOM에서 헤더 요소 찾기
|
||||
const gridEl1 = document.querySelector(
|
||||
"[data-grid-id]",
|
||||
) as HTMLElement | null;
|
||||
const gridEl2 = document.querySelector(
|
||||
".rg-grid",
|
||||
) as HTMLElement | null;
|
||||
const gridEl = gridEl1 || gridEl2;
|
||||
if (!gridEl) return;
|
||||
|
||||
// 헤더 셀 찾기 (fieldName 또는 name으로)
|
||||
const headerCells = gridEl.querySelectorAll(
|
||||
'.rg-header-cell, [class*="header"]',
|
||||
);
|
||||
let targetCell: HTMLElement | null = null;
|
||||
|
||||
headerCells.forEach((cell) => {
|
||||
const el = cell as HTMLElement;
|
||||
const text = el.textContent?.trim();
|
||||
const colName = col.name;
|
||||
const colFieldName = col.fieldName;
|
||||
if (
|
||||
text &&
|
||||
(text.includes(colName) ||
|
||||
(colFieldName && text.includes(colFieldName)))
|
||||
) {
|
||||
targetCell = el;
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetCell) return;
|
||||
|
||||
const headerCell = targetCell as HTMLElement;
|
||||
|
||||
// 기존 화살표 제거
|
||||
const existingIndicator = headerCell.querySelector(
|
||||
".rg-sort-indicator",
|
||||
) as HTMLElement | null;
|
||||
if (existingIndicator) {
|
||||
existingIndicator.remove();
|
||||
}
|
||||
|
||||
// 화살표 추가
|
||||
if (direction === "asc" || direction === "desc") {
|
||||
const indicator = document.createElement("span");
|
||||
indicator.className = "rg-sort-indicator";
|
||||
indicator.textContent = direction === "asc" ? "▲" : "▼";
|
||||
indicator.style.marginLeft = "4px";
|
||||
indicator.style.fontSize = "10px";
|
||||
indicator.style.color = "#666";
|
||||
indicator.style.display = "inline-block";
|
||||
indicator.style.verticalAlign = "middle";
|
||||
headerCell.appendChild(indicator);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("updateHeaderSortIndicator error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */
|
||||
function createOnSortingHandler(
|
||||
columns: {
|
||||
@@ -215,12 +113,14 @@ function createOnSortingHandler(
|
||||
),
|
||||
];
|
||||
// mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용
|
||||
// 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용
|
||||
// 시군구(sggNm) 우선, 없으면 행정구역명(admdstNm), 없으면 첫 번째 mergeCells 컬럼
|
||||
const mergeCellCols = columns.filter(
|
||||
(c) => c.mergeCells === true && c.fieldName,
|
||||
);
|
||||
const mergeCellCol =
|
||||
mergeCellCols.find((c) => c.fieldName === "gugun") || mergeCellCols[0];
|
||||
mergeCellCols.find((c) => c.fieldName === "sggNm") ||
|
||||
mergeCellCols.find((c) => c.fieldName === "admdstNm") ||
|
||||
mergeCellCols[0];
|
||||
const groupField = mergeCellCol?.fieldName || null;
|
||||
const orderByField = "displayYear"; // 고정값 또는 설정 가능하게 할 수 있음
|
||||
const hasMergeCells = mergeCellCols.length > 0;
|
||||
@@ -251,7 +151,8 @@ function createOnSortingHandler(
|
||||
const col = columns.find(
|
||||
(c) => c.fieldName === sortField || c.name === sortField,
|
||||
);
|
||||
if (!col || col.sortdisable) return true;
|
||||
// sortdisable 컬럼은 정렬 불가(클릭 무시)
|
||||
if (!col || col.sortdisable) return false;
|
||||
|
||||
// mergeCells: true인 컬럼이 없으면 RealGrid 기본 정렬 사용
|
||||
if (!hasMergeCells || !groupField) return true;
|
||||
@@ -276,6 +177,7 @@ function createOnSortingHandler(
|
||||
if (orig && setRows) {
|
||||
setRows(orig);
|
||||
setRowData(orig);
|
||||
(g as { refresh?: () => void }).refresh?.();
|
||||
}
|
||||
sortDirRef.current[sortField] = "none";
|
||||
return false;
|
||||
@@ -294,11 +196,13 @@ function createOnSortingHandler(
|
||||
);
|
||||
setRows?.(sorted);
|
||||
setRowData(sorted);
|
||||
// 데이터 변경 후 merge 재적용
|
||||
(g as { refresh?: () => void }).refresh?.();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/** sortPreserveGroupOrder: 그룹 간에는 sortField로 정렬, 그룹 내에서는 원본 순서 유지. */
|
||||
/** sortPreserveGroupOrder: 그룹 간에는 sortField로 정렬, 그룹 내에서는 원본 순서 유지(구분 포함). */
|
||||
function sortRowsGroupPreserving(
|
||||
rows: Record<string, string | number>[],
|
||||
sortField: string,
|
||||
@@ -415,17 +319,46 @@ const DataGrid = ({
|
||||
setRowData,
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
cellTooltipCallback,
|
||||
onRowClick,
|
||||
statusInfo,
|
||||
highlightedRowId,
|
||||
idFieldName = "id",
|
||||
enableSort = false,
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({});
|
||||
const originalRowsRef = useRef<Record<string, Record<string, string | number>[]>>({});
|
||||
const originalRowsRef = useRef<
|
||||
Record<string, Record<string, string | number>[]>
|
||||
>({});
|
||||
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
|
||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
|
||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(
|
||||
cellStyleCallback,
|
||||
);
|
||||
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
|
||||
cellTooltipCallback,
|
||||
);
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
cellTooltipCallbackRef.current = cellTooltipCallback;
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
if (!gridRef.current?.gridView || highlightedRowId == null) return;
|
||||
|
||||
const gridView = gridRef.current.gridView;
|
||||
const index = rowData.findIndex(
|
||||
(row) => String(row[idFieldName]) === String(highlightedRowId),
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
// 해당 행으로 포커스 이동 및 스크롤
|
||||
gridView.setCurrent({ itemIndex: index });
|
||||
gridView.setFocus();
|
||||
}
|
||||
}, [highlightedRowId, rowData, idFieldName]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!cellStyleCallback || !gridRef.current?.gridView) return;
|
||||
@@ -542,6 +475,7 @@ const DataGrid = ({
|
||||
target: "local",
|
||||
fileName: `${menuName}.xlsx`,
|
||||
footer: "hidden",
|
||||
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
@@ -624,10 +558,10 @@ const DataGrid = ({
|
||||
) {
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
XLSX.utils.encode_cell({
|
||||
r: rowIndex,
|
||||
c: col,
|
||||
})
|
||||
];
|
||||
if (
|
||||
cell &&
|
||||
@@ -703,7 +637,7 @@ const DataGrid = ({
|
||||
const col = range.s.c + colIndex;
|
||||
const cell =
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
@@ -826,12 +760,14 @@ const DataGrid = ({
|
||||
gridView as {
|
||||
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
|
||||
}
|
||||
).onSorting = createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
);
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
|
||||
(gridView.displayOptions as DisplayOptions).fitStyle =
|
||||
GridFitStyle.EVEN;
|
||||
@@ -945,12 +881,14 @@ const DataGrid = ({
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
...(col.width != null && { width: col.width }),
|
||||
...(col.styleName != null && {
|
||||
styleName: col.styleName,
|
||||
}),
|
||||
...(col.mergeCells && {
|
||||
mergeRule: { criteria: "value" as const },
|
||||
}),
|
||||
sortable: !col.sortdisable,
|
||||
};
|
||||
|
||||
if (col.footer) {
|
||||
@@ -999,7 +937,7 @@ const DataGrid = ({
|
||||
) {
|
||||
(
|
||||
column.footer[
|
||||
index
|
||||
index
|
||||
] as FooterItemWithCallback
|
||||
).valueCallback =
|
||||
footerItem.valueCallback;
|
||||
@@ -1041,6 +979,8 @@ const DataGrid = ({
|
||||
);
|
||||
}
|
||||
});
|
||||
// valueCallback 적용 후 푸터 셀 다시 그리기 (API 합계 등 우선 표시 반영)
|
||||
currentGridView.refresh?.();
|
||||
}, 200);
|
||||
|
||||
if (getGridDataRef) {
|
||||
@@ -1050,11 +990,11 @@ const DataGrid = ({
|
||||
getGridDataRef.current = () => {
|
||||
const gv = gridRef.current?.gridView as
|
||||
| {
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
@@ -1083,38 +1023,72 @@ const DataGrid = ({
|
||||
getGridDataRef,
|
||||
]);
|
||||
|
||||
// 행 클릭 이벤트 설정
|
||||
// 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식)
|
||||
useEffect(() => {
|
||||
if (!gridRef.current?.gridView || !onRowClick) return;
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || (!onRowClick && !cellTooltipCallback)) return;
|
||||
|
||||
const gridView = gridRef.current.gridView;
|
||||
|
||||
// onCellClicked 이벤트 핸들러 설정
|
||||
gridView.onCellClicked = (grid, clickData) => {
|
||||
// 데이터 셀만 처리 (헤더, 푸터 등 제외)
|
||||
if (
|
||||
clickData.cellType === "data" &&
|
||||
clickData.itemIndex !== undefined
|
||||
) {
|
||||
try {
|
||||
// 행 전체 데이터 가져오기
|
||||
const row = rowData[clickData.itemIndex];
|
||||
|
||||
// 부모 컴포넌트로 데이터 전달
|
||||
onRowClick(row ?? {});
|
||||
} catch (error) {
|
||||
console.error("행 데이터 가져오기 오류:", error);
|
||||
clickData.cellType !== "data" ||
|
||||
clickData.itemIndex === undefined
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const row = rowDataRef.current[clickData.itemIndex];
|
||||
const cellIndex = (
|
||||
grid as {
|
||||
getCurrent?: () => {
|
||||
itemIndex: number;
|
||||
column?: string;
|
||||
fieldName?: string;
|
||||
};
|
||||
}
|
||||
).getCurrent?.();
|
||||
// cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex)
|
||||
if (cellTooltipCallbackRef.current && row && cellIndex) {
|
||||
const fieldName =
|
||||
cellIndex.fieldName ?? cellIndex.column ?? "";
|
||||
const msg = cellTooltipCallbackRef.current({
|
||||
row: row as Record<string, string | number>,
|
||||
fieldName,
|
||||
});
|
||||
if (msg) {
|
||||
const colName =
|
||||
columns.find(
|
||||
(c) =>
|
||||
c.fieldName === fieldName ||
|
||||
c.name === cellIndex.column,
|
||||
)?.name ??
|
||||
cellIndex.column ??
|
||||
fieldName;
|
||||
(
|
||||
gridView as {
|
||||
showMessage?: (
|
||||
opts: {
|
||||
message: string;
|
||||
hideDuration?: number;
|
||||
},
|
||||
idx: { itemIndex: number; column: string },
|
||||
) => void;
|
||||
}
|
||||
).showMessage?.(
|
||||
{ message: msg, hideDuration: 5000 },
|
||||
{ itemIndex: cellIndex.itemIndex, column: colName },
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onRowClick)
|
||||
onRowClick((row ?? {}) as Record<string, string | number>);
|
||||
} catch (error) {
|
||||
console.error("행 데이터 가져오기 오류:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// cleanup: 컴포넌트 언마운트 시 이벤트 핸들러 제거
|
||||
return () => {
|
||||
if (gridView.onCellClicked) {
|
||||
gridView.onCellClicked = undefined;
|
||||
}
|
||||
if (gridView.onCellClicked) gridView.onCellClicked = undefined;
|
||||
};
|
||||
}, [onRowClick]);
|
||||
}, [onRowClick, cellTooltipCallback, columns]);
|
||||
|
||||
return (
|
||||
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
|
||||
@@ -1126,6 +1100,44 @@ const DataGrid = ({
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-[0.6rem] px-[1.2rem]">
|
||||
{statusInfo && (
|
||||
<div className="data-grid-status-bubble flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span>
|
||||
진행상황:{" "}
|
||||
{statusInfo.laeSbmsnSttsCd === null ||
|
||||
statusInfo.laeSbmsnSttsCd === "0"
|
||||
? "미작성"
|
||||
: statusInfo.laeSbmsnSttsCd === "1"
|
||||
? "작성중"
|
||||
: statusInfo.laeSbmsnSttsCd === "2"
|
||||
? "제출"
|
||||
: statusInfo.laeSbmsnSttsCd === "3"
|
||||
? "승인"
|
||||
: statusInfo.laeSbmsnSttsCd === "4"
|
||||
? "반려"
|
||||
: "검증중"}
|
||||
</span>
|
||||
<span>
|
||||
제출일:{" "}
|
||||
{statusInfo.laeSbmsnYmd
|
||||
? statusInfo.laeSbmsnYmd.length === 8 &&
|
||||
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
|
||||
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
|
||||
: statusInfo.laeSbmsnYmd
|
||||
: "미제출"}
|
||||
</span>
|
||||
<span>
|
||||
담당자:{" "}
|
||||
{statusInfo.laePicNm ? statusInfo.laePicNm : "-"}
|
||||
</span>
|
||||
<span>
|
||||
연락처:{" "}
|
||||
{statusInfo.laePicTelNo
|
||||
? statusInfo.laePicTelNo
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isExcelImport && (
|
||||
<Button
|
||||
className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
|
||||
@@ -1148,48 +1160,158 @@ const DataGrid = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[65vh]">
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
if (!grid) return;
|
||||
(grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef);
|
||||
const dataProvider = gridRef.current?.dataProvider as unknown as
|
||||
| { setFields?(f: unknown[]): void; setRows?(r: Record<string, string | number>[]): void; getJsonRow?(r: number): Record<string, unknown> }
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(buildFieldsFromColumns(columns));
|
||||
<RealGridReact
|
||||
ref={gridRef}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
if (!grid) return;
|
||||
(
|
||||
grid as {
|
||||
onSorting?: (
|
||||
g: unknown,
|
||||
f: unknown[],
|
||||
d: unknown[],
|
||||
) => boolean;
|
||||
}
|
||||
if (dataProvider?.setRows && rowData?.length) {
|
||||
dataProvider.setRows(rowData);
|
||||
).onSorting = enableSort
|
||||
? createOnSortingHandler(
|
||||
columns,
|
||||
setRowData,
|
||||
sortDirRef,
|
||||
originalRowsRef,
|
||||
)
|
||||
: () => false;
|
||||
const dataProvider = gridRef.current
|
||||
?.dataProvider as unknown as
|
||||
| {
|
||||
setFields?(f: unknown[]): void;
|
||||
setRows?(
|
||||
r: Record<string, string | number>[],
|
||||
): void;
|
||||
getJsonRow?(
|
||||
r: number,
|
||||
): Record<string, unknown>;
|
||||
}
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
if (!gv?.columnByName) return;
|
||||
columns.forEach((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string } | null;
|
||||
if (gridCol) {
|
||||
gridCol.styleCallback = (_g, dataCell) => {
|
||||
const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
|
||||
const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
|
||||
const data = rowDataRef.current;
|
||||
if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? '';
|
||||
const row = data[itemIndex] as Record<string, string | number>;
|
||||
const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row });
|
||||
if (!extra) return baseStyleName ?? '';
|
||||
return baseStyleName ? `${extra} ${baseStyleName}` : extra;
|
||||
};
|
||||
| undefined;
|
||||
if (dataProvider?.setFields) {
|
||||
dataProvider.setFields(
|
||||
buildFieldsFromColumns(columns),
|
||||
);
|
||||
}
|
||||
if (dataProvider?.setRows && rowData?.length) {
|
||||
dataProvider.setRows(rowData);
|
||||
}
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
if (!gv?.columnByName) return;
|
||||
// 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함)
|
||||
if (cellTooltipCallbackRef.current) {
|
||||
(
|
||||
gv as {
|
||||
onShowTooltip?: (
|
||||
grid: unknown,
|
||||
index: {
|
||||
itemIndex: number;
|
||||
column: string;
|
||||
},
|
||||
value: string,
|
||||
) => string | undefined;
|
||||
}
|
||||
});
|
||||
gv.refresh();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
).onShowTooltip = (_grid, index, value) => {
|
||||
const row =
|
||||
rowDataRef.current[index?.itemIndex];
|
||||
if (row == null) return value;
|
||||
const column = index.column;
|
||||
const col = columns.find(
|
||||
(c) => c.name === column,
|
||||
);
|
||||
const fieldName = col?.fieldName ?? column;
|
||||
const custom =
|
||||
cellTooltipCallbackRef.current?.({
|
||||
row: row as Record<
|
||||
string,
|
||||
string | number
|
||||
>,
|
||||
fieldName,
|
||||
});
|
||||
// verificationResult 등 메시지가 있을 때만 툴팁 표시 (없으면 undefined 반환 → 툴팁 안 띄움)
|
||||
return custom && custom.length > 0
|
||||
? custom
|
||||
: undefined;
|
||||
};
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
const gridCol = gv.columnByName(
|
||||
column.name,
|
||||
) as {
|
||||
styleCallback?: (
|
||||
g: unknown,
|
||||
dataCell: {
|
||||
itemIndex?: number;
|
||||
index?: { itemIndex?: number };
|
||||
dataField?: string;
|
||||
fieldName?: string;
|
||||
},
|
||||
) => string;
|
||||
renderer?: { showTooltip?: boolean };
|
||||
} | null;
|
||||
if (gridCol) {
|
||||
if (cellTooltipCallbackRef.current) {
|
||||
(
|
||||
gv as {
|
||||
setColumnProperty?: (
|
||||
col: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) => void;
|
||||
}
|
||||
).setColumnProperty?.(
|
||||
column.name,
|
||||
"renderer",
|
||||
{ showTooltip: true },
|
||||
);
|
||||
}
|
||||
gridCol.styleCallback = (_g, dataCell) => {
|
||||
const itemIndex =
|
||||
dataCell?.index?.itemIndex ??
|
||||
dataCell?.itemIndex ??
|
||||
-1;
|
||||
const fieldName =
|
||||
dataCell?.dataField ??
|
||||
dataCell?.fieldName ??
|
||||
column.fieldName;
|
||||
const data = rowDataRef.current;
|
||||
if (
|
||||
itemIndex < 0 ||
|
||||
itemIndex >= data.length
|
||||
)
|
||||
return baseStyleName ?? "";
|
||||
const row = data[itemIndex] as Record<
|
||||
string,
|
||||
string | number
|
||||
>;
|
||||
const extra =
|
||||
cellStyleCallbackRef.current?.({
|
||||
rowIndex: itemIndex,
|
||||
fieldName,
|
||||
row,
|
||||
});
|
||||
if (!extra) return baseStyleName ?? "";
|
||||
return baseStyleName
|
||||
? `${extra} ${baseStyleName}`
|
||||
: extra;
|
||||
};
|
||||
}
|
||||
});
|
||||
gv.refresh();
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
|
||||
@@ -1198,19 +1320,48 @@ const DataGrid = ({
|
||||
key={column.name}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={column.valueType ? column.valueType : ValueType.TEXT}
|
||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
|
||||
valueType={
|
||||
column.valueType
|
||||
? column.valueType
|
||||
: ValueType.TEXT
|
||||
}
|
||||
{...(column.valueType === ValueType.NUMBER && {
|
||||
numberFormat: column.dataFormat
|
||||
? column.dataFormat
|
||||
: "#,##0.000",
|
||||
})}
|
||||
header={column.header}
|
||||
editable={column.editable}
|
||||
{...(baseStyleName != null && { styleName: baseStyleName })}
|
||||
{...(column.visible === false && { visible: false })}
|
||||
footer={column.footer && !Array.isArray(column.footer) ? {
|
||||
text: column.footer.text,
|
||||
expression: column.footer.expression,
|
||||
numberFormat: column.footer.numberFormat,
|
||||
} : undefined}
|
||||
mergeRule={column.mergeCells ? { criteria: 'value' } : undefined}
|
||||
sortable={!column.sortdisable}
|
||||
{...(column.width != null && {
|
||||
width: column.width,
|
||||
})}
|
||||
{...(baseStyleName != null && {
|
||||
styleName: baseStyleName,
|
||||
})}
|
||||
{...(column.visible === false && {
|
||||
visible: false,
|
||||
})}
|
||||
{...(cellTooltipCallback && {
|
||||
renderer: { showTooltip: true },
|
||||
})}
|
||||
footer={
|
||||
column.footer &&
|
||||
!Array.isArray(column.footer)
|
||||
? {
|
||||
text: column.footer.text,
|
||||
expression:
|
||||
column.footer.expression,
|
||||
numberFormat:
|
||||
column.footer.numberFormat,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
mergeRule={
|
||||
column.mergeCells
|
||||
? { criteria: "value" }
|
||||
: undefined
|
||||
}
|
||||
sortable={enableSort && !column.sortdisable}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue
|
||||
SelectValue,
|
||||
Checkbox
|
||||
}
|
||||
from "@common/components";
|
||||
|
||||
@@ -64,10 +65,15 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
order: props.defaultValues?.order || '',
|
||||
fctName: props.defaultValues?.fctName || '',
|
||||
bsnmCd: props.defaultValues?.bsnmCd || '',
|
||||
isDetail: false,
|
||||
});
|
||||
|
||||
const sggKey: [string, string] | null =
|
||||
values.sido && values.sido !== "ALL"
|
||||
? ["sgg-list", values.sido]
|
||||
: null;
|
||||
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
|
||||
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null,
|
||||
sggKey,
|
||||
getSggListFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
@@ -87,7 +93,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const sggOptions = sggList?.data ?? [];
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -102,6 +108,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
order: '',
|
||||
fctName: '',
|
||||
bsnmCd: '',
|
||||
isDetail: false,
|
||||
};
|
||||
setValues(resetValues);
|
||||
props.onReset?.();
|
||||
@@ -109,7 +116,6 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
|
||||
const handleSidoChange = (value: string) => {
|
||||
const selectedBsnm = sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd || '';
|
||||
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
sido: value,
|
||||
@@ -119,9 +125,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
};
|
||||
|
||||
const handleSggChange = (value: string) => {
|
||||
const listForBsnm = sggList?.data ?? [];
|
||||
const selectedBsnm = value === 'ALL'
|
||||
? (sidoList?.data.sidoList.find((v) => v.sido === values.sido)?.bsnmCd || '')
|
||||
: (sggList?.data?.find((v) => v.gugun === value)?.bsnmCd || '');
|
||||
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd || '');
|
||||
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
@@ -169,7 +176,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{sggList?.data?.map((sgg) => (
|
||||
{sggOptions?.map((sgg) => (
|
||||
<SelectItem key={sgg.adstDstrCd} value={sgg.gugun}>
|
||||
{sgg.gugun}
|
||||
</SelectItem>
|
||||
@@ -197,6 +204,19 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.isDetail &&
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
세부 데이터 포함
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={values.isDetail}
|
||||
onCheckedChange={(value) =>
|
||||
setValues({ ...values, isDetail: value === true })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{mergedProps.order &&
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
@@ -225,12 +245,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
<div className="flex items-center p-2">
|
||||
{mergedProps.fctName &&
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
|
||||
<Label htmlFor="fctNmSearchInput" className="mr-2 text-label">
|
||||
시설명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem] mr-4"
|
||||
id="liSnSearchInput"
|
||||
id="fctNmSearchInput"
|
||||
value={values.fctName}
|
||||
onChange={(e) => setValues({ ...values, fctName: e.target.value })}
|
||||
autoComplete="off"
|
||||
|
||||
@@ -11,6 +11,7 @@ const SplitDataGrid = ({
|
||||
rightRowData,
|
||||
setLeftRowData,
|
||||
setRightRowData,
|
||||
enableSort = false,
|
||||
}: SplitDataGridProps) => {
|
||||
return (
|
||||
<div className="flex gap-[0.6rem] mx-[0.6rem]">
|
||||
@@ -18,6 +19,7 @@ const SplitDataGrid = ({
|
||||
columnInfo={leftColumnInfo}
|
||||
fixedColumns={leftFixedColumns}
|
||||
showHeaderCheckbox={showHeaderCheckbox}
|
||||
enableSort={enableSort}
|
||||
rowData={leftRowData}
|
||||
setRowData={setLeftRowData}
|
||||
/>
|
||||
@@ -25,6 +27,7 @@ const SplitDataGrid = ({
|
||||
columnInfo={rightColumnInfo}
|
||||
fixedColumns={rightFixedColumns}
|
||||
showHeaderCheckbox={showHeaderCheckbox}
|
||||
enableSort={enableSort}
|
||||
rowData={rightRowData}
|
||||
setRowData={setRightRowData}
|
||||
/>
|
||||
|
||||
@@ -56,115 +56,23 @@ function toNum(v: unknown): number {
|
||||
return parseFloat(String(v).replace(/,/g, "")) || NaN;
|
||||
}
|
||||
|
||||
/** 헤더에 정렬 화살표 표시 (CSS 방식) */
|
||||
function updateHeaderSortIndicator(
|
||||
gridView: {
|
||||
columnByIndex?: (i: number) => { fieldName?: string; name?: string } | undefined;
|
||||
columnByName?: (name: string) => { fieldName?: string; name?: string } | undefined;
|
||||
getColumn?: (i: number) => { fieldName?: string; name?: string } | undefined;
|
||||
getElement?: () => HTMLElement | null;
|
||||
container?: HTMLElement;
|
||||
},
|
||||
columns: { fieldName?: string; name: string }[],
|
||||
sortDir: Record<string, "asc" | "desc" | "none">,
|
||||
gridElement?: HTMLElement | null
|
||||
) {
|
||||
try {
|
||||
// 그리드 DOM 요소 찾기
|
||||
let gridEl: HTMLElement | null = null;
|
||||
if (gridElement) {
|
||||
gridEl = gridElement;
|
||||
} else if (gridView.getElement) {
|
||||
gridEl = gridView.getElement();
|
||||
} else if (gridView.container) {
|
||||
gridEl = gridView.container;
|
||||
} else {
|
||||
// 폴백: 모든 그리드 중에서 찾기
|
||||
const allGrids = document.querySelectorAll('.rg-grid, [data-grid-id]');
|
||||
if (allGrids.length > 0) {
|
||||
// 활성 탭의 그리드를 찾기 위해 TabsContent 내부의 그리드 찾기
|
||||
const activeTabContent = document.querySelector('[data-state="active"]')?.closest('[role="tabpanel"]');
|
||||
if (activeTabContent) {
|
||||
gridEl = activeTabContent.querySelector('.rg-grid, [data-grid-id]') as HTMLElement;
|
||||
}
|
||||
if (!gridEl && allGrids.length === 1) {
|
||||
gridEl = allGrids[0] as HTMLElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!gridEl) return;
|
||||
|
||||
columns.forEach((col) => {
|
||||
if (!col.fieldName && !col.name) return;
|
||||
|
||||
const fieldName = col.fieldName || col.name;
|
||||
const direction = sortDir[fieldName] || sortDir[col.name] || "none";
|
||||
if (direction === "none") return;
|
||||
|
||||
// 헤더 셀 찾기 (fieldName 또는 name으로)
|
||||
const headerCells = gridEl.querySelectorAll('.rg-header-cell, [class*="header"], .rg-header');
|
||||
let targetCell: HTMLElement | null = null;
|
||||
|
||||
headerCells.forEach((cell) => {
|
||||
const el = cell as HTMLElement;
|
||||
const text = el.textContent?.trim();
|
||||
const colName = col.name;
|
||||
const colFieldName = col.fieldName;
|
||||
// 헤더 텍스트와 컬럼 이름이 일치하는지 확인
|
||||
if (text && (
|
||||
text === colName ||
|
||||
text === colFieldName ||
|
||||
text.includes(colName) ||
|
||||
(colFieldName && text.includes(colFieldName))
|
||||
)) {
|
||||
targetCell = el;
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetCell) return;
|
||||
|
||||
const headerCell = targetCell as HTMLElement;
|
||||
|
||||
// 기존 화살표 제거
|
||||
const existingIndicator = headerCell.querySelector(".rg-sort-indicator") as HTMLElement | null;
|
||||
if (existingIndicator) {
|
||||
existingIndicator.remove();
|
||||
}
|
||||
|
||||
// 화살표 추가
|
||||
const indicator = document.createElement("span");
|
||||
indicator.className = "rg-sort-indicator";
|
||||
indicator.textContent = direction === "asc" ? "▲" : "▼";
|
||||
indicator.style.marginLeft = "4px";
|
||||
indicator.style.fontSize = "10px";
|
||||
indicator.style.color = "#666";
|
||||
indicator.style.display = "inline-block";
|
||||
indicator.style.verticalAlign = "middle";
|
||||
headerCell.appendChild(indicator);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("updateHeaderSortIndicator error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */
|
||||
function createOnSortingHandler(
|
||||
columns: { fieldName?: string; name: string; mergeCells?: boolean; sortdisable?: boolean; valueType?: ValueType }[],
|
||||
setRowData: React.Dispatch<React.SetStateAction<Record<string, string | number>[]>>,
|
||||
sortDirRef: { current: Record<string, "asc" | "desc" | "none"> },
|
||||
originalRowsRef: { current: Record<string, Record<string, string | number>[]> },
|
||||
findGridElement?: () => HTMLElement | null
|
||||
_findGridElement?: () => HTMLElement | null
|
||||
) {
|
||||
const fieldNames = [...new Set(columns.map((c) => c.fieldName).filter((x): x is string => typeof x === "string"))];
|
||||
// mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용
|
||||
// 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용
|
||||
const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName);
|
||||
const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0];
|
||||
const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'sggNm') || mergeCellCols[0];
|
||||
const groupField = mergeCellCol?.fieldName || null;
|
||||
const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음
|
||||
const hasMergeCells = mergeCellCols.length > 0;
|
||||
|
||||
|
||||
return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) {
|
||||
const grid = g as { getColumn?: (i: number) => { fieldName?: string; name?: string } | undefined; getDataSource?: () => { getRowCount(): number; getValue(r: number, f: string): unknown } };
|
||||
const idx = fields?.[0];
|
||||
@@ -178,10 +86,10 @@ function createOnSortingHandler(
|
||||
if (!sortField) return true;
|
||||
const col = columns.find((c) => (c.fieldName === sortField || c.name === sortField));
|
||||
if (!col || col.sortdisable) return true;
|
||||
|
||||
|
||||
// mergeCells: true인 컬럼이 없으면 RealGrid 기본 정렬 사용
|
||||
if (!hasMergeCells || !groupField) return true;
|
||||
|
||||
|
||||
const ds = grid.getDataSource?.();
|
||||
if (!ds) return true;
|
||||
const cnt = ds.getRowCount();
|
||||
@@ -229,7 +137,7 @@ function sortRowsGroupPreserving(
|
||||
if (!groups.has(k)) groups.set(k, []);
|
||||
groups.get(k)!.push(r);
|
||||
}
|
||||
|
||||
|
||||
// valueType에 따라 비교 함수 선택
|
||||
const compareValue = (val: unknown): number | string => {
|
||||
if (valueType === ValueType.NUMBER) {
|
||||
@@ -238,7 +146,7 @@ function sortRowsGroupPreserving(
|
||||
// 텍스트나 기타 타입은 문자열로 비교
|
||||
return String(val ?? "");
|
||||
};
|
||||
|
||||
|
||||
const rep = (arr: Record<string, string | number>[]): number | string => {
|
||||
const values = arr.map((r) => compareValue(r[sortField]));
|
||||
if (valueType === ValueType.NUMBER) {
|
||||
@@ -253,7 +161,7 @@ function sortRowsGroupPreserving(
|
||||
return sortAsc ? (strs[0] as string) : (strs[strs.length - 1] as string);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const entries = [...groups.entries()].sort((a, b) => {
|
||||
const valA = rep(a[1]);
|
||||
const valB = rep(b[1]);
|
||||
@@ -265,7 +173,7 @@ function sortRowsGroupPreserving(
|
||||
const strB = String(valB);
|
||||
return (sortAsc ? 1 : -1) * strA.localeCompare(strB);
|
||||
});
|
||||
|
||||
|
||||
return entries.flatMap(([, arr]) => arr);
|
||||
}
|
||||
|
||||
@@ -306,9 +214,10 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
|
||||
};
|
||||
}
|
||||
|
||||
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange }: TabDataGridProps) => {
|
||||
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange, getGridDataRef, enableSort = false }: TabDataGridProps) => {
|
||||
// 각 탭별로 그리드 ref와 데이터를 관리
|
||||
const gridRefs = useRef<Map<string, RealGridReact>>(new Map());
|
||||
const activeTabRef = useRef<string>("");
|
||||
const fileInputRefs = useRef<Map<string, HTMLInputElement>>(new Map());
|
||||
// 각 탭별로 정렬 상태 관리
|
||||
const sortDirRefs = useRef<Map<string, React.MutableRefObject<Record<string, "asc" | "desc" | "none">>>>(new Map());
|
||||
@@ -323,7 +232,8 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
});
|
||||
const isControlled = activeTabProp !== undefined;
|
||||
const activeTab = isControlled ? activeTabProp : internalActiveTab;
|
||||
const setActiveTab = isControlled ? (onActiveTabChange ?? (() => {})) : setInternalActiveTab;
|
||||
const setActiveTab = isControlled ? (onActiveTabChange ?? (() => { })) : setInternalActiveTab;
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const exportExcel = (tabKey: string) => {
|
||||
const currentRowData = rowDataMap.get(tabKey) || [];
|
||||
@@ -345,7 +255,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
|
||||
const columnInfoItem = columnInfo.find(info => info.tabKey === tabKey);
|
||||
if (!columnInfoItem) return;
|
||||
|
||||
|
||||
const tabLabel = columnInfoItem.tabLabel || tabKey;
|
||||
const columns = columnInfoItem.columns || [];
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
@@ -419,6 +329,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
target: 'local',
|
||||
fileName: `${menuName}_${tabLabel}.xlsx`,
|
||||
footer: 'hidden',
|
||||
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
@@ -519,7 +430,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
const columns = columnInfoItem.columns || [];
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
@@ -528,13 +439,13 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
|
||||
@@ -655,6 +566,41 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
});
|
||||
}, [rowDataMap, columnInfo]);
|
||||
|
||||
// getGridDataRef: 현재 활성 탭 그리드 데이터 반환 함수 할당
|
||||
useEffect(() => {
|
||||
if (!getGridDataRef) return;
|
||||
getGridDataRef.current = () => {
|
||||
const tabKey = activeTabRef.current;
|
||||
const gridRef = gridRefs.current.get(tabKey);
|
||||
const gv = gridRef?.gridView as
|
||||
| {
|
||||
getDataSource?: () => {
|
||||
getRowCount(): number;
|
||||
getValue(r: number, f: string): unknown;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
if (!gv?.getDataSource) return [];
|
||||
const ds = gv.getDataSource();
|
||||
if (!ds) return [];
|
||||
const currentInfo = columnInfo.find((info) => info.tabKey === tabKey);
|
||||
const columns = currentInfo?.columns ?? [];
|
||||
const fieldNames = columns
|
||||
.map((c) => c.fieldName)
|
||||
.filter((x): x is string => Boolean(x));
|
||||
const cnt = ds.getRowCount();
|
||||
const rows: Record<string, string | number>[] = [];
|
||||
for (let i = 0; i < cnt; i++) {
|
||||
const r: Record<string, string | number> = {};
|
||||
for (const f of fieldNames) r[f] = ds.getValue(i, f) as string | number;
|
||||
rows.push(r);
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
return () => {
|
||||
if (getGridDataRef) getGridDataRef.current = null;
|
||||
};
|
||||
}, [columnInfo, getGridDataRef]);
|
||||
|
||||
// 탭 전환 시 그리드 레이아웃·푸터·포맷 재적용 (여러 시점 + ResizeObserver)
|
||||
const appliedTabRef = useRef<string | null>(null);
|
||||
@@ -708,7 +654,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
...(col.styleName != null && { styleName: col.styleName }),
|
||||
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
|
||||
...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }),
|
||||
};
|
||||
if (col.footer) {
|
||||
if (Array.isArray(col.footer)) c.footer = col.footer;
|
||||
@@ -727,7 +673,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
if (item.valueCallback && footerArr[i])
|
||||
(footerArr[i] as FooterItemWithCallback).valueCallback = item.valueCallback;
|
||||
});
|
||||
} catch (_) {}
|
||||
} catch (_) { }
|
||||
});
|
||||
}
|
||||
columns.forEach(col => {
|
||||
@@ -737,7 +683,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
const fo = col.footer as { valueCallback?: (g: unknown, col: unknown, fi: number, cf: unknown, v: unknown) => unknown };
|
||||
if (column?.footer && fo.valueCallback)
|
||||
(column.footer as { valueCallback?: typeof fo.valueCallback }).valueCallback = fo.valueCallback;
|
||||
} catch (_) {}
|
||||
} catch (_) { }
|
||||
});
|
||||
|
||||
if (dp?.setRows && tabRowData.length) dp.setRows(tabRowData);
|
||||
@@ -772,8 +718,8 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" variant="outline">
|
||||
<TabsList className="mb-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
const tabLabel = info.tabLabel || tabKey;
|
||||
@@ -787,7 +733,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
|
||||
|
||||
// isSplitGrid가 true이고 left, right가 있으면 SplitDataGrid 사용
|
||||
if (info.isSplitGrid && info.left && info.right) {
|
||||
const leftRowData = rowDataMap.get(info.left.tabKey || `${tabKey}_left`) || [];
|
||||
@@ -811,6 +757,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
leftFixedColumns={info.left.fixedColumns}
|
||||
rightFixedColumns={info.right.fixedColumns}
|
||||
showHeaderCheckbox={showHeaderCheckbox}
|
||||
enableSort={enableSort}
|
||||
leftRowData={leftRowData}
|
||||
rightRowData={rightRowData}
|
||||
setLeftRowData={(updater) => {
|
||||
@@ -885,277 +832,277 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
</div>
|
||||
<div className="h-[60vh]" data-tab-key={tabKey}>
|
||||
{activeTab === tabKey ? (
|
||||
<RealGridReact
|
||||
key={tabKey}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
gridRefs.current.set(tabKey, el);
|
||||
// 그리드가 생성된 직후 fitStyle 설정
|
||||
if (el.gridView) {
|
||||
setTimeout(() => {
|
||||
const gridView = el.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
const currentInfo = columnInfo.find(info => info.tabKey === tabKey);
|
||||
if (!currentInfo || !currentInfo.columns) return;
|
||||
|
||||
const columns = currentInfo.columns;
|
||||
|
||||
const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record<string, string | number>[]) => void } | undefined;
|
||||
if (dp?.setFields) {
|
||||
dp.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
// setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지)
|
||||
const tabRowData = rowDataMap.get(tabKey) || [];
|
||||
if (dp?.setRows && tabRowData.length) {
|
||||
dp.setRows(tabRowData);
|
||||
}
|
||||
|
||||
gridView.setDisplayOptions({
|
||||
fitStyle: GridFitStyle.EVEN
|
||||
});
|
||||
gridView.header.height = 60;
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
checkBar: { visible: showHeaderCheckbox },
|
||||
rowIndicator: { visible: false },
|
||||
});
|
||||
|
||||
// footer가 있는 컬럼이 있으면 footer 높이 설정
|
||||
const hasFooter = columns.some(col => col.footer);
|
||||
if (hasFooter) {
|
||||
// footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행)
|
||||
const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer));
|
||||
const hasMultiRowFooter = columns.some(col => {
|
||||
const footer = col.footer;
|
||||
return footer && !Array.isArray(footer) && 'multiRow' in footer && footer.multiRow === true;
|
||||
});
|
||||
if (hasArrayFooter || hasMultiRowFooter) {
|
||||
// footer 배열의 길이를 확인하여 행 수 결정
|
||||
const arrayFooterLengths = columns
|
||||
.filter(col => col.footer && Array.isArray(col.footer))
|
||||
.map(col => (col.footer as unknown[]).length);
|
||||
const maxFooterLength = arrayFooterLengths.length > 0
|
||||
? Math.max(...arrayFooterLengths)
|
||||
: 2; // 기본값 2
|
||||
const footerHeights = Array.from({ length: maxFooterLength }, () => ({ height: 40 }));
|
||||
gridView.setFooters?.(footerHeights);
|
||||
} else {
|
||||
gridView.footer.height = 40;
|
||||
}
|
||||
}
|
||||
|
||||
gridView.setOptions({
|
||||
summaryMode: "aggregate"
|
||||
});
|
||||
|
||||
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
|
||||
const checkFooterUserSpans = (layout: unknown[]): boolean => {
|
||||
return layout.some(item => {
|
||||
if (typeof item === 'string') return false;
|
||||
if (item && typeof item === 'object' && 'footerUserSpans' in item) return true;
|
||||
if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) {
|
||||
return checkFooterUserSpans(item.items);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (checkFooterUserSpans(columnLayout)) {
|
||||
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
|
||||
setTimeout(() => {
|
||||
if (gridView && columnLayout) {
|
||||
gridView.setColumnLayout?.(columnLayout);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// footer 설정 (배열 또는 단일 객체 모두 처리)
|
||||
// setFooters 호출 후 footer 배열 설정
|
||||
<RealGridReact
|
||||
key={tabKey}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
gridRefs.current.set(tabKey, el);
|
||||
// 그리드가 생성된 직후 fitStyle 설정
|
||||
if (el.gridView) {
|
||||
setTimeout(() => {
|
||||
const currentGridView = gridView;
|
||||
if (!currentGridView || !currentGridView.columnByName) {
|
||||
console.warn('GridView not available for footer setup');
|
||||
return;
|
||||
const gridView = el.gridView;
|
||||
if (!gridView) return;
|
||||
|
||||
const currentInfo = columnInfo.find(info => info.tabKey === tabKey);
|
||||
if (!currentInfo || !currentInfo.columns) return;
|
||||
|
||||
const columns = currentInfo.columns;
|
||||
|
||||
const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record<string, string | number>[]) => void } | undefined;
|
||||
if (dp?.setFields) {
|
||||
dp.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
|
||||
// footer가 배열인 컬럼이 있으면 setColumns를 다시 호출하여 footer 설정
|
||||
const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer));
|
||||
if (hasArrayFooter) {
|
||||
// 모든 컬럼 정보를 다시 가져와서 footer 배열을 포함하여 setColumns 호출
|
||||
const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => {
|
||||
const colInfo: RealGridColumnInfo = {
|
||||
name: col.name,
|
||||
fieldName: col.fieldName,
|
||||
header: col.header,
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
...(col.styleName != null && { styleName: col.styleName }),
|
||||
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
|
||||
};
|
||||
|
||||
if (col.footer) {
|
||||
if (Array.isArray(col.footer)) {
|
||||
colInfo.footer = col.footer;
|
||||
} else {
|
||||
colInfo.footer = {
|
||||
text: col.footer.text,
|
||||
expression: col.footer.expression,
|
||||
numberFormat: col.footer.numberFormat,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return colInfo;
|
||||
// setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지)
|
||||
const tabRowData = rowDataMap.get(tabKey) || [];
|
||||
if (dp?.setRows && tabRowData.length) {
|
||||
dp.setRows(tabRowData);
|
||||
}
|
||||
|
||||
gridView.setDisplayOptions({
|
||||
fitStyle: GridFitStyle.EVEN
|
||||
});
|
||||
gridView.header.height = 60;
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
checkBar: { visible: showHeaderCheckbox },
|
||||
rowIndicator: { visible: false },
|
||||
});
|
||||
|
||||
// footer가 있는 컬럼이 있으면 footer 높이 설정
|
||||
const hasFooter = columns.some(col => col.footer);
|
||||
if (hasFooter) {
|
||||
// footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행)
|
||||
const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer));
|
||||
const hasMultiRowFooter = columns.some(col => {
|
||||
const footer = col.footer;
|
||||
return footer && !Array.isArray(footer) && 'multiRow' in footer && footer.multiRow === true;
|
||||
});
|
||||
|
||||
try {
|
||||
(currentGridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter);
|
||||
|
||||
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
|
||||
setTimeout(() => {
|
||||
columns.forEach(col => {
|
||||
if (!col.footer || !Array.isArray(col.footer)) return;
|
||||
|
||||
try {
|
||||
const column = currentGridView.columnByName(col.name);
|
||||
if (!column) return;
|
||||
|
||||
// footer 배열의 각 요소에 valueCallback 설정
|
||||
col.footer.forEach((footerItem, index) => {
|
||||
if (footerItem.valueCallback && column.footer) {
|
||||
// footer가 배열인 경우
|
||||
if (Array.isArray(column.footer) && column.footer[index]) {
|
||||
(column.footer[index] as FooterItemWithCallback).valueCallback = footerItem.valueCallback;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Error setting footer array valueCallback for column ${col.name}:`, e);
|
||||
if (hasArrayFooter || hasMultiRowFooter) {
|
||||
// footer 배열의 길이를 확인하여 행 수 결정
|
||||
const arrayFooterLengths = columns
|
||||
.filter(col => col.footer && Array.isArray(col.footer))
|
||||
.map(col => (col.footer as unknown[]).length);
|
||||
const maxFooterLength = arrayFooterLengths.length > 0
|
||||
? Math.max(...arrayFooterLengths)
|
||||
: 2; // 기본값 2
|
||||
const footerHeights = Array.from({ length: maxFooterLength }, () => ({ height: 40 }));
|
||||
gridView.setFooters?.(footerHeights);
|
||||
} else {
|
||||
gridView.footer.height = 40;
|
||||
}
|
||||
}
|
||||
|
||||
gridView.setOptions({
|
||||
summaryMode: "aggregate"
|
||||
});
|
||||
|
||||
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
|
||||
const checkFooterUserSpans = (layout: unknown[]): boolean => {
|
||||
return layout.some(item => {
|
||||
if (typeof item === 'string') return false;
|
||||
if (item && typeof item === 'object' && 'footerUserSpans' in item) return true;
|
||||
if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) {
|
||||
return checkFooterUserSpans(item.items);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (checkFooterUserSpans(columnLayout)) {
|
||||
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
|
||||
setTimeout(() => {
|
||||
if (gridView && columnLayout) {
|
||||
gridView.setColumnLayout?.(columnLayout);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// footer 설정 (배열 또는 단일 객체 모두 처리)
|
||||
// setFooters 호출 후 footer 배열 설정
|
||||
setTimeout(() => {
|
||||
const currentGridView = gridView;
|
||||
if (!currentGridView || !currentGridView.columnByName) {
|
||||
console.warn('GridView not available for footer setup');
|
||||
return;
|
||||
}
|
||||
|
||||
// footer가 배열인 컬럼이 있으면 setColumns를 다시 호출하여 footer 설정
|
||||
const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer));
|
||||
if (hasArrayFooter) {
|
||||
// 모든 컬럼 정보를 다시 가져와서 footer 배열을 포함하여 setColumns 호출
|
||||
const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => {
|
||||
const colInfo: RealGridColumnInfo = {
|
||||
name: col.name,
|
||||
fieldName: col.fieldName,
|
||||
header: col.header,
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
...(col.styleName != null && { styleName: col.styleName }),
|
||||
...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }),
|
||||
};
|
||||
|
||||
if (col.footer) {
|
||||
if (Array.isArray(col.footer)) {
|
||||
colInfo.footer = col.footer;
|
||||
} else {
|
||||
colInfo.footer = {
|
||||
text: col.footer.text,
|
||||
expression: col.footer.expression,
|
||||
numberFormat: col.footer.numberFormat,
|
||||
};
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
console.warn('Failed to set columns with footer array:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// footer가 단일 객체이고 valueCallback이 있는 경우
|
||||
columns.forEach(col => {
|
||||
if (!col.footer || Array.isArray(col.footer)) return;
|
||||
|
||||
try {
|
||||
const column = currentGridView.columnByName(col.name);
|
||||
if (!column || !column.footer) return;
|
||||
|
||||
const footerObj = col.footer as { valueCallback?: (grid: unknown, column: unknown, footerIndex: number, columnFooter: unknown, value: unknown) => unknown };
|
||||
if (footerObj.valueCallback) {
|
||||
(column.footer as { valueCallback?: typeof footerObj.valueCallback }).valueCallback = footerObj.valueCallback;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error setting footer valueCallback for column ${col.name}:`, e);
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// 고정 컬럼 설정
|
||||
if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) {
|
||||
gridView.setFixedOptions({
|
||||
colCount: currentInfo.fixedColumns,
|
||||
});
|
||||
}
|
||||
|
||||
// 각 탭별 ref 초기화
|
||||
if (!sortDirRefs.current.has(tabKey)) {
|
||||
sortDirRefs.current.set(tabKey, { current: {} });
|
||||
}
|
||||
if (!originalRowsRefs.current.has(tabKey)) {
|
||||
originalRowsRefs.current.set(tabKey, { current: {} });
|
||||
}
|
||||
|
||||
const sortDirRef = sortDirRefs.current.get(tabKey)!;
|
||||
const originalRowsRef = originalRowsRefs.current.get(tabKey)!;
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
|
||||
const hasMergeCells = columns.some((col) => col.mergeCells === true);
|
||||
|
||||
// setDataComparer로 숫자 정렬 (mergeCells가 있으면 onSorting에서 클라이언트 정렬로 처리)
|
||||
if (el.dataProvider) {
|
||||
const dp = el.dataProvider as RealGridDataProviderComparer;
|
||||
if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") {
|
||||
const numberComparer = createNumberComparer(dp);
|
||||
const seen = new Set<string>();
|
||||
columns.forEach((col) => {
|
||||
if (hasMergeCells) return; // mergeCells가 있으면 모든 컬럼을 클라이언트 정렬로 처리
|
||||
if (col.valueType === ValueType.NUMBER && col.fieldName && !seen.has(col.fieldName)) {
|
||||
seen.add(col.fieldName);
|
||||
try {
|
||||
dp.setDataComparer!(col.fieldName, numberComparer);
|
||||
} catch (e) {
|
||||
console.warn("setDataComparer:", e);
|
||||
}
|
||||
|
||||
return colInfo;
|
||||
});
|
||||
|
||||
try {
|
||||
(currentGridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter);
|
||||
|
||||
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
|
||||
setTimeout(() => {
|
||||
columns.forEach(col => {
|
||||
if (!col.footer || !Array.isArray(col.footer)) return;
|
||||
|
||||
try {
|
||||
const column = currentGridView.columnByName(col.name);
|
||||
if (!column) return;
|
||||
|
||||
// footer 배열의 각 요소에 valueCallback 설정
|
||||
col.footer.forEach((footerItem, index) => {
|
||||
if (footerItem.valueCallback && column.footer) {
|
||||
// footer가 배열인 경우
|
||||
if (Array.isArray(column.footer) && column.footer[index]) {
|
||||
(column.footer[index] as FooterItemWithCallback).valueCallback = footerItem.valueCallback;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Error setting footer array valueCallback for column ${col.name}:`, e);
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
console.warn('Failed to set columns with footer array:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// footer가 단일 객체이고 valueCallback이 있는 경우
|
||||
columns.forEach(col => {
|
||||
if (!col.footer || Array.isArray(col.footer)) return;
|
||||
|
||||
try {
|
||||
const column = currentGridView.columnByName(col.name);
|
||||
if (!column || !column.footer) return;
|
||||
|
||||
const footerObj = col.footer as { valueCallback?: (grid: unknown, column: unknown, footerIndex: number, columnFooter: unknown, value: unknown) => unknown };
|
||||
if (footerObj.valueCallback) {
|
||||
(column.footer as { valueCallback?: typeof footerObj.valueCallback }).valueCallback = footerObj.valueCallback;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error setting footer valueCallback for column ${col.name}:`, e);
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
|
||||
// 고정 컬럼 설정
|
||||
if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) {
|
||||
gridView.setFixedOptions({
|
||||
colCount: currentInfo.fixedColumns,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 그룹 내 순서 유지 정렬 적용
|
||||
const setRowData = (updater: React.SetStateAction<Record<string, string | number>[]>) => {
|
||||
setRowDataMap(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const currentData = newMap.get(tabKey) || [];
|
||||
const newData = typeof updater === 'function' ? updater(currentData) : updater;
|
||||
newMap.set(tabKey, newData);
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
|
||||
// 그리드 DOM 요소 찾기 (나중에 화살표 업데이트에 사용)
|
||||
// 탭별로 그리드 요소를 찾기 위해 data-tab-key 사용
|
||||
const findGridElement = () => {
|
||||
const tabContent = document.querySelector(`[data-tab-key="${tabKey}"]`);
|
||||
return tabContent?.querySelector('.rg-grid, [data-grid-id]') as HTMLElement | null;
|
||||
};
|
||||
|
||||
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
|
||||
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler;
|
||||
}, 150);
|
||||
|
||||
// 각 탭별 ref 초기화
|
||||
if (!sortDirRefs.current.has(tabKey)) {
|
||||
sortDirRefs.current.set(tabKey, { current: {} });
|
||||
}
|
||||
if (!originalRowsRefs.current.has(tabKey)) {
|
||||
originalRowsRefs.current.set(tabKey, { current: {} });
|
||||
}
|
||||
|
||||
const sortDirRef = sortDirRefs.current.get(tabKey)!;
|
||||
const originalRowsRef = originalRowsRefs.current.get(tabKey)!;
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
|
||||
const hasMergeCells = columns.some((col) => col.mergeCells === true);
|
||||
|
||||
// setDataComparer로 숫자 정렬 (mergeCells가 있으면 onSorting에서 클라이언트 정렬로 처리)
|
||||
if (el.dataProvider) {
|
||||
const dp = el.dataProvider as RealGridDataProviderComparer;
|
||||
if (typeof dp.setDataComparer === "function" && typeof dp.getValue === "function") {
|
||||
const numberComparer = createNumberComparer(dp);
|
||||
const seen = new Set<string>();
|
||||
columns.forEach((col) => {
|
||||
if (hasMergeCells) return; // mergeCells가 있으면 모든 컬럼을 클라이언트 정렬로 처리
|
||||
if (col.valueType === ValueType.NUMBER && col.fieldName && !seen.has(col.fieldName)) {
|
||||
seen.add(col.fieldName);
|
||||
try {
|
||||
dp.setDataComparer!(col.fieldName, numberComparer);
|
||||
} catch (e) {
|
||||
console.warn("setDataComparer:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 그룹 내 순서 유지 정렬 적용
|
||||
const setRowData = (updater: React.SetStateAction<Record<string, string | number>[]>) => {
|
||||
setRowDataMap(prev => {
|
||||
const newMap = new Map(prev);
|
||||
const currentData = newMap.get(tabKey) || [];
|
||||
const newData = typeof updater === 'function' ? updater(currentData) : updater;
|
||||
newMap.set(tabKey, newData);
|
||||
return newMap;
|
||||
});
|
||||
};
|
||||
|
||||
// 그리드 DOM 요소 찾기 (나중에 화살표 업데이트에 사용)
|
||||
// 탭별로 그리드 요소를 찾기 위해 data-tab-key 사용
|
||||
const findGridElement = () => {
|
||||
const tabContent = document.querySelector(`[data-tab-key="${tabKey}"]`);
|
||||
return tabContent?.querySelector('.rg-grid, [data-grid-id]') as HTMLElement | null;
|
||||
};
|
||||
|
||||
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
|
||||
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false;
|
||||
}, 150);
|
||||
}
|
||||
} else {
|
||||
gridRefs.current.delete(tabKey);
|
||||
}
|
||||
} else {
|
||||
gridRefs.current.delete(tabKey);
|
||||
}
|
||||
}}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={currentRowData}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<RGDataColumn
|
||||
key={column.name}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={column.valueType ? column.valueType : ValueType.TEXT}
|
||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
|
||||
header={column.header}
|
||||
editable={column.editable}
|
||||
{...(column.styleName != null
|
||||
? { styleName: column.styleName }
|
||||
: column.editable === false
|
||||
? { styleName: 'rg-cell-readonly' as const }
|
||||
: {})}
|
||||
{...(column.visible === false && { visible: false })}
|
||||
footer={column.footer && !Array.isArray(column.footer) ? {
|
||||
text: (column.footer as { text?: string }).text,
|
||||
expression: (column.footer as { expression?: 'sum' | 'avg' | 'count' | 'max' | 'min' }).expression,
|
||||
numberFormat: (column.footer as { numberFormat?: string }).numberFormat,
|
||||
} : undefined}
|
||||
mergeRule={column.mergeCells ? { criteria: 'value' } : undefined}
|
||||
sortable={!column.sortdisable}
|
||||
/>
|
||||
))}
|
||||
</RealGridReact>
|
||||
}}
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={currentRowData}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<RGDataColumn
|
||||
key={column.name}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={column.valueType ? column.valueType : ValueType.TEXT}
|
||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
|
||||
header={column.header}
|
||||
editable={column.editable}
|
||||
{...(column.styleName != null
|
||||
? { styleName: column.styleName }
|
||||
: column.editable === false
|
||||
? { styleName: 'rg-cell-readonly' as const }
|
||||
: {})}
|
||||
{...(column.visible === false && { visible: false })}
|
||||
footer={column.footer && !Array.isArray(column.footer) ? {
|
||||
text: (column.footer as { text?: string }).text,
|
||||
expression: (column.footer as { expression?: 'sum' | 'avg' | 'count' | 'max' | 'min' }).expression,
|
||||
numberFormat: (column.footer as { numberFormat?: string }).numberFormat,
|
||||
} : undefined}
|
||||
mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined}
|
||||
sortable={enableSort && !column.sortdisable}
|
||||
/>
|
||||
))}
|
||||
</RealGridReact>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,87 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { TabDataGrid } from '@rsone/components';
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
|
||||
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
|
||||
import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn";
|
||||
import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn";
|
||||
import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn";
|
||||
import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn";
|
||||
import { ConnectionStatusInpColumn } from "@/components/util/column/input/sewagePipe/ConnectionStatusInpColumn";
|
||||
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
|
||||
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
// 하수도 통계 관리 > 02. 하수관로
|
||||
// 하수도 통계 관리 > 02. 하수관로
|
||||
const SewagePipeInp = () => {
|
||||
|
||||
// tab List
|
||||
const SewagePipeInpTapList = [
|
||||
RegionalFactStatusInpColumn,
|
||||
FactInstallStatusInpColumn,
|
||||
PipeTypeStatusInpColumn,
|
||||
PipeDiameterStatusInpColumn,
|
||||
ManholeStatusInpColumn,
|
||||
ConnectionStatusInpColumn,
|
||||
];
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [_isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
|
||||
const [isEnterable, _setIsEnterable] = useState<boolean>(true);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const canEdit = isEnterable || isAdminInfo;
|
||||
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
|
||||
const applyEditable = (cols: ColumnConfig[] | undefined) =>
|
||||
(cols ?? []).map((col: ColumnConfig) =>
|
||||
canEdit
|
||||
? col
|
||||
: { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" }
|
||||
);
|
||||
|
||||
if (tab.left && tab.right) {
|
||||
return {
|
||||
...tab,
|
||||
left: { ...tab.left, columns: applyEditable(tab.left.columns) },
|
||||
right: { ...tab.right, columns: applyEditable(tab.right.columns) },
|
||||
};
|
||||
}
|
||||
return {
|
||||
...tab,
|
||||
columns: applyEditable(tab.columns),
|
||||
};
|
||||
});
|
||||
}, [isEnterable, isAdminInfo]);
|
||||
|
||||
// const handleSearch = async (values: SearchFormValues) => {
|
||||
// if (!values.sido || values.sido === 'ALL') {
|
||||
// alert('시도를 선택해주세요.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setActiveTab('ST00020001');
|
||||
|
||||
// setRowDataMap(new Map());
|
||||
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
|
||||
// // const searchParam: SewagePipeReviewRequestParams = {
|
||||
// // statsYr: values.year,
|
||||
// // sewerBzmnCd: values.bsnmCd,
|
||||
// // ctpvNm: values.sido,
|
||||
// // sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
// // };
|
||||
|
||||
// // searchParamRef.current = searchParam;
|
||||
// // const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam);
|
||||
|
||||
// // if (response?.status === 'success' && response.data) {
|
||||
// // const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
// // newRowDataMap.set('ST00020001', toGridRows(response.data as object[]));
|
||||
// // setRowDataMap(newRowDataMap);
|
||||
// // } else {
|
||||
// // alert(response?.message || "데이터가 없습니다.");
|
||||
// // }
|
||||
// } catch {
|
||||
// alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
@@ -47,7 +103,6 @@ const SewagePipeInp = () => {
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -60,10 +115,12 @@ const SewagePipeInp = () => {
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
<TabDataGrid
|
||||
columnInfo={SewagePipeInpTapList}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,34 +7,53 @@ import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import SubmitDailog from "./SubmitDailog";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { getFooterSummary } from "@/components/util/grid/footerSummary";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import type {
|
||||
SewerageSupplyRateItem,
|
||||
SewerageSupplyRateRequestParams,
|
||||
SewerageSupplyRateSaveItem,
|
||||
SewerageSupplyRateVerificationResult,
|
||||
SewerageSupplyRateSaveRequest
|
||||
} from "@/entities/statistics/model/statistics-input-interface";
|
||||
SewerageSupplyRateSaveRequest,
|
||||
SewerageSupplyRateStatusInfo,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import type { cancelSubmitRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { getFooterSummary } from "@/components/util/grid/footerSummary";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageSupplyRateVerificationResult;
|
||||
};
|
||||
|
||||
const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
||||
if (!verification) return false;
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
};
|
||||
|
||||
function getSewerageSupplyRateParams(
|
||||
values: SearchFormValues,
|
||||
overrides?: { yr?: string; demoYr?: string; mode?: string }
|
||||
): SewerageSupplyRateRequestParams {
|
||||
return {
|
||||
sido: values.sido,
|
||||
gugun: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
bsnmCd: values.bsnmCd,
|
||||
yr: overrides?.yr ?? values.year,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: overrides?.yr ?? values.year,
|
||||
...(overrides?.mode && { mode: overrides.mode }),
|
||||
};
|
||||
}
|
||||
@@ -47,13 +66,19 @@ const SeweragePenetrationRateInp = () => {
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [isEnterable, setIsEnterable] = useState<boolean>(true);
|
||||
const [lastSearchedYear, setLastSearchedYear] = useState<string | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
|
||||
const [showDailog, setShowDailog] = useState<boolean>(false);
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number } | null>(null);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
|
||||
const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm'] as const;
|
||||
const footerWithApiDefault = (fieldName: (typeof footerFieldNames)[number], numberFormat: string) => ({
|
||||
const _footerFieldNames = ['sewerBzmnWholPpltnCnt', 'sewerBzmnDomplWholPpltnCnt', 'sewerBzmnFrgnrWholPpltnCnt', 'sewerBzmnArea'] as const;
|
||||
const footerWithApiDefault = (fieldName: (typeof _footerFieldNames)[number], numberFormat: string) => ({
|
||||
expression: 'sum' as const,
|
||||
numberFormat,
|
||||
valueCallback: (grid: Parameters<typeof getFooterSummary>[0], column: Parameters<typeof getFooterSummary>[1]) =>
|
||||
@@ -63,59 +88,79 @@ const SeweragePenetrationRateInp = () => {
|
||||
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
|
||||
const base = isEnterable ? columns : columns.map((col) => ({ ...col, editable: false as const }));
|
||||
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
|
||||
|
||||
return {
|
||||
columnLayout,
|
||||
columns: base.map((col) => {
|
||||
if (col.fieldName === 'totalPopl') return { ...col, footer: footerWithApiDefault('totalPopl', '#,##0') };
|
||||
if (col.fieldName === 'poplSm') return { ...col, footer: footerWithApiDefault('poplSm', '#,##0') };
|
||||
if (col.fieldName === 'frgnrPoplSm') return { ...col, footer: footerWithApiDefault('frgnrPoplSm', '#,##0') };
|
||||
if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') };
|
||||
if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') };
|
||||
if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') };
|
||||
// 그리드 필드명은 refArea(행정참고자료면적). API commonInfo.sewerBzmnArea와 매핑
|
||||
if (col.fieldName === 'refArea') return { ...col, footer: footerWithApiDefault('sewerBzmnArea', '#,##0.000') };
|
||||
return col;
|
||||
}),
|
||||
};
|
||||
}, [isEnterable, footerFromApi]);
|
||||
}, [isEnterable, isAdminInfo, footerFromApi]);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
setLastSearchedYear(null);
|
||||
setLastSearchedValues(null);
|
||||
setFooterFromApi(null);
|
||||
};
|
||||
|
||||
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => {
|
||||
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsInputAPi.getSewerageSupplyRateList(params);
|
||||
const response = await statisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params);
|
||||
if (response.status === 'success' && response.data) {
|
||||
const commonInfo = response.data.commonInfo;
|
||||
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({
|
||||
const statusInfo = response.data.statusInfo;
|
||||
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d: SewerageSupplyRateItem) => ({
|
||||
...d,
|
||||
sido: commonInfo.sido,
|
||||
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
|
||||
piplSupprt: commonInfo.piplSupprt,
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn,
|
||||
swagePplPsplyRt: commonInfo.swagePplPsplyRt,
|
||||
}));
|
||||
setRowData(
|
||||
convertedData.map((d) => {
|
||||
const row = toGridRows([d])[0] as Record<string, string | number> & { verificationResult?: SewerageSupplyRateVerificationResult };
|
||||
const row = toGridRows([d])[0] as GridRowWithVerification;
|
||||
if (d.verificationResult) row.verificationResult = d.verificationResult;
|
||||
return row;
|
||||
})
|
||||
);
|
||||
|
||||
setStatusInfo(statusInfo);
|
||||
setFooterFromApi(
|
||||
commonInfo
|
||||
? {
|
||||
totalPopl: commonInfo.bsnmTotalPopl,
|
||||
poplSm: commonInfo.bsnmPoplSm,
|
||||
frgnrPoplSm: commonInfo.bsnmFrgnrPoplSm,
|
||||
sewerBzmnWholPpltnCnt: commonInfo.sewerBzmnWholPpltnCnt,
|
||||
sewerBzmnDomplWholPpltnCnt: commonInfo.sewerBzmnDomplWholPpltnCnt,
|
||||
sewerBzmnFrgnrWholPpltnCnt: commonInfo.sewerBzmnFrgnrWholPpltnCnt,
|
||||
sewerBzmnArea: commonInfo.sewerBzmnArea,
|
||||
}
|
||||
: null
|
||||
);
|
||||
setStatsCd(commonInfo.statsCd);
|
||||
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
|
||||
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
|
||||
setIsEnterable(false);
|
||||
} else {
|
||||
setIsEnterable(isEnterable);
|
||||
}
|
||||
} else {
|
||||
setIsEnterable(isEnterable);
|
||||
}
|
||||
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
setStatusInfo(null);
|
||||
setFooterFromApi(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -129,13 +174,14 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await statisticsInputAPi.checkInputPeriod(values.year || '');
|
||||
const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || '');
|
||||
|
||||
if (response.status === 'success') {
|
||||
setIsEnterable(response.data);
|
||||
const checkEnterable = response.data;
|
||||
// setIsEnterable(response.data);
|
||||
setLastSearchedYear(values.year || null);
|
||||
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values));
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable);
|
||||
setLastSearchedValues(values);
|
||||
} else {
|
||||
alert(response.message);
|
||||
}
|
||||
@@ -145,7 +191,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
// 예외 제출 로직
|
||||
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = async (values: SearchFormValues) => {
|
||||
@@ -160,26 +206,145 @@ const SeweragePenetrationRateInp = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }));
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true);
|
||||
};
|
||||
|
||||
const submitFunc = () => {
|
||||
// 제출 로직
|
||||
const submitFunc = async () => {
|
||||
const isError = rowData.filter((d) =>
|
||||
hasVerificationErrors((d as GridRowWithVerification).verificationResult)
|
||||
);
|
||||
|
||||
if (isError.length > 0) {
|
||||
alert(`데이터에 오류가 있습니다.
|
||||
수정 후 제출해주세요.`);
|
||||
return;
|
||||
}
|
||||
if (await confirm('제출 하시겠습니까?')) {
|
||||
setShowDailog(true);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
const currentData = getGridDataRef.current?.();
|
||||
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
|
||||
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
|
||||
if (col.fieldName && col.dataFormat) {
|
||||
columnFormatMap.set(col.fieldName, col.dataFormat);
|
||||
}
|
||||
});
|
||||
|
||||
const formatNumber = (value: string | number | null | undefined, fieldName: string): number => {
|
||||
if (value === null || value === undefined || value === '') return 0;
|
||||
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
};
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
sggNm: (d.sggNm as string) || null,
|
||||
admdstCd: d.admdstCd as string,
|
||||
admdstNm: d.admdstNm as string,
|
||||
trbtrNm: d.trbtrNm as string,
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null),
|
||||
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
|
||||
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
|
||||
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
|
||||
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
|
||||
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
|
||||
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
|
||||
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'),
|
||||
staaArea: formatNumber(d.staaArea, 'staaArea'),
|
||||
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'),
|
||||
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
|
||||
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
|
||||
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
|
||||
rmrk: (d.rmrk as string) ?? '',
|
||||
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
|
||||
wholArea: formatNumber(d.wholArea, 'wholArea'),
|
||||
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
|
||||
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'),
|
||||
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'),
|
||||
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'),
|
||||
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'),
|
||||
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'),
|
||||
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'),
|
||||
}));
|
||||
|
||||
const requestData: SewerageSupplyRateSaveRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
statsYr: searchValues?.year ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
submitList: convertedData,
|
||||
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt,
|
||||
laePicNm: submitInfo.laePicNm,
|
||||
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
||||
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
||||
laePicTelNo: submitInfo.laePicTelNo,
|
||||
laeIdfrNm: submitInfo.laeIdfrNm,
|
||||
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
|
||||
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
|
||||
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
|
||||
};
|
||||
|
||||
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
|
||||
if (response.status === 'success') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
||||
stats_cd: statsCd,
|
||||
stats_yr: searchValues?.year ?? '',
|
||||
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
|
||||
};
|
||||
|
||||
try {
|
||||
await statisticsAPi.callImrService(imrParam);
|
||||
} catch {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setIsLoading(false);
|
||||
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
|
||||
alert('제출되었습니다.');
|
||||
} else {
|
||||
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 제출 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFunc = async () => {
|
||||
try {
|
||||
if (await confirm('저장 하시겠습니까?')) {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const currentData = getGridDataRef.current?.();
|
||||
|
||||
|
||||
if (!currentData || currentData.length === 0) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 컬럼 정의에서 dataFormat 추출 (소수점 자리수 맞추기 위해)
|
||||
const columnFormatMap = new Map<string, string>();
|
||||
SeweragePenetrationRateInpColumn.columns.forEach((col) => {
|
||||
@@ -193,56 +358,90 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (value === null || value === undefined || value === '') return 0;
|
||||
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value;
|
||||
if (isNaN(num)) return 0;
|
||||
|
||||
|
||||
const format = columnFormatMap.get(fieldName);
|
||||
if (!format) return num;
|
||||
|
||||
|
||||
// #,##0.000 → 소수점 3자리, #,##0.0 → 소수점 1자리, #,##0 → 정수
|
||||
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
|
||||
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
|
||||
};
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
gugun: (d.gugun as string) || null,
|
||||
adstCd: d.adstCd as string,
|
||||
adstDong: d.adstDong as string,
|
||||
bstrm: d.bstrm as string,
|
||||
speclCntrplnAr: d.speclCntrplnAr === '해당없음' ? null : (d.speclCntrplnAr as string | null),
|
||||
pblswrgPrmTrt: formatNumber(d.pblswrgPrmTrt, 'pblswrgPrmTrt'),
|
||||
pblswrgScdTrt: formatNumber(d.pblswrgScdTrt, 'pblswrgScdTrt'),
|
||||
pblswrgTertTrt: formatNumber(d.pblswrgTertTrt, 'pblswrgTertTrt'),
|
||||
wstrPrmTrt: formatNumber(d.wstrPrmTrt, 'wstrPrmTrt'),
|
||||
wstrScdTrt: formatNumber(d.wstrScdTrt, 'wstrScdTrt'),
|
||||
wstrTertTrt: formatNumber(d.wstrTertTrt, 'wstrTertTrt'),
|
||||
notConectPopltn: formatNumber(d.notConectPopltn, 'notConectPopltn'),
|
||||
swgeProcZoneInnerAr: formatNumber(d.swgeProcZoneInnerAr, 'swgeProcZoneInnerAr'),
|
||||
townUnprocPopltn: formatNumber(d.townUnprocPopltn, 'townUnprocPopltn'),
|
||||
townSwttPopltn: formatNumber(d.townSwttPopltn, 'townSwttPopltn'),
|
||||
townWrrtnPopltn: formatNumber(d.townWrrtnPopltn, 'townWrrtnPopltn'),
|
||||
swgeProcZoneOuterAr: formatNumber(d.swgeProcZoneOuterAr, 'swgeProcZoneOuterAr'),
|
||||
rmk: (d.rmk as string) ?? '',
|
||||
popltnSm: formatNumber(d.popltnSm, 'popltnSm'),
|
||||
arSm: formatNumber(d.arSm, 'arSm'),
|
||||
swgeProcZoneInnerSm: formatNumber(d.swgeProcZoneInnerSm, 'swgeProcZoneInnerSm'),
|
||||
pblswrgPopltnSm: formatNumber(d.pblswrgPopltnSm, 'pblswrgPopltnSm'),
|
||||
wstrPopltnSm: formatNumber(d.wstrPopltnSm, 'wstrPopltnSm'),
|
||||
swgeProcZoneOuterSm: formatNumber(d.swgeProcZoneOuterSm, 'swgeProcZoneOuterSm'),
|
||||
pblswrgPopltnSuprt: formatNumber(d.pblswrgPopltnSuprt, 'pblswrgPopltnSuprt'),
|
||||
adtrtPopltnSuprt: formatNumber(d.adtrtPopltnSuprt, 'adtrtPopltnSuprt'),
|
||||
swrgInstlrt: formatNumber(d.swrgInstlrt, 'swrgInstlrt'),
|
||||
// 행정구역
|
||||
sggNm: (d.sggNm as string) || null, // 시군구
|
||||
admdstCd: d.admdstCd as string, // 행정코드
|
||||
admdstNm: d.admdstNm as string, // 행정구역명
|
||||
trbtrNm: d.trbtrNm as string, // 지류
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), // 특별대책지역
|
||||
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
|
||||
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
|
||||
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
|
||||
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), // 고도처리
|
||||
// 폐수처리인구(명) - 물리적/생물학적/고도처리
|
||||
ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'),
|
||||
ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'),
|
||||
ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'),
|
||||
yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), // 미접속인구
|
||||
staaArea: formatNumber(d.staaArea, 'staaArea'), // 하수처리구역 내 면적(㎢)
|
||||
// 하수처리구역 외
|
||||
unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), // 미처리인구
|
||||
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
|
||||
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
|
||||
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
|
||||
rmrk: (d.rmrk as string) ?? '', // 비고
|
||||
// 합계·소계(계산필드 저장용)
|
||||
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
|
||||
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
|
||||
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), // 하수처리구역 내 합계
|
||||
wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), // 공공하수처리인구 소계
|
||||
wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), // 폐수처리인구 소계
|
||||
wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), // 하수처리구역 외 합계
|
||||
// 보급률(%)
|
||||
staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), // 공공하수처리구역 인구보급률
|
||||
altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), // 고도처리 인구보급률
|
||||
sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), // 하수도 설치율
|
||||
}));
|
||||
|
||||
const requestData: SewerageSupplyRateSaveRequest = {
|
||||
bsnmCd: searchValues?.bsnmCd ?? '',
|
||||
sido: searchValues?.sido ?? '',
|
||||
yr: searchValues?.year ?? '',
|
||||
userId: userInfo.userName,
|
||||
saveList: convertedData,
|
||||
}
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드
|
||||
ctpvNm: searchValues?.sido ?? '', // 시도
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
statsYr: searchValues?.year ?? '', // 통계 연도
|
||||
userId: userInfo.userUnqId, // 사용자
|
||||
saveList: convertedData, // 저장 데이터
|
||||
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계
|
||||
};
|
||||
|
||||
const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData);
|
||||
console.log(convertedData)
|
||||
console.log(response);
|
||||
// if (isAdminInfo) {
|
||||
// requestData.superAdmin = 'superAdmin';
|
||||
// }
|
||||
|
||||
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
const imrParam = {
|
||||
user_id: userInfo.userUnqId,
|
||||
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
|
||||
stats_cd: statsCd,
|
||||
stats_yr: searchValues?.year ?? '',
|
||||
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'),
|
||||
};
|
||||
|
||||
try {
|
||||
await statisticsAPi.callImrService(imrParam);
|
||||
} catch {
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
setIsLoading(false);
|
||||
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
alert('저장되었습니다.');
|
||||
} else {
|
||||
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
} else {
|
||||
// 저장 취소
|
||||
}
|
||||
@@ -253,48 +452,112 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const cnacelSubmitFunc = async () => {
|
||||
try {
|
||||
if (await confirm('제출된 내용을 취소하시겠습니까?')) {
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
statsYr: searchValues?.year ?? '',
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: statsCd,
|
||||
};
|
||||
|
||||
const response = await statisticsAPi.cancelSubmit(requestData);
|
||||
|
||||
if (response.status === 'success') {
|
||||
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
|
||||
alert('제출 취소되었습니다.');
|
||||
} else {
|
||||
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
alert('제출 취소 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
||||
: true;
|
||||
|
||||
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
/** 카드는 저장/제출줄/제출취소 중 하나라도 보일 때만 (관리자만으로 빈 카드 방지) */
|
||||
const showActionToolbarCard =
|
||||
rowData.length > 0 &&
|
||||
(showSaveInToolbar ||
|
||||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
{rowData.length > 0 &&
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
{isEnterable && <SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />}
|
||||
{isEnterable && <LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />}
|
||||
{isEnterable && <SubmitButton submitFunc={submitFunc} />}
|
||||
{
|
||||
// 최고 관리자일땐 저장 가능하게
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
}
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
{isAdminInfo && statusInfo && statusInfo.laeSbmsnSttsCd !== '0' && (
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
)}
|
||||
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
|
||||
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
key={isEnterable ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={(rowData.length > 0) && isEnterable}
|
||||
rowStartNum={3}
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
getGridDataRef={getGridDataRef}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as any).verificationResult as SewerageSupplyRateVerificationResult | undefined;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return 'rg-cell-verify-error';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
)}
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
|
||||
rowStartNum={3}
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
statusInfo={statusInfo}
|
||||
getGridDataRef={getGridDataRef}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
// IMR 검증은 노란색으로 구분
|
||||
const hasImr = errors.some((e) => e.vrfcCd === 'IMR');
|
||||
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
|
||||
}
|
||||
}}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - ';
|
||||
return `${prefix}${e.rsnExpln}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog isOpen={false} />
|
||||
<SubmitDailog open={showDailog} onOpenChange={setShowDailog} statusInfo={statusInfo} onConfirm={executeSubmit} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,78 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import { DataGrid } from "@rsone/components";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import RejectButton from "@/components/button/RejectButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { StatsInputStatusListApiResponse } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
|
||||
const getStatsInputStatusFetcher = async (key: [string, StatsInputStatusListRequestParams]): Promise<StatsInputStatusListApiResponse> => {
|
||||
return StatisticsInputStatusApi.getStatisticsInputStatusList(key[1]);
|
||||
};
|
||||
|
||||
// 하수도 통계 관리 > 00. 통계 입력 현황
|
||||
const StatsInputStatusInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [searchParams, setSearchParams] = useState<StatsInputStatusListRequestParams | null>(null);
|
||||
|
||||
const swrKey = searchParams ? (["stats-input-status", searchParams] as const) : null;
|
||||
const { mutate } = useSWRConfig();
|
||||
const { isLoading } = useSWR<StatsInputStatusListApiResponse>(
|
||||
swrKey,
|
||||
getStatsInputStatusFetcher,
|
||||
{
|
||||
revalidateOnFocus: true,
|
||||
onSuccess: (data) => {
|
||||
if (data.status === "success" && data.data) {
|
||||
setRowData(toGridRows(data.data));
|
||||
} else {
|
||||
alert(data.message ?? "조회에 실패했습니다.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setRowData([]);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId === 'StatsInputStatusInp' && swrKey) {
|
||||
mutate(swrKey);
|
||||
}
|
||||
}, [activeTabId]);
|
||||
|
||||
const handleSearch = (values: SearchFormValues) => {
|
||||
if (values.sido === "" || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const params: StatsInputStatusListRequestParams = {
|
||||
ctpvNm: values.sido,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
statsYr: values.year,
|
||||
};
|
||||
setSearchParams(params);
|
||||
mutate(["stats-input-status", params]);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchParams(null);
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
const rejectFunc = () => {
|
||||
// 보완 요청 로직
|
||||
@@ -18,25 +82,31 @@ const StatsInputStatusInp = () => {
|
||||
// 승인 요청 로직
|
||||
};
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<RejectButton rejectFunc={rejectFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<RejectButton rejectFunc={rejectFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
columnInfo={ StatsInputStatusInpColumn }
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
/>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={ StatsInputStatusInpColumn }
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
/** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */
|
||||
function formatPhoneWithHyphen(value: string): string {
|
||||
const digits = value.replace(/\D/g, "");
|
||||
if (digits.length <= 3) return digits;
|
||||
if (digits.startsWith("010")) {
|
||||
if (digits.length <= 7) return `010-${digits.slice(3)}`;
|
||||
return `010-${digits.slice(3, 7)}-${digits.slice(7, 11)}`;
|
||||
}
|
||||
if (digits.startsWith("02")) {
|
||||
if (digits.length <= 5) return `02-${digits.slice(2)}`;
|
||||
if (digits.length <= 9) return `02-${digits.slice(2, 5)}-${digits.slice(5, 9)}`;
|
||||
return `02-${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
|
||||
}
|
||||
if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
||||
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
|
||||
}
|
||||
|
||||
const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["laePicTelNo", "laeIdfrTelNo"];
|
||||
|
||||
interface SubmitDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
statusInfo: SewerageSupplyRateStatusInfo | null;
|
||||
/** 다이얼로그에서 저장 클릭 시 호출. 제출 정보를 넘기면 부모에서 requestData에 포함해 제출 API 호출 */
|
||||
onConfirm?: (data: StatisticsInputSubmitInfoFormData) => void | Promise<void>;
|
||||
onExceptionConfirm?: (data: any) => void | Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const defaultValues: StatisticsInputSubmitInfoFormData = {
|
||||
laePicNm: "",
|
||||
laeKbrdrJbgdNm: "",
|
||||
laeKbrdrDeptNm: "",
|
||||
laePicTelNo: "",
|
||||
laeIdfrNm: "",
|
||||
laeIdfrJbgdNm: "",
|
||||
laeIdfrDeptNm: "",
|
||||
laeIdfrTelNo: "",
|
||||
};
|
||||
|
||||
function statusInfoToFormData(info: SewerageSupplyRateStatusInfo): StatisticsInputSubmitInfoFormData {
|
||||
return {
|
||||
laePicNm: info.laePicNm ?? "",
|
||||
laeKbrdrJbgdNm: info.laeKbrdrJbgdNm ?? "",
|
||||
laeKbrdrDeptNm: info.laeKbrdrDeptNm ?? "",
|
||||
laePicTelNo: info.laePicTelNo ?? "",
|
||||
laeIdfrNm: info.laeIdfrNm ?? "",
|
||||
laeIdfrJbgdNm: info.laeIdfrJbgdNm ?? "",
|
||||
laeIdfrDeptNm: info.laeIdfrDeptNm ?? "",
|
||||
laeIdfrTelNo: info.laeIdfrTelNo ?? "",
|
||||
};
|
||||
};
|
||||
|
||||
const SubmitDailog = ({ open, onOpenChange, statusInfo, onConfirm, onExceptionConfirm, isLoading }: SubmitDialogProps) => {
|
||||
const { control, handleSubmit, reset, formState: { errors } } = useForm<StatisticsInputSubmitInfoFormData>({
|
||||
resolver: zodResolver(statisticsInputSubmitInfoSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset(statusInfo ? statusInfoToFormData(statusInfo) : defaultValues);
|
||||
}
|
||||
}, [open, statusInfo, reset]);
|
||||
|
||||
async function onSubmit(data: StatisticsInputSubmitInfoFormData) {
|
||||
await onConfirm?.(data);
|
||||
onOpenChange(false);
|
||||
reset(defaultValues);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
onOpenChange(false);
|
||||
reset(defaultValues);
|
||||
}
|
||||
|
||||
const rows: { name: keyof StatisticsInputSubmitInfoFormData; label: string; placeholder: string }[] = [
|
||||
{ name: "laePicNm", label: "작성자 명", placeholder: "작성자 명 입력" },
|
||||
{ name: "laeKbrdrJbgdNm", label: "작성자 직제", placeholder: "작성자 직제 입력" },
|
||||
{ name: "laeKbrdrDeptNm", label: "작성자 부서", placeholder: "작성자 부서 입력" },
|
||||
{ name: "laePicTelNo", label: "작성자 연락처", placeholder: "작성자 연락처 입력" },
|
||||
{ name: "laeIdfrNm", label: "승인자명", placeholder: "승인자명 입력" },
|
||||
{ name: "laeIdfrJbgdNm", label: "승인자 직제", placeholder: "승인자 직제 입력" },
|
||||
{ name: "laeIdfrDeptNm", label: "승인자 부서", placeholder: "승인자 부서 입력" },
|
||||
{ name: "laeIdfrTelNo", label: "승인자 연락처", placeholder: "승인자 연락처" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>제출 정보</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Table variant="secondary">
|
||||
<TableBody>
|
||||
{rows.map(({ name, label, placeholder }) => (
|
||||
<TableRow key={name}>
|
||||
<TableHead className="w-[150px] bg-muted font-medium" data-required>
|
||||
{label}
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const isPhone = PHONE_FIELDS.includes(name);
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
field.onChange(isPhone ? formatPhoneWithHyphen(v) : v);
|
||||
}}
|
||||
/>
|
||||
{errors[name]?.message && (
|
||||
<p className="text-sm text-destructive ml-1">
|
||||
{errors[name]?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DialogFooter className="mt-6">
|
||||
<Button type="button" color="gray" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" color="primary">
|
||||
제출
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmitDailog;
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
// import useSWR from 'swr';
|
||||
|
||||
import {
|
||||
Button,
|
||||
// Input,
|
||||
Label,
|
||||
// Select,
|
||||
// SelectTrigger,
|
||||
// SelectContent,
|
||||
// SelectItem,
|
||||
// SelectValue
|
||||
}
|
||||
from "@common/components";
|
||||
import DataGrid from '@/components/module/DataGrid';
|
||||
import { CodeSelect } from '@/entities/code';
|
||||
|
||||
import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn';
|
||||
// import { LwpoCodeApi } from "@/entities/code";
|
||||
|
||||
// const getStateCdListFetcher = async () => {
|
||||
// return LwpoCodeApi.getLwpoCodeList({
|
||||
// sort: 'regDt,desc',
|
||||
// upCd: 'STITEM',
|
||||
// page: 0,
|
||||
// size: 100,
|
||||
// });
|
||||
// }
|
||||
|
||||
const LogHistory = () => {
|
||||
// const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [statsCd, setStatsCd] = useState('');
|
||||
|
||||
// const { data: lwpoCdList } = useSWR(
|
||||
// "lwpo-code-list",
|
||||
// getStateCdListFetcher,
|
||||
// {
|
||||
// revalidateOnFocus: false,
|
||||
// revalidateOnReconnect: false,
|
||||
// revalidateIfStale: false,
|
||||
// dedupingInterval: 2000,
|
||||
// shouldRetryOnError: false,
|
||||
// onSuccess: (data) => {
|
||||
// if (!data.content || data.content.length == 0) {
|
||||
// alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
// 새로고침 후 다시 시도해주세요.`);
|
||||
// }
|
||||
// },
|
||||
// onError: () => {
|
||||
// alert(`시도 목록을 불러오는데 실패했습니다.
|
||||
// 새로고침 후 다시 시도해주세요.`);
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="card card-border">
|
||||
<form>
|
||||
<div className="flex items-center gap-8 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
{/* <Select value={lwpoCd} onValueChange={setLwpoCd}>
|
||||
<SelectTrigger className="w-[22rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
{lwpoCdList?.content?.map((l) => (
|
||||
<SelectItem key={l.lwpoCd} value={l.lwpoCd}>
|
||||
{l.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
placeholder="선택"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button color="green" className="h-[3.2rem] w-[96px]" type="button">
|
||||
초기화
|
||||
</Button>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<DataGrid
|
||||
columnInfo={LogHistoryColumn}
|
||||
// rowStartNum={1}
|
||||
// fixedColumns={1}
|
||||
isExcelImport={false}
|
||||
// showHeaderCheckbox={true}
|
||||
rowData={[]}
|
||||
setRowData={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogHistory;
|
||||
@@ -1,26 +1,92 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { DataGrid } from "@rsone/components";
|
||||
import { useState, useMemo } from "react";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { MediationPumpStationItem } from "@/entities/statistics/model/statistics-pumping-station-review-interface";
|
||||
|
||||
// 하수도 통계 관리 05.펌프장
|
||||
/**
|
||||
* 하수도 통계 검토 05.중계펌프장
|
||||
*/
|
||||
const PumpingStationRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const approveFunc = () => { console.log('approveFunc'); };
|
||||
const saveFunc = () => { console.log('submit'); };
|
||||
const reviewOpinionFunc = () => { console.log('reviewOpinionFunc'); };
|
||||
const returnFunc = () => { console.log('returnFunc'); };
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout, fixedColumns, rowStartNum } = PumpingStationInfoList;
|
||||
return {
|
||||
columnLayout,
|
||||
fixedColumns,
|
||||
rowStartNum,
|
||||
columns: columns.map((col) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsManagementApi.getMediationPumpStationList({
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
pumpstNm: values.fctName,
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data as MediationPumpStationItem[];
|
||||
|
||||
const convertedData = rawList.map(item => ({
|
||||
...item,
|
||||
rmrk: item.rmrk || ''
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(convertedData));
|
||||
} else {
|
||||
setRowData([]);
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("중계펌프장 조회 중 에러 발생:", error);
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const approveFunc = () => console.log('approveFunc');
|
||||
const saveFunc = () => console.log('submit');
|
||||
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
|
||||
const returnFunc = () => console.log('returnFunc');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm fctName={true} />
|
||||
<div className="relative">
|
||||
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
@@ -29,8 +95,24 @@ const PumpingStationRvw = () => {
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
<DataGrid columnInfo={PumpingStationInfoList} isExcelImport={false} rowStartNum={3} fixedColumns={3} rowData={rowData} setRowData={setRowData} />
|
||||
</>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
fixedColumns={columnInfo.fixedColumns}
|
||||
rowStartNum={columnInfo.rowStartNum}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +1,161 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TabDataGrid } from "@rsone/components";
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
|
||||
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
|
||||
|
||||
// 하수도 통계 관리 03. 하수관로 유지 관리
|
||||
|
||||
// 하수도 통계 검토 03. 하수관로 유지 관리
|
||||
const SewagePipeMaintainRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('ST00030001');
|
||||
|
||||
const approveFunc = () => {
|
||||
console.log('approveFunc');
|
||||
// 승인 로직
|
||||
const searchParamRef = useRef<SewerPipeMtcRequestParams | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
return SewagePipeMaintainColumnInfoList.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab('ST00030001');
|
||||
searchParamRef.current = null;
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log('submit');
|
||||
// 저장 로직
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab('ST00030001');
|
||||
setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const searchParam: SewerPipeMtcRequestParams = {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
};
|
||||
|
||||
searchParamRef.current = searchParam;
|
||||
const response = await statisticsManagementApi.getSewerPipeMtcList(searchParam);
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
|
||||
const listData = Array.isArray(response.data)
|
||||
? response.data
|
||||
: (response.data.mtcStatusList || []);
|
||||
|
||||
newRowDataMap.set('ST00030001', toGridRows(listData as object[]));
|
||||
setRowDataMap(newRowDataMap);
|
||||
} else {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = () => {
|
||||
console.log('reviewOpinionFunc');
|
||||
// 검토의견 작성
|
||||
};
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const returnFunc = () => {
|
||||
console.log('returnFunc');
|
||||
// 반려
|
||||
const existingAData = rowDataMap.get('ST00030001');
|
||||
const activeTabData = rowDataMap.get(tabKey);
|
||||
|
||||
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
|
||||
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
|
||||
|
||||
if (hasAData && !hasActiveTabData && searchParamRef.current) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let response;
|
||||
|
||||
if (tabKey === 'ST00030001') {
|
||||
response = await statisticsManagementApi.getSewerPipeMtcList(searchParamRef.current);
|
||||
} else if (tabKey === 'ST00030002') {
|
||||
response = await statisticsManagementApi.getSewerPipeDrdgList(searchParamRef.current);
|
||||
}
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
setRowDataMap(prev => {
|
||||
const next = new Map(prev);
|
||||
|
||||
const listData = Array.isArray(response.data)
|
||||
? response.data
|
||||
: (response.data.mtcStatusList || []);
|
||||
|
||||
next.set(tabKey, toGridRows(listData as object[]));
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm fctName={true} />
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<ReturnButton returnFunc={returnFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
|
||||
<ApproveButton approveFunc={() => { }} />
|
||||
<ReturnButton returnFunc={() => { }} />
|
||||
<SaveButton saveFunc={() => { }} />
|
||||
</div>
|
||||
</div>
|
||||
<TabDataGrid columnInfo={SewagePipeMaintainColumnInfoList} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
|
||||
</>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,15 +13,17 @@ import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/Sew
|
||||
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SewagePipeReviewRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
|
||||
|
||||
|
||||
// 하수도 통계 검토 02.하수관로
|
||||
const SewagePipeRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('a');
|
||||
const [activeTab, setActiveTab] = useState<string>('ST00020001');
|
||||
|
||||
const searchParamRef = useRef<SearchFormValues | null>(null);
|
||||
const searchParamRef = useRef<SewagePipeReviewRequestParams | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
return SewagePipeColumnInfoListRvw.map((tab: ColumnInfoList) => ({
|
||||
@@ -36,7 +38,7 @@ const SewagePipeRvw = () => {
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab('a');
|
||||
setActiveTab('ST00020001');
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
@@ -45,18 +47,18 @@ const SewagePipeRvw = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab('a');
|
||||
setActiveTab('ST00020001');
|
||||
|
||||
setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const searchParam = {
|
||||
sido: values.sido,
|
||||
gugun: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
bsnmCd: values.bsnmCd,
|
||||
yr: values.year,
|
||||
const searchParam: SewagePipeReviewRequestParams = {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
};
|
||||
|
||||
searchParamRef.current = searchParam;
|
||||
@@ -64,12 +66,12 @@ const SewagePipeRvw = () => {
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
newRowDataMap.set('a', toGridRows(response.data as object[]));
|
||||
newRowDataMap.set('ST00020001', toGridRows(response.data as object[]));
|
||||
setRowDataMap(newRowDataMap);
|
||||
} else {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -79,7 +81,7 @@ const SewagePipeRvw = () => {
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const existingAData = rowDataMap.get('a');
|
||||
const existingAData = rowDataMap.get('ST00020001');
|
||||
const activeTabData = rowDataMap.get(tabKey);
|
||||
|
||||
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
|
||||
@@ -92,11 +94,16 @@ const SewagePipeRvw = () => {
|
||||
|
||||
let response;
|
||||
|
||||
if (tabKey === 'b') {
|
||||
if (tabKey === 'ST00020002') {
|
||||
response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current);
|
||||
}else if(tabKey === 'ST00020003'){
|
||||
response = await statisticsManagementApi.getSewagePipeTypeList(searchParamRef.current);
|
||||
}else if(tabKey === 'ST00020004'){
|
||||
response = await statisticsManagementApi.getSewagePipeDiamList(searchParamRef.current);
|
||||
}
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
|
||||
setRowDataMap(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, toGridRows(response.data as object[]));
|
||||
|
||||
@@ -1,52 +1,156 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TabDataGrid } from "@rsone/components";
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import TabDataGrid from "@/components/module/TabDataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn";
|
||||
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface";
|
||||
|
||||
// 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장
|
||||
/**
|
||||
* 하수도 통계 검토 04. 하수저류 시설 및 빗물 펌프장
|
||||
* Tab A: 하수저류시설 (ST00040001)
|
||||
* Tab B: 빗물펌프장 (ST00040002)
|
||||
*/
|
||||
const SewageReservoirPumpRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('ST00040001');
|
||||
|
||||
const approveFunc = () => {
|
||||
console.log('approveFunc');
|
||||
// 승인 로직
|
||||
const searchParamRef = useRef<SewerReservoirRequestParams | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
return SewageReservoirPumpInfoListRvw.map((tab: ColumnInfoList) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? ("rg-cell-readonly" as const),
|
||||
})),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab('ST00040001');
|
||||
searchParamRef.current = null;
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log('submit');
|
||||
// 저장 로직
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTab('ST00040001');
|
||||
setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const searchParam: SewerReservoirRequestParams = {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
};
|
||||
|
||||
searchParamRef.current = searchParam;
|
||||
|
||||
const response = await statisticsManagementApi.getSewerReservoirList(searchParam);
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
newRowDataMap.set('ST00040001', toGridRows(response.data as object[]));
|
||||
setRowDataMap(newRowDataMap);
|
||||
} else {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = () => {
|
||||
console.log('reviewOpinionFunc');
|
||||
// 검토의견 작성
|
||||
};
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
const returnFunc = () => {
|
||||
console.log('returnFunc');
|
||||
// 반려
|
||||
const existingAData = rowDataMap.get('ST00040001');
|
||||
const activeTabData = rowDataMap.get(tabKey);
|
||||
|
||||
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
|
||||
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
|
||||
|
||||
if (hasAData && !hasActiveTabData && searchParamRef.current) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
let response;
|
||||
|
||||
if (tabKey === 'ST00040001') {
|
||||
response = await statisticsManagementApi.getSewerReservoirList(searchParamRef.current);
|
||||
} else if (tabKey === 'ST00040002') {
|
||||
response = await statisticsManagementApi.getPumpStationList(searchParamRef.current);
|
||||
}
|
||||
|
||||
if (response?.status === 'success' && response.data) {
|
||||
setRowDataMap(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(tabKey, toGridRows(response.data as object[]));
|
||||
return next;
|
||||
});
|
||||
} else if (response) {
|
||||
alert(response?.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm fctName={true} />
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<ReturnButton returnFunc={returnFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
|
||||
<ApproveButton approveFunc={() => { }} />
|
||||
<ReturnButton returnFunc={() => { }} />
|
||||
<SaveButton saveFunc={() => { }} />
|
||||
</div>
|
||||
</div>
|
||||
<TabDataGrid columnInfo={SewageReservoirPumpInfoListRvw} isExcelImport={false} rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} />
|
||||
</>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabDataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
rowDataMap={rowDataMap}
|
||||
setRowDataMap={setRowDataMap}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SewageReservoirPumpRvw;
|
||||
export default SewageReservoirPumpRvw;
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import ReturnButton from "@/components/button/ReturnButton";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
|
||||
@@ -17,8 +15,8 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
// 하수도 통계 검토 01.하수도 보급률
|
||||
const SeweragePenetrationRateRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList;
|
||||
@@ -39,6 +37,7 @@ const SeweragePenetrationRateRvw = () => {
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
|
||||
if (values.sido === '' || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
@@ -48,17 +47,18 @@ const SeweragePenetrationRateRvw = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsManagementApi.getSewerageSupplyRateReviewList({
|
||||
sido: values.sido,
|
||||
gugun: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
bsnmCd: values.bsnmCd,
|
||||
yr: values.year,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data.sewerageSupplyRateList;
|
||||
const convertedData: SewerageSupplyRateReviewItem[] = rawList.map((item) => ({
|
||||
...item,
|
||||
speclCntrplnAr: !item.speclCntrplnAr ? '해당없음' : item.speclCntrplnAr,
|
||||
|
||||
spcCtpnRgnCn: !item.spcCtpnRgnCn ? '해당없음' : item.spcCtpnRgnCn,
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(convertedData));
|
||||
@@ -70,7 +70,7 @@ const SeweragePenetrationRateRvw = () => {
|
||||
console.error("조회 중 에러 발생:", error);
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -80,15 +80,13 @@ const SeweragePenetrationRateRvw = () => {
|
||||
const returnFunc = () => console.log('returnFunc');
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<ReturnButton returnFunc={returnFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { StatsInputStatusReviewItem } from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
|
||||
// 하수도 통계 검토 00. 통계 입력 현황
|
||||
const StatsInputStatusRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<any[]>([]);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -34,7 +35,7 @@ const StatsInputStatusRvw = () => {
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: any) => {
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
@@ -46,14 +47,13 @@ const StatsInputStatusRvw = () => {
|
||||
const response = await statisticsManagementApi.getStatsInputStatusList({
|
||||
sido: values.sido,
|
||||
gugun: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
yr: values.year || values.yr,
|
||||
yr: values.year,
|
||||
bsnmCd: values.bsnmCd
|
||||
});
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data.sewerageSupplyRateList || response.data;
|
||||
|
||||
const convertedData: StatsInputStatusReviewItem[] = rawList.map((item: any) => ({
|
||||
const rawList = response.data.sewerageSupplyRateList ?? [];
|
||||
const convertedData: StatsInputStatusReviewItem[] = rawList.map((item: StatsInputStatusReviewItem) => ({
|
||||
...item,
|
||||
status: item.status || '미확인',
|
||||
}));
|
||||
|
||||
@@ -1,140 +1,69 @@
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn";
|
||||
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
const SeweragePenetrationRateTot = () => {
|
||||
const columns = [
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'rvbsn', fieldName: '수계', header: { text: '수계' } },
|
||||
{ name: 'bstrm', fieldName: '지류', header: { text: '지류' } },
|
||||
{ name: 'mdbsn', fieldName: '세부단위유역', header: { text: '세부단위유역' } },
|
||||
{ name: 'lgbsn', fieldName: '중권역', header: { text: '중권역' } },
|
||||
{ name: 'mdmbsn', fieldName: '소권역', header: { text: '소권역' } },
|
||||
{ name: 'specl_cntrpln_ar', fieldName: '특별대책지역', header: { text: '특별대책지역' } },
|
||||
{ name: 'total_popl', fieldName: '총인구', header: { text: '총인구' } },
|
||||
{ name: 'popl_sm', fieldName: '내국인', header: { text: '내국인' } },
|
||||
{ name: 'frgnr_popl_sm', fieldName: '외국인', header: { text: '외국인' } },
|
||||
{ name: 'area', fieldName: '행정참고자료면적(㎢)', header: { text: '행정참고자료면적(㎢)' } },
|
||||
{ name: 'popltn_sm', fieldName: '총인구(명)', header: { text: '총인구(명)' } },
|
||||
{ name: 'ar_sm', fieldName: '총면적(㎢)', header: { text: '총면적(㎢)' } },
|
||||
{ name: 'swge_proc_zone_inner_sm', fieldName: '합계', header: { text: '합계' } },
|
||||
{ name: 'pblswrg_popltn_sm', fieldName: '소계', header: { text: '소계' } },
|
||||
{ name: 'pblswrg_prm_trt', fieldName: '물리적처리', header: { text: '물리적처리' } },
|
||||
{ name: 'pblswrg_scd_trt', fieldName: '생물학적처리', header: { text: '생물학적처리' } },
|
||||
{ name: 'pblswrg_tert_trt', fieldName: '고도처리', header: { text: '고도처리' } },
|
||||
{ name: 'wstr_popltn_sm', fieldName: '소계', header: { text: '소계' } },
|
||||
{ name: 'wstr_prm_trt', fieldName: '물리적처리', header: { text: '물리적처리' } },
|
||||
{ name: 'wstr_scd_trt', fieldName: '생물학적처리', header: { text: '생물학적처리' } },
|
||||
{ name: 'wstr_tert_trt', fieldName: '고도처리', header: { text: '고도처리' } },
|
||||
{ name: 'not_conect_popltn', fieldName: '미접속인구', header: { text: '미접속인구' } },
|
||||
{ name: 'swge_proc_zone_inner_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
|
||||
{ name: 'swge_proc_zone_outer_sm', fieldName: '합계', header: { text: '합계' } },
|
||||
{ name: 'town_unproc_popltn', fieldName: '미처리인구', header: { text: '미처리인구' } },
|
||||
{ name: 'town_swtt_popltn', fieldName: '오수처리인구', header: { text: '오수처리인구' } },
|
||||
{ name: 'town_wrrtn_popltn', fieldName: '정화조인구', header: { text: '정화조인구' } },
|
||||
{ name: 'swge_proc_zone_outer_ar', fieldName: '면적(㎢)', header: { text: '면적(㎢)' } },
|
||||
{ name: 'pblswrg_popltn_suprt', fieldName: '공공하수처리구역인구보급율(%)', header: { text: '공공하수처리구역인구보급율(%)' } },
|
||||
{ name: 'adtrt_popltn_suprt', fieldName: '고도처리인구보급율(%)', header: { text: `고도처리인구보급율(%)` } },
|
||||
{ name: 'swrg_instlrt', fieldName: '하수도설치율(%)', header: { text: '하수도설치율(%)' } },
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sido', 'sgg'],
|
||||
},
|
||||
'rvbsn',
|
||||
'bstrm',
|
||||
{
|
||||
name: 'groupB',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '수계영향권별분류류' },
|
||||
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
|
||||
},
|
||||
'specl_cntrpln_ar',
|
||||
{
|
||||
name: 'groupC',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정참고자료인구' },
|
||||
items: ['total_popl', 'popl_sm', 'frgnr_popl_sm'],
|
||||
},
|
||||
'area',
|
||||
'popltn_sm',
|
||||
'ar_sm',
|
||||
{
|
||||
name: 'groupD',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역내' },
|
||||
items: [
|
||||
'swge_proc_zone_inner_sm',
|
||||
{
|
||||
name: 'pblswrgGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '공공하수처리인구(명)' },
|
||||
items: ['pblswrg_popltn_sm', 'pblswrg_prm_trt', 'pblswrg_scd_trt', 'pblswrg_tert_trt'],
|
||||
},
|
||||
{
|
||||
name: 'wstrGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '폐수처리인구(명)' },
|
||||
items: ['wstr_popltn_sm', 'wstr_prm_trt', 'wstr_scd_trt', 'wstr_tert_trt'],
|
||||
},
|
||||
'not_conect_popltn',
|
||||
'swge_proc_zone_inner_ar'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupE',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역외' },
|
||||
items: ['swge_proc_zone_outer_sm', 'town_swtt_popltn', 'town_unproc_popltn', 'town_wrrtn_popltn', 'swge_proc_zone_outer_ar'],
|
||||
},
|
||||
'pblswrg_popltn_suprt',
|
||||
'adtrt_popltn_suprt',
|
||||
'swrg_instlrt',
|
||||
'rmk',
|
||||
];
|
||||
|
||||
const submitFunc = () => {
|
||||
console.log('submit');
|
||||
// 제출 로직
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log('submit');
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = () => {
|
||||
console.log('get last year data');
|
||||
// 지난연도 데이터 가져오기 로직
|
||||
};
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
// 예외 제출 로직
|
||||
};
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const param = {
|
||||
ctpvNm: values.sido === 'ALL' ? '' : values.sido,
|
||||
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
rowDataYn: values.isDetail
|
||||
};
|
||||
|
||||
const response = await statisticsTotalStatusApi.getSewerageSupplyRateList(param);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setRowData(toGridRows(response.data));
|
||||
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
</div>
|
||||
<DataGrid columnInfo={{columns, columnLayout}} rowStartNum={2} isExcelImport={true} fixedColumns={1} rowData={rowData} setRowData={setRowData} />
|
||||
<SearchForm isDetail={true} onSubmit={handleSearch} />
|
||||
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>}
|
||||
<DataGrid
|
||||
columnInfo={SeweragePentrationRateTotColumn}
|
||||
rowStartNum={2}
|
||||
isExcelImport={false}
|
||||
fixedColumns={1}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
if (rowType === 'DETAIL') return undefined;
|
||||
if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum';
|
||||
return 'rg-cell-rowtype-other';
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { NoData } from "@/shared/ui/no-data";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import DetailComponent from "./generalVerification/DetailComponent";
|
||||
import RegistDialog from "./generalVerification/RegistDialog";
|
||||
|
||||
import { getGeneralVerificationColumns } from "@/components/util/column/management/GeneralVerificationColumn";
|
||||
|
||||
const GeneralVerificationManagement = () => {
|
||||
const [searchValues, setSearchValues] = useState<{
|
||||
statsCd: string;
|
||||
useYn: string;
|
||||
vrfcClsfCd: string;
|
||||
}>({
|
||||
statsCd: "",
|
||||
useYn: "",
|
||||
vrfcClsfCd: "",
|
||||
});
|
||||
const [vrfcMasterData, setVrfcMasterData] = useState<
|
||||
GeneralverificationList[]
|
||||
>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [selectedVrfcRow, setSelectedVrfcRow] = useState<number | null>(null);
|
||||
const [showRegistForm, setShowRegistForm] = useState<boolean>(false);
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const handleSearch = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const params = Object.fromEntries(
|
||||
Object.entries({
|
||||
trgtStatsCd: searchValues.statsCd,
|
||||
vrfcClsfCd: searchValues.vrfcClsfCd,
|
||||
useYn: searchValues.useYn,
|
||||
}).filter(([, value]) => value !== "" && value !== "ALL"),
|
||||
);
|
||||
|
||||
const response =
|
||||
await generalverificationApiService.getGeneralverificationList(
|
||||
params,
|
||||
);
|
||||
|
||||
if (response.status === "success") {
|
||||
setVrfcMasterData(response.data);
|
||||
setRowSelection({});
|
||||
setSelectedVrfcRow(null);
|
||||
setShowRegistForm(false);
|
||||
} else {
|
||||
alert(
|
||||
response.message || "데이터 조회 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => getGeneralVerificationColumns(handleSearch),
|
||||
[searchValues.statsCd, searchValues.vrfcClsfCd, searchValues.useYn],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: vrfcMasterData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) => String(row.vrfcSn),
|
||||
state: { rowSelection },
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleVrfcMasterRowClick = async (row: GeneralverificationList) => {
|
||||
setRowSelection({ [row.vrfcSn]: true });
|
||||
setSelectedVrfcRow(row.vrfcSn);
|
||||
setShowRegistForm(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowSelection({});
|
||||
setSelectedVrfcRow(null);
|
||||
handleSearch();
|
||||
setShowRegistForm(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleSearch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card card-border !mx-0">
|
||||
<div className="flex items-center gap-6 pl-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="text-label">통계항목</Label>
|
||||
<CodeSelectWithAll
|
||||
upCd="STITEM"
|
||||
value={searchValues.statsCd}
|
||||
onValueChange={(value) =>
|
||||
setSearchValues({
|
||||
...searchValues,
|
||||
statsCd: value,
|
||||
})
|
||||
}
|
||||
placeholder="전체"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="text-label">검증분류</Label>
|
||||
<Select
|
||||
value={searchValues.vrfcClsfCd}
|
||||
onValueChange={(value) =>
|
||||
setSearchValues({
|
||||
...searchValues,
|
||||
vrfcClsfCd: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="DO_SI">
|
||||
도단위 검증
|
||||
</SelectItem>
|
||||
<SelectItem value="SI_GU">
|
||||
시단위 검증
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="text-label">사용여부</Label>
|
||||
<Select
|
||||
value={searchValues.useYn}
|
||||
onValueChange={(value) =>
|
||||
setSearchValues({
|
||||
...searchValues,
|
||||
useYn: value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="전체"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="Y">Y</SelectItem>
|
||||
<SelectItem value="N">N</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
color="gray"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[65vh] justify-center gap-5 !p-0">
|
||||
<div className="card card-border !mx-0 flex w-1/2 flex-col">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
일반 검증 목록
|
||||
</h3>
|
||||
<RegistDialog
|
||||
open={showRegistForm}
|
||||
onOpenChange={setShowRegistForm}
|
||||
onSuccess={handleSearch}
|
||||
>
|
||||
<Button>등록</Button>
|
||||
</RegistDialog>
|
||||
</div>
|
||||
<div className="card !mx-2 !my-4 h-full overflow-auto !p-0">
|
||||
{isLoading && (
|
||||
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? null : vrfcMasterData.length > 0 ? (
|
||||
<DataTable
|
||||
table={table}
|
||||
onRowClick={(value) =>
|
||||
handleVrfcMasterRowClick(value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<NoData />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DetailComponent selectedVrfcRow={selectedVrfcRow} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralVerificationManagement;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,547 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import type { ChartOptions } from "chart.js";
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip as ChartTooltip,
|
||||
} from "chart.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type {
|
||||
AdstZoneResponseDto,
|
||||
StAnlsMstResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsRsltResponse,
|
||||
} from "@/entities/statistics";
|
||||
import {
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT,
|
||||
imrDetailGridColumnInfo,
|
||||
imrVerificationHistoryColumns,
|
||||
} from "@/entities/statistics";
|
||||
import { imrApi } from "@/entities/statistics/api/imr-api";
|
||||
import {
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
DateRangePicker,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
ChartTooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
type DateRangeValue = {
|
||||
from: Date;
|
||||
to?: Date;
|
||||
};
|
||||
|
||||
const EMPTY_DATA: never[] = [];
|
||||
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
|
||||
|
||||
function getImrDetailGridColumnInfo(statsCd: string): ColumnInfo {
|
||||
const visible =
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD[statsCd] ??
|
||||
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT;
|
||||
const visibleSet = new Set(visible);
|
||||
return {
|
||||
columns: imrDetailGridColumnInfo.columns.map((col) =>
|
||||
visibleSet.has(col.name) ? col : { ...col, visible: false },
|
||||
),
|
||||
columnLayout: imrDetailGridColumnInfo.columnLayout,
|
||||
};
|
||||
}
|
||||
|
||||
const ImrVerificationHistory = () => {
|
||||
// Area A Search States
|
||||
const [searchSido, setSearchSido] = useState<string>("");
|
||||
const [searchSigungu, setSearchSigungu] = useState<string>("");
|
||||
const [searchStatsCd, setSearchStatsCd] = useState<string>("");
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue | undefined>({
|
||||
from: dayjs().subtract(1, "month").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
});
|
||||
const [appliedSearchParams, setAppliedSearchParams] =
|
||||
useState<StAnlsMstSearchParams>({
|
||||
page: 0,
|
||||
size: 10,
|
||||
vrfcMthdCd: "IMR",
|
||||
sort: "anlsSn,desc",
|
||||
});
|
||||
|
||||
// Selected States
|
||||
const [selectedHistory, setSelectedHistory] =
|
||||
useState<StAnlsMstResponse | null>(null);
|
||||
const [selectedDetail, setSelectedDetail] =
|
||||
useState<StAnlsRsltResponse | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
// Area B Grid State
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
// Area A Pagination
|
||||
const [historyPagination, setHistoryPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: sidoData } = useStAnlsSidoCombo();
|
||||
const { data: sigunguData } = useStAnlsSigunguCombo(searchSido);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const sigunguOptions = sigunguData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const imrDetailColumnInfo = useMemo(
|
||||
() => getImrDetailGridColumnInfo(selectedHistory?.statsCd ?? ""),
|
||||
[selectedHistory?.statsCd],
|
||||
);
|
||||
|
||||
// Area A Data Fetching
|
||||
const { data: historyData } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
|
||||
"imr",
|
||||
appliedSearchParams,
|
||||
historyPagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsMstApi.getImrAnalysisMasterList({
|
||||
...appliedSearchParams,
|
||||
page: historyPagination.pageIndex,
|
||||
size: historyPagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const historyTable = useReactTable({
|
||||
data: historyData?.content ?? EMPTY_DATA,
|
||||
columns: imrVerificationHistoryColumns,
|
||||
getRowId: (row) => String(row.anlsSn),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: historyData?.totalPages ?? 0,
|
||||
state: {
|
||||
pagination: historyPagination,
|
||||
rowSelection: selectedHistory
|
||||
? { [selectedHistory.anlsSn]: true }
|
||||
: {},
|
||||
},
|
||||
onPaginationChange: setHistoryPagination,
|
||||
});
|
||||
|
||||
// Area B Data Fetching
|
||||
const { data: detailData, isLoading: isDetailLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
selectedHistory?.anlsSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getImrResultList(selectedHistory!.anlsSn, {
|
||||
page: 0,
|
||||
size: 100,
|
||||
sort: "admdstCd,asc",
|
||||
}),
|
||||
enabled: !!selectedHistory,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailData || !detailData.content) {
|
||||
setGridRowData([]);
|
||||
return;
|
||||
}
|
||||
const mapped = detailData.content.map((item) => ({
|
||||
...item,
|
||||
statsNm: selectedHistory?.vrfcTrgtNm ?? "-",
|
||||
}));
|
||||
setGridRowData(mapped as Record<string, string | number>[]);
|
||||
}, [detailData, selectedHistory]);
|
||||
|
||||
// Area C Data Fetching
|
||||
const { data: chartData, isLoading: isChartLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.IMR,
|
||||
selectedHistory?.anlsSn,
|
||||
selectedDetail?.vrfcTrgtSn,
|
||||
selectedDetail?.admdstCd,
|
||||
],
|
||||
queryFn: () =>
|
||||
imrApi.getOutliersLog({
|
||||
jobUuid: selectedHistory!.jobUuid,
|
||||
anlsSn: selectedDetail!.anlsSn,
|
||||
vrfcTrgtSn: selectedDetail!.vrfcTrgtSn,
|
||||
admdstCd: selectedDetail!.admdstCd,
|
||||
}),
|
||||
enabled: !!selectedHistory && !!selectedDetail,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
const params: StAnlsMstSearchParams = {
|
||||
vrfcMthdCd: "IMR",
|
||||
sort: "anlsSn,desc",
|
||||
};
|
||||
if (searchSido !== "")
|
||||
params.sewerBzmnCd =
|
||||
searchSigungu !== "" ? searchSigungu : searchSido;
|
||||
if (searchStatsCd) params.statsCd = searchStatsCd;
|
||||
if (dateRange?.from)
|
||||
params.verifyDtStart = dayjs(dateRange.from).format("YYYY-MM-DD");
|
||||
if (dateRange?.to)
|
||||
params.verifyDtEnd = dayjs(dateRange.to).format("YYYY-MM-DD");
|
||||
|
||||
setAppliedSearchParams(params);
|
||||
setHistoryPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
setSelectedHistory(null);
|
||||
setSelectedDetail(null);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchSido("");
|
||||
setSearchSigungu("");
|
||||
setSearchStatsCd("");
|
||||
const defaultRange = {
|
||||
from: dayjs().subtract(1, "month").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
setDateRange(defaultRange);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!selectedHistory || isDownloading) return;
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
const blob = await stAnlsRsltApi.downloadExcelImr(
|
||||
selectedHistory.anlsSn,
|
||||
);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `IMR_분석결과.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error("Failed to download IMR excel", error);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const chartOptions: ChartOptions<"line"> = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top" as const,
|
||||
labels: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "I-MR 잔차 관리도",
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "수치",
|
||||
},
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "연도",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[selectedDetail],
|
||||
);
|
||||
|
||||
const lineChartData = useMemo(() => {
|
||||
if (!chartData || !chartData.history) return null;
|
||||
|
||||
const labels = chartData.history.map((h) => String(h.year));
|
||||
const actualData = chartData.history.map((h) => h.x);
|
||||
const uclData = chartData.history.map((h) => h.ucl);
|
||||
const lclData = chartData.history.map((h) => h.lcl);
|
||||
const predictedData = chartData.history.map((h) => h.predicted);
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "실측치 (X)",
|
||||
data: actualData,
|
||||
borderColor: "rgb(75, 192, 192)",
|
||||
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
||||
tension: 0.1,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
{
|
||||
label: "상한선 (UCL)",
|
||||
data: uclData,
|
||||
borderColor: "rgb(255, 99, 132)",
|
||||
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||
borderDash: [5, 5],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "하한선 (LCL)",
|
||||
data: lclData,
|
||||
borderColor: "rgb(54, 162, 235)",
|
||||
backgroundColor: "rgba(54, 162, 235, 0.2)",
|
||||
borderDash: [5, 5],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "예측치 (Predicted)",
|
||||
data: predictedData,
|
||||
borderColor: "rgb(153, 102, 255)",
|
||||
backgroundColor: "rgba(153, 102, 255, 0.5)",
|
||||
borderDash: [2, 2],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [chartData]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 영역 A: 검증 이력 그리드 + 검색 필터 */}
|
||||
<div className="card card-border bg-white p-6">
|
||||
<div className="flex flex-wrap items-end gap-6 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
시도
|
||||
</Label>
|
||||
<Select
|
||||
value={searchSido}
|
||||
onValueChange={(val) => {
|
||||
setSearchSido(val);
|
||||
setSearchSigungu("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue placeholder="시도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.adstCd}
|
||||
value={opt.adstCd}
|
||||
>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
시군구
|
||||
</Label>
|
||||
<Select
|
||||
value={searchSigungu}
|
||||
onValueChange={setSearchSigungu}
|
||||
disabled={searchSido === ""}
|
||||
>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sigunguOptions.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.adstCd}
|
||||
value={opt.adstCd}
|
||||
>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증일시
|
||||
</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(val) =>
|
||||
setDateRange(
|
||||
val
|
||||
? { from: val.from!, to: val.to }
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
className="w-[20rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={searchStatsCd}
|
||||
onValueChange={setSearchStatsCd}
|
||||
placeholder="통계항목 선택"
|
||||
className="w-[22rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button color="gray" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>검색</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pb-4">
|
||||
<DataTable
|
||||
table={historyTable}
|
||||
variant="secondary"
|
||||
onRowClick={(row) => {
|
||||
setSelectedHistory(row);
|
||||
setSelectedDetail(null);
|
||||
}}
|
||||
viewPagination={true}
|
||||
className="[&_td]:!p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{/* 영역 B: 상세 데이터 그리드 */}
|
||||
<div className="card card-border bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="pl-2 text-[1.6rem] font-bold text-title">
|
||||
이상치 데이터
|
||||
</h2>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={
|
||||
!detailData ||
|
||||
detailData.content.length === 0 ||
|
||||
isDownloading
|
||||
}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{isDetailLoading ? (
|
||||
<div className="flex h-[39rem] w-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedHistory || gridRowData.length === 0 ? (
|
||||
<div className="flex h-[39rem] w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
{!selectedHistory
|
||||
? "항목을 선택해주세요."
|
||||
: "표시할 항목이 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<DataGrid
|
||||
columnInfo={imrDetailColumnInfo}
|
||||
rowData={gridRowData}
|
||||
setRowData={setGridRowData}
|
||||
idFieldName="rsltSn"
|
||||
isExcelExport={false}
|
||||
isExcelImport={false}
|
||||
highlightedRowId={selectedDetail?.rsltSn}
|
||||
onRowClick={(e) => {
|
||||
if (!e.anlsSn) return;
|
||||
const row = detailData?.content?.find(
|
||||
(item) =>
|
||||
String(item.rsltSn) ===
|
||||
String(e.rsltSn),
|
||||
);
|
||||
if (row) setSelectedDetail(row);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 영역 C: Line Chart */}
|
||||
<div className="m-2 flex h-[45rem] items-center justify-center rounded-sm border bg-white p-4">
|
||||
{isChartLoading ? (
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
) : lineChartData ? (
|
||||
<Line options={chartOptions} data={lineChartData} />
|
||||
) : (
|
||||
<div className="text-[1.4rem]">
|
||||
이상치 데이터를 선택하면 차트가 표시됩니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImrVerificationHistory;
|
||||
@@ -1,13 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeSelect } from "@/entities/code/ui/code-select";
|
||||
import {
|
||||
hbArticleColumnInfo,
|
||||
imrArticleColumnInfo,
|
||||
validationArticleColumnInfo,
|
||||
} from "@/entities/statistics/model/verification-article-column";
|
||||
} from "@/entities/statistics";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type {
|
||||
VerificationTargetManagementResponse,
|
||||
VerificationTargetManagementSearchParams,
|
||||
} from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Tabs,
|
||||
@@ -15,24 +32,18 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code/ui/code-select";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
VerificationTargetManagementSearchParams,
|
||||
VerificationTargetManagementResponse,
|
||||
} from "@/entities/statistics/model/verification-target-management-interface";
|
||||
|
||||
import GeneralVerificationManagement from "./GeneralVerificationManagement";
|
||||
import { VerificationTargetDialog } from "./VerificationTargetDialog";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
|
||||
const TAB_CONFIG = {
|
||||
HB: { value: "HB", label: "H-B", columnInfo: hbArticleColumnInfo },
|
||||
IMR: { value: "IMR", label: "I-MR", columnInfo: imrArticleColumnInfo },
|
||||
VV: {
|
||||
value: "VV",
|
||||
label: "Validation",
|
||||
GENERAL: {
|
||||
value: "GENERAL",
|
||||
label: "일반",
|
||||
columnInfo: validationArticleColumnInfo,
|
||||
},
|
||||
} as const;
|
||||
@@ -54,6 +65,8 @@ function VerificationArticleManagement() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedData, setSelectedData] =
|
||||
useState<VerificationTargetManagementResponse | null>(null);
|
||||
const [resultDialogOpen, setResultDialogOpen] = useState(false);
|
||||
const [resultDialogMessage, setResultDialogMessage] = useState("");
|
||||
|
||||
const { data: verificationData } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, searchParams],
|
||||
@@ -64,6 +77,7 @@ function VerificationArticleManagement() {
|
||||
vrfcMthdCd: searchParams.vrfcMthdCd,
|
||||
statsCd: searchParams.statsCd || undefined,
|
||||
vrfcTrgtNm: searchParams.vrfcTrgtNm || undefined,
|
||||
sort: ["statsCd,asc", "vrfcTrgtNm,asc"],
|
||||
},
|
||||
);
|
||||
return response;
|
||||
@@ -104,6 +118,30 @@ function VerificationArticleManagement() {
|
||||
});
|
||||
}
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: VerificationTargetManagementResponse) =>
|
||||
verificationTargetManagementApi.saveVerificationTargetManagement({
|
||||
vrfcMthdCd: data.vrfcMthdCd || activeTab,
|
||||
statsCd: data.statsCd,
|
||||
schmNm: data.schmNm,
|
||||
tblNm: data.tblNm,
|
||||
colNm: data.colNm,
|
||||
pkColNm: data.pkColNm,
|
||||
ipcrCntVl: data.ipcrCntVl,
|
||||
pctlCrtrVl: data.pctlCrtrVl,
|
||||
scopCntCntVl: data.scopCntCntVl,
|
||||
thrsCntVl: data.thrsCntVl,
|
||||
useYn: data.useYn,
|
||||
vrfcTrgtNm: data.vrfcTrgtNm,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG],
|
||||
});
|
||||
setDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: VerificationTargetManagementResponse) =>
|
||||
verificationTargetManagementApi.updateVerificationTargetManagement(
|
||||
@@ -144,6 +182,23 @@ function VerificationArticleManagement() {
|
||||
},
|
||||
});
|
||||
|
||||
const checkColumnsMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
verificationTargetManagementApi.checkColumnsExist({
|
||||
vrfcMthdCd: activeTab,
|
||||
}),
|
||||
onSuccess: (response) => {
|
||||
setResultDialogMessage(
|
||||
response.message || "컬럼 검사가 완료되었습니다.",
|
||||
);
|
||||
setResultDialogOpen(true);
|
||||
},
|
||||
onError: () => {
|
||||
setResultDialogMessage("컬럼 검사 중 오류가 발생했습니다.");
|
||||
setResultDialogOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
function handleRowClick(rowData: Record<string, string | number>) {
|
||||
setSelectedData(
|
||||
rowData as unknown as VerificationTargetManagementResponse,
|
||||
@@ -151,6 +206,11 @@ function VerificationArticleManagement() {
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function handleRegister() {
|
||||
setSelectedData(null);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
@@ -158,7 +218,11 @@ function VerificationArticleManagement() {
|
||||
|
||||
async function handleSave(data: VerificationTargetManagementResponse) {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
updateMutation.mutate(data);
|
||||
if (data.vrfcTrgtSn) {
|
||||
updateMutation.mutate(data);
|
||||
} else {
|
||||
saveMutation.mutate(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,94 +232,144 @@ function VerificationArticleManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCheckColumns() {
|
||||
if (!(await confirm("컬럼 검사를 실행하시겠습니까?"))) return;
|
||||
checkColumnsMutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue="HB"
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
>
|
||||
<TabsList>
|
||||
<>
|
||||
<Tabs
|
||||
defaultValue="HB"
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
>
|
||||
<TabsList>
|
||||
{Object.entries(TAB_CONFIG).map(([key, config]) => (
|
||||
<TabsTrigger
|
||||
key={key}
|
||||
value={config.value}
|
||||
className="!px-[2rem] !py-[0.8rem] !text-[1.2rem]"
|
||||
>
|
||||
{config.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{activeTab !== "GENERAL" && (
|
||||
<div className="card card-border !mx-0">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-center gap-6 pl-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="whitespace-nowrap text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
placeholder="선택"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="whitespace-nowrap text-label">
|
||||
검증명
|
||||
</Label>
|
||||
<Input
|
||||
value={vrfcTrgtNm}
|
||||
onChange={(e) =>
|
||||
setVrfcTrgtNm(e.target.value)
|
||||
}
|
||||
placeholder="검증명"
|
||||
className="w-[20rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
onClick={handleCheckColumns}
|
||||
disabled={
|
||||
checkColumnsMutation.isPending
|
||||
}
|
||||
>
|
||||
컬럼 검사
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleRegister}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
<Button type="submit" color="green">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={handleInitialize}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(TAB_CONFIG).map(([key, config]) => (
|
||||
<TabsTrigger
|
||||
<TabsContent
|
||||
key={key}
|
||||
value={config.value}
|
||||
className="!px-[2rem] !py-[0.8rem] !text-[1.2rem]"
|
||||
className={cn(
|
||||
activeTab === "GENERAL" &&
|
||||
"!mt-[0.6rem] border-none p-0 shadow-none",
|
||||
)}
|
||||
>
|
||||
{config.label}
|
||||
</TabsTrigger>
|
||||
{activeTab !== "GENERAL" ? (
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowData={rowData}
|
||||
setRowData={() => {}}
|
||||
onRowClick={handleRowClick}
|
||||
isExcelImport={false}
|
||||
/>
|
||||
) : (
|
||||
<GeneralVerificationManagement />
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<div className="card card-border !mx-0">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="whitespace-nowrap px-4 text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
placeholder="선택"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="whitespace-nowrap px-4 text-label">
|
||||
검증명
|
||||
</Label>
|
||||
<Input
|
||||
value={vrfcTrgtNm}
|
||||
onChange={(e) => setVrfcTrgtNm(e.target.value)}
|
||||
placeholder="검증명"
|
||||
className="w-[20rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
color="green"
|
||||
className="h-[3.6rem] w-[80px]"
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
className="h-[3.6rem] w-[80px]"
|
||||
onClick={handleInitialize}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
<VerificationTargetDialog
|
||||
tabValue={activeTab}
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
data={selectedData}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</Tabs>
|
||||
<Dialog open={resultDialogOpen} onOpenChange={setResultDialogOpen}>
|
||||
<DialogContent
|
||||
className="max-w-[60rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>컬럼 검사 결과</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[50rem] overflow-y-auto whitespace-pre-wrap break-words text-[1.4rem]">
|
||||
{resultDialogMessage}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{Object.entries(TAB_CONFIG).map(([key, config]) => (
|
||||
<TabsContent key={key} value={config.value}>
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowData={rowData}
|
||||
setRowData={() => {}}
|
||||
onRowClick={handleRowClick}
|
||||
isExcelImport={false}
|
||||
/>
|
||||
</TabsContent>
|
||||
))}
|
||||
|
||||
<VerificationTargetDialog
|
||||
tabValue={activeTab}
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
data={selectedData}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setResultDialogOpen(false)}>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { CodeSelect } from "@/entities/code/ui/code-select";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type {
|
||||
NumericColumnDto,
|
||||
VerificationTargetManagementResponse,
|
||||
} from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
type VerificationTargetManagementFormData,
|
||||
verificationTargetManagementSchema,
|
||||
} from "@/entities/statistics/model/verification-target-management-schema";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -12,17 +28,10 @@ import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code/ui/code-select";
|
||||
import type { VerificationTargetManagementResponse } from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
verificationTargetManagementSchema,
|
||||
type VerificationTargetManagementFormData,
|
||||
} from "@/entities/statistics/model/verification-target-management-schema";
|
||||
import { useEffect } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import type { TabValue } from "./VerificationArticleManagement";
|
||||
|
||||
interface VerificationTargetDialogProps {
|
||||
@@ -42,24 +51,49 @@ export function VerificationTargetDialog({
|
||||
onSave,
|
||||
onDelete,
|
||||
}: VerificationTargetDialogProps) {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<VerificationTargetManagementFormData>({
|
||||
resolver: zodResolver(verificationTargetManagementSchema),
|
||||
defaultValues: data || undefined,
|
||||
});
|
||||
const { control, handleSubmit, reset, watch, getValues, setValue } =
|
||||
useForm<VerificationTargetManagementFormData>({
|
||||
resolver: zodResolver(verificationTargetManagementSchema),
|
||||
defaultValues: data || {
|
||||
vrfcMthdCd: tabValue,
|
||||
statsCd: "",
|
||||
vrfcTrgtNm: "",
|
||||
schmNm: "",
|
||||
tblNm: "",
|
||||
colNm: "",
|
||||
pkColNm: "",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
const watchStatsCd = watch("statsCd");
|
||||
|
||||
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
|
||||
const [numericColumns, setNumericColumns] = useState<NumericColumnDto[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && open) reset(data);
|
||||
}, [data, open, reset]);
|
||||
if (open) {
|
||||
reset(
|
||||
data || {
|
||||
vrfcMthdCd: tabValue,
|
||||
statsCd: "",
|
||||
vrfcTrgtNm: "",
|
||||
schmNm: "",
|
||||
tblNm: "",
|
||||
colNm: "",
|
||||
pkColNm: "",
|
||||
useYn: "Y",
|
||||
},
|
||||
);
|
||||
} else {
|
||||
setNumericColumns([]);
|
||||
}
|
||||
}, [data, open, reset, tabValue]);
|
||||
|
||||
function onSubmit(formData: VerificationTargetManagementFormData) {
|
||||
if (!data) return;
|
||||
onSave({
|
||||
...data,
|
||||
...(data || {}),
|
||||
...formData,
|
||||
} as VerificationTargetManagementResponse);
|
||||
}
|
||||
@@ -70,307 +104,468 @@ export function VerificationTargetDialog({
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) return null;
|
||||
async function handleSearchTableMetadata(e?: React.MouseEvent) {
|
||||
e?.preventDefault();
|
||||
const statsCd = getValues("statsCd");
|
||||
if (!statsCd) {
|
||||
alert("통계항목을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata =
|
||||
await verificationTargetManagementApi.getTableInfo(statsCd);
|
||||
|
||||
setValue("schmNm", metadata.schmNm, { shouldValidate: true });
|
||||
setValue("tblNm", metadata.tblNm, { shouldValidate: true });
|
||||
|
||||
if (metadata.pkColumns && metadata.pkColumns.length > 0) {
|
||||
setValue("pkColNm", metadata.pkColumns.join(", "), {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
|
||||
setNumericColumns(metadata.numericColumns || []);
|
||||
setIsColumnDialogOpen(true);
|
||||
} catch (error) {
|
||||
alert("테이블 정보를 조회하는 중 오류가 발생했습니다.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenColumnDialog(e?: React.MouseEvent) {
|
||||
e?.preventDefault();
|
||||
if (numericColumns.length === 0) {
|
||||
handleSearchTableMetadata();
|
||||
} else {
|
||||
setIsColumnDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectColumn(colNm: string) {
|
||||
setValue("colNm", colNm, { shouldValidate: true });
|
||||
setIsColumnDialogOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>검증 항목 상세</DialogTitle>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{data ? "검증 항목 상세" : "검증 항목 등록"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Table variant="secondary">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="w-[150px] bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
통계항목
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="statsCd"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={field.value}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
placeholder="선택"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead
|
||||
className="w-[150px] bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
검증명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="vrfcTrgtNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="검증명"
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="w-[150px] bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
통계항목
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Controller
|
||||
name="statsCd"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={field.value}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
placeholder="선택"
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
스키마명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="schmNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="스키마명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
테이블명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="tblNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="테이블명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
컬럼명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="colNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="컬럼명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
PK 키
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="pkColNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="PK 키"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{tabValue !== "IMR" && (
|
||||
<TableRow>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
중요도 계수(μ)
|
||||
<Button
|
||||
className="px-4"
|
||||
onClick={
|
||||
handleSearchTableMetadata
|
||||
}
|
||||
disabled={!watchStatsCd}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableHead
|
||||
className="w-[150px] bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
검증명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="ipcrCntVl"
|
||||
name="vrfcTrgtNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
placeholder="검증명"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
스키마명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="schmNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 1, 0.1 단위)"
|
||||
{...field}
|
||||
placeholder="스키마명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
백분위수 기준값(pct)
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
테이블명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="pctlCrtrVl"
|
||||
name="tblNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="0.5"
|
||||
step="0.1"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 0.5, 0.1 단위)"
|
||||
{...field}
|
||||
placeholder="테이블명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{tabValue !== "IMR" && (
|
||||
<TableRow>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
스케일 조정 계수(A)
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
컬럼명
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="scopCntCntVl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Controller
|
||||
name="colNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 1, 0.01 단위)"
|
||||
{...field}
|
||||
placeholder="컬럼명"
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
color="white"
|
||||
className="whitespace-nowrap px-4"
|
||||
onClick={handleOpenColumnDialog}
|
||||
disabled={!watchStatsCd}
|
||||
>
|
||||
목록보기
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
임계 계수(c)
|
||||
<TableHead
|
||||
className="bg-muted font-medium"
|
||||
data-required
|
||||
>
|
||||
PK 키
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="thrsCntVl"
|
||||
name="pkColNm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 100, 1 단위)"
|
||||
{...field}
|
||||
placeholder="PK 키"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<Button type="submit" color="green">
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{tabValue !== "IMR" && (
|
||||
<TableRow>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
중요도 계수(μ)
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="ipcrCntVl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 1, 0.1 단위)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
백분위수 기준값(pct)
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="pctlCrtrVl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="0.5"
|
||||
step="0.1"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 0.5, 0.1 단위)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{tabValue !== "IMR" && (
|
||||
<TableRow>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
스케일 조정 계수(A)
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="scopCntCntVl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 1, 0.01 단위)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead className="bg-muted font-medium">
|
||||
임계 계수(c)
|
||||
</TableHead>
|
||||
<TableCell>
|
||||
<Controller
|
||||
name="thrsCntVl"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
)
|
||||
: null,
|
||||
)
|
||||
}
|
||||
placeholder="(0 ~ 100, 1 단위)"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<DialogFooter className="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
{data && (
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" color="green">
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={isColumnDialogOpen}
|
||||
onOpenChange={setIsColumnDialogOpen}
|
||||
>
|
||||
<DialogContent className="max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>숫자형 컬럼 목록</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
<Table variant="default">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="bg-muted text-center font-bold">
|
||||
컬럼명
|
||||
</TableHead>
|
||||
<TableHead className="bg-muted text-center font-bold">
|
||||
데이터 타입
|
||||
</TableHead>
|
||||
<TableHead className="bg-muted text-center font-bold">
|
||||
코멘트
|
||||
</TableHead>
|
||||
<TableHead className="bg-muted text-center font-bold">
|
||||
선택
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{numericColumns.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="py-8 text-center text-gray-500"
|
||||
>
|
||||
조회된 숫자형 컬럼이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
numericColumns.map((col, idx) => (
|
||||
<TableRow key={idx}>
|
||||
<TableCell className="text-center">
|
||||
{col.columnName}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{col.dataType}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{col.columnComment}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleSelectColumn(
|
||||
col.columnName,
|
||||
)
|
||||
}
|
||||
>
|
||||
입력하기
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import Indicators from "./Indicators";
|
||||
import RegionalGrid from "./RegionalGrid";
|
||||
import ScatterChart from "./ScatterChart";
|
||||
import VerificationList from "./VerificationList";
|
||||
|
||||
interface HbVerificationDetailProps {
|
||||
anlsSn: number;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export interface AppliedFilter {
|
||||
sido: string | null;
|
||||
gugun: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
anlsSn,
|
||||
onBack,
|
||||
}) => {
|
||||
const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [checkedSns, setCheckedSns] = useState<string[]>([]);
|
||||
const [appliedFilter, setAppliedFilter] = useState<AppliedFilter>({
|
||||
sido: "",
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
|
||||
const [selectedTargetInfo, setSelectedTargetInfo] = useState<{
|
||||
statsNm: string;
|
||||
vrfcTrgtNm: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleTargetSelect = (row: StAnlsDtlResponse) => {
|
||||
setSelectedVrfcTrgtSn(String(row.vrfcTrgtSn));
|
||||
setSelectedTargetInfo({
|
||||
statsNm: row.statsNm,
|
||||
vrfcTrgtNm: row.vrfcTrgtNm,
|
||||
});
|
||||
setSelectedRowId(null);
|
||||
setAppliedFilter({
|
||||
sido: "",
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-end justify-between px-4 py-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={onBack}
|
||||
className=""
|
||||
>
|
||||
<i className="diveicon di-arrow-back" />
|
||||
<span className="sr-only">목록으로 돌아가기</span>
|
||||
</Button>
|
||||
<h2 className="text-[1.8rem] font-bold text-title">
|
||||
검증 상세
|
||||
</h2>
|
||||
</div>
|
||||
<Button disabled={checkedSns.length === 0}>검증확정</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="h-[20rem] overflow-y-auto">
|
||||
<VerificationList
|
||||
anlsSn={anlsSn}
|
||||
selectedVrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
onSelect={handleTargetSelect}
|
||||
checkedSns={checkedSns}
|
||||
onCheck={setCheckedSns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card card-border !m-4">
|
||||
<h3 className="p-4 pb-0 text-[1.8rem] font-bold text-title">
|
||||
검증 결과 상세
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<div className="flex-1 p-4">
|
||||
<RegionalGrid
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
statsNm={selectedTargetInfo?.statsNm || ""}
|
||||
vrfcTrgtNm={
|
||||
selectedTargetInfo?.vrfcTrgtNm || ""
|
||||
}
|
||||
onFilterChange={setAppliedFilter}
|
||||
selectedRowId={selectedRowId}
|
||||
appliedFilter={appliedFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4 pt-[4.8rem]">
|
||||
<Indicators
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
/>
|
||||
|
||||
<ScatterChart
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
appliedFilter={appliedFilter}
|
||||
onPointClick={setSelectedRowId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HbVerificationDetail;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
interface IndicatorsProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
}
|
||||
|
||||
const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
|
||||
const { data: chartData, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"chart",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
|
||||
const dtl = chartData?.dtl;
|
||||
|
||||
if (!vrfcTrgtSn) {
|
||||
return (
|
||||
<div className="card card-border flex min-h-[10rem] items-center justify-center gap-6 text-[1.4rem] text-muted-foreground">
|
||||
항목을 선택해주세요
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IndicatorItem = ({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: number | null | undefined;
|
||||
}) => (
|
||||
<div className="flex flex-1 flex-col gap-2 overflow-hidden rounded border bg-gray-50 p-2">
|
||||
<h4
|
||||
className="truncate text-[1.2rem] font-medium text-label"
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</h4>
|
||||
<div className="truncate text-[1.6rem] font-bold">
|
||||
{value != null ? Number(value).toFixed(4) : "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 grid min-h-[10rem] grid-cols-4 gap-2">
|
||||
<IndicatorItem label="r 중위수" value={dtl?.rtCnterVl} />
|
||||
<IndicatorItem
|
||||
label="(E_m)E 중위수"
|
||||
value={dtl?.lastTrsfScrCnterVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_l)E 하한값"
|
||||
value={dtl?.lastTrsfScrLlmtVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_u)E 상한값"
|
||||
value={dtl?.lastTrsfScrUplmtVl}
|
||||
/>
|
||||
<IndicatorItem label="E_D_L" value={dtl?.llmtVl} />
|
||||
<IndicatorItem label="E_D_U" value={dtl?.uplmtVl} />
|
||||
<IndicatorItem label="이상치 하한선" value={dtl?.otlrLlmtLine} />
|
||||
<IndicatorItem label="이상치 상한선" value={dtl?.otlrUplmtLine} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indicators;
|
||||
@@ -0,0 +1,276 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type {
|
||||
AdstZoneResponseDto,
|
||||
StAnlsRsltResponse,
|
||||
} from "@/entities/statistics";
|
||||
import {
|
||||
useStAnlsRsltList,
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns";
|
||||
import {
|
||||
Button,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import type { AppliedFilter } from "./HbVerificationDetail";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
|
||||
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
|
||||
|
||||
interface RegionalGridProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
statsNm: string;
|
||||
vrfcTrgtNm: string;
|
||||
onFilterChange: (filter: AppliedFilter) => void;
|
||||
selectedRowId?: number | null;
|
||||
appliedFilter: AppliedFilter;
|
||||
}
|
||||
|
||||
const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
statsNm,
|
||||
vrfcTrgtNm,
|
||||
onFilterChange,
|
||||
selectedRowId,
|
||||
appliedFilter,
|
||||
}) => {
|
||||
const [selectedSido, setSelectedSido] = useState<string>("");
|
||||
const [selectedGugun, setSelectedGugun] = useState<string>("");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||
const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] =
|
||||
useState<boolean>(true);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
const { data: sidoData } = useStAnlsSidoCombo();
|
||||
const { data: gugunData } = useStAnlsSigunguCombo(selectedSido);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const { data: resultData, isLoading } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
|
||||
|
||||
if (
|
||||
!appliedFilter.sido &&
|
||||
!appliedFilter.gugun &&
|
||||
!appliedFilter.status
|
||||
) {
|
||||
data = showAllOnEmptyFilter ? data : [];
|
||||
} else {
|
||||
const sidoCd = appliedFilter.sido;
|
||||
if (sidoCd) {
|
||||
data = data.filter((item) =>
|
||||
item.admdstCd.startsWith(sidoCd.slice(0, 2)),
|
||||
);
|
||||
}
|
||||
|
||||
const gugunCd = appliedFilter.gugun;
|
||||
if (gugunCd) {
|
||||
data = data.filter((item) => item.admdstCd === gugunCd);
|
||||
}
|
||||
|
||||
if (appliedFilter.status) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.otlrYnCd.toLowerCase() === appliedFilter.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapOtlrToLabel = (otlrYn: string) => {
|
||||
switch (otlrYn) {
|
||||
case "0":
|
||||
return "정상";
|
||||
case "1":
|
||||
return "이상치";
|
||||
case "2":
|
||||
return "제외";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const mappedData = data.map((item) => ({
|
||||
...item,
|
||||
otlrYn: mapOtlrToLabel(item.otlrYnCd),
|
||||
}));
|
||||
|
||||
return mappedData;
|
||||
}, [resultData, appliedFilter, showAllOnEmptyFilter]);
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
setSelectedSido(val);
|
||||
setSelectedGugun("");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
onFilterChange({
|
||||
sido: selectedSido,
|
||||
gugun: selectedGugun,
|
||||
status: selectedStatus,
|
||||
});
|
||||
setShowAllOnEmptyFilter(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedSido("");
|
||||
setSelectedGugun("");
|
||||
setSelectedStatus("");
|
||||
onFilterChange({
|
||||
sido: "",
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
setShowAllOnEmptyFilter(true);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!vrfcTrgtSn) return;
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
const blob = await stAnlsRsltApi.downloadExcel(
|
||||
anlsSn,
|
||||
Number(vrfcTrgtSn),
|
||||
);
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `분석결과_${statsNm}-${vrfcTrgtNm}.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error("Failed to download excel", error);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setGridRowData(rowData as Record<string, string | number>[]);
|
||||
}, [rowData]);
|
||||
|
||||
useEffect(() => {
|
||||
handleReset();
|
||||
}, [vrfcTrgtSn]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedSido || ""}
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
<SelectValue placeholder="시도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((opt) => (
|
||||
<SelectItem key={opt.adstCd} value={opt.adstCd}>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedGugun}
|
||||
onValueChange={setSelectedGugun}
|
||||
disabled={!selectedSido}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((opt) => (
|
||||
<SelectItem key={opt.adstCd} value={opt.adstCd}>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onValueChange={setSelectedStatus}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] bg-white">
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">정상</SelectItem>
|
||||
<SelectItem value="1">이상치</SelectItem>
|
||||
<SelectItem value="2">제외</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>조회</Button>
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleDownload}
|
||||
disabled={!vrfcTrgtSn || isDownloading}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (!vrfcTrgtSn || rowData.length === 0) && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
{vrfcTrgtSn
|
||||
? "표시할 항목이 없습니다."
|
||||
: "항목을 선택해주세요."}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowData={gridRowData}
|
||||
setRowData={setGridRowData}
|
||||
idFieldName="rsltSn"
|
||||
isExcelExport={false}
|
||||
isExcelImport={false}
|
||||
highlightedRowId={selectedRowId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionalGrid;
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Scatter } from "react-chartjs-2";
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
type ChartData,
|
||||
type ChartOptions,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
ScatterController,
|
||||
Tooltip,
|
||||
type TooltipItem,
|
||||
} from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics";
|
||||
import {
|
||||
useStAnlsChartData,
|
||||
useStAnlsRsltList,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import type { AppliedFilter } from "./HbVerificationDetail";
|
||||
|
||||
ChartJS.register(
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
ScatterController,
|
||||
Legend,
|
||||
Tooltip,
|
||||
zoomPlugin,
|
||||
);
|
||||
|
||||
interface ScatterChartProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
appliedFilter: AppliedFilter;
|
||||
onPointClick?: (rsltSn: number | null) => void;
|
||||
}
|
||||
|
||||
interface LinePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface DataPoint extends LinePoint {
|
||||
item: StAnlsRsltResponse;
|
||||
}
|
||||
|
||||
/** 모든 점의 x·y를 합쳐 두 축이 같은 [min, max] 구간을 쓰도록 할 때 사용 */
|
||||
function getUnifiedLinearRange(data: ChartData<"scatter">): {
|
||||
min: number;
|
||||
max: number;
|
||||
} | null {
|
||||
const values: number[] = [];
|
||||
for (const ds of data.datasets) {
|
||||
for (const pt of ds.data) {
|
||||
if (pt && typeof pt === "object" && "x" in pt && "y" in pt) {
|
||||
const p = pt as LinePoint;
|
||||
if (Number.isFinite(p.x)) values.push(p.x);
|
||||
if (Number.isFinite(p.y)) values.push(p.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (values.length === 0) return null;
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
if (min === max) {
|
||||
const pad = Math.abs(min) * 0.05 || 1;
|
||||
return { min: min - pad, max: max + pad };
|
||||
}
|
||||
const padding = (max - min) * 0.02;
|
||||
return { min: min - padding, max: max + padding };
|
||||
}
|
||||
|
||||
function matchesAdmdstFilter(
|
||||
admdstCd: string,
|
||||
sidoCd: string | null | undefined,
|
||||
gugunCd: string | undefined,
|
||||
): boolean {
|
||||
const gugun = (gugunCd ?? "").trim();
|
||||
if (gugun) {
|
||||
return admdstCd === gugun;
|
||||
}
|
||||
const sido = (sidoCd ?? "").trim();
|
||||
if (!sido) {
|
||||
return true;
|
||||
}
|
||||
return admdstCd.startsWith(sido.slice(0, 2));
|
||||
}
|
||||
|
||||
const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
appliedFilter,
|
||||
onPointClick,
|
||||
}) => {
|
||||
const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
|
||||
const isLoading = isLoadingGrid || isLoadingChart;
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
if (!resultData || !chartData) return null;
|
||||
|
||||
const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[];
|
||||
const boundaries = chartData.list ?? [];
|
||||
|
||||
const sortedBoundaries = [...boundaries].sort(
|
||||
(a, b) => a.xprev - b.xprev,
|
||||
);
|
||||
|
||||
const upperLineData: LinePoint[] = sortedBoundaries.map((b) => ({
|
||||
x: b.yupper,
|
||||
y: b.xprev,
|
||||
}));
|
||||
|
||||
const lowerLineData: LinePoint[] = sortedBoundaries.map((b) => ({
|
||||
x: b.xprev,
|
||||
y: b.ylower,
|
||||
}));
|
||||
|
||||
const normalPoints: DataPoint[] = [];
|
||||
const outlierPoints: DataPoint[] = [];
|
||||
|
||||
rawPoints.forEach((item) => {
|
||||
if (
|
||||
!matchesAdmdstFilter(
|
||||
item.admdstCd,
|
||||
appliedFilter.sido,
|
||||
appliedFilter.gugun,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
appliedFilter.status &&
|
||||
item.otlrYnCd.toLowerCase() !== appliedFilter.status
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pt: DataPoint = {
|
||||
x: Number(item.prvyrVl ?? 0),
|
||||
y: Number(item.cryrVl ?? 0),
|
||||
item,
|
||||
};
|
||||
|
||||
if (item.otlrYnCd === "0") {
|
||||
normalPoints.push(pt);
|
||||
} else {
|
||||
outlierPoints.push(pt);
|
||||
}
|
||||
});
|
||||
|
||||
const data: ChartData<"scatter"> = {
|
||||
datasets: [
|
||||
{
|
||||
label: "상한선",
|
||||
data: upperLineData,
|
||||
showLine: true,
|
||||
borderColor: "rgba(255, 99, 132, 0.8)",
|
||||
pointRadius: 0,
|
||||
},
|
||||
{
|
||||
label: "하한선",
|
||||
data: lowerLineData,
|
||||
showLine: true,
|
||||
borderColor: "rgba(54, 162, 235, 0.8)",
|
||||
pointRadius: 0,
|
||||
},
|
||||
{
|
||||
label: "이상치",
|
||||
data: outlierPoints,
|
||||
backgroundColor: "rgba(255, 99, 132, 0.7)",
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
{
|
||||
label: "정상",
|
||||
data: normalPoints,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.7)",
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const axisRange = getUnifiedLinearRange(data);
|
||||
|
||||
const options: ChartOptions<"scatter"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
onClick: (event, elements) => {
|
||||
if (elements.length > 0) {
|
||||
const element = elements[0];
|
||||
if (!element) return;
|
||||
const datasetIndex = element.datasetIndex;
|
||||
const index = element.index;
|
||||
const dataset = data.datasets[datasetIndex];
|
||||
|
||||
if (!dataset) return;
|
||||
|
||||
// 상한선/하한선 클릭 시 무시
|
||||
if (dataset.showLine) return;
|
||||
|
||||
const pt = dataset.data[index] as DataPoint;
|
||||
if (pt?.item?.rsltSn) {
|
||||
onPointClick?.(pt.item.rsltSn);
|
||||
}
|
||||
} else {
|
||||
onPointClick?.(null);
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "xy",
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true,
|
||||
},
|
||||
mode: "xy",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: TooltipItem<"scatter">) => {
|
||||
const dataset = context.dataset;
|
||||
if (dataset.showLine) {
|
||||
return `${context.dataset.label}: (${context?.parsed?.x?.toFixed(2) ?? "-"}, ${context?.parsed?.y?.toFixed(2) ?? "-"})`;
|
||||
}
|
||||
|
||||
const pt = (context.raw as DataPoint) ?? {};
|
||||
return `${pt?.item?.sido ?? "시/도"} ${pt?.item?.gugun ?? "구/군"}: 이전(${pt?.x ?? "-"}), 당해(${pt?.y ?? "-"})`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "linear",
|
||||
position: "bottom",
|
||||
min: axisRange?.min,
|
||||
max: axisRange?.max,
|
||||
title: {
|
||||
display: true,
|
||||
text: "이전년도 값",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
min: axisRange?.min,
|
||||
max: axisRange?.max,
|
||||
title: {
|
||||
display: true,
|
||||
text: "당해년도 값",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { data, options };
|
||||
}, [
|
||||
resultData,
|
||||
chartData,
|
||||
appliedFilter.sido,
|
||||
appliedFilter.gugun,
|
||||
appliedFilter.status,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
{!vrfcTrgtSn ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
항목을 선택해주세요
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : chartConfig ? (
|
||||
<div className="size-[52rem]">
|
||||
<Scatter
|
||||
data={chartConfig.data}
|
||||
options={chartConfig.options}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
표시할 데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScatterChart;
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics";
|
||||
import { getVerificationListColumns } from "@/entities/statistics";
|
||||
import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api";
|
||||
import { DataTable } from "@common/components";
|
||||
|
||||
interface VerificationListProps {
|
||||
anlsSn: number;
|
||||
selectedVrfcTrgtSn: string | null;
|
||||
onSelect: (row: StAnlsDtlResponse) => void;
|
||||
checkedSns: string[];
|
||||
onCheck: (checkedSns: string[], rows: StAnlsDtlResponse[]) => void;
|
||||
}
|
||||
|
||||
const VerificationList: React.FC<VerificationListProps> = ({
|
||||
anlsSn,
|
||||
selectedVrfcTrgtSn,
|
||||
onSelect,
|
||||
checkedSns,
|
||||
onCheck,
|
||||
}) => {
|
||||
const { data: detailData, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn],
|
||||
queryFn: () =>
|
||||
stAnlsDtlApi.getDetailList({ anlsSn, page: 0, size: 100 }),
|
||||
enabled: !!anlsSn,
|
||||
});
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
return (detailData?.content ?? []) as StAnlsDtlResponse[];
|
||||
}, [detailData]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getVerificationListColumns({ rowData, checkedSns, onCheck }),
|
||||
[rowData, checkedSns, onCheck],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
columns,
|
||||
state: {
|
||||
rowSelection: selectedVrfcTrgtSn
|
||||
? { [selectedVrfcTrgtSn]: true }
|
||||
: {},
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => String(row.vrfcTrgtSn),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowClick = (row: StAnlsDtlResponse) => {
|
||||
onSelect(row);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedVrfcTrgtSn && rowData.length > 0) {
|
||||
const firstRow = rowData[0];
|
||||
if (firstRow) onSelect(firstRow);
|
||||
}
|
||||
}, [selectedVrfcTrgtSn, rowData, onSelect]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 px-4 py-2">
|
||||
{isLoading && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
데이터를 불러오는 중...
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length === 0 && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
표시할 항목이 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination={false}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationList;
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Label,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
|
||||
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
interface CopyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
row?: GeneralverificationList;
|
||||
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const CopyDialog = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
row,
|
||||
onSuccess,
|
||||
...props
|
||||
}: CopyDialogProps) => {
|
||||
|
||||
const [selectedCode, setSelectedCode] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const handleChangeCode = (value: string) => {
|
||||
if (row?.trgtStatsCd === value) {
|
||||
alert('원본과 동일한 통계항목으로 복사할 경우\n복사 완료 후 검증 분류를 꼭 수정해주세요.');
|
||||
}
|
||||
|
||||
setSelectedCode(value);
|
||||
};
|
||||
|
||||
const handleCopyItem = async () => {
|
||||
if (!selectedCode) {
|
||||
alert('복사 대상 통계항목을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm('복사하시겠습니까?')) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await generalverificationApiService.CopyVrfcData({
|
||||
vrfcSn: row?.vrfcSn ?? null,
|
||||
targetStatsCd: selectedCode ?? '',
|
||||
rgtrId: userInfo.userUnqId
|
||||
});
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('복사되었습니다.');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
alert(response.message || '데이터 복사 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 복사 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setSelectedCode('');
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{row?.vrfcNm}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading &&
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col gap-4 m-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
원본 통계항목
|
||||
</Label>
|
||||
<Input
|
||||
value={row?.trgtStatsNm}
|
||||
disabled
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
대상 통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={selectedCode}
|
||||
onValueChange={handleChangeCode}
|
||||
placeholder="선택"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button size="lg" type="button" onClick={handleCopyItem}>
|
||||
복사
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyDialog;
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { NoData } from '@/shared/ui/no-data';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import FormulaComponents from './FormulaComponents';
|
||||
import { LoadingWithAnimation } from '@common/components';
|
||||
import FomulaDialog from './FomulaDialog';
|
||||
|
||||
import { useAlertStore } from '@/shared/lib';
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface';
|
||||
import { getUserInfo } from '../../../../../../../packages/common/components/src/api';
|
||||
|
||||
interface DetailComponentProps {
|
||||
selectedVrfcRow: number | null;
|
||||
}
|
||||
|
||||
const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
|
||||
const [detailInfo, setDetailInfo] = useState<VerifyDetailData | null>(null);
|
||||
const [formulaList, setFormulaList] = useState<(VerifyDetailResponseDto | null)[]>([]);
|
||||
const [hasPersistedDetails, setHasPersistedDetails] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [showFomulaDialog, setShowFomulaDialog] = useState<boolean>(false);
|
||||
const [selectedFormula, setSelectedFormula] = useState<VerifyDetailResponseDto | null>(null);
|
||||
const [selectedFormulaIndex, setSelectedFormulaIndex] = useState<number>(0);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const persistedFormulaSignatureRef = useRef<string>('');
|
||||
|
||||
const normalizeFormulaListForCompare = (list: (VerifyDetailResponseDto | null)[]) => {
|
||||
return list.map((row) =>
|
||||
row
|
||||
? {
|
||||
vrfcDtlSn: row.vrfcDtlSn ?? null,
|
||||
refStatsCd: row.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row.engArtopnEqtnCn ?? '',
|
||||
cmprOpert: row.cmprOpert ?? null,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
const trgtStatsCd = detailInfo?.commonInfo.trgtStatsCd;
|
||||
if (!trgtStatsCd) {
|
||||
alert('대상 통계항목(trgtStatsCd) 정보가 없습니다.');
|
||||
return;
|
||||
}
|
||||
const rows = formulaList.map((row, idx) => ({
|
||||
eqtnCombSeq: idx + 1,
|
||||
trgtStatsCd,
|
||||
refStatsCd: row?.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? '',
|
||||
cmprOpert: row?.cmprOpert ?? null,
|
||||
}));
|
||||
|
||||
const UNARY_TERMINAL_OPERATORS = new Set<string>(['is_null', '!is_null', 'floor']);
|
||||
const lastOperator = rows.at(-1)?.cmprOpert ?? null;
|
||||
const lastIsUnary = rows.length > 0 && lastOperator != null && UNARY_TERMINAL_OPERATORS.has(lastOperator);
|
||||
const isSingleUnaryFormula = rows.length === 1 && lastIsUnary;
|
||||
|
||||
// 단항 연산자일 때만 row 1개 허용
|
||||
if (rows.length === 1 && !isSingleUnaryFormula) {
|
||||
alert('해당 연산자는 최소 2개 항목이 필요합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 단항 연산자(is_null, !is_null, floor)는 반드시 맨 마지막 항목에만 허용
|
||||
if (rows.length > 1) {
|
||||
const unaryAtWrongIdx = rows.findIndex(
|
||||
(r, idx) => idx < rows.length - 1 && r.cmprOpert != null && UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
|
||||
);
|
||||
if (unaryAtWrongIdx !== -1) {
|
||||
alert(`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const invalidIdx = rows.findIndex(
|
||||
(r) => r.kornArtopnEqtnCn.trim().length === 0 || r.engArtopnEqtnCn.trim().length === 0,
|
||||
);
|
||||
if (invalidIdx !== -1) {
|
||||
alert(`수식이 비어있는 항목이 있습니다.\n(순번: ${invalidIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 마지막이 단항 연산자가 아닐 때만: 마지막을 제외한 항목들은 연산자(cmprOpert) 필수
|
||||
if (rows.length > 1 && !lastIsUnary) {
|
||||
const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null);
|
||||
if (invalidOpertIdx !== -1) {
|
||||
alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 항목일 때 마지막이 단항 연산자면: 이전 항목들 연산자는 무조건 null
|
||||
if (rows.length > 1 && lastIsUnary) {
|
||||
for (let i = 0; i < rows.length - 1; i++) {
|
||||
const prev = rows[i];
|
||||
if (prev) prev.cmprOpert = null;
|
||||
}
|
||||
setFormulaList((prev) => {
|
||||
if (prev.length <= 1) return prev;
|
||||
const next = prev.map((item, idx) =>
|
||||
idx < prev.length - 1 && item != null ? { ...item, cmprOpert: null } : item,
|
||||
);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// 마지막 항목의 연산자는 (단항 연산자 케이스 제외) 무조건 null 처리
|
||||
if (rows.length > 1 && !lastIsUnary) {
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (lastRow) lastRow.cmprOpert = null;
|
||||
setFormulaList((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
const last = prev[prev.length - 1];
|
||||
if (!last) return prev;
|
||||
if (last.cmprOpert == null) return prev;
|
||||
const next = [...prev];
|
||||
next[next.length - 1] = { ...last, cmprOpert: null };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
if (await confirm('등록하시겠습니까?')) {
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
let response;
|
||||
|
||||
if (!hasPersistedDetails) {
|
||||
response = await generalverificationApiService.RegistVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, rgtrId: userInfo.userUnqId });
|
||||
} else {
|
||||
response = await generalverificationApiService.UpdateVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, mdfrId: userInfo.userUnqId });
|
||||
}
|
||||
|
||||
if (response?.status === 'success') {
|
||||
alert('등록되었습니다.');
|
||||
getVrfcDetail();
|
||||
} else {
|
||||
alert(response?.message || '데이터 등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getVrfcDetail = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await generalverificationApiService.getVrfcDetail(selectedVrfcRow!);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setDetailInfo(response.data);
|
||||
const list = response.data.verifyDetailResponseDtoList;
|
||||
setFormulaList(list?.length ? [...list] : [null]);
|
||||
setHasPersistedDetails((list?.length ?? 0) > 0);
|
||||
// "기존 데이터" 스냅샷(서버 응답 기준)을 시그니처로 저장
|
||||
persistedFormulaSignatureRef.current = JSON.stringify(
|
||||
normalizeFormulaListForCompare(list?.length ? [...list] : [null]),
|
||||
);
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickVrfcDetail = (formula?: VerifyDetailResponseDto | null, index?: number) => {
|
||||
setSelectedFormula(formula ?? null);
|
||||
setSelectedFormulaIndex(index ?? 0);
|
||||
setShowFomulaDialog(true);
|
||||
};
|
||||
|
||||
const handleFormulaConfirm = (payload: { kornArtopnEqtnCn: string; engArtopnEqtnCn: string; refStatsCd: string | null }) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
const i = selectedFormulaIndex;
|
||||
if (i < 0 || i >= next.length) return prev;
|
||||
const item = next[i];
|
||||
next[i] = item
|
||||
? {
|
||||
...item,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
}
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
cmprOpert: null,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
setShowFomulaDialog(false);
|
||||
setSelectedFormula(null);
|
||||
};
|
||||
|
||||
const handleResetClick = async () => {
|
||||
const currentSignature = JSON.stringify(normalizeFormulaListForCompare(formulaList));
|
||||
if (currentSignature === persistedFormulaSignatureRef.current) {
|
||||
alert('변경된 내용이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm(`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`)) {
|
||||
getVrfcDetail();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVrfcRow != null) {
|
||||
getVrfcDetail();
|
||||
}
|
||||
}, [selectedVrfcRow]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='card card-border w-1/2 !mx-0 flex flex-col'>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">검증 상세 정보</h3>
|
||||
</div>
|
||||
<div className="card !m-4 h-full !p-0">
|
||||
{selectedVrfcRow ?
|
||||
isLoading ?
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='flex flex-col gap-12 m-2 h-full'>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>검증명</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcNm ?? "-"}</TableCell>
|
||||
<TableHead>통계항목</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.trgtStatsNm ?? "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검증분류</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcClsfNm ?? "-"}</TableCell>
|
||||
<TableHead>소수점처리방식</TableHead>
|
||||
<TableCell className='text-center'>
|
||||
{detailInfo?.commonInfo.dcptDigtCnt === 0 ?
|
||||
'소수점 생략'
|
||||
:
|
||||
`${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className='flex flex-col gap-4 overflow-auto'>
|
||||
<h3 className="font-bold text-title">수식 정보</h3>
|
||||
{formulaList.map((data, index) => (
|
||||
<FormulaComponents
|
||||
key={data?.vrfcDtlSn ?? `empty-${index}`}
|
||||
data={data}
|
||||
index={index}
|
||||
onOperatorChange={(v) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
if (index < 0 || index >= next.length) return prev;
|
||||
const current = next[index];
|
||||
const cmprOpert = v === 'none' ? null : v;
|
||||
next[index] = current
|
||||
? { ...current, cmprOpert }
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: null,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: '',
|
||||
engArtopnEqtnCn: '',
|
||||
cmprOpert,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onDelete={() => {
|
||||
setFormulaList((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
return next.length > 0 ? next : [null];
|
||||
});
|
||||
}}
|
||||
handleFomulaDialog={(show, formula, idx) => {
|
||||
if (show) handleClickVrfcDetail(formula, idx ?? index);
|
||||
else setShowFomulaDialog(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[4rem] rounded-full"
|
||||
onClick={() => {
|
||||
setFormulaList((prev) => [...prev, null]);
|
||||
}}
|
||||
>
|
||||
<i className="ri-play-list-add-fill text-[16px]"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex gap-2 justify-end items-end">
|
||||
<Button
|
||||
type="button"
|
||||
color='red'
|
||||
title='리셋'
|
||||
onClick={handleResetClick}
|
||||
>
|
||||
<i className="diveicon di-arrow-reset" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<NoData />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<FomulaDialog
|
||||
key={showFomulaDialog ? (selectedFormula?.vrfcDtlSn ?? 'new') : 'closed'}
|
||||
open={showFomulaDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowFomulaDialog(open);
|
||||
if (!open) setSelectedFormula(null);
|
||||
}}
|
||||
data={selectedFormula}
|
||||
trgtStatsCd={detailInfo?.commonInfo.trgtStatsCd}
|
||||
trgtStatsNm={detailInfo?.commonInfo.trgtStatsNm}
|
||||
onConfirm={handleFormulaConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailComponent;
|
||||
@@ -0,0 +1,673 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
Textarea,
|
||||
Skeleton,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
import type { VerifyDetailResponseDto, VerifyColumnListResponseData, SelectedVerifyColumnListResponseData } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export interface FomulaDialogConfirmPayload {
|
||||
kornArtopnEqtnCn: string;
|
||||
engArtopnEqtnCn: string;
|
||||
/** 참조 통계코드 (선택 안 했으면 null) */
|
||||
refStatsCd: string | null;
|
||||
}
|
||||
|
||||
interface FomulaDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
data?: VerifyDetailResponseDto | null;
|
||||
trgtStatsCd?: string;
|
||||
trgtStatsNm?: string;
|
||||
/** 확인 클릭 시 검사 통과 후 호출. 해당 행에 반영 후 다이얼로그 닫기는 부모에서 처리 */
|
||||
onConfirm?: (payload: FomulaDialogConfirmPayload) => void;
|
||||
}
|
||||
|
||||
const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onConfirm }: FomulaDialogProps) => {
|
||||
const [columnInfo, setColumnInfo] = useState<VerifyColumnListResponseData | null>(null);
|
||||
const [selectedColumnInfo, setSelectedColumnInfo] = useState<SelectedVerifyColumnListResponseData | null>(null);
|
||||
const [showCommonColumns, setShowCommonColumns] = useState<boolean>(true);
|
||||
const [showTargetColumns, setShowTargetColumns] = useState<boolean>(true);
|
||||
const [showRefColumns, setShowRefColumns] = useState<boolean>(true);
|
||||
const [commonColumnSearchValue, setCommonColumnSearchValue] = useState<string>('');
|
||||
const [targetColumnSearchValue, setTargetColumnSearchValue] = useState<string>('');
|
||||
const [refColumnSearchValue, setRefColumnSearchValue] = useState<string>('');
|
||||
/** 참조 통계 - 다이얼로그 내부에서만 사용. 열 때마다 해당 행(data)의 저장값으로 초기화, 저장 시에만 부모에 반영 예정 */
|
||||
const [refStatsCdLocal, setRefStatsCdLocal] = useState<string>('');
|
||||
/** 수식 = 토큰 배열. 한글명 중복 시 출처(공통/대상/참조)별 engColNm 보존 */
|
||||
const [formulaDisplaySegments, setFormulaDisplaySegments] = useState<string[]>([]);
|
||||
const [formulaValueSegments, setFormulaValueSegments] = useState<string[]>([]);
|
||||
/** 확인 클릭 시 검사 실패 메시지 (Textarea 아래 빨간 글씨) */
|
||||
const [formulaSaveError, setFormulaSaveError] = useState<string | null>(null);
|
||||
/** 참조 통계 변경 시 안내 메시지 */
|
||||
const [refStatsChangeNotice, setRefStatsChangeNotice] = useState<string | null>(null);
|
||||
|
||||
const formulaTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
/** 다이얼로그에서 쓰는 기호 → 제출용 value. 백엔드 규격에 맞게 수정 가능 */
|
||||
const OPERATOR_SYMBOL_TO_VALUE: Record<string, string> = {
|
||||
'+': '+',
|
||||
'-': '-',
|
||||
'×': '*',
|
||||
'%': '/',
|
||||
'(': '(',
|
||||
')': ')',
|
||||
};
|
||||
|
||||
/** 공통 → 대상 → 참조 순, 첫 등장 kornColNm만 사용 (로드/직접입력 시 파싱용) */
|
||||
const getKornToEngMapForParse = (): Map<string, string> => {
|
||||
const map = new Map<string, string>();
|
||||
const refCols = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? [];
|
||||
[...(columnInfo?.commonColumnInfo ?? []), ...(columnInfo?.targetColumnInfo?.colList ?? []), ...refCols].forEach(
|
||||
(item) => {
|
||||
if (!map.has(item.kornColNm)) map.set(item.kornColNm, item.engColNm);
|
||||
}
|
||||
);
|
||||
return map;
|
||||
};
|
||||
|
||||
/** 한글 수식 문자열을 토큰(표시/값) 배열로 파싱. 컬럼명은 longest match */
|
||||
const parseDisplayToSegments = (displayStr: string): { display: string[]; value: string[] } => {
|
||||
if (displayStr.length === 0) return { display: [], value: [] };
|
||||
const kornToEng = getKornToEngMapForParse();
|
||||
const kornList = [...kornToEng.keys()].filter(Boolean).sort((a, b) => b.length - a.length);
|
||||
const display: string[] = [];
|
||||
const value: string[] = [];
|
||||
let i = 0;
|
||||
while (i < displayStr.length) {
|
||||
let matched = false;
|
||||
for (const korn of kornList) {
|
||||
if (displayStr.slice(i, i + korn.length) === korn) {
|
||||
display.push(korn);
|
||||
value.push(kornToEng.get(korn) ?? korn);
|
||||
i += korn.length;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
const ch = displayStr[i];
|
||||
if (ch != null) {
|
||||
display.push(ch);
|
||||
value.push(ch);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return { display, value };
|
||||
};
|
||||
|
||||
const handleFormulaKey = (val: string) => {
|
||||
setFormulaSaveError(null);
|
||||
if (val === 'backspace') {
|
||||
setFormulaDisplaySegments((prev) => prev.slice(0, -1));
|
||||
setFormulaValueSegments((prev) => prev.slice(0, -1));
|
||||
} else if (val === 'ac') {
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
} else {
|
||||
setFormulaDisplaySegments((prev) => [...prev, val]);
|
||||
setFormulaValueSegments((prev) => [...prev, val]);
|
||||
}
|
||||
};
|
||||
|
||||
/** 컬럼 버튼: 한글 표시 + 해당 출처의 영문명 저장 (한글 중복 시에도 구분) */
|
||||
const handleFormulaColumn = (kornColNm: string, engColNm: string) => {
|
||||
setFormulaSaveError(null);
|
||||
setFormulaDisplaySegments((prev) => [...prev, kornColNm]);
|
||||
setFormulaValueSegments((prev) => [...prev, engColNm]);
|
||||
};
|
||||
|
||||
/** 제출/저장 시 사용할 영문 수식 (컬럼 engColNm + 연산자 기호 → value 치환) */
|
||||
const getFormulaValueForSubmit = (): string =>
|
||||
formulaValueSegments.map((seg) => OPERATOR_SYMBOL_TO_VALUE[seg] ?? seg).join('');
|
||||
|
||||
const OPERATORS = ['+', '-', '×', '/', '%'] as const;
|
||||
const isOperator = (s: string) => s.length === 1 && (OPERATORS as readonly string[]).includes(s);
|
||||
const isParen = (s: string) => s === '(' || s === ')';
|
||||
const isValueToken = (s: string) =>
|
||||
s.length > 1 || (s.length === 1 && !isOperator(s) && !isParen(s));
|
||||
const isDigit = (s: string) => s.length === 1 && /[0-9]/.test(s);
|
||||
|
||||
/** 저장 시 수식 규칙 검사. 오류 시 메시지 반환, 통과 시 null */
|
||||
const validateFormulaForSave = (segments: string[]): string | null => {
|
||||
if (segments.length === 0) return null;
|
||||
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
const a = segments[i];
|
||||
const b = segments[i + 1];
|
||||
if (a != null && b != null && isValueToken(a) && isValueToken(b) && !(isDigit(a) && isDigit(b))) {
|
||||
return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.';
|
||||
}
|
||||
}
|
||||
|
||||
// "(값)"도 하나의 값으로 취급: 값/닫는괄호 뒤에는 '(' 또는 값이 올 수 없고, 사이에 연산자가 있어야 함.
|
||||
// 예: 컬럼(100), 값)(100) 같은 형태는 연산자 누락으로 간주
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
const a = segments[i];
|
||||
const b = segments[i + 1];
|
||||
if (a == null || b == null) continue;
|
||||
// 연속 숫자(예: 50)는 예외 허용
|
||||
if (isDigit(a) && isDigit(b)) continue;
|
||||
|
||||
const aIsValueBoundary = isValueToken(a) || a === ')';
|
||||
const bIsValueBoundary = b === '(' || isValueToken(b);
|
||||
if (aIsValueBoundary && bIsValueBoundary) {
|
||||
return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.';
|
||||
}
|
||||
}
|
||||
|
||||
const last = segments[segments.length - 1];
|
||||
if (last && isOperator(last)) {
|
||||
return '수식은 기호로 끝날 수 없습니다. (닫는 괄호 제외)';
|
||||
}
|
||||
|
||||
const stack: boolean[] = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const s = segments[i];
|
||||
if (s == null) continue;
|
||||
if (s === '(') {
|
||||
stack.push(false);
|
||||
} else if (s === ')') {
|
||||
if (stack.length === 0) return '닫는 괄호가 여는 괄호 없이 사용되었습니다.';
|
||||
const hadValue = stack.pop();
|
||||
if (!hadValue) return '괄호 안에 값이 있어야 합니다.';
|
||||
if (stack.length > 0) stack[stack.length - 1] = true;
|
||||
} else if (stack.length > 0 && isValueToken(s)) {
|
||||
stack[stack.length - 1] = true;
|
||||
}
|
||||
}
|
||||
if (stack.length > 0) return '여는 괄호에 대응하는 닫는 괄호가 없습니다.';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getVerifyColumnList = async () => {
|
||||
try {
|
||||
if (!trgtStatsCd) return;
|
||||
let response;
|
||||
|
||||
if (data?.vrfcDtlSn == null || data.vrfcDtlSn <= 0) {
|
||||
response = await generalverificationApiService.getSelectedVerifyColumnList(trgtStatsCd)
|
||||
} else {
|
||||
response = await generalverificationApiService.getVerifyColumnList({
|
||||
trgtStatsCd,
|
||||
vrfcDtlSn: data.vrfcDtlSn,
|
||||
refStatsCd: data.refStatsCd,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (response?.status === 'success') {
|
||||
setColumnInfo(response.data);
|
||||
} else {
|
||||
alert(response?.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedVerifyColumnList = async (statsCd: string) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.getSelectedVerifyColumnList(statsCd);
|
||||
if (response?.status === 'success' && response.data) {
|
||||
setSelectedColumnInfo({
|
||||
targetColumnInfo: response.data.targetColumnInfo ?? { statsCd: statsCd, colList: [] },
|
||||
commonColumnInfo: response.data.commonColumnInfo ?? [],
|
||||
});
|
||||
} else {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setColumnInfo(null);
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
if (open) void getVerifyColumnList();
|
||||
}, [open, trgtStatsCd, data?.vrfcDtlSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setRefStatsCdLocal(data?.refStatsCd ?? '');
|
||||
}, [open, data?.refStatsCd, data?.vrfcDtlSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSelectedColumnInfo(null);
|
||||
setRefStatsCdLocal('');
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
setFormulaSaveError(null);
|
||||
setRefStatsChangeNotice(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const str = data?.kornArtopnEqtnCn ?? '';
|
||||
const { display, value } = parseDisplayToSegments(str);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}
|
||||
}, [open, data?.kornArtopnEqtnCn, data?.vrfcDtlSn]);
|
||||
|
||||
/**
|
||||
* 재오픈 직후에는 columnInfo가 아직 없어서 parseDisplayToSegments가 컬럼명을 매칭 못하고
|
||||
* 한글을 1글자씩 토큰화하는 경우가 있음.
|
||||
* 컬럼 목록 로딩 후(= map이 채워진 후) "표시 문자열이 원본과 동일 + 값 토큰이 표시 토큰과 동일"한 케이스만 재파싱해서
|
||||
* engColNm 치환이 반영되도록 보정한다.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const src = data?.kornArtopnEqtnCn ?? '';
|
||||
if (!src) return;
|
||||
|
||||
const displayJoined = formulaDisplaySegments.join('');
|
||||
const valueJoined = formulaValueSegments.join('');
|
||||
// 아직 컬럼 치환이 안 된 상태(= value가 display와 동일)에서만 재파싱
|
||||
if (displayJoined !== src) return;
|
||||
if (valueJoined !== displayJoined) return;
|
||||
|
||||
// 컬럼 목록이 준비된 이후에만 의미가 있음
|
||||
const hasColumns =
|
||||
(columnInfo?.commonColumnInfo?.length ?? 0) > 0 ||
|
||||
(columnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0 ||
|
||||
(columnInfo?.refColumnInfo?.colList?.length ?? 0) > 0 ||
|
||||
(selectedColumnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0;
|
||||
if (!hasColumns) return;
|
||||
|
||||
const { display, value } = parseDisplayToSegments(src);
|
||||
const nextDisplayJoined = display.join('');
|
||||
const nextValueJoined = value.join('');
|
||||
// 파싱 결과가 현재와 완전히 같으면 setState 반복으로 무한 렌더가 날 수 있으니 중단
|
||||
if (nextDisplayJoined === displayJoined && nextValueJoined === valueJoined) return;
|
||||
// 컬럼 치환이 전혀 안 됐다면(= value가 display와 동일) 보정의 의미가 없으니 중단
|
||||
if (nextValueJoined === nextDisplayJoined) return;
|
||||
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}, [
|
||||
open,
|
||||
data?.kornArtopnEqtnCn,
|
||||
columnInfo?.commonColumnInfo,
|
||||
columnInfo?.targetColumnInfo?.colList,
|
||||
columnInfo?.refColumnInfo?.colList,
|
||||
selectedColumnInfo?.targetColumnInfo?.colList,
|
||||
formulaDisplaySegments,
|
||||
formulaValueSegments,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || columnInfo?.kornArtopnEqtnCn == null) return;
|
||||
if (data?.kornArtopnEqtnCn !== columnInfo.kornArtopnEqtnCn) return;
|
||||
const { display, value } = parseDisplayToSegments(columnInfo.kornArtopnEqtnCn);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}, [open, data?.kornArtopnEqtnCn, columnInfo?.kornArtopnEqtnCn]);
|
||||
|
||||
const handleChangeRefStatsCd = (next: string) => {
|
||||
if (!open) return;
|
||||
|
||||
// 현재 refStatsCdLocal에 해당하는 참조 컬럼이 수식에 포함돼 있으면 변경을 막고 안내만 표시
|
||||
const currentRefCols = refColList ?? [];
|
||||
const currentRefEngSet = new Set(currentRefCols.map((c) => c.engColNm).filter(Boolean));
|
||||
const hasCurrentRefToken =
|
||||
currentRefEngSet.size > 0 && formulaValueSegments.some((seg) => currentRefEngSet.has(seg));
|
||||
|
||||
if (hasCurrentRefToken) {
|
||||
setRefStatsChangeNotice('수식에 기존 참조 컬럼이 포함되어 있어 참조 통계를 변경할 수 없습니다. 먼저 해당 참조 컬럼을 제거해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setRefStatsChangeNotice(null);
|
||||
setRefStatsCdLocal(next);
|
||||
};
|
||||
|
||||
/** 참조 통계 선택 시 해당 통계의 컬럼 목록으로 교체 */
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (refStatsCdLocal) {
|
||||
void getSelectedVerifyColumnList(refStatsCdLocal);
|
||||
} else {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
}, [open, refStatsCdLocal]);
|
||||
|
||||
/** 참조 컬럼: 선택한 참조 통계로 조회한 목록이 있으면 사용, 없으면 getVerifyColumnList 결과 사용 */
|
||||
const refColList = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? [];
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) {
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
setFormulaSaveError(null);
|
||||
setSelectedColumnInfo(null);
|
||||
setRefStatsCdLocal('');
|
||||
setCommonColumnSearchValue('');
|
||||
setTargetColumnSearchValue('');
|
||||
setRefColumnSearchValue('');
|
||||
}
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
|
||||
const handleSearchColumn = (value: string, type: string) => {
|
||||
if (type === 'common') {
|
||||
setCommonColumnSearchValue(value);
|
||||
} else if (type === 'target') {
|
||||
setTargetColumnSearchValue(value);
|
||||
} else {
|
||||
setRefColumnSearchValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeSearch = (v: string) => v.replace(/\s+/g, "").toLowerCase();
|
||||
|
||||
const commonSearch = normalizeSearch(commonColumnSearchValue);
|
||||
const commonColumns = columnInfo?.commonColumnInfo ?? [];
|
||||
const filteredCommonColumns =
|
||||
commonSearch.length > 0
|
||||
? commonColumns.filter((c) => {
|
||||
const korn = normalizeSearch(c.kornColNm ?? '');
|
||||
const eng = normalizeSearch(c.engColNm ?? '');
|
||||
return korn.includes(commonSearch) || eng.includes(commonSearch);
|
||||
})
|
||||
: commonColumns;
|
||||
|
||||
const targetSearch = normalizeSearch(targetColumnSearchValue);
|
||||
const targetColumns = columnInfo?.targetColumnInfo?.colList ?? [];
|
||||
const filteredTargetColumns =
|
||||
targetSearch.length > 0
|
||||
? targetColumns.filter((t) => {
|
||||
const korn = normalizeSearch(t.kornColNm ?? '');
|
||||
const eng = normalizeSearch(t.engColNm ?? '');
|
||||
return korn.includes(targetSearch) || eng.includes(targetSearch);
|
||||
})
|
||||
: targetColumns;
|
||||
|
||||
const refSearch = normalizeSearch(refColumnSearchValue);
|
||||
const filterdRefColumns =
|
||||
refSearch.length > 0
|
||||
? refColList.filter((r) => {
|
||||
const korn = normalizeSearch(r.kornColNm ?? '');
|
||||
const eng = normalizeSearch(r.engColNm ?? '');
|
||||
return korn.includes(refSearch) || eng.includes(refSearch);
|
||||
})
|
||||
: refColList;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined} className="max-w-[100rem] w-full max-h-[90%] overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
수식 상세 정보
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-start gap-20 p-4 border rounded-xl">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-bold text-title text-[14px]">기호 및 숫자</h3>
|
||||
<div className="grid grid-cols-3 gap-2 w-max">
|
||||
<Button type="button" value="+" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('+')}>
|
||||
<span className="text-[18px]">+</span>
|
||||
</Button>
|
||||
<Button type="button" value="-" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('-')}>
|
||||
<span className="text-[18px]">-</span>
|
||||
</Button>
|
||||
<Button type="button" value="*" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('×')}>
|
||||
<span className="text-[18px]">×</span>
|
||||
</Button>
|
||||
<Button type="button" value="/" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('%')}>
|
||||
<span className="text-[18px]">%</span>
|
||||
</Button>
|
||||
<Button type="button" value="(" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('(')}>
|
||||
<span className="text-[18px]">(</span>
|
||||
</Button>
|
||||
<Button type="button" value=")" color="gray" className="w-[65px]" onClick={() => handleFormulaKey(')')}>
|
||||
<span className="text-[18px]">)</span>
|
||||
</Button>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 0, '⌫', 'AC'].map((n) => {
|
||||
const keyVal = n !== 'AC' && n !== '⌫' ? String(n) : n === '⌫' ? 'backspace' : 'ac';
|
||||
return (
|
||||
<Button
|
||||
key={n}
|
||||
type="button"
|
||||
value={keyVal}
|
||||
color={n !== 'AC' && n !== '⌫' ? 'white' : 'primary'}
|
||||
className="w-[65px]"
|
||||
onClick={() => handleFormulaKey(keyVal)}
|
||||
>
|
||||
<span className="text-[18px]">{n}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<h3 className="font-bold text-title text-[14px]">수식 미리보기</h3>
|
||||
<Textarea
|
||||
className="w-full flex-1 bg-gray-100 resize-none !text-3xl p-4 !leading-loose text-gray-600 font-bold"
|
||||
id="formulaDisplayInput"
|
||||
ref={formulaTextareaRef}
|
||||
value={formulaDisplaySegments.join('')}
|
||||
onChange={(e) => {
|
||||
setFormulaSaveError(null);
|
||||
const { display, value } = parseDisplayToSegments(e.target.value);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}}
|
||||
rows={4}
|
||||
readOnly
|
||||
/>
|
||||
{formulaSaveError && (
|
||||
<p className="text-sm text-red-600 mt-1" role="alert">
|
||||
{formulaSaveError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-title text-[14px]">공통 컬럼</h3>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowCommonColumns((prev) => !prev)}
|
||||
>
|
||||
{showCommonColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
</div>
|
||||
{showCommonColumns &&
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
className="w-[20rem] rounded-sm !text-[1.3rem] focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value={commonColumnSearchValue}
|
||||
onChange={(e) => handleSearchColumn(e.target.value, 'common')}
|
||||
placeholder="검색어를 입력하세요."
|
||||
/>
|
||||
</div>}
|
||||
{showCommonColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{columnInfo?.commonColumnInfo?.length && columnInfo.commonColumnInfo.length > 0 ?
|
||||
filteredCommonColumns.map((c) => (
|
||||
<Button
|
||||
key={c.engColNm}
|
||||
type="button"
|
||||
value={c.engColNm}
|
||||
size="sm"
|
||||
color="lightGray"
|
||||
onClick={() => handleFormulaColumn(c.kornColNm, c.engColNm)}
|
||||
>
|
||||
{c.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
<Skeleton className="h-[12rem] w-full rounded-xl" />
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-title text-[14px]">{trgtStatsNm} 컬럼</h3>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowTargetColumns((prev) => !prev)}
|
||||
>
|
||||
{showTargetColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
</div>
|
||||
{showTargetColumns &&
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
className="w-[20rem] rounded-sm !text-[1.3rem] focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value={targetColumnSearchValue}
|
||||
onChange={(e) => handleSearchColumn(e.target.value, 'target')}
|
||||
placeholder="검색어를 입력하세요."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{showTargetColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{columnInfo?.targetColumnInfo?.colList.length && columnInfo.targetColumnInfo.colList.length > 0 ?
|
||||
filteredTargetColumns.map((t) => (
|
||||
<Button
|
||||
key={t.engColNm}
|
||||
type="button"
|
||||
value={t.engColNm}
|
||||
size="sm"
|
||||
onClick={() => handleFormulaColumn(t.kornColNm, t.engColNm)}
|
||||
>
|
||||
{t.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
<Skeleton className="h-[12rem] w-full rounded-xl" />
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
<h3 className="font-bold text-title text-[14px]">참조 컬럼</h3>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={refStatsCdLocal}
|
||||
onValueChange={handleChangeRefStatsCd}
|
||||
placeholder="선택"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
{(refColList?.length ?? 0) > 0 ?
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowRefColumns((prev) => !prev)}
|
||||
>
|
||||
{showRefColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{refStatsChangeNotice && (
|
||||
<p className="text-[12px] text-red-600" role="alert">
|
||||
{refStatsChangeNotice}
|
||||
</p>
|
||||
)}
|
||||
{(refStatsCdLocal && showRefColumns) &&
|
||||
<div className="mb-2">
|
||||
<Input
|
||||
className="w-[20rem] rounded-sm !text-[1.3rem] focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value={refColumnSearchValue}
|
||||
onChange={(e) => handleSearchColumn(e.target.value, 'ref')}
|
||||
placeholder="검색어를 입력하세요."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{showRefColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(filterdRefColumns?.length ?? 0) > 0 ?
|
||||
filterdRefColumns.map((r) => (
|
||||
<Button
|
||||
key={r.engColNm}
|
||||
type="button"
|
||||
value={r.engColNm}
|
||||
size="sm"
|
||||
color="sky"
|
||||
onClick={() => handleFormulaColumn(r.kornColNm, r.engColNm)}
|
||||
>
|
||||
{r.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const err = validateFormulaForSave(formulaValueSegments);
|
||||
if (err) {
|
||||
setFormulaSaveError(err);
|
||||
requestAnimationFrame(() => {
|
||||
formulaTextareaRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
formulaTextareaRef.current?.focus?.();
|
||||
});
|
||||
return;
|
||||
}
|
||||
setFormulaSaveError(null);
|
||||
const kornArtopnEqtnCn = formulaDisplaySegments.join('');
|
||||
const engArtopnEqtnCn = getFormulaValueForSubmit();
|
||||
const isEmptyFormula = kornArtopnEqtnCn.trim().length === 0;
|
||||
onConfirm?.({
|
||||
kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn,
|
||||
refStatsCd: isEmptyFormula ? null : (refStatsCdLocal ? refStatsCdLocal : null),
|
||||
});
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default FomulaDialog;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Button, Input } from "@common/components";;
|
||||
import OperatorsComponents from "./OperatorsComponents";
|
||||
import type { VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
interface FormulaComponentsProps {
|
||||
data?: VerifyDetailResponseDto | null;
|
||||
index?: number;
|
||||
onDelete?: () => void;
|
||||
onOperatorChange?: (value: string) => void;
|
||||
handleFomulaDialog?: (show: boolean, data?: VerifyDetailResponseDto | null, index?: number) => void;
|
||||
}
|
||||
|
||||
const FormulaComponents = ({ data, index, onDelete, onOperatorChange, handleFomulaDialog }: FormulaComponentsProps) => {
|
||||
const cmprOpert = data?.cmprOpert;
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-4 mb-2 p-2 justify-around rounded has-[.formulaDeleteBtn:hover]:bg-gray-100'>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[3rem] shrink-0 rounded-full formulaDeleteBtn"
|
||||
color='red'
|
||||
onClick={onDelete}
|
||||
>
|
||||
<i className="ri-subtract-fill text-[16px]"></i>
|
||||
</Button>
|
||||
<Input
|
||||
className='h-[4rem] !cursor-pointer'
|
||||
placeholder='클릭하여 수식을 등록해주세요.'
|
||||
id='formulaInput'
|
||||
value={data ? data.kornArtopnEqtnCn : ''}
|
||||
title={data ? data.kornArtopnEqtnCn : ''}
|
||||
onClick={() => handleFomulaDialog?.(true, data, index)}
|
||||
readOnly
|
||||
/>
|
||||
<OperatorsComponents cmprOpert={cmprOpert} onValueChange={onOperatorChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormulaComponents;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectValue,
|
||||
SelectItem,
|
||||
} from "@common/components";
|
||||
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
|
||||
type OperatorItem = {
|
||||
opertSn: string;
|
||||
opertSymbl: string;
|
||||
opertKornNm: string;
|
||||
};
|
||||
|
||||
interface OperatorsComponentsProps {
|
||||
cmprOpert?: string | null;
|
||||
onValueChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const OperatorsComponents = ({
|
||||
cmprOpert,
|
||||
onValueChange,
|
||||
placeholder = "연산자 없음",
|
||||
}: OperatorsComponentsProps) => {
|
||||
const [operators, setOperators] = useState<OperatorItem[]>([]);
|
||||
const [localValue, setLocalValue] = useState<string>(cmprOpert ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(cmprOpert ?? "");
|
||||
}, [cmprOpert]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOperators = async () => {
|
||||
const response = await generalverificationApiService.getVrfcOperators();
|
||||
if (response.status === 'success' && response.data) {
|
||||
setOperators(response.data);
|
||||
}
|
||||
};
|
||||
fetchOperators();
|
||||
}, []);
|
||||
|
||||
// onValueChange가 있으면 부모가 제어(controlled), 없으면 내부 state로 표시
|
||||
const isControlled = onValueChange !== undefined;
|
||||
const value = (isControlled ? (cmprOpert ?? "") : localValue) || "";
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => {
|
||||
setLocalValue(v);
|
||||
onValueChange?.(v);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="!w-[150px] min-w-[150px] max-w-[150px] h-[4rem] overflow-hidden focus:outline-none focus:ring-0">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[150px]">
|
||||
<SelectItem value='none'>
|
||||
연산자 없음
|
||||
</SelectItem>
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.opertSn} value={op.opertSymbl}>
|
||||
{op.opertKornNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorsComponents;
|
||||
@@ -0,0 +1,615 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, memo, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Label,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectValue,
|
||||
SelectItem,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
|
||||
import { type GeneralVerificationMasterSubmitFormData, generalVerificationMasterSubmitSchema } from '@/entities/statistics/model/general-verification-master-submit-schema';
|
||||
import type { UpdateVrfcMasterRequest, RegistVrfcMasterRequest } from "@/entities/statistics/model/general-verification-interface";
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
import { UserApi } from "@/entities/user/api/user-service";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
|
||||
|
||||
interface RegistDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
vrfcSn?: number;
|
||||
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const RegistDialog = memo(
|
||||
({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
vrfcSn,
|
||||
onSuccess,
|
||||
...props
|
||||
}: RegistDialogProps) => {
|
||||
|
||||
const isVrfcSn = vrfcSn != null;
|
||||
const [checkVrfcNm, setCheckVrfcNm] = useState<boolean>(false);
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: detailData } = useQuery({
|
||||
queryKey: [`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, vrfcSn],
|
||||
queryFn: () => generalverificationApiService.getGeneralverificationInfo(vrfcSn!),
|
||||
enabled: open && vrfcSn != null,
|
||||
});
|
||||
|
||||
const form = useForm<GeneralVerificationMasterSubmitFormData>({
|
||||
resolver: zodResolver(generalVerificationMasterSubmitSchema),
|
||||
values: {
|
||||
vrfcSn: detailData?.data.vrfcSn ?? undefined,
|
||||
vrfcNm: detailData?.data.vrfcNm ?? '',
|
||||
vrfcClsfCd: detailData?.data.vrfcClsfCd ?? '',
|
||||
trgtStatsCd: detailData?.data.trgtStatsCd ?? '',
|
||||
trgtStatsNm: detailData?.data.trgtStatsNm ?? '',
|
||||
dcptDigtCnt: detailData?.data.dcptDigtCnt ?? 0,
|
||||
dcptPrcsMthCd: detailData?.data.dcptPrcsMthCd ?? '',
|
||||
useYn: detailData?.data.useYn ?? 'Y',
|
||||
rgtrId: detailData?.data.rgtrId ?? '',
|
||||
regDt: detailData?.data.regDt ?? '',
|
||||
mdfrId: detailData?.data.mdfrId ?? '',
|
||||
mdfcnDt: detailData?.data.mdfcnDt ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const dcptDigtCnt = form.watch('dcptDigtCnt');
|
||||
const rgtrId = form.watch('rgtrId');
|
||||
const mdfrId = form.watch('mdfrId');
|
||||
const trgtStatsCd = form.watch('trgtStatsCd');
|
||||
const vrfcNm = form.watch('vrfcNm');
|
||||
|
||||
const { data: registrarUser } = useQuery({
|
||||
queryKey: ['user', 'rgtr', rgtrId],
|
||||
queryFn: () => UserApi.findByUserUnqId(rgtrId!),
|
||||
enabled: open && !!rgtrId,
|
||||
});
|
||||
|
||||
const { data: modifierUser } = useQuery({
|
||||
queryKey: ['user', 'mdfr', mdfrId],
|
||||
queryFn: () => UserApi.findByUserUnqId(mdfrId!),
|
||||
enabled: open && !!mdfrId,
|
||||
});
|
||||
|
||||
const originalVrfcNmRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (detailData?.data.vrfcNm && originalVrfcNmRef.current == null) {
|
||||
originalVrfcNmRef.current = detailData.data.vrfcNm;
|
||||
}
|
||||
}, [detailData]);
|
||||
|
||||
/** 중복 여부 반환: true = 중복, false = 사용 가능 */
|
||||
const runVerificationNameCheck = async (trgtStatsCdVal: string, vrfcNmVal: string, vrfcClsfCd: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await generalverificationApiService.checkVerificationName({
|
||||
trgtStatsCd: trgtStatsCdVal,
|
||||
vrfcNm: vrfcNmVal,
|
||||
vrfcClsfCd: vrfcClsfCd,
|
||||
});
|
||||
if (response.status === 'success') {
|
||||
const isDuplicate = response.data;
|
||||
setCheckVrfcNm(isDuplicate);
|
||||
return isDuplicate;
|
||||
}
|
||||
alert('중복검사 중 오류가 발생했습니다.');
|
||||
setCheckVrfcNm(true);
|
||||
return true;
|
||||
} catch {
|
||||
alert('중복검사 중 오류가 발생했습니다.');
|
||||
setCheckVrfcNm(true);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.UpdateVrfcMaster(data as UpdateVrfcMasterRequest);
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('수정되었습니다.');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
alert(response.message ?? '수정 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('수정 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegistVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.RegistVrfcMaster(data as RegistVrfcMasterRequest);
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('등록되었습니다.');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
alert(response.message ?? '등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteVrfcMaster = async () => {
|
||||
const deleteMsg = `${detailData?.data.vrfcNm} 검증룰을
|
||||
삭제하시겠습니까?`;
|
||||
|
||||
if (await confirm(deleteMsg)) {
|
||||
try {
|
||||
const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn!);
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('삭제되었습니다.');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
alert(response.message ?? '삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!vrfcNm) return;
|
||||
|
||||
const original = originalVrfcNmRef.current;
|
||||
// 수정 모드: 기준값이 아직 안 세팅됐으면 검사 안 함(다이얼로그 막 열었을 때)
|
||||
if (isVrfcSn && original === null) return;
|
||||
// 수정 모드: 사용자가 검증명을 바꾸지 않았으면 검사 안 함
|
||||
if (original !== null && original === vrfcNm) {
|
||||
setCheckVrfcNm(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// const timer = setTimeout(() => {
|
||||
// runVerificationNameCheck(trgtStatsCd ?? '', vrfcNm, vrfcClsfCd);
|
||||
// }, 500);
|
||||
|
||||
// return () => clearTimeout(timer);
|
||||
}, [vrfcNm, open, isVrfcSn]);
|
||||
|
||||
const onSubmit = async (data: GeneralVerificationMasterSubmitFormData) => {
|
||||
const currentVrfcNm = (data.vrfcNm ?? '').trim();
|
||||
const originalVrfcNm = originalVrfcNmRef.current;
|
||||
|
||||
// 수정 모드인데 기존값과 동일하면 중복검사 완전 생략
|
||||
const skipNameCheck =
|
||||
isVrfcSn &&
|
||||
(originalVrfcNm === null || currentVrfcNm === originalVrfcNm);
|
||||
|
||||
if (!skipNameCheck && currentVrfcNm) {
|
||||
// 제출 시점에 중복검사 실행
|
||||
const isDuplicate = await runVerificationNameCheck(
|
||||
data.trgtStatsCd ?? '',
|
||||
currentVrfcNm,
|
||||
data.vrfcClsfCd,
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmMessage = isVrfcSn
|
||||
? "수정 하시겠습니까?"
|
||||
: "등록 하시겠습니까?";
|
||||
if (await confirm(confirmMessage)) {
|
||||
const userInfo = await getUserInfo();
|
||||
if (isVrfcSn) {
|
||||
const updateData = {
|
||||
vrfcSn: data.vrfcSn,
|
||||
vrfcNm: data.vrfcNm,
|
||||
vrfcClsfCd: data.vrfcClsfCd,
|
||||
dcptDigtCnt: data.dcptDigtCnt,
|
||||
useYn: data.useYn,
|
||||
...(data.dcptDigtCnt === 0
|
||||
? {}
|
||||
: { dcptPrcsMthCd: data.dcptPrcsMthCd }),
|
||||
trgtStatsCd: data.trgtStatsCd,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
};
|
||||
await handleUpdateVrfcMaster(updateData);
|
||||
} else {
|
||||
const registData = {
|
||||
vrfcNm: data.vrfcNm,
|
||||
vrfcClsfCd: data.vrfcClsfCd,
|
||||
dcptDigtCnt: data.dcptDigtCnt,
|
||||
dcptPrcsMthCd: data.dcptPrcsMthCd,
|
||||
trgtStatsCd: data.trgtStatsCd,
|
||||
rgtrId: userInfo.userUnqId
|
||||
};
|
||||
await handleRegistVrfcMaster(registData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset();
|
||||
setCheckVrfcNm(false);
|
||||
originalVrfcNmRef.current = null;
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isVrfcSn ?
|
||||
detailData ? detailData.data.vrfcNm
|
||||
:
|
||||
<Skeleton className="h-[14px] rounded-xl w-2/3" />
|
||||
: "일반 검증 등록"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
void onSubmit(data);
|
||||
})}
|
||||
>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="75%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>* 통계항목</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trgtStatsCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={field.value ?? ''}
|
||||
onValueChange={field.onChange}
|
||||
placeholder="전체"
|
||||
className="w-[16rem]"
|
||||
disabled={isVrfcSn}
|
||||
/>
|
||||
</FormControl>
|
||||
{form.formState.errors.trgtStatsCd && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.trgtStatsCd.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>* 검증명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="vrfcNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="검증명을 입력해주세요"
|
||||
disabled={(trgtStatsCd ?? '') === ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
{checkVrfcNm &&
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
이미 등록된 검증명입니다.
|
||||
</p>
|
||||
}
|
||||
{form.formState.errors.vrfcNm && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.vrfcNm.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* <Button color="white" className="mt-2">중복검사</Button> */}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>* 검증분류</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="vrfcClsfCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">공통 검증</SelectItem>
|
||||
<SelectItem value="DO_SI">도단위 검증</SelectItem>
|
||||
<SelectItem value="SI_GU">시단위 검증</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{form.formState.errors.vrfcClsfCd && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.vrfcClsfCd.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>* 소수점자리수</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dcptDigtCnt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? 0}
|
||||
className="!text-[1.3rem] w-[10rem]"
|
||||
placeholder="소수점자리수를 입력해주세요"
|
||||
type="number"
|
||||
min={0}
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-gray-500 text-base !mt-0">
|
||||
※ 0일시 소수점자리 생략
|
||||
</p>
|
||||
{form.formState.errors.dcptDigtCnt && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.dcptDigtCnt.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소수점처리방식</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dcptPrcsMthCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={Number(dcptDigtCnt ?? 0) === 0}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="CEIL">올림</SelectItem>
|
||||
<SelectItem value="ROUND">반올림</SelectItem>
|
||||
<SelectItem value="FLOOR">내림</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<p className="text-gray-500 text-base !mt-0">
|
||||
※ 소수점자리가 1일시 두 번째 자리에서 올림/반올림/내림 처리<br />
|
||||
예시 - 14.16 → 14.2
|
||||
</p>
|
||||
{form.formState.errors.dcptPrcsMthCd && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.dcptPrcsMthCd.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isVrfcSn &&
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{form.formState.errors.useYn && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.useYn.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>등록자</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rgtrId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
{registrarUser?.userName != null ? (
|
||||
<span className="!text-[1.3rem]">{registrarUser.userName}</span>
|
||||
) : field.value ? (
|
||||
<Skeleton className="h-9 w-24 rounded-xl" />
|
||||
) : (
|
||||
<span className="!text-[1.3rem]">-</span>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>등록일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="regDt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
{field.value != null && field.value != '' ?
|
||||
<span className="!text-[1.3rem]">{field.value}</span>
|
||||
:
|
||||
<span className="!text-[1.3rem]">-</span>
|
||||
}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>수정자</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mdfrId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
{modifierUser?.userName != null ? (
|
||||
<span className="!text-[1.3rem]">{modifierUser.userName}</span>
|
||||
) : field.value ? (
|
||||
<Skeleton className="h-9 w-24 rounded-xl" />
|
||||
) : (
|
||||
<span className="!text-[1.3rem]">-</span>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>수정일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mdfcnDt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
{field.value != null && field.value != '' ?
|
||||
<span className="!text-[1.3rem]">{field.value}</span>
|
||||
:
|
||||
<span className="!text-[1.3rem]">-</span>
|
||||
}
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DialogFooter>
|
||||
{vrfcSn != null &&
|
||||
<Button size="lg" type="button" color="red" onClick={handleDeleteVrfcMaster}>
|
||||
삭제
|
||||
</Button>
|
||||
}
|
||||
<Button size="lg" type="submit">
|
||||
{vrfcSn != null ? '수정' : '등록'}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
})
|
||||
|
||||
RegistDialog.displayName = 'RegistDialog';
|
||||
|
||||
export default RegistDialog;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { ValueType } from "realgrid";
|
||||
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
|
||||
// RealGrid 타입 정의
|
||||
/** setColumns에 전달하는 컬럼 정보 (footer 배열/단일 객체 포함) */
|
||||
@@ -10,8 +11,10 @@ export type RealGridColumnInfo = {
|
||||
editable?: boolean;
|
||||
valueType?: import("realgrid").ValueType;
|
||||
dataFormat?: string;
|
||||
width?: number;
|
||||
styleName?: string;
|
||||
mergeRule?: { criteria: "value" };
|
||||
mergeRule?: { criteria: "value" | string };
|
||||
sortable?: boolean;
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
@@ -130,9 +133,13 @@ export type ColumnConfig = {
|
||||
editable?: boolean; // 셀 편집 가능 여부
|
||||
/** 그리드에서 컬럼 숨김 (수식용 필드 등) */
|
||||
visible?: boolean;
|
||||
/** 컬럼 너비 (px). 지정 시 ... 생략 없이 텍스트 표시에 유리 */
|
||||
width?: number;
|
||||
/** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */
|
||||
styleName?: string;
|
||||
mergeCells?: boolean;
|
||||
/** 병합 시 그룹 기준 필드(예: 시군구). 지정 시 같은 그룹 내에서만 value 기준 병합 */
|
||||
mergeGroupField?: string;
|
||||
sortdisable?: boolean;
|
||||
sortPreserveGroupOrder?: SortPreserveGroupOrder;
|
||||
valueType?: ValueType;
|
||||
@@ -211,6 +218,10 @@ export type CellStyleCallbackParams = {
|
||||
|
||||
export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined;
|
||||
|
||||
/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */
|
||||
export type CellTooltipCallbackParams = { row: Record<string, string | number>; fieldName: string };
|
||||
export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined;
|
||||
|
||||
export type DataGridProps = {
|
||||
columnInfo: ColumnInfo;
|
||||
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
|
||||
@@ -225,11 +236,20 @@ export type DataGridProps = {
|
||||
>;
|
||||
/** 셀 단위로 styleName을 동적으로 결정 (예: 검증 오류 셀) */
|
||||
cellStyleCallback?: CellStyleCallback;
|
||||
/** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */
|
||||
cellTooltipCallback?: CellTooltipCallback;
|
||||
/** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
|
||||
getGridDataRef?: {
|
||||
current: (() => Record<string, string | number>[]) | null;
|
||||
};
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
statusInfo?: SewerageSupplyRateStatusInfo | null;
|
||||
/** 강조 및 스크롤할 행의 ID */
|
||||
highlightedRowId?: string | number | null;
|
||||
/** ID로 사용할 필드명 (기본값: 'id') */
|
||||
idFieldName?: string;
|
||||
/** 전체 정렬 기능 on/off (기본값: false) */
|
||||
enableSort?: boolean;
|
||||
};
|
||||
|
||||
export type TabDataGridProps = {
|
||||
@@ -248,6 +268,12 @@ export type TabDataGridProps = {
|
||||
activeTab?: string;
|
||||
/** 활성 탭 변경 시 호출 (제어 모드). activeTab과 함께 넘기면 부모가 탭 상태를 가짐 */
|
||||
onActiveTabChange?: (tabKey: string) => void;
|
||||
/** 저장 등 시점에 현재 활성 탭 그리드 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
|
||||
getGridDataRef?: {
|
||||
current: (() => Record<string, string | number>[]) | null;
|
||||
};
|
||||
/** 전체 정렬 기능 on/off (기본값: false) */
|
||||
enableSort?: boolean;
|
||||
};
|
||||
|
||||
export type SplitDataGridProps = {
|
||||
@@ -264,4 +290,6 @@ export type SplitDataGridProps = {
|
||||
setRightRowData: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string | number>[]>
|
||||
>;
|
||||
/** 전체 정렬 기능 on/off (기본값: false) */
|
||||
enableSort?: boolean;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ export type SearchFormValues = {
|
||||
order?: string;
|
||||
fctName?: string;
|
||||
bsnmCd?: string;
|
||||
isDetail?: boolean;
|
||||
};
|
||||
|
||||
export type SearchFormProps = {
|
||||
@@ -14,6 +15,7 @@ export type SearchFormProps = {
|
||||
order?: boolean;
|
||||
fctName?: boolean;
|
||||
userMenual?: boolean;
|
||||
isDetail?: boolean;
|
||||
onSubmit?: (values: SearchFormValues) => void;
|
||||
onReset?: () => void;
|
||||
onValuesChange?: (values: SearchFormValues) => void;
|
||||
@@ -27,4 +29,5 @@ export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' |
|
||||
order: false,
|
||||
fctName: false,
|
||||
userMenual: false,
|
||||
isDetail: false,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export const doList = [
|
||||
"4100000000",
|
||||
"4200000000",
|
||||
"4300000000",
|
||||
"4400000000",
|
||||
"4500000000",
|
||||
"4600000000",
|
||||
"4700000000",
|
||||
"4800000000",
|
||||
];
|
||||
|
||||
export const needBsnmFrgnrPoplSmSidoList = [
|
||||
"3600000000",
|
||||
"4100000000",
|
||||
"4200000000",
|
||||
"4300000000",
|
||||
"4400000000",
|
||||
"4500000000",
|
||||
"4600000000",
|
||||
"4700000000",
|
||||
"4800000000",
|
||||
"5000000000",
|
||||
];
|
||||
@@ -7,13 +7,13 @@ import { ValueType } from "realgrid";
|
||||
export const SeweragePenetrationRateInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'sido', fieldName: 'sido', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 수계 및 권역
|
||||
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
|
||||
|
||||
// 수계영향권별분류
|
||||
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
@@ -21,121 +21,121 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 특별대책지역
|
||||
{ name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } },
|
||||
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } },
|
||||
|
||||
// 행정참고자료인구
|
||||
{ name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'poplSm', fieldName: 'poplSm', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'pbadmsWholPpltnCnt', fieldName: 'pbadmsWholPpltnCnt', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'domplWholPpltnCnt', fieldName: 'domplWholPpltnCnt', header: { text: '내국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'frgnrWholPpltnCnt', fieldName: 'frgnrWholPpltnCnt', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 행정참고자료면적
|
||||
{ name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
{ name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 총인구
|
||||
{
|
||||
name: 'popltnSm',
|
||||
fieldName: 'popltnSm',
|
||||
name: 'wholPpltnCnt',
|
||||
fieldName: 'wholPpltnCnt',
|
||||
header: { text: '총인구(명)' },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const },
|
||||
computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
|
||||
// 총면적
|
||||
{
|
||||
name: 'arSm',
|
||||
fieldName: 'arSm',
|
||||
name: 'wholArea',
|
||||
fieldName: 'wholArea',
|
||||
header: { text: '총면적(㎢)' },
|
||||
footer: footerDecimal3,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.000',
|
||||
computed: { fields: ['swgeProcZoneInnerAr', 'swgeProcZoneOuterAr'], type: 'sum' as const },
|
||||
computed: { fields: ['staaArea', 'staaExcpArea'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 합계
|
||||
{
|
||||
name: 'swgeProcZoneInnerSm',
|
||||
fieldName: 'swgeProcZoneInnerSm',
|
||||
name: 'wholSwtrmtPpltnCnt',
|
||||
fieldName: 'wholSwtrmtPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], type: 'sum' as const },
|
||||
computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 공공하수처리인구
|
||||
{
|
||||
name: 'pblswrgPopltnSm',
|
||||
fieldName: 'pblswrgPopltnSm',
|
||||
name: 'wholPblSwtrmtPpltnCnt',
|
||||
fieldName: 'wholPblSwtrmtPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], type: 'sum' as const },
|
||||
computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
{ name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pblswrgTertTrt', fieldName: 'pblswrgTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 폐수처리인구
|
||||
{
|
||||
name: 'wstrPopltnSm',
|
||||
fieldName: 'wstrPopltnSm',
|
||||
name: 'wholWwtPrcsPpltnCnt',
|
||||
fieldName: 'wholWwtPrcsPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
computed: { fields: ['wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const },
|
||||
computed: { fields: ['ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
{ name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wstrTertTrt', fieldName: 'wstrTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 미접속인구
|
||||
{ name: 'notConectPopltn', fieldName: 'notConectPopltn', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 면적
|
||||
{ name: 'swgeProcZoneInnerAr', fieldName: 'swgeProcZoneInnerAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 하수처리구역 외
|
||||
{
|
||||
name: 'swgeProcZoneOuterSm',
|
||||
fieldName: 'swgeProcZoneOuterSm',
|
||||
name: 'wholStaaExcpPpltnCnt',
|
||||
fieldName: 'wholStaaExcpPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
footer: footerInt,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
computed: { fields: ['townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const },
|
||||
computed: { fields: ['unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
{ name: 'townUnprocPopltn', fieldName: 'townUnprocPopltn', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'townSwttPopltn', fieldName: 'townSwttPopltn', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'townWrrtnPopltn', fieldName: 'townWrrtnPopltn', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'swgeProcZoneOuterAr', fieldName: 'swgeProcZoneOuterAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'pblswrgPopltnSuprt',
|
||||
fieldName: 'pblswrgPopltnSuprt',
|
||||
name: 'staaPsplyRt',
|
||||
fieldName: 'staaPsplyRt',
|
||||
header: { text: '공공하수처리구역인구보급률(%)' },
|
||||
footer: footerAvgDecimal,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
computed: {
|
||||
numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'],
|
||||
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'],
|
||||
numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'],
|
||||
denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'],
|
||||
type: 'pblswrgPopltnSuprtRate' as const
|
||||
},
|
||||
styleName: 'rg-cell-total',
|
||||
@@ -143,16 +143,16 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
|
||||
// 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'adtrtPopltnSuprt',
|
||||
fieldName: 'adtrtPopltnSuprt',
|
||||
name: 'altdPrcsPsplyRt',
|
||||
fieldName: 'altdPrcsPsplyRt',
|
||||
header: { text: '고도처리인구보급률(%)' },
|
||||
footer: footerAvgDecimal,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
computed: {
|
||||
numeratorFields: ['pblswrgTertTrt', 'wstrTertTrt'],
|
||||
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'],
|
||||
numeratorFields: ['ts3SwtrmtPpltnCnt', 'ts3WwtPrcsPpltnCnt'],
|
||||
denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'],
|
||||
type: 'adtrtPopltnSuprtRate' as const
|
||||
},
|
||||
styleName: 'rg-cell-total',
|
||||
@@ -160,27 +160,27 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
|
||||
// 하수도 설치율
|
||||
{
|
||||
name: 'swrgInstlrt',
|
||||
fieldName: 'swrgInstlrt',
|
||||
name: 'sewerInstlRt',
|
||||
fieldName: 'sewerInstlRt',
|
||||
header: { text: '하수도설치율(%)' },
|
||||
footer: footerAvgDecimal,
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
computed: {
|
||||
numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'swgeProcZoneOuterSm'],
|
||||
denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'],
|
||||
thirdFields: ['piplSupprt'],
|
||||
numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'wholStaaExcpPpltnCnt'],
|
||||
denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'],
|
||||
thirdFields: ['swagePplPsplyRt'],
|
||||
type: 'swrgInstlrtRate' as const
|
||||
},
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
|
||||
// 비고
|
||||
{ name: 'rmk', fieldName: 'rmk', header: { text: '비고' } },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
{ name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로보급률(%)' }, visible: false },
|
||||
{ name: 'adstCd', fieldName: 'adstCd', header: { text: '행정코드' }, visible: false },
|
||||
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -189,49 +189,49 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'sido', footerUserSpans: [{ colspan: 3 }] },
|
||||
'gugun',
|
||||
'adstDong',
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
],
|
||||
},
|
||||
'rvbsn',
|
||||
'bstrm',
|
||||
'trbtrNm',
|
||||
{
|
||||
name: 'groupB',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '수계영향권별분류' },
|
||||
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
|
||||
},
|
||||
'speclCntrplnAr',
|
||||
'spcCtpnRgnCn',
|
||||
{
|
||||
name: 'groupC',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정참고자료인구' },
|
||||
items: ['totalPopl', 'poplSm', 'frgnrPoplSm'],
|
||||
items: ['pbadmsWholPpltnCnt', 'domplWholPpltnCnt', 'frgnrWholPpltnCnt'],
|
||||
},
|
||||
'area',
|
||||
'popltnSm',
|
||||
'arSm',
|
||||
'refArea',
|
||||
'wholPpltnCnt',
|
||||
'wholArea',
|
||||
{
|
||||
name: 'groupD',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역내' },
|
||||
items: [
|
||||
'swgeProcZoneInnerSm',
|
||||
'wholSwtrmtPpltnCnt',
|
||||
{
|
||||
name: 'pblswrgGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '공공하수처리인구(명)' },
|
||||
items: ['pblswrgPopltnSm', 'pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'],
|
||||
items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'],
|
||||
},
|
||||
{
|
||||
name: 'wstrGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '폐수처리인구(명)' },
|
||||
items: ['wstrPopltnSm', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'],
|
||||
items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'],
|
||||
},
|
||||
'notConectPopltn',
|
||||
'swgeProcZoneInnerAr'
|
||||
'yetCntnPpltnCnt',
|
||||
'staaArea'
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -239,18 +239,18 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역외' },
|
||||
items: [
|
||||
'swgeProcZoneOuterSm',
|
||||
'townUnprocPopltn',
|
||||
'townSwttPopltn',
|
||||
'townWrrtnPopltn',
|
||||
'swgeProcZoneOuterAr'
|
||||
'wholStaaExcpPpltnCnt',
|
||||
'unprcsPpltnCnt',
|
||||
'drtwPrcsPpltnCnt',
|
||||
'spctnkPpltnCnt',
|
||||
'staaExcpArea'
|
||||
],
|
||||
},
|
||||
'pblswrgPopltnSuprt',
|
||||
'adtrtPopltnSuprt',
|
||||
'swrgInstlrt',
|
||||
'rmk',
|
||||
'piplSupprt',
|
||||
'adstCd',
|
||||
'staaPsplyRt',
|
||||
'altdPrcsPsplyRt',
|
||||
'sewerInstlRt',
|
||||
'rmrk',
|
||||
'swagePplPsplyRt',
|
||||
'admdstCd',
|
||||
]
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ValueType } from "realgrid";
|
||||
// import { ValueType } from "realgrid";
|
||||
|
||||
/**
|
||||
* 하수도 통계 관리 > 00. 통계 입력 현황
|
||||
@@ -6,40 +6,42 @@ import { ValueType } from "realgrid";
|
||||
*/
|
||||
export const StatsInputStatusInpColumn = {
|
||||
columns: [
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'menu_nm', fieldName: '구분', header: { text: '구분' } },
|
||||
{ name: 'locg_status_nm', fieldName: '진행상황', header: { text: '진행상황' } },
|
||||
{ name: 'locg_status', fieldName: '작성률', header: { text: '작성률' }, valueType: ValueType.NUMBER },
|
||||
{ name: 'locg_charger', fieldName: '작성자', header: { text: '작성자' } },
|
||||
{ name: 'locg_cttpc', fieldName: '작성자연락처', header: { text: '작성자연락처' } },
|
||||
{ name: 'locg_dept', fieldName: '부서', header: { text: '부서' } },
|
||||
{ name: 'locg_job', fieldName: '직제', header: { text: '직제' } },
|
||||
{ name: 'locg_cnfirm_nm', fieldName: '승인자', header: { text: '승인자' } },
|
||||
{ name: 'locg_cnfirm_dept', fieldName: '부서', header: { text: '부서' } },
|
||||
{ name: 'locg_cnfirm_job', fieldName: '직제', header: { text: '직제' } },
|
||||
{ name: 'return_opinion_sm', fieldName: '반려사유', header: { text: '반려사유' } },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false, editable: false },
|
||||
{ name: 'menuTabName', fieldName: 'menuTabName', header: { text: '구분' }, editable: false, width: 220 },
|
||||
{ name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false },
|
||||
// { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false },
|
||||
{ name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false },
|
||||
{ name: 'laeKbrdrDeptNm', fieldName: 'laeKbrdrDeptNm', header: { text: '부서' }, editable: false },
|
||||
{ name: 'laeKbrdrJbgdNm', fieldName: 'laeKbrdrJbgdNm', header: { text: '직제' }, editable: false },
|
||||
{ name: 'laeIdfrNm', fieldName: 'laeIdfrNm', header: { text: '승인자' }, editable: false },
|
||||
{ name: 'laeIdfrDeptNm', fieldName: 'laeIdfrDeptNm', header: { text: '부서' }, editable: false },
|
||||
{ name: 'laeIdfrJbgdNm', fieldName: 'laeIdfrJbgdNm', header: { text: '직제' }, editable: false },
|
||||
{ name: 'rjctOpnnCn', fieldName: 'rjctOpnnCn', header: { text: '반려사유' }, editable: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
'sido',
|
||||
'sgg',
|
||||
'menu_nm',
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'statsCd',
|
||||
'menuTabName',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수도사업자' },
|
||||
items: [
|
||||
'locg_status_nm',
|
||||
'locg_status',
|
||||
'locg_charger',
|
||||
'locg_cttpc',
|
||||
'locg_dept',
|
||||
'locg_job',
|
||||
'locg_cnfirm_nm',
|
||||
'locg_cnfirm_dept',
|
||||
'locg_cnfirm_job',
|
||||
'return_opinion_sm'
|
||||
'locgStatusNm',
|
||||
// 'inputRate',
|
||||
'laePicNm',
|
||||
'laePicTelNo',
|
||||
'laeKbrdrDeptNm',
|
||||
'laeKbrdrJbgdNm',
|
||||
'laeIdfrNm',
|
||||
'laeIdfrDeptNm',
|
||||
'laeIdfrJbgdNm',
|
||||
'rjctOpnnCn'
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
+79
-74
@@ -6,92 +6,97 @@ import { ValueType } from "realgrid";
|
||||
export const RegionalFactStatusInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' }, editable: false },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 기본 정보
|
||||
{ name: 'targetYr', fieldName: '하수정비기본계획목표년도', header: { text: '하수정비기본계획목표년도' }},
|
||||
{ name: 'piplImprnPlanExtn', fieldName: '총관로정비계획연장(m)', header: { text: '총관로정비계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'piplPlanExtn', fieldName: '총계획연장(m)', header: { text: '총계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'piplFcltyExtn', fieldName: '총시설연장(m)', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'piplSupprt', fieldName: '하수관로보급률(%)', header: { text: '하수관로보급률(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정비기본계획목표년도' }},
|
||||
{ name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 합류식(m)
|
||||
{ name: 'cmbnPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'cmbnImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnFcltyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnRctglExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnCicleExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnOpswrExtn', fieldName: '개거(m)', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cmbnSdgttrExtn', fieldName: '측구(m)', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
// 분류식(m) > 오수관로
|
||||
{ name: 'pltwtrPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'pltwtrImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrFlctyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrSqreExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrCiclExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
// 분류식(m) > 우수관로
|
||||
{ name: 'rainwtrPlanAr', fieldName: '관로정비계획면적(㎢)', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'rainwtrImprnPlanExtn', fieldName: '관로정비계획연장(m)', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrPlanExtn', fieldName: '계획연장(m)', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrFlctyExtn', fieldName: '시설연장(m)', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrSqreExtn', fieldName: '사각형(m)', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrCiclExtn', fieldName: '원형(m)', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrOpswrExtn', fieldName: '개거(m)', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrSdgttrExtn', fieldName: '측구(m)', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비계획면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비계획연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerDecimal2, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
// 합류식맨홀(개소)
|
||||
{ name: 'mnhleSite', fieldName: '합류식맨홀(개소)', header: { text: '합류식맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 분류식 > 오수맨홀, 우수맨홀
|
||||
{ name: 'pltwtrMnhleSite', fieldName: '오수맨홀(개소)', header: { text: '오수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'rainwtrMnhleSite', fieldName: '우수맨홀(개소)', header: { text: '우수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
{ name: 'pltwtrRcvrSite', fieldName: '오수받이(개소)', header: { text: '오수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'rainwtrRcvrSite', fieldName: '빗물받이(개소)', header: { text: '빗물받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'soilspllSite', fieldName: '우수토실(개소)', header: { text: '우수토실(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'soilholeSite', fieldName: '우수토구(개소)', header: { text: '우수토구(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수도대장 전산화
|
||||
{ name: 'sewerRegstrCmptExtn', fieldName: '하수도대장전산화연장(m)', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sewerRegstrCmptRt', fieldName: '하수도대장전산화율(%)', header: { text: '하수도대장전산화율(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerDecimal1, editable: false, styleName: 'rg-cell-readonly', valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 비고
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'adminArea',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sido', 'sgg'],
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
],
|
||||
},
|
||||
'targetYr',
|
||||
'piplImprnPlanExtn',
|
||||
'piplPlanExtn',
|
||||
'piplFcltyExtn',
|
||||
'piplSupprt',
|
||||
'goalYr',
|
||||
'wholPplMtncPlanPrlg',
|
||||
'wholPlanPrlg',
|
||||
'wholFcltPrlg',
|
||||
'swagePplPsplyRt',
|
||||
{
|
||||
name: 'combined',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '합류식(m)' },
|
||||
items: [
|
||||
'cmbnPlanAr',
|
||||
'cmbnImprnPlanExtn',
|
||||
'cmbnPlanExtn',
|
||||
'cmbnFcltyExtn',
|
||||
'cbsysPplMtncPlanArea',
|
||||
'cbsysPplMtncPlanPrlg',
|
||||
'cbsysPlanPrlg',
|
||||
'cbsysFcltPrlg',
|
||||
{
|
||||
name: 'cmbnCulvert',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '암거' },
|
||||
items: ['cmbnRctglExtn', 'cmbnCicleExtn'],
|
||||
items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'],
|
||||
},
|
||||
'cmbnOpswrExtn',
|
||||
'cmbnSdgttrExtn',
|
||||
'cbsysOpnPrlg',
|
||||
'cbsysGttrPrlg',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -104,15 +109,15 @@ export const RegionalFactStatusInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '오수관로' },
|
||||
items: [
|
||||
'pltwtrPlanAr',
|
||||
'pltwtrImprnPlanExtn',
|
||||
'pltwtrPlanExtn',
|
||||
'pltwtrFlctyExtn',
|
||||
'drtwPplMtncPlanArea',
|
||||
'drtwPplMtncPlanPrlg',
|
||||
'drtwPplPlanPrlg',
|
||||
'drtwPplFcltPrlg',
|
||||
{
|
||||
name: 'pltwtrCulvert',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '암거' },
|
||||
items: ['pltwtrSqreExtn', 'pltwtrCiclExtn'],
|
||||
items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -121,39 +126,39 @@ export const RegionalFactStatusInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '우수관로' },
|
||||
items: [
|
||||
'rainwtrPlanAr',
|
||||
'rainwtrImprnPlanExtn',
|
||||
'rainwtrPlanExtn',
|
||||
'rainwtrFlctyExtn',
|
||||
'exlnPplMtncPlanArea',
|
||||
'exlnPplMtncPlanPrlg',
|
||||
'exlnPplPlanPrlg',
|
||||
'exlnPplFcltPrlg',
|
||||
{
|
||||
name: 'rainwtrCulvert',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '암거' },
|
||||
items: ['rainwtrSqreExtn', 'rainwtrCiclExtn'],
|
||||
items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'],
|
||||
},
|
||||
'rainwtrOpswrExtn',
|
||||
'rainwtrSdgttrExtn',
|
||||
'exlnPplOpnPrlg',
|
||||
'exlnPplGttrPrlg',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'mnhleSite',
|
||||
'cbsysMnhlCnt',
|
||||
{
|
||||
name: 'separatedManhole',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '분류식' },
|
||||
items: ['pltwtrMnhleSite', 'rainwtrMnhleSite'],
|
||||
items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'],
|
||||
},
|
||||
'pltwtrRcvrSite',
|
||||
'rainwtrRcvrSite',
|
||||
'soilspllSite',
|
||||
'soilholeSite',
|
||||
'sewerRegstrCmptExtn',
|
||||
'sewerRegstrCmptRt',
|
||||
'rmk',
|
||||
'drtwRcvprpCnt',
|
||||
'exlnRcvprpCnt',
|
||||
'stofcbrCnt',
|
||||
'exlnOtflCnt',
|
||||
'sewerLdgrEdpPrlg',
|
||||
'sewerLdgrEdpRt',
|
||||
'rmrk',
|
||||
],
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 1,
|
||||
tabKey: 'regionalFactStatusInp',
|
||||
tabKey: 'ST00020001',
|
||||
tabLabel: '지역별 시설현황'
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import { RegionalFactStatusInpColumn } from "./RegionalFactStatusInpColumn";
|
||||
import { FactInstallStatusInpColumn } from "./FactInstallStatusInpColumn";
|
||||
import { PipeTypeStatusInpColumn } from "./PipeTypeStatusInpColumn";
|
||||
import { PipeDiameterStatusInpColumn } from "./PipeDiameterStatusInpColumn";
|
||||
import { ManholeStatusInpColumn } from "./ManholeStatusInpColumn";
|
||||
import { ConnectionStatusInpColumn } from "./ConnectionStatusInpColumn";
|
||||
|
||||
/** 하수관로 입력 탭 컬럼 리스트 (TabDataGrid columnInfo용) */
|
||||
export const SewagePipeInpColumnInfoList: ColumnInfoList[] = [
|
||||
RegionalFactStatusInpColumn,
|
||||
FactInstallStatusInpColumn,
|
||||
PipeTypeStatusInpColumn,
|
||||
PipeDiameterStatusInpColumn,
|
||||
ManholeStatusInpColumn,
|
||||
ConnectionStatusInpColumn,
|
||||
];
|
||||
@@ -0,0 +1,110 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
import RegistDialog from "@/components/statistics/verification/generalVerification/RegistDialog";
|
||||
import CopyDialog from "@/components/statistics/verification/generalVerification/CopyDialog";
|
||||
|
||||
export const getGeneralVerificationColumns = (
|
||||
onUpdateSuccess?: () => void,
|
||||
): ColumnDef<GeneralverificationList>[] => [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const { pageIndex, pageSize } = table.getState().pagination;
|
||||
return totalRows - (pageIndex * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "trgtStatsNm",
|
||||
header: "통계항목",
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfcNm",
|
||||
header: "검증명",
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfcClsfNm",
|
||||
header: "검증분류",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.regDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
header: "복사",
|
||||
cell: ({ row }) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<GeneralVerificationCopyButton
|
||||
row={row.original}
|
||||
onSuccess={onUpdateSuccess}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: ({ row }) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<GeneralVerificationManageButton
|
||||
vrfcSn={row.original.vrfcSn}
|
||||
onSuccess={onUpdateSuccess}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const GeneralVerificationManageButton = ({
|
||||
vrfcSn,
|
||||
onSuccess,
|
||||
}: {
|
||||
vrfcSn: number;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
return (
|
||||
<RegistDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
vrfcSn={vrfcSn}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<Button size="sm">관리</Button>
|
||||
</RegistDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralVerificationCopyButton = ({
|
||||
row,
|
||||
onSuccess,
|
||||
}: {
|
||||
row: GeneralverificationList;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
return (
|
||||
<CopyDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
row={row}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<Button size="sm" color="white">복사</Button>
|
||||
</CopyDialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
export const LogHistoryColumn = {
|
||||
columns: [
|
||||
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false },
|
||||
{ name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false },
|
||||
{ name: 'dmndFwkNm', fieldName: 'dmndFwkNm', header: { text: '요청 기능명' }, editable: false },
|
||||
{ name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false },
|
||||
{ name: 'regDt', fieldName: 'regDt', header: { text: '등록일' }, editable: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
'statsLogSn',
|
||||
'menuNm',
|
||||
'tblNm',
|
||||
'dmndFwkNm',
|
||||
'rgtrId',
|
||||
'regDt'
|
||||
]
|
||||
};
|
||||
@@ -1,106 +1,206 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
// 하수도 통계 관리 05.펌프장
|
||||
export const rawPumpingStationInfoList = {
|
||||
columns: [
|
||||
/* 1. 행정구역 및 기본정보 */
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'sgg', fieldName: '구군', header: { text: '구군' } },
|
||||
{ name: 'PUMP_NM', fieldName: '펌프장명', header: { text: '펌프장명' } },
|
||||
{ name: 'GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
{ name: 'TRT_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' } },
|
||||
{ name: 'TYPE_GBN', fieldName: '구분(합류식분류식)', header: { text: '구분(합류식분류식)' } },
|
||||
{ name: 'ADDR', fieldName: '위치', header: { text: '위치' } },
|
||||
{ name: 'RN_DT', fieldName: '가동년월일 yyyy-mm-dd', header: { text: '가동년월일\nyyyy-mm-dd' } },
|
||||
{ name: 'DSN_AR', fieldName: '계획배수면적(km2)', header: { text: '계획배수면적\n(km2)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true },
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } },
|
||||
{ name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } },
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } },
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } },
|
||||
{
|
||||
name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' },
|
||||
footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000'
|
||||
},
|
||||
|
||||
/* 2. 계획배수량 (m3/hr) */
|
||||
{ name: 'DSN_QTY_A', fieldName: '청천시(A)', header: { text: '청천시(A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DSN_QTY_B', fieldName: '우천시(B)', header: { text: '우천시(B)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DSN_QTY_DIFF', fieldName: '우수량(B-A)', header: { text: '우수량(B-A)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sndyArea', fieldName: 'sndyArea', header: { text: '청천시(A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // finewtDrngVl -> sndyArea
|
||||
{ name: 'rainArea', fieldName: 'rainArea', header: { text: '우천시(B)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtDrngVl -> rainArea
|
||||
{ name: 'exlnQnt', fieldName: 'exlnQnt', header: { text: '우수량(B-A)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // vlWtr -> exlnQnt
|
||||
|
||||
/* 3. 총 양수량 (m3/min) */
|
||||
{ name: 'TOT_PUMP_QTY_O', fieldName: '오수펌프 총양수량', header: { text: '오수펌프\n총양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'TOT_PUMP_QTY_W', fieldName: '우수펌프 총양수량', header: { text: '우수펌프\n총양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drtwPumpGrssPpqty', fieldName: 'drtwPumpGrssPpqty', header: { text: '오수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrTotCpcty -> drtwPumpGrssPpqty
|
||||
{ name: 'exlnPumpGrssPpqty', fieldName: 'exlnPumpGrssPpqty', header: { text: '우수펌프\n총양수량' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrTotCpcty -> exlnPumpGrssPpqty
|
||||
|
||||
/* 4. 오수펌프 상세 (1, 2, 3) */
|
||||
{ name: 'PUMP_O1_D', fieldName: '구경(mm)_오수1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O1_Q', fieldName: '양수량(m3/min)_오수1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O1_C', fieldName: '펌프대수_오수1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_O1_S', fieldName: '예비대수_오수1', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwPumpN1DmtrArea', fieldName: 'drtwPumpN1DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrPumpCalbr1 -> drtwPumpN1DmtrArea
|
||||
{ name: 'drtwPumpN1Ppqty', fieldName: 'drtwPumpN1Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // pltwtrPumpCpcty1 -> drtwPumpN1Ppqty
|
||||
{ name: 'drtwPumpN1Cnt', fieldName: 'drtwPumpN1Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // pltwtrPumpCnt1 -> drtwPumpN1Cnt
|
||||
{ name: 'drtwPumpN1RsrvCnt', fieldName: 'drtwPumpN1RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // pltwtrPumpPreparCnt1 -> drtwPumpN1RsrvCnt
|
||||
|
||||
{ name: 'drtwPumpN2DmtrArea', fieldName: 'drtwPumpN2DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drtwPumpN2Ppqty', fieldName: 'drtwPumpN2Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drtwPumpN2Cnt', fieldName: 'drtwPumpN2Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwPumpN2RsrvCnt', fieldName: 'drtwPumpN2RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
{ name: 'PUMP_O2_D', fieldName: '구경(mm)_오수2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O2_Q', fieldName: '양수량(m3/min)_오수2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O2_C', fieldName: '펌프대수_오수2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_O2_S', fieldName: '예비대수_오수2', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwPumpN3DmtrArea', fieldName: 'drtwPumpN3DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drtwPumpN3Ppqty', fieldName: 'drtwPumpN3Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drtwPumpN3Cnt', fieldName: 'drtwPumpN3Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwPumpN3RsrvCnt', fieldName: 'drtwPumpN3RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
{ name: 'PUMP_O3_D', fieldName: '구경(mm)_오수3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O3_Q', fieldName: '양수량(m3/min)_오수3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_O3_C', fieldName: '펌프대수_오수3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_O3_S', fieldName: '예비대수_오수3', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
/* 5. 우수펌프 상세 (1, 2, 3) - N1, N2, N3*/
|
||||
{ name: 'exlnPumpN1DmtrArea', fieldName: 'exlnPumpN1DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrPumpCalbr1 -> exlnPumpN1DmtrArea
|
||||
{ name: 'exlnPumpN1Ppqty', fieldName: 'exlnPumpN1Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // rainwtrPumpCpcty1 -> exlnPumpN1Ppqty
|
||||
{ name: 'exlnPumpN1Cnt', fieldName: 'exlnPumpN1Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // rainwtrPumpCnt1 -> exlnPumpN1Cnt
|
||||
{ name: 'exlnPumpN4RsrvCnt', fieldName: 'exlnPumpN4RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' }, // rainwtrPumpPreparCnt1 -> exlnPumpN4RsrvCnt
|
||||
|
||||
{ name: 'exlnPumpN2DmtrArea', fieldName: 'exlnPumpN2DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'exlnPumpN2Ppqty', fieldName: 'exlnPumpN2Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'exlnPumpN2Cnt', fieldName: 'exlnPumpN2Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnPumpN2RsrvCnt', fieldName: 'exlnPumpN2RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 5. 우수펌프 상세 (1, 2, 3) */
|
||||
{ name: 'PUMP_W1_D', fieldName: '구경(mm)_우수1', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W1_Q', fieldName: '양수량(m3/min)_우수1', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W1_C', fieldName: '펌프대수_우수1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_W1_S', fieldName: '예비대수_우수1', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnPumpN3DmtrArea', fieldName: 'exlnPumpN3DmtrArea', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'exlnPumpN3Ppqty', fieldName: 'exlnPumpN3Ppqty', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'exlnPumpN3Cnt', fieldName: 'exlnPumpN3Cnt', header: { text: '펌프대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnPumpN3RsrvCnt', fieldName: 'exlnPumpN3RsrvCnt', header: { text: '예비대수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
{ name: 'PUMP_W2_D', fieldName: '구경(mm)_우수2', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W2_Q', fieldName: '양수량(m3/min)_우수2', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W2_C', fieldName: '펌프대수_우수2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_W2_S', fieldName: '예비대수_우수2', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
{ name: 'PUMP_W3_D', fieldName: '구경(mm)_우수3', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W3_Q', fieldName: '양수량(m3/min)_우수3', header: { text: '양수량\n(m3/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_W3_C', fieldName: '펌프대수_우수3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_W3_S', fieldName: '예비대수_우수3', header: { text: '예비대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 6. 운영 정보 */
|
||||
{ name: 'PWR_USAGE', fieldName: '연간전력사용량(kWh/년)', header: { text: '연간전력사용량\n(kWh/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
/* 6. 기타 및 운영 */
|
||||
{ name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '연간전력사용량\n(kWh/년)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, // elcpwAmnt -> anlElpwrUseQnt
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'groupA', header: { text: '행정구역' }, items: ['sido', 'sgg'] },
|
||||
'PUMP_NM', 'GBN', 'TRT_FCT_NM', 'TYPE_GBN', 'ADDR', 'RN_DT', 'DSN_AR',
|
||||
{
|
||||
name: 'groupA',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm',
|
||||
]
|
||||
},
|
||||
'pumpstNm',
|
||||
'displayYear',
|
||||
'swtrmtFcltNm',
|
||||
'pumpSeNm',
|
||||
'impdFcltPstnCn',
|
||||
'oprtngStrtYmd',
|
||||
'planMltvArea',
|
||||
{
|
||||
name: 'groupB',
|
||||
header: { text: '계획시간대최대배수량(m3/hr)' },
|
||||
items: ['DSN_QTY_A', 'DSN_QTY_B', 'DSN_QTY_DIFF'],
|
||||
items: ['sndyArea', 'rainArea', 'exlnQnt'],
|
||||
},
|
||||
{
|
||||
name: 'groupC',
|
||||
header: { text: '배수능력' },
|
||||
items: [
|
||||
'TOT_PUMP_QTY_O',
|
||||
'drtwPumpGrssPpqty',
|
||||
{
|
||||
name: 'pumpO_Detail',
|
||||
header: { text: '오수펌프 상세' },
|
||||
items: [
|
||||
{ name: 'O1', header: { text: '오수펌프(1)' }, items: ['PUMP_O1_D', 'PUMP_O1_Q', 'PUMP_O1_C', 'PUMP_O1_S'] },
|
||||
{ name: 'O2', header: { text: '오수펌프(2)' }, items: ['PUMP_O2_D', 'PUMP_O2_Q', 'PUMP_O2_C', 'PUMP_O2_S'] },
|
||||
{ name: 'O3', header: { text: '오수펌프(3)' }, items: ['PUMP_O3_D', 'PUMP_O3_Q', 'PUMP_O3_C', 'PUMP_O3_S'] }
|
||||
{ name: 'O1', header: { text: '오수펌프(1)' }, items: ['drtwPumpN1DmtrArea', 'drtwPumpN1Ppqty', 'drtwPumpN1Cnt', 'drtwPumpN1RsrvCnt'] },
|
||||
{ name: 'O2', header: { text: '오수펌프(2)' }, items: ['drtwPumpN2DmtrArea', 'drtwPumpN2Ppqty', 'drtwPumpN2Cnt', 'drtwPumpN2RsrvCnt'] },
|
||||
{ name: 'O3', header: { text: '오수펌프(3)' }, items: ['drtwPumpN3DmtrArea', 'drtwPumpN3Ppqty', 'drtwPumpN3Cnt', 'drtwPumpN3RsrvCnt'] }
|
||||
]
|
||||
},
|
||||
'TOT_PUMP_QTY_W',
|
||||
'exlnPumpGrssPpqty',
|
||||
{
|
||||
name: 'pumpW_Detail',
|
||||
header: { text: '우수펌프 상세' },
|
||||
items: [
|
||||
{ name: 'W1', header: { text: '우수펌프(1)' }, items: ['PUMP_W1_D', 'PUMP_W1_Q', 'PUMP_W1_C', 'PUMP_W1_S'] },
|
||||
{ name: 'W2', header: { text: '우수펌프(2)' }, items: ['PUMP_W2_D', 'PUMP_W2_Q', 'PUMP_W2_C', 'PUMP_W2_S'] },
|
||||
{ name: 'W3', header: { text: '우수펌프(3)' }, items: ['PUMP_W3_D', 'PUMP_W3_Q', 'PUMP_W3_C', 'PUMP_W3_S'] }
|
||||
{ name: 'W1', header: { text: '우수펌프(1)' }, items: ['exlnPumpN1DmtrArea', 'exlnPumpN1Ppqty', 'exlnPumpN1Cnt', 'exlnPumpN4RsrvCnt'] },
|
||||
{ name: 'W2', header: { text: '우수펌프(2)' }, items: ['exlnPumpN2DmtrArea', 'exlnPumpN2Ppqty', 'exlnPumpN2Cnt', 'exlnPumpN2RsrvCnt'] },
|
||||
{ name: 'W3', header: { text: '우수펌프(3)' }, items: ['exlnPumpN3DmtrArea', 'exlnPumpN3Ppqty', 'exlnPumpN3Cnt', 'exlnPumpN3RsrvCnt'] }
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
'PWR_USAGE', 'RMK',
|
||||
]
|
||||
'anlElpwrUseQnt',
|
||||
'rmrk',
|
||||
],
|
||||
fixedColumns: 3,
|
||||
rowStartNum: 3
|
||||
};
|
||||
|
||||
export const PumpingStationInfoList = {
|
||||
...rawPumpingStationInfoList,
|
||||
columns: rawPumpingStationInfoList.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
};
|
||||
@@ -1,137 +1,313 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import { footerIntArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
// 하수도 통계 관리 03. 하수관로 유지 관리
|
||||
const rawMaintainList = [
|
||||
{
|
||||
tabKey: 'a',
|
||||
tabKey: 'ST00030001',
|
||||
tabLabel: '개보수',
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
/* 1. 행정 및 기본 정보 */
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
/* 전체보수 (m) */
|
||||
{ name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 2. 전체보수 (단위: m) */
|
||||
{ name: 'RPAIR_PIPE_SM', fieldName: '계(전체보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_CMBN_PIPE', fieldName: '합류식(전체보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_RAINWTR_PIPE', fieldName: '우수(전체보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_PLTWTR_PIPE', fieldName: '오수(전체보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
/* 부분보수 (개소) */
|
||||
{ name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnMendngPrlg', fieldName: 'sprsysExlnMendngPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysDrtwMendngPrlg', fieldName: 'sprsysDrtwMendngPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 3. 부분보수 (단위: 개소) */
|
||||
{ name: 'RPAIR_PIPE_SM2', fieldName: '계(부분보수)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_CMBN_PIPE2', fieldName: '합류식(부분보수)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_RAINWTR_PIPE2', fieldName: '우수(부분보수)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RPAIR_PLTWTR_PIPE2', fieldName: '오수(부분보수)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
/* 부속시설 (개소) */
|
||||
{ name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식\n맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '빗물받이' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '토실' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'otflCnt', fieldName: 'otflCnt', header: { text: '토구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 4. 부속시설 (개소) */
|
||||
{ name: 'MNHLE_SITE', fieldName: '합류식 맨홀', header: { text: '합류식\n맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PLTWTR_MNHLE_SITE', fieldName: '오수맨홀', header: { text: '오수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'RAINWTR_MNHLE_SITE', fieldName: '우수맨홀', header: { text: '우수맨홀' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PLTWTR_RCVR_SITE', fieldName: '오수받이(개소)', header: { text: '오수받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'RAINWTR_RCVR_SITE', fieldName: '빗물받이(개소)', header: { text: '빗물받이(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'SOILHOLE_SITE', fieldName: '토실(개소)', header: { text: '토실(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'SOILSPLL_SITE', fieldName: '토구(개소)', header: { text: '토구(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 5. 조사량 및 비고 */
|
||||
{ name: 'CCTV', fieldName: '수밀검사·CCTV조사량(m)', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
/* 조사량 및 비고 */
|
||||
{ name: 'cctvExmnPrlg', fieldName: 'cctvExmnPrlg', header: { text: '수밀검사·\nCCTV조사량\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'YR_GBN',
|
||||
{
|
||||
name: 'addrGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
]
|
||||
},
|
||||
'displayYear',
|
||||
{
|
||||
name: 'mtcPipeGroup', header: { text: '개·보수관로' },
|
||||
items: [
|
||||
{
|
||||
name: 'totalRepairGroup', header: { text: '전체보수(m)' },
|
||||
items: ['RPAIR_PIPE_SM', 'RPAIR_CMBN_PIPE', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE', 'RPAIR_PLTWTR_PIPE'] }]
|
||||
items: ['mendngWhol', 'cbsysMendngPrlg', { name: 'sepTotalGroup', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }]
|
||||
},
|
||||
{
|
||||
name: 'partRepairGroup', header: { text: '부분보수(개소)' },
|
||||
items: ['RPAIR_PIPE_SM2', 'RPAIR_CMBN_PIPE2', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['RPAIR_RAINWTR_PIPE2', 'RPAIR_PLTWTR_PIPE2'] }]
|
||||
items: ['partMendngWhol', 'cbsysPartMendngPrlg', { name: 'sepPartGroup', header: { text: '분류식' }, items: ['sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg'] }]
|
||||
}
|
||||
]
|
||||
},
|
||||
'MNHLE_SITE',
|
||||
{ name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['PLTWTR_MNHLE_SITE', 'RAINWTR_MNHLE_SITE'] },
|
||||
'PLTWTR_RCVR_SITE', 'RAINWTR_RCVR_SITE',
|
||||
{ name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['SOILHOLE_SITE', 'SOILSPLL_SITE'] },
|
||||
'CCTV', 'RMK'
|
||||
'cbsysMnhlCnt',
|
||||
{ name: 'sepMnhleGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] },
|
||||
'drtwRcvprpCnt', 'exlnRcvprpCnt',
|
||||
{ name: 'soilGroup', header: { text: '토실·토구(개소)' }, items: ['stofcbrCnt', 'otflCnt'] },
|
||||
'cctvExmnPrlg', 'rmrk'
|
||||
]
|
||||
},
|
||||
{
|
||||
tabKey: 'b',
|
||||
tabKey: 'ST00030002',
|
||||
tabLabel: '관로준설',
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
/* 1. 행정 및 기본 정보 */
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
|
||||
/* 2. 시설 기본 정보 */
|
||||
{ name: 'PIPL_FCLTY_EXTN', fieldName: '하수관로 총시설연장(m)', header: { text: '하수관로\n총시설연장\n(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 3. 준설연장 및 개소수 */
|
||||
{ name: 'DRDG_PIPE_SM', fieldName: '합계(준설)', header: { text: '합계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DRDG_CMBN_PIPE', fieldName: '합류식(준설)', header: { text: '합류식' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DRDG_RAINWTR_PIPE', fieldName: '우수(준설)', header: { text: '우수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DRDG_PLTWTR_PIPE', fieldName: '오수(준설)', header: { text: '오수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'MNHLE_SITE_DRDG', fieldName: '맨홀(개소)', header: { text: '맨홀\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PLTWTR_RCVR_SITE_DRDG', fieldName: '오·우수받이(개소)', header: { text: '오·우수받이\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'SOILSPLL_SOILHOLE_SITE', fieldName: '토실·토구(개소)', header: { text: '토실·토구\n(개소)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 4. 준설 계획 및 율 */
|
||||
{ name: 'DRDG_TRGET_EXTN', fieldName: '준설 계획 연장(m)', header: { text: '준설 계획\n연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DRDG_TRGET_RT', fieldName: '계획대비 준설율(%)', header: { text: '계획대비\n준설율\n(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PIPL_FCLTY_RT', fieldName: '준설계획 연장대비 준설율(%)', header: { text: '준설계획\n연장대비\n준설율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 5. 준설실적 */
|
||||
{ name: 'DRDG_ACMSLT_AMOUNT', fieldName: '금액(백만원)', header: { text: '금액\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'DRDG_ACMSLT_QY', fieldName: '준설량(㎥)', header: { text: '준설량\n(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 6. 준설토사처리 */
|
||||
{ name: 'DRDG_ACMSLT_UNTPC', fieldName: '준설토 처리비(백만원)', header: { text: '준설토\n처리비\n(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'DRDG_ETND_SM', fieldName: '처리량 계(톤)', header: { text: '처리량 계\n(톤)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RCLM', fieldName: '매립', header: { text: '매립' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ETC', fieldName: '기타(준설)', header: { text: '기타\n(소각, 고형화,\n시멘트원료 등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 7. 기타 및 의견 */
|
||||
{ name: 'RMK_DRDG', fieldName: '비고(준설)', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'YR_GBN', 'PIPL_FCLTY_EXTN',
|
||||
{
|
||||
name: 'drdgGroup', header: { text: '준설연장 및 개소수' },
|
||||
items: [
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
name: 'pipeLenGroup', header: { text: '관로(m)' },
|
||||
items: ['DRDG_PIPE_SM', 'DRDG_CMBN_PIPE', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['DRDG_RAINWTR_PIPE', 'DRDG_PLTWTR_PIPE'] }]
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
},
|
||||
'MNHLE_SITE_DRDG', 'PLTWTR_RCVR_SITE_DRDG', 'SOILSPLL_SOILHOLE_SITE'
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'DRDG_TRGET_EXTN', 'DRDG_TRGET_RT', 'PIPL_FCLTY_RT',
|
||||
{ name: 'acmsltGroup', header: { text: '준설실적' }, items: ['DRDG_ACMSLT_AMOUNT', 'DRDG_ACMSLT_QY'] },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{
|
||||
name: 'processGroup', header: { text: '준설토사처리' },
|
||||
items: ['DRDG_ACMSLT_UNTPC', 'DRDG_ETND_SM', 'RCLM', 'OCEAN_DPSL', 'RUSE', 'ETC']
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'RMK_DRDG'
|
||||
]
|
||||
}
|
||||
/* 2. 시설 기본 정보 */
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로\n총시설연장\n(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 3. 준설연장 및 개소수 */
|
||||
{ name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'stofcbrOtflCnt', fieldName: 'stofcbrOtflCnt', header: { text: '토실·토구\n(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 4. 준설 계획 및 율 */
|
||||
{ name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획\n연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비\n준설율\n(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '전체연장대비\n준설율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 5. 준설실적 */
|
||||
{ name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액\n(백만원)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drdgnQnt', fieldName: 'drdgnQnt', header: { text: '준설량\n(㎥)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 6. 준설토사처리 */
|
||||
{ name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설단가\n(원/m3)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계\n(톤)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양방출' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 7. 기타 및 의견 */
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'addrGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, // sido -> ctpvNm
|
||||
'sggNm',
|
||||
'admdstNm'
|
||||
]
|
||||
},
|
||||
'displayYear',
|
||||
'wholFcltPrlg',
|
||||
{
|
||||
name: 'drdgGroup', header: { text: '준설연장 및 개소수' },
|
||||
items: [
|
||||
{
|
||||
name: 'pipeLenGroup', header: { text: '관로(m)' },
|
||||
items: ['wholSwagePpl', 'cbsysPplPrlg', { name: 'sepRepairGroup', header: { text: '분류식' }, items: ['sprsysExlnPrlg', 'sprsysDrtwPrlg'] }]
|
||||
},
|
||||
'mnhlCnt', 'exlnDrtwRcvprpCnt', 'stofcbrOtflCnt'
|
||||
]
|
||||
},
|
||||
'drdgnPlanPrlg', 'drdgnTrgtCtrsDrdgnRt', 'wholPrlgCtrsDrdgnRt',
|
||||
{ name: 'acmsltGroup', header: { text: '준설실적' }, items: ['drdgnAmt', 'drdgnQnt'] },
|
||||
{
|
||||
name: 'processGroup', header: { text: '준설토사처리' },
|
||||
items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt']
|
||||
},
|
||||
'rmrk'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const SewagePipeMaintainColumnInfoList = rawMaintainList.map(tab => ({
|
||||
...tab,
|
||||
columns: tab.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
}));
|
||||
@@ -1,96 +1,85 @@
|
||||
import { ValueType } from "realgrid";
|
||||
const baseYear = new Date().getFullYear();
|
||||
import dayjs from 'dayjs';
|
||||
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
|
||||
import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
|
||||
const currentYear = dayjs().year() - 1;
|
||||
const startYear = 1996;
|
||||
const totalCount = currentYear - startYear + 1 + 1;
|
||||
|
||||
// 하수도 통계 관리 02. 하수관로
|
||||
const rawList = [
|
||||
{
|
||||
tabKey: 'a',
|
||||
tabKey: 'ST00020001',
|
||||
tabLabel: '지역별 시설현황',
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: 'sido',
|
||||
fieldName: 'sido',
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
editable: false,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계';
|
||||
return '합계';
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계';
|
||||
return '합계';
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
editable: false,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
@@ -99,70 +88,70 @@ const rawList = [
|
||||
]
|
||||
},
|
||||
|
||||
/* 하수정비 기본계획 */
|
||||
{ name: 'targetYr', fieldName: 'targetYr', header: { text: '하수정보기본계획목표년도' } },
|
||||
{ name: 'piplImprmnPlanExtn', fieldName: 'piplImprmnPlanExtn', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'piplPlanExtn', fieldName: 'piplPlanExtn', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
/* 하수정비 기본계획 및 총계 */
|
||||
{ name: 'goalYr', fieldName: 'goalYr', header: { text: '하수정보기본계획목표년도' } },
|
||||
{ name: 'wholPplMtncPlanPrlg', fieldName: 'wholPplMtncPlanPrlg', header: { text: '총관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'wholPlanPrlg', fieldName: 'wholPlanPrlg', header: { text: '총계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로\n보급률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
/* 합류식(m) */
|
||||
{ name: 'cmbnPlanAr', fieldName: 'cmbnPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'cmbnImprmnPlanExtn', fieldName: 'cmbnImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnPlanExtn', fieldName: 'cmbnPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnFcltyExtn', fieldName: 'cmbnFcltyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnRctglExtn', fieldName: 'cmbnRctglExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnCicleExtn', fieldName: 'cmbnCicleExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnOpswrExtn', fieldName: 'cmbnOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnSdgttrExtn', fieldName: 'cmbnSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
/* 합류식 */
|
||||
{ name: 'cbsysPplMtncPlanArea', fieldName: 'cbsysPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'cbsysPplMtncPlanPrlg', fieldName: 'cbsysPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysPlanPrlg', fieldName: 'cbsysPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysFcltPrlg', fieldName: 'cbsysFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysSqrePrlg', fieldName: 'cbsysSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysCiclPrlg', fieldName: 'cbsysCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysOpnPrlg', fieldName: 'cbsysOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysGttrPrlg', fieldName: 'cbsysGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
/* 분류식 - 오수관로 */
|
||||
{ name: 'pltwtrPlanAr', fieldName: 'pltwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'pltwtrImprmnPlanExtn', fieldName: 'pltwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrPlanExtn', fieldName: 'pltwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrFlctyExtn', fieldName: 'pltwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrSqreExtn', fieldName: 'pltwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrCiclExtn', fieldName: 'pltwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
/* 분류식 - 오수관로 */
|
||||
{ name: 'drtwPplMtncPlanArea', fieldName: 'drtwPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'drtwPplMtncPlanPrlg', fieldName: 'drtwPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplPlanPrlg', fieldName: 'drtwPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplFcltPrlg', fieldName: 'drtwPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplSqrePrlg', fieldName: 'drtwPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'drtwPplCiclPrlg', fieldName: 'drtwPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
/* 분류식 - 우수관로 */
|
||||
{ name: 'rainwtrPlanAr', fieldName: 'rainwtrPlanAr', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'rainwtrImprmnPlanExtn', fieldName: 'rainwtrImprmnPlanExtn', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrPlanExtn', fieldName: 'rainwtrPlanExtn', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrFlctyExtn', fieldName: 'rainwtrFlctyExtn', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrSqreExtn', fieldName: 'rainwtrSqreExtn', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrCiclExtn', fieldName: 'rainwtrCiclExtn', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrOpswrExtn', fieldName: 'rainwtrOpswrExtn', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrSdgttrExtn', fieldName: 'rainwtrSdgttrExtn', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
/* 분류식 - 우수관로 */
|
||||
{ name: 'exlnPplMtncPlanArea', fieldName: 'exlnPplMtncPlanArea', header: { text: '관로정비\n계획면적(㎢)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'exlnPplMtncPlanPrlg', fieldName: 'exlnPplMtncPlanPrlg', header: { text: '관로정비\n계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplPlanPrlg', fieldName: 'exlnPplPlanPrlg', header: { text: '계획연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplFcltPrlg', fieldName: 'exlnPplFcltPrlg', header: { text: '시설연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplSqrePrlg', fieldName: 'exlnPplSqrePrlg', header: { text: '사각형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplCiclPrlg', fieldName: 'exlnPplCiclPrlg', header: { text: '원형(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplOpnPrlg', fieldName: 'exlnPplOpnPrlg', header: { text: '개거(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnPplGttrPrlg', fieldName: 'exlnPplGttrPrlg', header: { text: '측구(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
/* 부속시설 */
|
||||
{ name: 'mnhleSite', fieldName: 'mnhleSite', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'pltwtrMnhleSite', fieldName: 'pltwtrMnhleSite', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'rainwtrMnhleSite', fieldName: 'rainwtrMnhleSite', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'pltwtrRcvrSite', fieldName: 'pltwtrRcvrSite', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'rainwtrRcvrSite', fieldName: 'rainwtrRcvrSite', header: { text: '빗물받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'soilspllSite', fieldName: 'soilspllSite', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'soilholeSite', fieldName: 'soilholeSite', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'cbsysMnhlCnt', fieldName: 'cbsysMnhlCnt', header: { text: '합류식맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysDrtwMnhlCnt', fieldName: 'sprsysDrtwMnhlCnt', header: { text: '오수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnMnhlCnt', fieldName: 'sprsysExlnMnhlCnt', header: { text: '우수맨홀(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'drtwRcvprpCnt', fieldName: 'drtwRcvprpCnt', header: { text: '오수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnRcvprpCnt', fieldName: 'exlnRcvprpCnt', header: { text: '우수받이(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'stofcbrCnt', fieldName: 'stofcbrCnt', header: { text: '우수토실(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 대장 및 기타 */
|
||||
{ name: 'sewerRegstrCmptExtn', fieldName: 'sewerRegstrCmptExtn', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sewerRegstrCmptRt', fieldName: 'sewerRegstrCmptRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'rmk', fieldName: 'rmk', header: { text: '비고' } },
|
||||
/* 대장 및 기타*/
|
||||
{ name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sewerLdgrEdpRt', fieldName: 'sewerLdgrEdpRt', header: { text: '하수도대장전산화율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'areaGroup', header: { text: '행정구역' }, items: [
|
||||
{ column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'gugun',
|
||||
'adstDong',
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
]
|
||||
},
|
||||
'displayYear',
|
||||
'targetYr', 'piplImprmnPlanExtn', 'piplPlanExtn', 'piplFcltyExtn', 'piplSupprt',
|
||||
'goalYr', 'wholPplMtncPlanPrlg', 'wholPlanPrlg', 'wholFcltPrlg', 'swagePplPsplyRt',
|
||||
{
|
||||
name: 'combinedGroup', header: { text: '합류식(m)' },
|
||||
items: ['cmbnPlanAr', 'cmbnImprmnPlanExtn', 'cmbnPlanExtn', 'cmbnFcltyExtn',
|
||||
{ name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cmbnRctglExtn', 'cmbnCicleExtn'] },
|
||||
'cmbnOpswrExtn', 'cmbnSdgttrExtn'
|
||||
items: ['cbsysPplMtncPlanArea', 'cbsysPplMtncPlanPrlg', 'cbsysPlanPrlg', 'cbsysFcltPrlg',
|
||||
{ name: 'cmbnAmGroup', header: { text: '암거' }, items: ['cbsysSqrePrlg', 'cbsysCiclPrlg'] },
|
||||
'cbsysOpnPrlg', 'cbsysGttrPrlg'
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -170,253 +159,390 @@ const rawList = [
|
||||
items: [
|
||||
{
|
||||
name: 'foulPipeGroup', header: { text: '오수관로' },
|
||||
items: ['pltwtrPlanAr', 'pltwtrImprmnPlanExtn', 'pltwtrPlanExtn', 'pltwtrFlctyExtn',
|
||||
{ name: 'foulAmGroup', header: { text: '암거' }, items: ['pltwtrSqreExtn', 'pltwtrCiclExtn'] }]
|
||||
items: ['drtwPplMtncPlanArea', 'drtwPplMtncPlanPrlg', 'drtwPplPlanPrlg', 'drtwPplFcltPrlg',
|
||||
{ name: 'foulAmGroup', header: { text: '암거' }, items: ['drtwPplSqrePrlg', 'drtwPplCiclPrlg'] }]
|
||||
},
|
||||
{
|
||||
name: 'rainPipeGroup', header: { text: '우수관로' },
|
||||
items: ['rainwtrPlanAr', 'rainwtrImprmnPlanExtn', 'rainwtrPlanExtn', 'rainwtrFlctyExtn',
|
||||
{ name: 'rainAmGroup', header: { text: '암거' }, items: ['rainwtrSqreExtn', 'rainwtrCiclExtn'] },
|
||||
'rainwtrOpswrExtn', 'rainwtrSdgttrExtn']
|
||||
items: ['exlnPplMtncPlanArea', 'exlnPplMtncPlanPrlg', 'exlnPplPlanPrlg', 'exlnPplFcltPrlg',
|
||||
{ name: 'rainAmGroup', header: { text: '암거' }, items: ['exlnPplSqrePrlg', 'exlnPplCiclPrlg'] },
|
||||
'exlnPplOpnPrlg', 'exlnPplGttrPrlg']
|
||||
}
|
||||
],
|
||||
},
|
||||
'mnhleSite',
|
||||
{ name: 'fcltyGroup', header: { text: '부속시설(개소)' }, items: ['pltwtrMnhleSite', 'rainwtrMnhleSite'] },
|
||||
'pltwtrRcvrSite', 'rainwtrRcvrSite', 'soilspllSite', 'soilholeSite', 'sewerRegstrCmptExtn', 'sewerRegstrCmptRt', 'rmk'
|
||||
'cbsysMnhlCnt',
|
||||
{ name: 'fcltyGroup', header: { text: '분류식' }, items: ['sprsysDrtwMnhlCnt', 'sprsysExlnMnhlCnt'] },
|
||||
'drtwRcvprpCnt', 'exlnRcvprpCnt', 'stofcbrCnt', 'exlnOtflCnt', 'sewerLdgrEdpPrlg', 'sewerLdgrEdpRt', 'rmrk'
|
||||
]
|
||||
},
|
||||
{
|
||||
tabKey: 'b',
|
||||
tabLabel: '시설별 설치현황',
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 3,
|
||||
columns: [
|
||||
{
|
||||
name: 'sido',
|
||||
fieldName: 'sido',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
editable: false,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: 'bscFctNm', fieldName: 'bscFctNm', header: { text: '하수처리장명' }, editable: false },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
editable: false,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'fctCpc', fieldName: 'fctCpc', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 처리구역면적(㎢) */
|
||||
{ name: 'procZoneSm', fieldName: 'procZoneSm', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'cmbnProcZoneAr', fieldName: 'cmbnProcZoneAr', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'sprtProcZoneAr', fieldName: 'sprtProcZoneAr', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'sepRate', fieldName: 'sepRate', header: { text: '분류식화(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 배수설비(개소) */
|
||||
{ name: 'drngEqpSm', fieldName: 'drngEqpSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'cmbnDrngEqp', fieldName: 'cmbnDrngEqp', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprtDrngEqpSm', fieldName: 'sprtDrngEqpSm', header: { text: '합계(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprtDrngEqpCnnc', fieldName: 'sprtDrngEqpCnnc', header: { text: '연결' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprtDrngEqpDcnnc', fieldName: 'sprtDrngEqpDcnnc', header: { text: '미연결' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 하수관로 */
|
||||
{ name: 'piplFcltyExtn', fieldName: 'piplFcltyExtn', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'icgMnnSm', fieldName: 'icgMnnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'icgExtn', fieldName: 'icgExtn', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'mnnExtn', fieldName: 'mnnExtn', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
/* 토실/토구(개소) */
|
||||
{ name: 'cmbnSoilholeSite', fieldName: 'cmbnSoilholeSite', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprtSoilholeSite', fieldName: 'sprtSoilholeSite', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprtSoilspllSite', fieldName: 'sprtSoilspllSite', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 지선(m) */
|
||||
{ name: 'brnchlnSm', fieldName: 'brnchlnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cmbnBrnchlnExtn', fieldName: 'cmbnBrnchlnExtn', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rainwtrBrnchlnExtn', fieldName: 'rainwtrBrnchlnExtn', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'pltwtrBrnchlnExtn', fieldName: 'pltwtrBrnchlnExtn', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
|
||||
{ name: 'rmk', fieldName: 'rmk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'addrGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'gugun',
|
||||
'bscFctNm',
|
||||
]
|
||||
},
|
||||
|
||||
'fctCpc',
|
||||
'displayYear',
|
||||
{ name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['procZoneSm', 'cmbnProcZoneAr', 'sprtProcZoneAr'] },
|
||||
'sepRate',
|
||||
{
|
||||
name: 'drainGroup', header: { text: '배수설비(개소)' },
|
||||
items: ['drngEqpSm', 'cmbnDrngEqp', { name: 'sepDrainGroup', header: { text: '분류식' }, items: ['sprtDrngEqpSm', 'sprtDrngEqpCnnc', 'sprtDrngEqpDcnnc'] }],
|
||||
},
|
||||
{
|
||||
name: 'pipeGroup', header: { text: '하수관로' },
|
||||
items: [
|
||||
'piplFcltyExtn',
|
||||
{ name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['icgMnnSm', 'icgExtn', 'mnnExtn'] },
|
||||
{
|
||||
name: 'siteGroup', header: { text: '토실/토구(개소)' },
|
||||
items: [
|
||||
{ name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cmbnSoilholeSite'] },
|
||||
{ name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprtSoilholeSite', 'sprtSoilspllSite'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'branchGroup', header: { text: '지선(m)' },
|
||||
items: ['brnchlnSm', 'cmbnBrnchlnExtn', { name: 'sepBranchGroup', header: { text: '우수/오수' }, items: ['rainwtrBrnchlnExtn', 'pltwtrBrnchlnExtn'] }]
|
||||
}
|
||||
],
|
||||
},
|
||||
'rmk'
|
||||
]
|
||||
},
|
||||
{
|
||||
tabKey: 'c',
|
||||
tabKey: 'ST00020002',
|
||||
tabLabel: '시설별 설치현황',
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 4,
|
||||
columns: [
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue) uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue) uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'sprsysVartnRt', fieldName: 'sprsysVartnRt', header: { text: '분류식화' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'mltvEqpmCnt', fieldName: 'mltvEqpmCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'cbsysMltvEqpmCnt', fieldName: 'cbsysMltvEqpmCnt', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysMltvEqpmCnt', fieldName: 'sprsysMltvEqpmCnt', header: { text: '분류식 합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '총\n시설연장\n(m)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'wholIcptgAtlfcPrlg', fieldName: 'wholIcptgAtlfcPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'icptgPrlg', fieldName: 'icptgPrlg', header: { text: '차집' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'atlfcsPrlg', fieldName: 'atlfcsPrlg', header: { text: '오수간선' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysStofcbrCnt', fieldName: 'cbsysStofcbrCnt', header: { text: '우수토실(합류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysStofcbrCnt', fieldName: 'sprsysStofcbrCnt', header: { text: '우수토실(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sprsysExlnOtflCnt', fieldName: 'sprsysExlnOtflCnt', header: { text: '우수토구(분류)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'wholBrchlnPrlg', fieldName: 'wholBrchlnPrlg', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'cbsysBrchlnPrlg', fieldName: 'cbsysBrchlnPrlg', header: { text: '합류식' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'exlnBrchlnPrlg', fieldName: 'exlnBrchlnPrlg', header: { text: '우수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'drtwBrchlnPrlg', fieldName: 'drtwBrchlnPrlg', header: { text: '오수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.00' },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'addrGroup', header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] },
|
||||
'sggNm', 'admdstNm'
|
||||
]
|
||||
},
|
||||
'swtrmtFcltNm', 'prcsFcltCpct', 'displayYear',
|
||||
{ name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] },
|
||||
'sprsysVartnRt',
|
||||
{ name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] },
|
||||
{
|
||||
name: 'pipeGroup', header: { text: '하수관로' },
|
||||
items: [
|
||||
'wholFcltPrlg',
|
||||
{ name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] },
|
||||
{
|
||||
name: 'siteGroup', header: { text: '토실/토구(개소)' },
|
||||
items: [
|
||||
{ name: 'cmbnSiteGroup', header: { text: '합류식' }, items: ['cbsysStofcbrCnt'] },
|
||||
{ name: 'sepSiteGroup', header: { text: '분류식' }, items: ['sprsysStofcbrCnt', 'sprsysExlnOtflCnt'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'branchGroup', header: { text: '지선(m)' },
|
||||
items: ['wholBrchlnPrlg', 'cbsysBrchlnPrlg', { name: 'sepBranchGroup', header: { text: '분류식' }, items: ['exlnBrchlnPrlg', 'drtwBrchlnPrlg'] }]
|
||||
}
|
||||
],
|
||||
},
|
||||
'rmrk'
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
tabKey: 'ST00020003',
|
||||
tabLabel: '관종별 현황',
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 3,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'SWRG_PIPE_NM', fieldName: '관종', header: { text: '관종' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{ name: 'TOTAL_EXTN_SUM', fieldName: '계(총계)', header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'LAYING_EXTN_SUM', fieldName: '매설연장(m)_총계', header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'REMVL_EXTN_SUM', fieldName: '철거연장(m)_총계', header: { text: '철거연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'pipeByPrstWholSum', fieldName: 'pipeByPrstWhol', header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
const lyngField = `yy${dtoNum}LyngPrlg`;
|
||||
const dmltnField = `yy${dtoNum}DmltnPrlg`;
|
||||
|
||||
...Array.from({ length: 30 }, (_, i) => {
|
||||
const yNum = (30 - i).toString().padStart(2, '0');
|
||||
const yrOffset = 29 - i;
|
||||
return [
|
||||
{ name: `TOTAL_EXTN_${yNum}Y`, fieldName: `계(-${yrOffset}년)`, header: { text: '계' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: `LAYING_EXTN_${yNum}Y`, fieldName: `매설연장(m)_(-${yrOffset}년)`, header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: `REMVL_EXTN_${yNum}Y`, fieldName: `철거연장(m)_(-${yrOffset}년)`, header: { text: '철거연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }
|
||||
{ name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }
|
||||
];
|
||||
}).flat(),
|
||||
|
||||
{ name: 'LAYING_EXTN', fieldName: `매설연장(m)_당해년도`, header: { text: '매설연장(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'SWRG_PIPE_NM', 'YR_GBN',
|
||||
{ name: 'summaryGroup', header: { text: '총계' }, items: ['TOTAL_EXTN_SUM', 'LAYING_EXTN_SUM', 'REMVL_EXTN_SUM'] },
|
||||
...Array.from({ length: 30 }, (_, i) => {
|
||||
const yNum = (30 - i).toString().padStart(2, '0');
|
||||
const yrOffset = 29 - i;
|
||||
return { name: `yr${yNum}Group`, header: { text: `-${yrOffset}년` }, items: [`TOTAL_EXTN_${yNum}Y`, `LAYING_EXTN_${yNum}Y`, `REMVL_EXTN_${yNum}Y`] };
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
], columnLayout: [
|
||||
{
|
||||
name: 'areaGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm', 'swrgPipeNm'
|
||||
] },
|
||||
'displayYear',
|
||||
{ name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum', 'lyngPrlgSum', 'dmltnPrlgSum'] },
|
||||
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
|
||||
const dtoNum = totalCount - i;
|
||||
const yearLabel = i === 0
|
||||
? '1996년이전'
|
||||
: `${startYear + (i - 1)}년`;
|
||||
|
||||
return {
|
||||
name: `group_${yearLabel}`,
|
||||
header: { text: yearLabel },
|
||||
items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`]
|
||||
};
|
||||
}),
|
||||
{ name: 'currentYrGroup', header: { text: `${baseYear}년` }, items: ['LAYING_EXTN'] },
|
||||
'RMK'
|
||||
'rmrk'
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
tabKey: 'd',
|
||||
tabKey: 'ST00020004',
|
||||
tabLabel: '관경별 현황',
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 3,
|
||||
columns: [
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '구군' } },
|
||||
{ name: 'SWRG_PIPE_NM', fieldName: '관종', header: { text: '관종' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
{ name: 'TOTAL_EXTN_M', fieldName: '총계(m)', header: { text: '총계(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'UNDER_200', fieldName: '200mm 미만', header: { text: '200mm 미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OVER_200_UNDEV_300', fieldName: '200mm이상~300mm미만', header: { text: '200mm이상~\n300mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OVER_300_UNDEV_500', fieldName: '300mm이상~500mm미만', header: { text: '300mm이상~\n500mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OVER_500_UNDEV_800', fieldName: '500mm이상~800mm미만', header: { text: '500mm이상~\n800mm미만' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OVER_800', fieldName: '800mm 이상', header: { text: '800mm 이상' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'BOX', fieldName: '사각형(m)', header: { text: '사각형(m)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ETC', fieldName: '기타(측구/개거 등)', header: { text: '기타\n(측구/개거 등)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, // gugun -> sggNm
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'swrgPipeNm', fieldName: 'swrgPipeNm', header: { text: '관종' }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
return Array.from(uniqueValues)[divisionIndex] ? Array.from(uniqueValues)[divisionIndex] + '합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/* 관경별 수치 필드 */
|
||||
{ name: 'pdmtByTsmWhol', fieldName: 'pdmtByTsmWhol', header: { text: '총계(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'step1PdmtPrlg', fieldName: 'step1PdmtPrlg', header: { text: '200mm 미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'step2PdmtPrlg', fieldName: 'step2PdmtPrlg', header: { text: '200mm이상~\n300mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'step3PdmtPrlg', fieldName: 'step3PdmtPrlg', header: { text: '300mm이상~\n500mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'step4PdmtPrlg', fieldName: 'step4PdmtPrlg', header: { text: '500mm이상~\n800mm미만' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'step5PdmtPrlg', fieldName: 'step5PdmtPrlg', header: { text: '800mm 이상' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'sqrePdmtPrlg', fieldName: 'sqrePdmtPrlg', header: { text: '사각형(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'etcPdmtPrlg', fieldName: 'etcPdmtPrlg', header: { text: '기타\n(측구/개거 등)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // rmk -> rmrk
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'SWRG_PIPE_NM', 'YR_GBN', 'TOTAL_EXTN_M',
|
||||
{ name: 'circleGroup', header: { text: '원형(m)' }, items: ['UNDER_200', 'OVER_200_UNDEV_300', 'OVER_300_UNDEV_500', 'OVER_500_UNDEV_800', 'OVER_800'] },
|
||||
'BOX', 'ETC', 'RMK',
|
||||
{
|
||||
name: 'addrGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
]
|
||||
},
|
||||
'swrgPipeNm',
|
||||
'displayYear',
|
||||
'pdmtByTsmWhol',
|
||||
{
|
||||
name: 'circleGroup',
|
||||
header: { text: '원형(m)' },
|
||||
items: ['step1PdmtPrlg', 'step2PdmtPrlg', 'step3PdmtPrlg', 'step4PdmtPrlg', 'step5PdmtPrlg'] // 이름 일괄 변경
|
||||
},
|
||||
'sqrePdmtPrlg',
|
||||
'etcPdmtPrlg',
|
||||
'rmrk'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -425,6 +551,7 @@ export const SewagePipeColumnInfoListRvw = rawList.map(tab => ({
|
||||
...tab,
|
||||
columns: tab.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
}));
|
||||
@@ -1,194 +1,348 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import {
|
||||
footerIntArrayByDivision,
|
||||
footerDecimal1ArrayByDivision,
|
||||
footerDecimal3ArrayByDivision
|
||||
} from "@/components/util/grid/footerSummary";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
// 하수도 통계 관리 04. 하수저류 시설 및 빗물 펌프장
|
||||
const rawReservoirList = [
|
||||
{
|
||||
tabKey: 'a',
|
||||
tabLabel: '하수저류시설',
|
||||
tabKey: 'ST00040001',
|
||||
tabLabel: '하수저류시설현황',
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
fixedColumns: 4,
|
||||
columns: [
|
||||
/* 1. 기본 정보 */
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'SWGE_STRGE_NM', fieldName: '시설명', header: { text: '시설명' } },
|
||||
{ name: 'ADDR', fieldName: '위치', header: { text: '위치' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
|
||||
/* 2. 시설 제원 */
|
||||
{ name: 'RN_DT', fieldName: '가동개시 년월일', header: { text: '가동개시\n년월일' } },
|
||||
{ name: 'PL_DSGN_FRQNC', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } },
|
||||
{ name: 'INS_PER1', fieldName: '침수예방', header: { text: '침수예방' } },
|
||||
{ name: 'INS_PER2', fieldName: '수질오염저감', header: { text: '수질오염저감' } },
|
||||
{ name: 'INS_PER3', fieldName: '하수 재이용 용도', header: { text: '하수\n재이용\n용도' } },
|
||||
{ name: 'FAC_RES', fieldName: '시설구조', header: { text: '시설구조(일반지하식,지하터널식,기타)' } },
|
||||
{ name: 'DGR_AR', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'SWGE_SGBS_CPCTY', fieldName: '시설용량(㎥)', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 3. 펌프시설 */
|
||||
{ name: 'TOT_CPCTY', fieldName: '총양수량(㎥/min)', header: { text: '총양수량\n(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CALBR', fieldName: '구경(mm)_1', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY', fieldName: '대당 양수량_1', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO', fieldName: '펌프대수_1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO', fieldName: '예비펌프대수_1', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_CALBR2', fieldName: '구경(mm)_2', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY2', fieldName: '대당 양수량_2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO2', fieldName: '펌프대수_2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO2', fieldName: '예비펌프대수_2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_CALBR3', fieldName: '구경(mm)_3', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY3', fieldName: '대당 양수량_3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO3', fieldName: '펌프대수_3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO3', fieldName: '예비펌프대수_3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
|
||||
/* 4. 부대시설 및 방식 */
|
||||
{ name: 'FRE_FNNC', fieldName: '전처리시설(유/무)', header: { text: '전처리시설(유/무)' } },
|
||||
{ name: 'DORZEQP_FNNC', fieldName: '탈취설비(유/무)', header: { text: '탈취설비(유/무)' } },
|
||||
{ name: 'SELF_ETRQ_FNNC', fieldName: '자가발전(유/무)', header: { text: '자가발전(유/무)' } },
|
||||
{ name: 'DRA_MTH', fieldName: '배수방식(자연배수,강제배수)', header: { text: '배수방식(자연배수,강제배수)' } },
|
||||
{ name: 'ELEC_MTH', fieldName: '저류방식(직렬,병렬)', header: { text: '저류방식(직렬,병렬)' } },
|
||||
{ name: 'ELEC_PRO_MAT', fieldName: '저류수 처리방법', header: { text: '저류수\n처리방법' } },
|
||||
{ name: 'DIS_WAT', fieldName: '방류수역(또는 연계처리장명)', header: { text: '방류수역(또는 연계처리장명)' } },
|
||||
|
||||
/* 5. 최종처분량 및 방법 */
|
||||
{ name: 'SDMT_TRE_CPCTY', fieldName: '침사(량)', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'IPRT_TRE_CPCTY', fieldName: '협잡물(량)', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'SDMT_TRE_MTH', fieldName: '침사(방법)', header: { text: '침사' } },
|
||||
{ name: 'IPRT_TRE_MTH', fieldName: '협잡물(방법)', header: { text: '협잡물' } },
|
||||
|
||||
/* 6. 운영실적 */
|
||||
{ name: 'YRSTP_RN_DYN', fieldName: '가동일수', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'DSPLCMNT_AMNT', fieldName: '총배수량(㎥/년)', header: { text: '총배수량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'SWGE_STRGE_NM', 'ADDR', 'YR_GBN', 'RN_DT', 'PL_DSGN_FRQNC',
|
||||
{
|
||||
name: 'purposeGroup',
|
||||
header: { text: '설치목적(O,X로 표기, 중복체크 가능)' },
|
||||
items: ['INS_PER1', 'INS_PER2', 'INS_PER3']
|
||||
},
|
||||
'FAC_RES', 'DGR_AR', 'SWGE_SGBS_CPCTY',
|
||||
{
|
||||
name: 'pumpMainGroup',
|
||||
header: { text: '펌프시설' },
|
||||
items: [
|
||||
'TOT_CPCTY',
|
||||
{ name: 'p1Group', header: { text: '펌프시설1' }, items: ['PUMP_CALBR', 'WP_CPCTY', 'PUMP_CO', 'PUMP_PREPAR_CO'] },
|
||||
{ name: 'p2Group', header: { text: '펌프시설2' }, items: ['PUMP_CALBR2', 'WP_CPCTY2', 'PUMP_CO2', 'PUMP_PREPAR_CO2'] },
|
||||
{ name: 'p3Group', header: { text: '펌프시설3' }, items: ['PUMP_CALBR3', 'WP_CPCTY3', 'PUMP_CO3', 'PUMP_PREPAR_CO3'] }
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{ name: 'impdFcltNm', fieldName: 'impdFcltNm', header: { text: '저류시설명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '저류시설위치' }, mergeCells: true, sortdisable: true },
|
||||
{
|
||||
name: 'subFacGroup',
|
||||
header: { text: '부대시설' },
|
||||
items: ['FRE_FNNC', 'DORZEQP_FNNC', 'SELF_ETRQ_FNNC']
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'DRA_MTH', 'ELEC_MTH', 'ELEC_PRO_MAT', 'DIS_WAT',
|
||||
{
|
||||
name: 'disposalAmtGroup',
|
||||
header: { text: '최종처분량(ton/년)' },
|
||||
items: ['SDMT_TRE_CPCTY', 'IPRT_TRE_CPCTY']
|
||||
},
|
||||
{
|
||||
name: 'disposalMthGroup',
|
||||
header: { text: '최종처분방법' },
|
||||
items: ['SDMT_TRE_MTH', 'IPRT_TRE_MTH']
|
||||
},
|
||||
{
|
||||
name: 'opGroup',
|
||||
header: { text: '운영실적' },
|
||||
items: ['YRSTP_RN_DYN', 'DSPLCMNT_AMNT']
|
||||
},
|
||||
'RMK'
|
||||
]
|
||||
},
|
||||
{
|
||||
tabKey: 'b',
|
||||
tabLabel: '빗물펌프장',
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
/* 1. 기본 정보 */
|
||||
{ name: 'SIDO', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'SWGE_STRGE_NM', fieldName: '시설명', header: { text: '시설명' } },
|
||||
{ name: 'ADDR', fieldName: '위치', header: { text: '위치' } },
|
||||
{ name: 'YR_GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
|
||||
/* 2. 시설 제원 */
|
||||
{ name: 'FCT_KND', fieldName: '시설물종류', header: { text: '시설물종류' } },
|
||||
{ name: 'RN_DT', fieldName: '가동개시 년월일', header: { text: '가동개시\n년월일' } },
|
||||
{ name: 'PL_DSGN_FRQNC', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } },
|
||||
{ name: 'DGR_AR', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
/* 2. 시설 제원 */
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시 년월일' } },
|
||||
{ name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } },
|
||||
{ name: 'fldngPrvtnYn', fieldName: 'fldngPrvtnYn', header: { text: '침수예방' } },
|
||||
{ name: 'wqPlltnMtgtYn', fieldName: 'wqPlltnMtgtYn', header: { text: '수질오염저감' } },
|
||||
{ name: 'rtlztnUsgYn', fieldName: 'rtlztnUsgYn', header: { text: '하수 재이용 용도' } },
|
||||
{ name: 'fcltStrctCn', fieldName: 'fcltStrctCn', header: { text: '시설구조' } },
|
||||
{ name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision },
|
||||
{ name: 'impdFcltCpct', fieldName: 'impdFcltCpct', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
|
||||
/* 3. 유수지 및 흡수정 */
|
||||
{ name: 'REV_FNNC', fieldName: '유수지유무(유,무)', header: { text: '유수지유무\n(유,무)' } },
|
||||
{ name: 'REV_CPCTY', fieldName: '저류량(유수지)', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ABS_CPCTY', fieldName: '저류량(흡수정)', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'SWGE_SGBS_CPCTY', fieldName: '시설용량(㎥)', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
/* 3. 펌프시설 */
|
||||
{ name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
{ name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
{ name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
/* 4. 펌프시설 */
|
||||
{ name: 'TOT_CPCTY', fieldName: '총양수량(㎥/min)', header: { text: '총양수량\n(㎥/min)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CALBR', fieldName: '구경(mm)_1', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY', fieldName: '대당 양수량_1', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO', fieldName: '펌프대수_1', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO', fieldName: '예비펌프대수_1', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_CALBR2', fieldName: '구경(mm)_2', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY2', fieldName: '대당 양수량_2', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO2', fieldName: '펌프대수_2', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO2', fieldName: '예비펌프대수_2', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_CALBR3', fieldName: '구경(mm)_3', header: { text: '구경(mm)' } },
|
||||
{ name: 'WP_CPCTY3', fieldName: '대당 양수량_3', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'PUMP_CO3', fieldName: '펌프대수_3', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'PUMP_PREPAR_CO3', fieldName: '예비펌프대수_3', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
/* 4. 부대시설 및 방식 */
|
||||
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설(유/무)' } },
|
||||
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비(유/무)' } },
|
||||
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전(유/무)' } },
|
||||
{ name: 'mltvMthCn', fieldName: 'mltvMthCn', header: { text: '배수방식' } },
|
||||
{ name: 'impdMthCn', fieldName: 'impdMthCn', header: { text: '저류방식' } },
|
||||
{ name: 'impdPrcsMthdCn', fieldName: 'impdPrcsMthdCn', header: { text: '저류수 처리방법' } },
|
||||
{ name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역' } },
|
||||
|
||||
/* 5. 부대시설 및 기타 */
|
||||
{ name: 'FRE_FNNC', fieldName: '전처리시설', header: { text: '전처리시설' } },
|
||||
{ name: 'DORZEQP_FNNC', fieldName: '탈취설비', header: { text: '탈취설비' } },
|
||||
{ name: 'SELF_ETRQ_FNNC', fieldName: '자가발전', header: { text: '자가발전' } },
|
||||
{ name: 'DIS_WAT', fieldName: '방류수역', header: { text: '방류수역' } },
|
||||
/* 5. 최종처분량 및 방법 */
|
||||
{ name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'prcptnLastDspsMthdExpln', fieldName: 'prcptnLastDspsMthdExpln', header: { text: '침사' } },
|
||||
{ name: 'ctmnLastDspsMthdExpln', fieldName: 'ctmnLastDspsMthdExpln', header: { text: '협잡물' } },
|
||||
|
||||
/* 6. 최종처분량 및 방법 */
|
||||
{ name: 'SDMT_TRE_CPCTY', fieldName: '침사(량)', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'IPRT_TRE_CPCTY', fieldName: '협잡물(량)', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'SDMT_TRE_MTH', fieldName: '침사(방법)', header: { text: '침사(방법)' } },
|
||||
{ name: 'IPRT_TRE_MTH', fieldName: '협잡물(방법)', header: { text: '협잡물(방법)' } },
|
||||
|
||||
/* 7. 운영실적 */
|
||||
{ name: 'YRSTP_RN_DYN', fieldName: '가동일수', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'DSPLCMNT_AMNT', fieldName: '총배수량(㎥/년)', header: { text: '총배수량\n(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
/* 6. 운영실적 */
|
||||
{ name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'addrGroup', header: { text: '행정구역' }, items: ['SIDO', 'GUGUN'] },
|
||||
'SWGE_STRGE_NM', 'ADDR', 'YR_GBN', 'FCT_KND', 'RN_DT', 'PL_DSGN_FRQNC', 'DGR_AR',
|
||||
{ name: 'revGroup', header: { text: '유수지' }, items: ['REV_FNNC', 'REV_CPCTY'] },
|
||||
{ name: 'absGroup', header: { text: '흡수정' }, items: ['ABS_CPCTY'] },
|
||||
'SWGE_SGBS_CPCTY',
|
||||
{
|
||||
name: 'areaGroup', header: { text: '행정구역' }, items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm','admdstNm'
|
||||
]
|
||||
},
|
||||
'impdFcltNm', 'impdFcltPstnCn','displayYear', 'oprtngStrtYmd', 'planDesignFreqCnt',
|
||||
{ name: 'purposeGroup', header: { text: '설치목적' }, items: ['fldngPrvtnYn', 'wqPlltnMtgtYn', 'rtlztnUsgYn'] },
|
||||
'fcltStrctCn', 'wtrStrgZoneArea', 'impdFcltCpct',
|
||||
{
|
||||
name: 'pumpMainGroup', header: { text: '펌프시설' },
|
||||
items: [
|
||||
'TOT_CPCTY',
|
||||
{ name: 'p1Group', header: { text: '펌프시설1' }, items: ['PUMP_CALBR', 'WP_CPCTY', 'PUMP_CO', 'PUMP_PREPAR_CO'] },
|
||||
{ name: 'p2Group', header: { text: '펌프시설2' }, items: ['PUMP_CALBR2', 'WP_CPCTY2', 'PUMP_CO2', 'PUMP_PREPAR_CO2'] },
|
||||
{ name: 'p3Group', header: { text: '펌프시설3' }, items: ['PUMP_CALBR3', 'WP_CPCTY3', 'PUMP_CO3', 'PUMP_PREPAR_CO3'] }
|
||||
'grssPpqty',
|
||||
{ name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpFcltN1DmtrExpln', 'pumpFcltN1PerPpqty', 'pumpFcltN1PumpCnt', 'pumpFcltN1RsrvPumpCnt'] },
|
||||
{ name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpFcltN2DmtrExpln', 'pumpFcltN2PerPpqty', 'pumpFcltN2PumpCnt', 'pumpFcltN2RsrvPumpCnt'] },
|
||||
{ name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpFcltN3DmtrExpln', 'pumpFcltN3PerPpqty', 'pumpFcltN3PumpCnt', 'pumpFcltN3RsrvPumpCnt'] }
|
||||
]
|
||||
},
|
||||
{ name: 'subFacGroup', header: { text: '부대시설' }, items: ['FRE_FNNC', 'DORZEQP_FNNC', 'SELF_ETRQ_FNNC'] },
|
||||
'DIS_WAT',
|
||||
{ name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['SDMT_TRE_CPCTY', 'IPRT_TRE_CPCTY'] },
|
||||
{ name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['SDMT_TRE_MTH', 'IPRT_TRE_MTH'] },
|
||||
{ name: 'opGroup', header: { text: '운영실적' }, items: ['YRSTP_RN_DYN', 'DSPLCMNT_AMNT'] },
|
||||
'RMK'
|
||||
{ name: 'subFacGroup', header: { text: '부대시설' }, items: ['prcsgFcltYn', 'ddrztnEqpmYn', 'oslfEtgtYn'] },
|
||||
'mltvMthCn', 'impdMthCn', 'impdPrcsMthdCn', 'dschgWtrsExpln',
|
||||
{ name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['prcptnLastDspsQnt', 'ctmnLastDspsQnt'] },
|
||||
{ name: 'disposalMthGroup', header: { text: '최종처분방법' }, items: ['prcptnLastDspsMthdExpln', 'ctmnLastDspsMthdExpln'] },
|
||||
{ name: 'opGroup', header: { text: '운영실적' }, items: ['oprtngDayCnt', 'grssMltvQnt'] },
|
||||
'rmrk'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
tabKey: 'ST00040002',
|
||||
tabLabel: '빗물펌프장',
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + ' 합계';
|
||||
return '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + ' 합계';
|
||||
return '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true },
|
||||
{ name: 'impdFcltNm', fieldName: 'impdFcltNm', header: { text: '시설명' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'impdFcltPstnCn',fieldName: 'impdFcltPstnCn',header: { text: '위치' }, mergeCells: true, sortdisable: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + ' 합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/* 2. 시설 제원 */
|
||||
{ name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } },
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시 년월일' } },
|
||||
{ name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } },
|
||||
{ name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000', footer: footerDecimal3ArrayByDivision },
|
||||
{ name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무\n(유,무)' } },
|
||||
{ name: 'rsvrImpndQnt', fieldName: 'rsvrImpndQnt', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'abstnRgnImdQnt', fieldName: 'abstnRgnImdQnt', header: { text: '저류량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(㎥)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
|
||||
/* 3. 펌프시설 */
|
||||
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
{ name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
{ name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0', footer: footerDecimal1ArrayByDivision },
|
||||
{ name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
/* 4. 부대시설 */
|
||||
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설' } },
|
||||
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비' } },
|
||||
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전' } },
|
||||
{ name: 'dschgWtrsExpln',fieldName: 'dschgWtrsExpln',header: { text: '방류수역' } },
|
||||
|
||||
/* 5. 최종처분량 */
|
||||
{ name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물(량)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
|
||||
/* 6. 운영실적 */
|
||||
{ name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(㎥/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0', footer: footerIntArrayByDivision },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'areaGroup', header: { text: '행정구역' }, items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm', 'admdstNm',
|
||||
]
|
||||
},
|
||||
'displayYear', 'fcltyKndCn', 'oprtngStrtYmd', 'planDesignFreqCnt', 'wtrStrgZoneArea',
|
||||
{ name: 'revGroup', header: { text: '유수지' }, items: ['rsvrYn', 'rsvrImpndQnt'] },
|
||||
{ name: 'absGroup', header: { text: '흡수정' }, items: ['abstnRgnImdQnt'] },
|
||||
'fcltCpct',
|
||||
{
|
||||
name: 'pumpMainGroup', header: { text: '펌프시설' },
|
||||
items: [
|
||||
{ name: 'p1Group', header: { text: '펌프시설1' }, items: ['pumpFcltN1DmtrExpln', 'pumpFcltN1PerPpqty', 'pumpFcltN1PumpCnt', 'pumpFcltN1RsrvPumpCnt'] },
|
||||
{ name: 'p2Group', header: { text: '펌프시설2' }, items: ['pumpFcltN2DmtrExpln', 'pumpFcltN2PerPpqty', 'pumpFcltN2PumpCnt', 'pumpFcltN2RsrvPumpCnt'] },
|
||||
{ name: 'p3Group', header: { text: '펌프시설3' }, items: ['pumpFcltN3DmtrExpln', 'pumpFcltN3PerPpqty', 'pumpFcltN3PumpCnt', 'pumpFcltN3RsrvPumpCnt'] }
|
||||
]
|
||||
},
|
||||
{ name: 'subFacGroup', header: { text: '부대시설' }, items: ['prcsgFcltYn', 'ddrztnEqpmYn', 'oslfEtgtYn'] },
|
||||
'dschgWtrsExpln',
|
||||
{ name: 'disposalAmtGroup', header: { text: '최종처분량(ton/년)' }, items: ['prcptnLastDspsQnt', 'ctmnLastDspsQnt'] },
|
||||
{ name: 'opGroup', header: { text: '운영실적' }, items: ['oprtngDayCnt', 'grssMltvQnt'] },
|
||||
'rmrk'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const SewageReservoirPumpInfoListRvw = rawReservoirList.map(tab => ({
|
||||
...tab,
|
||||
columns: tab.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
}));
|
||||
@@ -1,89 +1,74 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
|
||||
import type { GridView, DataColumn, ColumnFooter } from "@/components/type/DataGrid.type";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
export const SeweragePenetrationRateInfoList = {
|
||||
|
||||
const rawSeweragePenetrationRateInfoList = {
|
||||
columns: [
|
||||
{
|
||||
name: 'sido',
|
||||
fieldName: 'sido',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
editable: false,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계';
|
||||
return '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length > divisionIndex) return divisionValues[divisionIndex] + '합계';
|
||||
return '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
editable: false,
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '합계';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView, column: DataColumn, footerIndex: number, columnFooter: ColumnFooter, value: number | string | undefined) => {
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const rowCount = dataSource.getRowCount();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const divisionValue = dataSource.getValue(i, 'displayYear');
|
||||
if (divisionValue !== null && divisionValue !== undefined && divisionValue !== '') {
|
||||
uniqueValues.add(String(divisionValue));
|
||||
}
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
@@ -91,39 +76,34 @@ export const SeweragePenetrationRateInfoList = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' } },
|
||||
{ name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } },
|
||||
{ name: 'wtsysNm', fieldName: 'rvbsn', header: { text: '수계' } },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
|
||||
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' } },
|
||||
{ name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' } },
|
||||
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' } },
|
||||
{ name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } },
|
||||
{ name: 'popltnSm', fieldName: 'popltnSm', header: { text: '총인구' }, footer: footerIntArrayByDivision, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'arSm', fieldName: 'arSm', header: { text: '총면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'swgeProcZoneInnerSm', fieldName: 'swgeProcZoneInnerSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-total' },
|
||||
|
||||
// 공공하수처리인구
|
||||
{ name: 'pblswrgPopltnSm', fieldName: 'pblswrgPopltnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pblswrgTertTrt', fieldName: 'pblswrgTertTrt', header: { text: '3차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 폐수처리인구(명)
|
||||
{ name: 'wstrPopltnSm', fieldName: 'wstrPopltnSm', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wstrTertTrt', fieldName: 'wstrTertTrt', header: { text: '3차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
{ name: 'notConectPopltn', fieldName: 'notConectPopltn', header: { text: '미접속인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'swgeProcZoneInnerAr', fieldName: 'swgeProcZoneInnerAr', header: { text: '면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'swgeProcZoneOuterSm', fieldName: 'swgeProcZoneOuterSm', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'townUnprocPopltn', fieldName: 'townUnprocPopltn', header: { text: '미처리인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'townSwttPopltn', fieldName: 'townSwttPopltn', header: { text: '오수처리인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'townWrrtnPopltn', fieldName: 'townWrrtnPopltn', header: { text: '정화조인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'swgeProcZoneOuterAr', fieldName: 'swgeProcZoneOuterAr', header: { text: '면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'pblswrgPopltnSuprt', fieldName: 'pblswrgPopltnSuprt', header: { text: '공공하수처리구역인구보급율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'adtrtPopltnSuprt', fieldName: 'adtrtPopltnSuprt', header: { text: '고도처리인구보급율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'swrgInstlrt', fieldName: 'swrgInstlrt', header: { text: '하수도설치율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'rmk', fieldName: 'rmk', header: { text: '비고' } },
|
||||
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } },
|
||||
{ name: 'wholPpltnCnt', fieldName: 'wholPpltnCnt', header: { text: '총인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholArea', fieldName: 'wholArea', header: { text: '총면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'wholSwtrmtPpltnCnt', fieldName: 'wholSwtrmtPpltnCnt', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholPblSwtrmtPpltnCnt', fieldName: 'wholPblSwtrmtPpltnCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '3차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholWwtPrcsPpltnCnt', fieldName: 'wholWwtPrcsPpltnCnt', header: { text: '합계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '1차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '2차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '3차처리' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'wholStaaExcpPpltnCnt', fieldName: 'wholStaaExcpPpltnCnt', header: { text: '계' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'staaPsplyRt', fieldName: 'staaPsplyRt', header: { text: '공공하수처리구역인구보급율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'altdPrcsPsplyRt', fieldName: 'altdPrcsPsplyRt', header: { text: '고도처리인구보급율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'sewerInstlRt', fieldName: 'sewerInstlRt', header: { text: '하수도설치율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
@@ -131,56 +111,65 @@ export const SeweragePenetrationRateInfoList = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'sido', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'gugun',
|
||||
'adstDong',
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
]
|
||||
},
|
||||
'displayYear',
|
||||
'rvbsn',
|
||||
'bstrm',
|
||||
'wtsysNm',
|
||||
'trbtrNm',
|
||||
{
|
||||
name: 'groupB',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '수계영향권별분류' },
|
||||
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
|
||||
},
|
||||
'speclCntrplnAr',
|
||||
'popltnSm',
|
||||
'arSm',
|
||||
'spcCtpnRgnCn',
|
||||
'wholPpltnCnt',
|
||||
'wholArea',
|
||||
{
|
||||
name: 'groupD',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역내' },
|
||||
items: [
|
||||
'swgeProcZoneInnerSm',
|
||||
'wholSwtrmtPpltnCnt',
|
||||
{
|
||||
name: 'pblswrgGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '공공하수처리인구(명)' },
|
||||
items: ['pblswrgPopltnSm', 'pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'],
|
||||
items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'],
|
||||
},
|
||||
{
|
||||
name: 'wstrGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '폐수처리인구(명)' },
|
||||
items: ['wstrPopltnSm', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'],
|
||||
items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'],
|
||||
},
|
||||
'notConectPopltn',
|
||||
'swgeProcZoneInnerAr'
|
||||
'yetCntnPpltnCnt',
|
||||
'staaArea'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupE',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역외' },
|
||||
items: ['swgeProcZoneOuterSm', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn', 'swgeProcZoneOuterAr'],
|
||||
items: ['wholStaaExcpPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt', 'staaExcpArea'],
|
||||
},
|
||||
'pblswrgPopltnSuprt',
|
||||
'adtrtPopltnSuprt',
|
||||
'swrgInstlrt',
|
||||
'rmk',
|
||||
'staaPsplyRt',
|
||||
'altdPrcsPsplyRt',
|
||||
'sewerInstlRt',
|
||||
'rmrk',
|
||||
],
|
||||
fixedColumns: 2,
|
||||
fixedColumns: 2,
|
||||
rowStartNum: 3
|
||||
};
|
||||
|
||||
export const SeweragePenetrationRateInfoList = {
|
||||
...rawSeweragePenetrationRateInfoList,
|
||||
columns: rawSeweragePenetrationRateInfoList.columns.map(col => ({
|
||||
...col,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
};
|
||||
@@ -115,6 +115,7 @@ export const StatsInputStatusInfoList = {
|
||||
...StatsInputStatusInfoColumnRaw,
|
||||
columns: StatsInputStatusInfoColumnRaw.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import { ValueType } from "realgrid";
|
||||
/**
|
||||
* 하수도 통계 관리 > 01. 하수도 보급률
|
||||
* 단일 Tab
|
||||
*/
|
||||
export const SeweragePentrationRateTotColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, },
|
||||
|
||||
// 수계 및 권역
|
||||
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' }, editable: false, },
|
||||
|
||||
// 수계영향권별분류
|
||||
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, },
|
||||
{ name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' }, editable: false, },
|
||||
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, },
|
||||
|
||||
// 특별대책지역
|
||||
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' }, editable: false, },
|
||||
|
||||
// 총인구
|
||||
{
|
||||
name: 'wholPpltnCnt',
|
||||
fieldName: 'wholPpltnCnt',
|
||||
header: { text: '총인구(명)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
|
||||
// 총면적
|
||||
{
|
||||
name: 'wholArea',
|
||||
fieldName: 'wholArea',
|
||||
header: { text: '총면적(㎢)' },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.000',
|
||||
editable: false,
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 합계
|
||||
{
|
||||
name: 'wholSwtrmtPpltnCnt',
|
||||
fieldName: 'wholSwtrmtPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 공공하수처리인구
|
||||
{
|
||||
name: 'wholPblSwtrmtPpltnCnt',
|
||||
fieldName: 'wholPblSwtrmtPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 폐수처리인구
|
||||
{
|
||||
name: 'wholWwtPrcsPpltnCnt',
|
||||
fieldName: 'wholWwtPrcsPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 미접속인구
|
||||
{ name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 면적
|
||||
{ name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 하수처리구역 외
|
||||
{
|
||||
name: 'wholStaaExcpPpltnCnt',
|
||||
fieldName: 'wholStaaExcpPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'staaPsplyRt',
|
||||
fieldName: 'staaPsplyRt',
|
||||
header: { text: '공공하수처리구역인구보급률(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'altdPrcsPsplyRt',
|
||||
fieldName: 'altdPrcsPsplyRt',
|
||||
header: { text: '고도처리인구보급률(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 하수도 설치율
|
||||
{
|
||||
name: 'sewerInstlRt',
|
||||
fieldName: 'sewerInstlRt',
|
||||
header: { text: '하수도설치율(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 비고
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false },
|
||||
].map((col) => ({ ...col, editable: false })),
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
],
|
||||
},
|
||||
'rvbsn',
|
||||
'trbtrNm',
|
||||
{
|
||||
name: 'groupB',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '수계영향권별분류' },
|
||||
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
|
||||
},
|
||||
'spcCtpnRgnCn',
|
||||
'wholPpltnCnt',
|
||||
'wholArea',
|
||||
{
|
||||
name: 'groupD',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역내' },
|
||||
items: [
|
||||
'wholSwtrmtPpltnCnt',
|
||||
{
|
||||
name: 'pblswrgGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '공공하수처리인구(명)' },
|
||||
items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'],
|
||||
},
|
||||
{
|
||||
name: 'wstrGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '폐수처리인구(명)' },
|
||||
items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'],
|
||||
},
|
||||
'yetCntnPpltnCnt',
|
||||
'staaArea'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupE',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역외' },
|
||||
items: [
|
||||
'wholStaaExcpPpltnCnt',
|
||||
'unprcsPpltnCnt',
|
||||
'drtwPrcsPpltnCnt',
|
||||
'spctnkPpltnCnt',
|
||||
'staaExcpArea'
|
||||
],
|
||||
},
|
||||
'staaPsplyRt',
|
||||
'altdPrcsPsplyRt',
|
||||
'sewerInstlRt',
|
||||
'rmrk',
|
||||
'swagePplPsplyRt',
|
||||
'admdstCd',
|
||||
]
|
||||
};
|
||||
@@ -181,8 +181,8 @@ const createSumFooterArrayByDivision = (format: string, divisionFieldName: strin
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: unknown,
|
||||
value: number | string | undefined
|
||||
// columnFooter: unknown,
|
||||
// value: number | string | undefined
|
||||
) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName),
|
||||
},
|
||||
{
|
||||
@@ -191,8 +191,8 @@ const createSumFooterArrayByDivision = (format: string, divisionFieldName: strin
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: unknown,
|
||||
value: number | string | undefined
|
||||
// columnFooter: unknown,
|
||||
// value: number | string | undefined
|
||||
) => getFooterSummaryByDivision(grid, column, footerIndex, divisionFieldName),
|
||||
},
|
||||
];
|
||||
@@ -205,8 +205,8 @@ const createAvgFooterArrayByDivision = (format: string, divisionFieldName: strin
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: unknown,
|
||||
value: number | string | undefined
|
||||
// columnFooter: unknown,
|
||||
// value: number | string | undefined
|
||||
) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName),
|
||||
},
|
||||
{
|
||||
@@ -215,8 +215,8 @@ const createAvgFooterArrayByDivision = (format: string, divisionFieldName: strin
|
||||
grid: GridView,
|
||||
column: DataColumn,
|
||||
footerIndex: number,
|
||||
columnFooter: unknown,
|
||||
value: number | string | undefined
|
||||
// columnFooter: unknown,
|
||||
// value: number | string | undefined
|
||||
) => getFooterAvgSummaryByDivision(grid, column, footerIndex, divisionFieldName),
|
||||
},
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user