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/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 - - - - - - - {aplnQuery.isPending && elaprTmpltId ? ( + {isLoading ? (
- ) : !elaprTmpltId ? ( + ) : !isReady ? (

결재서식을 선택하세요.

) : visibleRows.length === 0 ? ( -

+

등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.

) : ( diff --git a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index ad9abfa3e..ed8a5de01 100644 --- a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; @@ -48,6 +48,7 @@ interface ElaprApprovalDialogProps { onOpenChange: (open: boolean) => 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/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/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index d0b906dae..8ec805852 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -2,11 +2,9 @@ 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", rqstTechRvw: "rqstTechRvw", fileSplmnt: "fileSplmnt", @@ -38,16 +36,12 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record([ - ...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 +142,19 @@ 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 (options?.hasCompletedDraft != null) { + if (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; + } + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { + 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..21668e8b8 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 @@ -72,9 +72,9 @@ export const fnrsCnsltnRegistSchema = z.object({ cklstSn: z.number(), cklstArtclSn: z.number(), cklstRvwSn: z.number(), - rvwStpComCd: z.string(), + rvwStpComCd: z.string().nullish(), selected: z.enum(["Y", "N"]).nullable(), - picRvwCn: z.string().nullable().optional(), + picRvwCn: z.string().nullish(), }), ), esntlAtchFileList: z.array( 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/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..7b776614d 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 @@ -14,6 +14,8 @@ import { ElaprApprovalDialog, type ElaprDmndSaveRequestDto, elaprDmndService, + type ElaprDocPrgrsPickerConfirmPayload, + ElaprDocPrgrsPickerDialog, type ElaprTmpltPickerConfirmPayload, ElaprTmpltPickerDialog, hasCurrentUserPendingApprovalRequest, @@ -77,6 +79,7 @@ interface FloatingActionButtonsProps { type ActiveDialog = | "elaprTmplt" + | "elaprDocPrgrs" | "rjct" | "dmndSplmnt" | "fileSplmnt" @@ -110,30 +113,37 @@ function FloatingActionButtons({ const isRegister = data.detDto?.rgtr === userInfo?.userUnqId; const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); - const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; + const needsElaprPrgrsCheck = needsAtrzResolve && ["TE", "RK"].includes(prgrsSttsComCd); const needsPwrkDraftCheck = dtlInstCd === "ROLE_USER01" && ["TE", "RK"].includes(prgrsSttsComCd); const needsPwrkForRb = dtlInstCd === "ROLE_USER05" && prgrsSttsComCd === "RB"; const needsPwrkLookup = needsPwrkDraftCheck || needsPwrkForRb; + 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 elaprPrgrsForPending = + prgrsSttsComCd === "RK" + ? elaprDocPrgrsList.filter((row) => Number(row.elaprCycl) === 2) + : elaprDocPrgrsList; const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( - elaprDocPrgrsList, + elaprPrgrsForPending, userInfo?.userUnqId, ); const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; - if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; + if (needsElaprPrgrsCheck && isElaprDocPrgrsLoading) return []; if (needsPwrkDraftCheck && isPwrkCnsltnLoading) return []; return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { - hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + hasPendingApprovalRequest: needsElaprPrgrsCheck ? hasPendingApprovalRequest : undefined, hasCompletedDraft: needsPwrkDraftCheck ? hasCompletedDraft : undefined, }); }, [ @@ -145,7 +155,7 @@ function FloatingActionButtons({ isPwrkCnsltnLoading, isRvwLoginUserLoading, needsAtrzResolve, - needsTeApprovalCheck, + needsElaprPrgrsCheck, needsPwrkDraftCheck, prgrsSttsComCd, ]); @@ -252,19 +262,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 +275,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 +297,7 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.sncnAprv, request: payload, - successMessage: "인가승인되었습니다.", + successMessage: "결과 회신이 완료되었습니다.", }); } @@ -329,15 +313,16 @@ function FloatingActionButtons({ ); if (percent !== 100) { - alert("모든 체크리스트 항목을 체크해야 검토결과 회신이 가능합니다."); + alert("모든 체크리스트 항목을 체크해야 검토 결과 회신이 가능합니다."); return; } + const isResultReply = prgrsSttsComCd === "RR"; const payload = buildTechReviewReplyPayload( data, checklistFormState, "ENV", - "RM", + isResultReply ? "EP" : "RB", "CP", getReviewOpinionFormData(), opnrNm, @@ -346,19 +331,32 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, request: payload, - successMessage: "재원협의 결과 회신이 완료되었습니다.", + successMessage: "검토 결과 회신이 완료되었습니다.", }); techReviewOpinionRef?.current?.resetForm(); } async function handleRqstTechRvwClick() { + if (prgrsSttsComCd === "LE") { + const { percent } = computeReviewStepChecklistProgress( + data.cklstResultList ?? [], + checklistFormState.results, + "ENV", + ); + + if (percent !== 100) { + alert("모든 체크리스트 항목을 체크해야 기술 검토 의뢰가 가능합니다."); + return; + } + } + const payload: FnrsExamineRequestDto = { prgrsSttsComCd: "FV", }; await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw, request: payload, - successMessage: "기술검토 의뢰가 완료되었습니다.", + successMessage: "기술 검토 의뢰가 완료되었습니다.", }); } @@ -376,7 +374,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 +388,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 +427,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 +445,7 @@ function FloatingActionButtons({ data, checklistFormState, "KECO", - "TE", + prgrsSttsComCd, "SV", getReviewOpinionFormData(), opnrNm, @@ -442,17 +461,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 +481,7 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, request: requestPayload, - successMessage: "파일보완 요청이 처리되었습니다.", + successMessage: "파일 보완 요청이 처리되었습니다.", }); } @@ -476,7 +491,7 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, request: requestPayload, - successMessage: "보완요청이 처리되었습니다.", + successMessage: "보완 요청이 처리되었습니다.", }); } @@ -540,21 +555,11 @@ function FloatingActionButtons({ 삭제 )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt) && ( - - )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && ( )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply) && ( - - )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( @@ -586,7 +591,12 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( - )} @@ -615,7 +625,11 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( - )} @@ -643,6 +657,13 @@ function FloatingActionButtons({ onConfirm={handleElaprTmpltConfirm} /> + setActiveDialog(null)} + onConfirm={handleElaprDocPrgrsConfirm} + /> + setActiveDialog(open ? "elaprApproval" : null)} elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo} currentUserUuid={userInfo?.userUnqId ?? ""} + elaprCycl={prgrsSttsComCd === "RK" ? 2 : undefined} onSuccess={() => { queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo), @@ -675,7 +697,8 @@ function FloatingActionButtons({ open={activeDialog === "pwrkCnsltnDocMng"} fnrsCnsltnDocNo={docNo} atrzSeComCdList={atrzSeComCdList} - canSave={prgrsSttsComCd === "RA" ? false : undefined} + prgrsSttsComCd={prgrsSttsComCd} + canSave={needsElaprPrgrsCheck && !hasPendingApprovalRequest ? false : undefined} onClose={() => setActiveDialog(null)} /> @@ -806,25 +829,13 @@ async function handleActnCmptn( } } -async function handleAtrzAprv(context: ActionHandlerContext): Promise { - // TODO: 결재 승인 처리 API 호출 - context.onSuccess?.(); -} - -async function handleAtrzRply(context: ActionHandlerContext): Promise { - // TODO: 결재 회신 처리 API 호출 - context.onSuccess?.(); -} - const buttonActionHandlers: Record = { strg: handleSaveApply, aply: handleSaveApply, del: handleDel, - fnrsCnsltnAncmnt: handleChangePrgrsSttsComCd, envoSbmsn: handleChangePrgrsSttsComCd, rjct: handleChangePrgrsSttsComCd, sncnAprv: handleChangePrgrsSttsComCd, - lclRply: handleChangePrgrsSttsComCd, rvwRply: handleTechReview, rqstTechRvw: handleRequestExamine, fileSplmnt: handleDmndSplmnt, @@ -835,8 +846,8 @@ const buttonActionHandlers: Record = rvwStrg: handleTechReview, rvwCmptn: handleTechReview, dmndSplmnt: handleDmndSplmnt, - atrzAprv: handleAtrzAprv, - atrzRply: handleAtrzRply, + atrzAprv: () => Promise.resolve(), + atrzRply: () => Promise.resolve(), actnCmptn: handleActnCmptn, rqstActnRvw: handleRequestActnReview, }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx index f87fbed9b..040d3ec40 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx @@ -1,9 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { PanelRightClose, PanelRightOpen } from "lucide-react"; import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib"; import { AttachmentFileField } from "@/shared/ui"; @@ -12,6 +13,7 @@ import { pwrkCnsltnDocCrtRsltSaveRequestSchema, pwrkCnsltnDocCrtRsltService, } from "@/entities/pwrk-cnsltn-doc-crt-rslt"; +import { usePwrkCnsltnDocPjtcoAjmtMngPage } from "@/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng"; import { findCompletedPwrkDraft, pwrkCnsltnMngKeys, @@ -27,6 +29,8 @@ import { Skeleton, } from "@common/components"; +import { PwrkCnsltnDocPjtcoAjmtTable } from "./pwrk-cnsltn-doc-pjtco-ajmt-table"; + const HangulGiangi = dynamic( () => import("@/widgets/hangulgiangi").then((module) => module.HangulGiangi), { @@ -39,10 +43,13 @@ const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::"; const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]); +const VIEW_ONLY_PRGRS_STTS_COM_CDS = new Set(["RA", "RR", "EP", "TA"]); + interface PwrkCnsltnDocMngDialogProps { open: boolean; fnrsCnsltnDocNo: string; - atrzSeComCdList: string[]; + atrzSeComCdList?: string[]; + prgrsSttsComCd?: string; canSave?: boolean; onClose: () => void; } @@ -62,6 +69,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, atrzSeComCdList, + prgrsSttsComCd, canSave: canSaveProp, onClose, }: PwrkCnsltnDocMngDialogProps) { @@ -69,6 +77,7 @@ function PwrkCnsltnDocMngDialog({ const alert = useAlertStore((state) => state.setMessage); const [atchFileGroupId, setAtchFileGroupId] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); + const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false); const { data: pwrkCnsltnData, @@ -92,6 +101,20 @@ function PwrkCnsltnDocMngDialog({ const documentFileName = `실무협의서_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; const isLoading = isPwrkCnsltnLoading || isDocumentLoading; const isError = isPwrkCnsltnError || isDocumentError; + const pjtcoAjmtSearchParams = useMemo( + () => ({ + pwrkCnsltnDocNo: pwrkCnsltnData?.pwrkCnsltnDocNo, + page: 0, + size: 1000, + sort: "rvwOpnnSn,asc", + }), + [pwrkCnsltnData?.pwrkCnsltnDocNo], + ); + const { + data: pjtcoAjmtPage, + isLoading: isPjtcoAjmtLoading, + isError: isPjtcoAjmtError, + } = usePwrkCnsltnDocPjtcoAjmtMngPage(pjtcoAjmtSearchParams, open); const { uploadFileAsync, isUploading, uploads } = useTus({ onSuccess: (atflCd, atflId, _file, fileKey) => { @@ -234,8 +257,11 @@ function PwrkCnsltnDocMngDialog({ } const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]); - const canSaveByAtrz = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)); - const canSave = canSaveProp ?? canSaveByAtrz; + const viewOnly = prgrsSttsComCd ? VIEW_ONLY_PRGRS_STTS_COM_CDS.has(prgrsSttsComCd) : false; + const canSaveByAtrz = atrzSeComCdList + ? !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)) + : true; + const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz); return ( @@ -258,17 +284,54 @@ function PwrkCnsltnDocMngDialog({ 생성 완료된 실무협의서 초안이 없습니다.

) : null} - {completedDraft ? ( - documentBlob ? ( - - ) : null + {completedDraft && documentBlob ? ( +
+
+ +
+ {pjtcoAjmtPage?.content?.length ? ( + isPjtcoAjmtTableOpen ? ( +
+
+

금액검증결과

+ +
+
+ +
+
+ ) : ( + + ) + ) : null} +
) : null} {completedDraft ? ( diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-pjtco-ajmt-table.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-pjtco-ajmt-table.tsx new file mode 100644 index 000000000..cc0c9ba35 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-pjtco-ajmt-table.tsx @@ -0,0 +1,97 @@ +"use client"; + +import type { PwrkCnsltnDocPjtcoAjmtMngResponseDto } from "@/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng"; +import { fmtNumber } from "@/widgets/pbadms-sprt"; +import { + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@common/components"; + +interface PwrkCnsltnDocPjtcoAjmtTableProps { + rows: PwrkCnsltnDocPjtcoAjmtMngResponseDto[]; + isLoading: boolean; + isError: boolean; +} + +function PwrkCnsltnDocPjtcoAjmtTable({ + rows, + isLoading, + isError, +}: PwrkCnsltnDocPjtcoAjmtTableProps) { + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (isError) { + return ( +

+ 사업비조정 정보를 불러오지 못했습니다. +

+ ); + } + + return ( + + + + 구분 + 검증결과 + 검증금액 + + + + {rows.length === 0 ? ( + + + 조회된 사업비조정 항목이 없습니다. + + + ) : ( + rows.map((row) => ( + + {formatDivision(row)} + +

{formatVrfcCn(row.vrfcCn)}

+
+ {formatVrfcAmt(row.vrfcTpcoAmt)} +
+ )) + )} +
+
+ ); +} + +function getPjtcoAjmtRowKey(row: PwrkCnsltnDocPjtcoAjmtMngResponseDto) { + return [row.pwrkCnsltnDocNo, row.rvwOpnnSn, row.acntgSeCd].join("::"); +} + +function formatDivision(row: PwrkCnsltnDocPjtcoAjmtMngResponseDto) { + const label = row.acntgSeCdNm?.trim() || row.acntgSeCd?.trim(); + return label || "-"; +} + +function formatVrfcCn(value: string | undefined) { + const text = value?.trim(); + return text || "-"; +} + +function formatVrfcAmt(value: number | undefined) { + if (value === undefined || value === null) return "-"; + const formatted = fmtNumber(value); + return formatted || "-"; +} + +export { PwrkCnsltnDocPjtcoAjmtTable }; +export type { PwrkCnsltnDocPjtcoAjmtTableProps }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx index e811873f4..91a5896ff 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx @@ -96,7 +96,7 @@ function PwrkCnsltnRvwRsltInqDialog({ aria-describedby={undefined} > - 검토결과조회 + 검토 결과 조회 {isLoading ? ( @@ -159,9 +159,9 @@ function CnsltnListTable({ return ( - - - + + + @@ -172,7 +172,7 @@ function CnsltnListTable({ aria-label="전체 선택" /> - 분류 + 구분 검토의견 @@ -194,8 +194,8 @@ function CnsltnListTable({ aria-label="행 선택" /> - -

{formatOptional(row.rmrk)}

+ +

{formatOptional(row.rvwOpnnSn)}

{formatOptional(row.rvwOpnn)}

diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx index 671b70f98..632c51e3b 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx @@ -47,6 +47,7 @@ import { import { getCreateDefaultValues, mapDetailToFormData } from "../lib/form-data-mapper"; import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; +import { PwrkCnsltnDocMngDialog } from "./components/pwrk-cnsltn-doc-mng-dialog"; import { BasicInfoFormSection } from "./sections/basic-info-form-section"; interface FnrsFormPageProps { mode: "create" | "edit"; @@ -66,8 +67,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) const userInfo = useUserStore((state) => state.userInfo); const alert = useAlertStore((state) => state.setMessage); const [isSubmitting, setIsSubmitting] = useState(false); + const [isPwrkCnsltnDocMngOpen, setIsPwrkCnsltnDocMngOpen] = useState(false); const isEditMode = mode === "edit"; const isSplmntPrgrsStts = initialData?.detDto?.prgrsSttsComCd === "SL"; + const fnrsCnsltnDocNo = initialData?.detDto?.fnrsCnsltnDocNo; const { data: masterSharTree = [] } = useFnrsSharSeMngTree(); const { data: acntgTree = [] } = useAcntgSeMngTree(); @@ -156,7 +159,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) (item) => !item.splmntCn || !item.splmntCn.trim(), ); if (hasEmptySplmntCn) { - alert("보완요청내용이 있습니다. 보완완료내용을 입력해 주세요."); + alert("보완요청내용이 있습니다. 보완 완료내용을 입력해 주세요."); return; } @@ -171,12 +174,12 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) const result = await submitSupplementForm(data); if (!result) return; - alert("보완완료 처리되었습니다."); + alert("보완 완료 처리되었습니다."); await queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all }); router.push(`/fnrs-cnsltn/${docNo}`); } catch (error) { - console.error("보완완료 실패:", error); - alert("보완완료 처리 중 오류가 발생했습니다."); + console.error("보완 완료 실패:", error); + alert("보완 완료 처리 중 오류가 발생했습니다."); } finally { setIsSubmitting(false); } @@ -334,14 +337,32 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) onClick={form.handleSubmit(onSplmntCmptn)} disabled={isSubmitting} > - 보완완료 + 보완 완료 )} + {isSplmntPrgrsStts && fnrsCnsltnDocNo ? ( + + ) : null} + {fnrsCnsltnDocNo ? ( + setIsPwrkCnsltnDocMngOpen(false)} + /> + ) : null} ); } diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx index caa4a3ada..837a97239 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx @@ -136,8 +136,7 @@ const TechReviewOpinionSection = forwardRef< const kecoRvwList = rvwList.filter((item) => item.opnrSeComCd === "KECO"); const hasEnvSection = envRvwList.length > 0 || editableReviewStep === "ENV"; - const hasKecoSection = - kecoRvwList.length > 0 || (editableReviewStep === "KECO" && canWriteKecoOpinion); + const hasKecoSection = kecoRvwList.length > 0 || editableReviewStep === "KECO"; if (!hasEnvSection && !hasKecoSection) { return null; diff --git a/apps/as/src/pages/instl-sncn/model/columns.tsx b/apps/as/src/pages/instl-sncn/model/columns.tsx index 2dcca5c58..11d51de65 100644 --- a/apps/as/src/pages/instl-sncn/model/columns.tsx +++ b/apps/as/src/pages/instl-sncn/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" }, diff --git a/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx index 4dc1af0c6..e75312b0b 100644 --- a/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/model/columns.tsx @@ -12,14 +12,14 @@ const columnHelper = createColumnHelper(); const MAIN_STEP_LABELS = { apply: "기본계획 신청", - envoReview: "환경청 검토", + envoReview: "기본계획 검토", techReview: "기술검토", reply: "검토결과 회신", } as const; export const PROGRESS_STATUS_COLUMNS = [ { label: "기본계획 신청 (시군)", sttsComCd: "LE" }, - { label: "환경청 검토", sttsComCd: "ER" }, + { label: "기본계획 검토", sttsComCd: "ER" }, { label: "기술검토", sttsComCd: "TE" }, { label: "기술검토 회신", sttsComCd: "TP" }, { label: "횐경청 검토 결과 회신", sttsComCd: "EP" }, diff --git a/apps/as/src/shared/config/api.ts b/apps/as/src/shared/config/api.ts index 3e7d08c08..a575822f2 100644 --- a/apps/as/src/shared/config/api.ts +++ b/apps/as/src/shared/config/api.ts @@ -28,6 +28,7 @@ export const ENDPOINT = { DCRAG_TO: "/as-service/api/v1/dcrag-to", PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng", PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt", + PWRK_CNSLTN_DOC_PJTCO_AJMT_MNG: "/as-service/api/v1/pwrk-cnsltn-doc-pjtco-ajmt-mng", }, USER_SERVICE: { LOGIN: "/user-service/login", diff --git a/apps/as/src/shared/model/pbadms-sprt.ts b/apps/as/src/shared/model/pbadms-sprt.ts index e8f2b6b1d..959c8133d 100644 --- a/apps/as/src/shared/model/pbadms-sprt.ts +++ b/apps/as/src/shared/model/pbadms-sprt.ts @@ -54,9 +54,9 @@ export interface BizCstTreeDto { bscPlanAmt: number; /** 금회설계금액(C) */ tstmDesignAmt: number; - /** 증감금액(금회설계금액 - 표준사업비, C-B) */ + /** 증감금액(금회설계금액 - 예산반영금액, C-A) */ chgAmt: number; - /** 증감율 % ((증감금액 / 표준사업비) * 100) */ + /** 증감율 % ((증감금액 / 예산반영금액(A)) * 100) */ chgRt: number; } @@ -151,6 +151,7 @@ export interface ElaprDocPrgrsClientResponseDto { jbgdSeNm?: string; ognzNm?: string; dcarbYn?: string; + elaprCycl?: number; } /** 문서 상태 변경 이력 응답 DTO */ @@ -190,7 +191,7 @@ export interface CklstResultSaveBase { cklstRvwSn?: number; cklstArtclSn?: number; cklstSn?: number; - rvwStpComCd: string; + rvwStpComCd?: string | null; picRvwCn?: string | null; } diff --git a/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts index 814a8c5a1..bc3ac97cd 100644 --- a/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts +++ b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts @@ -58,6 +58,7 @@ export interface HwpCtrl { callback?: HwpCtrlSaveAsCallback, callbackUserData?: unknown, ) => boolean; + ShowHorizontalScroll: (show: boolean) => boolean; } export interface HwpCtrlFrameWindow { diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx index 16663faa2..db9db7c32 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -87,7 +87,10 @@ function HangulGiangi({ "HWPX", "", (result) => { - const callbackDocumentKey = getCallbackDocumentKey(result.CallbackUserData, documentKey); + const callbackDocumentKey = getCallbackDocumentKey( + result.CallbackUserData, + documentKey, + ); if (openingDocumentKeyRef.current !== callbackDocumentKey) return; openingDocumentKeyRef.current = null; diff --git a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts index fbf63472d..52ad49f7a 100644 --- a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts +++ b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts @@ -21,14 +21,12 @@ interface CklstResultListItem { cklstSn: number; cklstArtclSn: number; cklstRvwSn: number; - rvwStpComCd: string; + rvwStpComCd?: string | null; selected: "Y" | "N" | null; picRvwCn?: string | null; } -export function normalizeTaskSeComCdArray( - value: string | string[] | undefined | null, -): string[] { +export function normalizeTaskSeComCdArray(value: string | string[] | undefined | null): string[] { if (!value) return []; if (Array.isArray(value)) return value.filter(Boolean); return value diff --git a/apps/as/src/widgets/pbadms-sprt/lib/fnrs-shar-form-utils.ts b/apps/as/src/widgets/pbadms-sprt/lib/fnrs-shar-form-utils.ts index 4700fdac1..d593d4aee 100644 --- a/apps/as/src/widgets/pbadms-sprt/lib/fnrs-shar-form-utils.ts +++ b/apps/as/src/widgets/pbadms-sprt/lib/fnrs-shar-form-utils.ts @@ -122,7 +122,7 @@ function findBizCstSyncItem(costItems: CostItemDef[]): CostItemDef | undefined { } export interface BizCstTotalAmounts { - bscPlanAmt: number; + bgtRfltAmt: number; tstmDesignAmt: number; } @@ -136,13 +136,13 @@ export function getBizCstTotalRow( costItems: CostItemDef[], ): BizCstTotalAmounts { const syncCostItem = findBizCstSyncItem(costItems); - if (!syncCostItem) return { bscPlanAmt: 0, tstmDesignAmt: 0 }; + if (!syncCostItem) return { bgtRfltAmt: 0, tstmDesignAmt: 0 }; const costRow = bizCstList.find((item) => item.acntgSeCd === syncCostItem.code); - if (!costRow) return { bscPlanAmt: 0, tstmDesignAmt: 0 }; + if (!costRow) return { bgtRfltAmt: 0, tstmDesignAmt: 0 }; return { - bscPlanAmt: costRow.bscPlanAmt || 0, + bgtRfltAmt: costRow.bgtRfltAmt || 0, tstmDesignAmt: costRow.tstmDesignAmt || 0, }; } @@ -178,7 +178,7 @@ export function calcBizCstSharRemaining( const leafSum = sumFnrsSharLeafAmounts(sharList, sharItems); return { - exstnRemaining: roundAmount(bizCstTotal.bscPlanAmt - leafSum.exstnAmt), + exstnRemaining: roundAmount(bizCstTotal.bgtRfltAmt - leafSum.exstnAmt), chgRqtRemaining: roundAmount(bizCstTotal.tstmDesignAmt - leafSum.chgRqtAmt), }; } diff --git a/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx index 22aa35c82..dc50504a5 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx @@ -120,14 +120,14 @@ function BusinessCostFormSection() { ( handleCostChange(idx, "bscPlanAmt", e.target.value)} + onChange={(e) => handleCostChange(idx, "bgtRfltAmt", e.target.value)} readOnly={item.readonly} aria-invalid={fieldState.invalid} className={`text-right ${item.readonly ? "bg-muted" : ""}`} @@ -138,14 +138,14 @@ function BusinessCostFormSection() { ( handleCostChange(idx, "bgtRfltAmt", e.target.value)} + onChange={(e) => handleCostChange(idx, "bscPlanAmt", e.target.value)} readOnly={item.readonly} aria-invalid={fieldState.invalid} className={`text-right ${item.readonly ? "bg-muted" : ""}`} diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx index 752898721..2e225a286 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx @@ -199,7 +199,7 @@ function SplmntDmndDialog({ try { await attachService.deleteAttachmentByAtflId(atflId); } catch (error) { - console.error("보완요청 첨부파일 삭제 실패:", error); + console.error("보완 요청 첨부파일 삭제 실패:", error); } } @@ -226,7 +226,7 @@ function SplmntDmndDialog({ - 보완요청 + 보완 요청
@@ -322,7 +322,7 @@ function SplmntDmndDialog({ 취소