[as] feat: 재원협의 지자체 조치완료(RB) 로직 작업

This commit is contained in:
2026-09-14 13:36:23 +09:00
parent 6c4b8a52dc
commit 6eeb774a21
11 changed files with 134 additions and 42 deletions
@@ -116,7 +116,7 @@ SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA
| `TE` | 기술검토 | 웹기안기·검토완료·보완. 결재선 1차 |
| `TP` | 기술검토 회신 | 환경청 → 광역 `RM` |
| `RM` | 재원협의결과회신 | 광역 → 기초 `RB` |
| `RB` | 지자체회신 | 기초 조치결과 저장/협의회신 |
| `RB` | 지자체회신 | 기초 실무협의서 관리 / 조치완료 |
| `RC` | 조치결과서 제출 | 광역 → 환경청 `RA` |
| `RA` | 조치결과서제출(환경청) | 환경청 2차 의뢰 `AV` |
| `AV` | 금액검증 | AI 진행 표시. **30분** 후 `RK` Fallback |
@@ -167,8 +167,8 @@ BEGIN
ON [삭제] (SV) → remove
ON [파일보완완료] (SL←ER) → submitsupplement
ON [보완완료] (SL←TE|RK) → submitsupplement
ON [저장] (RB) → submit-actn-result { isSubmit: false }
ON [협의회신] (RB) → submit-actn-result { isSubmit: true }
ON [실무협의서 관리] (RB) → GET webhwp/open (blob) → HwpCtrl.Open(File)
ON [조치완료] (RB) → submit-actn-result { isSubmit: true }
ON [결과회신] (TA) → API 없음 (조회)
// ----- 광역 ROLE_USER04 -----
@@ -230,7 +230,8 @@ END
| 기초 | 파일보완완료 / 보완완료 | `SL→ER | TE | RK` | `POST /fnrs-cnsltn-mng/{docNo}/submitsupplement` |
| 환경청 | 재원협의 결과 회신 | `TP→RM` | `POST /fnrs-cnsltn-mng/{docNo}/tech-review` |
| 광역 | 지자체회신 | `RM→RB` | `updatePrgrsStts` |
| 기초 | 저장(조치) / 협의회신 | `RB` / `RB→RC` | **신규** `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` |
| 기초 | 실무협의서 관리 | `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→AV` | **신규** `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review` |
| 공단 검토 | 금액검증 재실행 | `RK` 유지 | `POST /dcrag-to/documents/cost/verify` |
+10 -13
View File
@@ -39,8 +39,8 @@
| 23 | 보완완료(조치) | `SL→RK` | as | 동일 | 기존 |
| 24 | 재원협의 결과 회신 | `TP→RM` | as | `POST .../{docNo}/tech-review` | 기존 |
| 25 | 지자체회신 | `RM→RB` | as | `POST .../doc/updatePrgrsStts` | 기존 |
| 26 | 저장(조치결과) | `RB` | as | `POST /api/v1/pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result` | **신규** |
| 27 | 협의회신 | `RB→RC` | as | 동일 (`isSubmit=true`) | **신규** |
| 26 | 실무협의서 관리 | `RB` | as | `GET .../pwrk-cnsltn-doc-crt-rslt/{uuid}/webhwp/open` | 기존 |
| 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` | 서버 |
@@ -186,16 +186,16 @@ v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**.
반려: `ATRZ_PRGRS_004`. 기존 `elaprPrgrsSttsService.process`. 최종 시 서버가 `TP`/`RR` 전이.
### 3-5. `TP → RM → RB → RC`
### 3-5. `TP → RM → RB → RA`
**환경청 1차 회신** — `tech-review` (`TP→RM`).
**광역 지자체회신** — `updatePrgrsStts` `RB`.
**조치결과 저장/제출 (신규)** — `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result`
**조치완료 (신규)** — `POST /pwrk-cnsltn-mng/{pwrkDocNo}/submit-actn-result`
```json
{
"isSubmit": false,
"isSubmit": true,
"actnList": [
{
"rvwOpnnSn": 1,
@@ -203,19 +203,16 @@ v1.0 가이드의 “이 API를 프론트가 치면 안 된다”는 **폐기**.
"atchFileGroupId": "optional"
}
],
"totActnSummary": "제출 시 필수",
"muniDocNo": "제출 시 필수"
"totActnSummary": "제출 시 권장",
"muniDocNo": "제출 시 권장"
}
```
- `isSubmit: false` → `RB` 유지
- `isSubmit: true` → `RB→RC`
- `isSubmit: false` → `RB` 유지 (API만 존재, **화면 버튼 없음**. HWP·첨부는 실무협의서 관리 다이얼로그에서 저장)
- `isSubmit: true` → `RB→RA` (조치완료)
응답: `string` (pwrkDocNo). DTO명 `PwrkActnResultSubmitDto`.
### 3-6. `RC → RA → AV → RK`
**광역 제출** — `updatePrgrsStts` `RA`.
### 3-6. `RA → AV → RK`
**2차 의뢰 (신규)** — `POST /fnrs-cnsltn-mng/{docNo}/request-actn-review`
```json
@@ -168,13 +168,14 @@ requestCostVerification(body: DcRagCostVerifyRequestDto): Promise<DcRagCommonRes
| 필드 | 필수 | 비고 |
| --- | --- | --- |
| `isSubmit` | Y | `false` 저장, `true` 협의회신 |
| `actnList` | Y | |
| `actnList[].rvwOpnnSn` | Y | |
| `actnList[].actnRsltCn` | Y | |
| `isSubmit` | Y | `false` API만(화면 버튼 없음), `true` 조치완료(`RB→RA`) |
| `actnList` | N | 조치완료 시 선택 |
| `actnList[].rvwOpnnSn` | Y (항목 있을 때) | |
| `actnList[].actnRsltCn` | N | |
| `actnList[].atchFileGroupId` | N | |
| `totActnSummary` | 제출 시 | |
| `muniDocNo` | 제출 시 | |
| `totActnSummary` | N | 제출 시 권장 |
| `muniDocNo` | N | 제출 시 권장 |
| `pwrkAtchFileGroupId` | N | |
### 4-3. `PwrkCnsltnMng*`
@@ -16,7 +16,7 @@
2-Pass:
- **Pass 1** 기술 타당성: `SV→ST→LE→FV→ER→AC→TE→TP→RM→RB`
- **Pass 2** 조치결과·금액: `RB→RC→RA→AV→RK→RR→EP→TA`
- **Pass 2** 조치결과·금액: `RB→RA→AV→RK→RR→EP→TA`
자체종결(기술검토 생략): `LE→EP→TA`
@@ -74,7 +74,7 @@ sequenceDiagram
rect rgb(250, 240, 255)
환경청->>광역: [재원협의 결과 회신] TP → RM
광역->>기초: [지자체회신] RM → RB
기초->>광역: [협의회신] RB → RC
기초->>환경청: [조치완료] RB → RA
end
rect rgb(240, 255, 255)
@@ -123,8 +123,8 @@ sequenceDiagram
| 7 | 1차 기술검토 | 공단 결재선 | `TE` / `SL` | 웹기안기, HWP 파싱은 서버 |
| 8 | 기술검토 회신 | 공단 최종 | `TE→TP` | `process` 최종 |
| 9 | 검토결과 회신 | 환경청·광역 | `TP→RM→RB` | 1차 회신. `EP`와 혼동 금지 |
| 10 | 조치결과 작성 | 기초 | `RB→RC` | `submit-actn-result` |
| 11 | 조치 제출·의뢰 | 광역·환경청 | `RC→RA→AV` | `request-actn-review` |
| 10 | 조치결과 작성 | 기초 | `RB→RA` | `submit-actn-result` `isSubmit:true` (조치완료) |
| 11 | 조치 의뢰 | 환경청 | `RA→AV` | `request-actn-review` |
| 12 | AI 금액검증 | AI·공단 | `AV→RK` | 결재선 복사(서버) |
| 13 | 2차 검토·결재 | 공단 | `RK→RR` / `SL` | 2차 결재선 |
| 14 | 최종 회신 | 환경청·광역·기초 | `RR→EP→TA` | 종결 |
@@ -149,7 +149,7 @@ sequenceDiagram
| `RM` | 재원협의결과회신 | Result Message to Province | 환경청→광역 | `TP→RM` |
| `RB` | 재원협의결과회신(지자체) | Result Back to Municipality | 광역→기초 | `RM→RB` |
| `RC` | 조치결과서 제출 | Result Complete | 기초→광역 | `RB→RC` |
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 광역→환경청 | `RC→RA` |
| `RA` | 조치결과서 제출(환경청) | Result Ask to Basin | 기초→환경청 | `RB→RA` |
| `AV` | 금액검증 | Amount Verification | AI | `RA→AV` |
| `RK` | 조치결과서검토 | Result Check | 공단 2차 결재 | `AV→RK`, `SL→RK` |
| `RR` | 조치결과서검토 회신 | Result Return to Basin | 공단→환경청 | `RK→RR` |
@@ -11,9 +11,9 @@
| 화면 | 소유 | 라우트 (현행) | 비고 |
| --- | --- | --- | --- |
| 작성화면 | 기초 `ROLE_USER05` | `/fnrs-cnsltn/new`, `/fnrs-cnsltn/{id}/edit` | `SV`, `RB` |
| 작성화면 | 기초 `ROLE_USER05` | `/fnrs-cnsltn/new`, `/fnrs-cnsltn/{id}/edit` | `SV` |
| 수정화면 | 기초 | `/fnrs-cnsltn/{id}/edit` | `SL` 보완 |
| 상세화면 | 광역·환경청·공단·기초(TA) | `/fnrs-cnsltn/{id}` | 조회 + 액션 버튼 |
| 상세화면 | 광역·환경청·공단·기초(`RB`/`TA`) | `/fnrs-cnsltn/{id}` | 조회 + 액션 버튼 |
| 자동화 | AI | 버튼 없음 | `FV`/`AC`/`AV` 진행 패널 |
공단 상세는 결재선 구분으로 같은 상태라도 버튼이 다르다.
@@ -57,8 +57,8 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| 22 | 수정 | USER05 | - | `SL` | 보완완료 | `RK` | 공단 검토 | `submitsupplement` (직전=`RK`) |
| 23 | 상세 | USER03 | - | `TP` | 재원협의 결과 회신 | `RM` | 광역 | `tech-review` |
| 24 | 상세 | USER04 | - | `RM` | 지자체회신 | `RB` | 기초 | `updatePrgrsStts` |
| 25 | 작성 | USER05 | - | `RB` | 저장 | `RB` | 본인 | **`submit-actn-result` `isSubmit:false`** |
| 26 | 작성 | USER05 | - | `RB` | 협의회신 | `RC` | 광역 | **`submit-actn-result` `isSubmit:true`** |
| 25 | 상세 | USER05 | - | `RB` | 실무협의서 관리 | 유지 | — | 웹기안기 Open |
| 26 | 상세 | USER05 | - | `RB` | 조치완료 | `RA` | 환경청 | **`submit-actn-result` `isSubmit:true`** |
| 27 | 상세 | USER04 | - | `RC` | 조치결과서제출 | `RA` | 환경청 | `updatePrgrsStts` |
| 28 | 상세 | USER03 | - | `RA` | 조치결과서 검토요청 | `AV` | 공단 검토 | **`request-actn-review`** |
| 29 | 자동화 | AI | - | `AV` | (금액검증) | `RK` | 공단 검토 | FE 호출 없음. 진행 패널 |
@@ -85,11 +85,10 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
| `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 |
| `fileVrfc` | 파일검증 | 유지. `ER` 재실행 |
| `dsgntRvwr` | 검토자지정 | 유지. 전이를 `AC`로 |
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK` |
| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK`·`RB` |
| `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 |
| `rvwRsltInq` | 검토결과조회 | 신규 |
| `actnStrg` | 저장(조치결과) | `RB` |
| `cnsltnRply` | 협의회신 | `RB→RC` |
| `actnCmptn` | 조치완료 | `RB→RA`. `isSubmit:true` |
| `actnSbmsn` | 조치결과서제출 | 광역 `RC→RA` |
| `rqstActnRvw` | 조치결과서 검토요청 | 환경청 `RA→AV` |
| `costVrfc` | 금액검증 | `RK` |
@@ -107,7 +106,7 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일.
ROLE_USER05
"" | SV → 저장, 신청완료, 삭제
SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료
RB → 저장(조치), 협의회신
RB → 실무협의서 관리, 조치완료
TA → 결과회신 (조회)
ROLE_USER04
@@ -5,6 +5,10 @@ import type {
PwrkCnsltnMngDetResponseDto,
PwrkCnsltnMngService,
} from "../model/pwrk-cnsltn-mng-interface";
import {
type PwrkActnResultSubmitDto,
pwrkActnResultSubmitSchema,
} from "../model/pwrk-cnsltn-mng-payload-schema";
const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_MNG;
@@ -18,7 +22,20 @@ async function findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseD
return response.data;
}
async function submitActnResult(
pwrkCnsltnDocNo: string,
body: PwrkActnResultSubmitDto,
): Promise<string> {
const payload = pwrkActnResultSubmitSchema.parse(body);
const response = await client.post<string>(
`${BASE}/${pwrkCnsltnDocNo}/submit-actn-result`,
payload,
);
return response.data;
}
export const pwrkCnsltnMngService = {
findById,
findByUpDocNo,
submitActnResult,
} satisfies PwrkCnsltnMngService;
@@ -1,9 +1,5 @@
export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service";
export {
pwrkCnsltnMngKeys,
usePwrkCnsltnMng,
usePwrkCnsltnMngByUpDocNo,
} from "./api/queries";
export { pwrkCnsltnMngKeys, usePwrkCnsltnMng, usePwrkCnsltnMngByUpDocNo } from "./api/queries";
export { findCompletedPwrkDraft } from "./lib/find-completed-draft";
export type {
PwrkCnsltnCnsltnResponseDto,
@@ -13,3 +9,7 @@ export type {
PwrkCnsltnMngService,
PwrkCnsltnPjtcoAjmtResponseDto,
} from "./model/pwrk-cnsltn-mng-interface";
export {
type PwrkActnResultSubmitDto,
pwrkActnResultSubmitSchema,
} from "./model/pwrk-cnsltn-mng-payload-schema";
@@ -1,5 +1,7 @@
import type { AtflMngClientResponseDto } from "@/shared/model";
import type { PwrkActnResultSubmitDto } from "./pwrk-cnsltn-mng-payload-schema";
export interface PwrkCnsltnDocCrtRsltResponseDto {
crtDmndUuid: string;
verSn: number;
@@ -88,4 +90,8 @@ export interface PwrkCnsltnMngDetResponseDto {
export interface PwrkCnsltnMngService {
findById(pwrkCnsltnDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
findByUpDocNo(upDocNo: string): Promise<PwrkCnsltnMngDetResponseDto>;
submitActnResult(
pwrkCnsltnDocNo: string,
body: PwrkActnResultSubmitDto,
): Promise<string>;
}
@@ -0,0 +1,20 @@
import { z } from "zod";
/** 백엔드 PwrkActnResultSubmitDto.ActnItemDto */
export const pwrkActnResultActnItemSchema = z.object({
rvwOpnnSn: z.number(),
actnRsltCn: z.string().optional(),
rfltYn: z.string().optional(),
atchFileGroupId: z.string().optional(),
});
/** 백엔드 PwrkActnResultSubmitDto */
export const pwrkActnResultSubmitSchema = z.object({
isSubmit: z.boolean(),
actnList: z.array(pwrkActnResultActnItemSchema).optional(),
totActnSummary: z.string().optional(),
muniDocNo: z.string().optional(),
pwrkAtchFileGroupId: z.string().optional(),
});
export type PwrkActnResultSubmitDto = z.infer<typeof pwrkActnResultSubmitSchema>;
@@ -19,6 +19,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
dmndSplmnt: "dmndSplmnt",
atrzAprv: "atrzAprv",
atrzRply: "atrzRply",
actnCmptn: "actnCmptn",
} as const;
export type FnrsCnsltnViewButtonAction =
@@ -36,6 +37,10 @@ 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,
],
TA: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fnrsCnsltnAncmnt],
},
ROLE_USER04: {
@@ -30,7 +30,9 @@ import {
} from "@/entities/fnrs-cnsltn";
import {
findCompletedPwrkDraft,
type PwrkActnResultSubmitDto,
type PwrkCnsltnCnsltnResponseDto,
pwrkCnsltnMngService,
usePwrkCnsltnMngByUpDocNo,
} from "@/entities/pwrk-cnsltn-mng";
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
@@ -109,13 +111,16 @@ function FloatingActionButtons({
const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd);
const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE";
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);
const { data: pwrkCnsltnData, isLoading: isPwrkCnsltnLoading } = usePwrkCnsltnMngByUpDocNo(
docNo,
needsPwrkDraftCheck,
needsPwrkLookup,
);
const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData));
const pwrkCnsltnDocNo = pwrkCnsltnData?.pwrkCnsltnDocNo;
const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest(
elaprDocPrgrsList,
@@ -465,6 +470,24 @@ function FloatingActionButtons({
});
}
async function handleActnCmptnClick() {
if (!pwrkCnsltnDocNo) {
alert("실무협의 문서번호를 찾을 수 없습니다.");
return;
}
if (!(await confirm("조치완료 하시겠습니까?"))) return;
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn,
request: {
pwrkCnsltnDocNo,
body: { isSubmit: true } satisfies PwrkActnResultSubmitDto,
},
successMessage: "조치완료되었습니다.",
});
}
return (
<>
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
@@ -553,6 +576,11 @@ function FloatingActionButtons({
실무협의서 관리
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn) && (
<Button color="green" onClick={handleActnCmptnClick} disabled={isProcessing}>
조치완료
</Button>
)}
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && (
<Button
color="sky"
@@ -733,6 +761,23 @@ async function handleRequestExamine(
}
}
async function handleActnCmptn(
context: ActionHandlerContext<{
pwrkCnsltnDocNo: string;
body: PwrkActnResultSubmitDto;
}>,
): Promise<void> {
try {
await pwrkCnsltnMngService.submitActnResult(
context.request.pwrkCnsltnDocNo,
context.request.body,
);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
// TODO: 결재 승인 처리 API 호출
context.onSuccess?.();
@@ -764,6 +809,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
dmndSplmnt: handleDmndSplmnt,
atrzAprv: handleAtrzAprv,
atrzRply: handleAtrzRply,
actnCmptn: handleActnCmptn,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any