diff --git a/apps/as/docs/pwrk-cnsltn/개발가이드.md b/apps/as/docs/pwrk-cnsltn/개발가이드.md new file mode 100644 index 000000000..bedc4ef3b --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/개발가이드.md @@ -0,0 +1,800 @@ +# 실무협의문서 개발 가이드 + +> **대상 앱**: `frontend/apps/as` +> **작성일**: 2026-08-27 +> **버전**: 1.0 +> **SoT (백엔드)**: +> +> - `PwrkCnsltnDocCrtRsltController` — `/api/v1/pwrk-cnsltn-doc-crt-rslt` +> - `PwrkCnsltnMngController` — `/api/v1/pwrk-cnsltn-mng` +> - `DcRagApiToController` — `/api/v1/dcrag-to` +> - 초안생성 데이터 설명 (`02.03.초안생성_데이터_설명.md`) +> +> **현재 프론트 상태**: `pwrk-cnsltn-`\* 엔티티·화면 **미구현**. 재원협의(`fnrs-cnsltn`)만 존재한다. + +--- + +## 1. 문서 목적 + +재원협의 접수자가 **자료 검증 → 실무협의서 초안 생성 → 한글 웹기안기 열람/다운로드 → 편집본 버전 저장**까지 구현할 때 사용하는 프론트엔드 개발 가이드이다. + +백엔드 컨트롤러·DTO를 **API source of truth**로 두고, 초안생성 데이터 설명의 업무 시퀀스를 화면 동작에 매핑한다. + +--- + +## 2. 도메인 개요 + +실무협의서(PWRK)는 재원협의서(FNRS)의 **하위 문서**이다. 문서번호 규칙: + +| 문서 | 번호 예시 | 필드 | +| -------- | ----------------- | ----------------------------- | +| 재원협의 | `F-20260824-0001` | `fnrsCnsltnDocNo` / `upDocNo` | +| 실무협의 | `P-20260827-0001` | `pwrkCnsltnDocNo` | + +재원협의 1건당 실무협의서는 **1건만** 생성된다. `POST /pwrk-cnsltn-mng`는 `upDocNo`로 기존 문서를 재사용하고, 없으면 문서번호를 발급한다. + +### 2-1. 핵심 식별자 + +| 필드 | 의미 | 비고 | +| ----------------- | ------------------------------ | --------------------------------------------------------- | +| `pwrkCnsltnDocNo` | 실무협의문서번호 | 문서 PK. `P-YYYYMMDD-NNNN` | +| `upDocNo` | 상위문서번호(재원협의) | `upDocSeComCd = "FNRS"` | +| `crtDmndUuid` | 생성요청 UUID | 초안 생성 작업·결과·다운로드의 키 | +| `verSn` | 버전일련번호 | 복합 PK `(crtDmndUuid, verSn)`. 저장 시 서버가 `+1` | +| `orgnlOtptDocCn` | 원본출력문서내용(HWP 바이너리) | **목록/상세 응답에서는 항상** `null`. 다운로드 API만 제공 | + +### 2-2. IIPBS 약어 + +| 한글 | 약어 | 필드 예 | +| -------- | ------ | ----------------------------- | +| 실무 | PWRK | `pwrkCnsltnDocNo` | +| 협의 | CNSLTN | `pwrk-cnsltn-mng` | +| 관리 | MNG | `PwrkCnsltnMng` | +| 문서 | DOC | `pwrkCnsltnDocNo` | +| 생성 | CRT | `crtDmndUuid`, `crtSttsComCd` | +| 요청 | DMND | `crtDmndUuid` | +| 결과 | RSLT | `crtRslt` | +| 버전 | VER | `verSn` | +| 일련번호 | SN | `verSn` | +| 원본 | ORGNL | `orgnlOtptDocCn` | +| 출력 | OTPT | `orgnlOtptDocCn` | +| 내용 | CN | `orgnlOtptDocCn` | +| 완료 | CMPTN | `crtCmptnDt` | +| 일시 | DT | `crtCmptnDt` | +| 검증 | VRFC | `vrfcSttsComCd` | +| 전송 | TRSM | `trsmFileList` | +| 보완 | SPLMNT | `splmntDmndList` | +| 요청자 | RQSTR | `rqstrUuid`, `rqstrNm` | + +--- + +## 3. 전체 업무 흐름 + +재원협의 **접수자**가 실무협의서 초안을 만드는 경로이다. 프론트는 DC RAG를 직접 호출하지 않고 **as-service**만 호출한다. + +```mermaid +sequenceDiagram + autonumber + participant User as 재원협의 접수자 + participant FE as AS Frontend + participant AS as as-service + participant RAG as DC RAG + + User->>FE: [자료 검증 요청] + FE->>AS: POST /dcrag-to/documents/transfer
{ fnrsCnsltnDocNo } + AS->>RAG: 자료전송 (IF-01) + AS-->>FE: { resultCode: SUCCESS, requestId } + Note over FE: 재원협의 상세 재조회 + AS-->>FE: esntlFileList / etcFileList 에
vrfcDt, vrfcRsltCn, vrfcSttsComCd + + User->>FE: [초안 생성] + FE->>AS: POST /pwrk-cnsltn-mng
{ upDocNo, upDocSeComCd: FNRS, prgrsSttsComCd: DV } + alt 실무협의서 번호 없음 + AS->>AS: P-번호 발급 + 문서 등록 + else 이미 존재 + AS->>AS: 기존 pwrkCnsltnDocNo 재사용 + end + AS->>RAG: 초안생성 비동기 요청 (IF-03-1) + AS-->>FE: 201 { pwrkCnsltnDocNo, crtDmndUuid, crtSttsComCd: R, estimatedMinutes: 10 } + + loop 약 10분, status !== COMPLETED/FAILED + FE->>AS: GET /dcrag-to/documents/draft/status/{crtDmndUuid} + AS-->>FE: { status, events[], progressPercent } + end + + Note over FE: events 에 COMPLETED 가 있으면
웹기안기 열기 / 다운로드 가능 + + User->>FE: [문서 열기] 또는 [다운로드] + FE->>AS: GET /pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open
또는 /download + + User->>FE: 웹기안기 편집 후 [저장] + FE->>AS: POST /pwrk-cnsltn-doc-crt-rslt (multipart)
crtDmndUuid + file + AS-->>FE: 201 { verSn: N+1, ... } +``` + +### 3-1. 프론트가 호출하면 안 되는 API + +| API | 이유 | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `POST /dcrag-to/documents/draft/generate` | 초안 생성은 `POST /pwrk-cnsltn-mng`가 문서번호 발급 + DC RAG 호출을 **한 트랜잭션**으로 처리한다. 프론트는 관리 API만 호출한다. | + +--- + +## 4. 화면 데이터 흐름 + +초안 생성은 **재원협의 상세**에서 시작하고, 실무협의 목록/상세는 별도 화면이다. + +``` +BEGIN + + // ===== 재원협의 상세 (기존 fnrs-cnsltn view) ===== + CALL findById(fnrsCnsltnDocNo) + DISPLAY 재원협의 상세 + 필수/기타 첨부 + + ON [자료 검증 요청] 버튼 CLICK DO + CALL requestDocumentTransfer({ fnrsCnsltnDocNo }) + CALL findById(fnrsCnsltnDocNo) // 검증결과 필드 반영 + DISPLAY 첨부 리스트 (vrfcSttsComCd: PASS | FAIL) + END ON + + ON [초안 생성] 버튼 CLICK DO + CALL pwrkCnsltnMngService.save({ + upDocNo: fnrsCnsltnDocNo, + upDocSeComCd: "FNRS", + prgrsSttsComCd: "DV" + }) + // → pwrkCnsltnDocNo, crtDmndUuid, estimatedMinutes + START POLL getDraftGenerationStatus(crtDmndUuid) + DISPLAY 초안 생성 진행률 (약 10분) + END ON + + ON POLL status = COMPLETED 또는 events.eventType 에 COMPLETED DO + ENABLE [웹기안기 열기] [다운로드] + END ON + + ON [웹기안기 열기] 버튼 CLICK DO + CALL HwpCtrl.Open( webhwpOpenUrl(crtDmndUuid) ) // TBD: 컨트롤러 미구현 + END ON + + ON [다운로드] 버튼 CLICK DO + CALL downloadLatest(crtDmndUuid) // blob + Content-Disposition + SAVE 로컬 파일 + END ON + + // ===== 실무협의 목록 ===== + CALL pwrkCnsltnMngService.findPage(searchDto + pageable) + DISPLAY 실무협의 문서 그리드 + + ON 행 CLICK DO + NAVIGATE 실무협의 상세 (pwrkCnsltnDocNo) + END ON + + // ===== 실무협의 상세 / 재원협의에서 연계 조회 ===== + CALL findById(pwrkCnsltnDocNo) + // 또는 재원협의 번호로: findByUpDocNo(fnrsCnsltnDocNo) + DISPLAY 마스터 + cnsltnList + pjtcoAjmtList + crtDmndList.crtRslt + // crtRslt.orgnlOtptDocCn 은 항상 null — 바이너리는 download API + + ON 버전 이력 조회 DO + CALL pwrkCnsltnDocCrtRsltService.findPage({ crtDmndUuid, sort: "verSn,desc" }) + DISPLAY 버전 목록 + END ON + + ON 웹기안기 편집 완료 [저장] DO + CALL saveMultipart({ crtDmndUuid, file }) // 서버가 verSn + 1 + INVALIDATE 상세·버전 목록 + END ON + +END +``` + +--- + +## 5. API 명세 + +### 5-1. 공통 + +| 항목 | 값 | +| ----------- | ----------------------------------------------------------------- | +| Gateway | `/as-service` | +| Base prefix | `ENDPOINT.AS_SERVICE.*` (`apps/as/src/shared/config/api.ts`) | +| Auth 헤더 | `Authorization`, `X-Site-Id`, `token-id` (기존 `client` 인터셉터) | +| 날짜 포맷 | `yyyy-MM-dd HH:mm:ss` | + +추가할 ENDPOINT: + +```ts +AS_SERVICE: { + // 기존 … + PWRK_CNSLTN_MNG: "/as-service/api/v1/pwrk-cnsltn-mng", + PWRK_CNSLTN_DOC_CRT_RSLT: "/as-service/api/v1/pwrk-cnsltn-doc-crt-rslt", + DCRAG_TO: "/as-service/api/v1/dcrag-to", +} +``` + +--- + +### 5-2. DC RAG 연계 (`DcRagApiToController`) + +프론트 엔티티 슬라이스: `entities/dcrag-to/` + +| 기능 | Controller | Method | URL | Request | Response | +| ----------------- | -------------------------- | ------ | ------------------------------------------------ | --------------------------------- | ------------------------------------- | +| 자료전송요청 | `requestDocumentTransfer` | POST | `/dcrag-to/documents/transfer` | `DcRagDocumentTransferRequestDto` | `DcRagCommonResponseDto` | +| 초안생성 진행상태 | `getDraftGenerationStatus` | GET | `/dcrag-to/documents/draft/status/{crtDmndUuid}` | path | `DcRagDraftStatusResponseDto` | +| 금액검증요청 | `requestCostVerification` | POST | `/dcrag-to/documents/cost/verify` | `DcRagCostVerifyRequestDto` | `DcRagCommonResponseDto` (202) | +| 초안생성요청 | `requestDraftGeneration` | POST | `/dcrag-to/documents/draft/generate` | `DcRagDraftGenerateRequestDto` | `DcRagDraftGenerateResponseDto` (202) | + +**화면에서 쓰는 것**: 자료전송 + 상태 폴링 (+ 추후 금액검증). +초안생성요청은 5-3의 `pwrkCnsltnMngService.save`로 대체한다. + +#### 자료전송 Request / Response + +```json +POST /as-service/api/v1/dcrag-to/documents/transfer +{ "fnrsCnsltnDocNo": "F-20260824-0001" } +``` + +```json +{ + "resultCode": "SUCCESS", + "message": "자료전송요청 접수 완료", + "requestId": "F-20260824-0001" +} +``` + +자료 검증이 **완료된 뒤** 재원협의 상세를 다시 조회하면, 필수첨부(`esntlFileList`)·기타첨부(`etcFileList`) 각 파일에 검증 결과가 붙는다. + +| 필드 | 예 | 의미 | +| --------------- | ----------------------------- | ------------ | +| `vrfcDt` | `2026-08-25 18:13:49` | 검증일시 | +| `vrfcRsltCn` | `정상 확인 (재원협의 신청서)` | 검증결과내용 | +| `vrfcSttsComCd` | `PASS` / `FAIL` | 검증상태 | + +프론트 `FnrsCnsltnMngDetResponseDto` / `AtflMngClientResponseDto` / `EsntlAtchFileMngTreeResponseDto`에 위 3필드가 **아직 없다**. 재원협의 상세 첨부 영역에 배지를 그리려면 타입을 확장해야 한다. + +#### 초안생성 상태 폴링 + +```http +GET /as-service/api/v1/dcrag-to/documents/draft/status/{crtDmndUuid} +``` + +| 필드 | 설명 | +| ----------------------- | --------------------------------------------------------------------------------- | +| `status` | `REQUESTED` / `DOWNLOADING` / `ANALYZING` / `GENERATING` / `COMPLETED` / `FAILED` | +| `statusName` | 표시명 (예: `생성 완료`) | +| `progressPercent` | 0~100 | +| `events[]` | `{ eventType, eventMessage, occurredDt }` | +| `startedDt` | 시작일시 | +| `estimatedCompletionDt` | 예상 완료일시 | +| `errorMessage` | 실패 시 메시지 | + +폴링 규칙: + +- 주기: **5~10초** (TBD, UX 확정). +- 예상 소요: **약 10분** (`estimatedMinutes`). +- 종료 조건: `status === "COMPLETED"` **또는** `events`에 `eventType === "COMPLETED"`. +- 실패: `status === "FAILED"` → 폴링 중단, `errorMessage` 표시. +- 완료 전에는 웹기안기 열기·다운로드 버튼을 **비활성**. + +이벤트 순서 예시: + +| eventType | eventMessage | +| ------------- | --------------------------- | +| `REQUESTED` | 요청을 접수했습니다. | +| `DOWNLOADING` | 신청서를 수신하는 중입니다. | +| `ANALYZING` | 자료 준비가 완료되었습니다. | +| `GENERATING` | 문서 저장이 완료되었습니다. | +| `COMPLETED` | 문서 생성이 완료되었습니다. | + +--- + +### 5-3. 실무협의관리 (`PwrkCnsltnMngController`) + +프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-mng/` + +| 기능 | Controller | Method | URL | Request | Response | +| ------------------- | ---------------------- | ------ | ----------------------------------------------------- | ----------------------------------- | ------------------------------------ | +| 페이징 조회 | `findPage` | GET | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSearchDto` + Pageable | `Page` | +| 상세 (문서번호) | `findById` | GET | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | `PwrkCnsltnMngDetResponseDto` | +| 상세 (재원협의번호) | `findByUpDocNo` | GET | `/pwrk-cnsltn-mng/updocno/{upDocNo}` | path | `PwrkCnsltnMngDetResponseDto` | +| 등록 + 초안생성 | `save` | POST | `/pwrk-cnsltn-mng` | `PwrkCnsltnMngSaveRequestDto` | `PwrkCnsltnMngSaveResponseDto` (201) | +| 신청 통합 저장 | `saveApply` | POST | `/pwrk-cnsltn-mng/save-apply` | `PwrkCnsltnApplySaveRequestDto` | `string` (문서번호, 201) | +| 수정 | `update` | PUT | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | `PwrkCnsltnMngUpdateRequestDto` | `string` | +| 삭제 | `delete` → `remove` | DELETE | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}` | path | 204 | +| 보완완료 제출 | `submitSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/submitsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` | +| 보완요청 등록 | `submitReqSupplement` | POST | `/pwrk-cnsltn-mng/{pwrkCnsltnDocNo}/reqsupplement` | `PwrkCnsltnApplySaveRequestDto` | `string` | +| 진행상태 수정 | `changePrgrsSttsComCd` | POST | `/pwrk-cnsltn-mng/doc/updatePrgrsStts` | `DocPrgrsUpdateRequestDto` | 200 | + +목록 기본 호출: + +```http +GET /as-service/api/v1/pwrk-cnsltn-mng?page=0&size=10&sort=pwrkCnsltnDocNo,desc +``` + +#### 초안 생성 Request / Response (`save`) + +```json +POST /as-service/api/v1/pwrk-cnsltn-mng +{ + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV" +} +``` + +```json +{ + "pwrkCnsltnDocNo": "P-20260826-0001", + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV", + "resultCode": "ACCEPTED", + "message": "초안 생성 작업이 접수되었습니다.", + "crtDmndUuid": "382f2821-9231-42e6-8309-4a1718a2ead8", + "crtSttsComCd": "R", + "requestId": "382f2821-9231-42e6-8309-4a1718a2ead8", + "estimatedMinutes": 10 +} +``` + +백엔드 동작 (`PwrkCnsltnMngService.save`): + +1. `upDocNo`로 기존 실무협의서가 있으면 **그 번호를 재사용** (재원협의 1 : 실무협의 1). +2. 없으면 `docNoMngService.issueDocNo(PWRK)`로 `P-` 번호 발급 후 insert. +3. DC RAG 초안생성을 비동기로 호출하고 `crtDmndUuid`를 응답에 실어 반환. +4. DC RAG 실패 시에도 **문서는 롤백하지 않고** 로그만 남긴다. 프론트는 `resultCode`가 `ACCEPTED`/`SUCCESS`가 아니면 토스트로 실패를 알린다. + +#### 상세 응답 구조 (`PwrkCnsltnMngDetResponseDto`) + +재원협의 상세에서 `GET .../updocno/{fnrsCnsltnDocNo}`로 연계 데이터를 한 번에 가져온다. + +```json +{ + "pwrkCnsltnDocNo": "P-20260827-0001", + "upDocNo": "F-20260824-0001", + "upDocSeComCd": "FNRS", + "prgrsSttsComCd": "DV", + "cnsltnList": [], + "pjtcoAjmtList": [], + "crtDmndList": [ + { + "crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a", + "pwrkCnsltnDocNo": "P-20260827-0001", + "rqstrUuid": "65a00f65-8460-49af-98ec-042977e56f4b", + "rqstrNm": "관리자", + "crtSttsComCd": "C", + "crtRslt": { + "crtDmndUuid": "d0545bef-735f-4d7b-a781-88e96e03577a", + "verSn": 1, + "rgtrUuid": "...", + "rgtrNm": "관리자", + "orgnlOtptDocCn": null, + "crtCmptnDt": "2026-08-27 10:47:01" + } + } + ], + "splmntDmndList": [], + "trsmFileList": [] +} +``` + +`crtRslt`는 최신 버전 메타만 포함한다. `orgnlOtptDocCn`은 절대 쓰지 말고 download API를 호출한다. + +마스터에 재원협의 사업정보가 조인된다: `bizNm`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `aplcntNm` 등. + +--- + +### 5-4. 문서생성결과 (`PwrkCnsltnDocCrtRsltController`) + +프론트 엔티티 슬라이스: `entities/pwrk-cnsltn-doc-crt-rslt/` +복합 PK: `crtDmndUuid` + `verSn` + +| 기능 | Controller | Method | URL | Request | Response | +| ------------------ | ------------------------- | ------ | ---------------------------------- | ----------------------------------------------- | --------------------------------------- | +| 버전 이력 페이징 | `findPage` | GET | `/pwrk-cnsltn-doc-crt-rslt` | `PwrkCnsltnDocCrtRsltSearchDto` + Pageable | `Page` | +| 특정 버전 상세 | `findById` | GET | `/{crtDmndUuid}/{verSn}` | path | `PwrkCnsltnDocCrtRsltResponseDto` | +| 최신 버전 상세 | `findLatestByCrtDmndUuid` | GET | `/{crtDmndUuid}/latest` | path | `PwrkCnsltnDocCrtRsltResponseDto` | +| 특정 버전 다운로드 | `download` | GET | `/{crtDmndUuid}/{verSn}/download` | path | `byte[]` (attachment) | +| 최신 버전 다운로드 | `downloadLatest` | GET | `/{crtDmndUuid}/download` | path | `byte[]` (attachment) | +| 등록 (multipart) | `saveMultipart` | POST | `/` `multipart/form-data` | `PwrkCnsltnDocCrtRsltSaveRequestDto` + `file` | `PwrkCnsltnDocCrtRsltResponseDto` (201) | +| 등록 (JSON) | `saveJson` | POST | `/` `application/json` | `PwrkCnsltnDocCrtRsltSaveRequestDto` | 201 | +| 수정 (multipart) | `updateMultipart` | PUT | `/{crtDmndUuid}/{verSn}` multipart | `PwrkCnsltnDocCrtRsltUpdateRequestDto` + `file` | ResponseDto | +| 수정 (JSON) | `updateJson` | PUT | `/{crtDmndUuid}/{verSn}` json | UpdateRequestDto | ResponseDto | +| 삭제 | `delete` → `remove` | DELETE | `/{crtDmndUuid}/{verSn}` | path | 204 | + +Spring 매핑 주의: `/{crtDmndUuid}/latest` 와 `/{crtDmndUuid}/download` 는 `/{crtDmndUuid}/{verSn}` 보다 **구체 경로가 우선**한다. 프론트 path를 섞지 말 것. + +버전 이력 조회 예: + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt?page=0&size=10&crtDmndUuid={uuid}&sort=verSn,desc +``` + +#### 다운로드 (attachment) + +화면 [다운로드] 버튼 → PC 저장. + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/download +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/download +``` + +- axios: `responseType: "blob"`. +- 파일명: `Content-Disposition`의 `filename*=UTF-8''...` (`parseFilenameFromResponse` 재사용). +- 서버 파일명 규칙: `재원협의 실무협의서({bizNm})_v{verSn}.hwp`. +- **현재 컨트롤러 Content-Type**: `application/x-hwp`. + 데이터 설명 예시는 `application/haansofthwpx`. 프론트는 Content-Type에 의존하지 말고 blob으로 저장한다. + +기존 `attachService` 다운로드와 동일 패턴: + +```ts +const response = await client.get(url, { responseType: "blob" }); +const filename = parseFilenameFromResponse(response); +const objectUrl = window.URL.createObjectURL(response.data); +``` + +#### 웹기안기 열기 (inline) — TBD + +데이터 설명 기준 엔드포인트. **현재** `PwrkCnsltnDocCrtRsltController`**에는 없음.** 백엔드 추가 후 연결한다. + +```http +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/webhwp/open +GET /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt/{crtDmndUuid}/{verSn}/webhwp/open +``` + +의도: + +- `Content-Disposition: inline` +- 한컴 웹기안기 `HwpCtrl.Open(url)` 이 이 URL을 직접 호출 +- 브라우저 다운로드 창이 뜨지 않고 캔버스에 로드 + +프론트는 **blob fetch 후 Object URL을 넘기지 않는다.** 인증 헤더가 필요한 절대 URL을 기안기에 전달하는 방식이므로, 쿠키/게이트웨이 인증 가능 여부를 백엔드와 확인한다 (TBD). + +#### 신규 버전 등록 (웹기안기 저장) + +서버가 최신 `verSn + 1`을 부여한다. 클라이언트가 `verSn`을 보내지 않는다. + +```http +POST /as-service/api/v1/pwrk-cnsltn-doc-crt-rslt +Content-Type: multipart/form-data + +crtDmndUuid: {uuid} +file: 재원협의 실무협의서_v2.hwpx +``` + +화면에서는 **multipart만** 사용한다. JSON `saveJson`은 바이너리를 body에 넣는 경로라 UI에서 쓰지 않는다. + +`file` part 이름: `"file"`. DTO 필드는 `@ModelAttribute` 로 평문 필드(`crtDmndUuid`) 또는 `saveDto` part. + +--- + +## 6. DTO 매핑 (프론트 타입명 = 백엔드 클래스명) + +### 6-1. `PwrkCnsltnDocCrtRslt*` + +| 계층 | 파일 | 정의 | +| ------------------------------------ | -------------------------------------------- | ----------------------------------------------- | +| Save Request | `pwrk-cnsltn-doc-crt-rslt-payload-schema.ts` | zod SSOT → `PwrkCnsltnDocCrtRsltSaveRequestDto` | +| Response / Search / Update / Service | `pwrk-cnsltn-doc-crt-rslt-interface.ts` | `interface` | + +**Response** + +| 필드 | 타입 | 비고 | +| ---------------- | -------- | --------------------- | +| `crtDmndUuid` | `string` | UUID | +| `verSn` | `number` | BigDecimal | +| `rgtrUuid` | `string` | | +| `rgtrNm` | `string` | 서버 enrich | +| `orgnlOtptDocCn` | `null` | 목록/상세 미포함 | +| `crtCmptnDt` | `string` | `yyyy-MM-dd HH:mm:ss` | + +**Search** (`PageableParams` 확장) + +| 필드 | 비고 | +| ----------------- | ---------------------- | +| `crtDmndUuid` | 버전 이력 조회 시 필수 | +| `pwrkCnsltnDocNo` | | +| `verSn` | | +| `rgtrUuid` | | + +**Save Request (zod)** + +| 필드 | 필수 | 비고 | +| ------------- | ---- | ------------------------------------ | +| `crtDmndUuid` | Y | | +| `rgtrUuid` | N | 미전송 시 서버가 로그인 사용자 사용 | +| `file` | UI | multipart `file` part. DTO 필드 아님 | + +**Update Request** + +| 필드 | 비고 | +| ------------ | --------- | +| `rgtrUuid` | | +| `crtCmptnDt` | | +| `file` | multipart | + +### 6-2. `PwrkCnsltnMng*` + +**Search** + +`pwrkCnsltnDocNo`, `upDocNo`, `upDocSeComCd`, `prgrsSttsComCd`, `admdstCd`, `envoSeComCd`, `taskSeComCd`, `bizNm` + +**Save Request (초안 생성)** — 필드가 얇으므로 interface만으로도 가능. 권장은 zod. + +| 필드 | 필수 | 값 | +| ----------------- | ---- | ----------------- | +| `upDocNo` | Y | 재원협의 문서번호 | +| `upDocSeComCd` | | `"FNRS"` | +| `prgrsSttsComCd` | | `"DV"` (초안생성) | +| `pwrkCnsltnDocNo` | N | 서버 발급 | + +**Save Response** — `save`만 이 DTO. `fnrsCnsltnMngService.save`가 `string`을 반환하는 것과 **다름**. + +| 필드 | 의미 | +| ------------------ | --------------------------------- | +| `pwrkCnsltnDocNo` | 발급/재사용 번호 | +| `resultCode` | `ACCEPTED` / `SUCCESS` / 실패코드 | +| `crtDmndUuid` | 폴링 키 | +| `crtSttsComCd` | `R` 생성요청 | +| `estimatedMinutes` | 기본 10 | + +**DetResponse 중첩** + +- `cnsltnList`: 협의내역 (`rvwOpnnSn`, `rvwOpnnClsfComCd`, `cnsltnCn`, `rfltYn`, …) +- `pjtcoAjmtList`: 사업비조정 +- `crtDmndList[].crtRslt`: 생성요청 + 최신 결과 +- `splmntDmndList`: 보완요청 +- `sttsChgHstryList`: 상태변경이력 +- `trsmFileList`: 전송파일 (데이터 설명 기준. Det DTO Java 필드 확인 후 타입 반영) + +### 6-3. DC RAG + +```ts +interface DcRagDocumentTransferRequestDto { + fnrsCnsltnDocNo: string; + taskSeComCd?: string; + taskSeCdNm?: string; +} + +interface DcRagCommonResponseDto { + resultCode: string; + message?: string; + requestId?: string; +} + +interface DcRagDraftStatusResponseDto { + crtDmndUuid: string; + status: DraftStatus; + statusName?: string; + progressPercent?: number; + events?: EventInfoDto[]; + startedDt?: string; + estimatedCompletionDt?: string; + errorMessage?: string | null; +} + +type DraftStatus = + | "REQUESTED" + | "DOWNLOADING" + | "ANALYZING" + | "GENERATING" + | "COMPLETED" + | "FAILED"; +``` + +--- + +## 7. 공통코드 + +| 상위코드 | 필드 | 값 | 명칭 | +| ----------------------- | ---------------- | --------------- | ------------- | +| `ASDOCSE` | `upDocSeComCd` | `FNRS` | 재원협의 | +| `ASDOCSE` | 문서구분 | `PWRK` | 실무협의 | +| `PWRKSTTS` | `prgrsSttsComCd` | `DV` | 초안생성 | +| `PWRKSTTS` | | `AR` | 검토자지정 | +| `PWRKSTTS` | | `SV` | 작성중 | +| `PWRKSTTS` | | `RB` | 검토회신 | +| `PWRKSTTS` | | `RC` | 협의회신 | +| `PWRKSTTS` | | `TR` | 검토결과 회신 | +| `PWRKSTTS` | | `SL` | 보완요청 | +| `PWRKSTTS` | | `AA` | 금액조정중 | +| `AICRTSTTS` / `CRTSTTS` | `crtSttsComCd` | `R` | 생성요청 | +| | | `I` | 생성중 | +| | | `C` | 생성완료 | +| | | `F` | 생성실패 | +| (검증) | `vrfcSttsComCd` | `PASS` / `FAIL` | | + +`CrtDmndResponseDto`는 `@CodeName(upCd = "CRTSTTS")`, 생성요청 단건 DTO는 `AICRTSTTS`를 쓴다. 코드값이 `R/I/C/F`로 같으므로 UI는 코드값으로 분기하고, 표시명은 응답의 `*Nm` 필드를 쓴다. + +초안 생성 직후 `crtSttsComCd = R`, 폴링 `COMPLETED` 이후 상세의 `crtSttsComCd = C`. + +--- + +## 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/sewer-bsc-plan/` + +### 8-2. 모듈·파일 배치 + +| 구분 | 경로 | +| -------------------- | ------------------------------------------------------------------- | +| ENDPOINT | `apps/as/src/shared/config/api.ts` | +| 실무협의 관리 엔티티 | `entities/pwrk-cnsltn-mng/` | +| 생성결과 엔티티 | `entities/pwrk-cnsltn-doc-crt-rslt/` | +| DC RAG 엔티티 | `entities/dcrag-to/` | +| 목록 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/page.tsx` | +| 상세 라우트 | `app/(main)/(with-base-layout)/pwrk-cnsltn/[id]/page.tsx` | +| 페이지 UI | `pages/pwrk-cnsltn/` | +| 재원협의 상세 연계 | `pages/fnrs-cnsltn/ui/fnrs-view-page.tsx` (버튼·폴링·첨부 검증배지) | + +엔티티 내부: + +``` +entities/pwrk-cnsltn-mng/ +├── model/ +│ ├── pwrk-cnsltn-mng-interface.ts +│ └── pwrk-cnsltn-mng-payload-schema.ts +├── api/ +│ ├── pwrk-cnsltn-mng-service.ts +│ └── queries.ts +└── index.ts + +entities/pwrk-cnsltn-doc-crt-rslt/ +├── model/ +│ ├── pwrk-cnsltn-doc-crt-rslt-interface.ts +│ └── pwrk-cnsltn-doc-crt-rslt-payload-schema.ts +├── api/ +│ ├── pwrk-cnsltn-doc-crt-rslt-service.ts +│ └── queries.ts +└── index.ts +``` + +### 8-3. Service 시그니처 요지 + +`pwrkCnsltnMngService.save` 반환 타입이 `string`이 아니라 `PwrkCnsltnMngSaveResponseDto` 인 점을 `fnrs-cnsltn`과 혼동하지 말 것. + +```ts +export interface PwrkCnsltnMngService { + findPage(searchDto: PwrkCnsltnMngSearchDto): Promise>; + findById(pwrkCnsltnDocNo: string): Promise; + findByUpDocNo(upDocNo: string): Promise; + save(body: PwrkCnsltnMngSaveRequestDto): Promise; + saveApply(body: PwrkCnsltnApplySaveRequestDto): Promise; + update(pwrkCnsltnDocNo: string, body: PwrkCnsltnMngUpdateRequestDto): Promise; + submitSupplement(pwrkCnsltnDocNo: string, body: PwrkCnsltnApplySaveRequestDto): Promise; + submitReqSupplement( + pwrkCnsltnDocNo: string, + body: PwrkCnsltnApplySaveRequestDto, + ): Promise; + remove(pwrkCnsltnDocNo: string): Promise; + changePrgrsSttsComCd(body: DocPrgrsUpdateRequestDto): Promise; +} + +export interface PwrkCnsltnDocCrtRsltService { + findPage( + searchDto: PwrkCnsltnDocCrtRsltSearchDto, + ): Promise>; + findById(crtDmndUuid: string, verSn: number): Promise; + findLatestByCrtDmndUuid(crtDmndUuid: string): Promise; + download(crtDmndUuid: string, verSn: number): Promise>; + downloadLatest(crtDmndUuid: string): Promise>; + saveMultipart(formData: FormData): Promise; + updateMultipart( + crtDmndUuid: string, + verSn: number, + formData: FormData, + ): Promise; + remove(crtDmndUuid: string, verSn: number): Promise; +} +``` + +multipart `Content-Type`은 axios가 boundary를 붙이도록 **헤더를 수동 지정하지 않는다.** + +### 8-4. 폴링 훅 + +`useQuery` + `refetchInterval`: + +```ts +useQuery({ + queryKey: [...dcragToKeys.draftStatus(crtDmndUuid)], + queryFn: () => dcragToService.getDraftGenerationStatus(crtDmndUuid), + enabled: Boolean(crtDmndUuid) && isPolling, + refetchInterval: (query) => { + const status = query.state.data?.status; + if (status === "COMPLETED" || status === "FAILED") return false; + return 5_000; + }, +}); +``` + +`COMPLETED` 시 `pwrkCnsltnMng` 상세 쿼리를 invalidate 하여 `crtDmndList[].crtRslt`를 갱신한다. + +### 8-5. 재원협의 상세에 붙일 UI + +`pages/fnrs-cnsltn` 조회 모드: + +1. **자료 검증 요청** — `requestDocumentTransfer` → 상세 refetch → 첨부 행에 PASS/FAIL 배지 (`vrfcSttsComCd`, `vrfcRsltCn`, `vrfcDt`). +2. **초안 생성** — 검증이 끝난 뒤 활성. `pwrkCnsltnMngService.save`. +3. **진행 표시** — `progressPercent` + 이벤트 타임라인. `estimatedMinutes`(10)를 안내. +4. **웹기안기 열기 / 다운로드** — `COMPLETED` 이후에만. + +`findByUpDocNo(fnrsCnsltnDocNo)`로 이미 초안이 있으면 `crtDmndUuid`를 복원해 폴링·열기를 이어간다. + +### 8-6. 실무협의 목록 컬럼 (권장) + +마스터 응답 + 재원협의 조인 필드 기준. 화면설계 확정 전 TBD. + +| 표시명 | Field | +| ------------ | ------------------ | +| 실무협의번호 | `pwrkCnsltnDocNo` | +| 재원협의번호 | `upDocNo` | +| 진행상태 | `prgrsSttsComCdNm` | +| 사업명 | `bizNm` | +| 시도/구군 | `sido` / `gugun` | +| 신청자 | `aplcntNm` | +| 등록일 | `regDt` | + +행 클릭 → `/pwrk-cnsltn/{pwrkCnsltnDocNo}`. + +--- + +## 9. 구현 순서 + +``` +Task Progress: +- [ ] 1. ENDPOINT 3개 추가 (PWRK_CNSLTN_MNG, PWRK_CNSLTN_DOC_CRT_RSLT, DCRAG_TO) +- [ ] 2. entities/dcrag-to (transfer + draft status) +- [ ] 3. entities/pwrk-cnsltn-mng (findPage, findById, findByUpDocNo, save) +- [ ] 4. entities/pwrk-cnsltn-doc-crt-rslt (findPage, downloadLatest, saveMultipart) +- [ ] 5. 재원협의 상세: 검증결과 3필드 타입 확장 + 배지 +- [ ] 6. 재원협의 상세: 자료검증 / 초안생성 / 폴링 UI +- [ ] 7. 실무협의 목록·상세 라우트 +- [ ] 8. 다운로드 (blob) +- [ ] 9. 웹기안기 Open — 백엔드 webhwp/open 확인 후 +- [ ] 10. 웹기안기 저장 → saveMultipart (verSn + 1) +``` + +1~4는 엔티티 레이어만으로 먼저 닫는다. 화면은 5부터. + +--- + +## 10. TBD · 백엔드 확인 + +| 항목 | 내용 | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `webhwp/open` | 데이터 설명에는 있으나 **현재 Controller에 없음**. 추가 일정·인증 방식 확인 | +| Content-Type | Controller `application/x-hwp` vs 설명 `application/haansofthwpx` | +| 파일 확장자 | 다운로드 파일명이 `.hwp`로 고정. 실제 바이너리가 hwpx일 수 있음 | +| 폴링 주기 | 5초 vs 10초, 탭 비활성 시 중단 여부 | +| 검증 필드 타입 | `FnrsCnsltnMngDetResponseDto.esntlFileList` / `etcFileList` Java 타입이 아직 `FnrsCnsltnAtflResponseDto`가 아님. 런타임 JSON에 `vrfc*`가 오는지 확인 | +| `trsmFileList` | 데이터 설명 상세 예시에는 있으나 Det DTO Java 선언과 불일치 가능 | +| `crtSttsComCd` 상위코드 | `CRTSTTS` vs `AICRTSTTS` | +| 웹기안기 라이선스 URL | `HwpCtrl.Open`에 넘길 게이트웨이 절대 경로 (쿠키 인증 가능 여부) | +| 초안 생성 1회 제한 | `requestDraftGeneration` 주석에 “1회 제한”. 재클릭 UX (비활성 / 재요청 확인) | +| 자료전송 허용 확장자 | `.doc .docx .hwp .hwpx .md .pdf .txt .xls .xlsm .xlsx`. 이미지 첨부 시 DC RAG 실패 | + +--- + +## 11. 참고 + +| 자료 | 위치 | +| ---------------------- | ------------------------------------------------------------------------------ | +| 생성결과 컨트롤러 | `backend/.../pwrkcnsltndoccrtrslt/PwrkCnsltnDocCrtRsltController.java` | +| 실무협의관리 컨트롤러 | `backend/.../pwrkcnsltnmng/PwrkCnsltnMngController.java` | +| DC RAG TO 컨트롤러 | `backend/.../dcrag/DcRagApiToController.java` | +| 초안생성 데이터 설명 | `02.03.초안생성_데이터_설명.md` | +| 엔티티 구현 스킬 | `frontend/.cursor/skills/backend-controller-to-entity/` | +| 재원협의 엔티티 (패턴) | `apps/as/src/entities/fnrs-cnsltn/` | +| IIPBS 단어목록 | `frontend/docs/IIPBS_단어목록.md` | +| 진행상태 코드 | `backend/IIPBS_CS/docs/2026/05.행정지원/01.문서상태별_사용자_상태변경_이력.md` |