This commit is contained in:
@@ -202,3 +202,5 @@ http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=atta
|
||||
- 쿠키명 : menu-code-key
|
||||
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
|
||||
|
||||
## FSD 관련
|
||||
https://fsd.how/kr/docs/get-started/overview/
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* 📄 Proxy Route
|
||||
* API 프록시 라우트
|
||||
*
|
||||
* - 서버 API로의 요청 프록시 처리
|
||||
* - 쿠키 기반 인증 토큰 및 리프레시 토큰 관리
|
||||
* - POST 및 PUT 요청 핸들링
|
||||
* - 사용자 인증 정보 검증 및 응답 처리
|
||||
* - 에러 핸들링 및 상태 코드 반환
|
||||
* - 사이트 ID 및 헤더 설정
|
||||
*/
|
||||
import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
@@ -113,12 +124,8 @@ export async function PUT(
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
// 개발에서는 현재 사이트 전용, 운영에서는 환경변수의 공용 도메인을 사용한다.
|
||||
return process.env.NODE_ENV === "production"
|
||||
? COOKIE_DOMAIN || undefined
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ExmnMapFilters, ExmnMapJobType } from "./exmn-map-types";
|
||||
import { EXMN_RESULT_PPL_LEGEND_COLOR } from "./exmn-map-types";
|
||||
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
||||
|
||||
type Props = {
|
||||
@@ -52,7 +53,8 @@ export default function ExmnLayerSettingPanel({
|
||||
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
|
||||
<span className={`flex items-center gap-1 ${labelClass}`}>
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${jobType === "PPL" ? "bg-[#facc15]" : "bg-[#8b5cf6]"}`}
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: jobType === "PPL" ? EXMN_RESULT_PPL_LEGEND_COLOR : "#8b5cf6" }}
|
||||
/>
|
||||
사업차수
|
||||
</span>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const EXMN_RESULT_ROOT_LAYER_TYPE = "sn_exmn_result_group";
|
||||
export const EXMN_RESULT_PPL_LAYER_TYPE = "sn_exmn_result_ppl";
|
||||
export const EXMN_RESULT_MNHL_LAYER_TYPE = "sn_exmn_result_mnhl";
|
||||
// 레이어 트리에서 조사 관로와 기존 맨홀 분류 심볼을 즉시 구분하기 위한 전용 범례 스타일이다.
|
||||
export const EXMN_RESULT_PPL_LEGEND_COLOR = "#facc15";
|
||||
export const EXMN_RESULT_PPL_LEGEND_COLOR = "#000080";
|
||||
export const EXMN_RESULT_MNHL_LEGEND_ICON = "SN_EXMN_MANHOLE_PURPLE.png?v=3";
|
||||
|
||||
export type ExmnMapJobType = "PPL" | "MNHL";
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from "ol/style/Icon.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Text from "ol/style/Text.js";
|
||||
import { EXMN_RESULT_PPL_LEGEND_COLOR } from "../components/exmn/exmn-map-types";
|
||||
import {
|
||||
EXMN_GRADE_COLORS,
|
||||
EXMN_RESULT_MANHOLE_ICON_SRC,
|
||||
@@ -14,10 +15,11 @@ export const getExmnGradeColor = (feature: any) => {
|
||||
return EXMN_GRADE_COLORS[grade] ?? "#2563eb";
|
||||
};
|
||||
|
||||
export const createExmnPipeStyle = (feature: any) =>
|
||||
// 조사 관로는 상태등급과 관계없이 범례와 같은 짙은 남색으로 표시한다.
|
||||
export const createExmnPipeStyle = () =>
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: getExmnGradeColor(feature),
|
||||
color: EXMN_RESULT_PPL_LEGEND_COLOR,
|
||||
width: 4,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -122,12 +122,8 @@ export async function PUT(
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
}
|
||||
// 개발에서는 현재 사이트 전용, 운영에서는 환경변수의 공용 도메인을 사용한다.
|
||||
return process.env.NODE_ENV === "production"
|
||||
? COOKIE_DOMAIN || undefined
|
||||
: undefined;
|
||||
}
|
||||
|
||||
+4
-36
@@ -1,50 +1,18 @@
|
||||
/**
|
||||
* ➡️ 경로 : 루트 (/)
|
||||
*
|
||||
*
|
||||
* 📄 Home Page
|
||||
* 홈 페이지 컴포넌트
|
||||
*
|
||||
* - 쿠키 기반 인증 토큰 검증 및 자동 로그인 처리
|
||||
* - 로그인 성공 시 지도 페이지로 리다이렉트
|
||||
* - 개발 환경에서는 루트 진입 시 자동 로그인 후 지도 페이지 이동
|
||||
* - 로그인 실패 시 에러 로깅 및 처리
|
||||
* - 루트 진입 시 지도 페이지(/map)로 리다이렉트
|
||||
* - 인증 토큰 검증 및 로그인 화면 이동은 /map 및 공통 인증 흐름에서 처리
|
||||
* - 사용자 세션 관리 및 초기 라우팅 제어
|
||||
* - Next.js useRouter를 통한 클라이언트 사이드 라우팅
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 로그인: POST /api/v1/auth/login (user-service)
|
||||
*/
|
||||
"use client";
|
||||
import { authService } from "@/entities/auth";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const DEV_HOSTNAMES = new Set(["localhost", "127.0.0.1", "con.mind-one.co.kr"]);
|
||||
|
||||
const DEV_LOGIN_PAYLOAD = {
|
||||
userId: process.env.NEXT_PUBLIC_DEV_LOGIN_USER_ID || "CLEANWATER",
|
||||
password: process.env.NEXT_PUBLIC_DEV_LOGIN_PASSWORD || "gktneh12!@",
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
const isDevHost = DEV_HOSTNAMES.has(hostname);
|
||||
|
||||
if (!isDevHost) {
|
||||
window.location.replace("/map");
|
||||
return;
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
try {
|
||||
// 개발 환경에서는 남아 있는 만료/부분 쿠키로 인한 /map <-> / 루프를 막기 위해 항상 토큰을 재발급한다.
|
||||
await authService.login(DEV_LOGIN_PAYLOAD);
|
||||
window.location.replace("/map");
|
||||
} catch (err) {
|
||||
console.error("로그인 실패", err);
|
||||
}
|
||||
}
|
||||
|
||||
void doLogin();
|
||||
window.location.replace("/map");
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -719,3 +719,19 @@
|
||||
- 실브라우저: 테스트 사업→초이처리분구→지도3개 표시→실제188001 클릭→사업명 테스트/GIS 맨홀번호188001 확인→조사보고서 보기→1쪽 PDF와188001 표시 성공. 사용자도 최종 정상동작 확인했다.
|
||||
- 실행 서버: 기존 재시작 승인 범위에서 코드 반영 후8883 PID27788로 유지한다(이전PID24204 종료). classpath/실행로그는 기존 .codex-tmp/manhole-backend-* 경로 유지. 종료명령 Stop-Process -Id 27788.
|
||||
- 롤백: 위 FE3개 소스와 ExmnManholeReportService의 이번 변경을 함께 복원한다. DB 롤백 없음. 잔여 리스크는 GIS 미연결 자료 열람 불가 및 대형 사업의 지도 매칭·PDF 판독 비용이며, 임의 위치/다른 사업 fallback을 하지 않는다.
|
||||
|
||||
|
||||
## 2026-10-28 관로조사 남색 표시
|
||||
- 정적 확인: 관로 선·범례·설정 표식은 `EXMN_RESULT_PPL_LEGEND_COLOR(#000080)`을 공유하고 관로 선 두께는 4를 유지한다. 맨홀·선택 이벤트·API·DB 변경 없음.
|
||||
- `git -C frontend diff --check`: 통과.
|
||||
- 3002 리스너 부재로 HMR·실브라우저 검증은 미수행. 실제 지도에서 관로조사 켜기, 등급 필터 변경 후 남색 유지, 선택 강조·맨홀 동시 표시 구분을 후속 확인한다.
|
||||
|
||||
### 2026-10-28 개발 서버 기동 후 확인
|
||||
- 사용자 요청으로 `pnpm --filter sn dev` 실행, Next.js Ready 및 localhost:3002 접속 확인. 서버는 유지한다.
|
||||
- Chrome에서 /map 진입 시 /auth/login으로 이동하고 로그인 화면이 표시됨. 로컬 로그인 세션이 없어 실제 지도 선·범례 가시성 검증은 로그인 후 계속해야 한다. 인증 우회·환경설정 변경 없음.
|
||||
|
||||
### 2026-10-28 백엔드 복구 및 실지도 검증 완료
|
||||
- 원인: 프론트만 실행되어 localhost:8883 지도 API가 ECONNREFUSED로 실패했다. 기존 local 설정으로 Config Server와 sewerage-service를 기동했다. 코드·환경 파일·DB 변경 없음.
|
||||
- 지도 레이어 API HTTP 200. 실제 레이어 목록·조사사업 조회 정상.
|
||||
- 테스트 사업 → 관로조사 → 초이처리분구 → 지도보기: 3개 표시, 줌18.0에서 짙은 남색 관로가 일반지도 배경과 주황색 기존 관로에서 선명하게 구분됨. 이전 실지도 미검증 상태 해소. 위성지도·모든 등급·선택 강조 조합은 미검증.
|
||||
- 사용자 계속 실행 지시에 따라 FE3002 PID28176, BE8883 PID31232, Config8888 PID27092 유지. 종료 시 해당 PID가 같은 프로세스인지 재확인 후 Stop-Process -Id 명령으로 종료한다.
|
||||
@@ -38,7 +38,7 @@
|
||||
| 파일 | 역할 | 선언/주석 보조 근거 | 행 | 직접 참조 | 상태 |
|
||||
|---|---|---|---:|---:|---|
|
||||
| [layout.tsx](<D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/layout.tsx>) | Next 루트 HTML·테마·메타데이터·Provider 진입점 | metadata, RootLayout / ➡️ 경로 : 모든 페이지 | 44 | 0 | 등록/도구 진입 규칙 대상 |
|
||||
| [page.tsx](<D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/page.tsx>) | 루트 진입/개발 자동로그인·지도 이동 | DEV_HOSTNAMES, DEV_LOGIN_PAYLOAD, Home / 홈 페이지 컴포넌트 | 51 | 0 | 등록/도구 진입 규칙 대상 |
|
||||
| [page.tsx](<D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/page.tsx>) | 루트 진입/지도 이동 | Home / 홈 페이지 컴포넌트 | 18 | 0 | 등록/도구 진입 규칙 대상 |
|
||||
| [providers.tsx](<D:/workspace/IIPBS-FRONTEND/frontend/apps/sn/app/providers.tsx>) | 실제 src/app/providers의 AppProvider 재수출. 현재 layout에서 사용 | | 2 | 1 | 정적 참조 있음 |
|
||||
|
||||
## app/(auth)/auth
|
||||
|
||||
@@ -1045,3 +1045,12 @@
|
||||
- 실브라우저: 테스트 사업→초이처리분구→지도3개 표시→실제188001 클릭→사업명 테스트/GIS 맨홀번호188001 확인→조사보고서 보기→1쪽 PDF와188001 표시 성공. 사용자도 최종 정상동작 확인했다.
|
||||
- 실행 서버: 기존 재시작 승인 범위에서 코드 반영 후8883 PID27788로 유지한다(이전PID24204 종료). classpath/실행로그는 기존 .codex-tmp/manhole-backend-* 경로 유지. 종료명령 Stop-Process -Id 27788.
|
||||
- 롤백: 위 FE3개 소스와 ExmnManholeReportService의 이번 변경을 함께 복원한다. DB 롤백 없음. 잔여 리스크는 GIS 미연결 자료 열람 불가 및 대형 사업의 지도 매칭·PDF 판독 비용이며, 임의 위치/다른 사업 fallback을 하지 않는다.
|
||||
|
||||
|
||||
## 2026-10-28 관로조사 지도 색상 가시성 개선
|
||||
- 사용자 요청에 따라 조사결과 > 관로조사의 지도 선을 상태등급별 색상에서 짙은 남색 `#000080`으로 통일했다. 선 두께 4와 등급 조회·필터는 유지한다.
|
||||
- `app/(map-layout)/map/lib/analysis-layer-styles.ts`, `components/exmn/exmn-map-types.ts`, `components/exmn/exmn-layer-setting-panel.tsx`에서 지도 선·레이어 범례·설정 표식이 같은 색상 상수를 사용하도록 했다(뒤 두 경로도 같은 map 하위).
|
||||
- 원본 근거: 요구사항정의서 `3. (기능)요구사항정의 및 추적표`의 SFR-010-004, 화면설계서 45쪽 SN_MN040103 및 48쪽 SN_MN040107의 조사관로 지도 표출. 색상 변경은 이번 사용자 요청을 적용했다.
|
||||
- 영향: SN 지도 표시 전용. 맨홀 심볼·선택 처리·상세 탭·API·DB 변경 없음.
|
||||
- 검증: git diff --check 통과, 지도 스타일과 범례·설정 표식의 공통 상수 참조 확인. 3002 개발 서버 리스너가 없어 HMR·실제 배경지도 가시성은 미검증. 빌드·서버 기동/재시작은 수행하지 않았다.
|
||||
- 롤백: 위 3개 파일의 이번 색상 변경만 되돌린다. DB 조치 없음.
|
||||
|
||||
@@ -20,3 +20,30 @@
|
||||
|
||||
## 수동 확인 필요
|
||||
- 실제 로그인 화면에서 팝업 크기·가로 스크롤·강조 색상 및 지도 이동 회귀 확인
|
||||
|
||||
## 추가 작업: 확률강우량 팝업 엑셀다운로드 (2026-10-30)
|
||||
|
||||
### 변경 내용
|
||||
- 팝업 헤더 우측(닫기 X 왼쪽)에 `엑셀다운로드` 버튼을 추가했다. 닫기 버튼과 겹치지 않도록 헤더에 `pr-14` 여백을 둔다.
|
||||
- 공통 유틸 `src/shared/util/download-excel-workbook.ts`의 `downloadExcelWorkbook`을 재사용한다.
|
||||
- 파일명은 `{시군구명}_확률강우량.xlsx`이며 시트는 두 개다.
|
||||
- `확률강우량`: 재현기간(년) 행 × `N시간` 열. 셀은 소수 4자리로 자르지 않은 원본 숫자 값이다.
|
||||
- `확률관측소 면적비율`: 확률관측소코드·관측소명·면적비율(%)과 합계(API `areaRtTotal`).
|
||||
- 로딩 중·오류·표 데이터 없음 상태에서는 버튼을 비활성화한다.
|
||||
- API 호출, 응답 구조, 화면 문구와 기존 표 렌더링은 변경하지 않았다.
|
||||
|
||||
### 이슈: 팝업 본체가 투명하게 렌더링됨
|
||||
- 증상: 엑셀다운로드 버튼 추가 후 팝업 본체 배경이 투명해져 뒤쪽 목록 화면이 비쳐 보였다. 새로고침과 애니메이션 완료 후에도 재현됐다.
|
||||
- 추정 원인: `DialogContent`의 배경이 `bg-[var(--dialog-bg)]`(`--dialog-bg: var(--background)`, `packages/common/assets/src/styles/base.css`)인데, 이 모달에서 해당 변수가 값을 가져오지 못한 것으로 추정한다. 정확한 근본 원인은 확인하지 못했다. 공통 패키지(`@common/components`, `@common/assets`)는 수정 범위 밖이라 변경하지 않았다.
|
||||
- 조치: `PrbbtRnflDialog.tsx`의 `DialogContent` className에 `bg-background`를 추가해 배경을 직접 지정했다. 사용자가 해결을 확인했다(2026-10-30).
|
||||
- 참고: 같은 증상이 다른 `DialogContent` 사용 팝업에서 보이면 공통 변수 문제일 수 있으므로 별도 보고 대상이다.
|
||||
|
||||
### 수정 파일
|
||||
- `apps/sn/src/features/fldng-predc-prst/ui/PrbbtRnflDialog.tsx`
|
||||
|
||||
### 검증
|
||||
- 사용자가 실제 화면에서 팝업 배경 정상 표시를 확인했다.
|
||||
- 타입 체크·빌드·엑셀 파일 내용 열람은 수행하지 않았다.
|
||||
|
||||
### 롤백
|
||||
- 위 파일의 변경(헤더 버튼, `handleExcelDownload`, 두 import, `bg-background`)을 되돌린다.
|
||||
|
||||
@@ -3,6 +3,10 @@ import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
export type FloodPredictionExcelRow = {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
/** `/prst` 조회 때 시군구-관측소 매핑에서 붙인 관측소명이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
|
||||
obsrvnBrnchNm: string | null;
|
||||
/** 시군구 면적 중 관측소 영역의 비율(%)이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
|
||||
areaRt: number | null;
|
||||
analysisDateTime: string;
|
||||
rainBeginningDateTime: string;
|
||||
observedAccumulatedRainfall: number | null;
|
||||
@@ -12,10 +16,13 @@ export type FloodPredictionExcelRow = {
|
||||
floodRiskGrade: string;
|
||||
};
|
||||
|
||||
/** Excel 행 객체 키를 한글 헤더로 바꾸는 매핑이다. 실제 열 순서는 행 객체의 키 순서를 따르므로 같은 순서로 둔다. */
|
||||
const HEADERS: Record<string, string> = {
|
||||
rowNo: "순번",
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
obsrvnBrnchNm: "관측소",
|
||||
areaRt: "면적비율(%)",
|
||||
analysisDateTime: "분석시각",
|
||||
rainBeginningDateTime: "강우시작시각",
|
||||
observedAccumulatedRainfall: "관측 누적강수량(mm)",
|
||||
@@ -39,12 +46,20 @@ export const downloadFloodPredictionStatusExcel = async (
|
||||
rowNo: index + 1,
|
||||
ctpvNm: row.ctpvNm || "-",
|
||||
sggNm: row.sggNm || "-",
|
||||
// 관측소·면적비율은 시군구 뒤에 두고, 기존 로직 행이나 매핑에 없는 조합은 `-`로 내보낸다.
|
||||
obsrvnBrnchNm: row.obsrvnBrnchNm || "-",
|
||||
// 면적비율은 목록과 같이 소수 둘째 자리까지 반올림한 숫자로 내보낸다.
|
||||
areaRt:
|
||||
row.areaRt == null || !Number.isFinite(Number(row.areaRt))
|
||||
? "-"
|
||||
: Number(Number(row.areaRt).toFixed(2)),
|
||||
analysisDateTime: formatDateTime(row.analysisDateTime),
|
||||
rainBeginningDateTime: formatDateTime(row.rainBeginningDateTime),
|
||||
observedAccumulatedRainfall:
|
||||
row.observedAccumulatedRainfall == null
|
||||
? "-"
|
||||
: Number(row.observedAccumulatedRainfall.toFixed(1)),
|
||||
// 관측소 기준 행의 예측시간 0(대표값이 분석시각)도 목록과 같이 `0시간 후`로 내보낸다.
|
||||
predictionHour:
|
||||
row.predictionHour == null ? "-" : `${row.predictionHour}시간 후`,
|
||||
predictedAccumulatedRainfall:
|
||||
@@ -60,7 +75,8 @@ export const downloadFloodPredictionStatusExcel = async (
|
||||
data: excelRows as unknown as JSON[],
|
||||
fileName: "침수예측현황.xlsx",
|
||||
sheetNameArray: ["침수예측현황"],
|
||||
headerRange: "A1:J1",
|
||||
// 순번~침수위험도 12개 열(관측소·면적비율 포함)의 헤더 행 범위다.
|
||||
headerRange: "A1:L1",
|
||||
headerMapping: HEADERS,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
||||
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
|
||||
|
||||
/** 시군구별 면적가중 확률강우량 한 셀의 API 응답이다. */
|
||||
import { downloadExcelWorkbook } from "@/shared/util/download-excel-workbook";
|
||||
|
||||
/**
|
||||
* 시군구별 면적가중 확률강우량 한 셀의 API 응답이다.
|
||||
* 관측소 기준표 응답에서는 최근접 확률관측소 1곳의 값을 면적가중 없이 그대로 담는다.
|
||||
*/
|
||||
export type PrbbtRnflCell = {
|
||||
frequencyYear: number;
|
||||
durationHour: number;
|
||||
@@ -18,18 +23,37 @@ export type PrbbtRnflStation = {
|
||||
areaRt: number;
|
||||
};
|
||||
|
||||
/** 확률강우량 표와 그 계산에 사용한 관측소 비율을 함께 받는다. */
|
||||
/**
|
||||
* 확률강우량 표와 그 계산에 사용한 관측소 비율을 함께 받는다.
|
||||
* 시군구 기준표(`/prst/prbbt-rnfl?sggCd=`)는 `sggCd`를, 관측소 기준표(`/prst/prbbt-rnfl/obsvtr?obsrvnBrnchCd=`)는
|
||||
* `sggCd` 대신 `obsrvnBrnchCd`·`obsrvnBrnchNm`을 담으므로 두 응답을 모두 받도록 식별 필드는 선택값으로 둔다.
|
||||
*/
|
||||
export type PrbbtRnflResponse = {
|
||||
sggCd: string;
|
||||
/** 시군구 기준표 응답의 시군구코드다. 관측소 기준표 응답에는 없다. */
|
||||
sggCd?: string;
|
||||
/** 관측소 기준표 응답의 관측지점코드다. 시군구 기준표 응답에는 없다. */
|
||||
obsrvnBrnchCd?: string;
|
||||
/** 관측소 기준표 응답의 관측소명이다. 시군구 기준표 응답에는 없다. */
|
||||
obsrvnBrnchNm?: string;
|
||||
rainfall: PrbbtRnflCell[];
|
||||
/** 시군구 기준은 면적가중에 쓴 확률관측소 전부, 관측소 기준은 최근접 확률관측소 1곳(비율 100)이다. */
|
||||
stations: PrbbtRnflStation[];
|
||||
areaRtTotal: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 기준표를 연 목록 행의 분석 기준이다.
|
||||
* `sgg`는 기존 시군구 면적가중 행, `obsvtr`는 관측지점코드가 있는 관측소 기준(새 로직) 행이다.
|
||||
*/
|
||||
export type PrbbtRnflTargetKind = "sgg" | "obsvtr";
|
||||
|
||||
type PrbbtRnflDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
sggName: string;
|
||||
/** 상단 제목·표 제목·Excel 파일명에 쓰는 대상 이름이다. 시군구 기준은 시군구명, 관측소 기준은 관측소명이다. */
|
||||
targetName: string;
|
||||
/** 대상 이름이 시군구인지 관측소인지 구분해 아래 표 제목과 안내 문구를 고른다. */
|
||||
targetKind: PrbbtRnflTargetKind;
|
||||
selectedDurationHour: number | null;
|
||||
selectedFrequencyYear: number | null;
|
||||
data: PrbbtRnflResponse | null;
|
||||
@@ -43,11 +67,27 @@ const formatRainfall = (value: number) =>
|
||||
const formatAreaRatio = (value: number) =>
|
||||
Number.isFinite(Number(value)) ? Number(value).toFixed(3) : "-";
|
||||
|
||||
/**
|
||||
* 아래 확률관측소 표의 제목(Excel 두 번째 시트명 겸용)이다.
|
||||
* 관측소 기준은 면적가중 없이 최근접 확률관측소 1곳을 쓰므로 `적용 확률관측소`로 보인다.
|
||||
*/
|
||||
const STATION_TABLE_TITLE: Record<PrbbtRnflTargetKind, string> = {
|
||||
sgg: "확률관측소 면적비율",
|
||||
obsvtr: "적용 확률관측소",
|
||||
};
|
||||
|
||||
/** 기준자료가 없을 때 보이는 안내 문구다. 관측소 기준은 시군구 대신 관측소로 안내한다. */
|
||||
const EMPTY_MESSAGE: Record<PrbbtRnflTargetKind, string> = {
|
||||
sgg: "해당 시군구의 확률강우량 기준자료가 없습니다.",
|
||||
obsvtr: "해당 관측소의 확률강우량 기준자료가 없습니다.",
|
||||
};
|
||||
|
||||
/** 재현기간을 행, 강우지속시간을 열로 구성하고 선택한 두 기준이 교차하는 값만 색으로 강조한다. */
|
||||
export default function PrbbtRnflDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
sggName,
|
||||
targetName,
|
||||
targetKind,
|
||||
selectedDurationHour,
|
||||
selectedFrequencyYear,
|
||||
data,
|
||||
@@ -58,6 +98,8 @@ export default function PrbbtRnflDialog({
|
||||
const tableScrollRef = useRef<HTMLDivElement>(null);
|
||||
/** 목록에서 클릭한 지속시간에 대응하는 표 헤더이다. */
|
||||
const selectedDurationHeaderRef = useRef<HTMLTableCellElement>(null);
|
||||
/** 행 종류에 따른 아래 표 제목이다. 시군구 기준 행은 기존 제목을 그대로 쓴다. */
|
||||
const stationTableTitle = STATION_TABLE_TITLE[targetKind];
|
||||
|
||||
const { frequencies, durations, rainfallByKey } = useMemo(() => {
|
||||
const rainfall = data?.rainfall ?? [];
|
||||
@@ -77,6 +119,46 @@ export default function PrbbtRnflDialog({
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
/**
|
||||
* 화면에 표시 중인 확률강우량 표와 관측소 면적비율 표를 두 시트로 내려받는다.
|
||||
* 파일명은 대상 이름(시군구명 또는 관측소명)을, 두 번째 시트명은 아래 표 제목을 따른다.
|
||||
*/
|
||||
const handleExcelDownload = async () => {
|
||||
const stations = data?.stations ?? [];
|
||||
await downloadExcelWorkbook({
|
||||
fileName: `${targetName}_확률강우량`,
|
||||
sheets: [
|
||||
{
|
||||
name: "확률강우량",
|
||||
rows: [
|
||||
["재현기간(년)", ...durations.map((hour) => `${hour}시간`)],
|
||||
...frequencies.map((frequencyYear) => [
|
||||
frequencyYear,
|
||||
...durations.map(
|
||||
(durationHour) =>
|
||||
rainfallByKey.get(`${frequencyYear}:${durationHour}`) ?? null,
|
||||
),
|
||||
]),
|
||||
],
|
||||
columnWidths: [14, ...durations.map(() => 12)],
|
||||
},
|
||||
{
|
||||
name: stationTableTitle,
|
||||
rows: [
|
||||
["확률관측소코드", "관측소명", "면적비율(%)"],
|
||||
...stations.map((station) => [
|
||||
station.prbbtObsvtrCd,
|
||||
station.prbbtObsvtrNm || "-",
|
||||
station.areaRt,
|
||||
]),
|
||||
["합계", "", data?.areaRtTotal ?? null],
|
||||
],
|
||||
columnWidths: [18, 18, 14],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
/** 표가 표시되면 선택 지속시간 열을 가로 스크롤 영역의 가운데로 맞춘다. */
|
||||
useLayoutEffect(() => {
|
||||
if (!open || loading || error || selectedDurationHour == null) return;
|
||||
@@ -99,8 +181,16 @@ export default function PrbbtRnflDialog({
|
||||
className="flex max-h-[90vh] w-[94vw] !max-w-[150rem] flex-col gap-4 overflow-hidden"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{sggName} 확률강우량</DialogTitle>
|
||||
{/* 우측 상단 닫기(X) 버튼과 겹치지 않도록 pr-14 여백을 둔다. */}
|
||||
<DialogHeader className="flex-row items-center justify-between pr-14">
|
||||
<DialogTitle>{targetName} 확률강우량</DialogTitle>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={loading || !!error || !frequencies.length || !durations.length}
|
||||
onClick={() => void handleExcelDownload()}
|
||||
>
|
||||
엑셀다운로드
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
@@ -113,14 +203,14 @@ export default function PrbbtRnflDialog({
|
||||
</p>
|
||||
) : !frequencies.length || !durations.length ? (
|
||||
<p className="py-8 text-center text-xl" role="status">
|
||||
해당 시군구의 확률강우량 기준자료가 없습니다.
|
||||
{EMPTY_MESSAGE[targetKind]}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div ref={tableScrollRef} className="min-h-0 overflow-auto rounded-md border border-border">
|
||||
<table className="w-max min-w-full border-collapse text-right text-lg">
|
||||
<caption className="sr-only">
|
||||
{sggName} 재현기간 및 강우지속시간별 확률강우량(mm)
|
||||
{targetName} 재현기간 및 강우지속시간별 확률강우량(mm)
|
||||
</caption>
|
||||
<thead className="sticky top-0 z-10 bg-muted text-foreground">
|
||||
<tr>
|
||||
@@ -182,7 +272,7 @@ export default function PrbbtRnflDialog({
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 space-y-2">
|
||||
<h3 className="text-xl font-semibold">확률관측소 면적비율</h3>
|
||||
<h3 className="text-xl font-semibold">{stationTableTitle}</h3>
|
||||
<div className="max-h-[16rem] overflow-auto rounded-md border border-border">
|
||||
<table className="w-full border-collapse text-lg">
|
||||
<thead className="bg-muted text-foreground">
|
||||
|
||||
@@ -31,9 +31,16 @@ type ApiRow = {
|
||||
ctpvCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
/** 관측소 기준(새 로직) 분석 행의 관측지점코드다. 기존 시군구 면적가중 행은 null이다. */
|
||||
obsrvnBrnchCd: string | null;
|
||||
/** `/prst` 조회 때 시군구-관측소 매핑에서 붙인 관측소명이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
|
||||
obsrvnBrnchNm: string | null;
|
||||
/** 시군구 면적 중 관측소 영역의 비율(%)이다. 기존 로직 행이나 매핑에 없는 조합은 null이다. */
|
||||
areaRt: number | null;
|
||||
rainBeginningDateTime: string;
|
||||
analysisDateTime: string;
|
||||
observedAccumulatedRainfall: number | null;
|
||||
/** 분석시각 기준 대표 예측시간이다. 관측소 기준 행의 0은 대표값이 분석시각(관측 누적)이라는 뜻이다. */
|
||||
predictionHour: number | null;
|
||||
predcRainHr: number | null;
|
||||
predictedAccumulatedRainfall: number | null;
|
||||
@@ -46,6 +53,10 @@ type TableRow = ApiRow &
|
||||
rowNo: number;
|
||||
analysisDateTimeDisplay: string;
|
||||
rainBeginningDateTimeDisplay: string;
|
||||
/** 관측소 열 표시값이다. 관측소명이 없으면 `-`이다. */
|
||||
obsrvnBrnchNmDisplay: string;
|
||||
/** 면적비율(%) 열 표시값이다. 소수 둘째 자리까지, 값이 없으면 `-`이다. */
|
||||
areaRtDisplay: string;
|
||||
observedAccumulatedRainfallDisplay: string;
|
||||
predictionHourDisplay: string;
|
||||
predictedAccumulatedRainfallDisplay: string;
|
||||
@@ -76,47 +87,61 @@ const toFloodMapFrequencyCode = (value: number) => {
|
||||
return String(normalized).padStart(3, "0");
|
||||
};
|
||||
|
||||
/**
|
||||
* 침수예측현황 목록 열 정의다.
|
||||
* 시군구 뒤에 관측소 기준 분석의 관측소·면적비율 열을 두고, 13개 열의 widthPercent 합은 100으로 맞춘다.
|
||||
*/
|
||||
const HEADS: UiHeadProps[] = [
|
||||
{ id: "rowNo", title: "순번", widthPercent: 5 },
|
||||
{ id: "rowNo", title: "순번", widthPercent: 4 },
|
||||
{ id: "ctpvNm", title: "시도", widthPercent: 7 },
|
||||
{ id: "sggNm", title: "시군구", widthPercent: 8 },
|
||||
{ id: "obsrvnBrnchNmDisplay", title: "관측소", widthPercent: 7 },
|
||||
{
|
||||
id: "areaRtDisplay",
|
||||
title: "면적비율(%)",
|
||||
widthPercent: 6,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "analysisDateTimeDisplay",
|
||||
title: "분석시각",
|
||||
widthPercent: 13,
|
||||
widthPercent: 11,
|
||||
},
|
||||
{
|
||||
id: "rainBeginningDateTimeDisplay",
|
||||
title: "강우시작시각",
|
||||
widthPercent: 13,
|
||||
widthPercent: 11,
|
||||
},
|
||||
{
|
||||
id: "observedAccumulatedRainfallDisplay",
|
||||
title: "관측 누적강수량(mm)",
|
||||
widthPercent: 11,
|
||||
widthPercent: 9,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "predictionHourDisplay",
|
||||
title: "예측시간",
|
||||
widthPercent: 7,
|
||||
widthPercent: 6,
|
||||
},
|
||||
{
|
||||
id: "predictedAccumulatedRainfallDisplay",
|
||||
title: "예측 누적강수량(mm)",
|
||||
widthPercent: 12,
|
||||
widthPercent: 9,
|
||||
align: "right",
|
||||
},
|
||||
{
|
||||
id: "exceededFrequencyYearDisplay",
|
||||
title: "초과 빈도(년)",
|
||||
widthPercent: 8,
|
||||
widthPercent: 7,
|
||||
align: "right",
|
||||
},
|
||||
{ id: "floodRiskGrade", title: "침수위험도", widthPercent: 8 },
|
||||
{ id: "floodRiskGrade", title: "침수위험도", widthPercent: 7 },
|
||||
{ id: "probabilityRainfallDisplay", title: "확률강우량", widthPercent: 8 },
|
||||
];
|
||||
|
||||
/** 관측소·면적비율 열이 더해진 목록의 최소 표 너비다. 기본 팝업 너비(1280px)에서는 가로 스크롤 없이 보이도록 잡는다. */
|
||||
const LIST_MIN_TABLE_WIDTH = "118rem";
|
||||
|
||||
/** 날짜·시각의 뒷부분이 잘리지 않도록 전체 표시할 목록 열이다. */
|
||||
const FULL_TEXT_TIME_HEAD_IDS = [
|
||||
"analysisDateTimeDisplay",
|
||||
@@ -157,6 +182,34 @@ const predictionHourText = (value: number | null) => {
|
||||
return hour > 0 ? `${hour}시간 후` : "-";
|
||||
};
|
||||
|
||||
/** 관측지점코드가 있으면 관측소 기준(새 로직) 분석 행이다. 기준표 API·표시 문구를 고를 때 쓴다. */
|
||||
const isObsvtrRow = (row: Pick<ApiRow, "obsrvnBrnchCd">) =>
|
||||
Boolean(row.obsrvnBrnchCd);
|
||||
|
||||
/**
|
||||
* 목록 예측시간 열의 표시문구를 정한다.
|
||||
* 관측소 기준 행의 예측시간 0은 대표값이 분석시각(관측 누적)이라는 뜻이므로 `0시간 후`로 보인다.
|
||||
* 그 밖의 행은 기존 `predictionHourText` 규칙(0 이하 `-`)을 그대로 따른다.
|
||||
*/
|
||||
const predictionHourDisplayText = (
|
||||
row: Pick<ApiRow, "predictionHour" | "obsrvnBrnchCd">,
|
||||
) =>
|
||||
isObsvtrRow(row) && row.predictionHour === 0
|
||||
? "0시간 후"
|
||||
: predictionHourText(row.predictionHour);
|
||||
|
||||
/**
|
||||
* 확률강우량 기준표 버튼 aria-label과 모달 제목에 쓸 대상 이름이다.
|
||||
* 관측소 기준 행은 관측소명(목록 → 기준표 응답 → 관측지점코드 순으로 대체), 기존 행은 시군구명을 쓴다.
|
||||
*/
|
||||
const probabilityTargetName = (
|
||||
row: Pick<ApiRow, "obsrvnBrnchCd" | "obsrvnBrnchNm" | "sggNm">,
|
||||
responseStationName?: string,
|
||||
) =>
|
||||
isObsvtrRow(row)
|
||||
? row.obsrvnBrnchNm || responseStationName || row.obsrvnBrnchCd || ""
|
||||
: row.sggNm;
|
||||
|
||||
/** 분석 결과의 대표 누적강우시간을 확률강우량 기준표 링크 문구로 표시한다. */
|
||||
const probabilityDurationText = (value: number | null) => {
|
||||
const hour = Number(value);
|
||||
@@ -291,11 +344,14 @@ export default function FldngPredcPrstPopupContent() {
|
||||
rainBeginningDateTimeDisplay: displayDate(
|
||||
item.rainBeginningDateTime,
|
||||
),
|
||||
// 기존 로직 행(관측지점코드 null)이나 매핑에 없는 조합은 관측소·면적비율을 `-`로 보인다.
|
||||
obsrvnBrnchNmDisplay: item.obsrvnBrnchNm || "-",
|
||||
areaRtDisplay: formatNumber(item.areaRt, 2),
|
||||
observedAccumulatedRainfallDisplay: formatNumber(
|
||||
item.observedAccumulatedRainfall,
|
||||
1,
|
||||
),
|
||||
predictionHourDisplay: predictionHourText(item.predictionHour),
|
||||
predictionHourDisplay: predictionHourDisplayText(item),
|
||||
predictedAccumulatedRainfallDisplay: formatNumber(
|
||||
item.predictedAccumulatedRainfall,
|
||||
1,
|
||||
@@ -417,9 +473,19 @@ export default function FldngPredcPrstPopupContent() {
|
||||
}
|
||||
};
|
||||
|
||||
/** 선택 시군구의 확률강우량과 관측소 비율을 조회한다. 지도 행 클릭은 버튼에서 차단한다. */
|
||||
/**
|
||||
* 선택 행의 확률강우량과 관측소 비율을 조회한다. 지도 행 클릭은 버튼에서 차단한다.
|
||||
* 관측지점코드가 있는 관측소 기준 행은 판정에 쓴 최근접 확률관측소 기준표(`/prst/prbbt-rnfl/obsvtr`)를,
|
||||
* 기존 시군구 기준 행은 지금처럼 시군구 면적가중 기준표(`/prst/prbbt-rnfl`)를 부른다.
|
||||
*/
|
||||
const handleProbabilityClick = async (row: TableRow) => {
|
||||
if (!row.sggCd || !Number.isInteger(Number(row.predcRainHr)) || Number(row.predcRainHr) <= 0) {
|
||||
// 관측소 기준 행이면 조회에 쓸 관측지점코드, 기존 행이면 null이다.
|
||||
const obsrvnBrnchCd = row.obsrvnBrnchCd || null;
|
||||
if (
|
||||
(!obsrvnBrnchCd && !row.sggCd) ||
|
||||
!Number.isInteger(Number(row.predcRainHr)) ||
|
||||
Number(row.predcRainHr) <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++probabilityRequestIdRef.current;
|
||||
@@ -428,16 +494,22 @@ export default function FldngPredcPrstPopupContent() {
|
||||
setProbabilityError(null);
|
||||
setProbabilityLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ sggCd: row.sggCd });
|
||||
const response = await getRawJson(
|
||||
`/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl?${params.toString()}`,
|
||||
);
|
||||
// 행 종류에 맞는 기준표 API 주소다. 기존 시군구 기준 행은 이전과 같은 sggCd 쿼리를 쓴다.
|
||||
const requestUrl = obsrvnBrnchCd
|
||||
? `/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl/obsvtr?${new URLSearchParams({ obsrvnBrnchCd }).toString()}`
|
||||
: `/sewerage-service/api/v1/fldng/predc/prst/prbbt-rnfl?${new URLSearchParams({ sggCd: row.sggCd }).toString()}`;
|
||||
const response = await getRawJson(requestUrl);
|
||||
if (requestId !== probabilityRequestIdRef.current) return;
|
||||
const payload: PrbbtRnflResponse = response?.data ?? response;
|
||||
setProbabilityData(payload);
|
||||
} catch (error) {
|
||||
if (requestId !== probabilityRequestIdRef.current) return;
|
||||
console.error("시군구 확률강우량 조회 중 오류:", error);
|
||||
console.error(
|
||||
obsrvnBrnchCd
|
||||
? "관측소 확률강우량 조회 중 오류:"
|
||||
: "시군구 확률강우량 조회 중 오류:",
|
||||
error,
|
||||
);
|
||||
setProbabilityError("확률강우량 기준표를 조회하지 못했습니다.");
|
||||
} finally {
|
||||
if (requestId === probabilityRequestIdRef.current) {
|
||||
@@ -479,6 +551,7 @@ export default function FldngPredcPrstPopupContent() {
|
||||
getItems: (response) => response.content,
|
||||
getTotal: (response) => response.totalElements,
|
||||
});
|
||||
// API 원본 행을 그대로 넘겨 관측지점코드·관측소명·면적비율도 Excel 헬퍼에서 함께 쓴다.
|
||||
await downloadFloodPredictionStatusExcel(result.items);
|
||||
} catch (error) {
|
||||
console.error("침수예측현황 Excel 다운로드 중 오류:", error);
|
||||
@@ -659,7 +732,7 @@ export default function FldngPredcPrstPopupContent() {
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-primary underline underline-offset-2 hover:opacity-75 focus-visible:rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
||||
aria-label={`${row.sggNm} ${row.probabilityRainfallDisplay} 확률강우량 기준표 보기`}
|
||||
aria-label={`${probabilityTargetName(row)} ${row.probabilityRainfallDisplay} 확률강우량 기준표 보기`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleProbabilityClick(row);
|
||||
@@ -671,7 +744,7 @@ export default function FldngPredcPrstPopupContent() {
|
||||
) : undefined
|
||||
}
|
||||
scrollMode="horizontal"
|
||||
minTableWidth="112rem"
|
||||
minTableWidth={LIST_MIN_TABLE_WIDTH}
|
||||
fullTextHeadIds={FULL_TEXT_TIME_HEAD_IDS}
|
||||
fillAvailableHeight={rows.length === 0}
|
||||
emptyMessage="조회 조건에 해당하는 침수예측현황이 없습니다."
|
||||
@@ -706,7 +779,17 @@ export default function FldngPredcPrstPopupContent() {
|
||||
<PrbbtRnflDialog
|
||||
open={probabilityRow !== null}
|
||||
onOpenChange={handleProbabilityOpenChange}
|
||||
sggName={probabilityRow?.sggNm ?? ""}
|
||||
targetName={
|
||||
probabilityRow
|
||||
? probabilityTargetName(
|
||||
probabilityRow,
|
||||
probabilityData?.obsrvnBrnchNm,
|
||||
)
|
||||
: ""
|
||||
}
|
||||
targetKind={
|
||||
probabilityRow && isObsvtrRow(probabilityRow) ? "obsvtr" : "sgg"
|
||||
}
|
||||
selectedDurationHour={probabilityRow?.predcRainHr ?? null}
|
||||
selectedFrequencyYear={probabilityRow?.exceededFrequencyYear ?? null}
|
||||
data={probabilityData}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
hasAdmdstCdColumn,
|
||||
isExcelImportAdmdstCdInvalid,
|
||||
mergeExcelImportWithExistingRows,
|
||||
parseExcelImportCellValue,
|
||||
prepareGridColumnsForExcelExport,
|
||||
releaseFixedOptionsForExcelExport,
|
||||
resolveColumnStyleName,
|
||||
@@ -1346,6 +1347,11 @@ useEffect(() => {
|
||||
columnLayout,
|
||||
{ skipNonEditable: false },
|
||||
);
|
||||
const valueTypeByFieldName = new Map(
|
||||
columns
|
||||
.filter((c) => c.fieldName)
|
||||
.map((c) => [c.fieldName as string, c.valueType]),
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -1367,36 +1373,13 @@ useEffect(() => {
|
||||
];
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
const allFieldName = excelColumnFieldNamesAll[colIndex];
|
||||
|
||||
let cellValue: string | number = "";
|
||||
if (
|
||||
cell &&
|
||||
cell.v !== undefined &&
|
||||
cell.v !== null
|
||||
) {
|
||||
// 엑셀 셀 타입이 숫자이거나 값이 숫자인 경우 숫자로 변환
|
||||
if (
|
||||
cell.t === "n" ||
|
||||
(typeof cell.v === "number" &&
|
||||
!isNaN(cell.v))
|
||||
) {
|
||||
cellValue = cell.v as number;
|
||||
} else {
|
||||
const stringValue = String(cell.v);
|
||||
// 문자열이지만 숫자로 변환 가능한 경우 숫자로 변환
|
||||
const numericValue = Number(
|
||||
stringValue.replace(/,/g, ""),
|
||||
);
|
||||
if (
|
||||
!isNaN(numericValue) &&
|
||||
stringValue.trim() !== ""
|
||||
) {
|
||||
cellValue = numericValue;
|
||||
} else {
|
||||
cellValue = stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const typeFieldName = fieldName || allFieldName;
|
||||
const cellValue = parseExcelImportCellValue(
|
||||
cell,
|
||||
typeFieldName
|
||||
? valueTypeByFieldName.get(typeFieldName)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (allFieldName === "admdstCd" && cellValue !== "") {
|
||||
rowAdmdstCd = cellValue;
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
hasAdmdstCdColumn,
|
||||
isExcelImportAdmdstCdInvalid,
|
||||
mergeExcelImportWithExistingRows,
|
||||
parseExcelImportCellValue,
|
||||
prepareGridColumnsForExcelExport,
|
||||
releaseFixedOptionsForExcelExport,
|
||||
resolveColumnStyleName,
|
||||
@@ -611,6 +612,11 @@ const TabDataGrid = ({
|
||||
columnLayout,
|
||||
{ skipNonEditable: false },
|
||||
);
|
||||
const valueTypeByFieldName = new Map(
|
||||
columns
|
||||
.filter((c) => c.fieldName)
|
||||
.map((c) => [c.fieldName as string, c.valueType]),
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -625,23 +631,13 @@ const TabDataGrid = ({
|
||||
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
const allFieldName = excelColumnFieldNamesAll[colIndex];
|
||||
|
||||
let cellValue: string | number = '';
|
||||
if (cell && cell.v !== undefined && cell.v !== null) {
|
||||
// 엑셀 셀 타입이 숫자이거나 값이 숫자인 경우 숫자로 변환
|
||||
if (cell.t === 'n' || (typeof cell.v === 'number' && !isNaN(cell.v))) {
|
||||
cellValue = cell.v as number;
|
||||
} else {
|
||||
const stringValue = String(cell.v);
|
||||
// 문자열이지만 숫자로 변환 가능한 경우 숫자로 변환
|
||||
const numericValue = Number(stringValue.replace(/,/g, ''));
|
||||
if (!isNaN(numericValue) && stringValue.trim() !== '') {
|
||||
cellValue = numericValue;
|
||||
} else {
|
||||
cellValue = stringValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const typeFieldName = fieldName || allFieldName;
|
||||
const cellValue = parseExcelImportCellValue(
|
||||
cell,
|
||||
typeFieldName
|
||||
? valueTypeByFieldName.get(typeFieldName)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (allFieldName === 'admdstCd' && cellValue !== '') {
|
||||
rowAdmdstCd = cellValue;
|
||||
|
||||
@@ -620,7 +620,6 @@ function buildExcretaSlurgeProcessingStatusSaveItem(
|
||||
rcycQnt: toGridNumber(row.rcycQnt),
|
||||
olandLfvlm: toGridNumber(row.olandLfvlm),
|
||||
incnrQnt: toGridNumber(row.incnrQnt),
|
||||
oceanInputQnt: toGridNumber(row.oceanInputQnt),
|
||||
fuelQnt: toGridNumber(row.fuelQnt),
|
||||
etcQnt: toGridNumber(row.etcQnt),
|
||||
unprcsQnt: toGridNumber(row.unprcsQnt),
|
||||
|
||||
@@ -20,8 +20,8 @@ const StatisticsReviewHistory = () => {
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false },
|
||||
{ name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false },
|
||||
{ name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false },
|
||||
{ name: "splmntOpnnCn", fieldName: "splmntOpnnCn", header: { text: "검토자의견" }, editable: false },
|
||||
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
@@ -29,8 +29,8 @@ const StatisticsReviewHistory = () => {
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"menuNm",
|
||||
"chckerOpinionSm",
|
||||
"wrterOpinionSm",
|
||||
"splmntOpnnCn",
|
||||
"rjctOpnnCn",
|
||||
];
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
|
||||
@@ -49,7 +49,6 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -116,7 +115,6 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
setReviewOpinionOpen(false);
|
||||
setReturnDialogOpen(false);
|
||||
@@ -170,14 +168,12 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
statsCd: statsInfo.statsCd ?? "",
|
||||
});
|
||||
setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(statsInfo.splmntRsltCn ?? "");
|
||||
setRowData(toGridRows(convertedData as unknown as object[]));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
@@ -216,7 +212,6 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.splmntRsltCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
@@ -234,10 +229,8 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.splmntRsltCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
@@ -297,7 +290,6 @@ const ExcretaTreatmentFacilityRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -102,7 +102,6 @@ const PrivateSewageFacilityRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -284,7 +283,6 @@ const PrivateSewageFacilityRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -307,7 +305,6 @@ const PrivateSewageFacilityRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -411,7 +408,6 @@ const PrivateSewageFacilityRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -41,7 +41,6 @@ const PublicSewageFacilityRvw = () => {
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -100,7 +99,6 @@ const PublicSewageFacilityRvw = () => {
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
setSubmitDialogOpen(false);
|
||||
};
|
||||
@@ -143,16 +141,12 @@ const PublicSewageFacilityRvw = () => {
|
||||
setLatestReviewOpinion(
|
||||
response.data.statsStatusInfo.splmntOpnnCn ?? "",
|
||||
);
|
||||
setLatestProcessHistory(
|
||||
response.data.statsStatusInfo.splmntRsltCn ?? "",
|
||||
);
|
||||
setRowData(toGridRows(convertedData as unknown as object[]));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -192,7 +186,6 @@ const PublicSewageFacilityRvw = () => {
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.splmntRsltCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
@@ -210,10 +203,8 @@ const PublicSewageFacilityRvw = () => {
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.splmntRsltCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
@@ -267,7 +258,6 @@ const PublicSewageFacilityRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -47,7 +47,6 @@ const PumpingStationRvw = () => {
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -111,7 +110,6 @@ const PumpingStationRvw = () => {
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
setReviewOpinionOpen(false);
|
||||
setReturnDialogOpen(false);
|
||||
@@ -157,14 +155,12 @@ const PumpingStationRvw = () => {
|
||||
statsCd: statsInfo.statsCd ?? "",
|
||||
});
|
||||
setLatestReviewOpinion(statsInfo.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(statsInfo.splmntRsltCn ?? "");
|
||||
setRowData(toGridRows(convertedData));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
@@ -203,7 +199,6 @@ const PumpingStationRvw = () => {
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.splmntRsltCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
@@ -221,10 +216,8 @@ const PumpingStationRvw = () => {
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.splmntRsltCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
@@ -284,7 +277,6 @@ const PumpingStationRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -30,8 +30,6 @@ export interface ReviewOpinionDialogProps {
|
||||
context: ReviewOpinionDialogContext | null;
|
||||
/** 다이얼로그 열릴 때 검토의견 초기값 */
|
||||
initialReviewOpinion?: string;
|
||||
/** 다이얼로그 열릴 때 처리내역 초기값 */
|
||||
initialProcessHistory?: string;
|
||||
/** 저장 시 생성된 param 전달 (API 호출 등) */
|
||||
onSave?: (param: ReviewOpinionSaveRequest) => void | Promise<void>;
|
||||
}
|
||||
@@ -41,18 +39,15 @@ export function ReviewOpinionDialog({
|
||||
onOpenChange,
|
||||
context,
|
||||
initialReviewOpinion = "",
|
||||
initialProcessHistory = "",
|
||||
onSave,
|
||||
}: ReviewOpinionDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [reviewOpinion, setReviewOpinion] = useState("");
|
||||
const [processHistory, setProcessHistory] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [reviewOpinionError, setReviewOpinionError] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setReviewOpinion("");
|
||||
setProcessHistory("");
|
||||
setReviewOpinionError(false);
|
||||
};
|
||||
|
||||
@@ -64,8 +59,7 @@ export function ReviewOpinionDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setReviewOpinion(initialReviewOpinion);
|
||||
setProcessHistory(initialProcessHistory);
|
||||
}, [open, initialReviewOpinion, initialProcessHistory]);
|
||||
}, [open, initialReviewOpinion]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (reviewOpinion.trim() === "") {
|
||||
@@ -96,7 +90,6 @@ export function ReviewOpinionDialog({
|
||||
sewerBzmnCd: context.sewerBzmnCd,
|
||||
statsCd: context.statsCd,
|
||||
splmntOpnnCn: reviewOpinion,
|
||||
splmntRsltCn: processHistory,
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
};
|
||||
await onSave?.(param);
|
||||
@@ -136,16 +129,6 @@ export function ReviewOpinionDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label mb-2">처리내역</Label>
|
||||
<Textarea
|
||||
value={processHistory}
|
||||
onChange={(e) => setProcessHistory(e.target.value)}
|
||||
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
|
||||
placeholder="처리내역을 입력하세요."
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
|
||||
@@ -30,8 +30,8 @@ export interface ReviewSubmitDialogProps {
|
||||
context: ReviewerInfoRequest | null;
|
||||
onSubmitted?: () => void | Promise<void>;
|
||||
/**
|
||||
* true면 검토의견·처리내역 UI를 숨기고
|
||||
* submit body에 statsCd / splmntOpnnCn / splmntRsltCn을 넣지 않음
|
||||
* true면 검토의견 UI를 숨기고
|
||||
* submit body에 statsCd / splmntOpnnCn을 넣지 않음
|
||||
*/
|
||||
hideOpinionFields?: boolean;
|
||||
}
|
||||
@@ -116,7 +116,6 @@ export function ReviewSubmitDialog({
|
||||
statsCd: context.statsCd,
|
||||
mdfrUuid: userInfo.userUnqId,
|
||||
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: reviewerInfo?.splmntRsltCn ?? "",
|
||||
};
|
||||
|
||||
const response = await statisticsApi.submitReview(submitParam);
|
||||
@@ -175,14 +174,6 @@ export function ReviewSubmitDialog({
|
||||
className="min-h-[10rem] p-2 resize-none !text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-2">
|
||||
<Label className="text-label">처리내역</Label>
|
||||
<Textarea
|
||||
value={reviewerInfo?.splmntRsltCn ?? "-"}
|
||||
readOnly
|
||||
className="min-h-[10rem] p-2 resize-none !text-xl"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,6 @@ const SewageExcretaRelatedRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -289,7 +288,6 @@ const SewageExcretaRelatedRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -312,7 +310,6 @@ const SewageExcretaRelatedRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -416,7 +413,6 @@ const SewageExcretaRelatedRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -105,7 +105,6 @@ const SewageExcretaSlurgeRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -290,7 +289,6 @@ const SewageExcretaSlurgeRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -313,7 +311,6 @@ const SewageExcretaSlurgeRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -417,7 +414,6 @@ const SewageExcretaSlurgeRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -120,7 +120,6 @@ const SewagePipeMaintainRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -302,7 +301,6 @@ const SewagePipeMaintainRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -325,7 +323,6 @@ const SewagePipeMaintainRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -429,7 +426,6 @@ const SewagePipeMaintainRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -181,7 +181,6 @@ const SewagePipeRvw = () => {
|
||||
};
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
|
||||
@@ -362,7 +361,6 @@ const SewagePipeRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -385,7 +383,6 @@ const SewagePipeRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -488,7 +485,6 @@ const SewagePipeRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -126,7 +126,6 @@ const SewageReservoirPumpRvw = () => {
|
||||
}, [activeStatsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -308,7 +307,6 @@ const SewageReservoirPumpRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -331,7 +329,6 @@ const SewageReservoirPumpRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -435,7 +432,6 @@ const SewageReservoirPumpRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -105,7 +105,6 @@ const SewerageFinanceRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -287,7 +286,6 @@ const SewerageFinanceRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -310,7 +308,6 @@ const SewerageFinanceRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -414,7 +411,6 @@ const SewerageFinanceRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -40,7 +40,6 @@ const SeweragePenetrationRateRvw = () => {
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -100,7 +99,6 @@ const SeweragePenetrationRateRvw = () => {
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
};
|
||||
|
||||
@@ -139,15 +137,11 @@ const SeweragePenetrationRateRvw = () => {
|
||||
setLatestReviewOpinion(
|
||||
response.data.statsStatusInfo.splmntOpnnCn ?? "",
|
||||
);
|
||||
setLatestProcessHistory(
|
||||
response.data.statsStatusInfo.splmntRsltCn ?? "",
|
||||
);
|
||||
setRowData(toGridRows(convertedData));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -187,7 +181,6 @@ const SeweragePenetrationRateRvw = () => {
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.splmntRsltCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
@@ -205,10 +198,8 @@ const SeweragePenetrationRateRvw = () => {
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.splmntRsltCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
@@ -264,7 +255,6 @@ const SeweragePenetrationRateRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -103,7 +103,6 @@ const SewerageServiceLevelRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -288,7 +287,6 @@ const SewerageServiceLevelRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -311,7 +309,6 @@ const SewerageServiceLevelRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -415,7 +412,6 @@ const SewerageServiceLevelRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
@@ -39,22 +39,14 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
|
||||
|
||||
const checkPrvnStatusCd = (code: string) => {
|
||||
return (code === '0' || code === '1') ? '미작성' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '반려'));
|
||||
};
|
||||
|
||||
const checkSubmitStatusCd = (code: string) => {
|
||||
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
|
||||
};
|
||||
|
||||
const isStatsInputStatusRowApprovable = (row: Record<string, string | number>) =>
|
||||
String(row.submitStatusCd ?? "") === "3" && (String(row.prvnStatusCd ?? "") === "2" || String(row.prvnStatusCd ?? "") === "3");
|
||||
String(row.lclgvSbmsnSttsCd ?? "") === "3" &&
|
||||
(String(row.mctpvPicSbmsnSttsCd ?? "") === "2" ||
|
||||
String(row.mctpvPicSbmsnSttsCd ?? "") === "3");
|
||||
|
||||
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
||||
rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || "0"),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || "0"),
|
||||
reviewHistory: "검토이력",
|
||||
}));
|
||||
|
||||
@@ -97,7 +89,7 @@ const StatsInputStatusRvw = () => {
|
||||
const reviewHistoryColumns = useMemo(() => {
|
||||
const col = createColumnHelper<ReviewHistoryRow>();
|
||||
return [
|
||||
col.accessor("logDt", {
|
||||
col.accessor("regDt", {
|
||||
header: "등록일",
|
||||
cell: (info) => info.getValue(),
|
||||
}),
|
||||
@@ -238,23 +230,23 @@ const StatsInputStatusRvw = () => {
|
||||
const allSubmitStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.submitStatusCd ?? "") === "3"),
|
||||
rowData.every((row) => String(row.lclgvSbmsnSttsCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
/** 진행상태(prvn) 코드가 모두 승인완료(3) */
|
||||
/** 진행상태(광역) 코드가 모두 승인완료(3) */
|
||||
const allPrvnStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.prvnStatusCd ?? "") === "3"),
|
||||
rowData.every((row) => String(row.mctpvPicSbmsnSttsCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
/** 진행상태(prvn) 코드가 모두 제출완료(2) */
|
||||
/** 진행상태(광역) 코드가 모두 제출완료(2) */
|
||||
const allPrvnStatusSubmitted = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.prvnStatusCd ?? "") === "2"),
|
||||
rowData.every((row) => String(row.mctpvPicSbmsnSttsCd ?? "") === "2"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
@@ -392,8 +384,8 @@ const StatsInputStatusRvw = () => {
|
||||
setRowData={setRowData}
|
||||
onDataCellClick={handleReviewHistoryCellClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName === "submitStatusNm") {
|
||||
const statusCd = String(row.submitStatusCd ?? "");
|
||||
if (fieldName === "lclgvSbmsnSttsNm") {
|
||||
const statusCd = String(row.lclgvSbmsnSttsCd ?? "");
|
||||
|
||||
if (statusCd === "0" || statusCd === "1") {
|
||||
return "rg-cell-stats-status-pending";
|
||||
@@ -408,8 +400,8 @@ const StatsInputStatusRvw = () => {
|
||||
return "rg-cell-stats-status-rejected";
|
||||
}
|
||||
return undefined;
|
||||
} else if (fieldName === "prvnStatusNm") {
|
||||
const statusCd = String(row.prvnStatusCd ?? "");
|
||||
} else if (fieldName === "mctpvPicSbmsnSttsNm") {
|
||||
const statusCd = String(row.mctpvPicSbmsnSttsCd ?? "");
|
||||
|
||||
if (statusCd === "0" || statusCd === "1") {
|
||||
return "rg-cell-stats-status-pending";
|
||||
|
||||
@@ -108,7 +108,6 @@ const WaterReuseRvw = () => {
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.splmntRsltCn ?? "";
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
@@ -291,7 +290,6 @@ const WaterReuseRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
splmntRsltCn: row?.splmntRsltCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -314,7 +312,6 @@ const WaterReuseRvw = () => {
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
splmntRsltCn: param.splmntRsltCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
@@ -418,7 +415,6 @@ const WaterReuseRvw = () => {
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
|
||||
-4
@@ -27,7 +27,6 @@ export const ExcretaSlurgeProcessingStatusInpColumn = {
|
||||
'rcycQnt',
|
||||
'olandLfvlm',
|
||||
'incnrQnt',
|
||||
'oceanInputQnt',
|
||||
'fuelQnt',
|
||||
'etcQnt',
|
||||
'unprcsQnt'
|
||||
@@ -51,7 +50,6 @@ export const ExcretaSlurgeProcessingStatusInpColumn = {
|
||||
'rcycQnt',
|
||||
'olandLfvlm',
|
||||
'incnrQnt',
|
||||
'oceanInputQnt',
|
||||
'fuelQnt',
|
||||
'etcQnt',
|
||||
],
|
||||
@@ -61,7 +59,6 @@ export const ExcretaSlurgeProcessingStatusInpColumn = {
|
||||
{ name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'olandLfvlm', fieldName: 'olandLfvlm', header: { text: '육상매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'incnrQnt', fieldName: 'incnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oceanInputQnt', fieldName: 'oceanInputQnt', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'fuelQnt', fieldName: 'fuelQnt', header: { text: '연료화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
@@ -100,7 +97,6 @@ export const ExcretaSlurgeProcessingStatusInpColumn = {
|
||||
'rcycQnt',
|
||||
'olandLfvlm',
|
||||
'incnrQnt',
|
||||
'oceanInputQnt',
|
||||
'fuelQnt',
|
||||
'etcQnt'
|
||||
]
|
||||
|
||||
+2
@@ -75,6 +75,8 @@ export const PublicSewageTreatWaterReuseInpColumn = {
|
||||
name: 'lglYn',
|
||||
fieldName: 'lglYn',
|
||||
header: { text: '법적의무대상(대상/미대상)' },
|
||||
editable: false,
|
||||
styleName: 'rg-cell-readonly'
|
||||
},
|
||||
PUBLIC_SEWAGE_TREAT_WATER_REUSE_LGL_YN_VALUES,
|
||||
),
|
||||
|
||||
@@ -32,11 +32,11 @@ export const getGeneralVerificationColumns = (
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfcClsfNm",
|
||||
header: "검증분류",
|
||||
header: "검증 분류",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
header: "사용 여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
|
||||
@@ -346,7 +346,6 @@ const rawSludgeList = [
|
||||
decimal1Col("rcycQnt", "재활용"),
|
||||
decimal1Col("olandLfvlm", "육상매립"),
|
||||
decimal1Col("incnrQnt", "소각"),
|
||||
decimal1Col("oceanInputQnt", "해양투기"),
|
||||
decimal1Col("fuelQnt", "연료화"),
|
||||
decimal1Col("etcQnt", "기타"),
|
||||
decimal1Col("unprcsQnt", "미처분량\n(이월량)(C)"),
|
||||
@@ -366,7 +365,6 @@ const rawSludgeList = [
|
||||
"rcycQnt",
|
||||
"olandLfvlm",
|
||||
"incnrQnt",
|
||||
"oceanInputQnt",
|
||||
"fuelQnt",
|
||||
"etcQnt",
|
||||
],
|
||||
|
||||
@@ -17,12 +17,12 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
styleName: 'rg-cell-outline-action-btn',
|
||||
},
|
||||
|
||||
{ name: 'chargerNm', fieldName: 'chargerNm', header: { text: '제출자' } },
|
||||
{ name: 'chargerCttpc', fieldName: 'chargerCttpc', header: { text: '연락처' } },
|
||||
{ name: 'submitStatusNm', fieldName: 'submitStatusNm', header: { text: '지자체진행상태' } },
|
||||
{ name: 'submitStatusCd', fieldName: 'submitStatusCd', header: { text: '지자체진행상태코드' }, visible: false },
|
||||
{ name: 'prvnStatusNm', fieldName: 'prvnStatusNm', header: { text: '진행상태' } },
|
||||
{ name: 'prvnStatusCd', fieldName: 'prvnStatusCd', header: { text: '진행상태코드' }, visible: false },
|
||||
{ name: 'lclgvPicNm', fieldName: 'lclgvPicNm', header: { text: '제출자' } },
|
||||
{ name: 'lclgvPicTelnoCn', fieldName: 'lclgvPicTelnoCn', header: { text: '연락처' } },
|
||||
{ name: 'lclgvSbmsnSttsNm', fieldName: 'lclgvSbmsnSttsNm', header: { text: '지자체진행상태' } },
|
||||
{ name: 'lclgvSbmsnSttsCd', fieldName: 'lclgvSbmsnSttsCd', header: { text: '지자체진행상태코드' }, visible: false },
|
||||
{ name: 'mctpvPicSbmsnSttsNm', fieldName: 'mctpvPicSbmsnSttsNm', header: { text: '진행상태' } },
|
||||
{ name: 'mctpvPicSbmsnSttsCd', fieldName: 'mctpvPicSbmsnSttsCd', header: { text: '진행상태코드' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -30,14 +30,14 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
'sggNm',
|
||||
'tabNm',
|
||||
'reviewHistory',
|
||||
'chargerNm',
|
||||
'chargerCttpc',
|
||||
'submitStatusNm',
|
||||
'prvnStatusNm',
|
||||
'lclgvPicNm',
|
||||
'lclgvPicTelnoCn',
|
||||
'lclgvSbmsnSttsNm',
|
||||
'mctpvPicSbmsnSttsNm',
|
||||
'statsCd',
|
||||
'sewerBzmnCd',
|
||||
'submitStatusCd',
|
||||
'prvnStatusCd'
|
||||
'lclgvSbmsnSttsCd',
|
||||
'mctpvPicSbmsnSttsCd',
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@ export const HumanWasteSludgeTab4ColumnConfig = {
|
||||
{ name: "rcycQnt", fieldName: "rcycQnt", header: { text: "재활용" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "olandLfvlm", fieldName: "olandLfvlm", header: { text: "육상매립" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "incnrQnt", fieldName: "incnrQnt", header: { text: "소각" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "oceanInputQnt", fieldName: "oceanInputQnt", header: { text: "해양투기" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "fuelQnt", fieldName: "fuelQnt", header: { text: "연료화" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "etcQnt", fieldName: "etcQnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "unprcsQnt", fieldName: "unprcsQnt", header: { text: "미처분량\n(이월량)(C)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
@@ -29,7 +28,7 @@ export const HumanWasteSludgeTab4ColumnConfig = {
|
||||
{
|
||||
name: "dspsGroup",
|
||||
header: { text: "처분량" },
|
||||
items: ["wholDspsQnt", "rcycQnt", "olandLfvlm", "incnrQnt", "oceanInputQnt", "fuelQnt", "etcQnt"]
|
||||
items: ["wholDspsQnt", "rcycQnt", "olandLfvlm", "incnrQnt", "fuelQnt", "etcQnt"]
|
||||
},
|
||||
"unprcsQnt",
|
||||
"swtrmtFcltNm"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GridView } from "realgrid";
|
||||
import { type GridView, ValueType } from "realgrid";
|
||||
|
||||
import type {
|
||||
ColumnConfig,
|
||||
@@ -124,6 +124,82 @@ export function getNonEditableColumnFieldNames(
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
type ExcelImportCell = {
|
||||
t?: string;
|
||||
v?: unknown;
|
||||
w?: string;
|
||||
};
|
||||
|
||||
/** 엑셀 셀 → 그리드 값. NUMBER 컬럼만 숫자화하고, 코드(01)는 문자열로 유지 */
|
||||
export function parseExcelImportCellValue(
|
||||
cell: ExcelImportCell | undefined,
|
||||
valueType?: ColumnConfig["valueType"],
|
||||
): string | number {
|
||||
if (!cell || cell.v === undefined || cell.v === null || cell.v === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (valueType === ValueType.NUMBER) {
|
||||
if (typeof cell.v === "number" && !Number.isNaN(cell.v)) {
|
||||
return cell.v;
|
||||
}
|
||||
const stringValue = String(cell.v);
|
||||
const numericValue = Number(stringValue.replace(/,/g, ""));
|
||||
if (!Number.isNaN(numericValue) && stringValue.trim() !== "") {
|
||||
return numericValue;
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
const formatted = typeof cell.w === "string" ? cell.w.trim() : "";
|
||||
if (/^0\d+$/.test(formatted)) {
|
||||
return formatted;
|
||||
}
|
||||
return String(cell.v);
|
||||
}
|
||||
|
||||
function collectZeroPaddedCodeWidths(
|
||||
rows: Record<string, string | number>[],
|
||||
): Map<string, number> {
|
||||
const widthByField = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
for (const [field, value] of Object.entries(row)) {
|
||||
const s = String(value ?? "").trim();
|
||||
if (!/^0\d+$/.test(s)) continue;
|
||||
widthByField.set(field, Math.max(widthByField.get(field) ?? 0, s.length));
|
||||
}
|
||||
}
|
||||
return widthByField;
|
||||
}
|
||||
|
||||
function restoreImportedCodeLeadingZeros(
|
||||
importedRow: Record<string, string | number>,
|
||||
existingRow: Record<string, string | number> | undefined,
|
||||
codeWidthByField: Map<string, number>,
|
||||
): Record<string, string | number> {
|
||||
const next: Record<string, string | number> = { ...importedRow };
|
||||
for (const [field, value] of Object.entries(next)) {
|
||||
if (value === "" || value == null) continue;
|
||||
if (typeof value === "number" && !Number.isInteger(value)) continue;
|
||||
const importedStr = String(value).trim();
|
||||
if (!/^\d+$/.test(importedStr)) continue;
|
||||
|
||||
const existingStr =
|
||||
existingRow != null ? String(existingRow[field] ?? "").trim() : "";
|
||||
let width = codeWidthByField.get(field);
|
||||
if (!width && /^0\d+$/.test(existingStr)) {
|
||||
width = existingStr.length;
|
||||
}
|
||||
if (!width && field.endsWith("Cd") && importedStr.length <= 2) {
|
||||
width = 2;
|
||||
}
|
||||
if (width && importedStr.length < width) {
|
||||
next[field] = importedStr.padStart(width, "0");
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 엑셀 import 결과를 기존 행과 병합 — preserveFieldNames는 기존 그리드 값 유지 */
|
||||
export function mergeExcelImportWithExistingRows(
|
||||
existingRows: Record<string, string | number>[],
|
||||
@@ -134,14 +210,20 @@ export function mergeExcelImportWithExistingRows(
|
||||
options?.preserveFieldNames && options.preserveFieldNames.length > 0
|
||||
? new Set(options.preserveFieldNames)
|
||||
: null;
|
||||
const codeWidthByField = collectZeroPaddedCodeWidths(existingRows);
|
||||
|
||||
return importedRows.map((importedRow, index) => {
|
||||
const existingRow = existingRows[index];
|
||||
if (!existingRow) return { ...importedRow };
|
||||
const normalizedImported = restoreImportedCodeLeadingZeros(
|
||||
importedRow,
|
||||
existingRow,
|
||||
codeWidthByField,
|
||||
);
|
||||
if (!existingRow) return normalizedImported;
|
||||
|
||||
const merged: Record<string, string | number> = {
|
||||
...existingRow,
|
||||
...importedRow,
|
||||
...normalizedImported,
|
||||
};
|
||||
if (preserveSet) {
|
||||
for (const fieldName of preserveSet) {
|
||||
|
||||
-1
@@ -437,7 +437,6 @@ export interface ExcretaSlurgeProcessingStatusItem {
|
||||
rcycQnt: number;
|
||||
olandLfvlm: number;
|
||||
incnrQnt: number;
|
||||
oceanInputQnt: number;
|
||||
fuelQnt: number;
|
||||
etcQnt: number;
|
||||
unprcsQnt: number;
|
||||
|
||||
@@ -81,7 +81,6 @@ export interface ReportStatusResponseDto {
|
||||
lastCfmtnPicTelnoCn?: string | null;
|
||||
lastCfmtnPicDeptNm?: string | null;
|
||||
lastCfmtnPicJbgdNm?: string | null;
|
||||
splmntRsltCn?: string | null;
|
||||
splmntOpnnCn?: string | null;
|
||||
rjctOpnnCn?: string | null;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ export interface StatsInputStatusReviewItem {
|
||||
sewerBzmnCd?: string;
|
||||
tabNm?: string;
|
||||
statsCd?: string;
|
||||
submitStatusNm?: string;
|
||||
prvnStatusNm?: string;
|
||||
chargerNm?: string;
|
||||
chargerCttpc?: string;
|
||||
submitStatusCd?: string;
|
||||
prvnStatusCd?: string;
|
||||
lclgvPicNm?: string;
|
||||
lclgvPicTelnoCn?: string;
|
||||
lclgvSbmsnSttsCd?: string;
|
||||
lclgvSbmsnSttsNm?: string;
|
||||
mctpvPicSbmsnSttsCd?: string;
|
||||
mctpvPicSbmsnSttsNm?: string;
|
||||
}
|
||||
|
||||
export interface StatsInputStatusReviewResponseData {
|
||||
@@ -37,7 +37,7 @@ export interface ReviewHistoryResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: {
|
||||
logDt: string;
|
||||
regDt: string;
|
||||
historyContent: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
@@ -342,7 +342,6 @@ export interface ReviewOpinionDetailItem {
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
splmntOpnnCn: string | null;
|
||||
splmntRsltCn: string | null;
|
||||
mdfrUuid: string | null;
|
||||
}
|
||||
|
||||
@@ -358,7 +357,6 @@ export interface ReviewOpinionSaveRequest {
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
splmntOpnnCn: string;
|
||||
splmntRsltCn: string;
|
||||
mdfrUuid: string;
|
||||
}
|
||||
|
||||
@@ -403,7 +401,6 @@ export interface ReviewSubmitRequest {
|
||||
/** 통계항목 단위 제출 시 사용 (통계 입력 현황 일괄 제출에는 미사용) */
|
||||
statsCd?: string;
|
||||
splmntOpnnCn?: string;
|
||||
splmntRsltCn?: string;
|
||||
}
|
||||
|
||||
export interface ReviewSubmitResponse {
|
||||
@@ -469,7 +466,6 @@ export interface ReviewerInfoResponse {
|
||||
dpmNm: string;
|
||||
cps: string;
|
||||
splmntOpnnCn: string;
|
||||
splmntRsltCn: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,9 +489,8 @@ export interface StatiscicsManagementReviewHistoryItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
menuNm: string;
|
||||
wrterOpinionSm: string | null;
|
||||
chckerOpinionSm: string | null;
|
||||
returnOpinionSm: string | null;
|
||||
splmntOpnnCn: string | null;
|
||||
rjctOpnnCn: string | null;
|
||||
}
|
||||
|
||||
export interface StatiscicsManagementReviewHistoryResponse {
|
||||
|
||||
-2
@@ -127,7 +127,6 @@ export interface HumanWasteSludgeReviewItem {
|
||||
rcycQnt: number | null;
|
||||
olandLfvlm: number | null;
|
||||
incnrQnt: number | null;
|
||||
oceanInputQnt: number | null;
|
||||
fuelQnt: number | null;
|
||||
etcQnt: number | null;
|
||||
unprcsQnt: number | null;
|
||||
@@ -218,7 +217,6 @@ export function mapSewageExcretaSlurgeReviewRows(list: unknown[]): object[] {
|
||||
rcycQnt: row.rcycQnt ?? 0,
|
||||
olandLfvlm: row.olandLfvlm ?? 0,
|
||||
incnrQnt: row.incnrQnt ?? 0,
|
||||
oceanInputQnt: row.oceanInputQnt ?? 0,
|
||||
fuelQnt: row.fuelQnt ?? 0,
|
||||
etcQnt: row.etcQnt ?? 0,
|
||||
unprcsQnt: row.unprcsQnt ?? 0,
|
||||
|
||||
@@ -108,5 +108,4 @@ export interface StatsStatusInfo {
|
||||
mctpvPicSbmsnSttsCd?: string;
|
||||
rjctOpnnCn?: string;
|
||||
splmntOpnnCn?: string;
|
||||
splmntRsltCn?: string;
|
||||
}
|
||||
@@ -270,5 +270,4 @@ export interface StatsStatusInfo {
|
||||
mctpvPicSbmsnSttsCd?: string;
|
||||
rjctOpnnCn?: string;
|
||||
splmntOpnnCn?: string;
|
||||
splmntRsltCn?: string;
|
||||
}
|
||||
-1
@@ -59,7 +59,6 @@ export interface statsStatusInfo {
|
||||
mctpvPicSbmsnSttsCd?: string;
|
||||
rjctOpnnCn?: string;
|
||||
splmntOpnnCn?: string;
|
||||
splmntRsltCn?: string;
|
||||
}
|
||||
|
||||
export interface SewerageSupplyRateReviewResponseData {
|
||||
|
||||
-1
@@ -135,7 +135,6 @@ export interface HumanWasteSludgeStatusTotalItem {
|
||||
rcycQnt: number;
|
||||
olandLfvlm: number;
|
||||
incnrQnt: number;
|
||||
oceanInputQnt: number;
|
||||
fuelQnt: number;
|
||||
etcQnt: number;
|
||||
unprcsQnt: number;
|
||||
|
||||
@@ -113,12 +113,8 @@ export async function PUT(
|
||||
}
|
||||
|
||||
function getCookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
|
||||
|
||||
const { hostname } = window.location;
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
|
||||
|
||||
return hostname;
|
||||
// 개발에서는 현재 사이트 전용, 운영에서는 환경변수의 공용 도메인을 사용한다.
|
||||
return process.env.NODE_ENV === "production"
|
||||
? COOKIE_DOMAIN || undefined
|
||||
: undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user