[as] feat: 실무협의서 검토자 지정(EV) 로직 작업
This commit is contained in:
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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차 탭 표시.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -343,5 +343,6 @@ 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차 결재선 시드 다이얼로그(서식 없음)
|
||||
|
||||
@@ -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`:
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ 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` |
|
||||
@@ -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` | 검토결과조회 | 신규 |
|
||||
@@ -129,6 +130,9 @@ ROLE_USER03
|
||||
ER / ATRZ01 접수자
|
||||
→ 파일보완, 파일검증, 검토자지정
|
||||
|
||||
EV / ATRZ01 접수자
|
||||
→ 검토자지정 (2차 결재선. 1차 결재선 시드, 서식 선택 없음)
|
||||
|
||||
TE / ATRZ02 검토자
|
||||
→ (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청
|
||||
→ (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤)
|
||||
|
||||
@@ -49,9 +49,13 @@ async function remove(elaprDmndId: string): Promise<void> {
|
||||
await client.delete(`${BASE}/${elaprDmndId}`);
|
||||
}
|
||||
|
||||
async function findDocPrgrsByDocNo(elaprDocNo: string): Promise<ElaprDocPrgrsResponseDto[]> {
|
||||
async function findDocPrgrsByDocNo(
|
||||
elaprDocNo: string,
|
||||
elaprCycl?: number,
|
||||
): Promise<ElaprDocPrgrsResponseDto[]> {
|
||||
const response = await client.get<ElaprDocPrgrsResponseDto[]>(
|
||||
`${BASE}/sanc/docprgrs/${elaprDocNo}`,
|
||||
{ params: elaprCycl != null ? { elaprCycl } : undefined },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -185,11 +185,15 @@ 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 },
|
||||
) {
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,13 +288,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);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -22,33 +22,35 @@ function toRow(item: ElaprApln): ElaprAplnRow {
|
||||
}
|
||||
|
||||
interface UseAplnRowsOptions {
|
||||
elaprTmpltId: string | null;
|
||||
seedKey: string | null;
|
||||
serverRows: ElaprApln[] | undefined;
|
||||
elaprTmpltId?: string;
|
||||
}
|
||||
|
||||
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<ElaprAplnRow[]>([]);
|
||||
const [editingRowKey, setEditingRowKey] = useState<string | null>(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();
|
||||
|
||||
@@ -66,7 +68,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) {
|
||||
},
|
||||
]);
|
||||
setEditingRowKey(rowKey);
|
||||
}, [elaprTmpltId, isEditingLocked]);
|
||||
}, [canAdd, elaprTmpltId]);
|
||||
|
||||
const startEdit = useCallback(
|
||||
(rowKey: string) => {
|
||||
|
||||
@@ -45,5 +45,5 @@ export interface ElaprDmndService {
|
||||
update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise<ElaprDmndResponseDto>;
|
||||
updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise<ElaprDmndResponseDto>;
|
||||
remove(elaprDmndId: string): Promise<void>;
|
||||
findDocPrgrsByDocNo(elaprDocNo: string): Promise<ElaprDocPrgrsResponseDto[]>;
|
||||
findDocPrgrsByDocNo(elaprDocNo: string, elaprCycl?: number): Promise<ElaprDocPrgrsResponseDto[]>;
|
||||
}
|
||||
|
||||
@@ -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<string | null>(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,7 +85,10 @@ 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;
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
@@ -121,7 +134,7 @@ function ElaprAplnPickerGrid({
|
||||
size="sm"
|
||||
type="button"
|
||||
color="green"
|
||||
disabled={isDisabled || isEditingLocked}
|
||||
disabled={!isReady || isEditingLocked}
|
||||
onClick={addRow}
|
||||
>
|
||||
결재선 추가
|
||||
@@ -129,15 +142,15 @@ function ElaprAplnPickerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aplnQuery.isPending && elaprTmpltId ? (
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-[4rem]" />
|
||||
<Skeleton className="h-[16rem]" />
|
||||
</div>
|
||||
) : !elaprTmpltId ? (
|
||||
) : !isReady ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">결재서식을 선택하세요.</p>
|
||||
) : visibleRows.length === 0 ? (
|
||||
<p className="py-8 text-center text-lg text-muted-foreground">
|
||||
<p className="py-16 text-center text-lg text-muted-foreground">
|
||||
등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요.
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -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<ElaprAplnRow[]>([]);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>결재선 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<ElaprAplnPickerGrid
|
||||
externalServerRows={externalServerRows}
|
||||
isExternalLoading={docPrgrsQuery.isPending}
|
||||
onRowsChange={handleRowsChange}
|
||||
onEditingLockedChange={handleEditingLockedChange}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={() => handleOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
<Button type="button" disabled={isEditingLocked} onClick={handleConfirm}>
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprDocPrgrsPickerDialog };
|
||||
export type { ElaprDocPrgrsPickerConfirmPayload };
|
||||
@@ -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 <p className="px-4 py-6 text-xl text-muted-foreground">결재선이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
{index > 0 && (
|
||||
<div className="flex items-center">
|
||||
<ChevronRight className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SectionCollapsible title="결재현황">
|
||||
<div className="flex flex-wrap items-stretch justify-start gap-4 overflow-x-auto p-4">
|
||||
{steps.map((item, index) => (
|
||||
<Fragment key={item.elaprPrgrsId ?? `${item.atrzSeqNo}-${item.aprvrUuid ?? index}`}>
|
||||
{index > 0 && (
|
||||
<div className="flex items-center">
|
||||
{<ChevronRight className="size-8 text-muted-foreground" />}
|
||||
</div>
|
||||
)}
|
||||
<ElaprDocPrgrsStep item={item} />
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{hasCycl2 ? (
|
||||
<Tabs
|
||||
value={activeCycl}
|
||||
onValueChange={(value) => setActiveCycl(value as "1" | "2")}
|
||||
className="p-2"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="1" className="!px-6 !py-3 text-xl">
|
||||
재원협의
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="2" className="!px-6 !py-3 text-xl">
|
||||
실무협의서
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="1" className="mt-2 border-none p-0 shadow-none">
|
||||
<div className="-mx-2">
|
||||
<ElaprDocPrgrsStepList steps={cycl1Steps} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="2" className="mt-2 border-none p-0 shadow-none">
|
||||
<div className="-mx-2">
|
||||
<ElaprDocPrgrsStepList steps={cycl2Steps} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<ElaprDocPrgrsStepList steps={cycl1Steps} />
|
||||
)}
|
||||
</SectionCollapsible>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,10 +38,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.aply,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.del,
|
||||
],
|
||||
RB: [
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn,
|
||||
],
|
||||
RB: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng, FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn],
|
||||
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt],
|
||||
},
|
||||
ROLE_USER04: {
|
||||
@@ -59,7 +56,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
|
||||
},
|
||||
};
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE", "RK"] as const;
|
||||
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE", "RK", "EV"] as const;
|
||||
|
||||
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
string,
|
||||
@@ -68,6 +65,9 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
ER: {
|
||||
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
|
||||
},
|
||||
EV: {
|
||||
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
|
||||
},
|
||||
TE: {
|
||||
ATRZ02: [
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt,
|
||||
|
||||
@@ -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"
|
||||
@@ -376,7 +379,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 +393,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,
|
||||
@@ -442,17 +464,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 +484,7 @@ function FloatingActionButtons({
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
|
||||
request: requestPayload,
|
||||
successMessage: "파일보완 요청이 처리되었습니다.",
|
||||
successMessage: "파일 보완 요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -476,7 +494,7 @@ function FloatingActionButtons({
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
|
||||
request: requestPayload,
|
||||
successMessage: "보완요청이 처리되었습니다.",
|
||||
successMessage: "보완 요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -586,7 +604,12 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
|
||||
<Button onClick={() => setActiveDialog("elaprTmplt")} disabled={isProcessing}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setActiveDialog(prgrsSttsComCd === "EV" ? "elaprDocPrgrs" : "elaprTmplt")
|
||||
}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
검토자 지정
|
||||
</Button>
|
||||
)}
|
||||
@@ -615,7 +638,11 @@ function FloatingActionButtons({
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && (
|
||||
<Button color="gray" onClick={handleDmndSplmntClick} disabled={isProcessing}>
|
||||
<Button
|
||||
color="gray"
|
||||
onClick={() => setActiveDialog("dmndSplmnt")}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
보완 요청
|
||||
</Button>
|
||||
)}
|
||||
@@ -643,6 +670,13 @@ function FloatingActionButtons({
|
||||
onConfirm={handleElaprTmpltConfirm}
|
||||
/>
|
||||
|
||||
<ElaprDocPrgrsPickerDialog
|
||||
open={activeDialog === "elaprDocPrgrs"}
|
||||
elaprDocNo={docNo}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
onConfirm={handleElaprDocPrgrsConfirm}
|
||||
/>
|
||||
|
||||
<SplmntDmndDialog
|
||||
splmntArtclUpCd="SPLMNTARTCL"
|
||||
open={activeDialog === "dmndSplmnt"}
|
||||
|
||||
@@ -151,6 +151,7 @@ export interface ElaprDocPrgrsClientResponseDto {
|
||||
jbgdSeNm?: string;
|
||||
ognzNm?: string;
|
||||
dcarbYn?: string;
|
||||
elaprCycl?: number;
|
||||
}
|
||||
|
||||
/** 문서 상태 변경 이력 응답 DTO */
|
||||
|
||||
Reference in New Issue
Block a user