[as] WIP: 기본계획조회 다이얼로그 구현
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
# 화면설계서: 이전기본계획조회 (기본 계획 찾기)
|
||||
|
||||
## 1. 기본 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ---------- | --------------------------------------------------------------- |
|
||||
| 버전 | 0.2 |
|
||||
| 프로세스명 | UI설계 |
|
||||
| 화면명 | 이전기본계획조회 (기본 계획 찾기) |
|
||||
| 화면 유형 | [v] 조회 [ ] 입력 [ ] 수정 [ ] 삭제 [ ] 출력 |
|
||||
| 메뉴 경로 | 하수도기본계획 등록·수정 > 사업개요 > [이전기본계획조회] Dialog |
|
||||
|
||||
---
|
||||
|
||||
## 2. 화면 목적
|
||||
|
||||
하수도기본계획 등록·수정 화면에서 **이전(기수립) 기본계획** 수치와 관련 보고서를 **조회·다운로드**한다. 시도·구군·연도·처리시설을 선택한 뒤 총설·처리구역별 현황을 표시하고, 분류 트리에서 기본계획 파일을 내려받는다. **부모 사업개요 폼에는 수치를 반영하지 않는다**(레거시 `bscPlnPopup.jsp`와 동일).
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면 구성
|
||||
|
||||
### 3-1. 전체 레이아웃
|
||||
|
||||
부모: 하수도기본계획 등록/수정 폼 → 사업개요 섹션의 `[이전기본계획조회]` 버튼으로 Dialog 오픈.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Dialog 타이틀: 기본 계획 찾기 [닫기] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 A] 검색 필터 │
|
||||
│ 시도 ▼ 구군 ▼ 연도 ▼ 처리시설 ▼ [조회] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 B] 기본계획파일 │
|
||||
│ ┌──────────────────────┬────────────────────────────────────────────────────┐ │
|
||||
│ │ [영역 B-1] 분류 트리 │ [영역 B-2] 파일 목록 │ │
|
||||
│ │ (약 40%) │ (약 60%) │ │
|
||||
│ │ └ 노드… │ 파일명 / 다운로드 │ │
|
||||
│ └──────────────────────┴────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 C] 총설 │
|
||||
│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
|
||||
│ │ 구분 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ 기준 │ 1단계 │ 2단계 │ 3단계 │ 4단계 │ │
|
||||
│ ├────────┼───────┼───────┼───────┼───────┼────────┼───────┼───────┼───────┼───────┤ │
|
||||
│ │계획목표│ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ 년 │ │
|
||||
│ │년도 │ │ │ │ │ │ │ │ │ │ │
|
||||
│ └────────┴───────┴───────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┘ │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ [영역 D] 처리구역별 현황 │
|
||||
│ ┌────────┬──────────────당초──────────────┬──기준──┬──────────────변경──────────────┐ │
|
||||
│ │ 구분 │ 1~4단계(+연도헤더) │ 기준 │ 1~4단계(+연도헤더) │ │
|
||||
│ ├────────┼────────────────────────────────┼────────┼────────────────────────────────┤ │
|
||||
│ │ 하수처리인구 … 배제방식 (읽기 전용 행) │ │
|
||||
│ └────────┴────────────────────────────────┴────────┴────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3-2. 화면 데이터 흐름
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
// 부모 폼 — 사업개요
|
||||
ON [이전기본계획조회] 버튼 CLICK DO
|
||||
OPEN PreviousPlanLookupDialog
|
||||
// 프리필: 부모 폼 sewerBscPlan.sido / gugun / admdstCd
|
||||
CALL findSidoCombo()
|
||||
DISPLAY 시도 옵션 (영역 A)
|
||||
IF 시도 프리필 THEN
|
||||
CALL findGugunCombo(sidoCd)
|
||||
DISPLAY 구군 옵션 (영역 A)
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
|
||||
// TBD: useLvNowDmgMtncGnsvy enabled — level별 필수 파라미터만 검사하도록 조정
|
||||
IF 구군 프리필 THEN
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
|
||||
DISPLAY 연도 옵션 (영역 A) // children 중 level=3 노드의 gnsvyYr
|
||||
CALL getClsfSystm({ clsfSystmNm: gugunNm })
|
||||
DISPLAY 분류 트리 (영역 B-1)
|
||||
END IF
|
||||
END IF
|
||||
END ON
|
||||
|
||||
// 영역 A: 시도 변경
|
||||
ON 영역 A 시도 CHANGE DO
|
||||
RESET 구군 / 연도 / 처리시설
|
||||
CALL findGugunCombo(sidoCd)
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "1", ctpvNm })
|
||||
// 구군 미선택 → 연도·처리시설 옵션 비움, 트리 빈 안내
|
||||
END ON
|
||||
|
||||
// 영역 A: 구군 변경
|
||||
ON 영역 A 구군 CHANGE DO
|
||||
RESET 연도 / 처리시설
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })
|
||||
DISPLAY 연도 옵션 (영역 A) // level=3 노드 → gnsvyYr
|
||||
CALL getClsfSystm({ clsfSystmNm: sggNm })
|
||||
DISPLAY 분류 트리 (영역 B-1)
|
||||
END ON
|
||||
|
||||
// 영역 A: 연도 변경
|
||||
ON 영역 A 연도 CHANGE DO
|
||||
RESET 처리시설
|
||||
CALL getLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })
|
||||
DISPLAY 처리시설 옵션 (영역 A) // level=3 노드 children → swtrmtFcltCd / swtrmtFcltNm
|
||||
END ON
|
||||
|
||||
// 영역 A: 조회
|
||||
ON [조회] 버튼 CLICK DO
|
||||
VALIDATE 시도, 구군, 연도(targetYear), 처리시설(swtrmtFcltCd)
|
||||
CALL getNowDmgMtncGnsvy({ swtrmtFcltCd, ctpvNm, sggNm, targetYear })
|
||||
DISPLAY 총설 (영역 C) // 응답[0]의 planYr / planYrDt* / planYrDc*
|
||||
DISPLAY 처리구역별 현황 (영역 D) // 응답[0]의 지표 Dt1~4 / base / Dc1~4
|
||||
END ON
|
||||
|
||||
// 영역 B: 트리 노드 선택
|
||||
ON 영역 B-1 노드 CLICK DO
|
||||
CALL getFile({ clsfSystmSn, sort, vetPrcsYn }) // Response DTO TBD
|
||||
DISPLAY 파일 목록 (영역 B-2)
|
||||
END ON
|
||||
|
||||
// 영역 B: 파일 다운로드
|
||||
ON 영역 B-2 파일 CLICK DO
|
||||
CALL getFilePrcs(fileSn) // 파일 메타(경로·제목) — Response TBD
|
||||
CALL fileDownload({ filePath, fileName })
|
||||
// 브라우저 다운로드 트리거
|
||||
END ON
|
||||
|
||||
ON [닫기] DO
|
||||
CLOSE PreviousPlanLookupDialog
|
||||
// 부모 폼 값 변경 없음
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 영역 A — 검색 필터
|
||||
|
||||
구성: **시도 · 구군 · 연도 · 처리시설** (+ 조회).
|
||||
|
||||
- 시도·구군: `adstdstr` 콤보 (기존 스펙 유지)
|
||||
- 연도·처리시설: 각각 `useLvNowDmgMtncGnsvy`를 **별도 호출**. `level`에 따라 반환 계층이 달라진다.
|
||||
|
||||
### 4-1. 필드 정의
|
||||
|
||||
| 표시명 | 입력 유형 | 필수 | 기본값 | API 파라미터 매핑 | 비고 |
|
||||
| -------- | --------- | ---- | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| 시도 | Select | Y | 부모 `sewerBscPlan.sido` | `ctpvNm`(표시명), 코드는 `adstDstrCd` | `useFindSidoCombo` |
|
||||
| 구군 | Select | Y | 부모 `sewerBscPlan.gugun` | `sggNm`(표시명), 코드는 `adstDstrCd` | `useFindGugunCombo(sidoCd)` |
|
||||
| 연도 | Select | Y | 없음 | `gnsvyYr` / 조회 시 `targetYear` | `useLvNowDmgMtncGnsvy({ level: "2", ctpvNm, sggNm })` |
|
||||
| 처리시설 | Select | Y | 없음 | `swtrmtFcltCd` ← 옵션 value, 라벨 `swtrmtFcltNm` | `useLvNowDmgMtncGnsvy({ level: "3", ctpvNm, sggNm, gnsvyYr })` |
|
||||
|
||||
> 처리시설은 `bns-if` `useFacilityList`를 **사용하지 않는다**.
|
||||
|
||||
### 4-2. 버튼
|
||||
|
||||
| 버튼명 | 동작 |
|
||||
| ------ | -------------------------------------------------------- |
|
||||
| 조회 | 필수값 검증 후 `getNowDmgMtncGnsvy` 호출 → 영역 C·D 갱신 |
|
||||
| 닫기 | Dialog 종료 (부모 미반영) |
|
||||
|
||||
### 4-3. `lvNowDmgMtncGnsvy` level 캐스케이드
|
||||
|
||||
| 트리거 | Request `level` | Request 파라미터 | UI에 쓰는 응답 | 옵션 추출 |
|
||||
| --------- | --------------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------ |
|
||||
| 시도 선택 | `"1"` | `ctpvNm` | (하위 초기화용) | 연도·처리시설 Select 리셋. 구군은 `findGugunCombo` |
|
||||
| 구군 선택 | `"2"` | `ctpvNm`, `sggNm` | 연도 Select | 트리에서 `level === 3` 노드의 `gnsvyYr` |
|
||||
| 연도 선택 | `"3"` | `ctpvNm`, `sggNm`, `gnsvyYr` | 처리시설 Select | 선택 연도(`level === 3`) 노드의 `children[]` → `swtrmtFcltCd` / `swtrmtFcltNm` |
|
||||
|
||||
상위 변경 시 하위 선택값·옵션을 RESET한다 (시도→구군·연도·시설 / 구군→연도·시설 / 연도→시설).
|
||||
|
||||
---
|
||||
|
||||
## 5. 영역 B — 기본계획파일
|
||||
|
||||
### 5-1. 트리 (B-1)
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | --------------- | ------------------------ |
|
||||
| 분류명 | `clsfSystmNm` | `ClsfSystmResponseDto` |
|
||||
| 노드키 | `clsfSystmSn` | 선택 시 파일 목록 조회 |
|
||||
| 상위키 | `upClsfSystmSn` | 클라이언트에서 트리 조립 |
|
||||
| 정렬 | `sortSeq` | |
|
||||
| 경로 | `path` | |
|
||||
|
||||
- 조회 파라미터: `clsfSystmNm` = 선택 구군명(레거시와 동일). `upClsfSystmSn`은 하위 전개 시 TBD.
|
||||
- 시도·구군 미선택 시: 「시도와 구군을 선택하세요.」 안내.
|
||||
|
||||
### 5-2. 파일 목록 (B-2)
|
||||
|
||||
| 표시명 | Field | 비고 |
|
||||
| ------ | ----- | ------------------------------------------------------------------- |
|
||||
| 파일명 | TBD | `getFile` Response DTO가 `unknown` — 백엔드 스키마 확정 후 매핑 |
|
||||
| (기타) | TBD | 레거시: 트리 선택 후 목록 표시, 클릭 시 `fileprcs` → `fileDownload` |
|
||||
|
||||
### 5-3. 버튼·상호작용
|
||||
|
||||
| 동작 | 설명 |
|
||||
| -------------- | -------------------------------------------------------------- |
|
||||
| 트리 노드 클릭 | `getFile({ clsfSystmSn, … })` → B-2 목록 |
|
||||
| 파일 클릭 | `getFilePrcs(fileSn)` → `fileDownload({ filePath, fileName })` |
|
||||
| 빈 목록 | 「트리에서 항목을 선택하세요.」 / 「파일이 없습니다.」 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 영역 C — 총설
|
||||
|
||||
### 6-1. 테이블 구조
|
||||
|
||||
| 행(구분) | 열 매핑 (당초1~~4 / 기준 / 변경1~~4) |
|
||||
| ------------ | ------------------------------------------------------------------------------ |
|
||||
| 계획목표년도 | `planYrDt1planYrDt4` ~~/~~ `planYr` ~~/~~ `planYrDc1planYrDc4` (+ `년` 접미사) |
|
||||
|
||||
- 헤더: 당초(1~~4단계) · 기준 · 변경(1~~4단계)
|
||||
- 데이터 소스: `getNowDmgMtncGnsvy` 응답 배열의 **첫 번째** 요소
|
||||
- 읽기 전용
|
||||
|
||||
---
|
||||
|
||||
## 7. 영역 D — 처리구역별 현황
|
||||
|
||||
### 7-1. 행 정의 (표시명 → Field 접두사)
|
||||
|
||||
각 행은 `Dt1Dt4` ~~/ (접미사 없음=기준) /~~ `Dc1Dc4` 셀로 표시한다. 숫자는 천단위 콤마, 없으면 `-`.
|
||||
|
||||
| 표시명 | Field 접두사 | 비고 |
|
||||
| ------------------------ | -------------------- | ---------------------------------------------- |
|
||||
| 하수처리인구(인) | `swtrmtPpltnCnt` | |
|
||||
| 자연적하수처리인구수(인) | `natrSwtrmtPpltnCnt` | |
|
||||
| 사회적하수처리인구수(인) | `scilSwtrmtPpltnCnt` | |
|
||||
| 하수처리구역면적 | `swtrmtZoneArea` | |
|
||||
| 물사용량원단위 | `wtrUsqtyOrgnUnit` | |
|
||||
| 오수전환율(%) | `drtwSwtcRt` | |
|
||||
| 변동부하율(%) | `flctnLodRt` | |
|
||||
| 오수원단위 | `drtwOrgnUnit` | |
|
||||
| 계획하수량합계(㎥/일) | `planSwageQntSum` | |
|
||||
| 생활오수(㎥/일) | `lvngDrtwSum` | |
|
||||
| 지하수(㎥/일) | `ugwtSum` | |
|
||||
| 공장폐수(㎥/일) | `fctryWwtSum` | |
|
||||
| 관광오수(㎥/일) | `toursmDrtwSum` | |
|
||||
| 군부대(㎥/일) | `mtbsSum` | |
|
||||
| 연계처리수(㎥/일) | `linkPrcsCntSum` | |
|
||||
| 기타오수(㎥/일) | `etcDrtwSum` | |
|
||||
| 지하수사용량(㎥/일) | `ugwtUseSum` | |
|
||||
| 시설용량(㎥/일) | `fcltCpctSum` | |
|
||||
| 배제방식 | `exclsMethExpln` | 문자열; 단계별 `*Dt*`/`*Dc*` 존재 시 동일 규칙 |
|
||||
|
||||
헤더 3행째 연도: 영역 C와 동일하게 `planYrDt*` / `planYr` / `planYrDc*` 표시.
|
||||
|
||||
---
|
||||
|
||||
## 8. API/DTO 명세
|
||||
|
||||
### 8-1. 공통
|
||||
|
||||
| 항목 | 값 |
|
||||
| --------------- | ---------------------------------------------------- |
|
||||
| DC Base | `/dc-service/api/v1` (`ENDPOINT.DC_SERVICE`) |
|
||||
| AS adstdstr | `/as-service/api/v1/adstdstr` |
|
||||
| 클라이언트 모듈 | `@/entities/dc` (`dcService`), `@/entities/adstdstr` |
|
||||
|
||||
> 영역 A 처리시설은 `bns-if`를 사용하지 않음.
|
||||
|
||||
### 8-2. 시도·구군 콤보 (non-DC)
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| -------- | ------ | ----------------------------------- | ------------------------ | -------------------------------- | ---------------------------------------------------------- |
|
||||
| 시도목록 | GET | `/as-service/api/v1/adstdstr/sido` | — | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findSidoCombo` / `useFindSidoCombo` |
|
||||
| 구군목록 | GET | `/as-service/api/v1/adstdstr/gugun` | Query: `sido` (시도코드) | `CmCdAdstDstrComboResponseDto[]` | `cmCdAdstDstrService.findGugunCombo` / `useFindGugunCombo` |
|
||||
|
||||
### 8-3. 연도·처리시설 계층 조회 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| --------------- | ------ | -------------------------------------- | ---------------------------- | -------------------------------- | --------------------------------------------------------- |
|
||||
| 계층형 계획목록 | GET | `/dc-service/api/v1/lvNowDmgMtncGnsvy` | `LvNowDmgMtncGnsvySearchDto` | `LvNowDmgMtncGnsvyResponseDto[]` | `dcService.getLvNowDmgMtncGnsvy` / `useLvNowDmgMtncGnsvy` |
|
||||
|
||||
연도 Select·처리시설 Select는 **각각** `useLvNowDmgMtncGnsvy`를 호출한다 (`level`만 다르게).
|
||||
|
||||
**Request (**`LvNowDmgMtncGnsvySearchDto`**)**
|
||||
|
||||
| 필드 | 시도(`level=1`) | 구군→연도(`level=2`) | 연도→시설(`level=3`) |
|
||||
| --------- | --------------- | -------------------- | -------------------- |
|
||||
| `level` | `"1"` | `"2"` | `"3"` |
|
||||
| `ctpvNm` | 필수 | 필수 | 필수 |
|
||||
| `sggNm` | — | 필수 | 필수 |
|
||||
| `gnsvyYr` | — | — | 필수 |
|
||||
|
||||
**Response 노드 필드 (**`LvNowDmgMtncGnsvyResponseDto`**, 부분 optional)**
|
||||
|
||||
| 필드 | 등장 level | 설명 |
|
||||
| -------------- | ---------- | ------------------------ |
|
||||
| `level` | 1~3 | 계층 깊이 |
|
||||
| `ctpvNm` | 1 | 시도명 |
|
||||
| `sggNm` | 2 | 구군명 |
|
||||
| `gnsvyYr` | 3 | 연도 → 연도 Select value |
|
||||
| `swtrmtFcltCd` | 시설(리프) | 처리시설 코드 |
|
||||
| `swtrmtFcltNm` | 시설(리프) | 처리시설명 |
|
||||
| `children` | 1~3 | 하위 노드 배열 |
|
||||
|
||||
> **TBD**: 현재 `useLvNowDmgMtncGnsvy` `enabled`가 `level && ctpvNm && sggNm && gnsvyYr`로 고정되어 `level=1`/`2` 호출이 막힘. level별 필수 파라미터만 검사하도록 수정 필요.
|
||||
>
|
||||
> 예: `level=1` → `ctpvNm` / `level=2` → `ctpvNm && sggNm` / `level=3` → `ctpvNm && sggNm && gnsvyYr`
|
||||
|
||||
### 8-4. 기본계획 수치 조회 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ---------------- | ------ | ------------------------------------ | -------------------------- | ------------------------------ | ----------------------------------------------------- |
|
||||
| 현재·단계별 수치 | GET | `/dc-service/api/v1/nowDmgMtncGnsvy` | `NowDmgMtncGnsvySearchDto` | `NowDmgMtncGnsvyResponseDto[]` | `dcService.getNowDmgMtncGnsvy` / `useNowDmgMtncGnsvy` |
|
||||
|
||||
**Request (**`NowDmgMtncGnsvySearchDto`**)**
|
||||
|
||||
| 필드 | 매핑 |
|
||||
| -------------- | ----------------------- |
|
||||
| `swtrmtFcltCd` | 영역 A 처리시설 |
|
||||
| `ctpvNm` | 영역 A 시도 표시명 |
|
||||
| `sggNm` | 영역 A 구군 표시명 |
|
||||
| `targetYear` | 영역 A 연도 (`gnsvyYr`) |
|
||||
|
||||
**Response**: 지표 필드 + `Dt1Dt4` ~~/~~ `Dc1Dc4` 접미사 스테이지 필드 (`NowDmgMtncGnsvyResponseDto`). UI는 `data[0]` 사용.
|
||||
|
||||
### 8-5. 분류체계 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO | Response DTO | API |
|
||||
| ------------ | ------ | ------------------------------ | -------------------- | ------------------------ | ----------------------------------------- |
|
||||
| 분류트리조회 | GET | `/dc-service/api/v1/clsfSystm` | `ClsfSystmParamsDto` | `ClsfSystmResponseDto[]` | `dcService.getClsfSystm` / `useClsfSystm` |
|
||||
|
||||
**Request**: `clsfSystmNm?`, `upClsfSystmSn?`
|
||||
**Response**: `clsfSystmSn`, `upClsfSystmSn`, `clsfSystmNm`, `sortSeq`, `droppAble`, `path`
|
||||
|
||||
> 레거시 경로 `/clsf` → 현재 클라이언트는 `/clsfSystm`.
|
||||
|
||||
### 8-6. 파일 목록·처리·다운로드 — DC
|
||||
|
||||
| 기능 | Method | URL | Request DTO / Params | Response DTO | API |
|
||||
| ------------ | ------ | --------------------------------- | ------------------------------------------------------- | ----------------- | -------------------------------------------- |
|
||||
| 파일목록 | GET | `/dc-service/api/v1/file` | `FileParamsDto` (`sort?`, `clsfSystmSn?`, `vetPrcsYn?`) | `unknown` **TBD** | `dcService.getFile` / `useFile` |
|
||||
| 파일처리정보 | GET | `/dc-service/api/v1/fileprcs` | Query: `fileSn` | `unknown` **TBD** | `dcService.getFilePrcs` / `useFilePrcs` |
|
||||
| 파일다운로드 | POST | `/dc-service/api/v1/fileDownload` | `FileDownloadParamsDto` (`filePath?`, `fileName?`) | `unknown` **TBD** | `dcService.fileDownload` / `useFileDownload` |
|
||||
|
||||
> **TBD**: 파일 관련 Response 스키마·다운로드 blob/헤더 처리·`fileDownload` POST body vs query 전달 방식은 백엔드/게이트웨이 확인 후 확정.
|
||||
|
||||
---
|
||||
|
||||
## 9. UI 구현 가이드
|
||||
|
||||
### 9-1. 기술 스택·원칙
|
||||
|
||||
- UI: `@common/components` — `Dialog`, `Select`, `Button`, `Table`, `Skeleton`
|
||||
- 상태: Dialog 로컬 state + React Query (`entities/dc`, `adstdstr`)
|
||||
- 영역 A 연도·처리시설: `useLvNowDmgMtncGnsvy`를 level별 **두 번**(또는 시도 포함 시 세 번) 호출
|
||||
- 테이블: 읽기 전용 고정 행 → `@tanstack/react-table` / `DataTable` **불필요**
|
||||
- 트리: jsTree 미사용 — 재귀 들여쓰기 트리
|
||||
- 번들: `PreviousPlanLookupDialog`는 outline에서 `next/dynamic` 로드 권장
|
||||
- 부모 폼: **수치 미반영** (조회·다운로드 only)
|
||||
- 1차 구현 시 DC 미연동이면 `@faker-js/faker` mock + TODO → 이후 `dcService` 훅으로 교체
|
||||
|
||||
### 9-2. 앱·파일 배치
|
||||
|
||||
| 구분 | 경로 |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| 진입 버튼 | `pages/sewer-bsc-plan/ui/sections/outline-form-section.tsx` |
|
||||
| Dialog | `pages/sewer-bsc-plan/ui/previous-plan-lookup-dialog.tsx` |
|
||||
| 필터 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-search-filters.tsx` |
|
||||
| 파일 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-file-section.tsx` |
|
||||
| 총설 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-summary-table.tsx` |
|
||||
| 처리구역 | `pages/sewer-bsc-plan/ui/previous-plan/previous-plan-area-table.tsx` |
|
||||
| 셀 매핑 유틸 | `pages/sewer-bsc-plan/lib/previous-plan-table-utils.ts` |
|
||||
| mock(선택) | `pages/sewer-bsc-plan/lib/previous-plan-mock.ts` |
|
||||
| DC entity | `entities/dc/api/dc-service.ts`, `queries.ts`, `model/dc-interface.ts` |
|
||||
| 레거시 참조 | `adssNewOld/.../cmmn/bscPlnPopup.jsp` |
|
||||
|
||||
### 9-3. 영역별 UI 매핑
|
||||
|
||||
| 영역 | 컴포넌트 | 데이터 |
|
||||
| ---- | ------------------------------ | -------------------------------------------------------------- |
|
||||
| A | `previous-plan-search-filters` | adstdstr + `useLvNowDmgMtncGnsvy`(level 1·2·3) |
|
||||
| B | `previous-plan-file-section` | `useClsfSystm` + `useFile` + `useFilePrcs` + `useFileDownload` |
|
||||
| C | `previous-plan-summary-table` | `useNowDmgMtncGnsvy` / 조회 결과 state |
|
||||
| D | `previous-plan-area-table` | 동일 |
|
||||
|
||||
### 9-4. 다이얼로그·라우팅
|
||||
|
||||
| 항목 | 내용 |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| 다이얼로그명 | `PreviousPlanLookupDialog` / 타이틀 `기본 계획 찾기` |
|
||||
| 오픈 | 사업개요 `[이전기본계획조회]` |
|
||||
| 라우트 | 없음 (모달). 부모: `/sewer-bsc-plan/regist`, `/sewer-bsc-plan/[id]/edit` |
|
||||
| 선택 반영 | 없음 |
|
||||
|
||||
### 9-5. 데이터 흐름 (구현 요약)
|
||||
|
||||
```
|
||||
BEGIN
|
||||
|
||||
ON [이전기본계획조회] CLICK DO
|
||||
OPEN PreviousPlanLookupDialog (sido/gugun 프리필)
|
||||
END ON
|
||||
|
||||
ON 시도 CHANGE DO
|
||||
CALL findGugunCombo
|
||||
CALL getLvNowDmgMtncGnsvy(level=1)
|
||||
RESET 연도·처리시설
|
||||
END ON
|
||||
|
||||
ON 구군 CHANGE DO
|
||||
CALL getLvNowDmgMtncGnsvy(level=2) // → 연도 옵션
|
||||
CALL getClsfSystm
|
||||
RESET 처리시설
|
||||
END ON
|
||||
|
||||
ON 연도 CHANGE DO
|
||||
CALL getLvNowDmgMtncGnsvy(level=3) // → 처리시설 옵션
|
||||
END ON
|
||||
|
||||
ON [조회] CLICK DO
|
||||
VALIDATE
|
||||
CALL getNowDmgMtncGnsvy
|
||||
DISPLAY 영역 C · 영역 D
|
||||
END ON
|
||||
|
||||
ON 트리 노드 / 파일 CLICK DO
|
||||
CALL getFile → getFilePrcs → fileDownload // Response TBD
|
||||
END ON
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 미확정 (TBD) 추적
|
||||
|
||||
| ID | 항목 | 비고 |
|
||||
| --- | -------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| T1 | `useLvNowDmgMtncGnsvy` enabled을 level별 필수값으로 분기 | `1`: ctpvNm / `2`: +sggNm / `3`: +gnsvyYr |
|
||||
| T2 | `getFile` / `getFilePrcs` / `fileDownload` Response DTO | 현재 `unknown` |
|
||||
| T3 | 파일 다운로드 UX (blob, Content-Disposition, 게이트웨이) | 레거시 apigateway 직접 호출과 차이 |
|
||||
| T4 | `clsfSystm`의 `upClsfSystmSn` 점진 로드 여부 | flat 목록 vs 계층 API |
|
||||
| T5 | 1차 mock vs 즉시 `dcService` 실연동 | 마이그레이션 플랜은 mock 우선, entity는 준비됨 |
|
||||
| T6 | 부모 폼 수치 반영 | 요구 없음(레거시 동일). 요청 시 별도 스펙 |
|
||||
| T7 | `level=1` 응답 UI 활용 여부 | 시도 선택 시 호출하나 구군은 adstdstr — 응답은 캐시·검증용일 수 있음 |
|
||||
@@ -2,9 +2,11 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmRequestDto,
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
DcService,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
@@ -31,15 +33,39 @@ async function getLvNowDmgMtncGnsvy(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getClsfSystm(params: ClsfSystmRequestDto): Promise<ClsfSystmResponseDto[]> {
|
||||
async function getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]> {
|
||||
const response = await client.get<ClsfSystmResponseDto[]>(`${ENDPOINT.DC_SERVICE}/clsfSystm`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFile(params: FileParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/file`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getFilePrcs(fileSn: string): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileprcs`, {
|
||||
params: { fileSn },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function fileDownload(params: FileDownloadParamsDto): Promise<unknown> {
|
||||
const response = await client.get<unknown>(`${ENDPOINT.DC_SERVICE}/fileDownload`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const dcService = {
|
||||
getNowDmgMtncGnsvy,
|
||||
getLvNowDmgMtncGnsvy,
|
||||
getClsfSystm,
|
||||
getFile,
|
||||
getFilePrcs,
|
||||
fileDownload,
|
||||
} satisfies DcService;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ClsfSystmRequestDto,
|
||||
ClsfSystmParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "../model/dc-interface";
|
||||
@@ -20,17 +21,45 @@ export function useNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto) {
|
||||
});
|
||||
}
|
||||
|
||||
function isLvNowDmgMtncGnsvyEnabled(params: LvNowDmgMtncGnsvySearchDto): boolean {
|
||||
if (!params.level || !params.ctpvNm) return false;
|
||||
if (params.level === "1") return true;
|
||||
if (params.level === "2") return Boolean(params.sggNm);
|
||||
if (params.level === "3") return Boolean(params.sggNm && params.gnsvyYr);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "lvNowDmgMtncGnsvy", params],
|
||||
queryFn: () => dcService.getLvNowDmgMtncGnsvy(params),
|
||||
enabled: Boolean(params.level && params.ctpvNm && params.sggNm && params.gnsvyYr),
|
||||
enabled: isLvNowDmgMtncGnsvyEnabled(params),
|
||||
});
|
||||
}
|
||||
|
||||
export function useClsfSystm(params: ClsfSystmRequestDto) {
|
||||
export function useClsfSystm(params: ClsfSystmParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "clsfSystm", params],
|
||||
queryFn: () => dcService.getClsfSystm(params),
|
||||
// TODO: T4 — upClsfSystmSn 점진 로드 여부 확정 후 enabled/파라미터 조정
|
||||
enabled: Boolean(params.clsfSystmNm),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFile(params: FileParamsDto) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "file", params],
|
||||
queryFn: () => dcService.getFile(params),
|
||||
// TODO: T2 — getFile Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(params.clsfSystmSn),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFilePrcs(fileSn: string) {
|
||||
return useQuery({
|
||||
queryKey: [...baseKey, "filePrcs", fileSn],
|
||||
queryFn: () => dcService.getFilePrcs(fileSn),
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 교체
|
||||
enabled: Boolean(fileSn),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export { dcService } from "./api/dc-service";
|
||||
export { baseKey as nowDmgMtncGnsvyBaseKey, useNowDmgMtncGnsvy } from "./api/queries";
|
||||
export type { NowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvySearchDto } from "./model/dc-interface";
|
||||
export {
|
||||
baseKey as nowDmgMtncGnsvyBaseKey,
|
||||
useClsfSystm,
|
||||
useFile,
|
||||
useFilePrcs,
|
||||
useLvNowDmgMtncGnsvy,
|
||||
useNowDmgMtncGnsvy,
|
||||
} from "./api/queries";
|
||||
export type {
|
||||
ClsfSystmParamsDto,
|
||||
ClsfSystmResponseDto,
|
||||
FileDownloadParamsDto,
|
||||
FileParamsDto,
|
||||
LvNowDmgMtncGnsvyResponseDto,
|
||||
LvNowDmgMtncGnsvySearchDto,
|
||||
NowDmgMtncGnsvyResponseDto,
|
||||
NowDmgMtncGnsvySearchDto,
|
||||
} from "./model/dc-interface";
|
||||
|
||||
@@ -12,11 +12,22 @@ export interface LvNowDmgMtncGnsvySearchDto {
|
||||
gnsvyYr?: string;
|
||||
}
|
||||
|
||||
export interface ClsfSystmRequestDto {
|
||||
export interface ClsfSystmParamsDto {
|
||||
clsfSystmNm?: string;
|
||||
upClsfSystmSn?: string;
|
||||
}
|
||||
|
||||
export interface FileParamsDto {
|
||||
sort?: string;
|
||||
clsfSystmSn?: string;
|
||||
vetPrcsYn?: string;
|
||||
}
|
||||
|
||||
export interface FileDownloadParamsDto {
|
||||
filePath?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
type NullableNumber = number | null;
|
||||
type NullableString = string | null;
|
||||
|
||||
@@ -67,14 +78,21 @@ export interface NowDmgMtncGnsvyResponseDto
|
||||
sggNm?: NullableString;
|
||||
}
|
||||
|
||||
/**
|
||||
* lvNowDmgMtncGnsvy 계층 노드.
|
||||
* - level 1: ctpvNm + children(구군)
|
||||
* - level 2: sggNm + children(연도)
|
||||
* - level 3: gnsvyYr + children(처리시설)
|
||||
* - 시설 리프: swtrmtFcltCd / swtrmtFcltNm (level 없음)
|
||||
*/
|
||||
export interface LvNowDmgMtncGnsvyResponseDto {
|
||||
level: number;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
gnsvyYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
children: LvNowDmgMtncGnsvyResponseDto[];
|
||||
level?: number;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
gnsvyYr?: string;
|
||||
swtrmtFcltCd?: string;
|
||||
swtrmtFcltNm?: string;
|
||||
children?: LvNowDmgMtncGnsvyResponseDto[];
|
||||
}
|
||||
|
||||
export interface ClsfSystmResponseDto {
|
||||
@@ -89,5 +107,8 @@ export interface ClsfSystmResponseDto {
|
||||
export interface DcService {
|
||||
getNowDmgMtncGnsvy(params: NowDmgMtncGnsvySearchDto): Promise<NowDmgMtncGnsvyResponseDto[]>;
|
||||
getLvNowDmgMtncGnsvy(params: LvNowDmgMtncGnsvySearchDto): Promise<LvNowDmgMtncGnsvyResponseDto[]>;
|
||||
getClsfSystm(params: ClsfSystmRequestDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getClsfSystm(params: ClsfSystmParamsDto): Promise<ClsfSystmResponseDto[]>;
|
||||
getFile(params: FileParamsDto): Promise<unknown>;
|
||||
getFilePrcs(fileSn: string): Promise<unknown>;
|
||||
fileDownload(params: FileDownloadParamsDto): Promise<unknown>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { LvNowDmgMtncGnsvyResponseDto, NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
|
||||
type StageKey = "Dt1" | "Dt2" | "Dt3" | "Dt4" | "" | "Dc1" | "Dc2" | "Dc3" | "Dc4";
|
||||
|
||||
const STAGE_KEYS: StageKey[] = ["Dt1", "Dt2", "Dt3", "Dt4", "", "Dc1", "Dc2", "Dc3", "Dc4"];
|
||||
|
||||
function formatPlanCell(value: string | number | null | undefined, asYear = false): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (asYear) return `${value}년`;
|
||||
if (typeof value === "number") return value.toLocaleString();
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num) && value.trim() !== "" && /^-?\d+(\.\d+)?$/.test(value.trim()))
|
||||
return num.toLocaleString();
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getStagedValue(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
stage: StageKey,
|
||||
): string | number | null | undefined {
|
||||
if (!data) return undefined;
|
||||
const key = `${fieldPrefix}${stage}` as keyof NowDmgMtncGnsvyResponseDto;
|
||||
return data[key] as string | number | null | undefined;
|
||||
}
|
||||
|
||||
function getPlanYearCells(data: NowDmgMtncGnsvyResponseDto | null | undefined): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, "planYr", stage), true));
|
||||
}
|
||||
|
||||
function getMetricCells(
|
||||
data: NowDmgMtncGnsvyResponseDto | null | undefined,
|
||||
fieldPrefix: string,
|
||||
): string[] {
|
||||
return STAGE_KEYS.map((stage) => formatPlanCell(getStagedValue(data, fieldPrefix, stage)));
|
||||
}
|
||||
|
||||
function collectNodesByLevel(
|
||||
nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
targetLevel: number,
|
||||
acc: LvNowDmgMtncGnsvyResponseDto[] = [],
|
||||
): LvNowDmgMtncGnsvyResponseDto[] {
|
||||
if (!nodes) return acc;
|
||||
for (const node of nodes) {
|
||||
if (node.level === targetLevel) acc.push(node);
|
||||
if (node.children?.length) collectNodesByLevel(node.children, targetLevel, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function extractYearOptions(tree: LvNowDmgMtncGnsvyResponseDto[] | undefined): string[] {
|
||||
const years = collectNodesByLevel(tree, 3)
|
||||
.map((node) => node.gnsvyYr)
|
||||
.filter((year): year is string => Boolean(year));
|
||||
return [...new Set(years)];
|
||||
}
|
||||
|
||||
interface FacilityOption {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function extractFacilityOptions(
|
||||
tree: LvNowDmgMtncGnsvyResponseDto[] | undefined,
|
||||
gnsvyYr: string,
|
||||
): FacilityOption[] {
|
||||
const yearNodes = collectNodesByLevel(tree, 3).filter((node) => node.gnsvyYr === gnsvyYr);
|
||||
const options: FacilityOption[] = [];
|
||||
|
||||
function pushFacility(node: LvNowDmgMtncGnsvyResponseDto) {
|
||||
if (!node.swtrmtFcltCd) return;
|
||||
options.push({
|
||||
code: node.swtrmtFcltCd,
|
||||
name: node.swtrmtFcltNm ?? node.swtrmtFcltCd,
|
||||
});
|
||||
}
|
||||
|
||||
if (yearNodes.length > 0) {
|
||||
for (const yearNode of yearNodes) {
|
||||
for (const child of yearNode.children ?? []) pushFacility(child);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
// level=3 응답이 시설 리프만 내려주는 경우
|
||||
function walk(nodes: LvNowDmgMtncGnsvyResponseDto[] | undefined) {
|
||||
if (!nodes) return;
|
||||
for (const node of nodes) {
|
||||
pushFacility(node);
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
walk(tree);
|
||||
return options;
|
||||
}
|
||||
|
||||
export {
|
||||
extractFacilityOptions,
|
||||
extractYearOptions,
|
||||
formatPlanCell,
|
||||
getMetricCells,
|
||||
getPlanYearCells,
|
||||
STAGE_KEYS,
|
||||
};
|
||||
export type { FacilityOption };
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
type CmCdAdstDstrComboResponseDto,
|
||||
useFindGugunCombo,
|
||||
useFindSidoCombo,
|
||||
} from "@/entities/adstdstr";
|
||||
import { dcService, type NowDmgMtncGnsvyResponseDto, useLvNowDmgMtncGnsvy } from "@/entities/dc";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
|
||||
import { extractFacilityOptions, extractYearOptions } from "../lib/previous-plan-table-utils";
|
||||
|
||||
import { PreviousPlanAreaTable } from "./previous-plan/previous-plan-area-table";
|
||||
import { PreviousPlanFileSection } from "./previous-plan/previous-plan-file-section";
|
||||
import { PreviousPlanSearchFilters } from "./previous-plan/previous-plan-search-filters";
|
||||
import { PreviousPlanSummaryTable } from "./previous-plan/previous-plan-summary-table";
|
||||
|
||||
interface PreviousPlanLookupDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** 부모 폼 `sewerBscPlan.sido` 표시명 */
|
||||
initialSidoNm?: string;
|
||||
/** 부모 폼 `sewerBscPlan.gugun` 표시명 */
|
||||
initialGugunNm?: string;
|
||||
}
|
||||
|
||||
const EMPTY_ADST_LIST: CmCdAdstDstrComboResponseDto[] = [];
|
||||
|
||||
function PreviousPlanLookupDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialSidoNm = "",
|
||||
initialGugunNm = "",
|
||||
}: PreviousPlanLookupDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [sidoComCd, setSidoComCd] = useState("");
|
||||
const [sggComCd, setSggComCd] = useState("");
|
||||
const [gnsvyYr, setGnsvyYr] = useState("");
|
||||
const [swtrmtFcltCd, setSwtrmtFcltCd] = useState("");
|
||||
const [planData, setPlanData] = useState<NowDmgMtncGnsvyResponseDto | null>(null);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [hasPrefillApplied, setHasPrefillApplied] = useState(false);
|
||||
|
||||
const { data: sidoData, isPending: isSidoPending } = useFindSidoCombo();
|
||||
const { data: gugunData, isPending: isGugunPending } = useFindGugunCombo(sidoComCd);
|
||||
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const ctpvNm = sidoOptions.find((option) => option.adstDstrCd === sidoComCd)?.adstDstrNm ?? "";
|
||||
const sggNm = gugunOptions.find((option) => option.adstDstrCd === sggComCd)?.adstDstrNm ?? "";
|
||||
|
||||
// TODO: T7 — level=1 응답 UI 활용 여부(현재는 캐시·호출만, 구군은 adstdstr)
|
||||
useLvNowDmgMtncGnsvy({
|
||||
level: "1",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
});
|
||||
|
||||
const { data: yearTree, isPending: isYearPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "2",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
});
|
||||
|
||||
const { data: facilityTree, isPending: isFacilityPending } = useLvNowDmgMtncGnsvy({
|
||||
level: "3",
|
||||
ctpvNm: ctpvNm || undefined,
|
||||
sggNm: sggNm || undefined,
|
||||
gnsvyYr: gnsvyYr || undefined,
|
||||
});
|
||||
|
||||
const yearOptions = useMemo(() => extractYearOptions(yearTree), [yearTree]);
|
||||
const facilityOptions = useMemo(
|
||||
() => extractFacilityOptions(facilityTree, gnsvyYr),
|
||||
[facilityTree, gnsvyYr],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSidoComCd("");
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
setHasPrefillApplied(false);
|
||||
return;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || hasPrefillApplied || isSidoPending || !sidoOptions.length) return;
|
||||
if (!initialSidoNm) {
|
||||
setHasPrefillApplied(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedSido = sidoOptions.find((option) => option.adstDstrNm === initialSidoNm);
|
||||
if (matchedSido) setSidoComCd(matchedSido.adstDstrCd);
|
||||
setHasPrefillApplied(true);
|
||||
}, [open, hasPrefillApplied, isSidoPending, sidoOptions, initialSidoNm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !sidoComCd || isGugunPending || !initialGugunNm) return;
|
||||
if (sggComCd) return;
|
||||
const matchedGugun = gugunOptions.find((option) => option.adstDstrNm === initialGugunNm);
|
||||
if (matchedGugun) setSggComCd(matchedGugun.adstDstrCd);
|
||||
}, [open, sidoComCd, isGugunPending, gugunOptions, initialGugunNm, sggComCd]);
|
||||
|
||||
function handleSidoChange(value: string) {
|
||||
setSidoComCd(value);
|
||||
setSggComCd("");
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleGugunChange(value: string) {
|
||||
setSggComCd(value);
|
||||
setGnsvyYr("");
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleYearChange(value: string) {
|
||||
setGnsvyYr(value);
|
||||
setSwtrmtFcltCd("");
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
function handleFacilityChange(value: string) {
|
||||
setSwtrmtFcltCd(value);
|
||||
setPlanData(null);
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
if (!ctpvNm) {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!sggNm) {
|
||||
alert("구군을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!gnsvyYr) {
|
||||
alert("연도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!swtrmtFcltCd) {
|
||||
alert("공공하수처리시설을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const list = await dcService.getNowDmgMtncGnsvy({
|
||||
swtrmtFcltCd,
|
||||
ctpvNm,
|
||||
sggNm,
|
||||
targetYear: gnsvyYr,
|
||||
});
|
||||
if (!list.length) {
|
||||
alert("조회된 데이터가 없습니다.");
|
||||
setPlanData(null);
|
||||
return;
|
||||
}
|
||||
setPlanData(list[0] ?? null);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setPlanData(null);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="flex max-h-[90vh] flex-col overflow-hidden sm:max-w-[90rem]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>기본 계획 찾기</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
{isSidoPending ? (
|
||||
<Skeleton className="h-[6rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSearchFilters
|
||||
sidoComCd={sidoComCd}
|
||||
sggComCd={sggComCd}
|
||||
gnsvyYr={gnsvyYr}
|
||||
swtrmtFcltCd={swtrmtFcltCd}
|
||||
sidoOptions={sidoOptions}
|
||||
gugunOptions={gugunOptions}
|
||||
yearOptions={yearOptions}
|
||||
facilityOptions={facilityOptions}
|
||||
isGugunPending={isGugunPending}
|
||||
isYearPending={Boolean(sggNm) && isYearPending}
|
||||
isFacilityPending={Boolean(gnsvyYr) && isFacilityPending}
|
||||
isSearching={isSearching}
|
||||
onSidoChange={handleSidoChange}
|
||||
onGugunChange={handleGugunChange}
|
||||
onYearChange={handleYearChange}
|
||||
onFacilityChange={handleFacilityChange}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SectionCollapsible
|
||||
title="기본계획파일(기본계획의 보고서를 트리에서 클릭하시면 파일을 다운받으실수 있습니다.)"
|
||||
defaultOpen
|
||||
>
|
||||
<PreviousPlanFileSection key={sggNm || "empty"} gugunNm={sggNm} />
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="총설" defaultOpen>
|
||||
{isSearching ? (
|
||||
<Skeleton className="h-[8rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanSummaryTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
|
||||
<SectionCollapsible title="처리구역별 현황" defaultOpen>
|
||||
{isSearching ? (
|
||||
<Skeleton className="h-[20rem] w-full" />
|
||||
) : (
|
||||
<PreviousPlanAreaTable data={planData} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanLookupDialog };
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getMetricCells, getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
const AREA_ROWS = [
|
||||
{ label: "하수처리인구(인)", field: "swtrmtPpltnCnt" },
|
||||
{ label: "자연적하수처리인구수(인)", field: "natrSwtrmtPpltnCnt" },
|
||||
{ label: "사회적하수처리인구수(인)", field: "scilSwtrmtPpltnCnt" },
|
||||
{ label: "하수처리구역면적", field: "swtrmtZoneArea" },
|
||||
{ label: "물사용량원단위", field: "wtrUsqtyOrgnUnit" },
|
||||
{ label: "오수전환율(%)", field: "drtwSwtcRt" },
|
||||
{ label: "변동부하율(%)", field: "flctnLodRt" },
|
||||
{ label: "오수원단위", field: "drtwOrgnUnit" },
|
||||
{ label: "계획하수량합계(㎥/일)", field: "planSwageQntSum" },
|
||||
{ label: "생활오수(㎥/일)", field: "lvngDrtwSum" },
|
||||
{ label: "지하수(㎥/일)", field: "ugwtSum" },
|
||||
{ label: "공장폐수(㎥/일)", field: "fctryWwtSum" },
|
||||
{ label: "관광오수(㎥/일)", field: "toursmDrtwSum" },
|
||||
{ label: "군부대(㎥/일)", field: "mtbsSum" },
|
||||
{ label: "연계처리수(㎥/일)", field: "linkPrcsCntSum" },
|
||||
{ label: "기타오수(㎥/일)", field: "etcDrtwSum" },
|
||||
{ label: "지하수사용량(㎥/일)", field: "ugwtUseSum" },
|
||||
{ label: "시설용량(㎥/일)", field: "fcltCpctSum" },
|
||||
{ label: "배제방식", field: "exclsMethExpln" },
|
||||
] as const;
|
||||
|
||||
interface PreviousPlanAreaTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanAreaTable({ data }: PreviousPlanAreaTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="16%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
<col width="9.3%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={3} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableHead key={index} className="text-center font-normal">
|
||||
{cell}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{AREA_ROWS.map((row) => {
|
||||
const cells = getMetricCells(data, row.field);
|
||||
return (
|
||||
<TableRow key={row.field}>
|
||||
<TableHead>{row.label}</TableHead>
|
||||
{cells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-right">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanAreaTable };
|
||||
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { type ClsfSystmResponseDto, dcService, useClsfSystm, useFile } from "@/entities/dc";
|
||||
import { Button, Skeleton } from "@common/components";
|
||||
|
||||
interface ClsfTreeNode {
|
||||
node: ClsfSystmResponseDto;
|
||||
children: ClsfTreeNode[];
|
||||
}
|
||||
|
||||
interface LegacyFileItem {
|
||||
no?: number | string;
|
||||
fileTitle?: string;
|
||||
regDt?: string;
|
||||
fileSn?: string;
|
||||
}
|
||||
|
||||
interface PreviousPlanFileSectionProps {
|
||||
gugunNm: string;
|
||||
}
|
||||
|
||||
function buildClsfTree(list: ClsfSystmResponseDto[]): ClsfTreeNode[] {
|
||||
const byId = new Map<string, ClsfTreeNode>();
|
||||
for (const item of list) {
|
||||
byId.set(item.clsfSystmSn, { node: item, children: [] });
|
||||
}
|
||||
|
||||
const roots: ClsfTreeNode[] = [];
|
||||
for (const item of list) {
|
||||
const current = byId.get(item.clsfSystmSn);
|
||||
if (!current) continue;
|
||||
const parent = item.upClsfSystmSn ? byId.get(item.upClsfSystmSn) : undefined;
|
||||
if (parent) parent.children.push(current);
|
||||
else roots.push(current);
|
||||
}
|
||||
|
||||
function sortNodes(nodes: ClsfTreeNode[]) {
|
||||
nodes.sort((a, b) => Number(a.node.sortSeq) - Number(b.node.sortSeq));
|
||||
nodes.forEach((node) => sortNodes(node.children));
|
||||
}
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
// TODO: T2 — getFile Response DTO 확정 후 정규화 로직 교체 (레거시: no, fileTitle, regDt, fileSn)
|
||||
function normalizeFileList(data: unknown): LegacyFileItem[] {
|
||||
if (!Array.isArray(data)) return [];
|
||||
return data.map((item) => {
|
||||
if (!item || typeof item !== "object") return {};
|
||||
const row = item as Record<string, unknown>;
|
||||
return {
|
||||
no: row.no as number | string | undefined,
|
||||
fileTitle: (row.fileTitle ?? row.fileName ?? row.fileNm) as string | undefined,
|
||||
regDt: row.regDt as string | undefined,
|
||||
fileSn: (row.fileSn ?? row.fileId) as string | undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function ClsfTreeItem({
|
||||
item,
|
||||
depth,
|
||||
selectedSn,
|
||||
onSelect,
|
||||
}: {
|
||||
item: ClsfTreeNode;
|
||||
depth: number;
|
||||
selectedSn: string;
|
||||
onSelect: (sn: string) => void;
|
||||
}) {
|
||||
const isSelected = selectedSn === item.node.clsfSystmSn;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className={`w-full rounded px-2 py-1 text-left text-sm hover:bg-muted ${
|
||||
isSelected ? "bg-muted font-semibold" : ""
|
||||
}`}
|
||||
style={{ paddingLeft: `${0.5 + depth * 0.75}rem` }}
|
||||
onClick={() => onSelect(item.node.clsfSystmSn)}
|
||||
>
|
||||
{item.node.clsfSystmNm}
|
||||
</button>
|
||||
{item.children.length > 0 && (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{item.children.map((child) => (
|
||||
<ClsfTreeItem
|
||||
key={child.node.clsfSystmSn}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
selectedSn={selectedSn}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviousPlanFileSection({ gugunNm }: PreviousPlanFileSectionProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [selectedClsfSn, setSelectedClsfSn] = useState("");
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const { data: clsfList, isPending: isClsfPending } = useClsfSystm({
|
||||
clsfSystmNm: gugunNm || undefined,
|
||||
});
|
||||
const { data: fileData, isPending: isFilePending } = useFile({
|
||||
clsfSystmSn: selectedClsfSn || undefined,
|
||||
});
|
||||
|
||||
const tree = useMemo(() => buildClsfTree(clsfList ?? []), [clsfList]);
|
||||
const files = useMemo(() => normalizeFileList(fileData), [fileData]);
|
||||
|
||||
async function handleDownload(fileSn?: string, fallbackName?: string) {
|
||||
if (!fileSn) {
|
||||
alert("파일 식별자가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
// TODO: T2 — getFilePrcs Response DTO 확정 후 타입 가드
|
||||
const prcsRaw = await dcService.getFilePrcs(fileSn);
|
||||
const fileInfo = (Array.isArray(prcsRaw) ? prcsRaw[0] : prcsRaw) as
|
||||
| { filePath?: string; fileTitle?: string; fileName?: string }
|
||||
| undefined;
|
||||
const filePath = fileInfo?.filePath;
|
||||
const fileName = fileInfo?.fileTitle ?? fileInfo?.fileName ?? fallbackName;
|
||||
|
||||
if (!filePath || !fileName) {
|
||||
alert("파일 정보를 가져올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: T3 — blob / Content-Disposition / 게이트웨이 다운로드 UX 확정
|
||||
await dcService.fileDownload({ filePath, fileName });
|
||||
alert("다운로드 요청을 전송했습니다. (다운로드 UX TBD)");
|
||||
} catch {
|
||||
alert("파일 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!gugunNm) {
|
||||
return (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">시도와 구군을 선택하세요.</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[40rem] gap-2">
|
||||
<div className="w-[40%] overflow-y-auto rounded border p-2">
|
||||
{isClsfPending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : tree.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<ul className="m-0 list-none p-0">
|
||||
{tree.map((item) => (
|
||||
<ClsfTreeItem
|
||||
key={item.node.clsfSystmSn}
|
||||
item={item}
|
||||
depth={0}
|
||||
selectedSn={selectedClsfSn}
|
||||
onSelect={(sn) => setSelectedClsfSn(sn)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[60%] overflow-y-auto rounded border p-2">
|
||||
{!selectedClsfSn ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
트리에서 항목을 선택하세요.
|
||||
</p>
|
||||
) : isFilePending ? (
|
||||
<Skeleton className="h-full w-full" />
|
||||
) : files.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">파일이 없습니다.</p>
|
||||
) : (
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="bg-muted/40">
|
||||
<th className="border px-2 py-1">번호</th>
|
||||
<th className="border px-2 py-1 text-left">파일명</th>
|
||||
<th className="border px-2 py-1">등록일</th>
|
||||
<th className="border px-2 py-1">다운로드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file, index) => (
|
||||
<tr key={file.fileSn ?? `${file.fileTitle}-${index}`}>
|
||||
<td className="border px-2 py-1 text-center">{file.no ?? index + 1}</td>
|
||||
<td className="border px-2 py-1">{file.fileTitle ?? "-"}</td>
|
||||
<td className="border px-2 py-1 text-center">{file.regDt ?? "-"}</td>
|
||||
<td className="border px-2 py-1 text-center">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="sm"
|
||||
disabled={isDownloading}
|
||||
onClick={() => handleDownload(file.fileSn, file.fileTitle)}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanFileSection };
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import type { CmCdAdstDstrComboResponseDto } from "@/entities/adstdstr";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
|
||||
import type { FacilityOption } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSearchFiltersProps {
|
||||
sidoComCd: string;
|
||||
sggComCd: string;
|
||||
gnsvyYr: string;
|
||||
swtrmtFcltCd: string;
|
||||
sidoOptions: CmCdAdstDstrComboResponseDto[];
|
||||
gugunOptions: CmCdAdstDstrComboResponseDto[];
|
||||
yearOptions: string[];
|
||||
facilityOptions: FacilityOption[];
|
||||
isGugunPending: boolean;
|
||||
isYearPending: boolean;
|
||||
isFacilityPending: boolean;
|
||||
isSearching: boolean;
|
||||
onSidoChange: (value: string) => void;
|
||||
onGugunChange: (value: string) => void;
|
||||
onYearChange: (value: string) => void;
|
||||
onFacilityChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
function PreviousPlanSearchFilters({
|
||||
sidoComCd,
|
||||
sggComCd,
|
||||
gnsvyYr,
|
||||
swtrmtFcltCd,
|
||||
sidoOptions,
|
||||
gugunOptions,
|
||||
yearOptions,
|
||||
facilityOptions,
|
||||
isGugunPending,
|
||||
isYearPending,
|
||||
isFacilityPending,
|
||||
isSearching,
|
||||
onSidoChange,
|
||||
onGugunChange,
|
||||
onYearChange,
|
||||
onFacilityChange,
|
||||
onSearch,
|
||||
}: PreviousPlanSearchFiltersProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||
<Table variant="secondary" className="flex-1">
|
||||
<colgroup>
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
<col width="12%" />
|
||||
<col width="13%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead data-required>시도</TableHead>
|
||||
<TableCell>
|
||||
<Select value={sidoComCd || undefined} onValueChange={onSidoChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead data-required>구군</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={sggComCd || undefined}
|
||||
onValueChange={onGugunChange}
|
||||
disabled={!sidoComCd || isGugunPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((option) => (
|
||||
<SelectItem key={option.adstDstrCd} value={option.adstDstrCd}>
|
||||
{option.adstDstrNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead data-required>연도</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={gnsvyYr || undefined}
|
||||
onValueChange={onYearChange}
|
||||
disabled={!sggComCd || isYearPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem key={year} value={year}>
|
||||
{year}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableHead data-required>처리시설</TableHead>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={swtrmtFcltCd || undefined}
|
||||
onValueChange={onFacilityChange}
|
||||
disabled={!gnsvyYr || isFacilityPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{facilityOptions.map((option) => (
|
||||
<SelectItem key={option.code} value={option.code}>
|
||||
{option.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
</Table>
|
||||
<Button type="button" className="shrink-0" onClick={onSearch} disabled={isSearching}>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSearchFilters };
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import type { NowDmgMtncGnsvyResponseDto } from "@/entities/dc";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
import { getPlanYearCells } from "../../lib/previous-plan-table-utils";
|
||||
|
||||
interface PreviousPlanSummaryTableProps {
|
||||
data: NowDmgMtncGnsvyResponseDto | null;
|
||||
}
|
||||
|
||||
function PreviousPlanSummaryTable({ data }: PreviousPlanSummaryTableProps) {
|
||||
const yearCells = getPlanYearCells(data);
|
||||
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
<col width="10%" />
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
구분
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
당초
|
||||
</TableHead>
|
||||
<TableHead rowSpan={2} className="text-center">
|
||||
기준
|
||||
</TableHead>
|
||||
<TableHead colSpan={4} className="text-center">
|
||||
변경
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
<TableHead className="text-center">1단계</TableHead>
|
||||
<TableHead className="text-center">2단계</TableHead>
|
||||
<TableHead className="text-center">3단계</TableHead>
|
||||
<TableHead className="text-center">4단계</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>계획목표년도</TableHead>
|
||||
{yearCells.map((cell, index) => (
|
||||
<TableCell key={index} className="text-center">
|
||||
{cell}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviousPlanSummaryTable };
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useFormContext, useWatch } from "react-hook-form";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { Field } from "@/shared/ui/field";
|
||||
import { SectionCollapsible } from "@/widgets/pbadms-sprt";
|
||||
@@ -17,6 +19,14 @@ import {
|
||||
|
||||
import type { SewerBscPlanRegistFormData } from "../../lib/sewer-bsc-plan-regist-schema";
|
||||
|
||||
const PreviousPlanLookupDialog = dynamic(
|
||||
() =>
|
||||
import("../previous-plan-lookup-dialog").then((mod) => ({
|
||||
default: mod.PreviousPlanLookupDialog,
|
||||
})),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
const OTLN_ROWS = [
|
||||
{ key: "sewerPlanPpltnCnt" as const, label: "계획인구" },
|
||||
{ key: "sewerPrcsPpltnCnt" as const, label: "처리인구" },
|
||||
@@ -25,11 +35,17 @@ const OTLN_ROWS = [
|
||||
|
||||
function OutlineFormSection() {
|
||||
const { control, setValue, getValues } = useFormContext<SewerBscPlanRegistFormData>();
|
||||
const [isLookupOpen, setIsLookupOpen] = useState(false);
|
||||
|
||||
const otlnList = useWatch({ control, name: "sewerBscPlanOtlnList" }) ?? [];
|
||||
const frlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.frlvlGoalYr" });
|
||||
const sclvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.sclvlGoalYr" });
|
||||
const thlvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.thlvlGoalYr" });
|
||||
const folvlGoalYr = useWatch({ control, name: "sewerBscPlanDtl.folvlGoalYr" });
|
||||
const [sido, gugun] = useWatch({
|
||||
control,
|
||||
name: ["sewerBscPlan.sido", "sewerBscPlan.gugun"],
|
||||
});
|
||||
|
||||
const stepHeaders = [
|
||||
{ label: "1단계", year: frlvlGoalYr },
|
||||
@@ -54,7 +70,8 @@ function OutlineFormSection() {
|
||||
return (
|
||||
<SectionCollapsible title="사업개요">
|
||||
<div className="mb-2 flex justify-end px-2">
|
||||
<Button type="button" color="white" disabled>
|
||||
{/* TODO: T6 — 부모 폼 수치 반영은 요구 없음(레거시 동일) */}
|
||||
<Button type="button" color="white" onClick={() => setIsLookupOpen(true)}>
|
||||
이전기본계획조회
|
||||
</Button>
|
||||
</div>
|
||||
@@ -102,6 +119,13 @@ function OutlineFormSection() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<PreviousPlanLookupDialog
|
||||
open={isLookupOpen}
|
||||
onOpenChange={setIsLookupOpen}
|
||||
initialSidoNm={sido}
|
||||
initialGugunNm={gugun}
|
||||
/>
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user