[as] feat: 재원협의 서비스(fnrsCnsltnService) 수정

This commit is contained in:
2026-09-07 17:05:59 +09:00
parent dbea497cc5
commit b9ac4a1c18
8 changed files with 175 additions and 146 deletions
+15 -135
View File
@@ -17,7 +17,7 @@
> - `as-service` — `/api/v1/fnrs-cnsltn-mng`, `/api/v1/pwrk-cnsltn-mng`, `/api/v1/pwrk-cnsltn-doc-crt-rslt`, `/api/v1/dcrag-to`
> - `as-elapr-service` — `/api/v1/elapr-dmnd`, `/api/v1/elapr-prgrs-stts`, `/api/v1/elapr-apln`
>
> **현재 프론트 상태**: 재원협의(`fnrs-cnsltn`)·전자결재(`elapr`)·DC RAG TO(`dcrag-to`)는 존재한다. 실무협의(`pwrk-cnsltn-*`) 엔티티·화면은 **미구현**. 버튼 매트릭스·상태코드는 **v2.1 잔존**이며 v3.0(`FNRSSTTS`)과 불일치한다. [9절](#9-현재-코드-vs-to-be-갭) 참고.
> **현재 프론트 상태**: 재원협의(`fnrs-cnsltn`)·전자결재(`elapr`)·DC RAG TO(`dcrag-to`)는 존재한다. 실무협의(`pwrk-cnsltn-`\*) 엔티티·화면은 **미구현**. 버튼 매트릭스·상태코드는 **v2.1 잔존**이며 v3.0(`FNRSSTTS`)과 불일치한다. [9절](#9-현재-코드-vs-to-be-갭) 참고.
---
@@ -25,7 +25,7 @@
재원협의 4대 주체(기초·광역·환경청·공단)의 **상태 전이·버튼·API**를 `apps/as`에 구현할 때 쓰는 프론트엔드 가이드이다.
v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**만 다뤘다. v2.0은 2026-09-03 완결 프로세스 기준으로 **신청부터 최종종결(`TA`)까지**, 실무협의·AI 3파이프라인·결재선·보완 루프를 포함한다.
v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**만 다뤘다. v2.0은 2026-09-03 완결 프로세스 기준으로 **신청부터 최종종결(**`TA`**)까지**, 실무협의·AI 3파이프라인·결재선·보완 루프를 포함한다.
상세 표·시퀀스·DTO는 본문에 넣지 않고 레퍼런스로 분리한다. 구현 중에는 본문의 규칙만 보고, 표가 필요하면 해당 파일을 연다.
@@ -92,7 +92,7 @@ v1.0은 공단 접수자의 **자료검증 → 초안생성 → 웹기안기**
## 4. 상태코드 한눈에 보기
상위코드 **`FNRSSTTS`**. v1.0의 `PWRKSTTS`(`DV`, `AR`, `TR` 등)는 **폐기**한다.
상위코드 `FNRSSTTS`. v1.0의 `PWRKSTTS`(`DV`, `AR`, `TR` 등)는 **폐기**한다.
정방향:
@@ -122,7 +122,7 @@ SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA
| `RK` | 조치결과서검토 | 웹기안기·금액검증 재실행·보완. 결재선 2차 |
| `RR` | 조치결과서검토 회신 | 환경청 → 광역 `EP` |
| `EP` | 검토결과 회신 | 광역 → 기초 `TA` |
| `TA` | 검토결과 회신(종료) | 기초 조회만. **고시(`TB`) 아님** |
| `TA` | 검토결과 회신(종료) | 기초 조회만. **고시(**`TB`**) 아님** |
코드 의미·구버전 대조·보완 루프·AI Fallback은 [ref-process-status.md](./ref-process-status.md).
@@ -130,17 +130,19 @@ SV → ST → LE → FV → ER → AC → TE → TP → RM → RB → RC → RA
## 5. 프론트가 지켜야 할 핵심 규칙
1. **상태 SSOT는 `FNRSSTTS`**. 버튼 가시성은 `(dtlInstCd, prgrsSttsComCd, atrzSeComCd)`로만 계산한다. 공단은 결재선 구분(`ATRZ01` 접수자, `ATRZ02` 검토자, `ATRZ03` 승인자, `ATRZ99` 최종승인자)을 추가로 본다.
1. **상태 SSOT는** `FNRSSTTS`. 버튼 가시성은 `(dtlInstCd, prgrsSttsComCd, atrzSeComCd)`로만 계산한다. 공단은 결재선 구분(`ATRZ01` 접수자, `ATRZ02` 검토자, `ATRZ03` 승인자, `ATRZ99` 최종승인자)을 추가로 본다.
2. **AI 콜백 API는 화면에서 호출하지 않는다.** `POST /dcrag/documents/verify-result`, `POST /dcrag/draft/complete`, `POST /dcrag/cost/verify-result`는 서버 전용이다.
3. **초안생성 1차 트리거는 검토자지정이다.** `POST /elapr-dmnd` (`ER → AC`). 예전처럼 `POST /pwrk-cnsltn-mng`로 초안을 시작하지 않는다.
4. **기술검토 의뢰는 `LE → FV` 전용 API** `POST /fnrs-cnsltn-mng/{docNo}/request-examine`. 현재 코드의 `updatePrgrsStts(ER)` 직행은 폐기.
4. **기술검토 의뢰는** `LE → FV` **전용 API** `POST /fnrs-cnsltn-mng/{docNo}/request-examine`. 현재 코드의 `updatePrgrsStts(ER)` 직행은 폐기.
5. **수동 AI 재실행은 상태를 바꾸지 않는다.**
- `ER` 파일검증 → `POST /dcrag-to/documents/transfer`
- `TE` 초안생성 → `POST /dcrag-to/documents/draft/generate` (타임아웃·실패 시에만)
- `RK` 금액검증 → `POST /dcrag-to/documents/cost/verify`
6. **진행상태만 바꾸는 버튼은 `POST /fnrs-cnsltn-mng/doc/updatePrgrsStts`를 재사용한다.** (`ST→LE`, `RM→RB`, `RC→RA`, `RR→EP`, `EP→TA`)
- `ER` 파일검증 → `POST /dcrag-to/documents/transfer`
- `TE` 초안생성 → `POST /dcrag-to/documents/draft/generate` (타임아웃·실패 시에만)
- `RK` 금액검증 → `POST /dcrag-to/documents/cost/verify`
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으로 처리한다.
8. **결재 승인/회신은** `PUT /elapr-prgrs-stts/{id}/process` **하나**. 1차 최종승인자 → `TP`, 2차 → `RR`은 서버가 Feign으로 처리한다.
9. **2차 결재선 복사는 백엔드**. `POST /elapr-apln/copy-line`은 `AV→RK` 콜백에서 호출한다. 프론트는 `RK`에서 2차 결재선을 **조회·표시**만 한다.
10. **웹기안기는 URL을 직접 Open**. blob Object URL을 `HwpCtrl.Open`에 넘기지 않는다. `orgnlOtptDocCn`은 쓰지 않는다.
@@ -209,7 +211,7 @@ 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` |
@@ -224,7 +226,7 @@ END
| 공단 검토 | 저장 | `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` |
| 기초 | 파일보완완료 / 보완완료 | `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` |
@@ -239,125 +241,3 @@ END
신규 4개: `request-examine`, `submit-actn-result`, `request-actn-review`, (백엔드) `elapr-apln/copy-line`.
---
## 8. 프론트엔드 구현 가이드
### 8-1. 기술 스택·원칙
- App Router + FSD (`app/` → `pages/` → `widgets/` → `entities/` → `shared/`)
- React Query. ad-hoc `useQuery` + service 직접 호출 금지
- `@common/components`, `useDataTable`, `@tanstack/react-table`
- `'use client'`는 Web API(다운로드, 폴링, 웹기안기)가 있는 페이지만
- 함수명 = Controller public 메서드명 (`delete` → `remove`)
- 타입명 = 백엔드 DTO명
- Save/Apply Request = zod SSOT. Response는 `interface`
참조 슬라이스: `entities/fnrs-cnsltn/`, `entities/elapr/`, `entities/dcrag-to/`, `entities/sewer-bsc-plan/`
### 8-2. 모듈 배치
| 구분 | 경로 |
| ------------- | ------------------------------------------------------------------------------------------------- |
| ENDPOINT | `apps/as/src/shared/config/api.ts` |
| 재원협의 | `entities/fnrs-cnsltn/` — **확장** (`requestExamine`, `requestActnReview`, `findFileVrfcResults`) |
| 실무협의 관리 | `entities/pwrk-cnsltn-mng/` — **신규** |
| 생성결과 | `entities/pwrk-cnsltn-doc-crt-rslt/` — **신규** |
| DC RAG TO | `entities/dcrag-to/` — 구현됨 |
| 전자결재 | `entities/elapr/` — 구현됨. `copyLine`은 서버 전용 |
| 재원협의 화면 | `pages/fnrs-cnsltn/` — 버튼 매트릭스 전면 개편 |
| 실무협의 화면 | `pages/pwrk-cnsltn/` — 목록/상세 (선택, 공단 보조) |
파일 트리·Service 시그니처·폴링 훅은 [ref-frontend-entities.md](./ref-frontend-entities.md).
### 8-3. 버튼 매트릭스 구현
`pages/fnrs-cnsltn/lib/button-config.ts`를 v3.0 표로 교체한다.
- 키: `Record<Role, Record<FnrsStts, Action[]>>`
- 공단은 `Record<FnrsStts, Record<AtrzSe, Action[]>>` (`ER`/`TE`/`RK`)
- AI 상태(`FV`, `AC`, `AV`)는 버튼 대신 **진행 패널** (진행률·경과시간·30분 안내)
- `SL`은 기초만 수정 버튼. 복귀 종류(파일/기술/금액)는 `sttsChgHstryList` 직전 코드로 라벨만 분기
- `TA`는 조회 전용. **고시(`TB`) 액션 삭제**
### 8-4. AI 3파이프라인 UX
| 상태 | 진입 버튼 | 완료 상태 | 30분 Fallback | 수동 버튼 |
| ---- | ------------------- | --------- | ----------------------- | ----------- |
| `FV` | 기술검토 의뢰 | `ER` | `ER` + [파일검증] | 공단 접수자 |
| `AC` | 검토자지정 | `TE` | `TE` + [실무협의서생성] | 공단 검토자 |
| `AV` | 조치결과서 검토요청 | `RK` | `RK` + [금액검증] | 공단 검토자 |
폴링: 초안은 `GET /dcrag-to/documents/draft/status/{crtDmndUuid}` (5~10초). 파일검증·금액검증은 상세 `prgrsSttsComCd` refetch로도 충분하다. `COMPLETED`/`FAILED` 또는 상태 이탈 시 중단하고 상세 invalidate.
초안 `COMPLETED` 전에는 [실무협의서관리]·[다운로드] 비활성. Fallback으로 `TE`가 되었는데 초안이 없으면 [실무협의서생성]만 활성.
### 8-5. 보완 팝업 3종
같은 `reqsupplement` / `submitsupplement`를 쓰고 팝업 UI만 나눈다.
| 단계 | 팝업 | 요청 주체 | 복귀 |
| ---- | ------------------------- | ----------- | ---------------------- |
| `ER` | 보완 검토팝업(파일리스트) | 공단 접수자 | `SL→ER` [파일보완완료] |
| `TE` | 보완 검토팝업() | 공단 검토자 | `SL→TE` [보완완료] |
| `RK` | 보완 검토팝업(금액) | 공단 검토자 | `SL→RK` [보완완료] |
파일 팝업 데이터: `GET /fnrs-cnsltn-mng/{docNo}/file-vrfc-results`.
---
## 9. 현재 코드 vs To-Be 갭
`pages/fnrs-cnsltn` 기준.
| 현재 | v3.0 |
| ------------------------------------------------------ | --------------------------------------------------------------------- |
| 기술검토 의뢰 → `updatePrgrsStts("ER")` | `request-examine` → **`FV`** |
| `ROLE_USER01` 결재 상태 `ER`, `TE`만 | `RK` 2차 결재선 추가. `ER`에 파일보완 |
| `TA` [고시] → `TB` | `TA`는 종료 조회. `TB` 없음 |
| `EP` [인가승인] → `TA` | `EP` [결과회신] → `TA` (광역) |
| `TP` 환경청 [검토결과 회신] → `EP` | `TP` → **`RM`** (1차). 최종은 `RR→EP` |
| `LE` 결과회신 → `EP` (자체종결만 맞음) | 자체종결 유지. 일반 건은 의뢰 |
| 조치결과 `RB/RC/RA/AV` 버튼 없음 | 기초 `submit-actn-result`, 광역 `RC→RA`, 환경청 `request-actn-review` |
| `SL` 수정화면 미연결 | 파일/기술/금액 3버튼 |
| 초안생성을 `pwrk-cnsltn-mng.save`로 가정 (가이드 v1.0) | **검토자지정**이 1차 트리거 |
| `pwrk-cnsltn-*` 엔티티 없음 | 신규 슬라이스 |
| `dcrag-to` 구현됨 | 수동 재실행 3종에 연결 |
| `elapr-dmnd.save` 구현됨 | `docPrgrsSttsComCd: "AC"` 로 지정 |
---
## 10. 구현 순서
```
Task Progress:
- [ ] 1. FNRSSTTS 상수·표시명. button-config / floating-action-buttons 를 38행 매트릭스로 교체
- [ ] 2. fnrs-cnsltn 엔티티 확장: requestExamine, requestActnReview, findFileVrfcResults
(기존 saveApply, updatePrgrsStts, techReview, req/submit supplement 재사용)
- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, saveApply, submitActnResult)
- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (download, webhwp open URL, saveMultipart)
- [ ] 5. AI 진행 패널: FV / AC / AV (draft status 폴링 + 상세 refetch, 30분 안내)
- [ ] 6. 환경청: 기술검토 의뢰(FV), 자체종결(EP), TP→RM, RA→AV, RR→EP
- [ ] 7. 공단 접수: 파일검증 결과 배지·파일보완 팝업, 검토자지정(AC)
- [ ] 8. 공단 검토 TE/RK: 웹기안기, 수동 초안/금액검증, 보완 팝업, 결재 process
- [ ] 9. 기초: SL 보완완료 3분기, RB 조치결과 submit-actn-result, TA 조회
- [ ] 10. 광역: ST→LE, RM→RB, RC→RA, EP→TA
- [ ] 11. (선택) 실무협의 목록·상세 라우트 /pwrk-cnsltn
```
1~4는 엔티티·버튼 뼈대. 화면 전이는 5부터 역할 레인 단위로 닫는다.
---
## 11. 공통코드 (화면 분기용)
| 상위코드 | 필드 | 값 | 명칭 |
| ----------------------- | ---------------- | ----------------------------------- | ------------------------------ |
| `ASDOCSE` | `upDocSeComCd` | `FNRS` / `PWRK` | 재원협의 / 실무협의 |
| `FNRSSTTS` | `prgrsSttsComCd` | 18종 | [4절](#4-상태코드-한눈에-보기) |
| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` `I` `C` `F` | 생성요청/중/완료/실패 |
| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` / `WARN` | 파일별 검증 |
| 결재선 구분 | `atrzSeComCd` | `ATRZ01` `ATRZ02` `ATRZ03` `ATRZ99` | 접수/검토/승인/최종 |
표시명은 응답 `*Nm` / `*CdNm`. 분기는 코드값.
초안 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 `C`. 문서 상태는 `AC → TE`.
@@ -5,6 +5,7 @@ import type { PageResponse } from "@/shared/model";
import type {
DocPrgrsUpdateRequestDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
@@ -12,6 +13,10 @@ import type {
FnrsCnsltnMngService,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
import type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "../model/fnrs-cnsltn-payload-schema";
const BASE = ENDPOINT.AS_SERVICE.FNRS_CNSLTN_MNG;
@@ -51,6 +56,16 @@ async function techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Prom
await client.post<void>(`${BASE}/${id}/tech-review`, body);
}
async function requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/${docNo}/request-examine`, body);
return response.data;
}
async function requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string> {
const response = await client.post<string>(`${BASE}/${docNo}/request-actn-review`, body);
return response.data;
}
async function submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string> {
const response = await client.post<string>(`${BASE}/${id}/submitsupplement`, body);
return response.data;
@@ -68,6 +83,15 @@ async function remove(fnrsCnsltnDocNo: string): Promise<void> {
await client.delete<void>(`${BASE}/${fnrsCnsltnDocNo}`);
}
async function findFileVrfcResults(
fnrsCnsltnDocNo: string,
): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]> {
const response = await client.get<FnrsCnsltnFileVrfcRsltResponseDto[]>(
`${BASE}/${fnrsCnsltnDocNo}/file-vrfc-results`,
);
return response.data;
}
async function changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void> {
await client.post<void>(`${BASE}/doc/updatePrgrsStts`, body);
}
@@ -79,8 +103,11 @@ export const fnrsCnsltnMngService = {
saveApply,
update,
techReview,
requestExamine,
requestActnReview,
submitSupplement,
submitReqSupplement,
remove,
findFileVrfcResults,
changePrgrsSttsComCd,
} satisfies FnrsCnsltnMngService;
@@ -7,6 +7,10 @@ import type {
FnrsCnsltnMngSearchDto,
FnrsCnsltnMngUpdateRequestDto,
} from "../model/fnrs-cnsltn-interface";
import type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "../model/fnrs-cnsltn-payload-schema";
import { fnrsCnsltnMngService } from "./fnrs-cnsltn-service";
@@ -14,6 +18,7 @@ export const fnrsCnsltnKeys = {
all: ["fnrs-cnsltn"] as const,
list: () => [...fnrsCnsltnKeys.all, "list"] as const,
detail: (id: string) => [...fnrsCnsltnKeys.all, "detail", id] as const,
fileVrfcResults: (id: string) => [...fnrsCnsltnKeys.all, "fileVrfcResults", id] as const,
};
export function useFnrsCnsltnMngPage(params: FnrsCnsltnMngSearchDto) {
@@ -31,6 +36,14 @@ export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) {
});
}
export function useFnrsCnsltnFileVrfcResults(fnrsCnsltnDocNo: string | null | undefined) {
return useQuery({
queryKey: fnrsCnsltnKeys.fileVrfcResults(fnrsCnsltnDocNo!),
queryFn: () => fnrsCnsltnMngService.findFileVrfcResults(fnrsCnsltnDocNo!),
enabled: Boolean(fnrsCnsltnDocNo),
});
}
export function useSaveFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
@@ -84,6 +97,34 @@ export function useTechReviewFnrsCnsltnMng() {
});
}
export function useRequestExamineFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ docNo, body }: { docNo: string; body: FnrsExamineRequestDto }) =>
fnrsCnsltnMngService.requestExamine(docNo, body),
onSuccess: (_data, { docNo }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(docNo),
});
},
});
}
export function useRequestActnReviewFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ docNo, body }: { docNo: string; body: EnvoActnReviewReqDto }) =>
fnrsCnsltnMngService.requestActnReview(docNo, body),
onSuccess: (_data, { docNo }) => {
queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.all });
queryClient.invalidateQueries({
queryKey: fnrsCnsltnKeys.detail(docNo),
});
},
});
}
export function useSubmitSupplementFnrsCnsltnMng() {
const queryClient = useQueryClient();
return useMutation({
+12
View File
@@ -3,8 +3,11 @@ export { fnrsCnsltnKeys } from "./api/queries";
export {
useChangePrgrsSttsComCd,
useDeleteFnrsCnsltnMng,
useFnrsCnsltnFileVrfcResults,
useFnrsCnsltnMng,
useFnrsCnsltnMngPage,
useRequestActnReviewFnrsCnsltnMng,
useRequestExamineFnrsCnsltnMng,
useSaveApplyFnrsCnsltnMng,
useSaveFnrsCnsltnMng,
useSubmitReqSupplementFnrsCnsltnMng,
@@ -25,6 +28,7 @@ export type {
EsntlAtchFileItem,
EsntlAtchFileMngTreeResponseDto,
FnrsCnsltnApplySaveRequestDto,
FnrsCnsltnFileVrfcRsltResponseDto,
FnrsCnsltnMngDetResponseDto,
FnrsCnsltnMngResponseDto,
FnrsCnsltnMngSaveRequestDto,
@@ -37,3 +41,11 @@ export type {
FnrsCnsltnSplmntDmndResponseDto,
FnrsSharItem,
} from "./model/fnrs-cnsltn-interface";
export type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "./model/fnrs-cnsltn-payload-schema";
export {
envoActnReviewReqSchema,
fnrsExamineRequestSchema,
} from "./model/fnrs-cnsltn-payload-schema";
@@ -16,6 +16,11 @@ import type {
} from "@/shared/model";
import type { FnrsSharBizCstTreeDto } from "@/entities/fnrs-shar-se";
import type {
EnvoActnReviewReqDto,
FnrsExamineRequestDto,
} from "./fnrs-cnsltn-payload-schema";
export type {
AtflMngClientResponseDto,
BizCstTreeDto,
@@ -79,6 +84,8 @@ export interface FnrsCnsltnMngSearchDto extends PageableParams {
atchFileGroupId?: string;
sharAtchFileGroupId?: string;
prgrsSttsComCd?: string;
rtlztnFcltSeComCd?: string;
rtlztnUsgSeComCd?: string;
}
interface FnrsCnsltnMngResponseExtension {
@@ -86,6 +93,10 @@ interface FnrsCnsltnMngResponseExtension {
envoSeCdNm?: string;
taskSeCdNm?: string;
prgrsSttsCdNm?: string;
beforePrgrsSttsComCd?: string;
beforePrgrsSttsCdNm?: string;
rtlztnFcltSeComNm?: string;
rtlztnUsgSeComNm?: string;
regDt?: string;
rgtr?: string;
rgtrNm?: string;
@@ -163,6 +174,22 @@ export interface FnrsCnsltnApplySaveRequestDto extends PbadmsApplySaveHeader {
fnrsSharList?: FnrsSharItem[];
}
/** 백엔드 FnrsCnsltnFileVrfcRsltResponseDto */
export interface FnrsCnsltnFileVrfcRsltResponseDto {
fileSeComCd?: string;
fileSeNm?: string;
esntlAtchFileId?: string;
atchFileGroupId?: string;
atflId?: string;
orgnlFileNm?: string;
atflFlieSz?: number;
vrfcSttsComCd?: string;
vrfcSttsComNm?: string;
vrfcRsltCn?: string;
vrfcDt?: string;
regDt?: string;
}
/** Controller public 메서드명·시그니처와 동일 */
export interface FnrsCnsltnMngService {
findPage(searchDto: FnrsCnsltnMngSearchDto): Promise<PageResponse<FnrsCnsltnMngResponseDto>>;
@@ -171,8 +198,11 @@ export interface FnrsCnsltnMngService {
saveApply(body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
update(fnrsCnsltnDocNo: string, body: FnrsCnsltnMngUpdateRequestDto): Promise<string>;
techReview(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<void>;
requestExamine(docNo: string, body: FnrsExamineRequestDto): Promise<string>;
requestActnReview(docNo: string, body: EnvoActnReviewReqDto): Promise<string>;
submitSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
submitReqSupplement(id: string, body: FnrsCnsltnApplySaveRequestDto): Promise<string>;
remove(fnrsCnsltnDocNo: string): Promise<void>;
findFileVrfcResults(fnrsCnsltnDocNo: string): Promise<FnrsCnsltnFileVrfcRsltResponseDto[]>;
changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise<void>;
}
@@ -0,0 +1,15 @@
import { z } from "zod";
/** 백엔드 FnrsExamineRequestDto */
export const fnrsExamineRequestSchema = z.object({
prgrsSttsComCd: z.string().optional(),
});
/** 백엔드 EnvoActnReviewReqDto */
export const envoActnReviewReqSchema = z.object({
envoDocNo: z.string().min(1),
specialRqstCn: z.string().optional(),
});
export type FnrsExamineRequestDto = z.infer<typeof fnrsExamineRequestSchema>;
export type EnvoActnReviewReqDto = z.infer<typeof envoActnReviewReqSchema>;
@@ -25,6 +25,7 @@ import {
fnrsCnsltnKeys,
type FnrsCnsltnMngDetResponseDto,
fnrsCnsltnMngService,
type FnrsExamineRequestDto,
} from "@/entities/fnrs-cnsltn";
import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt";
import { Button } from "@common/components";
@@ -299,10 +300,8 @@ function FloatingActionButtons({
}
async function handleRqstTechRvwClick() {
const payload: DocPrgrsUpdateRequestDto = {
docNo,
docSeComCd,
prgrsSttsComCd: "ER",
const payload: FnrsExamineRequestDto = {
prgrsSttsComCd: "FV",
};
await executeAction({
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw,
@@ -602,6 +601,19 @@ async function handleDmndSplmnt(
}
}
async function handleRequestExamine(
context: ActionHandlerContext<FnrsExamineRequestDto>,
): Promise<void> {
try {
const docNo = context.data.detDto?.fnrsCnsltnDocNo;
if (!docNo) throw new Error("문서번호가 없습니다.");
await fnrsCnsltnMngService.requestExamine(docNo, context.request);
context.onSuccess?.();
} catch (error) {
context.onError?.(error as Error);
}
}
async function handleAtrzAprv(context: ActionHandlerContext): Promise<void> {
// TODO: 결재 승인 처리 API 호출
context.onSuccess?.();
@@ -621,7 +633,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
rjct: handleChangePrgrsSttsComCd,
sncnAprv: handleChangePrgrsSttsComCd,
rvwRply: handleTechReview,
rqstTechRvw: handleChangePrgrsSttsComCd,
rqstTechRvw: handleRequestExamine,
fileVrfc: () => Promise.resolve(),
dsgntRvwr: handleDsgntRvwr,
pwrkCnsltnDocCrt: () => Promise.resolve(),
@@ -456,11 +456,12 @@ function AttachmentsFormSection({
</div>
)}
<div>
<label>
<label htmlFor={`${file.esntlAtchFileId}-file-input`}>
<input
type="file"
id={`${file.esntlAtchFileId}-file-input`}
multiple
className="hidden"
className="peer sr-only"
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
onChange={(e) => {
void handleEssentialFileUpload(
@@ -470,7 +471,12 @@ function AttachmentsFormSection({
e.target.value = "";
}}
/>
<Button size="sm" variant="outline" asChild>
<Button
size="sm"
variant="outline"
className="peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2"
asChild
>
<span>파일추가</span>
</Button>
</label>
@@ -489,17 +495,23 @@ function AttachmentsFormSection({
<div className="px-2">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-xl font-semibold">기타 첨부파일</h3>
<label>
<label htmlFor="etc-file-input">
<input
type="file"
id="etc-file-input"
multiple
className="hidden"
className="peer sr-only"
onChange={(e) => {
void handleEtcFileUpload(e.target.files);
e.target.value = "";
}}
/>
<Button size="sm" variant="outline" asChild>
<Button
size="sm"
variant="outline"
className="peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2"
asChild
>
<span>파일추가</span>
</Button>
</label>