diff --git a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/001-qa-SFR-010-004~007-조사사업-통합검증계획.md b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/001-qa-SFR-010-004~007-조사사업-통합검증계획.md index 356f5852b..2348e90df 100644 --- a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/001-qa-SFR-010-004~007-조사사업-통합검증계획.md +++ b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/001-qa-SFR-010-004~007-조사사업-통합검증계획.md @@ -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` 진입이 선행조건이다. +- 개발 서버가 실행 중이면 별도 프로덕션 빌드를 실행하지 않고 정적 검사와 사용자 화면 확인을 우선한다. diff --git a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md index 6580e124e..94b41df03 100644 --- a/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md +++ b/apps/sn/docs/작업검증/SFR-010-004~SFR-010-007-조사사업/002-qa-SFR-010-004~007-조사사업-통합검증결과.md @@ -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. 흡수한 기존 문서 - 없음 diff --git a/apps/sn/docs/작업계획/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-작업카드.md b/apps/sn/docs/작업계획/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-작업카드.md index 493f4c80b..257b14e92 100644 --- a/apps/sn/docs/작업계획/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-작업카드.md +++ b/apps/sn/docs/작업계획/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-작업카드.md @@ -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` 화면, 코드 헤더, 필터 결과와 소계·합계가 일치하는 것이다. diff --git a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md index c25bf7349..9ca267fe6 100644 --- a/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md +++ b/apps/sn/docs/작업내용/SFR-010-004~SFR-010-007-조사사업/001-fe-SFR-010-004~007-조사사업-통합구현기록.md @@ -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 코드별 건수 계약을 사용해야 한다. diff --git a/apps/sn/src/features/exmn/model/exmn-detail-table.ts b/apps/sn/src/features/exmn/model/exmn-detail-table.ts index 1e8beed87..681000074 100644 --- a/apps/sn/src/features/exmn/model/exmn-detail-table.ts +++ b/apps/sn/src/features/exmn/model/exmn-detail-table.ts @@ -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; /** 화면 열과 추후 JSONB 코드 매핑이 같은 식별자를 사용하도록 하는 이상항목 정의다. */ @@ -25,8 +26,6 @@ export interface ExmnSurveyDetailRow { specification: number; extensionM: number; drivingDistanceM: number; - structuralInternalDefect: number; - operationalInternalDefect: number; anomalyTotals: ExmnSeverityCounts; defectCounts: Record; } diff --git a/apps/sn/src/features/exmn/model/exmn-sample-data.ts b/apps/sn/src/features/exmn/model/exmn-sample-data.ts index 5230064dc..a10a115a6 100644 --- a/apps/sn/src/features/exmn/model/exmn-sample-data.ts +++ b/apps/sn/src/features/exmn/model/exmn-sample-data.ts @@ -239,8 +239,6 @@ export const getExmnSampleSurveyDetails = ( drainageType: "합류", pipeType: "HP", specification: 600, - structuralInternalDefect: rowIndex === 1 ? 3 : 1, - operationalInternalDefect: 1, anomalyTotals, defectCounts, }; diff --git a/apps/sn/src/features/exmn/ui/ExmnBusinessRegisterPopupPage.tsx b/apps/sn/src/features/exmn/ui/ExmnBusinessRegisterPopupPage.tsx index e0640dfca..0ed3f3597 100644 --- a/apps/sn/src/features/exmn/ui/ExmnBusinessRegisterPopupPage.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnBusinessRegisterPopupPage.tsx @@ -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([]); + /** 등록 후보가 많을 때 처리분구명으로 화면 목록을 좁히는 검색어다. */ + const [districtKeyword, setDistrictKeyword] = useState(""); const [selectedDistrictIds, setSelectedDistrictIds] = useState([]); const [savedBusiness, setSavedBusiness] = useState(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 ( -
+
-
-

사업정보 등록

+
+

사업정보 등록

- - - + + +
-
+
{notice && (
{notice.text}
)} -
-
사업정보 입력
+
+
사업정보 입력
-
-
-

사업대상 처리분구

- 가나다순 · {selectedDistrictIds.length}개 선택 +
+
+

사업대상 처리분구

+ setDistrictKeyword(event.target.value)} + /> +
+ + + + {selectedDistrictIds.length}개 선택 + +
{districts.length === 0 ? (

시도·시군구를 선택하거나 등록된 활성 처리분구를 확인해주세요.

- ) : districts.map((district) => ( + ) : filteredDistricts.length === 0 ? ( +

검색 조건에 맞는 처리분구가 없습니다.

+ ) : filteredDistricts.map((district) => (
-
-
+ + ); } diff --git a/apps/sn/src/features/exmn/ui/ExmnResultRegisterPopupPage.tsx b/apps/sn/src/features/exmn/ui/ExmnResultRegisterPopupPage.tsx index e141f1a27..09beb9f67 100644 --- a/apps/sn/src/features/exmn/ui/ExmnResultRegisterPopupPage.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnResultRegisterPopupPage.tsx @@ -294,16 +294,25 @@ export function ExmnResultRegisterPopupPage({ }; return ( -
+
-
-

조사 결과 업로드

+
+

조사 결과 업로드

- +
-
+
{notice && (
-
- 사업차수 - {detail?.bizCycl ?? businessSummary.bizCycl ?? "-"}차 -
-
- 사업년도 - {detail?.bizYr ?? businessSummary.bizYr ?? "-"} -
-
- 행정구역 - {detail?.ctpvNm ?? businessSummary.ctpvNm} {detail?.sggNm ?? businessSummary.sggNm} -
-
- 사업명 - {detail?.bizNm ?? businessSummary.bizNm ?? "-"} +
+
+ 사업차수 + + {detail?.bizCycl ?? businessSummary.bizCycl ?? "-"}차 + +
+
+ 사업년도 + + {detail?.bizYr ?? businessSummary.bizYr ?? "-"} + +
+
+ 행정구역 + + {detail?.ctpvNm ?? businessSummary.ctpvNm} {detail?.sggNm ?? businessSummary.sggNm} + +
+
+ 사업명 + + {detail?.bizNm ?? businessSummary.bizNm ?? "-"} + +
-
+
파일 구분선택 파일찾기데이터 검토 @@ -351,7 +376,7 @@ export function ExmnResultRegisterPopupPage({ return (
{validation && ( -
+

데이터 검토 결과

@@ -416,7 +441,7 @@ export function ExmnResultRegisterPopupPage({ )} {uploadStatus &&

{uploadStatus}

} -
-
+ + ); } diff --git a/apps/sn/src/features/exmn/ui/ExmnStatusDetailPanel.tsx b/apps/sn/src/features/exmn/ui/ExmnStatusDetailPanel.tsx index 45d874b0e..96a5302b8 100644 --- a/apps/sn/src/features/exmn/ui/ExmnStatusDetailPanel.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnStatusDetailPanel.tsx @@ -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 { + trtdstId: string; rowNumber: string | number; trtdstNm: string; trgtPrlgText: string; @@ -108,6 +110,8 @@ export function ExmnStatusDetailPanel({ business, sampleMode }: ExmnStatusDetail const [jobType, setJobType] = useState("PPL"); const [summarySort, setSummarySort] = useState(); const [summaryRows, setSummaryRows] = useState([]); + /** 상세보기 팝업에 전달할 현재 선택 처리분구다. */ + const [selectedTrtdstId, setSelectedTrtdstId] = useState(null); const [isLoading, setIsLoading] = useState(false); const [notice, setNotice] = useState(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 ))}
- + + + ); + })} + + ); + })} ) : ( diff --git a/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx b/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx index 0b93a3aaf..3f3cb5212 100644 --- a/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx +++ b/apps/sn/src/features/exmn/ui/ExmnSurveyDetailPopupPage.tsx @@ -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 = { MNHL: "맨홀", }; -/** 백엔드 처리분구 목록 연동 전 화면 검증에 사용하는 처리분구 선택값이다. */ -const SAMPLE_DISTRICT_NAME = "덕풍"; +const DEFECT_TYPE_LABELS: Record = { + 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(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>; 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, }; - }, [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 (
-

{definition.title}

+

{popupTitle}

{notice && ( @@ -310,10 +346,10 @@ export function ExmnSurveyDetailPopupPage({
처리분구
@@ -324,7 +360,10 @@ export function ExmnSurveyDetailPopupPage({ defaultValue={jobType} aria-label="조사대상" > - + @@ -374,7 +413,7 @@ export function ExmnSurveyDetailPopupPage({

- {SAMPLE_DISTRICT_NAME} 처리분구 {definition.defectGroupTitle}조사 집계표 + {tableTitle}

조회 결과 총 {rows.length.toLocaleString("ko-KR")}건 @@ -399,7 +438,7 @@ export function ExmnSurveyDetailPopupPage({ ))} {Array.from({ - length: (definition.defectColumns.length + 1) + length: (visibleDefectColumns.length + 1) * EXMN_DETAIL_SEVERITIES.length, }).map((_, index) => ( @@ -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 ( handleSortChange(column.id)} > - {column.label} + {columnLabel} {direction === "asc" ? ( 이상항목 @@ -481,10 +524,10 @@ export function ExmnSurveyDetailPopupPage({ 합계 - {definition.defectGroupTitle} + {visibleDefectGroupTitle} @@ -497,19 +540,19 @@ export function ExmnSurveyDetailPopupPage({ {SEVERITY_LABELS[severity]} ))} - {definition.defectColumns.map((column) => ( + {visibleDefectColumns.map((column) => ( - {column.label} + {column.label} ({column.code}) ))} - {definition.defectColumns.flatMap((column) => + {visibleDefectColumns.flatMap((column) => EXMN_DETAIL_SEVERITIES.map((severity) => ( {rows.length > 0 ? ( <> - {rows.map((row) => ( - + {rows.map((row) => { + const visibleAnomalyTotals = getVisibleAnomalyTotals( + row, + visibleDefectColumns, + ); + return ( + {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])} ))} - {definition.defectColumns.flatMap((column) => + {visibleDefectColumns.flatMap((column) => EXMN_DETAIL_SEVERITIES.map((severity) => ( )), )} - - ))} + + ); + })} ))} - {definition.defectColumns.flatMap((column) => + {visibleDefectColumns.flatMap((column) => EXMN_DETAIL_SEVERITIES.map((severity) => ( {formatCount(defectSummary.anomalyTotal)} - {definition.defectColumns.map((column) => ( + {visibleDefectColumns.map((column) => ( diff --git a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts index 181cb2569..3caca3772 100644 --- a/apps/sn/src/features/windowPopup/model/popupDefinitions.ts +++ b/apps/sn/src/features/windowPopup/model/popupDefinitions.ts @@ -19,7 +19,7 @@ export const windowPopupDefinitions = { title: "조사 상세현황", width: 1500, height: 720, - requiredParams: ["exmnBizId", "jobType"], + requiredParams: ["exmnBizId", "trtdstId", "trtdstNm", "jobType"], }, oldPpUpload: { key: "oldPpUpload", diff --git a/apps/sn/src/features/windowPopup/ui/ExmnSurveyDetailPopupContent.tsx b/apps/sn/src/features/windowPopup/ui/ExmnSurveyDetailPopupContent.tsx index c1494ebe4..49d3ad46f 100644 --- a/apps/sn/src/features/windowPopup/ui/ExmnSurveyDetailPopupContent.tsx +++ b/apps/sn/src/features/windowPopup/ui/ExmnSurveyDetailPopupContent.tsx @@ -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 ( );