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..ddb0f5f3d 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md +++ b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md @@ -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차 결재선 시드 다이얼로그(서식 없음) 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..1c9d8565d 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md +++ b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md @@ -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 검토자 → (초안 성공) 실무협의서관리, 검토결과조회, 저장, 검토완료, 보완요청 → (초안 실패·타임아웃) 실무협의서생성 (+ 위 항목 중 관리/조회는 초안 생긴 뒤) diff --git a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts index 368600110..b366afb60 100644 --- a/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-dmnd-service.ts @@ -49,9 +49,13 @@ async function remove(elaprDmndId: string): Promise { await client.delete(`${BASE}/${elaprDmndId}`); } -async function findDocPrgrsByDocNo(elaprDocNo: string): Promise { +async function findDocPrgrsByDocNo( + elaprDocNo: string, + elaprCycl?: number, +): Promise { const response = await client.get( `${BASE}/sanc/docprgrs/${elaprDocNo}`, + { params: elaprCycl != null ? { elaprCycl } : undefined }, ); return response.data; } diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts index 734c6cdf4..19f5fce9b 100644 --- a/apps/as/src/entities/elapr/api/queries.ts +++ b/apps/as/src/entities/elapr/api/queries.ts @@ -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); }, diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index 4805fecc3..777245649 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -57,6 +57,7 @@ export { buildElaprPrgrsSttsListFromApln, buildElaprPrgrsSttsListFromAplnRows, } from "./lib/dmnd-save-utils"; +export { mapDocPrgrsToAplnRows } from "./lib/doc-prgrs-to-apln-rows"; export type { ElaprPrgrsSttsRow } from "./lib/prgrs-stts-save-utils"; export { assignPrgrsSttsAtrzSeqNumbers, @@ -113,6 +114,10 @@ export type { ReviewerOption } from "./model/reviewer-option"; export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog"; export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid"; export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog"; +export { + type ElaprDocPrgrsPickerConfirmPayload, + ElaprDocPrgrsPickerDialog, +} from "./ui/elapr-doc-prgrs-picker-dialog"; export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status"; export { type ElaprTmpltPickerConfirmPayload, diff --git a/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts b/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts new file mode 100644 index 000000000..a4cf562d2 --- /dev/null +++ b/apps/as/src/entities/elapr/lib/doc-prgrs-to-apln-rows.ts @@ -0,0 +1,21 @@ +import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model"; + +import type { ElaprApln } from "../model/elapr-apln-interface"; + +/** 문서 결재진행 → 결재선 그리드 시드 (ElaprApln 호환) */ +export function mapDocPrgrsToAplnRows( + list: ElaprDocPrgrsClientResponseDto[] | undefined, +): ElaprApln[] { + if (!list?.length) return []; + + return list.map((item, index) => ({ + aplnId: item.elaprPrgrsId ?? `prgrs-${item.atrzSeqNo ?? index}`, + elaprTmpltId: "", + atrzSeqNo: item.atrzSeqNo, + atrzSeComCd: item.atrzSeComCd, + atrzSeComNm: item.atrzSeComNm, + aprvrUuid: item.aprvrUuid, + aprvrNm: item.aprvrNm, + aprvrUserId: item.aprvrUserId, + })); +} diff --git a/apps/as/src/entities/elapr/lib/use-apln-rows.ts b/apps/as/src/entities/elapr/lib/use-apln-rows.ts index 384c8ffc1..dfa731d07 100644 --- a/apps/as/src/entities/elapr/lib/use-apln-rows.ts +++ b/apps/as/src/entities/elapr/lib/use-apln-rows.ts @@ -22,33 +22,35 @@ function toRow(item: ElaprApln): ElaprAplnRow { } interface UseAplnRowsOptions { - elaprTmpltId: string | null; + seedKey: string | null; serverRows: ElaprApln[] | undefined; + elaprTmpltId?: string; } export function createDraftRowKey(): string { return `draft-${Date.now()}`; } -export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { +export function useAplnRows({ seedKey, serverRows, elaprTmpltId = "" }: UseAplnRowsOptions) { const [rows, setRows] = useState([]); const [editingRowKey, setEditingRowKey] = useState(null); useEffect(() => { - if (!elaprTmpltId) { + if (!seedKey) { setRows([]); setEditingRowKey(null); return; } setRows((serverRows ?? []).map(toRow)); setEditingRowKey(null); - }, [elaprTmpltId, serverRows]); + }, [seedKey, serverRows]); const isEditingLocked = editingRowKey != null; const visibleRows = getVisibleRows(rows); + const canAdd = Boolean(seedKey) && !isEditingLocked; const addRow = useCallback(() => { - if (!elaprTmpltId || isEditingLocked) return; + if (!canAdd) return; const rowKey = createDraftRowKey(); @@ -66,7 +68,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { }, ]); setEditingRowKey(rowKey); - }, [elaprTmpltId, isEditingLocked]); + }, [canAdd, elaprTmpltId]); const startEdit = useCallback( (rowKey: string) => { diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts index 264f45db7..5d05fd04b 100644 --- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -45,5 +45,5 @@ export interface ElaprDmndService { update(elaprDmndId: string, body: ElaprDmndSaveRequestDto): Promise; updateStatus(elaprDmndId: string, atrzSttsComCd: string): Promise; remove(elaprDmndId: string): Promise; - findDocPrgrsByDocNo(elaprDocNo: string): Promise; + findDocPrgrsByDocNo(elaprDocNo: string, elaprCycl?: number): Promise; } diff --git a/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx index 92c3d58c5..7f7a88911 100644 --- a/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-apln-picker-grid.tsx @@ -20,6 +20,7 @@ import { useElaprAplnPage } from "../api/queries"; import { validateAplnRow } from "../lib/apln-validators"; import { useAplnRows } from "../lib/use-apln-rows"; import { useDndSensors } from "../lib/use-dnd-sensors"; +import type { ElaprApln } from "../model/elapr-apln-interface"; import type { ElaprAplnRow } from "../model/elapr-apln-row-types"; import type { ReviewerOption } from "../model/reviewer-option"; @@ -27,13 +28,17 @@ import { AprvrPickerDialog } from "./aprvr-picker-dialog"; import { SortableAplnPickerRow } from "./sortable-apln-picker-row"; interface ElaprAplnPickerGridProps { - elaprTmpltId: string | null; + elaprTmpltId?: string | null; + externalServerRows?: ElaprApln[]; + isExternalLoading?: boolean; onRowsChange?: (rows: ElaprAplnRow[]) => void; onEditingLockedChange?: (locked: boolean) => void; } function ElaprAplnPickerGrid({ - elaprTmpltId, + elaprTmpltId = null, + externalServerRows, + isExternalLoading = false, onRowsChange, onEditingLockedChange, }: ElaprAplnPickerGridProps) { @@ -41,14 +46,18 @@ function ElaprAplnPickerGrid({ const [pickingRowKey, setPickingRowKey] = useState(null); const alert = useAlertStore((state) => state.setMessage); + const isExternalMode = externalServerRows != null; const aplnQuery = useElaprAplnPage({ - elaprTmpltId: elaprTmpltId ?? undefined, + elaprTmpltId: !isExternalMode ? (elaprTmpltId ?? undefined) : undefined, page: 0, size: 200, sort: "atrzSeqNo,asc", }); + const seedKey = isExternalMode ? "external-doc-prgrs" : elaprTmpltId; + const serverRows = isExternalMode ? externalServerRows : aplnQuery.data?.content; + const { rows, visibleRows, @@ -62,8 +71,9 @@ function ElaprAplnPickerGrid({ markDeleted, reorderRows, } = useAplnRows({ - elaprTmpltId, - serverRows: aplnQuery.data?.content, + seedKey, + serverRows, + elaprTmpltId: elaprTmpltId ?? "", }); useEffect(() => { @@ -75,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({ - {aplnQuery.isPending && elaprTmpltId ? ( + {isLoading ? (
- ) : !elaprTmpltId ? ( + ) : !isReady ? (

결재서식을 선택하세요.

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

+

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

) : ( 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/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index d0b906dae..92bf5b83e 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -38,10 +38,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( - )} @@ -615,7 +638,11 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( - )} @@ -643,6 +670,13 @@ function FloatingActionButtons({ onConfirm={handleElaprTmpltConfirm} /> + setActiveDialog(null)} + onConfirm={handleElaprDocPrgrsConfirm} + /> +