diff --git a/apps/as/docs/pwrk-cnsltn/development-guide.md b/apps/as/docs/pwrk-cnsltn/development-guide.md
index 6812c3db1..08193cd83 100644
--- a/apps/as/docs/pwrk-cnsltn/development-guide.md
+++ b/apps/as/docs/pwrk-cnsltn/development-guide.md
@@ -10,7 +10,7 @@
>
> - 프로세스·상태·버튼: `03.재원협의_실무협의_프로세스_통합_v완결.md`
> - REST 설계: `05.재원협의_상태별_endpoint_설계.md`
-> - 공통코드: `FNRSSTTS` (`SV, ST, LE, FV, ER, SL, AC, TE, TP, RM, RB, RC, RA, AV, RK, RR, EP, TA`)
+> - 공통코드: `FNRSSTTS` (`SV, ST, LE, FV, ER, SL, AC, TE, TP, RM, RB, RC, RA, AV, EV, RK, RR, EP, TA`)
>
> **백엔드 서비스**:
>
@@ -40,7 +40,7 @@ v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**
| [ref-screen-buttons.md](./ref-screen-buttons.md) | 38행 화면·역할·버튼 매트릭스, 버튼 가시성 | `button-config.ts` 개편 |
| [ref-endpoint.md](./ref-endpoint.md) | 버튼→API 총괄, 단계별 입출력, FE 호출 금지 API, Gap Analysis | 엔티티·service 매핑 |
| [ref-frontend-entities.md](./ref-frontend-entities.md) | ENDPOINT, 슬라이스, Service 시그니처, 폴링, 웹기안기, DTO | 엔티티 레이어 구현 |
-| [ref-webhwp-api.md](./ref-webhwp-api.md) | 웹한글기안기 v3 `HwpCtrl` Properties/Methods, Open/SaveAs | 기안기 위젯 연동 |
+| [ref-webhwp-api.md](./ref-webhwp-api.md) | 웹한글기안기 v3 `HwpCtrl` Properties/Methods, Open/SaveAs | 기안기 위젯 연동 |
---
@@ -98,7 +98,7 @@ v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**
정방향:
```
-SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA → AV → RK → RR → EP → TA
+SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA → AV → EV → RK → RR → EP → TA
```
자체종결: `LE → EP → TA`
@@ -119,7 +119,8 @@ SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA
| `RB` | 지자체회신 | 기초 실무협의서 관리 / 조치완료 |
| `RC` | 조치결과서 제출 | 광역 → 환경청 `RA` |
| `RA` | 조치결과서제출(환경청) | 환경청 2차 의뢰 `AV` |
-| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `RK` Fallback |
+| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `EV` Fallback |
+| `EV` | 조치결과검토의뢰 | 공단 접수자: 2차 결재선 검토자지정 → `RK` |
| `RK` | 조치결과서검토 | 웹기안기·금액검증 재실행·보완. 결재선 2차 |
| `RR` | 조치결과서검토 회신 | 환경청 → 광역 `EP` |
| `EP` | 검토결과 회신 | 광역 → 기초 `TA` |
@@ -144,7 +145,7 @@ SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA
6. **진행상태만 바꾸는 버튼은** `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts`**를 재사용한다.** (`ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`)
7. **보완요청/보완완료는 공통 API** `reqsupplement` / `submitsupplement`. 복귀 상태(`ER`/`TE`/`RK`)는 서버가 이력에서 결정한다. 프론트는 직전 상태를 추측해 보내지 않는다.
8. **결재 승인/회신은** `PUT /elapr-prgrs-stts/{id}/process` **하나**. 1차 최종승인자 → `TP`, 2차 → `RR`은 서버가 Feign으로 처리한다.
-9. **2차 결재선 복사는 백엔드**. `POST /elapr-apln/copy-line`은 `AV→RK` 콜백에서 호출한다. 프론트는 `RK`에서 2차 결재선을 **조회·표시**만 한다.
+9. **2차 결재선은 EV에서 수동 검토자지정**. `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선 자동 copy-line은 사용하지 않는다.
10. **웹기안기는 `webhwp/open` 응답을 axios `blob`으로 조회한 뒤 `File`로 감싸 `HwpCtrl.Open`에 전달한다.** `blob:` Object URL과 `orgnlOtptDocCn`은 쓰지 않는다.
---
@@ -189,6 +190,7 @@ BEGIN
ON [파일보완] (ER, ATRZ01) → GET file-vrfc-results → 팝업 → reqsupplement
ON [파일검증] (ER, ATRZ01) → dcrag-to transfer (상태 유지)
ON [검토자지정] (ER, ATRZ01) → POST elapr-dmnd (docPrgrsSttsComCd: AC) → START POLL AC
+ ON [검토자지정] (EV, ATRZ01) → POST elapr-dmnd (elaprCycl: 2, docPrgrsSttsComCd: RK)
ON [실무협의서관리] (TE|RK) → GET webhwp/open (blob) → HwpCtrl.Open(File)
ON [실무협의서생성] (TE, 실패/타임아웃) → dcrag-to draft/generate
ON [검토결과조회] (TE) → pwrk-cnsltn-mng findById | findByUpDocNo
@@ -212,36 +214,37 @@ END
프론트가 치는 것만. 콜백·스케줄러는 제외.
-| 주체 | 버튼 | From → To | Method & URI |
-| --------- | -------------------------- | ----------------------------- | --------------------------------------------------------------- | --- | ------------------------------------------------ |
-| 기초 | 저장 / 신청완료 | →`SV` / `SV→ST` | `POST /fnrs-cnsltn-mng/save-apply` |
-| 기초 | 삭제 | `SV` | `DELETE /fnrs-cnsltn-mng/{docNo}` |
-| 광역 | 환경청제출 | `ST→LE` | `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` |
-| 환경청 | 결과회신(자체종결) | `LE→EP` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
-| 환경청 | 기술검토 의뢰 | `LE→FV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-examine` |
-| 공단 접수 | 파일보완 팝업 | `ER` | `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results` |
-| 공단 접수 | 파일보완요청 | `ER→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
-| 공단 접수 | 파일검증 재실행 | `ER` 유지 | `POST /dcrag-to/documents/transfer` |
-| 공단 접수 | 검토자지정 | `ER→AC` | `POST /elapr-dmnd` |
-| 공단 검토 | 실무협의서관리 | `TE`/`RK` | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
-| 공단 검토 | 실무협의서생성 | `TE` 유지 | `POST /dcrag-to/documents/draft/generate` |
-| 공단 검토 | 저장 | `TE`/`RK` | `POST /pwrk-cnsltn-mng/save-apply` |
-| 공단 | 검토완료 / 결재(승인·회신) | `TE` 유지 / `TE→TP` / `RK→RR` | `PUT /elapr-prgrs-stts/{id}/process` |
-| 공단 검토 | 보완요청 | `TE→SL` / `RK→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
-| 기초 | 파일보완완료 / 보완완료 | `SL→ER | TE | RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
-| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
-| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
-| 기초 | 실무협의서 관리 | `RB` 유지 | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
+| 주체 | 버튼 | From → To | Method & URI |
+| --------- | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
+| 기초 | 저장 / 신청완료 | →`SV` / `SV→ST` | `POST /fnrs-cnsltn-mng/save-apply` |
+| 기초 | 삭제 | `SV` | `DELETE /fnrs-cnsltn-mng/{docNo}` |
+| 광역 | 환경청제출 | `ST→LE` | `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts` |
+| 환경청 | 결과회신(자체종결) | `LE→EP` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
+| 환경청 | 기술검토 의뢰 | `LE→FV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-examine` |
+| 공단 접수 | 파일보완 팝업 | `ER` | `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results` |
+| 공단 접수 | 파일보완요청 | `ER→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
+| 공단 접수 | 파일검증 재실행 | `ER` 유지 | `POST /dcrag-to/documents/transfer` |
+| 공단 접수 | 검토자지정 | `ER→AC` | `POST /elapr-dmnd` (`elaprCycl: 1`) |
+| 공단 접수 | 검토자지정 | `EV→RK` | `POST /elapr-dmnd` (`elaprCycl: 2`) |
+| 공단 검토 | 실무협의서관리 | `TE`/`RK` | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
+| 공단 검토 | 실무협의서생성 | `TE` 유지 | `POST /dcrag-to/documents/draft/generate` |
+| 공단 검토 | 저장 | `TE`/`RK` | `POST /pwrk-cnsltn-mng/save-apply` |
+| 공단 | 검토완료 / 결재(승인·회신) | `TE` 유지 / `TE→TP` / `RK→RR` | `PUT /elapr-prgrs-stts/{id}/process` |
+| 공단 검토 | 보완요청 | `TE→SL` / `RK→SL` | `POST /fnrs-cnsltn-mng/{docNo}/reqsupplement` |
+| 기초 | 파일보완완료 / 보완완료 | `SL→ER` / `TE` / `RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
+| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
+| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
+| 기초 | 실무협의서 관리 | `RB` 유지 | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
| 기초 | 조치완료 | `RB→RA` | **신규** `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit: true`) |
-| 광역 | 조치결과서제출 | `RC→RA` | `updatePrgrsStts` |
-| 환경청 | 실무협의서 관리 | `RA` 유지 (조회) | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
-| 환경청 | 조치결과 검토의뢰 | `RA→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
-| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
-| 환경청 | 결과회신 | `RR→EP` | `updatePrgrsStts` |
-| 광역 | 결과회신 | `EP→TA` | `updatePrgrsStts` |
-| 웹기안기 | 저장 | 버전 +1 | `POST /pwrk-cnsltn-doc-crt-rslt` multipart |
-| 공통 | 다운로드 | — | `GET /pwrk-cnsltn-doc-crt-rslt/{uuid}/download` |
+| 광역 | 조치결과서제출 | `RC→RA` | `updatePrgrsStts` |
+| 환경청 | 실무협의서 관리 | `RA` 유지 (조회) | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` |
+| 환경청 | 조치결과 검토의뢰 | `RA→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
+| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
+| 환경청 | 결과회신 | `RR→EP` | `updatePrgrsStts` |
+| 광역 | 결과회신 | `EP→TA` | `updatePrgrsStts` |
+| 웹기안기 | 저장 | 버전 +1 | `POST /pwrk-cnsltn-doc-crt-rslt` multipart |
+| 공통 | 다운로드 | — | `GET /pwrk-cnsltn-doc-crt-rslt/{uuid}/download` |
-신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, (백엔드) `elapr-apln/copy-line`.
+신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, EV 2차 `elapr-dmnd`.
---
diff --git a/apps/as/docs/pwrk-cnsltn/ref-endpoint.md b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md
index d63d2d4de..712ae57d6 100644
--- a/apps/as/docs/pwrk-cnsltn/ref-endpoint.md
+++ b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md
@@ -43,7 +43,8 @@
| 27 | 조치완료 | `RB→RA` | as | `POST /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` (`isSubmit=true`) | **신규** |
| 28 | 조치결과서제출 | `RC→RA` | as | `updatePrgrsStts` | 기존 |
| 29 | 조치결과 검토의뢰 | `RA→AV` | as | `POST /api/v1/fnrs-cnsltn-mng/{docNo}/request-actn-review` | **신규** |
-| 30 | 금액검증 콜백 | `AV→RK` | as | `POST /api/v1/dcrag/cost/verify-result` | 서버 |
+| 30 | 금액검증 콜백 | `AV→EV` | as | `POST /api/v1/dcrag/cost/verify-result` | 서버 |
+| 30a | 검토자지정(2차) | `EV→RK` | elapr | `POST /api/v1/elapr-dmnd` (`elaprCycl=2`) | 기존 |
| 31 | 실무협의서관리 | `RK` | as | `GET /pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 |
| 32 | 금액검증 재실행 | `RK` | as | `POST /api/v1/dcrag-to/documents/cost/verify` | 기존 |
| 33 | 보완요청(금액) | `RK→SL` | as | `reqsupplement` | 기존 |
@@ -212,7 +213,7 @@ v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**.
- `isSubmit: true` → `RB→RA` (조치완료)
응답: `string` (pwrkDocNo). DTO명 `PwrkActnResultSubmitDto`.
-### 3-6. `RA → AV → RK`
+### 3-6. `RA → AV → EV → RK`
**2차 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review`
```json
@@ -223,7 +224,8 @@ v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**.
`RA→AV`. 응답: `string` (docNo). DTO명 `EnvoActnReviewReqDto`. `specialRqstCn`만 선택.
-금액검증 콜백은 서버. 콜백 후 서버가 `copy-line` 호출.
+금액검증 콜백은 서버 → `AV→EV`.
+**2차 검토자지정** — `POST /elapr-dmnd` (`elaprCycl: 2`, `docPrgrsSttsComCd: RK`). 1차 결재선(`findDocPrgrsByDocNo`, cycl=1)을 시드로 편집.
**금액검증 재실행** — `POST /dcrag-to/documents/cost/verify`
@@ -292,7 +294,7 @@ multipart `Content-Type` 헤더를 수동 지정하지 않는다 (boundary). JSO
}
```
-`RK`에서 2차 결재선은 기존 `findDocPrgrsByDocNo` / `elaprCycl=2` 조회로 표시.
+`RK`에서 결재현황은 `findDocPrgrsByDocNo`(전체) 조회 후 `elaprCycl`로 1차/2차 탭 표시.
---
diff --git a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md
index e63531842..fdb817e08 100644
--- a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md
+++ b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md
@@ -343,5 +343,8 @@ docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, // ER
To-Be:
-- 검토자지정은 **`ER`에서만**. `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`
-- 2차 결재선은 서버 `copy-line`. `RK`에서 검토자지정 팝업을 다시 열지 않음
+- 검토자지정은 **`ER`(1차)** 과 **`EV`(2차)** 에서.
+- `ER`: `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`, 서식 선택 다이얼로그
+- `EV`: `elaprCycl: 2`, `docPrgrsSttsComCd: "RK"`, 1차 결재선 시드 다이얼로그(서식 없음)
+
+`RK` / ATRZ02 FAB: `findDocPrgrsByDocNo` 전체 조회 후 pending은 `elaprCycl === 2`만 사용. 검토완료 후 `ElaprApprovalDialog`에 `elaprCycl={2}`. 보완요청은 TE와 동일 `SplmntDmndDialog`.
diff --git a/apps/as/docs/pwrk-cnsltn/ref-process-status.md b/apps/as/docs/pwrk-cnsltn/ref-process-status.md
index e5da682d3..d5d55171b 100644
--- a/apps/as/docs/pwrk-cnsltn/ref-process-status.md
+++ b/apps/as/docs/pwrk-cnsltn/ref-process-status.md
@@ -16,7 +16,7 @@
2-Pass:
- **Pass 1** 기술 타당성: `SV→ST→LE→FV→ER→AC→TE→TP→RM→RB`
-- **Pass 2** 조치결과·금액: `RB→RA→AV→RK→RR→EP→TA`
+- **Pass 2** 조치결과·금액: `RB→RA→AV→EV→RK→RR→EP→TA`
자체종결(기술검토 생략): `LE→EP→TA`
@@ -80,9 +80,9 @@ sequenceDiagram
rect rgb(240, 255, 255)
광역->>환경청: [조치결과서제출] RC → RA
환경청->>AI: [조치결과 검토의뢰] RA → AV
- AI-->>공단: 금액검증 콜백 AV → RK
- Note over 공단: 30분 미응답 시 RK + 수동 [금액검증]
- Note over 공단: AV→RK 시 1차 결재선 → 2차 복사 (서버)
+ AI-->>공단: 금액검증 콜백 AV → EV
+ 공단->>공단: [검토자지정] EV → RK
+ Note over 공단: EV에서 1차 결재선 시드로 2차 결재선 수동 배정
end
rect rgb(255, 255, 230)
@@ -125,7 +125,8 @@ sequenceDiagram
| 9 | 검토결과 회신 | 환경청·광역 | `TP→RM→RB` | 1차 회신. `EP`와 혼동 금지 |
| 10 | 조치결과 작성 | 기초 | `RB→RA` | `submit-actn-result` `isSubmit:true` (조치완료) |
| 11 | 조치 의뢰 | 환경청 | `RA→AV` | `request-actn-review` |
-| 12 | AI 금액검증 | AI·공단 | `AV→RK` | 결재선 복사(서버) |
+| 12 | AI 금액검증 | AI·공단 | `AV→EV` | 콜백/Fallback 후 접수자 대기 |
+| 12a | 2차 검토자지정 | 공단 접수 | `EV→RK` | `elapr-dmnd` cycl=2 |
| 13 | 2차 검토·결재 | 공단 | `RK→RR` / `SL` | 2차 결재선 |
| 14 | 최종 회신 | 환경청·광역·기초 | `RR→EP→TA` | 종결 |
@@ -151,7 +152,8 @@ sequenceDiagram
| `RC` | 조치결과서 제출 | Result Complete | 기초→광역 | `RB→RC` |
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 기초→환경청 | `RB→RA` |
| `AV` | 금액검증 | Amount Verification | AI | `RA→AV` |
-| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `AV→RK`, `SL→RK` |
+| `EV` | 조치결과검토의뢰 | Examine Verify | 공단 접수자 | `AV→EV` |
+| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `EV→RK`, `SL→RK` |
| `RR` | 조치결과서검토 회신 | Result Return to Basin | 공단→환경청 | `RK→RR` |
| `EP` | 검토결과 회신 | Examine Pass to Province | 환경청→광역 | `LE→EP`(자체), `RR→EP`(최종) |
| `TA` | 검토결과 회신(종료) | Technical Approval | 광역→기초 | `EP→TA` |
@@ -200,7 +202,7 @@ stateDiagram-v2
| --- | --- | --- | --- | --- | --- |
| AI-1 | 파일 무결성 | `FV→ER` | 환경청 [기술검토 의뢰] | 30분 → `ER` + [파일검증] | `POST /dcrag-to/documents/transfer` |
| AI-2 | 초안 생성 | `AC→TE` | 공단 [검토자지정] | 30분 → `TE` + [실무협의서생성] | `POST /dcrag-to/documents/draft/generate` |
-| AI-3 | 금액 검증 | `AV→RK` | 환경청 [조치결과 검토의뢰] | 30분 → `RK` + [금액검증] | `POST /dcrag-to/documents/cost/verify` |
+| AI-3 | 금액 검증 | `AV→EV` | 환경청 [조치결과 검토의뢰] | 30분 → `EV` + 수동 [금액검증] | `POST /dcrag-to/documents/cost/verify` |
- 예상 소요 안내는 **30분** (v1.0의 10분은 폐기).
- 콜백 수신·스케줄러 전이는 서버. FE는 상세 refetch / draft status 폴링만.
@@ -221,14 +223,12 @@ stateDiagram-v2
---
-## 7. 결재선 복사
-
-규칙: `"금액검증후: 2차결재선 1차결재선 복사생성"`
+## 7. 2차 결재선 (수동 검토자지정)
1. `TE`에서 1차 결재선(`elaprCycl = 1`) 완료.
-2. `AV→RK` 콜백 시 서버가 `POST /elapr-apln/copy-line` (`source=1`, `target=2`).
-3. `RK` 화면은 2차 결재선을 조회해 검토자/승인자/최종승인자를 표시.
-4. 프론트는 copy-line을 **호출하지 않는다.**
+2. 금액검증 콜백/Fallback → `AV→EV`.
+3. 공단 접수자가 `EV`에서 [검토자지정]: 1차 결재선을 시드로 편집 후 `POST /elapr-dmnd` (`elaprCycl: 2`) → `RK`.
+4. `copy-line` 자동 복사는 사용하지 않는다.
결재 `process`:
diff --git a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md
index a500fad11..9fb11d306 100644
--- a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md
+++ b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md
@@ -62,16 +62,17 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| 27 | 상세 | USER04 | - | `RC` | 조치결과서제출 | `RA` | 환경청 | `updatePrgrsStts` |
| 28 | 상세 | USER03 | - | `RA` | 실무협의서 관리 | 유지 | — | 웹기안기 Open. **저장 불가** |
| 28a | 상세 | USER03 | - | `RA` | 조치결과 검토의뢰 | `AV` | 공단 검토 | **`request-actn-review`** |
-| 29 | 자동화 | AI | - | `AV` | (금액검증) | `RK` | 공단 검토 | FE 호출 없음. 진행 패널 |
+| 29 | 자동화 | AI | - | `AV` | (금액검증) | `EV` | 공단 접수 | FE 호출 없음. 진행 패널 |
+| 29a | 상세 | USER01 | 접수자 | `EV` | 검토자지정 | `RK` | 공단 검토 | `POST /elapr-dmnd` cycl=2. 1차 결재선 시드 |
| 30 | 상세 | USER01 | 검토자 | `RK` | 실무협의서관리 | 유지 | — | 웹기안기 |
| 31 | 상세 | USER01 | 검토자 | `RK` | 금액검증 | `RK` | — | `dcrag-to/cost/verify` |
| 32 | 상세 | USER01 | 검토자 | `RK` | 저장 | `RK` | — | `pwrk save-apply` |
| 33 | 상세 | USER01 | 검토자 | `RK` | 검토완료 | `RK` | 다음 결재자 | `process` |
| 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` |
| 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 |
-| 36 | 상세 | USER03 | - | `RR` | 결과회신 | `EP` | 광역 | `updatePrgrsStts` |
-| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신 | `TA` | 기초 | `updatePrgrsStts` |
-| 38 | 상세 | USER05 | - | `TA` | 결과회신 | — | — | **API 없음. 조회** |
+| 36 | 상세 | USER03 | - | `RR` | 실무협의서 관리(조회), 검토결과 회신 | `EP` | 광역 | `tech-review` |
+| 37 | 작성/상세 | USER04 | - | `EP` | 결과회신, 실무협의서 관리(조회) | `TA` | 기초 | `updatePrgrsStts` |
+| 38 | 상세 | USER05 | - | `TA` | 결과회신, 실무협의서 관리(조회) | — | — | **API 없음. 조회** |
`SL` 20~22행: 버튼은 직전 상태에 따라 **하나만** 노출. 파일 루프면 [파일보완완료], 그 외 [보완완료].
@@ -85,7 +86,7 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| --- | --- | --- |
| `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 |
| `fileVrfc` | 파일검증 | 유지. `ER` 재실행 |
-| `dsgntRvwr` | 검토자지정 | 유지. 전이를 `AC`로 |
+| `dsgntRvwr` | 검토자지정 | `ER→AC`(1차), `EV→RK`(2차) |
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK`·`RB`·`RA`(조회 전용) |
| `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 |
| `rvwRsltInq` | 검토결과조회 | 신규 |
@@ -108,19 +109,19 @@ ROLE_USER05
"" | SV → 저장, 신청완료, 삭제
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
RB → 실무협의서 관리, 조치완료
- TA → 결과회신 (조회)
+ TA → 결과회신 (조회), 실무협의서 관리(조회)
ROLE_USER04
ST → 환경청제출 // 현행 반려(rjct→SV)는 SoT 38행에 없음. 유지 시 별도 합의
RM → 지자체회신
RC → 조치결과서제출
- EP → 결과회신
+ EP → 결과회신, 실무협의서 관리(조회)
ROLE_USER03
LE → 결과회신(자체종결), 기술검토 의뢰
TP → 재원협의 결과 회신
RA → 실무협의서 관리(조회), 조치결과 검토의뢰
- RR → 결과회신
+ RR → 실무협의서 관리(조회), 검토결과 회신 (`tech-review` → `EP`)
```
### 3-2. 공단 결재선 × 상태
@@ -129,6 +130,9 @@ ROLE_USER03
ER / ATRZ01 접수자
→ 파일보완, 파일검증, 검토자지정
+EV / ATRZ01 접수자
+ → 검토자지정 (2차 결재선. 1차 결재선 시드, 서식 선택 없음)
+
TE / ATRZ02 검토자
→ (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청
→ (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤)
@@ -137,14 +141,18 @@ TE / ATRZ03 승인자 → 결재(승인)
TE / ATRZ99 최종 → 결재(회신)
RK / ATRZ02 검토자
- → 실무협의서관리, 금액검증, 저장, 검토완료, 보완요청
-RK / ATRZ03 → 결재(승인)
-RK / ATRZ99 → 결재(회신)
+ → (초안 완료) 실무협의서관리
+ → (초안 완료 + 결재 대기 ATRZ_PRGRS_002, cycl2) 보완요청, 검토완료
+ · 보완요청 UI: TE와 동일 SplmntDmndDialog
+RK / ATRZ03 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(승인)
+RK / ATRZ99 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(회신)
+ · 결재 UI: ElaprApprovalDialog (elaprCycl=2) 승인/반려 → PUT process
```
`FV`, `AC`, `AV`: 액션 빈 배열 + 진행 패널.
-현행 `TE`에서 `hasPendingApprovalRequest === false`이면 버튼을 숨긴다. v3.0에서는 검토자 버튼(저장·웹기안기·보완)은 결재 대기와 무관하게 두고, **검토완료/결재**만 결재건 존재 여부에 묶는다.
+`PENDING_APPROVAL_ACTIONS`: `dmndSplmnt`, `rvwCmptn`, `atrzAprv`, `atrzRply`만.
+`pwrkCnsltnDocMng`는 결재 대기와 무관(초안 완료만). ATRZ02·ATRZ03·ATRZ99라도 pending 없으면 관리만 노출.
---
diff --git a/apps/as/package.json b/apps/as/package.json
index 075300898..40aa693d4 100644
--- a/apps/as/package.json
+++ b/apps/as/package.json
@@ -43,6 +43,7 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+ "@hookform/devtools": "^4.4.0",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.0.0",
"eslint-config-next": "15.3.8",
diff --git a/apps/as/public/webhwpctrl/hwpsdkmain.html b/apps/as/public/webhwpctrl/hwpsdkmain.html
index d24c7eee7..3f4bc1526 100644
--- a/apps/as/public/webhwpctrl/hwpsdkmain.html
+++ b/apps/as/public/webhwpctrl/hwpsdkmain.html
@@ -6,36 +6,57 @@
-
-
-
- Hwp SDK Wasm Sample
-
-
-
-
-
-
-
+
+
+
+
Hwp SDK Wasm Sample
+
+
+
+
+
+
+
+
+
-
-
-
-
- 구성 중 ... 0%
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts b/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts
new file mode 100644
index 000000000..124dde1dd
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/api/ai-auto-prcs-mng-service.ts
@@ -0,0 +1,24 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+
+import type {
+ AiAutoPrcsLatestResponseDto,
+ AiAutoPrcsMngService,
+} from "../model/ai-auto-prcs-mng-interface";
+
+const BASE = ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG;
+
+async function findLatestPerAutoSeByFnrsDocNo(
+ fnrsCnsltnDocNo: string,
+ atmtnSeComCd?: string,
+): Promise
{
+ const response = await client.get(
+ `${BASE}/fnrs/${fnrsCnsltnDocNo}/latest`,
+ { params: atmtnSeComCd ? { atmtnSeComCd } : undefined },
+ );
+ return response.data;
+}
+
+export const aiAutoPrcsMngService = {
+ findLatestPerAutoSeByFnrsDocNo,
+} satisfies AiAutoPrcsMngService;
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts b/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts
new file mode 100644
index 000000000..fe97be2db
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/api/queries.ts
@@ -0,0 +1,30 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { ENDPOINT } from "@/shared/config";
+
+import { aiAutoPrcsMngService } from "./ai-auto-prcs-mng-service";
+
+export const aiAutoPrcsMngKeys = {
+ all: [ENDPOINT.AS_SERVICE.AI_AUTO_PRCS_MNG] as const,
+ latest: (fnrsCnsltnDocNo: string, atmtnSeComCd?: string) =>
+ [...aiAutoPrcsMngKeys.all, "latest", fnrsCnsltnDocNo, atmtnSeComCd ?? "all"] as const,
+};
+
+interface UseAiAutoPrcsLatestOptions {
+ enabled?: boolean;
+ atmtnSeComCd?: string;
+}
+
+export function useAiAutoPrcsLatest(
+ fnrsCnsltnDocNo: string | null | undefined,
+ options: UseAiAutoPrcsLatestOptions = {},
+) {
+ const { enabled = true, atmtnSeComCd } = options;
+
+ return useQuery({
+ queryKey: aiAutoPrcsMngKeys.latest(fnrsCnsltnDocNo!, atmtnSeComCd),
+ queryFn: () =>
+ aiAutoPrcsMngService.findLatestPerAutoSeByFnrsDocNo(fnrsCnsltnDocNo!, atmtnSeComCd),
+ enabled: Boolean(fnrsCnsltnDocNo) && enabled,
+ });
+}
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/index.ts b/apps/as/src/entities/ai-auto-prcs-mng/index.ts
new file mode 100644
index 000000000..0668a4e0c
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/index.ts
@@ -0,0 +1,8 @@
+export { aiAutoPrcsMngService } from "./api/ai-auto-prcs-mng-service";
+export { aiAutoPrcsMngKeys, useAiAutoPrcsLatest } from "./api/queries";
+export type {
+ AiAutoPrcsLatestResponseDto,
+ AiAutoPrcsMngService,
+ AiAutoSeComCd,
+} from "./model/ai-auto-prcs-mng-interface";
+export { AI_AUTO_SE } from "./model/ai-auto-prcs-mng-interface";
diff --git a/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts b/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts
new file mode 100644
index 000000000..ad833b3c6
--- /dev/null
+++ b/apps/as/src/entities/ai-auto-prcs-mng/model/ai-auto-prcs-mng-interface.ts
@@ -0,0 +1,44 @@
+/** 자동화구분 공통코드 (AIAUTOSE) */
+export const AI_AUTO_SE = {
+ FILE_VRFC: "FILE_VRFC",
+ DRAFT_GEN: "DRAFT_GEN",
+ COST_VRFC: "COST_VRFC",
+} as const;
+
+export type AiAutoSeComCd = (typeof AI_AUTO_SE)[keyof typeof AI_AUTO_SE];
+
+/** 백엔드 AiAutoPrcsLatestResponseDto */
+export interface AiAutoPrcsLatestResponseDto {
+ atmtnPrcsUuid?: string;
+ fnrsCnsltnDocNo?: string;
+ pwrkCnsltnDocNo?: string;
+ crtDmndUuid?: string;
+ atmtnSeComCd?: string;
+ atmtnSeComNm?: string;
+ bfrDocSttsComCd?: string;
+ bfrDocSttsComNm?: string;
+ aftrDocSttsComCd?: string;
+ aftrDocSttsComNm?: string;
+ atmtnPrcsSttsComCd?: string;
+ atmtnPrcsSttsComNm?: string;
+ dmndDt?: string;
+ dmndSysId?: string;
+ rplyDt?: string;
+ rplyRsltComCd?: string;
+ prcsErrCn?: string | null;
+ lastSttsInqDt?: string;
+ sttsInqRsltComCd?: string;
+ rtrsmCtpvNmtm?: number;
+ lastRtrsmDt?: string;
+ lastFrcSttsChgYn?: string;
+ isFailed?: boolean;
+ isRetryable?: boolean;
+}
+
+/** Controller public 메서드명·시그니처와 동일 */
+export interface AiAutoPrcsMngService {
+ findLatestPerAutoSeByFnrsDocNo(
+ fnrsCnsltnDocNo: string,
+ atmtnSeComCd?: string,
+ ): Promise;
+}
diff --git a/apps/as/src/entities/attach/index.ts b/apps/as/src/entities/attach/index.ts
index ca804b4a0..69b220102 100644
--- a/apps/as/src/entities/attach/index.ts
+++ b/apps/as/src/entities/attach/index.ts
@@ -6,3 +6,7 @@ export {
} from "./api/queries";
export { parseFilenameFromResponse } from "./lib/parse-filename-from-response";
export type { Attach, AttachService } from "./model/attach-interface";
+export {
+ AttachmentDownloadLink,
+ type AttachmentDownloadLinkProps,
+} from "./ui/attachment-download-link";
diff --git a/apps/as/src/entities/attach/ui/attachment-download-link.tsx b/apps/as/src/entities/attach/ui/attachment-download-link.tsx
new file mode 100644
index 000000000..85aab6ec5
--- /dev/null
+++ b/apps/as/src/entities/attach/ui/attachment-download-link.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { Paperclip } from "lucide-react";
+
+import { cn } from "@common/components";
+
+import { useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "../api/queries";
+
+interface AttachmentDownloadLinkProps {
+ atflCd?: string;
+ atflId?: string;
+ fileName?: string;
+ className?: string;
+}
+
+function AttachmentDownloadLink({
+ atflCd,
+ atflId,
+ fileName,
+ className,
+}: AttachmentDownloadLinkProps) {
+ const downloadByAtflCd = useDownloadAttachmentByAtflCd();
+ const downloadByAtflId = useDownloadAttachmentByAtflId();
+ const displayName = fileName || "첨부파일";
+ const canDownload = Boolean(atflId || atflCd);
+ const isPending = atflId ? downloadByAtflId.isPending : downloadByAtflCd.isPending;
+
+ function handleClick() {
+ if (atflId) {
+ downloadByAtflId.mutate(atflId);
+ return;
+ }
+ if (atflCd) downloadByAtflCd.mutate(atflCd);
+ }
+
+ return (
+
+ );
+}
+
+export { AttachmentDownloadLink };
+export type { AttachmentDownloadLinkProps };
diff --git a/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts b/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts
index 16c2ccdbd..6643972e5 100644
--- a/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts
+++ b/apps/as/src/entities/dcrag-to/api/dcrag-to-service.ts
@@ -46,9 +46,40 @@ async function requestCostVerification(
return response.data;
}
+async function reTransferDocument(
+ body: DcRagDocumentTransferRequestDto,
+): Promise {
+ const response = await client.post(
+ `${BASE}/documents/re-transfer`,
+ body,
+ );
+ return response.data;
+}
+
+async function reGenerateDraft(
+ body: DcRagDraftGenerateRequestDto,
+): Promise {
+ const response = await client.post(
+ `${BASE}/documents/draft/re-generate`,
+ body,
+ );
+ return response.data;
+}
+
+async function reVerifyCost(body: DcRagCostVerifyRequestDto): Promise {
+ const response = await client.post(
+ `${BASE}/documents/cost/re-verify`,
+ body,
+ );
+ return response.data;
+}
+
export const dcRagApiToService = {
requestDocumentTransfer,
requestDraftGeneration,
getDraftGenerationStatus,
requestCostVerification,
+ reTransferDocument,
+ reGenerateDraft,
+ reVerifyCost,
} satisfies DcRagApiToService;
diff --git a/apps/as/src/entities/dcrag-to/api/queries.ts b/apps/as/src/entities/dcrag-to/api/queries.ts
index 4e9fc4483..bc14d9f7f 100644
--- a/apps/as/src/entities/dcrag-to/api/queries.ts
+++ b/apps/as/src/entities/dcrag-to/api/queries.ts
@@ -1,6 +1,9 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
+import { useMutationWrapper } from "@/shared/lib";
+import { aiAutoPrcsMngKeys } from "@/entities/ai-auto-prcs-mng";
+import { fnrsCnsltnKeys } from "@/entities/fnrs-cnsltn";
import type { DcRagDraftStatusResponseDto } from "../model/dcrag-to-interface";
import type {
@@ -21,6 +24,18 @@ function isDraftGenerationSettled(data: DcRagDraftStatusResponseDto | undefined)
return data.events?.some((event) => event.eventType === "COMPLETED") ?? false;
}
+function invalidateAfterAiRetry(
+ queryClient: ReturnType,
+ fnrsCnsltnDocNo?: string,
+) {
+ queryClient.invalidateQueries({ queryKey: dcRagApiToBaseKey });
+ queryClient.invalidateQueries({ queryKey: aiAutoPrcsMngKeys.all });
+ if (fnrsCnsltnDocNo) {
+ queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo) });
+ queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
+ }
+}
+
export function useDraftGenerationStatus(crtDmndUuid: string | null | undefined) {
return useQuery({
queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", crtDmndUuid],
@@ -68,3 +83,39 @@ export function useRequestCostVerification() {
},
});
}
+
+export function useReTransferDocument() {
+ const queryClient = useQueryClient();
+ return useMutationWrapper({
+ mutationFn: (body: DcRagDocumentTransferRequestDto) =>
+ dcRagApiToService.reTransferDocument(body),
+ onSuccess: (_data, body) => {
+ invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
+ },
+ });
+}
+
+export function useReGenerateDraft() {
+ const queryClient = useQueryClient();
+ return useMutationWrapper({
+ mutationFn: (body: DcRagDraftGenerateRequestDto) => dcRagApiToService.reGenerateDraft(body),
+ onSuccess: (_data, body) => {
+ invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
+ if (body.crtDmndUuid) {
+ queryClient.invalidateQueries({
+ queryKey: [...dcRagApiToBaseKey, "getDraftGenerationStatus", body.crtDmndUuid],
+ });
+ }
+ },
+ });
+}
+
+export function useReVerifyCost() {
+ const queryClient = useQueryClient();
+ return useMutationWrapper({
+ mutationFn: (body: DcRagCostVerifyRequestDto) => dcRagApiToService.reVerifyCost(body),
+ onSuccess: (_data, body) => {
+ invalidateAfterAiRetry(queryClient, body.fnrsCnsltnDocNo);
+ },
+ });
+}
diff --git a/apps/as/src/entities/dcrag-to/index.ts b/apps/as/src/entities/dcrag-to/index.ts
index 2944e98c6..2e496f7cf 100644
--- a/apps/as/src/entities/dcrag-to/index.ts
+++ b/apps/as/src/entities/dcrag-to/index.ts
@@ -2,9 +2,12 @@ export { dcRagApiToService } from "./api/dcrag-to-service";
export { dcRagApiToBaseKey } from "./api/queries";
export {
useDraftGenerationStatus,
+ useReGenerateDraft,
useRequestCostVerification,
useRequestDocumentTransfer,
useRequestDraftGeneration,
+ useReTransferDocument,
+ useReVerifyCost,
} from "./api/queries";
export type {
DcRagApiToService,
diff --git a/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts b/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts
index d80f91049..5896ee546 100644
--- a/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts
+++ b/apps/as/src/entities/dcrag-to/model/dcrag-to-interface.ts
@@ -49,4 +49,7 @@ export interface DcRagApiToService {
): Promise;
getDraftGenerationStatus(crtDmndUuid: string): Promise;
requestCostVerification(body: DcRagCostVerifyRequestDto): Promise;
+ reTransferDocument(body: DcRagDocumentTransferRequestDto): Promise;
+ reGenerateDraft(body: DcRagDraftGenerateRequestDto): Promise;
+ reVerifyCost(body: DcRagCostVerifyRequestDto): Promise;
}
diff --git a/apps/as/src/entities/design-cnstn/api/design-cnstn-mng-service.ts b/apps/as/src/entities/design-cnstn/api/design-cnstn-mng-service.ts
index 3fc3e52ae..39ff512a9 100644
--- a/apps/as/src/entities/design-cnstn/api/design-cnstn-mng-service.ts
+++ b/apps/as/src/entities/design-cnstn/api/design-cnstn-mng-service.ts
@@ -4,9 +4,11 @@ import type { PageResponse } from "@/shared/model";
import type {
DesignCnstnApplySaveRequestDto,
+ DesignCnstnMngSaveRequestDto,
+} from "../model/design-cnstn-apply-payload-schema";
+import type {
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
- DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
diff --git a/apps/as/src/entities/design-cnstn/api/design-cnstn-rvw-opnn-service.ts b/apps/as/src/entities/design-cnstn/api/design-cnstn-rvw-opnn-service.ts
index 514a0423f..ddfc3d885 100644
--- a/apps/as/src/entities/design-cnstn/api/design-cnstn-rvw-opnn-service.ts
+++ b/apps/as/src/entities/design-cnstn/api/design-cnstn-rvw-opnn-service.ts
@@ -2,10 +2,10 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
+import type { DesignCnstnRvwOpnnSaveRequestDto } from "../model/design-cnstn-apply-payload-schema";
import type {
DesignCnstnRvwOpnnId,
DesignCnstnRvwOpnnResponseDto,
- DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnService,
DesignCnstnRvwOpnnUpdateRequestDto,
diff --git a/apps/as/src/entities/design-cnstn/api/queries.ts b/apps/as/src/entities/design-cnstn/api/queries.ts
index c98a5ad15..7a04eb8d8 100644
--- a/apps/as/src/entities/design-cnstn/api/queries.ts
+++ b/apps/as/src/entities/design-cnstn/api/queries.ts
@@ -18,11 +18,13 @@ import type {
import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngSaveRequestDto,
+ DesignCnstnRvwOpnnSaveRequestDto,
+} from "../model/design-cnstn-apply-payload-schema";
+import type {
DesignCnstnMngSearchDto,
DesignCnstnMngUpdateRequestDto,
} from "../model/design-cnstn-mng-interface";
import type {
- DesignCnstnRvwOpnnSaveRequestDto,
DesignCnstnRvwOpnnSearchDto,
DesignCnstnRvwOpnnUpdateRequestDto,
} from "../model/design-cnstn-rvw-opnn-interface";
diff --git a/apps/as/src/entities/design-cnstn/index.ts b/apps/as/src/entities/design-cnstn/index.ts
index cad262c32..c9e3143de 100644
--- a/apps/as/src/entities/design-cnstn/index.ts
+++ b/apps/as/src/entities/design-cnstn/index.ts
@@ -37,6 +37,22 @@ export {
useUpdateDesignCnstnMng,
useUpdateDesignCnstnRvwOpnn,
} from "./api/queries";
+export type {
+ BizCstItem,
+ BzScaleItem,
+ DesignCnstnApplySaveRequestDto,
+ DesignCnstnMngSaveRequestDto,
+ DesignCnstnRvwOpnnSaveRequestDto,
+ EsntlAtchFileItem,
+} from "./model/design-cnstn-apply-payload-schema";
+export {
+ designCnstnApplySaveSchema,
+ designCnstnBizCstItemSchema,
+ designCnstnBzScaleItemSchema,
+ designCnstnEsntlAtchFileItemSchema,
+ designCnstnMngSaveSchema,
+ designCnstnRvwOpnnSaveSchema,
+} from "./model/design-cnstn-apply-payload-schema";
export type {
DesignCnstnBizCstId,
DesignCnstnBizCstResponseDto,
@@ -63,21 +79,15 @@ export type {
} from "./model/design-cnstn-esntl-atch-file-interface";
export type {
AtflMngClientResponseDto,
- BizCstItem,
BizCstTreeDto,
BizSclTreeDto,
- BzScaleItem,
- DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
DesignCnstnMngResponseDto,
- DesignCnstnMngSaveRequestDto,
DesignCnstnMngSearchDto,
DesignCnstnMngService,
DesignCnstnMngUpdateRequestDto,
DesignCnstnRvwOpnnResponseDto,
- DesignCnstnRvwOpnnSaveRequestDto,
ElaprDocPrgrsClientResponseDto,
- EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
} from "./model/design-cnstn-mng-interface";
export type {
diff --git a/apps/as/src/entities/design-cnstn/model/design-cnstn-apply-payload-schema.ts b/apps/as/src/entities/design-cnstn/model/design-cnstn-apply-payload-schema.ts
new file mode 100644
index 000000000..5b6c76b10
--- /dev/null
+++ b/apps/as/src/entities/design-cnstn/model/design-cnstn-apply-payload-schema.ts
@@ -0,0 +1,74 @@
+import { z } from "zod";
+
+/** 백엔드 DesignCnstnMngSaveRequestDto */
+export const designCnstnMngSaveSchema = z.object({
+ designCnstnDocNo: z.string().optional(),
+ admdstCd: z.string().optional(),
+ envoSeComCd: z.string().optional(),
+ taskSeComCd: z.string().optional(),
+ bizNm: z.string().optional(),
+ bizBgngYr: z.string().optional(),
+ bizEndYr: z.string().optional(),
+ bizCn: z.string().optional(),
+ aplcntNm: z.string().optional(),
+ aplcntMblTelno: z.string().optional(),
+ aplcntOgdpNm: z.string().optional(),
+ aplcntEmlAddr: z.string().optional(),
+ dsgnrNm: z.string().optional(),
+ dsgnrTelno: z.string().optional(),
+ dsgnrOgdpNm: z.string().optional(),
+ dsgnrEmlAddr: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+ prgrsSttsComCd: z.string().optional(),
+});
+
+/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
+export const designCnstnBzScaleItemSchema = z.object({
+ cnstpSeCd: z.string().optional(),
+ bscPl: z.number().optional(),
+ budgetReflct: z.number().optional(),
+ thtiDsgn: z.number().optional(),
+});
+
+/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
+export const designCnstnBizCstItemSchema = z.object({
+ acntgSeCd: z.string().optional(),
+ bscPlanAmt: z.number().optional(),
+ bgtRfltAmt: z.number().optional(),
+ tstmDesignAmt: z.number().optional(),
+});
+
+/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
+export const designCnstnEsntlAtchFileItemSchema = z.object({
+ esntlAtchFileId: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
+export const designCnstnRvwOpnnSaveSchema = z.object({
+ designCnstnDocNo: z.string(),
+ rvwOpnnSn: z.number().optional(),
+ opnrSeComCd: z.string().optional(),
+ opnrNm: z.string().optional(),
+ rvwRsltCn: z.string().optional(),
+ rvwRsltAnmlyYn: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 DesignCnstnApplySaveRequestDto */
+export const designCnstnApplySaveSchema = z.object({
+ techReViewSaveCd: z.string().optional(),
+ rvwStpCd: z.string().optional(),
+ designCnstnMng: designCnstnMngSaveSchema,
+ bzScaleList: z.array(designCnstnBzScaleItemSchema).optional(),
+ bizCstList: z.array(designCnstnBizCstItemSchema).optional(),
+ esntlAtchFileList: z.array(designCnstnEsntlAtchFileItemSchema).optional(),
+ rvwOpnnList: z.array(designCnstnRvwOpnnSaveSchema).optional(),
+});
+
+export type DesignCnstnMngSaveRequestDto = z.infer;
+export type BzScaleItem = z.infer;
+export type BizCstItem = z.infer;
+export type EsntlAtchFileItem = z.infer;
+export type DesignCnstnRvwOpnnSaveRequestDto = z.infer;
+export type DesignCnstnApplySaveRequestDto = z.infer;
diff --git a/apps/as/src/entities/design-cnstn/model/design-cnstn-mng-interface.ts b/apps/as/src/entities/design-cnstn/model/design-cnstn-mng-interface.ts
index 1ee2be991..bc4949eb3 100644
--- a/apps/as/src/entities/design-cnstn/model/design-cnstn-mng-interface.ts
+++ b/apps/as/src/entities/design-cnstn/model/design-cnstn-mng-interface.ts
@@ -1,22 +1,12 @@
-import type {
- BizCstSaveBase,
- BzScaleSaveBase,
- EsntlAtchFileSaveBase,
- PageableParams,
- PageResponse,
- PbadmsApplySaveHeader,
- PbadmsMngDetResponseBase,
-} from "@/shared/model";
+import type { PageableParams, PageResponse, PbadmsMngDetResponseBase } from "@/shared/model";
import type {
- DesignCnstnRvwOpnnResponseDto,
- DesignCnstnRvwOpnnSaveRequestDto,
-} from "./design-cnstn-rvw-opnn-interface";
+ DesignCnstnApplySaveRequestDto,
+ DesignCnstnMngSaveRequestDto,
+} from "./design-cnstn-apply-payload-schema";
+import type { DesignCnstnRvwOpnnResponseDto } from "./design-cnstn-rvw-opnn-interface";
-export type {
- DesignCnstnRvwOpnnResponseDto,
- DesignCnstnRvwOpnnSaveRequestDto,
-} from "./design-cnstn-rvw-opnn-interface";
+export type { DesignCnstnRvwOpnnResponseDto } from "./design-cnstn-rvw-opnn-interface";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
@@ -25,28 +15,6 @@ export type {
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
-/** 백엔드 DesignCnstnMngSaveRequestDto */
-export interface DesignCnstnMngSaveRequestDto {
- designCnstnDocNo?: string;
- admdstCd?: string;
- envoSeComCd?: string;
- taskSeComCd?: string;
- bizNm?: string;
- bizBgngYr?: string;
- bizEndYr?: string;
- bizCn?: string;
- aplcntNm?: string;
- aplcntMblTelno?: string;
- aplcntOgdpNm?: string;
- aplcntEmlAddr?: string;
- dsgnrNm?: string;
- dsgnrTelno?: string;
- dsgnrOgdpNm?: string;
- dsgnrEmlAddr?: string;
- atchFileGroupId?: string;
- prgrsSttsComCd?: string;
-}
-
/** 백엔드 DesignCnstnMngUpdateRequestDto */
export type DesignCnstnMngUpdateRequestDto = Omit;
@@ -88,30 +56,12 @@ interface DesignCnstnMngResponseExtension {
export type DesignCnstnMngResponseDto = Omit &
DesignCnstnMngResponseExtension;
-/** 백엔드 DesignCnstnApplySaveRequestDto.BzScaleItem */
-export type BzScaleItem = BzScaleSaveBase;
-
-/** 백엔드 DesignCnstnApplySaveRequestDto.BizCstItem */
-export type BizCstItem = BizCstSaveBase;
-
-/** 백엔드 DesignCnstnApplySaveRequestDto.EsntlAtchFileItem */
-export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
-
/** 백엔드 DesignCnstnMngDetResponseDto */
export type DesignCnstnMngDetResponseDto = PbadmsMngDetResponseBase<
DesignCnstnMngResponseDto,
DesignCnstnRvwOpnnResponseDto
>;
-/** 백엔드 DesignCnstnApplySaveRequestDto */
-export interface DesignCnstnApplySaveRequestDto extends PbadmsApplySaveHeader {
- designCnstnMng: DesignCnstnMngSaveRequestDto;
- bzScaleList?: BzScaleItem[];
- bizCstList?: BizCstItem[];
- esntlAtchFileList?: EsntlAtchFileItem[];
- rvwOpnnList?: DesignCnstnRvwOpnnSaveRequestDto[];
-}
-
/** Controller public 메서드명·시그니처와 동일 */
export interface DesignCnstnMngService {
findPage(searchDto: DesignCnstnMngSearchDto): Promise>;
diff --git a/apps/as/src/entities/design-cnstn/model/design-cnstn-rvw-opnn-interface.ts b/apps/as/src/entities/design-cnstn/model/design-cnstn-rvw-opnn-interface.ts
index 8b9841ee6..56cc7b94c 100644
--- a/apps/as/src/entities/design-cnstn/model/design-cnstn-rvw-opnn-interface.ts
+++ b/apps/as/src/entities/design-cnstn/model/design-cnstn-rvw-opnn-interface.ts
@@ -1,22 +1,13 @@
import type { PageableParams, PageResponse, RvwOpnnResponseExtension } from "@/shared/model";
+import type { DesignCnstnRvwOpnnSaveRequestDto } from "./design-cnstn-apply-payload-schema";
+
/** 백엔드 DesignCnstnRvwOpnnId */
export interface DesignCnstnRvwOpnnId {
designCnstnMng: string;
rvwOpnnSn: number;
}
-/** 백엔드 DesignCnstnRvwOpnnSaveRequestDto */
-export interface DesignCnstnRvwOpnnSaveRequestDto {
- designCnstnDocNo: string;
- rvwOpnnSn?: number;
- opnrSeComCd?: string;
- opnrNm?: string;
- rvwRsltCn?: string;
- rvwRsltAnmlyYn?: string;
- atchFileGroupId?: string;
-}
-
/** 백엔드 DesignCnstnRvwOpnnUpdateRequestDto */
export interface DesignCnstnRvwOpnnUpdateRequestDto {
opnrSeComCd?: string;
diff --git a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts
index 368600110..b366afb60 100644
--- a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts
+++ b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts
@@ -49,9 +49,13 @@ async function remove(elaprDmndId: string): Promise {
await client.delete(`${BASE}/${elaprDmndId}`);
}
-async function findDocPrgrsByDocNo(elaprDocNo: string): Promise {
+async function findDocPrgrsByDocNo(
+ elaprDocNo: string,
+ elaprCycl?: number,
+): Promise {
const response = await client.get(
`${BASE}/sanc/docprgrs/${elaprDocNo}`,
+ { params: elaprCycl != null ? { elaprCycl } : undefined },
);
return response.data;
}
diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts
index 734c6cdf4..9908ef44f 100644
--- a/apps/as/src/entities/elapr/api/queries.ts
+++ b/apps/as/src/entities/elapr/api/queries.ts
@@ -185,11 +185,16 @@ export function useElaprDmnd(elaprDmndId: string | null | undefined) {
});
}
-export function useElaprDocPrgrsByDocNo(elaprDocNo: string | null | undefined) {
+export function useElaprDocPrgrsByDocNo(
+ elaprDocNo: string | null | undefined,
+ options?: { elaprCycl?: number; enabled?: boolean; structuralSharing?: boolean },
+) {
+ const elaprCycl = options?.elaprCycl;
return useQuery({
- queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo],
- queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!),
- enabled: Boolean(elaprDocNo),
+ queryKey: [...elaprDmndBaseKey, "findDocPrgrsByDocNo", elaprDocNo, elaprCycl],
+ queryFn: () => elaprDmndService.findDocPrgrsByDocNo(elaprDocNo!, elaprCycl),
+ enabled: Boolean(elaprDocNo) && (options?.enabled ?? true),
+ structuralSharing: options?.structuralSharing ?? true,
});
}
@@ -284,13 +289,8 @@ export function useProcessElaprPrgrsStts() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: ({
- elaprPrgrsId,
- body,
- }: {
- elaprPrgrsId: string;
- body: ElaprProcessRequestDto;
- }) => elaprPrgrsSttsService.process(elaprPrgrsId, body),
+ mutationFn: ({ elaprPrgrsId, body }: { elaprPrgrsId: string; body: ElaprProcessRequestDto }) =>
+ elaprPrgrsSttsService.process(elaprPrgrsId, body),
onSuccess: () => {
invalidateElaprPrgrsSttsQueries(queryClient);
},
diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts
index 4805fecc3..777245649 100644
--- a/apps/as/src/entities/elapr/index.ts
+++ b/apps/as/src/entities/elapr/index.ts
@@ -57,6 +57,7 @@ export {
buildElaprPrgrsSttsListFromApln,
buildElaprPrgrsSttsListFromAplnRows,
} from "./lib/dmnd-save-utils";
+export { mapDocPrgrsToAplnRows } from "./lib/doc-prgrs-to-apln-rows";
export type { ElaprPrgrsSttsRow } from "./lib/prgrs-stts-save-utils";
export {
assignPrgrsSttsAtrzSeqNumbers,
@@ -113,6 +114,10 @@ export type { ReviewerOption } from "./model/reviewer-option";
export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog";
export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid";
export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog";
+export {
+ type ElaprDocPrgrsPickerConfirmPayload,
+ ElaprDocPrgrsPickerDialog,
+} from "./ui/elapr-doc-prgrs-picker-dialog";
export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status";
export {
type ElaprTmpltPickerConfirmPayload,
diff --git a/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts b/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts
new file mode 100644
index 000000000..a4cf562d2
--- /dev/null
+++ b/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts
@@ -0,0 +1,21 @@
+import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
+
+import type { ElaprApln } from "../model/elapr-apln-interface";
+
+/** 문서 결재진행 → 결재선 그리드 시드 (ElaprApln 호환) */
+export function mapDocPrgrsToAplnRows(
+ list: ElaprDocPrgrsClientResponseDto[] | undefined,
+): ElaprApln[] {
+ if (!list?.length) return [];
+
+ return list.map((item, index) => ({
+ aplnId: item.elaprPrgrsId ?? `prgrs-${item.atrzSeqNo ?? index}`,
+ elaprTmpltId: "",
+ atrzSeqNo: item.atrzSeqNo,
+ atrzSeComCd: item.atrzSeComCd,
+ atrzSeComNm: item.atrzSeComNm,
+ aprvrUuid: item.aprvrUuid,
+ aprvrNm: item.aprvrNm,
+ aprvrUserId: item.aprvrUserId,
+ }));
+}
diff --git a/apps/as/src/entities/elapr/lib/use-apln-rows.ts b/apps/as/src/entities/elapr/lib/use-apln-rows.ts
index 384c8ffc1..576b722b4 100644
--- a/apps/as/src/entities/elapr/lib/use-apln-rows.ts
+++ b/apps/as/src/entities/elapr/lib/use-apln-rows.ts
@@ -22,33 +22,35 @@ function toRow(item: ElaprApln): ElaprAplnRow {
}
interface UseAplnRowsOptions {
- elaprTmpltId: string | null;
+ seedKey: string | null;
serverRows: ElaprApln[] | undefined;
+ elaprTmpltId?: string | null;
}
export function createDraftRowKey(): string {
return `draft-${Date.now()}`;
}
-export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
+export function useAplnRows({ seedKey, serverRows, elaprTmpltId = "" }: UseAplnRowsOptions) {
const [rows, setRows] = useState([]);
const [editingRowKey, setEditingRowKey] = useState(null);
useEffect(() => {
- if (!elaprTmpltId) {
+ if (!seedKey) {
setRows([]);
setEditingRowKey(null);
return;
}
setRows((serverRows ?? []).map(toRow));
setEditingRowKey(null);
- }, [elaprTmpltId, serverRows]);
+ }, [seedKey, serverRows]);
const isEditingLocked = editingRowKey != null;
const visibleRows = getVisibleRows(rows);
+ const canAdd = Boolean(seedKey) && !isEditingLocked;
const addRow = useCallback(() => {
- if (!elaprTmpltId || isEditingLocked) return;
+ if (!canAdd) return;
const rowKey = createDraftRowKey();
@@ -56,7 +58,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
...prev,
{
rowKey,
- elaprTmpltId,
+ elaprTmpltId: elaprTmpltId ?? "",
atrzSeComCd: "",
aprvrUuid: "",
aprvrNm: "",
@@ -66,7 +68,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
},
]);
setEditingRowKey(rowKey);
- }, [elaprTmpltId, isEditingLocked]);
+ }, [canAdd, elaprTmpltId]);
const startEdit = useCallback(
(rowKey: string) => {
diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts
index 264f45db7..5d05fd04b 100644
--- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts
+++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts
@@ -45,5 +45,5 @@ export interface ElaprDmndService {
update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise;
updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise;
remove(elaprDmndId: string): Promise;
- findDocPrgrsByDocNo(elaprDocNo: string): Promise;
+ findDocPrgrsByDocNo(elaprDocNo: string, elaprCycl?: number): Promise;
}
diff --git a/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx
index 92c3d58c5..c8c84e1c6 100644
--- a/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx
+++ b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx
@@ -20,6 +20,7 @@ import { useElaprAplnPage } from "../api/queries";
import { validateAplnRow } from "../lib/apln-validators";
import { useAplnRows } from "../lib/use-apln-rows";
import { useDndSensors } from "../lib/use-dnd-sensors";
+import type { ElaprApln } from "../model/elapr-apln-interface";
import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
import type { ReviewerOption } from "../model/reviewer-option";
@@ -27,13 +28,17 @@ import { AprvrPickerDialog } from "./aprvr-picker-dialog";
import { SortableAplnPickerRow } from "./sortable-apln-picker-row";
interface ElaprAplnPickerGridProps {
- elaprTmpltId: string | null;
+ elaprTmpltId?: string | null;
+ externalServerRows?: ElaprApln[];
+ isExternalLoading?: boolean;
onRowsChange?: (rows: ElaprAplnRow[]) => void;
onEditingLockedChange?: (locked: boolean) => void;
}
function ElaprAplnPickerGrid({
- elaprTmpltId,
+ elaprTmpltId = null,
+ externalServerRows,
+ isExternalLoading = false,
onRowsChange,
onEditingLockedChange,
}: ElaprAplnPickerGridProps) {
@@ -41,14 +46,18 @@ function ElaprAplnPickerGrid({
const [pickingRowKey, setPickingRowKey] = useState(null);
const alert = useAlertStore((state) => state.setMessage);
+ const isExternalMode = externalServerRows != null;
const aplnQuery = useElaprAplnPage({
- elaprTmpltId: elaprTmpltId ?? undefined,
+ elaprTmpltId: !isExternalMode ? (elaprTmpltId ?? undefined) : undefined,
page: 0,
size: 200,
sort: "atrzSeqNo,asc",
});
+ const seedKey = isExternalMode ? "external-doc-prgrs" : elaprTmpltId;
+ const serverRows = isExternalMode ? externalServerRows : aplnQuery.data?.content;
+
const {
rows,
visibleRows,
@@ -62,8 +71,9 @@ function ElaprAplnPickerGrid({
markDeleted,
reorderRows,
} = useAplnRows({
- elaprTmpltId,
- serverRows: aplnQuery.data?.content,
+ seedKey,
+ serverRows,
+ elaprTmpltId: elaprTmpltId ?? "",
});
useEffect(() => {
@@ -75,8 +85,12 @@ function ElaprAplnPickerGrid({
}, [isEditingLocked, onEditingLockedChange]);
const sensors = useDndSensors();
- const isDisabled = !elaprTmpltId;
+ const isReady = isExternalMode || Boolean(elaprTmpltId);
+ const isLoading = isExternalMode
+ ? isExternalLoading
+ : aplnQuery.isPending && Boolean(elaprTmpltId);
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
+ const excludedVisibleRows = visibleRows.filter((r) => r.atrzSeComCd !== "ATRZ01");
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
@@ -121,7 +135,7 @@ function ElaprAplnPickerGrid({
size="sm"
type="button"
color="green"
- disabled={isDisabled || isEditingLocked}
+ disabled={!isReady || isEditingLocked}
onClick={addRow}
>
결재선 추가
@@ -129,15 +143,15 @@ function ElaprAplnPickerGrid({
- {aplnQuery.isPending && elaprTmpltId ? (
+ {isLoading ? (
- ) : !elaprTmpltId ? (
+ ) : !isReady ? (
결재서식을 선택하세요.
- ) : visibleRows.length === 0 ? (
-
+ ) : excludedVisibleRows.length === 0 ? (
+
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
) : (
@@ -161,10 +175,10 @@ function ElaprAplnPickerGrid({
r.rowKey)}
+ items={excludedVisibleRows.map((r) => r.rowKey)}
strategy={verticalListSortingStrategy}
>
- {visibleRows.map((row, index) => (
+ {excludedVisibleRows.map((row, index) => (
void;
elaprDocNo: string;
currentUserUuid: string;
+ elaprCycl?: number;
onSuccess: () => void;
}
@@ -58,6 +59,7 @@ function ElaprApprovalDialog({
onOpenChange,
elaprDocNo,
currentUserUuid,
+ elaprCycl,
onSuccess,
}: ElaprApprovalDialogProps) {
const queryClient = useQueryClient();
@@ -67,9 +69,16 @@ function ElaprApprovalDialog({
const [pickerOpen, setPickerOpen] = useState(false);
const [pickingRowKey, setPickingRowKey] = useState(null);
- const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null);
+ const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null, {
+ elaprCycl,
+ structuralSharing: false,
+ });
+ const cyclDocPrgrsList = useMemo(() => {
+ if (elaprCycl == null) return elaprDocPrgrsList;
+ return elaprDocPrgrsList?.filter((row) => Number(row.elaprCycl) === elaprCycl);
+ }, [elaprCycl, elaprDocPrgrsList]);
- const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId;
+ const elaprDmndId = cyclDocPrgrsList?.[0]?.elaprDmndId;
const {
rows,
@@ -85,22 +94,19 @@ function ElaprApprovalDialog({
markDeleted,
reorderRows,
} = usePrgrsSttsRows({
- serverRows: elaprDocPrgrsList,
+ serverRows: cyclDocPrgrsList,
elaprDmndId,
});
const savePrgrsSttsMutation = useSaveElaprPrgrsStts();
const sensors = useDndSensors();
- const currentUserPrgrs = findCurrentUserPendingApprovalRequest(
- elaprDocPrgrsList,
- currentUserUuid,
- );
+ const currentUserPrgrs = findCurrentUserPendingApprovalRequest(cyclDocPrgrsList, currentUserUuid);
const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId);
const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined;
const isBusy = isProcessing || savePrgrsSttsMutation.isPending;
- const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, elaprDocPrgrsList ?? []);
+ const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, cyclDocPrgrsList ?? []);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
@@ -164,7 +170,7 @@ function ElaprApprovalDialog({
return;
}
- const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []);
+ const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, cyclDocPrgrsList ?? []);
if (payload.length === 0) {
alert("변경된 결재선이 없습니다.");
return;
diff --git a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-picker-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-picker-dialog.tsx
new file mode 100644
index 000000000..0d9b5161e
--- /dev/null
+++ b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-picker-dialog.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useCallback, useMemo, useState } from "react";
+
+import { useAlertStore } from "@/shared/lib";
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@common/components";
+
+import { useElaprDocPrgrsByDocNo } from "../api/queries";
+import {
+ getVisibleRows,
+ validateAllRowsForSave,
+ validateAtrzOrder,
+ validateUniqueSeqNo,
+} from "../lib/apln-validators";
+import { mapDocPrgrsToAplnRows } from "../lib/doc-prgrs-to-apln-rows";
+import type { ElaprAplnRow } from "../model/elapr-apln-row-types";
+
+import { ElaprAplnPickerGrid } from "./elapr-apln-picker-grid";
+
+interface ElaprDocPrgrsPickerConfirmPayload {
+ aplnRows: ElaprAplnRow[];
+}
+
+interface ElaprDocPrgrsPickerDialogProps {
+ open: boolean;
+ elaprDocNo: string;
+ onClose: () => void;
+ onConfirm?: (payload: ElaprDocPrgrsPickerConfirmPayload) => void;
+}
+
+function ElaprDocPrgrsPickerDialog({
+ open,
+ elaprDocNo,
+ onClose,
+ onConfirm,
+}: ElaprDocPrgrsPickerDialogProps) {
+ const alert = useAlertStore((state) => state.setMessage);
+ const [aplnRows, setAplnRows] = useState([]);
+ const [isEditingLocked, setIsEditingLocked] = useState(false);
+
+ const docPrgrsQuery = useElaprDocPrgrsByDocNo(elaprDocNo, {
+ elaprCycl: 1,
+ enabled: open && Boolean(elaprDocNo),
+ });
+
+ const externalServerRows = useMemo(
+ () => mapDocPrgrsToAplnRows(docPrgrsQuery.data),
+ [docPrgrsQuery.data],
+ );
+
+ const handleRowsChange = useCallback((rows: ElaprAplnRow[]) => {
+ setAplnRows(rows);
+ }, []);
+
+ const handleEditingLockedChange = useCallback((locked: boolean) => {
+ setIsEditingLocked(locked);
+ }, []);
+
+ function handleOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ setAplnRows([]);
+ setIsEditingLocked(false);
+ onClose();
+ }
+ }
+
+ function handleConfirm() {
+ const rowsError = validateAllRowsForSave(aplnRows, isEditingLocked);
+ if (rowsError) {
+ alert(rowsError);
+ return;
+ }
+
+ const orderError = validateAtrzOrder(aplnRows);
+ if (orderError) {
+ alert(orderError);
+ return;
+ }
+
+ const seqError = validateUniqueSeqNo(aplnRows);
+ if (seqError) {
+ alert(seqError);
+ return;
+ }
+
+ const visibleRows = getVisibleRows(aplnRows);
+ onConfirm?.({ aplnRows: visibleRows });
+
+ setAplnRows([]);
+ setIsEditingLocked(false);
+ onClose();
+ }
+
+ return (
+
+ );
+}
+
+export { ElaprDocPrgrsPickerDialog };
+export type { ElaprDocPrgrsPickerConfirmPayload };
diff --git a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx
index 4be4cf0c3..ea23484e4 100644
--- a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx
+++ b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Fragment } from "react";
+import { Fragment, useEffect, useMemo, useState } from "react";
import { ChevronRight } from "lucide-react";
@@ -8,7 +8,7 @@ import { dayjs, useUserStore } from "@/shared/lib";
import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model";
import { SectionCollapsible } from "@/shared/ui";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
-import { cn, Skeleton } from "@common/components";
+import { cn, Skeleton, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { useElaprDocPrgrsByDocNo } from "../api/queries";
import { getApproverStatus, sortByAtrzSeqNo } from "../lib/approval-utils";
@@ -67,10 +67,46 @@ function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) {
);
}
+function ElaprDocPrgrsStepList({ steps }: { steps: ElaprDocPrgrsClientResponseDto[] }) {
+ if (steps.length === 0) {
+ return 결재선이 없습니다.
;
+ }
+
+ return (
+
+ {steps.map((item, index) => (
+
+ {index > 0 && (
+
+
+
+ )}
+
+
+ ))}
+
+ );
+}
+
function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
const userInfo = useUserStore((state) => state.userInfo);
const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo);
- const steps = sortByAtrzSeqNo(data ?? []);
+ const [activeCycl, setActiveCycl] = useState<"1" | "2">("1");
+
+ const { cycl1Steps, cycl2Steps, hasCycl2 } = useMemo(() => {
+ const rows = data ?? [];
+ const cycl1 = sortByAtrzSeqNo(rows.filter((item) => Number(item.elaprCycl ?? 1) !== 2));
+ const cycl2 = sortByAtrzSeqNo(rows.filter((item) => Number(item.elaprCycl) === 2));
+ return {
+ cycl1Steps: cycl1,
+ cycl2Steps: cycl2,
+ hasCycl2: cycl2.length > 0,
+ };
+ }, [data]);
+
+ useEffect(() => {
+ setActiveCycl(hasCycl2 ? "2" : "1");
+ }, [hasCycl2]);
if (userInfo?.dtlInstCd !== "ROLE_USER01") {
return null;
@@ -88,24 +124,40 @@ function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) {
);
}
- if (steps.length === 0) {
+ if (cycl1Steps.length === 0 && cycl2Steps.length === 0) {
return null;
}
return (
-
- {steps.map((item, index) => (
-
- {index > 0 && (
-
- {}
-
- )}
-
-
- ))}
-
+ {hasCycl2 ? (
+ setActiveCycl(value as "1" | "2")}
+ className="p-2"
+ >
+
+
+ 재원협의
+
+
+ 실무협의서
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ )}
);
}
diff --git a/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts b/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts
index b48e5a059..ffac5eb4a 100644
--- a/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts
+++ b/apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-service.ts
@@ -3,12 +3,14 @@ import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
import type {
- DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
+ FnrsCnsltnMngSaveRequestDto,
+} from "../model/fnrs-cnsltn-apply-payload-schema";
+import type {
+ DocPrgrsUpdateRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
- FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
diff --git a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts
index 357aa58c0..e136bd59f 100644
--- a/apps/as/src/entities/fnrs-cnsltn/api/queries.ts
+++ b/apps/as/src/entities/fnrs-cnsltn/api/queries.ts
@@ -1,9 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
- DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnMngSaveRequestDto,
+} from "../model/fnrs-cnsltn-apply-payload-schema";
+import type {
+ DocPrgrsUpdateRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
diff --git a/apps/as/src/entities/fnrs-cnsltn/index.ts b/apps/as/src/entities/fnrs-cnsltn/index.ts
index 31bc2f528..19941fb04 100644
--- a/apps/as/src/entities/fnrs-cnsltn/index.ts
+++ b/apps/as/src/entities/fnrs-cnsltn/index.ts
@@ -16,30 +16,44 @@ export {
useUpdateFnrsCnsltnMng,
} from "./api/queries";
export type {
- AtflMngClientResponseDto,
BizCstItem,
- BizCstTreeDto,
- BizSclTreeDto,
BzScaleItem,
CklstResultItem,
+ EsntlAtchFileItem,
+ FnrsCnsltnApplySaveRequestDto,
+ FnrsCnsltnMngSaveRequestDto,
+ FnrsCnsltnRvwOpnnSaveRequestDto,
+ FnrsCnsltnSplmntDmndItem,
+ FnrsSharItem,
+} from "./model/fnrs-cnsltn-apply-payload-schema";
+export {
+ fnrsCnsltnApplySaveSchema,
+ fnrsCnsltnBizCstItemSchema,
+ fnrsCnsltnBzScaleItemSchema,
+ fnrsCnsltnCklstResultItemSchema,
+ fnrsCnsltnEsntlAtchFileItemSchema,
+ fnrsCnsltnMngSaveSchema,
+ fnrsCnsltnRvwOpnnSaveSchema,
+ fnrsCnsltnSplmntDmndItemSchema,
+ fnrsSharItemSchema,
+} from "./model/fnrs-cnsltn-apply-payload-schema";
+export type {
+ AtflMngClientResponseDto,
+ BizCstTreeDto,
+ BizSclTreeDto,
CklstRvwRsltTreeDto,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
- EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
- FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
- FnrsCnsltnMngSaveRequestDto,
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
FnrsCnsltnRvwOpnnResponseDto,
- FnrsCnsltnRvwOpnnSaveRequestDto,
- FnrsCnsltnSplmntDmndItem,
FnrsCnsltnSplmntDmndResponseDto,
- FnrsSharItem,
+ RagIndexingInfoDto,
} from "./model/fnrs-cnsltn-interface";
export type {
EnvoActnReviewReqDto,
diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-apply-payload-schema.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-apply-payload-schema.ts
new file mode 100644
index 000000000..709ae9012
--- /dev/null
+++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-apply-payload-schema.ts
@@ -0,0 +1,120 @@
+import { z } from "zod";
+
+/** 백엔드 FnrsCnsltnMngSaveRequestDto */
+export const fnrsCnsltnMngSaveSchema = z.object({
+ fnrsCnsltnDocNo: z.string().optional(),
+ admdstCd: z.string().optional(),
+ envoSeComCd: z.string().optional(),
+ taskSeComCd: z.string().optional(),
+ cfmtnBgtBizNo: z.string().optional(),
+ bgtAplyYr: z.string().optional(),
+ bizNm: z.string().optional(),
+ bizBgngYr: z.string().optional(),
+ bizEndYr: z.string().optional(),
+ bizCn: z.string().optional(),
+ aplcntNm: z.string().optional(),
+ aplcntMblTelno: z.string().optional(),
+ aplcntOgdpNm: z.string().optional(),
+ aplcntEmlAddr: z.string().optional(),
+ dsgnrNm: z.string().optional(),
+ dsgnrTelno: z.string().optional(),
+ dsgnrOgdpNm: z.string().optional(),
+ dsgnrEmlAddr: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+ sharAtchFileGroupId: z.string().optional(),
+ prgrsSttsComCd: z.string().optional(),
+ rtlztnFcltSeComCd: z.string().optional(),
+ rtlztnUsgSeComCd: z.string().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.BzScaleItem */
+export const fnrsCnsltnBzScaleItemSchema = z.object({
+ cnstpSeCd: z.string().optional(),
+ bscPl: z.number().optional(),
+ budgetReflct: z.number().optional(),
+ thtiDsgn: z.number().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.BizCstItem */
+export const fnrsCnsltnBizCstItemSchema = z.object({
+ acntgSeCd: z.string().optional(),
+ bscPlanAmt: z.number().optional(),
+ bgtRfltAmt: z.number().optional(),
+ tstmDesignAmt: z.number().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.CklstResultItem */
+export const fnrsCnsltnCklstResultItemSchema = z.object({
+ cklstRvwSn: z.number().optional(),
+ cklstArtclSn: z.number().optional(),
+ cklstSn: z.number().optional(),
+ rvwStpComCd: z.string().nullish(),
+ selected: z.enum(["Y", "N"]).nullish(),
+ picRvwCn: z.string().nullish(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.EsntlAtchFileItem */
+export const fnrsCnsltnEsntlAtchFileItemSchema = z.object({
+ esntlAtchFileId: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsCnsltnSplmntDmndItem */
+export const fnrsCnsltnSplmntDmndItemSchema = z.object({
+ splmntDmndSn: z.number().nullish(),
+ splmntArtclComCd: z.string().nullish(),
+ splmntDmndCn: z.string().optional(),
+ splmntDmndDt: z.string().optional(),
+ splmntDmndAtchFileGroupId: z.string().optional(),
+ splmntCn: z.string().optional(),
+ splmntPicNm: z.string().optional(),
+ splmntCmptnDt: z.string().optional(),
+ splmntAtchFileGroupId: z.string().optional(),
+ cmptnYn: z.string().optional(),
+});
+
+/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
+export const fnrsCnsltnRvwOpnnSaveSchema = z.object({
+ fnrsCnsltnDocNo: z.string(),
+ rvwOpnnSn: z.number().optional(),
+ opnrSeComCd: z.string().optional(),
+ opnrNm: z.string().optional(),
+ rvwRsltCn: z.string().optional(),
+ rvwRsltAnmlyYn: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
+export const fnrsSharItemSchema = z.object({
+ fnrsSharSeCd: z.string().optional(),
+ exstnAmt: z.number().optional(),
+ chgRqtAmt: z.number().optional(),
+ ajmtAmt: z.number().optional(),
+ rmrkCn: z.string().optional(),
+ envoRmrkCn: z.string().optional(),
+});
+
+/** 백엔드 FnrsCnsltnApplySaveRequestDto */
+export const fnrsCnsltnApplySaveSchema = z.object({
+ techReViewSaveCd: z.string().optional(),
+ rvwStpCd: z.string().optional(),
+ fnrsCnsltnMng: fnrsCnsltnMngSaveSchema,
+ bzScaleList: z.array(fnrsCnsltnBzScaleItemSchema).optional(),
+ bizCstList: z.array(fnrsCnsltnBizCstItemSchema).optional(),
+ cklstResultList: z.array(fnrsCnsltnCklstResultItemSchema).optional(),
+ cklstResultMap: z.record(z.array(fnrsCnsltnCklstResultItemSchema)).optional(),
+ esntlAtchFileList: z.array(fnrsCnsltnEsntlAtchFileItemSchema).optional(),
+ rvwOpnnList: z.array(fnrsCnsltnRvwOpnnSaveSchema).optional(),
+ splmntDmndList: z.array(fnrsCnsltnSplmntDmndItemSchema).optional(),
+ fnrsSharList: z.array(fnrsSharItemSchema).optional(),
+});
+
+export type FnrsCnsltnMngSaveRequestDto = z.infer;
+export type BzScaleItem = z.infer;
+export type BizCstItem = z.infer;
+export type CklstResultItem = z.infer;
+export type EsntlAtchFileItem = z.infer;
+export type FnrsCnsltnSplmntDmndItem = z.infer;
+export type FnrsCnsltnRvwOpnnSaveRequestDto = z.infer;
+export type FnrsSharItem = z.infer;
+export type FnrsCnsltnApplySaveRequestDto = z.infer;
diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts
index 8e50d5d79..07d3983e9 100644
--- a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts
+++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-interface.ts
@@ -1,21 +1,20 @@
import type {
AtflMngClientResponseDto,
- BizCstSaveBase,
- BzScaleSaveBase,
- CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
- EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
- PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
- RvwOpnnSaveBase,
SplmntDmndResponseExtension,
- SplmntDmndSaveBase,
} from "@/shared/model";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
+import type {
+ FnrsCnsltnApplySaveRequestDto,
+ FnrsCnsltnMngSaveRequestDto,
+ FnrsCnsltnRvwOpnnSaveRequestDto,
+ FnrsCnsltnSplmntDmndItem,
+} from "./fnrs-cnsltn-apply-payload-schema";
import type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
@@ -31,33 +30,6 @@ export type {
EsntlAtchFileMngTreeResponseDto,
} from "@/shared/model";
-/** 백엔드 FnrsCnsltnMngSaveRequestDto */
-export interface FnrsCnsltnMngSaveRequestDto {
- fnrsCnsltnDocNo?: string;
- admdstCd?: string;
- envoSeComCd?: string;
- taskSeComCd?: string;
- cfmtnBgtBizNo?: string;
- bgtAplyYr?: string;
- bizNm?: string;
- bizBgngYr?: string;
- bizEndYr?: string;
- bizCn?: string;
- aplcntNm?: string;
- aplcntMblTelno?: string;
- aplcntOgdpNm?: string;
- aplcntEmlAddr?: string;
- dsgnrNm?: string;
- dsgnrTelno?: string;
- dsgnrOgdpNm?: string;
- dsgnrEmlAddr?: string;
- atchFileGroupId?: string;
- sharAtchFileGroupId?: string;
- prgrsSttsComCd?: string;
- rtlztnFcltSeComCd?: string;
- rtlztnUsgSeComCd?: string;
-}
-
/** 백엔드 FnrsCnsltnMngUpdateRequestDto */
export type FnrsCnsltnMngUpdateRequestDto = Omit;
@@ -110,34 +82,6 @@ interface FnrsCnsltnMngResponseExtension {
export type FnrsCnsltnMngResponseDto = Omit &
FnrsCnsltnMngResponseExtension;
-export type BzScaleItem = BzScaleSaveBase;
-
-export type BizCstItem = BizCstSaveBase;
-
-export interface CklstResultItem extends CklstResultSaveBase {
- selected?: string;
-}
-
-export type EsntlAtchFileItem = EsntlAtchFileSaveBase;
-
-export type FnrsCnsltnSplmntDmndItem = SplmntDmndSaveBase;
-
-/** 백엔드 FnrsCnsltnApplySaveRequestDto.FnrsSharItem */
-export interface FnrsSharItem {
- fnrsSharSeCd?: string;
- exstnAmt?: number;
- chgRqtAmt?: number;
- ajmtAmt?: number;
- rmrkCn?: string;
- envoRmrkCn?: string;
-}
-
-/** 백엔드 FnrsCnsltnRvwOpnnSaveRequestDto */
-export interface FnrsCnsltnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
- fnrsCnsltnDocNo: string;
- rvwOpnnSn: number;
-}
-
/** 백엔드 FnrsCnsltnRvwOpnnResponseDto */
export type FnrsCnsltnRvwOpnnResponseDto = Partial<
Pick
@@ -151,6 +95,14 @@ export type FnrsCnsltnSplmntDmndResponseDto = FnrsCnsltnSplmntDmndItem &
fnrsCnsltnDocNo?: string;
};
+/** 백엔드 RagIndexingInfoDto */
+export interface RagIndexingInfoDto {
+ isInProgress: boolean;
+ elapsedSeconds?: number;
+ estimatedRemainingSeconds?: number;
+ message?: string;
+}
+
/** 백엔드 FnrsCnsltnMngDetResponseDto */
export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase<
FnrsCnsltnMngResponseDto,
@@ -159,21 +111,9 @@ export type FnrsCnsltnMngDetResponseDto = PbadmsMngDetResponseBase<
> & {
fnrsSharList?: FnrsSharBizCstTreeDto[];
sharAtchFileList?: AtflMngClientResponseDto[];
+ ragIndexingInfo?: RagIndexingInfoDto | null;
};
-/** 백엔드 FnrsCnsltnApplySaveRequestDto */
-export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader {
- fnrsCnsltnMng: FnrsCnsltnMngSaveRequestDto;
- bzScaleList?: BzScaleItem[];
- bizCstList?: BizCstItem[];
- cklstResultList?: CklstResultItem[];
- cklstResultMap?: Record;
- esntlAtchFileList?: EsntlAtchFileItem[];
- rvwOpnnList?: FnrsCnsltnRvwOpnnSaveRequestDto[];
- splmntDmndList?: FnrsCnsltnSplmntDmndItem[];
- fnrsSharList?: FnrsSharItem[];
-}
-
/** 백엔드 FnrsCnsltnFileVrfcRsltResponseDto */
export interface FnrsCnsltnFileVrfcRsltResponseDto {
fileSeComCd?: string;
diff --git a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts
index 2bef72628..4c442a3da 100644
--- a/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts
+++ b/apps/as/src/entities/instl-sncn/api/instl-sncn-service.ts
@@ -2,13 +2,15 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageResponse } from "@/shared/model";
+import type {
+ InstlSncnApplySaveRequestDto,
+ InstlSncnMngSaveRequestDto,
+} from "../model/instl-sncn-apply-payload-schema";
import type {
AdstZoneResponseDto,
DocPrgrsUpdateRequestDto,
- InstlSncnApplySaveRequestDto,
InstlSncnMngDetResponse,
InstlSncnMngResponseDto,
- InstlSncnMngSaveRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngService,
InstlSncnMngUpdateRequestDto,
diff --git a/apps/as/src/entities/instl-sncn/api/queries.ts b/apps/as/src/entities/instl-sncn/api/queries.ts
index f9d94dbff..278277668 100644
--- a/apps/as/src/entities/instl-sncn/api/queries.ts
+++ b/apps/as/src/entities/instl-sncn/api/queries.ts
@@ -3,9 +3,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import type {
- DocPrgrsUpdateRequestDto,
InstlSncnApplySaveRequestDto,
InstlSncnMngSaveRequestDto,
+} from "../model/instl-sncn-apply-payload-schema";
+import type {
+ DocPrgrsUpdateRequestDto,
InstlSncnMngSearchParams,
InstlSncnMngUpdateRequestDto,
} from "../model/instl-sncn-interface";
diff --git a/apps/as/src/entities/instl-sncn/index.ts b/apps/as/src/entities/instl-sncn/index.ts
index 946e756f1..5579b4c06 100644
--- a/apps/as/src/entities/instl-sncn/index.ts
+++ b/apps/as/src/entities/instl-sncn/index.ts
@@ -9,6 +9,26 @@ export {
useInstlSncnSubmitSupplement,
useInstlSncnTechReview,
} from "./api/queries";
+export type {
+ InstlSncnApplySaveRequestDto,
+ InstlSncnBizCstSaveRequestDto,
+ InstlSncnBzScaleSaveDto,
+ InstlSncnCklstResultSaveDto,
+ InstlSncnEsntlAtchFileSaveDto,
+ InstlSncnMngSaveRequestDto,
+ InstlSncnRvwOpnnSaveRequestDto,
+ InstlSncnSplmntDmndSaveDto,
+} from "./model/instl-sncn-apply-payload-schema";
+export {
+ instlSncnApplySaveSchema,
+ instlSncnBizCstItemSchema,
+ instlSncnBzScaleItemSchema,
+ instlSncnCklstResultItemSchema,
+ instlSncnEsntlAtchFileItemSchema,
+ instlSncnMngSaveSchema,
+ instlSncnRvwOpnnSaveSchema,
+ instlSncnSplmntDmndItemSchema,
+} from "./model/instl-sncn-apply-payload-schema";
export {
type AdstZoneResponseDto,
type AtflMngClientResponseDto,
@@ -19,18 +39,10 @@ export {
type ElaprDocPrgrsClientResponseDto,
type EsntlAtchFileMngTreeResponseDto,
INSTL_SNCN_REGISTER_SELECTION_KEY,
- type InstlSncnApplySaveRequestDto,
- type InstlSncnBizCstSaveRequestDto,
- type InstlSncnBzScaleSaveDto,
- type InstlSncnCklstResultSaveDto,
- type InstlSncnEsntlAtchFileSaveDto,
type InstlSncnMngDetResponse,
type InstlSncnMngResponseDto,
- type InstlSncnMngSaveRequestDto,
type InstlSncnMngSearchParams,
type InstlSncnRvwOpnnResponseDto,
- type InstlSncnRvwOpnnSaveRequestDto,
type InstlSncnSplmntDmndResponseDto,
- type InstlSncnSplmntDmndSaveDto,
type ReviewStepCode,
} from "./model/instl-sncn-interface";
diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-apply-payload-schema.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-apply-payload-schema.ts
new file mode 100644
index 000000000..8e6e17616
--- /dev/null
+++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-apply-payload-schema.ts
@@ -0,0 +1,108 @@
+import { z } from "zod";
+
+/** 백엔드 InstlSncnMngSaveRequestDto */
+export const instlSncnMngSaveSchema = z.object({
+ instlSncnDocNo: z.string().optional(),
+ admdstCd: z.string().optional(),
+ envoSeComCd: z.string().optional(),
+ sncnSeComCd: z.string().optional(),
+ taskSeComCd: z.string().optional(),
+ bgtBizCd: z.string().optional(),
+ bgtAplyYr: z.string().optional(),
+ bizNm: z.string().optional(),
+ bizBgngYr: z.string().optional(),
+ bizEndYr: z.string().optional(),
+ bizCn: z.string().optional(),
+ aplcntNm: z.string().optional(),
+ aplcntMblTelno: z.string().optional(),
+ aplcntOgdpNm: z.string().optional(),
+ aplcntEmlAddr: z.string().optional(),
+ dsgnrNm: z.string().optional(),
+ dsgnrTelno: z.string().optional(),
+ dsgnrOgdpNm: z.string().optional(),
+ dsgnrEmlAddr: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+ prgrsSttsComCd: z.string().optional(),
+ rtlztnFcltSeComCd: z.string().optional(),
+ rtlztnUsgSeComCd: z.string().optional(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto.BzScaleItem (diff는 API 미전송) */
+export const instlSncnBzScaleItemSchema = z.object({
+ cnstpSeCd: z.string().optional(),
+ bscPl: z.number().optional(),
+ budgetReflct: z.number().optional(),
+ thtiDsgn: z.number().optional(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto.BizCstItem */
+export const instlSncnBizCstItemSchema = z.object({
+ acntgSeCd: z.string().optional(),
+ bscPlanAmt: z.number().optional(),
+ bgtRfltAmt: z.number().optional(),
+ tstmDesignAmt: z.number().optional(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto.CklstResultItem */
+export const instlSncnCklstResultItemSchema = z.object({
+ cklstRvwSn: z.number().optional(),
+ cklstArtclSn: z.number().optional(),
+ cklstSn: z.number().optional(),
+ rvwStpComCd: z.string().nullish(),
+ selected: z.enum(["Y", "N"]).nullish(),
+ picRvwCn: z.string().nullish(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto.EsntlAtchFileItem */
+export const instlSncnEsntlAtchFileItemSchema = z.object({
+ esntlAtchFileId: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 InstlSncnRvwOpnnSaveRequestDto */
+export const instlSncnRvwOpnnSaveSchema = z.object({
+ instlSncnDocNo: z.string(),
+ rvwOpnnSn: z.number().optional(),
+ opnrSeComCd: z.string().optional(),
+ opnrNm: z.string().optional(),
+ rvwRsltCn: z.string().optional(),
+ rvwRsltAnmlyYn: z.string().optional(),
+ atchFileGroupId: z.string().optional(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto.InstlSncnSplmntDmndItem */
+export const instlSncnSplmntDmndItemSchema = z.object({
+ splmntDmndSn: z.number().nullish(),
+ splmntArtclComCd: z.string().nullish(),
+ splmntDmndCn: z.string().optional(),
+ splmntDmndDt: z.string().optional(),
+ splmntDmndAtchFileGroupId: z.string().optional(),
+ splmntCn: z.string().optional(),
+ splmntPicNm: z.string().optional(),
+ splmntCmptnDt: z.string().optional(),
+ splmntAtchFileGroupId: z.string().optional(),
+ cmptnYn: z.string().optional(),
+});
+
+/** 백엔드 InstlSncnApplySaveRequestDto */
+export const instlSncnApplySaveSchema = z.object({
+ techReViewSaveCd: z.string().optional(),
+ rvwStpCd: z.string().optional(),
+ instlSncnMng: instlSncnMngSaveSchema,
+ bzScaleList: z.array(instlSncnBzScaleItemSchema).optional(),
+ bizCstList: z.array(instlSncnBizCstItemSchema).optional(),
+ cklstResultList: z.array(instlSncnCklstResultItemSchema).optional(),
+ cklstResultMap: z.record(z.array(instlSncnCklstResultItemSchema)).optional(),
+ esntlAtchFileList: z.array(instlSncnEsntlAtchFileItemSchema).optional(),
+ rvwOpnnList: z.array(instlSncnRvwOpnnSaveSchema).optional(),
+ splmntDmndList: z.array(instlSncnSplmntDmndItemSchema).optional(),
+});
+
+export type InstlSncnMngSaveRequestDto = z.infer;
+export type InstlSncnBzScaleSaveDto = z.infer;
+export type InstlSncnBizCstSaveRequestDto = z.infer;
+export type InstlSncnCklstResultSaveDto = z.infer;
+export type InstlSncnEsntlAtchFileSaveDto = z.infer;
+export type InstlSncnRvwOpnnSaveRequestDto = z.infer;
+export type InstlSncnSplmntDmndSaveDto = z.infer;
+export type InstlSncnApplySaveRequestDto = z.infer;
diff --git a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts
index a5e4ce791..1eea8c4c9 100644
--- a/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts
+++ b/apps/as/src/entities/instl-sncn/model/instl-sncn-interface.ts
@@ -1,24 +1,24 @@
import type {
AtflMngClientResponseDto,
- BizCstSaveBase,
BizCstTreeDto,
BizSclTreeDto,
- BzScaleSaveBase,
- CklstResultSaveBase,
DocPrgrsUpdateRequestDto,
ElaprDocPrgrsClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
- EsntlAtchFileSaveBase,
PageableParams,
PageResponse,
- PbadmsApplySaveHeader,
PbadmsMngDetResponseBase,
RvwOpnnResponseExtension,
- RvwOpnnSaveBase,
SplmntDmndResponseExtension,
- SplmntDmndSaveBase,
} from "@/shared/model";
+import type {
+ InstlSncnApplySaveRequestDto,
+ InstlSncnMngSaveRequestDto,
+ InstlSncnRvwOpnnSaveRequestDto,
+ InstlSncnSplmntDmndSaveDto,
+} from "./instl-sncn-apply-payload-schema";
+
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
@@ -41,189 +41,48 @@ export const INSTL_SNCN_REGISTER_SELECTION_KEY = "instlSncnRegisterSelection";
/** 백엔드 InstlSncnMngSearchDto + Pageable */
export interface InstlSncnMngSearchParams extends PageableParams {
- /** 설치인가문서번호 */
instlSncnDocNo?: string;
- /** 행정구역코드 */
admdstCd?: string;
- /** 환경청구분공통코드 */
envoSeComCd?: string;
- /** 업무구분공통코드 */
taskSeComCd?: string;
- /** 예산사업코드 */
bgtBizCd?: string;
- /** 예산신청연도 */
bgtAplyYr?: string;
- /** 사업명 */
bizNm?: string;
- /** 사업시작연도 */
bizBgngYr?: string;
- /** 사업종료연도 */
bizEndYr?: string;
- /** 사업내용 */
bizCn?: string;
- /** 신청자명 */
aplcntNm?: string;
- /** 신청자휴대전화번호 */
aplcntMblTelno?: string;
- /** 신청자소속명 */
aplcntOgdpNm?: string;
- /** 신청자이메일주소 */
aplcntEmlAddr?: string;
- /** 설계자명 */
dsgnrNm?: string;
- /** 설계자전화번호 */
dsgnrTelno?: string;
- /** 설계자소속명 */
dsgnrOgdpNm?: string;
- /** 설계자이메일주소 */
dsgnrEmlAddr?: string;
- /** 첨부파일그룹아이디 */
atchFileGroupId?: string;
- /** 진행상태공통코드 */
prgrsSttsComCd?: string;
}
-/** 설치인가관리 저장 요청 DTO */
-export interface InstlSncnMngSaveRequestDto {
- /** 설치인가문서번호 */
- instlSncnDocNo?: string;
- /** 행정구역코드 */
- admdstCd?: string;
- /** 환경청구분공통코드 */
- envoSeComCd?: string;
- /** 인가구분공통코드 */
- sncnSeComCd: string;
- /** 업무구분공통코드 */
- taskSeComCd: string;
- /** 예산사업코드 */
- bgtBizCd?: string;
- /** 예산신청연도 */
- bgtAplyYr?: string;
- /** 사업명 */
- bizNm: string;
- /** 사업시작연도 */
- bizBgngYr: string;
- /** 사업종료연도 */
- bizEndYr: string;
- /** 사업내용 */
- bizCn: string;
- /** 신청자명 */
- aplcntNm?: string;
- /** 신청자휴대전화번호 */
- aplcntMblTelno?: string;
- /** 신청자소속명 */
- aplcntOgdpNm?: string;
- /** 신청자이메일주소 */
- aplcntEmlAddr?: string;
- /** 설계자명 */
- dsgnrNm: string;
- /** 설계자전화번호 */
- dsgnrTelno: string;
- /** 설계자소속명 */
- dsgnrOgdpNm: string;
- /** 설계자이메일주소 */
- dsgnrEmlAddr: string;
- /** 첨부파일그룹아이디 */
- atchFileGroupId?: string;
- /** 진행상태공통코드 */
- prgrsSttsComCd?: string;
- /** 재이용시설구분공통코드 */
- rtlztnFcltSeComCd?: string;
- /** 재이용용도구분공통코드 */
- rtlztnUsgSeComCd?: string;
-}
-
-/** 설치인가관리 수정 요청 DTO */
+/** 백엔드 InstlSncnMngUpdateRequestDto */
export type InstlSncnMngUpdateRequestDto = InstlSncnMngSaveRequestDto;
-/** 설치인가관리 응답 DTO */
-export interface InstlSncnMngResponseDto extends InstlSncnMngSaveRequestDto {
- /** 설치인가문서번호 (필수로 재정의) */
+/** 백엔드 InstlSncnMngResponseDto */
+export type InstlSncnMngResponseDto = InstlSncnMngSaveRequestDto & {
instlSncnDocNo: string;
- /** 환경청구분공통코드명 */
envoSeCdNm: string;
- /** 인가구분공통코드명 */
sncnSeCdNm: string;
- /** 업무구분공통코드명 */
taskSeCdNm: string;
- /** 진행상태공통코드명 */
prgrsSttsCdNm: string;
- /** 기술검토회신이력여부 */
techRplyHstryYn: "Y" | "N";
- /** 재이용시설구분공통코드명 */
rtlztnFcltSeComNm: string;
- /** 재이용용도구분공통코드명 */
rtlztnUsgSeComNm: string;
- /** 등록일시 */
regDt: string;
- /** 등록자 UUID */
rgtr: string;
- /** 등록자명 */
rgtrNm: string;
- /** 수정 일시 */
mdfcnDt: string;
- /** 시도 */
sido: string;
- /** 구군 */
gugun: string;
-}
-
-/** 설치인가 신청 - 사업규모 저장 DTO(배열 원소) */
-export interface InstlSncnBzScaleSaveDto extends BzScaleSaveBase {
- /** 공종구분코드 */
- cnstpSeCd: string;
- /** 기본계획수량 */
- bscPl: number;
- /** 예산반영수량 */
- budgetReflct: number;
- /** 금회설계수량 */
- thtiDsgn: number;
- /** 증감(C-B) — 클라이언트 계산용, API 미전송 */
- diff?: number;
-}
-
-/** 설치인가 신청 - 총사업비 저장 DTO(배열 원소) */
-export interface InstlSncnBizCstSaveRequestDto extends BizCstSaveBase {
- /** 회계구분코드 */
- acntgSeCd: string;
- /** 기본계획금액 */
- bscPlanAmt: number;
- /** 예산반영금액 */
- bgtRfltAmt: number;
- /** 금회설계금액 */
- tstmDesignAmt: number;
-}
-
-/** 설치인가 신청 - 체크리스트 결과 저장 DTO(배열 원소) */
-export interface InstlSncnCklstResultSaveDto extends CklstResultSaveBase {
- /** 점검표일련번호 */
- cklstSn: number;
- /** 점검표항목일련번호 */
- cklstArtclSn: number;
- /** 점검표검토일련번호 */
- cklstRvwSn: number;
- /** 선택값(Y/N/null) */
- selected: "Y" | "N" | null;
- /** 담당자검토내용(기술검토의견) */
- picRvwCn?: string | null;
-}
-
-/** 설치인가 신청 - 필수첨부파일 저장 DTO(배열 원소) */
-export interface InstlSncnEsntlAtchFileSaveDto extends EsntlAtchFileSaveBase {
- /** 필수첨부파일아이디 */
- esntlAtchFileId: string;
- /** 첨부파일그룹아이디 */
- atchFileGroupId: string;
-}
-
-/** 설치인가검토의견 저장 요청 DTO */
-export interface InstlSncnRvwOpnnSaveRequestDto extends RvwOpnnSaveBase {
- /** 설치인가문서번호 */
- instlSncnDocNo: string;
-}
-
-/** 설치인가 보완요구 저장 DTO(배열 원소) */
-export type InstlSncnSplmntDmndSaveDto = SplmntDmndSaveBase;
+};
/** 설치인가 보완요구 응답 DTO */
export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto &
@@ -231,26 +90,6 @@ export type InstlSncnSplmntDmndResponseDto = InstlSncnSplmntDmndSaveDto &
instlSncnDocNo?: string;
};
-/** 설치인가 신청 - 통합 저장 요청 DTO */
-export interface InstlSncnApplySaveRequestDto extends PbadmsApplySaveHeader {
- /** 설치인가관리 정보 */
- instlSncnMng: InstlSncnMngSaveRequestDto;
- /** 사업규모 목록 */
- bzScaleList?: InstlSncnBzScaleSaveDto[];
- /** 총사업비 목록 */
- bizCstList?: InstlSncnBizCstSaveRequestDto[];
- /** 체크리스트 검토결과 목록 */
- cklstResultList?: InstlSncnCklstResultSaveDto[];
- /** 체크리스트 검토결과 맵 (rvwStpComCd -> 목록) */
- cklstResultMap?: Record;
- /** 필수첨부파일 목록 */
- esntlAtchFileList?: InstlSncnEsntlAtchFileSaveDto[];
- /** 검토의견 목록 */
- rvwOpnnList?: InstlSncnRvwOpnnSaveRequestDto[];
- /** 보완요구 목록 */
- splmntDmndList?: InstlSncnSplmntDmndSaveDto[];
-}
-
export type ReviewStepCode = "LOCAL" | "ENV" | "KECO";
/** 설치인가검토의견 응답 DTO */
diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/pwrk-cnsltn-doc-pjtco-ajmt-mng-service.ts b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/pwrk-cnsltn-doc-pjtco-ajmt-mng-service.ts
new file mode 100644
index 000000000..c525ac87b
--- /dev/null
+++ b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/pwrk-cnsltn-doc-pjtco-ajmt-mng-service.ts
@@ -0,0 +1,24 @@
+import { client } from "@/shared/api";
+import { ENDPOINT } from "@/shared/config";
+import type { PageResponse } from "@/shared/model";
+
+import type {
+ PwrkCnsltnDocPjtcoAjmtMngResponseDto,
+ PwrkCnsltnDocPjtcoAjmtMngSearchDto,
+ PwrkCnsltnDocPjtcoAjmtMngService,
+} from "../model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface";
+
+const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_PJTCO_AJMT_MNG;
+
+async function findPage(
+ searchDto: PwrkCnsltnDocPjtcoAjmtMngSearchDto,
+): Promise> {
+ const response = await client.get>(BASE, {
+ params: searchDto,
+ });
+ return response.data;
+}
+
+export const pwrkCnsltnDocPjtcoAjmtMngService = {
+ findPage,
+} satisfies PwrkCnsltnDocPjtcoAjmtMngService;
diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/queries.ts b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/queries.ts
new file mode 100644
index 000000000..21f357c2b
--- /dev/null
+++ b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/api/queries.ts
@@ -0,0 +1,22 @@
+import { useQuery } from "@tanstack/react-query";
+
+import type { PwrkCnsltnDocPjtcoAjmtMngSearchDto } from "../model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface";
+
+import { pwrkCnsltnDocPjtcoAjmtMngService } from "./pwrk-cnsltn-doc-pjtco-ajmt-mng-service";
+
+export const pwrkCnsltnDocPjtcoAjmtMngKeys = {
+ all: ["pwrk-cnsltn-doc-pjtco-ajmt-mng"] as const,
+ page: (params: PwrkCnsltnDocPjtcoAjmtMngSearchDto) =>
+ [...pwrkCnsltnDocPjtcoAjmtMngKeys.all, "findPage", params] as const,
+};
+
+export function usePwrkCnsltnDocPjtcoAjmtMngPage(
+ params: PwrkCnsltnDocPjtcoAjmtMngSearchDto,
+ isEnabled = true,
+) {
+ return useQuery({
+ queryKey: pwrkCnsltnDocPjtcoAjmtMngKeys.page(params),
+ queryFn: () => pwrkCnsltnDocPjtcoAjmtMngService.findPage(params),
+ enabled: Boolean(params.pwrkCnsltnDocNo) && isEnabled,
+ });
+}
diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/index.ts b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/index.ts
new file mode 100644
index 000000000..143bef3ba
--- /dev/null
+++ b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/index.ts
@@ -0,0 +1,10 @@
+export { pwrkCnsltnDocPjtcoAjmtMngService } from "./api/pwrk-cnsltn-doc-pjtco-ajmt-mng-service";
+export {
+ pwrkCnsltnDocPjtcoAjmtMngKeys,
+ usePwrkCnsltnDocPjtcoAjmtMngPage,
+} from "./api/queries";
+export type {
+ PwrkCnsltnDocPjtcoAjmtMngResponseDto,
+ PwrkCnsltnDocPjtcoAjmtMngSearchDto,
+ PwrkCnsltnDocPjtcoAjmtMngService,
+} from "./model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface";
diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface.ts b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface.ts
new file mode 100644
index 000000000..420b9d44b
--- /dev/null
+++ b/apps/as/src/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng/model/pwrk-cnsltn-doc-pjtco-ajmt-mng-interface.ts
@@ -0,0 +1,44 @@
+import type { PageableParams, PageResponse } from "@/shared/model";
+
+/** 백엔드 PwrkCnsltnDocPjtcoAjmtMngResponseDto */
+export interface PwrkCnsltnDocPjtcoAjmtMngResponseDto {
+ pwrkCnsltnDocNo: string;
+ rvwOpnnSn: number;
+ acntgSeCd: string;
+ acntgSeCdNm?: string;
+ rvwOpnnClsfComCd?: string;
+ rvwOpnnClsfComCdNm?: string;
+ ognTpcoAmt?: number;
+ ognGvexpAmt?: number;
+ ognLcexpAmt?: number;
+ ognOrgprBrdnAmt?: number;
+ chgTpcoAmt?: number;
+ chgGvexpAmt?: number;
+ chgLcexpAmt?: number;
+ chgOrgprBrdnAmt?: number;
+ ajmtRsn?: string;
+ vrfcTpcoAmt?: number;
+ vrfcGvexpAmt?: number;
+ vrfcLcexpAmt?: number;
+ vrfcOrgprBrdnAmt?: number;
+ rsbnYn?: string;
+ vrfcCn?: string;
+ regDt?: string;
+ rgtr?: string;
+ rgtrNm?: string;
+ mdfcnDt?: string;
+ mdfr?: string;
+}
+
+/** 백엔드 PwrkCnsltnDocPjtcoAjmtMngSearchDto + Pageable */
+export interface PwrkCnsltnDocPjtcoAjmtMngSearchDto extends PageableParams {
+ pwrkCnsltnDocNo?: string;
+ rvwOpnnSn?: number;
+ acntgSeCd?: string;
+}
+
+export interface PwrkCnsltnDocPjtcoAjmtMngService {
+ findPage(
+ searchDto: PwrkCnsltnDocPjtcoAjmtMngSearchDto,
+ ): Promise>;
+}
diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts
index 2980cfffb..6ad7af1ba 100644
--- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts
+++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts
@@ -47,7 +47,7 @@ export const sewerBscPlanCklstResultItemSchema = z.object({
cklstArtclSn: z.number().optional(),
cklstSn: z.number().optional(),
rvwStpComCd: z.string(),
- selected: z.string().optional(),
+ selected: z.enum(["Y", "N"]).nullish(),
picRvwCn: z.string().nullish(),
});
diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx
index e703551d5..5873803da 100644
--- a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx
+++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx
@@ -68,6 +68,7 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) {
markDeleted,
reorderRows,
} = useAplnRows({
+ seedKey: elaprTmpltId,
elaprTmpltId,
serverRows: aplnQuery.data?.content,
});
diff --git a/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts
index 3eecde578..9cbcdc270 100644
--- a/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts
+++ b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts
@@ -1,7 +1,13 @@
import { z } from "zod";
+import {
+ designCnstnBzScaleItemSchema,
+ designCnstnEsntlAtchFileItemSchema,
+ designCnstnMngSaveSchema,
+} from "@/entities/design-cnstn";
+
export const designCnstnRegistSchema = z.object({
- designCnstnMng: z.object({
+ designCnstnMng: designCnstnMngSaveSchema.extend({
bizNm: z.string().min(1, "사업명은 필수입니다"),
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
@@ -17,27 +23,11 @@ export const designCnstnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
- aplcntNm: z.string().optional(),
- aplcntMblTelno: z.string().optional(),
- aplcntOgdpNm: z.string().optional(),
- aplcntEmlAddr: z.string().optional(),
- designCnstnDocNo: z.string().optional(),
- admdstCd: z.string().optional(),
- envoSeComCd: z.string().optional(),
- prgrsSttsComCd: z.string().optional(),
- atchFileGroupId: z.string().optional(),
- envoSeCdNm: z.string().optional(),
- taskSeCdNm: z.string().optional(),
- prgrsSttsCdNm: z.string().optional(),
sido: z.string().optional(),
gugun: z.string().optional(),
- regDt: z.string().optional(),
- rgtr: z.string().optional(),
- rgtrNm: z.string().optional(),
- mdfcnDt: z.string().optional(),
}),
bzScaleList: z.array(
- z.object({
+ designCnstnBzScaleItemSchema.extend({
cnstpSeCd: z.string(),
bscPl: z.number(),
budgetReflct: z.number(),
@@ -46,7 +36,7 @@ export const designCnstnRegistSchema = z.object({
}),
),
esntlAtchFileList: z.array(
- z.object({
+ designCnstnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
atchFileGroupId: z.string(),
}),
diff --git a/apps/as/src/pages/design-cnstn/lib/form-data-mapper.ts b/apps/as/src/pages/design-cnstn/lib/form-data-mapper.ts
index 3150bfaa7..326f6e237 100644
--- a/apps/as/src/pages/design-cnstn/lib/form-data-mapper.ts
+++ b/apps/as/src/pages/design-cnstn/lib/form-data-mapper.ts
@@ -1,9 +1,13 @@
import type { IUserInfo } from "@/shared/store";
-import type { DesignCnstnMngDetResponseDto } from "@/entities/design-cnstn";
+import {
+ type DesignCnstnApplySaveRequestDto,
+ designCnstnApplySaveSchema,
+ type DesignCnstnMngDetResponseDto,
+} from "@/entities/design-cnstn";
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { DesignCnstnRegistFormData } from "./design-cnstn-regist-schema";
-import { parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
+import { formatTaskSeComCdForApi, parseTaskSeComCdFromApi } from "./design-cnstn-regist-schema";
export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignCnstnRegistFormData {
const detDto = data.detDto;
@@ -33,15 +37,8 @@ export function mapDetailToFormData(data: DesignCnstnMngDetResponseDto): DesignC
envoSeComCd: detDto.envoSeComCd,
prgrsSttsComCd: detDto.prgrsSttsComCd,
atchFileGroupId: detDto.atchFileGroupId ?? "",
- envoSeCdNm: detDto.envoSeCdNm,
- taskSeCdNm: detDto.taskSeCdNm,
- prgrsSttsCdNm: detDto.prgrsSttsCdNm,
sido: detDto.sido,
gugun: detDto.gugun,
- regDt: detDto.regDt,
- rgtr: detDto.rgtr,
- rgtrNm: detDto.rgtrNm,
- mdfcnDt: detDto.mdfcnDt,
},
bzScaleList: bizSclList.map((item) => ({
cnstpSeCd: item.cnstpSeCd,
@@ -90,3 +87,20 @@ export function getCreateDefaultValues(
privacyConsent: "N",
};
}
+
+export function buildFormPayload(
+ data: DesignCnstnRegistFormData,
+ prgrsSttsComCd: "CV" | "CT",
+ options: { isEditMode: boolean; docNo?: string },
+): DesignCnstnApplySaveRequestDto {
+ return designCnstnApplySaveSchema.parse({
+ designCnstnMng: {
+ ...data.designCnstnMng,
+ taskSeComCd: formatTaskSeComCdForApi(data.designCnstnMng.taskSeComCd),
+ designCnstnDocNo: options.isEditMode ? options.docNo : undefined,
+ prgrsSttsComCd,
+ },
+ bzScaleList: data.bzScaleList,
+ esntlAtchFileList: data.esntlAtchFileList,
+ });
+}
diff --git a/apps/as/src/pages/design-cnstn/lib/save-payload-utils.ts b/apps/as/src/pages/design-cnstn/lib/save-payload-utils.ts
index 48da7a731..625ff0486 100644
--- a/apps/as/src/pages/design-cnstn/lib/save-payload-utils.ts
+++ b/apps/as/src/pages/design-cnstn/lib/save-payload-utils.ts
@@ -4,6 +4,7 @@ import type {
DesignCnstnApplySaveRequestDto,
DesignCnstnMngDetResponseDto,
} from "@/entities/design-cnstn";
+import { designCnstnApplySaveSchema } from "@/entities/design-cnstn";
import { toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
import type { ReviewStepCode } from "../model/view-types";
@@ -30,11 +31,11 @@ export function buildSavePayload(
throw new Error("상세 정보(detDto)가 없습니다.");
}
- return {
+ return designCnstnApplySaveSchema.parse({
designCnstnMng: { ...detDto, prgrsSttsComCd },
bzScaleList: toBzScaleSaveDto(data.bizSclList ?? []),
esntlAtchFileList: toEsntlAtchFileSaveList(data.esntlFileList),
- };
+ });
}
export function buildTechReviewPayload(
@@ -45,10 +46,10 @@ export function buildTechReviewPayload(
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
opnrNm?: string,
): DesignCnstnApplySaveRequestDto {
- return {
+ return designCnstnApplySaveSchema.parse({
...buildSavePayload(data, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
- };
+ });
}
diff --git a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx
index 041e1d812..254a7a010 100644
--- a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx
+++ b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx
@@ -9,9 +9,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useQueryClient } from "@tanstack/react-query";
import { useAlertStore, useUserStore } from "@/shared/lib";
-import { SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
+import { RhfDevTool, SectionCollapsibleGroup, SectionCollapsibleToggle } from "@/shared/ui";
import {
- type DesignCnstnApplySaveRequestDto,
designCnstnKeys,
type DesignCnstnMngDetResponseDto,
designCnstnMngService,
@@ -27,12 +26,12 @@ import {
} from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
+import { type DesignCnstnRegistFormData, designCnstnRegistSchema } from "../lib/design-cnstn-regist-schema";
import {
- type DesignCnstnRegistFormData,
- designCnstnRegistSchema,
- formatTaskSeComCdForApi,
-} from "../lib/design-cnstn-regist-schema";
-import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper";
+ buildFormPayload,
+ getCreateDefaultValues,
+ mapDetailToFormData,
+} from "../lib/form-data-mapper";
import { PROGRESS_STATUS_COLUMNS } from "../model/columns";
import { BasicInfoFormSection } from "./sections/basic-info-form-section";
@@ -68,29 +67,18 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
: getCreateDefaultValues(userInfo, createDefaults),
});
- function buildFormPayload(
- data: DesignCnstnRegistFormData,
- prgrsSttsComCd: "CV" | "CT",
- ): DesignCnstnApplySaveRequestDto {
- const { taskSeComCd: formTaskSeComCd, ...designCnstnMngRest } = data.designCnstnMng;
-
- return {
- designCnstnMng: {
- ...designCnstnMngRest,
- taskSeComCd: formatTaskSeComCdForApi(formTaskSeComCd),
- designCnstnDocNo: isEditMode ? initialData?.detDto?.designCnstnDocNo : undefined,
- prgrsSttsComCd,
- },
- bzScaleList: data.bzScaleList,
- esntlAtchFileList: data.esntlAtchFileList,
- };
+ function createFormPayload(data: DesignCnstnRegistFormData, prgrsSttsComCd: "CV" | "CT") {
+ return buildFormPayload(data, prgrsSttsComCd, {
+ isEditMode,
+ docNo: initialData?.detDto?.designCnstnDocNo,
+ });
}
async function submitForm(
data: DesignCnstnRegistFormData,
prgrsSttsComCd: "CV" | "CT",
): Promise {
- return designCnstnMngService.saveApply(buildFormPayload(data, prgrsSttsComCd));
+ return designCnstnMngService.saveApply(createFormPayload(data, prgrsSttsComCd));
}
async function onSave(data: DesignCnstnRegistFormData) {
@@ -229,6 +217,7 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
+
);
}
diff --git a/apps/as/src/pages/design-cnstn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/design-cnstn/ui/sections/tech-review-opinion-section.tsx
index d467bdb31..f81f0aaf9 100644
--- a/apps/as/src/pages/design-cnstn/ui/sections/tech-review-opinion-section.tsx
+++ b/apps/as/src/pages/design-cnstn/ui/sections/tech-review-opinion-section.tsx
@@ -2,14 +2,11 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
-import { Paperclip } from "lucide-react";
-
-import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
-import { attachService } from "@/entities/attach";
+import { AttachmentDownloadLink, attachService } from "@/entities/attach";
import type { DesignCnstnRvwOpnnResponseDto } from "@/entities/design-cnstn";
import {
Button,
@@ -319,15 +316,10 @@ function TechReviewOpinionTable({ items }: TechReviewOpinionTableProps) {
{item.atchFileInfo?.atflCd ? (
-
-
- {item.atchFileInfo.orgnlFileNm || "다운로드"}
-
+
) : (
"-"
)}
diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
index 710d6c93d..c7c63ffed 100644
--- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
+++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { AttachmentFileField } from "@/shared/ui";
-import { attachService } from "@/entities/attach";
+import { attachService, useDownloadAttachmentByAtflId } from "@/entities/attach";
import { CodeSelect } from "@/entities/code";
import type { EsntlAtchFileMng } from "@/entities/esntl";
import {
@@ -61,6 +61,7 @@ function EsntlAtchFileFormDialog({
const formId = useId();
const atchFileGroupIdRef = useRef("");
const [uploadedFiles, setUploadedFiles] = useState([]);
+ const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
const { register, handleSubmit, reset, setValue, watch } = useForm({
defaultValues: {
@@ -219,6 +220,9 @@ function EsntlAtchFileFormDialog({
files={uploadedFiles.map((item) => ({
fileKey: item.fileKey,
fileName: item.fileName,
+ onDownload: item.atflId
+ ? () => downloadByAtflId(item.atflId!)
+ : undefined,
}))}
inputId={fileInputId}
isUploadingByKey={isUploading}
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts
index 1ac079216..99b1b29ab 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/ai-pending-utils.ts
@@ -1,3 +1,5 @@
+import type { RagIndexingInfoDto } from "@/entities/fnrs-cnsltn";
+
const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const;
type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number];
@@ -17,5 +19,17 @@ function getAiPendingMessage(prgrsSttsComCd: string): string {
return AI_PENDING_MESSAGES[prgrsSttsComCd as AiPendingStatusCode];
}
-export { AI_PENDING_STATUS_CODES, getAiPendingMessage, isAiPendingStatus };
+function isRagIndexingInProgress(
+ prgrsSttsComCd: string,
+ ragIndexingInfo?: RagIndexingInfoDto | null,
+): boolean {
+ return prgrsSttsComCd === "ER" && ragIndexingInfo?.isInProgress === true;
+}
+
+export {
+ AI_PENDING_STATUS_CODES,
+ getAiPendingMessage,
+ isAiPendingStatus,
+ isRagIndexingInProgress,
+};
export type { AiPendingStatusCode };
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts
index d0b906dae..6f711405b 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts
@@ -2,14 +2,14 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
strg: "strg",
aply: "aply",
del: "del",
- fnrsCnsltnAncmnt: "fnrsCnsltnAncmnt",
envoSbmsn: "envoSbmsn",
rjct: "rjct",
sncnAprv: "sncnAprv",
- lclRply: "lclRply",
rvwRply: "rvwRply",
+ rvwOpnnRply: "rvwOpnnRply",
rqstTechRvw: "rqstTechRvw",
fileSplmnt: "fileSplmnt",
+ fileVrfc: "fileVrfc",
dsgntRvwr: "dsgntRvwr",
pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt",
pwrkCnsltnDocMng: "pwrkCnsltnDocMng",
@@ -21,6 +21,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
atrzRply: "atrzRply",
actnCmptn: "actnCmptn",
rqstActnRvw: "rqstActnRvw",
+ costVrfc: "costVrfc",
} as const;
export type FnrsCnsltnViewButtonAction =
@@ -38,37 +39,63 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record
> = {
ER: {
- ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
+ ATRZ01: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
+ ],
+ },
+ SL: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
TE: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
@@ -81,10 +108,80 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
],
- ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply],
+ ATRZ99: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
+ ],
+ },
+ TP: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ RB: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ RA: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ AV: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ EV: {
+ ATRZ01: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
+ ],
+ ATRZ02: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.costVrfc,
+ ],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
RK: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
+ ],
+ ATRZ03: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
+ ],
+ ATRZ99: [
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
+ ],
+ },
+ RR: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ EP: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ },
+ TA: {
+ ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ03: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
+ ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
},
};
@@ -95,10 +192,16 @@ function resolveButtonConfigs(actions: FnrsCnsltnViewButtonAction[]): FnrsCnsltn
interface GetButtonConfigsOptions {
hasPendingApprovalRequest?: boolean;
hasCompletedDraft?: boolean;
+ canRetryFileVrfc?: boolean;
+ canRetryDraftGen?: boolean;
+ canRetryCostVrfc?: boolean;
}
const PENDING_APPROVAL_ACTIONS = new Set([
- ...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
+ FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
]);
@@ -129,15 +232,26 @@ function resolveRoleUser01AtrzButtonActions(
const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action);
if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false;
- if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) {
- if (
- action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng ||
- action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq
- ) {
- return options.hasCompletedDraft;
+ if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) {
+ return options?.canRetryFileVrfc === true;
+ }
+ if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
+ return options?.canRetryDraftGen === true;
+ }
+ if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.costVrfc) {
+ return options?.canRetryCostVrfc === true;
+ }
+
+ if (options?.hasCompletedDraft != null) {
+ if (prgrsSttsComCd === "SL" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "RK") {
+ if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) {
+ return options.hasCompletedDraft;
+ }
}
- if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) {
- return !options.hasCompletedDraft;
+ if (prgrsSttsComCd === "TE") {
+ if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) {
+ return options.hasCompletedDraft;
+ }
}
}
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts
index 0faa372e9..5739c8e6e 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts
@@ -1,14 +1,22 @@
import { z } from "zod";
+import {
+ fnrsCnsltnBizCstItemSchema,
+ fnrsCnsltnBzScaleItemSchema,
+ fnrsCnsltnCklstResultItemSchema,
+ fnrsCnsltnEsntlAtchFileItemSchema,
+ fnrsCnsltnMngSaveSchema,
+ fnrsCnsltnSplmntDmndItemSchema,
+ fnrsSharItemSchema,
+} from "@/entities/fnrs-cnsltn";
+
export const fnrsCnsltnRegistSchema = z.object({
- fnrsCnsltnMng: z.object({
+ fnrsCnsltnMng: fnrsCnsltnMngSaveSchema.extend({
bizNm: z.string().min(1, "사업명은 필수입니다"),
bizBgngYr: z.string().min(1, "사업시작연도는 필수입니다"),
bizEndYr: z.string().min(1, "사업종료연도는 필수입니다"),
bizCn: z.string().min(1, "사업내용은 필수입니다"),
taskSeComCd: z.array(z.string()).min(1, "구분은 필수입니다"),
- cfmtnBgtBizNo: z.string().optional(),
- bgtAplyYr: z.string().optional(),
dsgnrNm: z.string().min(1, "설계자 이름은 필수입니다"),
dsgnrTelno: z
.string()
@@ -19,30 +27,11 @@ export const fnrsCnsltnRegistSchema = z.object({
.string()
.min(1, "설계자 이메일은 필수입니다")
.email("올바른 이메일 형식이 아닙니다"),
- aplcntNm: z.string().optional(),
- aplcntMblTelno: z.string().optional(),
- aplcntOgdpNm: z.string().optional(),
- aplcntEmlAddr: z.string().optional(),
- fnrsCnsltnDocNo: z.string().optional(),
- admdstCd: z.string().optional(),
- envoSeComCd: z.string().optional(),
- rtlztnFcltSeComCd: z.string().optional(),
- rtlztnUsgSeComCd: z.string().optional(),
- prgrsSttsComCd: z.string().optional(),
- atchFileGroupId: z.string().optional(),
- sharAtchFileGroupId: z.string().optional(),
- envoSeCdNm: z.string().optional(),
- taskSeCdNm: z.string().optional(),
- prgrsSttsCdNm: z.string().optional(),
sido: z.string().optional(),
gugun: z.string().optional(),
- regDt: z.string().optional(),
- rgtr: z.string().optional(),
- rgtrNm: z.string().optional(),
- mdfcnDt: z.string().optional(),
}),
bzScaleList: z.array(
- z.object({
+ fnrsCnsltnBzScaleItemSchema.extend({
cnstpSeCd: z.string(),
bscPl: z.number(),
budgetReflct: z.number(),
@@ -51,7 +40,7 @@ export const fnrsCnsltnRegistSchema = z.object({
}),
),
bizCstList: z.array(
- z.object({
+ fnrsCnsltnBizCstItemSchema.extend({
acntgSeCd: z.string(),
bscPlanAmt: z.number(),
bgtRfltAmt: z.number(),
@@ -59,7 +48,7 @@ export const fnrsCnsltnRegistSchema = z.object({
}),
),
fnrsSharList: z.array(
- z.object({
+ fnrsSharItemSchema.extend({
fnrsSharSeCd: z.string(),
exstnAmt: z.number(),
chgRqtAmt: z.number(),
@@ -68,37 +57,24 @@ export const fnrsCnsltnRegistSchema = z.object({
}),
),
cklstResultList: z.array(
- z.object({
+ fnrsCnsltnCklstResultItemSchema.extend({
cklstSn: z.number(),
cklstArtclSn: z.number(),
cklstRvwSn: z.number(),
- rvwStpComCd: z.string(),
- selected: z.enum(["Y", "N"]).nullable(),
- picRvwCn: z.string().nullable().optional(),
+ selected: z.enum(["Y", "N"]).nullish(),
+ picRvwCn: z.string().nullish(),
}),
),
esntlAtchFileList: z.array(
- z.object({
+ fnrsCnsltnEsntlAtchFileItemSchema.extend({
esntlAtchFileId: z.string(),
atchFileGroupId: z.string(),
}),
),
splmntDmndList: z.array(
- z.object({
- splmntDmndSn: z.number().nullish(),
- splmntArtclComCd: z.string().nullish(),
+ fnrsCnsltnSplmntDmndItemSchema.extend({
splmntArtclCdNm: z.string().nullish(),
- splmntDmndCn: z.string().optional(),
- splmntDmndDt: z.string().optional(),
- splmntDmndAtchFileGroupId: z.string().optional(),
splmntDmndAtchFileNm: z.string().optional(),
- splmntCn: z.string().optional(),
- splmntPicNm: z.string().optional(),
- splmntCmptnDt: z.string().optional(),
- splmntAtchFileGroupId: z.string().optional(),
- splmntAtchFileNm: z.string().optional(),
- splmntAtchFileAtflId: z.string().optional(),
- cmptnYn: z.string().optional(),
}),
),
privacyConsent: z.enum(["Y", "N"], {
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts
index 09e4817f5..1e5c6902d 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/form-data-mapper.ts
@@ -1,9 +1,21 @@
import type { IUserInfo } from "@/shared/store";
-import type { FnrsCnsltnMngDetResponseDto } from "@/entities/fnrs-cnsltn";
-import { mapFnrsSharDetToFormList, toEsntlAtchFileSaveList } from "@/widgets/pbadms-sprt";
+import {
+ type FnrsCnsltnApplySaveRequestDto,
+ fnrsCnsltnApplySaveSchema,
+ type FnrsCnsltnMngDetResponseDto,
+} from "@/entities/fnrs-cnsltn";
+import {
+ type CostItemDef,
+ type FnrsSharItemDef,
+ mapFnrsSharDetToFormList,
+ recalculateFnrsSharTotals,
+ toEsntlAtchFileSaveList,
+ toFnrsSharSaveList,
+ validateBizCstSharBalance,
+} from "@/widgets/pbadms-sprt";
import type { FnrsCnsltnRegistFormData } from "./fnrs-cnsltn-regist-schema";
-import { parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema";
+import { formatTaskSeComCdForApi, parseTaskSeComCdFromApi } from "./fnrs-cnsltn-regist-schema";
function mapSelectedValue(value?: string | null): "Y" | "N" | null {
if (value === "Y" || value === "N") return value;
@@ -47,15 +59,8 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
prgrsSttsComCd: detDto.prgrsSttsComCd,
atchFileGroupId: detDto.atchFileGroupId ?? "",
sharAtchFileGroupId: detDto.sharAtchFileGroupId ?? "",
- envoSeCdNm: detDto.envoSeCdNm,
- taskSeCdNm: detDto.taskSeCdNm,
- prgrsSttsCdNm: detDto.prgrsSttsCdNm,
sido: detDto.sido,
gugun: detDto.gugun,
- regDt: detDto.regDt,
- rgtr: detDto.rgtr,
- rgtrNm: detDto.rgtrNm,
- mdfcnDt: detDto.mdfcnDt,
},
bzScaleList: bizSclList.map((item) => ({
cnstpSeCd: item.cnstpSeCd,
@@ -94,8 +99,6 @@ export function mapDetailToFormData(data: FnrsCnsltnMngDetResponseDto): FnrsCnsl
splmntPicNm: item.splmntPicNm ?? "",
splmntCmptnDt: item.splmntCmptnDt ?? "",
splmntAtchFileGroupId: item.splmntAtchFileGroupId ?? "",
- splmntAtchFileNm: item.splmntAtchFileInfo?.orgnlFileNm ?? "",
- splmntAtchFileAtflId: item.splmntAtchFileInfo?.atflId ?? "",
cmptnYn: item.cmptnYn,
})) ?? [],
privacyConsent: "Y",
@@ -146,3 +149,46 @@ export function getCreateDefaultValues(
privacyConsent: "N",
};
}
+
+export function buildFormPayload(
+ data: FnrsCnsltnRegistFormData,
+ prgrsSttsComCd: string,
+ options: {
+ isEditMode: boolean;
+ docNo?: string;
+ sharItems: FnrsSharItemDef[];
+ costItems: CostItemDef[];
+ },
+): FnrsCnsltnApplySaveRequestDto | { validationMessage: string } {
+ const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, options.sharItems);
+ const validationMessage = validateBizCstSharBalance(
+ data.bizCstList,
+ withTotals,
+ options.sharItems,
+ options.costItems,
+ );
+ if (validationMessage) {
+ return { validationMessage };
+ }
+
+ return fnrsCnsltnApplySaveSchema.parse({
+ fnrsCnsltnMng: {
+ ...data.fnrsCnsltnMng,
+ taskSeComCd: formatTaskSeComCdForApi(data.fnrsCnsltnMng.taskSeComCd),
+ fnrsCnsltnDocNo: options.isEditMode ? options.docNo : undefined,
+ prgrsSttsComCd,
+ },
+ bzScaleList: data.bzScaleList,
+ bizCstList: data.bizCstList,
+ fnrsSharList: toFnrsSharSaveList(withTotals),
+ cklstResultList: data.cklstResultList.map((item) => ({
+ ...item,
+ selected: item.selected ?? undefined,
+ })),
+ esntlAtchFileList: data.esntlAtchFileList,
+ splmntDmndList: data.splmntDmndList.map((item) => ({
+ ...item,
+ ...(prgrsSttsComCd !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }),
+ })),
+ });
+}
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts
index bce365857..0ba84b926 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts
@@ -32,7 +32,10 @@ export function getEditableReviewStep(
dtlInstCd?: string,
prgrsSttsComCd?: string,
): ReviewStepCode | undefined {
- if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) {
+ if (
+ dtlInstCd === "ROLE_USER03" &&
+ (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP" || prgrsSttsComCd === "RR")
+ ) {
return "ENV";
}
diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts
index 39ebd7582..c4d7d5d88 100644
--- a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts
@@ -10,6 +10,7 @@ import type {
FnrsCnsltnMngResponseDto,
FnrsCnsltnSplmntDmndItem,
} from "@/entities/fnrs-cnsltn";
+import { fnrsCnsltnApplySaveSchema } from "@/entities/fnrs-cnsltn";
import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng";
import {
mapFnrsSharDetToFormList,
@@ -61,14 +62,14 @@ export function buildSavePayload(
const cklstResultList = data.cklstResultList ?? [];
const esntlFileList = data.esntlFileList ?? [];
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
fnrsCnsltnMng: { ...detDto, prgrsSttsComCd },
bzScaleList: toBzScaleSaveDto(bizSclList),
bizCstList: toBizCstSaveDto(bizCstList),
cklstResultList: toCklstSaveDto(cklstResultList, checklistFormState, reviewStep),
esntlAtchFileList: toEsntlAtchFileSaveList(esntlFileList),
fnrsSharList: toFnrsSharSaveList(mapFnrsSharDetToFormList(data.fnrsSharList ?? [])),
- };
+ });
}
export function buildTechReviewReplyPayload(
@@ -80,12 +81,12 @@ export function buildTechReviewReplyPayload(
reviewOpinionFormData?: ReviewOpinionGetFormDataResult | null,
opnrNm?: string,
): FnrsCnsltnApplySaveRequestDto {
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, reviewStep, prgrsSttsComCd),
techReViewSaveCd,
rvwStpCd: reviewStep,
rvwOpnnList: toRvwOpnnSaveDto(data, reviewOpinionFormData, opnrNm),
- };
+ });
}
export function buildSupplementReqPayload(
@@ -107,10 +108,10 @@ export function buildSupplementReqPayload(
cmptnYn: "N",
}));
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
- };
+ });
}
export function buildFileSplmntReqPayload(
@@ -124,10 +125,10 @@ export function buildFileSplmntReqPayload(
cmptnYn: "N",
}));
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...newItems],
- };
+ });
}
export function buildRvwRsltSplmntReqPayload(
@@ -144,10 +145,10 @@ export function buildRvwRsltSplmntReqPayload(
cmptnYn: "N",
}));
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
splmntDmndList: [...existingItems, ...newItems],
- };
+ });
}
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
@@ -169,21 +170,21 @@ export function buildSupplementSubmitPayload(
data: FnrsCnsltnMngDetResponseDto,
checklistFormState: ChecklistFormState,
): FnrsCnsltnApplySaveRequestDto {
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
- splmntDmndList: data.splmntDmndList,
- };
+ splmntDmndList: (data.splmntDmndList ?? []).map(toSplmntDmndSaveItem),
+ });
}
export function buildTechReviewPayload(
det: FnrsCnsltnMngResponseDto,
cklstResultList: CklstResultItem[],
): FnrsCnsltnApplySaveRequestDto {
- return {
+ return fnrsCnsltnApplySaveSchema.parse({
fnrsCnsltnMng: { ...det, prgrsSttsComCd: "ER" },
bzScaleList: [],
bizCstList: [],
cklstResultList,
esntlAtchFileList: [],
- };
+ });
}
diff --git a/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx
index fff55d21a..2374b0c40 100644
--- a/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx
+++ b/apps/as/src/pages/fnrs-cnsltn/model/columns.tsx
@@ -12,7 +12,7 @@ const columnHelper = createColumnHelper();
const MAIN_STEP_LABELS = {
apply: "재원협의 신청",
- envoReview: "환경청 검토",
+ envoReview: "재원협의 검토",
techReview: "기술검토",
reply: "검토결과 회신",
} as const;
@@ -20,7 +20,7 @@ const MAIN_STEP_LABELS = {
export const PROGRESS_STATUS_COLUMNS = [
{ label: "재원협의 신청 (시군)", sttsComCd: "ST" },
{ label: "재원협의 신청 (시도)", sttsComCd: "LE" },
- { label: "환경청 검토", sttsComCd: "ER" },
+ { label: "재원협의 검토", sttsComCd: "ER" },
{ label: "기술검토", sttsComCd: "TE" },
{ label: "기술검토 회신", sttsComCd: "TP" },
{ label: "환경청 검토 결과 회신", sttsComCd: "EP" },
@@ -61,7 +61,7 @@ function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N")
];
}
- if (prgrsSttsComCd === "ER" || prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL") {
+ if (["FV", "ER", "AC", "TE", "RB", "RA", "AV", "EV", "RK", "RR", "SL"].includes(prgrsSttsComCd)) {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
@@ -82,7 +82,7 @@ function buildProgressBadges(prgrsSttsComCd: string, techRplyHstryYn: "Y" | "N")
];
}
- if (prgrsSttsComCd === "EP") {
+ if (prgrsSttsComCd === "EP" || prgrsSttsComCd === "TA") {
return [
{ label: MAIN_STEP_LABELS.apply, state: "completed" },
{ label: MAIN_STEP_LABELS.envoReview, state: "completed" },
diff --git a/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts b/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts
index db60bd696..c3a21fb94 100644
--- a/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts
+++ b/apps/as/src/pages/fnrs-cnsltn/model/view-types.ts
@@ -61,16 +61,25 @@ export const TASK_CODE_LABELS: Record = {
};
export const PBADMS_PROGRESS_ORDER = [
+ // LOCAL
"SV",
- "SL",
- "TA",
"ST",
+ "SL",
+ "RB",
"EP",
+ "TA",
+
+ // ENV
"LE",
"TP",
+ "RA",
+ "RR",
+
+ // KECO
"ER",
"TE",
- "TB",
+ "EV",
+ "RK",
] as const;
export const ROLE_TO_REVIEW_STEP: Record = {
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx
index 00f14e430..4076a83fa 100644
--- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx
+++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx
@@ -9,11 +9,19 @@ import { AxiosError } from "axios";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, useUserStore } from "@/shared/lib";
+import {
+ AI_AUTO_SE,
+ type AiAutoPrcsLatestResponseDto,
+ useAiAutoPrcsLatest,
+} from "@/entities/ai-auto-prcs-mng";
+import { useReGenerateDraft, useReTransferDocument, useReVerifyCost } from "@/entities/dcrag-to";
import {
buildElaprPrgrsSttsListFromAplnRows,
ElaprApprovalDialog,
type ElaprDmndSaveRequestDto,
elaprDmndService,
+ type ElaprDocPrgrsPickerConfirmPayload,
+ ElaprDocPrgrsPickerDialog,
type ElaprTmpltPickerConfirmPayload,
ElaprTmpltPickerDialog,
hasCurrentUserPendingApprovalRequest,
@@ -39,6 +47,7 @@ import {
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
+import { isRagIndexingInProgress } from "../../lib/ai-pending-utils";
import {
FNRS_CNSLTN_VIEW_BUTTON_ACTION,
type FnrsCnsltnViewButtonAction,
@@ -61,6 +70,15 @@ import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog";
import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog";
import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog";
+const AI_RETRY_PRGRS_STTS = new Set(["ER", "TE", "EV"]);
+
+function findLatestByAutoSe(
+ list: AiAutoPrcsLatestResponseDto[] | undefined,
+ atmtnSeComCd: string,
+): AiAutoPrcsLatestResponseDto | undefined {
+ return list?.find((item) => item.atmtnSeComCd === atmtnSeComCd);
+}
+
interface FloatingActionButtonsProps {
data: FnrsCnsltnMngDetResponseDto;
docSeComCd: string;
@@ -77,6 +95,7 @@ interface FloatingActionButtonsProps {
type ActiveDialog =
| "elaprTmplt"
+ | "elaprDocPrgrs"
| "rjct"
| "dmndSplmnt"
| "fileSplmnt"
@@ -110,42 +129,86 @@ function FloatingActionButtons({
const isRegister = data.detDto?.rgtr === userInfo?.userUnqId;
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
- const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
- const needsPwrkDraftCheck = dtlInstCd === "ROLE_USER01" && ["TE", "RK"].includes(prgrsSttsComCd);
+ const needsElaprPrgrsCheck = needsAtrzResolve && ["TE", "RK"].includes(prgrsSttsComCd);
+ const needsPwrkDraftCheck =
+ dtlInstCd === "ROLE_USER01" && ["SL", "TE", "RK"].includes(prgrsSttsComCd);
const needsPwrkForRb = dtlInstCd === "ROLE_USER05" && prgrsSttsComCd === "RB";
- const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb;
+ const needsPwrkForAiRetry = dtlInstCd === "ROLE_USER01" && ["TE", "EV"].includes(prgrsSttsComCd);
+ const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb || needsPwrkForAiRetry;
+ const needsAiAutoLatest = dtlInstCd === "ROLE_USER01" && AI_RETRY_PRGRS_STTS.has(prgrsSttsComCd);
+
const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } =
- useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null);
+ useElaprDocPrgrsByDocNo(needsElaprPrgrsCheck ? docNo : null);
+
const { data: pwrkCnsltnData, isLoading: isPwrkCnsltnLoading } = usePwrkCnsltnMngByUpDocNo(
docNo,
needsPwrkLookup,
);
- const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
- const pwrkCnsltnDocNo = pwrkCnsltnData?.pwrkCnsltnDocNo;
+ const { data: aiAutoLatestList, isLoading: isAiAutoLatestLoading } = useAiAutoPrcsLatest(docNo, {
+ enabled: needsAiAutoLatest,
+ });
+
+ const fileVrfcLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.FILE_VRFC);
+ const draftGenLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.DRAFT_GEN);
+ const costVrfcLatest = findLatestByAutoSe(aiAutoLatestList, AI_AUTO_SE.COST_VRFC);
+ const canRetryFileVrfc = fileVrfcLatest?.isRetryable === true;
+ const canRetryDraftGen = draftGenLatest?.isRetryable === true;
+ const canRetryCostVrfc = costVrfcLatest?.isRetryable === true;
+
+ const reTransferDocument = useReTransferDocument();
+ const reGenerateDraft = useReGenerateDraft();
+ const reVerifyCost = useReVerifyCost();
+ const isBusy =
+ isProcessing ||
+ reTransferDocument.isPending ||
+ reGenerateDraft.isPending ||
+ reVerifyCost.isPending;
+
+ const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
+ const pwrkCnsltnDocNo =
+ pwrkCnsltnData?.pwrkCnsltnDocNo ??
+ draftGenLatest?.pwrkCnsltnDocNo ??
+ costVrfcLatest?.pwrkCnsltnDocNo;
+
+ const elaprPrgrsForPending =
+ prgrsSttsComCd === "RK"
+ ? elaprDocPrgrsList.filter((row) => Number(row.elaprCycl) === 2)
+ : elaprDocPrgrsList;
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
- elaprDocPrgrsList,
+ elaprPrgrsForPending,
userInfo?.userUnqId,
);
+ const viewOnlyPwrkCnsltnDoc =
+ prgrsSttsComCd === "SL" || (prgrsSttsComCd === "RB" && dtlInstCd !== "ROLE_USER05");
const actionButtons = useMemo(() => {
if (needsAtrzResolve && isRvwLoginUserLoading) return [];
- if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return [];
+ if (needsElaprPrgrsCheck && isElaprDocPrgrsLoading) return [];
if (needsPwrkDraftCheck && isPwrkCnsltnLoading) return [];
+ if (needsAiAutoLatest && isAiAutoLatestLoading) return [];
return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, {
- hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined,
+ hasPendingApprovalRequest: needsElaprPrgrsCheck ? hasPendingApprovalRequest : undefined,
hasCompletedDraft: needsPwrkDraftCheck ? hasCompletedDraft : undefined,
+ canRetryFileVrfc: needsAiAutoLatest ? canRetryFileVrfc : undefined,
+ canRetryDraftGen: needsAiAutoLatest ? canRetryDraftGen : undefined,
+ canRetryCostVrfc: needsAiAutoLatest ? canRetryCostVrfc : undefined,
});
}, [
atrzSeComCdList,
+ canRetryCostVrfc,
+ canRetryDraftGen,
+ canRetryFileVrfc,
dtlInstCd,
hasCompletedDraft,
hasPendingApprovalRequest,
+ isAiAutoLatestLoading,
isElaprDocPrgrsLoading,
isPwrkCnsltnLoading,
isRvwLoginUserLoading,
+ needsAiAutoLatest,
needsAtrzResolve,
- needsTeApprovalCheck,
+ needsElaprPrgrsCheck,
needsPwrkDraftCheck,
prgrsSttsComCd,
]);
@@ -252,19 +315,6 @@ function FloatingActionButtons({
router.push("/fnrs-cnsltn");
}
- async function handleFnrsCnsltnAncmntClick() {
- const payload: DocPrgrsUpdateRequestDto = {
- docNo,
- docSeComCd,
- prgrsSttsComCd: "TB",
- };
- await executeAction({
- action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt,
- request: payload,
- successMessage: "고시되었습니다.",
- });
- }
-
async function handleEnvoSbmsnClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -278,19 +328,6 @@ function FloatingActionButtons({
});
}
- async function handleLclRplyClick() {
- const payload: DocPrgrsUpdateRequestDto = {
- docNo,
- docSeComCd,
- prgrsSttsComCd: "RB",
- };
- await executeAction({
- action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply,
- request: payload,
- successMessage: "지자체 회신이 완료되었습니다.",
- });
- }
-
async function handleRjctClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
@@ -313,7 +350,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv,
request: payload,
- successMessage: "인가승인되었습니다.",
+ successMessage: "결과 회신이 완료되었습니다.",
});
}
@@ -329,7 +366,7 @@ function FloatingActionButtons({
);
if (percent !== 100) {
- alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다.");
+ alert("모든 체크리스트 항목을 체크해야 검토 결과 회신이 가능합니다.");
return;
}
@@ -337,7 +374,7 @@ function FloatingActionButtons({
data,
checklistFormState,
"ENV",
- "RM",
+ "EP",
"CP",
getReviewOpinionFormData(),
opnrNm,
@@ -346,7 +383,37 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
request: payload,
- successMessage: "재원협의 결과 회신이 완료되었습니다.",
+ successMessage: "검토 결과 회신이 완료되었습니다.",
+ });
+ techReviewOpinionRef?.current?.resetForm();
+ }
+
+ async function handleRvwOpnnRplyClick() {
+ const { percent } = computeReviewStepChecklistProgress(
+ data.cklstResultList ?? [],
+ checklistFormState.results,
+ "ENV",
+ );
+
+ if (percent !== 100) {
+ alert("모든 체크리스트 항목을 체크해야 검토 의견 회신이 가능합니다.");
+ return;
+ }
+
+ const payload = buildTechReviewReplyPayload(
+ data,
+ checklistFormState,
+ "ENV",
+ "RB",
+ "CP",
+ getReviewOpinionFormData(),
+ opnrNm,
+ );
+
+ await executeAction({
+ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwOpnnRply,
+ request: payload,
+ successMessage: "검토 의견 회신이 완료되었습니다.",
});
techReviewOpinionRef?.current?.resetForm();
}
@@ -358,7 +425,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw,
request: payload,
- successMessage: "기술검토 의뢰가 완료되었습니다.",
+ successMessage: "기술 검토 의뢰가 완료되었습니다.",
});
}
@@ -367,7 +434,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
request: payload,
- successMessage: "조치결과 검토의뢰가 완료되었습니다.",
+ successMessage: "조치 결과 검토 의뢰가 완료되었습니다.",
});
}
@@ -376,7 +443,7 @@ function FloatingActionButtons({
elaprDocNo: docNo,
dmndTtl: data.detDto?.bizNm ?? "",
dmndCn: data.detDto?.bizCn ?? "",
- elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1,
+ elaprCycl: 1,
docPrgrsSttsComCd: "AC",
elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
};
@@ -390,6 +457,25 @@ function FloatingActionButtons({
});
}
+ async function handleElaprDocPrgrsConfirm({ aplnRows }: ElaprDocPrgrsPickerConfirmPayload) {
+ const payload: ElaprDmndSaveRequestDto = {
+ elaprDocNo: docNo,
+ dmndTtl: data.detDto?.bizNm ?? "",
+ dmndCn: data.detDto?.bizCn ?? "",
+ elaprCycl: 2,
+ docPrgrsSttsComCd: "RK",
+ elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
+ };
+ await executeAction({
+ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
+ request: payload,
+ successMessage: "검토자가 지정되었습니다.",
+ });
+ await queryClient.invalidateQueries({
+ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo],
+ });
+ }
+
async function handleRvwStrgClick() {
const payload = buildTechReviewReplyPayload(
data,
@@ -410,15 +496,17 @@ function FloatingActionButtons({
}
async function handleRvwCmptnClick() {
- const { percent } = computeReviewStepChecklistProgress(
- data.cklstResultList ?? [],
- checklistFormState.results,
- "KECO",
- );
+ if (prgrsSttsComCd === "TE") {
+ const { percent } = computeReviewStepChecklistProgress(
+ data.cklstResultList ?? [],
+ checklistFormState.results,
+ "KECO",
+ );
- if (percent !== 100) {
- alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
- return;
+ if (percent !== 100) {
+ alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다.");
+ return;
+ }
}
const payload = {
@@ -426,7 +514,7 @@ function FloatingActionButtons({
data,
checklistFormState,
"KECO",
- "TE",
+ prgrsSttsComCd,
"SV",
getReviewOpinionFormData(),
opnrNm,
@@ -442,17 +530,13 @@ function FloatingActionButtons({
setActiveDialog("elaprApproval");
}
- function handleDmndSplmntClick() {
- setActiveDialog("dmndSplmnt");
- }
-
async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) {
setActiveDialog(null);
const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState);
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
request: requestPayload,
- successMessage: "보완요청이 처리되었습니다.",
+ successMessage: "보완 요청이 처리되었습니다.",
});
}
@@ -466,7 +550,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
request: requestPayload,
- successMessage: "파일보완 요청이 처리되었습니다.",
+ successMessage: "파일 보완 요청이 처리되었습니다.",
});
}
@@ -476,7 +560,7 @@ function FloatingActionButtons({
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
request: requestPayload,
- successMessage: "보완요청이 처리되었습니다.",
+ successMessage: "보완 요청이 처리되었습니다.",
});
}
@@ -496,17 +580,60 @@ function FloatingActionButtons({
});
}
+ function handleFileVrfcClick() {
+ reTransferDocument.mutate(
+ { fnrsCnsltnDocNo: docNo },
+ { onSuccess: () => alert("파일 검증을 요청했습니다.") },
+ );
+ }
+
+ function handlePwrkCnsltnDocCrtClick() {
+ if (!pwrkCnsltnDocNo) {
+ alert("실무협의 문서번호를 찾을 수 없습니다.");
+ return;
+ }
+
+ reGenerateDraft.mutate(
+ {
+ fnrsCnsltnDocNo: docNo,
+ pwrkCnsltnDocNo,
+ crtDmndUuid: draftGenLatest?.crtDmndUuid,
+ },
+ { onSuccess: () => alert("초안 생성을 요청했습니다.") },
+ );
+ }
+
+ function handleCostVrfcClick() {
+ if (!pwrkCnsltnDocNo) {
+ alert("실무협의 문서번호를 찾을 수 없습니다.");
+ return;
+ }
+
+ reVerifyCost.mutate(
+ {
+ fnrsCnsltnDocNo: docNo,
+ pwrkCnsltnDocNo,
+ },
+ { onSuccess: () => alert("금액 검증을 요청했습니다.") },
+ );
+ }
+
return (
<>
-