조사 결함 분류별 상세보기 및 등록 팝업 개선
This commit is contained in:
+24
-2
@@ -146,9 +146,9 @@
|
||||
| 표 구조 | 관로·맨홀 팝업 비교 | 같은 고정 정보 열과 대·중·소 3열 구조, 조사유형별 이상항목 코드·명칭 적용 |
|
||||
| 스크롤 | 표를 좌우·상하로 이동 | 팝업 문서 전체 스크롤 없음, 표 영역에서만 스크롤 |
|
||||
| 고정 열 | 표를 오른쪽으로 이동 | 보고서번호·관로/맨홀번호가 고정되고 규격 이후 경계선 유지 |
|
||||
| 정렬 | 고정 정보 헤더를 3회 클릭 | 보고서번호~운영적 내부결함만 오름차순→내림차순→기본 순환 |
|
||||
| 정렬 | 고정 정보 헤더를 3회 클릭 | 보고서번호~주행거리만 오름차순→내림차순→기본 순환 |
|
||||
| 조회 | 관로/맨홀번호 입력 후 조회 | 번호 포함 행만 표시되고 빈 조건에서 전체 3행 복원 |
|
||||
| 소계 | 샘플 3행 값 합산 | 연장·주행거리·내부결함과 코드별 대·중·소 합계 일치 |
|
||||
| 소계 | 샘플 3행 값 합산 | 연장·주행거리와 코드별 대·중·소 합계 일치 |
|
||||
| 합계 | 이상항목 합계 행 확인 | 각 항목 대·중·소 3열 병합 후 총합 표시 |
|
||||
| 요약 라벨 | 가로 스크롤 전·후 비교 | 규격 전에는 병합영역 중앙, 규격 이후에도 고정열 오른쪽에 소계·합계 라벨 표시 |
|
||||
| 목록 정렬 | 조사사업 `NO` 헤더 3회 클릭 | 정순→역순→기본 순환, 서버에 가상 `rowNumber` 필드 미전달 |
|
||||
@@ -158,3 +158,25 @@
|
||||
|
||||
- 사용자 화면 확인을 이번 프론트 상세보기 완료 기준으로 사용한다.
|
||||
- 실 API 응답, JSONB 코드 매핑, 실제 엑셀 다운로드 파일 검증은 백엔드 구현 후 통합 검증으로 이관한다.
|
||||
|
||||
## 13. 결함 분류별 상세보기와 팝업 최종 회귀 계획
|
||||
|
||||
| 검증 항목 | 조작 | 기대 결과 |
|
||||
| --- | --- | --- |
|
||||
| 행 선택 | 상세현황 처리분구 행의 임의 셀 클릭 | 행 전체가 선택 강조되고 상단 상세보기 대상 갱신 |
|
||||
| 0건 처리 | 결함 건수 0 셀 확인 | 파란 링크가 아니며 상세 팝업을 열지 않음 |
|
||||
| 구조적 링크 | 구조적 결함 1건 이상 링크 클릭 | 행 선택 후 `STRUCTURAL` 상세 팝업 표시 |
|
||||
| 운영적 링크 | 운영적 결함 1건 이상 링크 클릭 | 행 선택 후 `OPERATIONAL` 상세 팝업 표시 |
|
||||
| 전체 상세 | 행 선택 후 상단 상세보기 클릭 | `ALL` 기준 구조·운영 이상항목을 함께 표시 |
|
||||
| 조사유형 | 관로·맨홀 탭에서 각각 링크 클릭 | 선택 탭에 맞는 관로/맨홀 상세표 표시 |
|
||||
| 분류 열 필터 | 전체·구조·운영 팝업 비교 | 선택 분류 코드 열만 표시 |
|
||||
| 분류 행 필터 | 분류별 샘플 데이터 확인 | 선택 분류의 결함이 하나 이상인 시설 행만 표시 |
|
||||
| 코드 헤더 | 이상항목 헤더 확인 | 모든 헤더가 `명칭 (코드)` 형식 |
|
||||
| 제거 열 | 고정 정보 열 확인 | 구조적·운영적 내부결함 열 없음 |
|
||||
| 집계 | 전체·구조·운영 각각 소계·합계 확인 | 현재 표시 행·열의 대·중·소 값과 일치 |
|
||||
| 사업등록 검색 | 처리분구명 검색·결과 선택·초기화 | 필터된 목록 선택과 선택 건수 갱신 |
|
||||
| 결과등록 요약 | 사업정보 영역 확인 | 읽기 전용 텍스트가 한 줄에서 잘리지 않음 |
|
||||
| 회귀 | 사업등록·결과등록·상세보기 팝업 비교 | 파란 헤더와 옅은 파란 본문 배경 일관성 유지 |
|
||||
|
||||
- 브라우저 수동 검증은 로그인된 `SN` 세션과 `/oper/exmn/register?sample=Y` 진입이 선행조건이다.
|
||||
- 개발 서버가 실행 중이면 별도 프로덕션 빌드를 실행하지 않고 정적 검사와 사용자 화면 확인을 우선한다.
|
||||
|
||||
+17
-3
@@ -178,8 +178,8 @@
|
||||
- 관로·맨홀 화면은 같은 고정 정보 열과 대·중·소 3열 구조를 사용하며 이상항목 내용만 코드 정의에 따라 달라지는 것을 확인했다.
|
||||
- 상세표 헤더 배경·글꼴을 공통 그리드와 맞추고 팝업 문서 대신 표 영역에서만 가로·세로 스크롤하는 구조를 확인했다.
|
||||
- 관로/맨홀번호 입력 폭을 축소하고 번호 포함 검색으로 샘플 행을 필터링하는 동작을 확인했다.
|
||||
- 보고서번호부터 운영적 내부결함까지만 정렬 버튼이 표시되고 `asc → desc → undefined`로 순환하는 소스를 확인했다.
|
||||
- 소계는 연장·주행거리·구조적·운영적 내부결함과 이상항목별 대·중·소 합산값을 표시한다.
|
||||
- 보고서번호부터 주행거리까지만 정렬 버튼이 표시되고 `asc → desc → undefined`로 순환하는 소스를 확인했다.
|
||||
- 소계는 연장·주행거리와 이상항목별 대·중·소 합산값을 표시한다.
|
||||
- 합계는 각 이상항목의 대·중·소 3열을 병합하고 총합을 표시한다.
|
||||
- 소계·합계 라벨을 보고서번호부터 규격까지 가운데 배치하고, 가로 스크롤로 규격이 사라진 뒤에도 고정열 오른쪽에서 라벨과 경계선이 유지되도록 보정했다.
|
||||
- 조사사업 목록 `NO` 정렬은 가상 필드를 서버에 전달하지 않고 조회 원본 정·역순으로 동작한다.
|
||||
@@ -189,6 +189,20 @@
|
||||
- `git diff --check`는 통과했다. 실행 중 개발 서버 보호와 로컬 TypeScript compiler의 샌드박스 `EPERM` 이력 때문에 별도 프로덕션 빌드·타입 검사는 수행하지 않았다.
|
||||
- 실 상세 API, JSONB 코드 매핑, 실제 엑셀 다운로드와 지도보기는 검증되지 않았으며 후속 범위다.
|
||||
|
||||
## 7. 흡수한 기존 문서
|
||||
## 13. 결함 분류별 상세보기와 팝업 최종 보완 검증 결과
|
||||
|
||||
- 구조적·운영적 결함 건수가 1개 이상일 때만 파란색 밑줄 링크와 팝업 클릭 동작이 활성화되는 조건을 소스에서 확인했다.
|
||||
- 링크 클릭이 처리분구 선택과 `defectType=STRUCTURAL|OPERATIONAL` 상세 팝업 열기를 함께 수행하는 것을 확인했다.
|
||||
- 상단 상세보기는 기본값 `ALL`을 전달하며 잘못된 `defectType`은 팝업 content에서 `ALL`로 정규화한다.
|
||||
- 관로·맨홀 상세표 모두 선택 분류의 코드 열과 해당 결함이 존재하는 시설 행만 남기는 필터를 확인했다.
|
||||
- 구조적·운영적 내부결함 고정 열과 관련 샘플 필드가 제거됐고 이상항목 헤더가 `명칭 (코드)`로 표시되는 것을 확인했다.
|
||||
- 소계·합계가 필터 후 표시 행·열을 기준으로 다시 계산되는 것을 소스와 샘플 데이터로 확인했다.
|
||||
- 사업등록의 처리분구 검색·검색결과 선택·초기화와 결과등록의 읽기 전용 사업정보 요약 구현을 확인했다.
|
||||
- 사업등록·결과등록·상세보기 팝업이 공통 파란 헤더와 옅은 파란 본문 배경을 사용하는 것을 확인했다.
|
||||
- `git diff --check` 정적 검증을 수행 대상으로 유지한다.
|
||||
- 이번 문서 반영 시점에는 로그인 세션이 없어 브라우저 자동 재검증을 수행하지 못했다. 기존 사용자 화면 완료 판정을 유지하되 이번 결함 분류 링크는 로그인된 화면에서 최종 클릭 확인이 필요하다.
|
||||
- 로컬 TypeScript compiler는 샌드박스 `EPERM` 이력으로 별도 실행하지 않았다. 이는 기능 실패가 아니라 실행 환경 차단으로 분류한다.
|
||||
|
||||
## 14. 흡수한 기존 문서
|
||||
|
||||
- 없음
|
||||
|
||||
@@ -183,9 +183,26 @@
|
||||
|
||||
- 공통 WindowPopup의 파란 헤더와 공통 그리드 헤더 스타일을 사용한다.
|
||||
- 사업정보 요약 띠를 제거하고 처리분구·조사대상·번호 조건과 조회·다운로드를 본문 상단에 배치한다.
|
||||
- 보고서번호부터 운영적 내부결함까지 정렬을 지원하고 보고서번호·관로/맨홀번호를 가로 스크롤 고정 열로 사용한다.
|
||||
- 구조적·운영적 내부결함 고정 열을 제거하고 보고서번호부터 주행거리까지 정렬을 지원한다. 보고서번호·관로/맨홀번호는 가로 스크롤 고정 열로 사용한다.
|
||||
- 이상항목은 관로·맨홀 코드값을 열 식별자로 유지하고 화면은 대·중·소 3열로 표시한다.
|
||||
- 소계는 심각도별 값을, 합계는 항목별 병합 총합을 표시한다. 소계·합계 라벨은 규격까지의 병합영역을 사용하되 가로 스크롤 후에도 고정열 오른쪽에서 보이게 한다.
|
||||
- 조사사업 목록 `NO`와 조사 상세현황의 모든 데이터 열에 정렬을 제공한다. 상세현황 합계 행은 정렬 대상에서 제외해 상단에 고정한다.
|
||||
- 상세현황 버튼 순서는 `상세보기 → 지도보기 → 다운로드`이며 다운로드만 기존 초록색 스타일을 사용한다.
|
||||
- 사용자 화면 확인으로 상세보기 프론트 단계는 완료했다. 실 상세 API, 엑셀 다운로드, 지도보기는 완료 범위가 아니다.
|
||||
|
||||
## 16. 결함 분류별 상세보기와 팝업 최종 보완 범위
|
||||
|
||||
| 구분 | 완료 내용 |
|
||||
| --- | --- |
|
||||
| 상세현황 행 선택 | 처리분구 행 전체 선택·강조와 상세보기 대상을 동기화 |
|
||||
| 결함 건수 링크 | 1개 이상인 구조적·운영적 결함 건수를 파란색 밑줄 링크로 표시 |
|
||||
| 팝업 분기 | 상단 상세보기 `ALL`, 건수 링크 `STRUCTURAL|OPERATIONAL` 전달 |
|
||||
| 분류별 상세표 | 선택 분류의 코드 열과 결함이 있는 시설 행만 표시 |
|
||||
| 코드 표기 | 관로·맨홀 이상항목 헤더를 `명칭 (코드)`로 표시 |
|
||||
| 표 정리 | 구조적·운영적 내부결함 고정 열 제거, 표시 열 기준 소계·합계 재계산 |
|
||||
| 사업등록 팝업 | 공통 팝업 디자인 통일, 처리분구 검색·검색결과 선택·초기화 |
|
||||
| 결과등록 팝업 | 공통 팝업 디자인 통일, 사업정보를 읽기 전용 요약 텍스트로 정리 |
|
||||
|
||||
- 수정 대상은 `src/features/exmn/**`와 `src/features/windowPopup/**` 안으로 제한한다.
|
||||
- 백엔드 API·DTO·DB는 수정하지 않는다.
|
||||
- 완료 게이트는 관로·맨홀 각각 `ALL|STRUCTURAL|OPERATIONAL` 화면, 코드 헤더, 필터 결과와 소계·합계가 일치하는 것이다.
|
||||
|
||||
+37
-3
@@ -308,14 +308,48 @@
|
||||
- `exmnSurveyDetail` 팝업 키와 `1500×720` definition을 추가하고 선택 사업 ID·조사유형을 필수 query로 전달한다.
|
||||
- `ExmnSurveyDetailPopupContent`가 query를 `ExmnBusiness`, `PPL|MNHL`, `sample=Y` 화면 속성으로 변환하고 `ExmnSurveyDetailPopupPage`를 렌더링한다.
|
||||
- `exmn-detail-table.ts`에 관로 24개, 맨홀 23개 이상항목 코드·명칭·구조/운영 구분을 화면 열 정의로 분리했다.
|
||||
- 관로·맨홀 모두 보고서번호, 관로/맨홀번호, 상·하류맨홀, 배수방식, 관종, 규격, 연장, 주행거리, 구조적·운영적 내부결함을 공통 고정 정보 열로 사용한다.
|
||||
- 관로·맨홀 모두 보고서번호, 관로/맨홀번호, 상·하류맨홀, 배수방식, 관종, 규격, 연장, 주행거리를 공통 고정 정보 열로 사용한다.
|
||||
- 사용자 제공 엑셀 구조에 맞춰 이상항목마다 `대·중·소` 3열을 만들고 그룹·항목·심각도의 다단 병합 헤더를 직접 구성했다.
|
||||
- 보고서번호부터 운영적 내부결함까지는 `오름차순 → 내림차순 → 기본`으로 정렬한다. 보고서번호와 관로/맨홀번호는 가로 스크롤 고정 열이다.
|
||||
- 보고서번호부터 주행거리까지는 `오름차순 → 내림차순 → 기본`으로 정렬한다. 보고서번호와 관로/맨홀번호는 가로 스크롤 고정 열이다.
|
||||
- 팝업 문서 overflow를 잠그고 표 영역에서만 가로·세로 스크롤한다.
|
||||
- 현재 조회 결과의 연장·주행거리·내부결함 및 코드별 대·중·소를 합산해 소계·합계를 표시한다. 소계는 심각도별, 합계는 항목별 3열 병합 총합이다.
|
||||
- 현재 조회 결과의 연장·주행거리 및 코드별 대·중·소를 합산해 소계·합계를 표시한다. 소계는 심각도별, 합계는 항목별 3열 병합 총합이다.
|
||||
- 소계·합계 라벨은 보고서번호부터 규격까지의 병합영역 중앙에서 시작하고, 규격이 스크롤로 사라진 뒤에도 고정열 오른쪽에 라벨과 경계선이 남도록 `scrollLeft` 기반 위치를 보정한다.
|
||||
- `sample=Y`에서 관로·맨홀 각 3행을 제공하며 일반 URL은 빈 화면과 실제 API 후속 안내를 유지한다.
|
||||
- 조사사업 목록 `NO` 정렬을 추가했다. `NO`는 서버 엔티티 필드가 아니므로 서버 sort에는 전달하지 않고 현재 기본 조회 결과를 정·역순으로 표시한다.
|
||||
- 조사 상세현황은 합계 행을 별도 렌더링해 고정하고 나머지 처리분구 행만 전 열 정렬한다.
|
||||
- 상세현황 액션을 `상세보기 → 지도보기 → 다운로드`로 배치하고 다운로드 버튼에 기존 초록색 스타일을 적용했다.
|
||||
- 백엔드 API·DTO·DB는 변경하지 않았다. 실 상세조회와 엑셀 다운로드 계약은 후속이다.
|
||||
|
||||
## 12. 2026-07-24 결함 분류별 상세보기와 등록 팝업 최종 보완
|
||||
|
||||
### 12.1 조사 상세현황 선택과 링크
|
||||
|
||||
- `ExmnStatusDetailPanel.tsx`에서 처리분구 행 선택 상태를 상세보기 대상과 연결했다.
|
||||
- 구조적·운영적 결함 건수가 1개 이상인 셀은 파란색 밑줄 링크로 렌더링한다.
|
||||
- 링크 클릭 시 해당 행을 선택·강조하고 기존 `exmnSurveyDetail` WindowPopup을 연다.
|
||||
- 상단 상세보기는 `defectType=ALL`, 구조적 링크는 `STRUCTURAL`, 운영적 링크는 `OPERATIONAL`을 전달한다.
|
||||
- 건수가 0이면 일반 텍스트로 유지해 잘못된 빈 상세 팝업 진입을 막는다.
|
||||
|
||||
### 12.2 상세 팝업 필터
|
||||
|
||||
- `ExmnSurveyDetailPopupContent.tsx`가 `defectType`을 파싱하고 누락되거나 잘못된 값은 `ALL`로 정규화한다.
|
||||
- `ExmnSurveyDetailPopupPage.tsx`는 선택 분류의 이상항목 열만 남기고 해당 열에 결함 건수가 있는 시설 행만 표시한다.
|
||||
- 관로·맨홀 모두 동일한 필터 규칙을 사용하며 팝업 제목과 집계표 제목에 선택 분류를 표시한다.
|
||||
- 구조적·운영적 내부결함 고정 열과 샘플 필드를 제거했다.
|
||||
- 표시된 이상항목 헤더는 `명칭 (코드)` 형식이며 소계·합계는 현재 표시 행과 열 기준으로 재계산한다.
|
||||
- 소계·합계 라벨 병합 범위는 보고서번호부터 규격까지, 실제 수치 합산 시작은 연장부터 유지한다.
|
||||
|
||||
### 12.3 사업등록·결과등록 WindowPopup
|
||||
|
||||
- `ExmnBusinessRegisterPopupPage.tsx`와 `ExmnResultRegisterPopupPage.tsx`를 상세보기와 같은 파란 헤더·옅은 파란 본문 배경으로 통일했다.
|
||||
- 사업등록 처리분구 영역에 이름 검색, 검색결과 선택, 선택 초기화를 추가하고 선택 건수만 노출한다.
|
||||
- 조사결과 업로드의 사업차수·사업연도·행정구역·사업명은 입력 컴포넌트처럼 보이지 않는 읽기 전용 텍스트로 정리했다.
|
||||
- 팝업 크기와 본문 배치를 조정해 사업정보와 파일 업로드 표가 잘리지 않게 했다.
|
||||
|
||||
### 12.4 수정 파일과 연동 경계
|
||||
|
||||
- `src/features/exmn/model/{exmn-detail-table,exmn-sample-data}.ts`
|
||||
- `src/features/exmn/ui/{ExmnBusinessRegisterPopupPage,ExmnResultRegisterPopupPage,ExmnStatusDetailPanel,ExmnSurveyDetailPopupPage}.tsx`
|
||||
- `src/features/windowPopup/model/popupDefinitions.ts`
|
||||
- `src/features/windowPopup/ui/ExmnSurveyDetailPopupContent.tsx`
|
||||
- 백엔드 코드·API·DTO·DB는 변경하지 않았다. 후속 API는 `jobType`, 처리분구 식별자, `defectType`과 JSONB 코드별 건수 계약을 사용해야 한다.
|
||||
|
||||
@@ -5,6 +5,7 @@ export const EXMN_DETAIL_SEVERITIES = ["L", "M", "S"] as const;
|
||||
|
||||
export type ExmnDetailSeverity = (typeof EXMN_DETAIL_SEVERITIES)[number];
|
||||
export type ExmnDefectGroup = "STRUCTURAL" | "OPERATIONAL";
|
||||
export type ExmnDetailDefectType = "ALL" | ExmnDefectGroup;
|
||||
export type ExmnSeverityCounts = Record<ExmnDetailSeverity, number>;
|
||||
|
||||
/** 화면 열과 추후 JSONB 코드 매핑이 같은 식별자를 사용하도록 하는 이상항목 정의다. */
|
||||
@@ -25,8 +26,6 @@ export interface ExmnSurveyDetailRow {
|
||||
specification: number;
|
||||
extensionM: number;
|
||||
drivingDistanceM: number;
|
||||
structuralInternalDefect: number;
|
||||
operationalInternalDefect: number;
|
||||
anomalyTotals: ExmnSeverityCounts;
|
||||
defectCounts: Record<string, ExmnSeverityCounts>;
|
||||
}
|
||||
|
||||
@@ -239,8 +239,6 @@ export const getExmnSampleSurveyDetails = (
|
||||
drainageType: "합류",
|
||||
pipeType: "HP",
|
||||
specification: 600,
|
||||
structuralInternalDefect: rowIndex === 1 ? 3 : 1,
|
||||
operationalInternalDefect: 1,
|
||||
anomalyTotals,
|
||||
defectCounts,
|
||||
};
|
||||
|
||||
@@ -23,10 +23,8 @@ import type {
|
||||
} from "../model/exmn-business-types";
|
||||
import type { TrtdstSettingDto } from "../model/trtdst-types";
|
||||
|
||||
const ACTION_BUTTON_CLASS =
|
||||
"rounded-sm bg-blue-600 px-4 py-2 text-sm font-medium !text-white hover:bg-blue-700 disabled:!text-white";
|
||||
const DOWNLOAD_BUTTON_CLASS =
|
||||
"green rounded-sm px-4 py-2 text-sm font-medium !text-white disabled:!text-white";
|
||||
"rounded-sm px-4 py-2 text-sm font-medium !text-white disabled:!text-white";
|
||||
|
||||
type Notice = { type: "success" | "error" | "info"; text: string } | null;
|
||||
|
||||
@@ -59,6 +57,8 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
const [sggNm, setSggNm] = useState(sampleMode ? "양평군" : "");
|
||||
const [prcisExmnPrlg, setPrcisExmnPrlg] = useState(sampleMode ? "18.42" : "0");
|
||||
const [districts, setDistricts] = useState<TrtdstSettingDto[]>([]);
|
||||
/** 등록 후보가 많을 때 처리분구명으로 화면 목록을 좁히는 검색어다. */
|
||||
const [districtKeyword, setDistrictKeyword] = useState("");
|
||||
const [selectedDistrictIds, setSelectedDistrictIds] = useState<string[]>([]);
|
||||
const [savedBusiness, setSavedBusiness] = useState<ExmnBusinessDetail | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -81,6 +81,7 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
|
||||
/** 선택 행정구역의 활성 처리분구를 읽고 화면 요구사항대로 가나다순으로 제공한다. */
|
||||
useEffect(() => {
|
||||
setDistrictKeyword("");
|
||||
if (!ctpvNm || !sggNm) {
|
||||
setDistricts([]);
|
||||
setSelectedDistrictIds([]);
|
||||
@@ -126,6 +127,33 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
const selectedDistricts = useMemo(() => districts.filter((district) =>
|
||||
selectedDistrictIds.includes(district.trtdstId)), [districts, selectedDistrictIds]);
|
||||
|
||||
/** 공백 차이를 무시한 처리분구명 부분일치 검색 결과다. */
|
||||
const filteredDistricts = useMemo(() => {
|
||||
const keyword = districtKeyword.replace(/\s+/g, "").toLocaleLowerCase("ko-KR");
|
||||
if (!keyword) return districts;
|
||||
return districts.filter((district) =>
|
||||
district.trtdstNm.replace(/\s+/g, "").toLocaleLowerCase("ko-KR").includes(keyword));
|
||||
}, [districtKeyword, districts]);
|
||||
|
||||
/** 현재 검색 결과가 모두 선택된 상태인지 일괄선택 버튼 문구와 동작에 사용한다. */
|
||||
const allFilteredDistrictsSelected = filteredDistricts.length > 0
|
||||
&& filteredDistricts.every((district) => selectedDistrictIds.includes(district.trtdstId));
|
||||
|
||||
/** 검색된 처리분구만 한 번에 선택하거나 해제하며 다른 검색에서 고른 값은 유지한다. */
|
||||
const handleToggleFilteredDistricts = () => {
|
||||
const filteredIds = new Set(filteredDistricts.map((district) => district.trtdstId));
|
||||
setSelectedDistrictIds((current) => {
|
||||
const nextIds = new Set(current);
|
||||
filteredIds.forEach((id) => {
|
||||
if (allFilteredDistrictsSelected) nextIds.delete(id);
|
||||
else nextIds.add(id);
|
||||
});
|
||||
return districts
|
||||
.filter((district) => nextIds.has(district.trtdstId))
|
||||
.map((district) => district.trtdstId);
|
||||
});
|
||||
};
|
||||
|
||||
/** 현재 입력값을 조사사업 신규 저장 계약으로 변환한다. */
|
||||
const buildRequest = (): ExmnBusinessSaveRequest => ({
|
||||
bizYr: bizYr.trim(),
|
||||
@@ -217,18 +245,27 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#f3f7fa] p-4">
|
||||
<main className="sn-popup-page">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<header className="flex items-center justify-between rounded-t border border-[#d7e0e8] bg-white px-5 py-4">
|
||||
<h1 className="text-xl font-semibold text-gray-800">사업정보 등록</h1>
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">사업정보 등록</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button className={ACTION_BUTTON_CLASS} size="sm" disabled={isLoading || Boolean(savedBusiness)} onClick={() => void handleSave()}>저장</Button>
|
||||
<Button className={DOWNLOAD_BUTTON_CLASS} size="sm" disabled={isLoading} onClick={() => void handleDownloadTemplates()}>양식 받기</Button>
|
||||
<Button className={DOWNLOAD_BUTTON_CLASS} size="sm" disabled={isLoading} onClick={handleReportTemplate}>조사보고서 양식</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="sn-popup-header-button"
|
||||
disabled={isLoading || Boolean(savedBusiness)}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button color="green" className={DOWNLOAD_BUTTON_CLASS} size="sm" disabled={isLoading} onClick={() => void handleDownloadTemplates()}>양식 받기</Button>
|
||||
<Button color="green" className={DOWNLOAD_BUTTON_CLASS} size="sm" disabled={isLoading} onClick={handleReportTemplate}>조사보고서 양식</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="space-y-3 border-x border-b border-[#d7e0e8] bg-white p-4">
|
||||
<section className="sn-popup-section space-y-3 overflow-y-auto">
|
||||
{notice && (
|
||||
<div role="alert" className={`rounded border px-4 py-3 text-sm ${
|
||||
notice.type === "error"
|
||||
@@ -239,8 +276,8 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
}`}>{notice.text}</div>
|
||||
)}
|
||||
|
||||
<section className="rounded border border-[#d7e0e8]">
|
||||
<div className="border-b border-[#d7e0e8] bg-[#e8f2fb] px-4 py-3 font-semibold text-gray-700">사업정보 입력</div>
|
||||
<section className="border border-border">
|
||||
<div className="border-b border-border bg-[#eaf3fb] px-4 py-3 font-semibold text-title">사업정보 입력</div>
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex flex-wrap items-center gap-y-4">
|
||||
<SelectBox
|
||||
@@ -296,15 +333,50 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded border border-[#d7e0e8]">
|
||||
<div className="flex items-center justify-between border-b border-[#d7e0e8] bg-[#e8f2fb] px-4 py-3">
|
||||
<h2 className="font-semibold text-gray-700">사업대상 처리분구</h2>
|
||||
<span className="text-sm text-gray-600">가나다순 · {selectedDistrictIds.length}개 선택</span>
|
||||
<section className="min-h-0 border border-border">
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-border bg-[#eaf3fb] px-4 py-2">
|
||||
<h2 className="mr-2 font-semibold text-title">사업대상 처리분구</h2>
|
||||
<Input
|
||||
type="search"
|
||||
value={districtKeyword}
|
||||
className="sn-popup-input-sm !h-9 w-[22rem] bg-white"
|
||||
placeholder="처리분구명 검색"
|
||||
aria-label="처리분구명 검색"
|
||||
disabled={Boolean(savedBusiness)}
|
||||
onChange={(event) => setDistrictKeyword(event.target.value)}
|
||||
/>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="sn-popup-secondary-button-sm"
|
||||
disabled={Boolean(savedBusiness) || filteredDistricts.length === 0}
|
||||
onClick={handleToggleFilteredDistricts}
|
||||
>
|
||||
{allFilteredDistrictsSelected ? "검색결과 해제" : "검색결과 선택"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="sn-popup-secondary-button-sm"
|
||||
disabled={Boolean(savedBusiness) || selectedDistrictIds.length === 0}
|
||||
onClick={() => setSelectedDistrictIds([])}
|
||||
>
|
||||
선택 초기화
|
||||
</Button>
|
||||
<span className="ml-1 whitespace-nowrap text-sm text-label">
|
||||
{selectedDistrictIds.length}개 선택
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 content-start gap-3 overflow-y-auto p-4" style={{ height: "21rem" }}>
|
||||
{districts.length === 0 ? (
|
||||
<p className="col-span-3 py-7 text-center text-sm text-gray-500">시도·시군구를 선택하거나 등록된 활성 처리분구를 확인해주세요.</p>
|
||||
) : districts.map((district) => (
|
||||
) : filteredDistricts.length === 0 ? (
|
||||
<p className="col-span-3 py-7 text-center text-sm text-gray-500">검색 조건에 맞는 처리분구가 없습니다.</p>
|
||||
) : filteredDistricts.map((district) => (
|
||||
<label key={district.trtdstId} className="flex cursor-pointer items-center gap-2 rounded border border-gray-200 bg-white px-3 py-2.5 text-sm hover:border-blue-300 hover:bg-blue-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -321,7 +393,7 @@ export function ExmnBusinessRegisterPopupPage({ sampleMode }: ExmnBusinessRegist
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,16 +294,25 @@ export function ExmnResultRegisterPopupPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full max-w-full flex-col overflow-hidden bg-[#f3f7fa] p-2">
|
||||
<main className="sn-popup-page">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<header className="flex shrink-0 items-center justify-between rounded-t border border-[#d7e0e8] bg-white px-4 py-2.5">
|
||||
<h1 className="text-xl font-semibold text-gray-800">조사 결과 업로드</h1>
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">조사 결과 업로드</h1>
|
||||
<div>
|
||||
<Button className={ACTION_BUTTON_CLASS} size="sm" disabled={isLoading || !validation?.valid} onClick={() => void handleCommit()}>저장</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="sn-popup-header-button"
|
||||
disabled={isLoading || !validation?.valid}
|
||||
onClick={() => void handleCommit()}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="min-h-0 flex-1 space-y-2 overflow-x-hidden overflow-y-auto border-x border-b border-[#d7e0e8] bg-white p-2">
|
||||
<section className="flex min-h-0 flex-1 flex-col gap-2 overflow-x-hidden overflow-y-auto px-4 pb-3 pt-3">
|
||||
{notice && (
|
||||
<div role="alert" className={`rounded border px-4 py-3 text-sm ${
|
||||
notice.type === "error"
|
||||
@@ -315,30 +324,46 @@ export function ExmnResultRegisterPopupPage({
|
||||
)}
|
||||
|
||||
<section
|
||||
className="grid gap-8 rounded border border-[#d7e0e8] bg-[#f8fafc] px-5 py-2.5 text-sm"
|
||||
style={{ gridTemplateColumns: "0.7fr 0.8fr 1.2fr 2.3fr" }}
|
||||
className="rounded-md border border-border bg-secondary p-3"
|
||||
aria-label="선택 조사사업 요약"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<strong className="shrink-0 text-gray-700">사업차수</strong>
|
||||
<span className="text-gray-600">{detail?.bizCycl ?? businessSummary.bizCycl ?? "-"}차</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<strong className="shrink-0 text-gray-700">사업년도</strong>
|
||||
<span className="text-gray-600">{detail?.bizYr ?? businessSummary.bizYr ?? "-"}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<strong className="shrink-0 text-gray-700">행정구역</strong>
|
||||
<span className="truncate text-gray-600">{detail?.ctpvNm ?? businessSummary.ctpvNm} {detail?.sggNm ?? businessSummary.sggNm}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<strong className="shrink-0 text-gray-700">사업명</strong>
|
||||
<span className="truncate text-gray-600" title={detail?.bizNm ?? businessSummary.bizNm}>{detail?.bizNm ?? businessSummary.bizNm ?? "-"}</span>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-7 gap-y-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-medium text-label">사업차수</span>
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-semibold text-foreground">
|
||||
{detail?.bizCycl ?? businessSummary.bizCycl ?? "-"}차
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-medium text-label">사업년도</span>
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-semibold text-foreground">
|
||||
{detail?.bizYr ?? businessSummary.bizYr ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className="whitespace-nowrap text-[1.05rem] font-medium text-label">행정구역</span>
|
||||
<span
|
||||
className="max-w-[22rem] truncate text-[1.05rem] font-semibold text-foreground"
|
||||
title={`${detail?.ctpvNm ?? businessSummary.ctpvNm} ${detail?.sggNm ?? businessSummary.sggNm}`}
|
||||
>
|
||||
{detail?.ctpvNm ?? businessSummary.ctpvNm} {detail?.sggNm ?? businessSummary.sggNm}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex min-w-[24rem] flex-1 items-center gap-2">
|
||||
<span className="shrink-0 whitespace-nowrap text-[1.05rem] font-medium text-label">사업명</span>
|
||||
<span
|
||||
className="min-w-0 truncate text-[1.05rem] font-semibold text-foreground"
|
||||
title={detail?.bizNm ?? businessSummary.bizNm}
|
||||
>
|
||||
{detail?.bizNm ?? businessSummary.bizNm ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="overflow-hidden rounded border border-[#d7e0e8]">
|
||||
<section className="overflow-hidden border border-border">
|
||||
<div
|
||||
className="grid items-center gap-3 bg-[#e8f2fb] px-3 py-2.5 text-center text-sm font-semibold text-gray-700"
|
||||
className="grid items-center gap-3 bg-[#eaf3fb] px-3 py-2.5 text-center text-sm font-semibold text-title"
|
||||
style={{ gridTemplateColumns: "180px minmax(0, 1fr) 80px 112px" }}
|
||||
>
|
||||
<span>파일 구분</span><span>선택 파일</span><span>찾기</span><span>데이터 검토</span>
|
||||
@@ -351,7 +376,7 @@ export function ExmnResultRegisterPopupPage({
|
||||
return (
|
||||
<div
|
||||
key={fileType}
|
||||
className="grid items-center gap-3 border-t border-[#d7e0e8] px-3 py-3 text-sm"
|
||||
className="grid items-center gap-3 border-t border-border px-3 py-3 text-sm"
|
||||
style={{ gridTemplateColumns: "180px minmax(0, 1fr) 80px 112px" }}
|
||||
>
|
||||
<label htmlFor={inputId} className="font-medium text-gray-700">
|
||||
@@ -389,7 +414,7 @@ export function ExmnResultRegisterPopupPage({
|
||||
</section>
|
||||
|
||||
{validation && (
|
||||
<section className="rounded border border-[#d7e0e8] p-4">
|
||||
<section className="border border-border p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-gray-700">데이터 검토 결과</h2>
|
||||
@@ -416,7 +441,7 @@ export function ExmnResultRegisterPopupPage({
|
||||
)}
|
||||
|
||||
{uploadStatus && <p className="text-right text-sm text-blue-700">{uploadStatus}</p>}
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
ExmnJobType,
|
||||
ExmnSectionSummaryRow,
|
||||
} from "../model/exmn-business-types";
|
||||
import type { ExmnDetailDefectType } from "../model/exmn-detail-table";
|
||||
import { getExmnSampleSectionSummaries } from "../model/exmn-sample-data";
|
||||
|
||||
const ACTION_BUTTON_CLASS =
|
||||
@@ -93,6 +94,7 @@ const sortSummaryRows = (
|
||||
};
|
||||
|
||||
interface DisplaySummaryRow extends Record<string, string | number> {
|
||||
trtdstId: string;
|
||||
rowNumber: string | number;
|
||||
trtdstNm: string;
|
||||
trgtPrlgText: string;
|
||||
@@ -108,6 +110,8 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
const [jobType, setJobType] = useState<ExmnJobType>("PPL");
|
||||
const [summarySort, setSummarySort] = useState<UiTableSort>();
|
||||
const [summaryRows, setSummaryRows] = useState<ExmnSectionSummaryRow[]>([]);
|
||||
/** 상세보기 팝업에 전달할 현재 선택 처리분구다. */
|
||||
const [selectedTrtdstId, setSelectedTrtdstId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [notice, setNotice] = useState<Notice>(null);
|
||||
const summaryRequestIdRef = useRef(0);
|
||||
@@ -115,11 +119,13 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
useEffect(() => {
|
||||
setJobType("PPL");
|
||||
setSummarySort(undefined);
|
||||
setSelectedTrtdstId(null);
|
||||
setNotice(null);
|
||||
}, [business?.exmnBizId]);
|
||||
|
||||
useEffect(() => {
|
||||
setSummarySort(undefined);
|
||||
setSelectedTrtdstId(null);
|
||||
}, [jobType]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -183,6 +189,7 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
const rows: DisplaySummaryRow[] = summaryRows.map((row, index) => {
|
||||
const counts = getCounts(row);
|
||||
return {
|
||||
trtdstId: row.trtdstId,
|
||||
rowNumber: index + 1,
|
||||
trtdstNm: row.trtdstNm,
|
||||
trgtPrlgText: formatNumber(row.trgtPrlg),
|
||||
@@ -207,6 +214,9 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
}, [jobType, summaryRows, summarySort]);
|
||||
|
||||
const summaryColumns = jobType === "PPL" ? PIPE_SUMMARY_TABLE_HEAD : MANHOLE_SUMMARY_TABLE_HEAD;
|
||||
const selectedSummaryRow = summaryTable.rows.find(
|
||||
(row) => row.trtdstId === selectedTrtdstId,
|
||||
) ?? null;
|
||||
|
||||
/** 상세현황 헤더를 오름차순·내림차순·기본 순서로 순환한다. */
|
||||
const handleSummarySort = (column: (typeof summaryColumns)[number]) => {
|
||||
@@ -227,12 +237,18 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
setNotice({ type: "info", text });
|
||||
};
|
||||
|
||||
/** 선택 사업과 현재 관로·맨홀 탭을 공통 WindowPopup 상세표에 전달한다. */
|
||||
const openSurveyDetailPopup = () => {
|
||||
if (!business) return;
|
||||
/** 선택 사업·처리분구, 조사유형, 결함 범위를 공통 WindowPopup 상세표에 전달한다. */
|
||||
const openSurveyDetailPopup = (
|
||||
summaryRow: DisplaySummaryRow | null = selectedSummaryRow,
|
||||
defectType: ExmnDetailDefectType = "ALL",
|
||||
) => {
|
||||
if (!business || !summaryRow) return;
|
||||
const popup = openWindowPopup("exmnSurveyDetail", {
|
||||
exmnBizId: business.exmnBizId,
|
||||
trtdstId: summaryRow.trtdstId,
|
||||
trtdstNm: summaryRow.trtdstNm,
|
||||
jobType,
|
||||
defectType,
|
||||
bizYr: business.bizYr,
|
||||
bizCycl: business.bizCycl,
|
||||
bizNm: business.bizNm,
|
||||
@@ -252,6 +268,12 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
}
|
||||
};
|
||||
|
||||
/** 사업목록과 동일하게 처리분구 행의 어느 셀을 눌러도 상세 대상 행으로 선택한다. */
|
||||
const handleSummaryRowSelect = (row: DisplaySummaryRow) => {
|
||||
setSelectedTrtdstId(row.trtdstId);
|
||||
setNotice(null);
|
||||
};
|
||||
|
||||
/** 보이는 버튼 전체에서 조사유형을 즉시 바꾸고, 상세 조회는 별도 effect에서 수행한다. */
|
||||
const handleJobTypeChange = (nextJobType: ExmnJobType) => {
|
||||
if (nextJobType === jobType) return;
|
||||
@@ -292,7 +314,15 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
))}
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button className={ACTION_BUTTON_CLASS} size="sm" onClick={openSurveyDetailPopup}>상세보기</Button>
|
||||
<Button
|
||||
className={ACTION_BUTTON_CLASS}
|
||||
size="sm"
|
||||
disabled={!selectedSummaryRow}
|
||||
title={selectedSummaryRow ? `${selectedSummaryRow.trtdstNm} 상세보기` : "처리분구 행을 먼저 선택해주세요."}
|
||||
onClick={() => openSurveyDetailPopup()}
|
||||
>
|
||||
상세보기
|
||||
</Button>
|
||||
<Button className={ACTION_BUTTON_CLASS} size="sm" onClick={() => showPendingNotice("지도보기")}>지도보기</Button>
|
||||
<Button
|
||||
color="green"
|
||||
@@ -417,26 +447,79 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
{summaryTable.rows.map((row, rowIndex) => (
|
||||
<TableRow
|
||||
key={`${row.trtdstNm}-${rowIndex}`}
|
||||
style={{ height: `${SUMMARY_GRID_ROW_HEIGHT_REM}rem` }}
|
||||
>
|
||||
{summaryColumns.map((column) => (
|
||||
<TableCell
|
||||
key={`${rowIndex}-${column.id}`}
|
||||
className="!py-[0.8rem] text-center"
|
||||
style={
|
||||
rowIndex === summaryTable.rows.length - 1
|
||||
? { borderBottomWidth: 0 }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{row[column.id]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
{summaryTable.rows.map((row, rowIndex) => {
|
||||
const selected = row.trtdstId === selectedTrtdstId;
|
||||
return (
|
||||
<TableRow
|
||||
key={row.trtdstId}
|
||||
data-state={selected ? "selected" : undefined}
|
||||
className="cursor-pointer"
|
||||
style={{ height: `${SUMMARY_GRID_ROW_HEIGHT_REM}rem` }}
|
||||
onClick={() => handleSummaryRowSelect(row)}
|
||||
>
|
||||
{summaryColumns.map((column) => {
|
||||
const defectType: ExmnDetailDefectType | null =
|
||||
column.id === "structuralCountText"
|
||||
? "STRUCTURAL"
|
||||
: column.id === "operationalCountText"
|
||||
? "OPERATIONAL"
|
||||
: null;
|
||||
const defectCount = defectType
|
||||
? Number(
|
||||
String(row[column.id] ?? "0").replaceAll(",", ""),
|
||||
)
|
||||
: 0;
|
||||
const opensFilteredDetail =
|
||||
defectType !== null && defectCount > 0;
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={`${row.trtdstId}-${column.id}`}
|
||||
className="!p-0 text-center"
|
||||
style={
|
||||
rowIndex === summaryTable.rows.length - 1
|
||||
? { borderBottomWidth: 0 }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full cursor-pointer items-center justify-center px-2 py-[0.8rem] text-[1.3rem] ${
|
||||
opensFilteredDetail
|
||||
? "font-semibold text-blue-600 underline underline-offset-4 hover:text-blue-800"
|
||||
: selected
|
||||
? "font-bold"
|
||||
: "font-normal"
|
||||
}`}
|
||||
style={{ minHeight: `${SUMMARY_GRID_ROW_HEIGHT_REM}rem` }}
|
||||
aria-label={
|
||||
opensFilteredDetail
|
||||
? `${row.trtdstNm} ${
|
||||
defectType === "STRUCTURAL" ? "구조적" : "운영적"
|
||||
} 결함 상세보기`
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
opensFilteredDetail
|
||||
? "해당 결함만 상세보기"
|
||||
: undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleSummaryRowSelect(row);
|
||||
if (opensFilteredDetail && defectType) {
|
||||
openSurveyDetailPopup(row, defectType);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{row[column.id]}
|
||||
</button>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<TableRow>
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { ExmnBusiness, ExmnJobType } from "../model/exmn-business-types";
|
||||
import {
|
||||
EXMN_DETAIL_SEVERITIES,
|
||||
EXMN_DETAIL_TABLE_DEFINITIONS,
|
||||
type ExmnDetailDefectColumn,
|
||||
type ExmnDetailDefectType,
|
||||
type ExmnDetailSeverity,
|
||||
type ExmnSeverityCounts,
|
||||
type ExmnSurveyDetailRow,
|
||||
} from "../model/exmn-detail-table";
|
||||
import { getExmnSampleSurveyDetails } from "../model/exmn-sample-data";
|
||||
@@ -24,8 +27,11 @@ const DETAIL_SCOPE_LABELS: Record<ExmnJobType, string> = {
|
||||
MNHL: "맨홀",
|
||||
};
|
||||
|
||||
/** 백엔드 처리분구 목록 연동 전 화면 검증에 사용하는 처리분구 선택값이다. */
|
||||
const SAMPLE_DISTRICT_NAME = "덕풍";
|
||||
const DEFECT_TYPE_LABELS: Record<ExmnDetailDefectType, string> = {
|
||||
ALL: "전체",
|
||||
STRUCTURAL: "구조적결함",
|
||||
OPERATIONAL: "운영적결함",
|
||||
};
|
||||
|
||||
const FIXED_COLUMNS = [
|
||||
{ id: "reportNo", label: "보고서\n번호", width: 118 },
|
||||
@@ -37,8 +43,6 @@ const FIXED_COLUMNS = [
|
||||
{ id: "specification", label: "규격", width: 82 },
|
||||
{ id: "extensionM", label: "연장", width: 86 },
|
||||
{ id: "drivingDistanceM", label: "주행\n거리", width: 92 },
|
||||
{ id: "structuralInternalDefect", label: "구조적\n내부결함", width: 105 },
|
||||
{ id: "operationalInternalDefect", label: "운영적\n내부결함", width: 105 },
|
||||
] as const;
|
||||
|
||||
const REPORT_COLUMN_LEFT = 0;
|
||||
@@ -62,7 +66,10 @@ type ExmnDetailFixedColumnId = (typeof FIXED_COLUMNS)[number]["id"];
|
||||
|
||||
interface ExmnSurveyDetailPopupPageProps {
|
||||
business: ExmnBusiness;
|
||||
trtdstId: string;
|
||||
trtdstNm: string;
|
||||
jobType: ExmnJobType;
|
||||
defectType: ExmnDetailDefectType;
|
||||
sampleMode: boolean;
|
||||
}
|
||||
|
||||
@@ -102,9 +109,26 @@ const compareFixedColumnValue = (
|
||||
});
|
||||
};
|
||||
|
||||
/** 현재 화면에 노출하는 결함 열만 합산해 행의 대·중·소 합계를 계산한다. */
|
||||
const getVisibleAnomalyTotals = (
|
||||
row: ExmnSurveyDetailRow,
|
||||
defectColumns: ExmnDetailDefectColumn[],
|
||||
): ExmnSeverityCounts => {
|
||||
const totals: ExmnSeverityCounts = { L: 0, M: 0, S: 0 };
|
||||
defectColumns.forEach((column) => {
|
||||
EXMN_DETAIL_SEVERITIES.forEach((severity) => {
|
||||
totals[severity] += row.defectCounts[column.code]?.[severity] ?? 0;
|
||||
});
|
||||
});
|
||||
return totals;
|
||||
};
|
||||
|
||||
/** 엑셀 `01. 덕풍`·`01. 덕풍 (2)` 구조를 관로·맨홀 공통 WindowPopup으로 표시한다. */
|
||||
export function ExmnSurveyDetailPopupPage({
|
||||
trtdstId,
|
||||
trtdstNm,
|
||||
jobType,
|
||||
defectType,
|
||||
sampleMode,
|
||||
}: ExmnSurveyDetailPopupPageProps) {
|
||||
/** 관로·맨홀 번호 조회 입력값과 실제 적용값을 분리해 조회 버튼 동작을 재현한다. */
|
||||
@@ -117,20 +141,39 @@ export function ExmnSurveyDetailPopupPage({
|
||||
/** 다운로드 미연결 등 현재 팝업 안내 메시지를 표시한다. */
|
||||
const [notice, setNotice] = useState<Notice>(null);
|
||||
const definition = EXMN_DETAIL_TABLE_DEFINITIONS[jobType];
|
||||
const defectTypeLabel = DEFECT_TYPE_LABELS[defectType];
|
||||
/** 건수 링크 진입 시 선택한 구조·운영 그룹의 결함 열만 표에 남긴다. */
|
||||
const visibleDefectColumns = useMemo(
|
||||
() => defectType === "ALL"
|
||||
? definition.defectColumns
|
||||
: definition.defectColumns.filter((column) => column.group === defectType),
|
||||
[defectType, definition],
|
||||
);
|
||||
/** 샘플 모드에서 선택 조사유형에 해당하는 상세 원본 행을 제공한다. */
|
||||
const sourceRows = useMemo(
|
||||
() => (sampleMode ? getExmnSampleSurveyDetails(jobType) : []),
|
||||
[jobType, sampleMode],
|
||||
);
|
||||
/** 분류별 상세에서는 선택 결함이 한 건 이상 존재하는 시설 행만 표시한다. */
|
||||
const scopedRows = useMemo(() => {
|
||||
if (defectType === "ALL") return sourceRows;
|
||||
return sourceRows.filter((row) =>
|
||||
visibleDefectColumns.some((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.some(
|
||||
(severity) => (row.defectCounts[column.code]?.[severity] ?? 0) > 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}, [defectType, sourceRows, visibleDefectColumns]);
|
||||
/** 조회 버튼으로 확정한 번호 조건을 현재 상세 행에 적용한다. */
|
||||
const filteredRows = useMemo(() => {
|
||||
const normalizedKeyword = appliedDetailNumberKeyword.trim().toLocaleLowerCase("ko-KR");
|
||||
if (!normalizedKeyword) return sourceRows;
|
||||
return sourceRows.filter((row) =>
|
||||
if (!normalizedKeyword) return scopedRows;
|
||||
return scopedRows.filter((row) =>
|
||||
row.pipeNo.toLocaleLowerCase("ko-KR").includes(normalizedKeyword),
|
||||
);
|
||||
}, [appliedDetailNumberKeyword, sourceRows]);
|
||||
/** 보고서번호부터 운영적 내부결함까지의 고정 열에 선택 정렬을 적용한다. */
|
||||
}, [appliedDetailNumberKeyword, scopedRows]);
|
||||
/** 보고서번호부터 주행거리까지의 고정 열에 선택 정렬을 적용한다. */
|
||||
const rows = useMemo(() => {
|
||||
if (!sort) return filteredRows;
|
||||
const columnId = sort.key as ExmnDetailFixedColumnId;
|
||||
@@ -155,13 +198,13 @@ export function ExmnSurveyDetailPopupPage({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/** 관로·맨홀 조사유형이 바뀌면 이전 번호 검색 조건과 안내를 초기화한다. */
|
||||
/** 관로·맨홀 조사유형 또는 결함 범위가 바뀌면 이전 검색 조건과 안내를 초기화한다. */
|
||||
useEffect(() => {
|
||||
setDetailNumberKeyword("");
|
||||
setAppliedDetailNumberKeyword("");
|
||||
setSort(undefined);
|
||||
setNotice(null);
|
||||
}, [jobType]);
|
||||
}, [defectType, jobType]);
|
||||
|
||||
/** 화면 조회 버튼으로 입력 번호를 확정해 샘플 상세 행을 필터링한다. */
|
||||
const handleSearch = () => {
|
||||
@@ -207,55 +250,39 @@ export function ExmnSurveyDetailPopupPage({
|
||||
(sum, row) => ({
|
||||
extensionM: sum.extensionM + row.extensionM,
|
||||
drivingDistanceM: sum.drivingDistanceM + row.drivingDistanceM,
|
||||
structuralInternalDefect:
|
||||
sum.structuralInternalDefect + row.structuralInternalDefect,
|
||||
operationalInternalDefect:
|
||||
sum.operationalInternalDefect + row.operationalInternalDefect,
|
||||
}),
|
||||
{
|
||||
extensionM: 0,
|
||||
drivingDistanceM: 0,
|
||||
structuralInternalDefect: 0,
|
||||
operationalInternalDefect: 0,
|
||||
},
|
||||
),
|
||||
[rows],
|
||||
);
|
||||
|
||||
/** 고정 열 중 합산 의미가 있는 연장·주행거리·내부결함 4개 열의 요약 표시값이다. */
|
||||
/** 고정 열 중 합산 의미가 있는 연장·주행거리 2개 열의 요약 표시값이다. */
|
||||
const fixedSummaryValues = [
|
||||
{ key: "extensionM", value: formatNumber(totals.extensionM) },
|
||||
{ key: "drivingDistanceM", value: formatNumber(totals.drivingDistanceM) },
|
||||
{
|
||||
key: "structuralInternalDefect",
|
||||
value: formatCount(totals.structuralInternalDefect),
|
||||
},
|
||||
{
|
||||
key: "operationalInternalDefect",
|
||||
value: formatCount(totals.operationalInternalDefect),
|
||||
},
|
||||
];
|
||||
|
||||
/** 현재 조회 결과를 기준으로 관로·맨홀 이상항목의 대·중·소 소계와 항목별 합계를 계산한다. */
|
||||
/** 현재 보이는 구조·운영 결함 열만 기준으로 대·중·소 소계와 항목별 합계를 계산한다. */
|
||||
const defectSummary = useMemo(() => {
|
||||
const anomalySubtotals = { L: 0, M: 0, S: 0 };
|
||||
const defectSubtotals = Object.fromEntries(
|
||||
definition.defectColumns.map((column) => [
|
||||
visibleDefectColumns.map((column) => [
|
||||
column.code,
|
||||
{ L: 0, M: 0, S: 0 },
|
||||
]),
|
||||
) as Record<string, Record<ExmnDetailSeverity, number>>;
|
||||
|
||||
rows.forEach((row) => {
|
||||
EXMN_DETAIL_SEVERITIES.forEach((severity) => {
|
||||
anomalySubtotals[severity] += row.anomalyTotals[severity] ?? 0;
|
||||
});
|
||||
definition.defectColumns.forEach((column) => {
|
||||
visibleDefectColumns.forEach((column) => {
|
||||
const columnCounts = defectSubtotals[column.code];
|
||||
if (!columnCounts) return;
|
||||
EXMN_DETAIL_SEVERITIES.forEach((severity) => {
|
||||
columnCounts[severity] +=
|
||||
row.defectCounts[column.code]?.[severity] ?? 0;
|
||||
const count = row.defectCounts[column.code]?.[severity] ?? 0;
|
||||
columnCounts[severity] += count;
|
||||
anomalySubtotals[severity] += count;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -271,7 +298,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
anomalyTotal: sumSeverities(anomalySubtotals),
|
||||
defectSubtotals,
|
||||
defectTotals: Object.fromEntries(
|
||||
definition.defectColumns.map((column) => [
|
||||
visibleDefectColumns.map((column) => [
|
||||
column.code,
|
||||
sumSeverities(
|
||||
defectSubtotals[column.code] ?? { L: 0, M: 0, S: 0 },
|
||||
@@ -279,20 +306,29 @@ export function ExmnSurveyDetailPopupPage({
|
||||
]),
|
||||
) as Record<string, number>,
|
||||
};
|
||||
}, [definition.defectColumns, rows]);
|
||||
}, [rows, visibleDefectColumns]);
|
||||
|
||||
const tableWidth = useMemo(
|
||||
() => FIXED_COLUMNS.reduce((sum, column) => sum + column.width, 0)
|
||||
+ (definition.defectColumns.length + 1)
|
||||
+ (visibleDefectColumns.length + 1)
|
||||
* EXMN_DETAIL_SEVERITIES.length
|
||||
* SEVERITY_COLUMN_WIDTH,
|
||||
[definition.defectColumns.length],
|
||||
[visibleDefectColumns.length],
|
||||
);
|
||||
const popupTitle = defectType === "ALL"
|
||||
? definition.title
|
||||
: `${definition.defectGroupTitle}조사 ${defectTypeLabel} 상세현황`;
|
||||
const tableTitle = defectType === "ALL"
|
||||
? `${trtdstNm} ${definition.defectGroupTitle}조사 집계표`
|
||||
: `${trtdstNm} ${definition.defectGroupTitle}조사 ${defectTypeLabel} 집계표`;
|
||||
const visibleDefectGroupTitle = defectType === "ALL"
|
||||
? definition.defectGroupTitle
|
||||
: `${definition.defectGroupTitle} ${defectTypeLabel}`;
|
||||
|
||||
return (
|
||||
<main className="sn-popup-page">
|
||||
<header className="sn-popup-header">
|
||||
<h1 className="sn-popup-title">{definition.title}</h1>
|
||||
<h1 className="sn-popup-title">{popupTitle}</h1>
|
||||
</header>
|
||||
|
||||
{notice && (
|
||||
@@ -310,10 +346,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<div className="sn-popup-field-label">처리분구</div>
|
||||
<select
|
||||
className="sn-popup-input !text-[1.1rem]"
|
||||
defaultValue={SAMPLE_DISTRICT_NAME}
|
||||
defaultValue={trtdstId}
|
||||
aria-label="처리분구"
|
||||
>
|
||||
<option value={SAMPLE_DISTRICT_NAME}>{SAMPLE_DISTRICT_NAME}</option>
|
||||
<option value={trtdstId}>{trtdstNm}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -324,7 +360,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
defaultValue={jobType}
|
||||
aria-label="조사대상"
|
||||
>
|
||||
<option value={jobType}>{DETAIL_SCOPE_LABELS[jobType]}</option>
|
||||
<option value={jobType}>
|
||||
{DETAIL_SCOPE_LABELS[jobType]}
|
||||
{defectType === "ALL" ? "" : ` / ${defectTypeLabel}`}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -374,7 +413,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<div className="flex shrink-0 items-center justify-between gap-4 py-3">
|
||||
<div className="flex min-w-0 items-baseline gap-3">
|
||||
<h2 className="shrink-0 text-[1.25rem] font-semibold text-title">
|
||||
{SAMPLE_DISTRICT_NAME} 처리분구 {definition.defectGroupTitle}조사 집계표
|
||||
{tableTitle}
|
||||
</h2>
|
||||
<span className="truncate text-[1.05rem] text-label">
|
||||
조회 결과 총 {rows.length.toLocaleString("ko-KR")}건
|
||||
@@ -399,7 +438,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<col key={column.id} style={{ width: `${column.width}px` }} />
|
||||
))}
|
||||
{Array.from({
|
||||
length: (definition.defectColumns.length + 1)
|
||||
length: (visibleDefectColumns.length + 1)
|
||||
* EXMN_DETAIL_SEVERITIES.length,
|
||||
}).map((_, index) => (
|
||||
<col key={`severity-col-${index}`} style={{ width: `${SEVERITY_COLUMN_WIDTH}px` }} />
|
||||
@@ -410,6 +449,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
{FIXED_COLUMNS.map((column, columnIndex) => {
|
||||
const sticky = columnIndex < STICKY_COLUMN_COUNT;
|
||||
const direction = sort?.key === column.id ? sort.direction : undefined;
|
||||
const columnLabel =
|
||||
column.id === "pipeNo" && jobType === "MNHL"
|
||||
? "맨홀\n번호"
|
||||
: column.label;
|
||||
return (
|
||||
<th
|
||||
key={column.id}
|
||||
@@ -435,10 +478,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-full w-full items-center justify-center gap-1.5"
|
||||
aria-label={`${column.label.replace("\n", " ")} 정렬`}
|
||||
aria-label={`${columnLabel.replace("\n", " ")} 정렬`}
|
||||
onClick={() => handleSortChange(column.id)}
|
||||
>
|
||||
<span className="whitespace-pre-line">{column.label}</span>
|
||||
<span className="whitespace-pre-line">{columnLabel}</span>
|
||||
{direction === "asc" ? (
|
||||
<span
|
||||
className="inline-flex min-w-2 justify-center text-[8px] leading-none text-[#1d4ed8]"
|
||||
@@ -467,7 +510,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
);
|
||||
})}
|
||||
<th
|
||||
colSpan={(definition.defectColumns.length + 1) * EXMN_DETAIL_SEVERITIES.length}
|
||||
colSpan={(visibleDefectColumns.length + 1) * EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border bg-[#eaf3fb] px-2 py-2"
|
||||
>
|
||||
이상항목
|
||||
@@ -481,10 +524,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
합계
|
||||
</th>
|
||||
<th
|
||||
colSpan={definition.defectColumns.length * EXMN_DETAIL_SEVERITIES.length}
|
||||
colSpan={visibleDefectColumns.length * EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border bg-[#eaf3fb] px-2 py-2"
|
||||
>
|
||||
{definition.defectGroupTitle}
|
||||
{visibleDefectGroupTitle}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -497,19 +540,19 @@ export function ExmnSurveyDetailPopupPage({
|
||||
{SEVERITY_LABELS[severity]}
|
||||
</th>
|
||||
))}
|
||||
{definition.defectColumns.map((column) => (
|
||||
{visibleDefectColumns.map((column) => (
|
||||
<th
|
||||
key={column.code}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
title={`${column.label} (${column.code})`}
|
||||
className="border-b border-r border-border bg-[#eaf3fb] px-2 py-2 whitespace-nowrap"
|
||||
>
|
||||
{column.label}
|
||||
{column.label} ({column.code})
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{definition.defectColumns.flatMap((column) =>
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<th
|
||||
key={`${column.code}-${severity}`}
|
||||
@@ -524,8 +567,13 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
<>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.reportNo} className="sn-popup-grid-row sn-popup-grid-row-hover">
|
||||
{rows.map((row) => {
|
||||
const visibleAnomalyTotals = getVisibleAnomalyTotals(
|
||||
row,
|
||||
visibleDefectColumns,
|
||||
);
|
||||
return (
|
||||
<tr key={row.reportNo} className="sn-popup-grid-row sn-popup-grid-row-hover">
|
||||
{FIXED_COLUMNS.map((column, columnIndex) => {
|
||||
const sticky = columnIndex < STICKY_COLUMN_COUNT;
|
||||
return (
|
||||
@@ -552,10 +600,10 @@ export function ExmnSurveyDetailPopupPage({
|
||||
key={`${row.reportNo}-total-${severity}`}
|
||||
className="border-b border-r border-border bg-background px-2 py-2.5"
|
||||
>
|
||||
{formatCount(row.anomalyTotals[severity])}
|
||||
{formatCount(visibleAnomalyTotals[severity])}
|
||||
</td>
|
||||
))}
|
||||
{definition.defectColumns.flatMap((column) =>
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`${row.reportNo}-${column.code}-${severity}`}
|
||||
@@ -565,8 +613,9 @@ export function ExmnSurveyDetailPopupPage({
|
||||
</td>
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="bg-background font-medium">
|
||||
<td
|
||||
colSpan={STICKY_COLUMN_COUNT}
|
||||
@@ -607,7 +656,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
{formatCount(defectSummary.anomalySubtotals[severity])}
|
||||
</td>
|
||||
))}
|
||||
{definition.defectColumns.flatMap((column) =>
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`subtotal-${column.code}-${severity}`}
|
||||
@@ -658,7 +707,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
>
|
||||
{formatCount(defectSummary.anomalyTotal)}
|
||||
</td>
|
||||
{definition.defectColumns.map((column) => (
|
||||
{visibleDefectColumns.map((column) => (
|
||||
<td
|
||||
key={`total-${column.code}`}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
@@ -673,7 +722,7 @@ export function ExmnSurveyDetailPopupPage({
|
||||
<tr>
|
||||
<td
|
||||
colSpan={FIXED_COLUMNS.length
|
||||
+ (definition.defectColumns.length + 1)
|
||||
+ (visibleDefectColumns.length + 1)
|
||||
* EXMN_DETAIL_SEVERITIES.length}
|
||||
className="sn-popup-empty-cell h-28 border-b border-r border-border text-center"
|
||||
>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const windowPopupDefinitions = {
|
||||
title: "조사 상세현황",
|
||||
width: 1500,
|
||||
height: 720,
|
||||
requiredParams: ["exmnBizId", "jobType"],
|
||||
requiredParams: ["exmnBizId", "trtdstId", "trtdstNm", "jobType"],
|
||||
},
|
||||
oldPpUpload: {
|
||||
key: "oldPpUpload",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { ExmnBusiness, ExmnJobType } from "@/features/exmn/model/exmn-business-types";
|
||||
import type { ExmnDetailDefectType } from "@/features/exmn/model/exmn-detail-table";
|
||||
import { ExmnSurveyDetailPopupPage } from "@/features/exmn/ui/ExmnSurveyDetailPopupPage";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
@@ -13,6 +14,16 @@ const getParam = (value: string | string[] | undefined) =>
|
||||
const getJobType = (value: string | string[] | undefined): ExmnJobType =>
|
||||
getParam(value) === "MNHL" ? "MNHL" : "PPL";
|
||||
|
||||
/** 결함 건수 링크가 전달한 구조·운영 구분을 복원하며 직접 진입은 통합 보기로 처리한다. */
|
||||
const getDefectType = (
|
||||
value: string | string[] | undefined,
|
||||
): ExmnDetailDefectType => {
|
||||
const defectType = getParam(value);
|
||||
return defectType === "STRUCTURAL" || defectType === "OPERATIONAL"
|
||||
? defectType
|
||||
: "ALL";
|
||||
};
|
||||
|
||||
/** 부모 조사사업 목록의 선택 행 정보를 팝업 상단 표시 계약으로 복원한다. */
|
||||
const buildBusinessSummary = (
|
||||
searchParams: WindowPopupContentProps["searchParams"],
|
||||
@@ -46,7 +57,10 @@ export default function ExmnSurveyDetailPopupContent({
|
||||
return (
|
||||
<ExmnSurveyDetailPopupPage
|
||||
business={buildBusinessSummary(searchParams)}
|
||||
trtdstId={getParam(searchParams.trtdstId)}
|
||||
trtdstNm={getParam(searchParams.trtdstNm)}
|
||||
jobType={getJobType(searchParams.jobType)}
|
||||
defectType={getDefectType(searchParams.defectType)}
|
||||
sampleMode={getParam(searchParams.sample) === "Y"}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user