diff --git a/apps/as/docs/pwrk-cnsltn/development-guide.md b/apps/as/docs/pwrk-cnsltn/development-guide.md index 99e38c569..6812c3db1 100644 --- a/apps/as/docs/pwrk-cnsltn/development-guide.md +++ b/apps/as/docs/pwrk-cnsltn/development-guide.md @@ -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 ----- @@ -181,7 +181,8 @@ BEGIN ON [결과회신] (LE 자체종결) → tech-review → EP ON [기술검토 의뢰] (LE) → request-examine → FV → START POLL FV ON [재원협의 결과 회신] (TP) → tech-review → RM - ON [조치결과서 검토요청] (RA) → request-actn-review → AV → START POLL AV + ON [실무협의서 관리] (RA) → GET webhwp/open (blob) → HwpCtrl.Open(File) (저장 불가) + ON [조치결과 검토의뢰] (RA) → request-actn-review → AV → START POLL AV ON [결과회신] (RR) → updatePrgrsStts(EP) // ----- 공단 ROLE_USER01 ----- @@ -230,9 +231,11 @@ 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` | +| 환경청 | 실무협의서 관리 | `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` | diff --git a/apps/as/docs/pwrk-cnsltn/ref-endpoint.md b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md index b14a429ce..d63d2d4de 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-endpoint.md +++ b/apps/as/docs/pwrk-cnsltn/ref-endpoint.md @@ -39,10 +39,10 @@ | 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` | **신규** | +| 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` | 서버 | | 31 | 실무협의서관리 | `RK` | as | `GET /pwrk-cnsltn-mng/{pwrkDocNo}` | 기존 | | 32 | 금액검증 재실행 | `RK` | as | `POST /api/v1/dcrag-to/documents/cost/verify` | 기존 | @@ -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,29 +203,25 @@ 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 { - "envoDocNo": "환경청 공문번호", "specialRqstCn": "중점 검증 요청 (선택)" } ``` -`RA→AV`. 응답: `string` (pwrkDocNo). DTO명 `EnvoActnReviewReqDto`. +`RA→AV`. 응답: `string` (docNo). DTO명 `EnvoActnReviewReqDto`. `specialRqstCn`만 선택. 금액검증 콜백은 서버. 콜백 후 서버가 `copy-line` 호출. diff --git a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md index f74b63571..e63531842 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md +++ b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md @@ -157,7 +157,6 @@ requestCostVerification(body: DcRagCostVerifyRequestDto): Promise>광역: [재원협의 결과 회신] TP → RM 광역->>기초: [지자체회신] RM → RB - 기초->>광역: [협의회신] RB → RC + 기초->>환경청: [조치완료] RB → RA end rect rgb(240, 255, 255) 광역->>환경청: [조치결과서제출] RC → RA - 환경청->>AI: [조치결과서 검토요청] RA → AV + 환경청->>AI: [조치결과 검토의뢰] RA → AV AI-->>공단: 금액검증 콜백 AV → RK Note over 공단: 30분 미응답 시 RK + 수동 [금액검증] Note over 공단: AV→RK 시 1차 결재선 → 2차 복사 (서버) @@ -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` | @@ -200,7 +200,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→RK` | 환경청 [조치결과 검토의뢰] | 30분 → `RK` + [금액검증] | `POST /dcrag-to/documents/cost/verify` | - 예상 소요 안내는 **30분** (v1.0의 10분은 폐기). - 콜백 수신·스케줄러 전이는 서버. FE는 상세 refetch / draft status 폴링만. diff --git a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md index 556aab39b..a500fad11 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md +++ b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md @@ -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,10 +57,11 @@ 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`** | +| 28 | 상세 | USER03 | - | `RA` | 실무협의서 관리 | 유지 | — | 웹기안기 Open. **저장 불가** | +| 28a | 상세 | USER03 | - | `RA` | 조치결과 검토의뢰 | `AV` | 공단 검토 | **`request-actn-review`** | | 29 | 자동화 | AI | - | `AV` | (금액검증) | `RK` | 공단 검토 | FE 호출 없음. 진행 패널 | | 30 | 상세 | USER01 | 검토자 | `RK` | 실무협의서관리 | 유지 | — | 웹기안기 | | 31 | 상세 | USER01 | 검토자 | `RK` | 금액검증 | `RK` | — | `dcrag-to/cost/verify` | @@ -85,13 +86,12 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일. | `fileSplmnt` | 파일보완 | `ER` 접수자. 현행에 없음 | | `fileVrfc` | 파일검증 | 유지. `ER` 재실행 | | `dsgntRvwr` | 검토자지정 | 유지. 전이를 `AC`로 | -| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK` | +| `pwrkCnsltnDocMng` | 실무협의서관리 | 유지. `TE`·`RK`·`RB`·`RA`(조회 전용) | | `pwrkCnsltnDocCrt` | 실무협의서생성 | 유지. 실패/타임아웃만 | | `rvwRsltInq` | 검토결과조회 | 신규 | -| `actnStrg` | 저장(조치결과) | `RB` | -| `cnsltnRply` | 협의회신 | `RB→RC` | +| `actnCmptn` | 조치완료 | `RB→RA`. `isSubmit:true` | | `actnSbmsn` | 조치결과서제출 | 광역 `RC→RA` | -| `rqstActnRvw` | 조치결과서 검토요청 | 환경청 `RA→AV` | +| `rqstActnRvw` | 조치결과 검토의뢰 | 환경청 `RA→AV` | | `costVrfc` | 금액검증 | `RK` | | `lclRply` | 지자체회신 | 광역 `RM→RB` | | `fnrsRsltRply` | 재원협의 결과 회신 | 환경청 `TP→RM` | @@ -107,7 +107,7 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일. ROLE_USER05 "" | SV → 저장, 신청완료, 삭제 SL → (직전 ER) 파일보완완료 | (직전 TE|RK) 보완완료 - RB → 저장(조치), 협의회신 + RB → 실무협의서 관리, 조치완료 TA → 결과회신 (조회) ROLE_USER04 @@ -119,7 +119,7 @@ ROLE_USER04 ROLE_USER03 LE → 결과회신(자체종결), 기술검토 의뢰 TP → 재원협의 결과 회신 - RA → 조치결과서 검토요청 + RA → 실무협의서 관리(조회), 조치결과 검토의뢰 RR → 결과회신 ``` diff --git a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts index a34b22ad6..82764bd3b 100644 --- a/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts +++ b/apps/as/src/entities/fnrs-cnsltn/model/fnrs-cnsltn-payload-schema.ts @@ -7,7 +7,6 @@ export const fnrsExamineRequestSchema = z.object({ /** 백엔드 EnvoActnReviewReqDto */ export const envoActnReviewReqSchema = z.object({ - envoDocNo: z.string().min(1), specialRqstCn: z.string().optional(), }); diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts index ac27e8f5d..2faf8acef 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts @@ -5,14 +5,37 @@ 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; +async function findById(pwrkCnsltnDocNo: string): Promise { + const response = await client.get(`${BASE}/${pwrkCnsltnDocNo}`); + return response.data; +} + async function findByUpDocNo(upDocNo: string): Promise { const response = await client.get(`${BASE}/updocno/${upDocNo}`); return response.data; } +async function submitActnResult( + pwrkCnsltnDocNo: string, + body: PwrkActnResultSubmitDto, +): Promise { + const payload = pwrkActnResultSubmitSchema.parse(body); + const response = await client.post( + `${BASE}/${pwrkCnsltnDocNo}/submit-actn-result`, + payload, + ); + return response.data; +} + export const pwrkCnsltnMngService = { + findById, findByUpDocNo, + submitActnResult, } satisfies PwrkCnsltnMngService; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts index a5ba4cd8e..f3dbc0f68 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts @@ -4,9 +4,21 @@ import { pwrkCnsltnMngService } from "./pwrk-cnsltn-mng-service"; export const pwrkCnsltnMngKeys = { all: ["pwrk-cnsltn-mng"] as const, + byId: (pwrkCnsltnDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-id", pwrkCnsltnDocNo] as const, byUpDocNo: (upDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-up-doc-no", upDocNo] as const, }; +export function usePwrkCnsltnMng( + pwrkCnsltnDocNo: string | null | undefined, + isEnabled = true, +) { + return useQuery({ + queryKey: pwrkCnsltnMngKeys.byId(pwrkCnsltnDocNo!), + queryFn: () => pwrkCnsltnMngService.findById(pwrkCnsltnDocNo!), + enabled: Boolean(pwrkCnsltnDocNo) && isEnabled, + }); +} + export function usePwrkCnsltnMngByUpDocNo(upDocNo: string | null | undefined, isEnabled = true) { return useQuery({ queryKey: pwrkCnsltnMngKeys.byUpDocNo(upDocNo!), diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/index.ts b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts index 58a3dc75d..e2191546f 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/index.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts @@ -1,9 +1,15 @@ export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service"; -export { pwrkCnsltnMngKeys, usePwrkCnsltnMngByUpDocNo } from "./api/queries"; +export { pwrkCnsltnMngKeys, usePwrkCnsltnMng, usePwrkCnsltnMngByUpDocNo } from "./api/queries"; export { findCompletedPwrkDraft } from "./lib/find-completed-draft"; export type { + PwrkCnsltnCnsltnResponseDto, PwrkCnsltnDocCrtDmndResponseDto, PwrkCnsltnDocCrtRsltResponseDto, PwrkCnsltnMngDetResponseDto, PwrkCnsltnMngService, + PwrkCnsltnPjtcoAjmtResponseDto, } from "./model/pwrk-cnsltn-mng-interface"; +export { + type PwrkActnResultSubmitDto, + pwrkActnResultSubmitSchema, +} from "./model/pwrk-cnsltn-mng-payload-schema"; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts index fe5c4e5a4..b89f06d2d 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts @@ -1,3 +1,7 @@ +import type { AtflMngClientResponseDto } from "@/shared/model"; + +import type { PwrkActnResultSubmitDto } from "./pwrk-cnsltn-mng-payload-schema"; + export interface PwrkCnsltnDocCrtRsltResponseDto { crtDmndUuid: string; verSn: number; @@ -10,25 +14,84 @@ export interface PwrkCnsltnDocCrtRsltResponseDto { export interface PwrkCnsltnDocCrtDmndResponseDto { crtDmndUuid: string; pwrkCnsltnDocNo: string; + orgnlDocNo?: string; + orgnlDocSeComCd?: string; + orgnlDocSeComCdNm?: string; + crtSysDmndId?: string; crtSttsComCd?: string; crtSttsComCdNm?: string; failRsn?: string; dmndDt?: string; cmptnDt?: string; + rqstrUuid?: string; + rqstrNm?: string; crtRslt?: PwrkCnsltnDocCrtRsltResponseDto | null; } +/** 백엔드 PwrkCnsltnMngDetResponseDto.CnsltnResponseDto */ +export interface PwrkCnsltnCnsltnResponseDto { + pwrkCnsltnDocNo?: string; + rvwOpnnSn?: number; + rvwOpnnClsfComCd?: string; + rvwOpnnClsfComCdNm?: string; + rvwOpnn?: string; + cnsltnCn?: string; + rfltYn?: string; + rmrk?: string; + atchFileGroupId?: string; + atchFileInfo?: AtflMngClientResponseDto; + rvwrNm?: string; + rvwrMblTelno?: string; + rvwrOgdpNm?: string; + rvwrEmlAddr?: string; + lclgvPicNm?: string; + lclgvPicTelno?: string; + lclgvPicOgdpNm?: string; + lclgvPicEmlAddr?: string; + crtDmndUuid?: string; + crtYn?: string; + hwrtChgYn?: string; + verSn?: number; + dtlSn?: number; +} + +/** 백엔드 PwrkCnsltnMngDetResponseDto.PjtcoAjmtResponseDto */ +export interface PwrkCnsltnPjtcoAjmtResponseDto { + pwrkCnsltnDocNo?: string; + rvwOpnnSn?: number; + cnstpSeCd?: string; + rvwOpnnClsfComCd?: string; + rvwOpnnClsfComCdNm?: string; + ognTpcoAmt?: number; + ognGvexpAmt?: number; + ognLcexpAmt?: number; + chgTpcoAmt?: number; + chgGvexpAmt?: number; + chgLcexpAmt?: number; +} + export interface PwrkCnsltnMngDetResponseDto { pwrkCnsltnDocNo: string; upDocNo: string; upDocSeComCd?: string; + upDocSeComCdNm?: string; prgrsSttsComCd?: string; + prgrsSttsComCdNm?: string; bizNm?: string; atchFileGroupId?: string; + regDt?: string; + rgtrNm?: string; + cnsltnList?: PwrkCnsltnCnsltnResponseDto[]; + pjtcoAjmtList?: PwrkCnsltnPjtcoAjmtResponseDto[]; crtDmndList?: PwrkCnsltnDocCrtDmndResponseDto[]; crtRsltList?: PwrkCnsltnDocCrtRsltResponseDto[]; } export interface PwrkCnsltnMngService { + findById(pwrkCnsltnDocNo: string): Promise; findByUpDocNo(upDocNo: string): Promise; + submitActnResult( + pwrkCnsltnDocNo: string, + body: PwrkActnResultSubmitDto, + ): Promise; } diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-payload-schema.ts b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-payload-schema.ts new file mode 100644 index 000000000..d12926429 --- /dev/null +++ b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-payload-schema.ts @@ -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; diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts index 0060467b7..2980cfffb 100644 --- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts @@ -64,7 +64,7 @@ export const sewerBscPlanFnrsCstItemSchema = z.object({ /** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */ export const sewerBscPlanSplmntDmndItemSchema = z.object({ splmntDmndSn: z.number().nullish(), - splmntArtclComCd: z.string().optional(), + splmntArtclComCd: z.string().nullish(), splmntDmndCn: z.string().optional(), splmntDmndDt: z.string().optional(), splmntDmndAtchFileGroupId: z.string().optional(), diff --git a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx index 4e5a5ad76..041e1d812 100644 --- a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx +++ b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx @@ -169,7 +169,9 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
- +
+ +
{initialData?.detDto?.designCnstnDocNo && (
-
- {uploadedFiles.length > 0 ? ( -
- {uploadedFiles.map((item, index) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - {item.fileName} - - -
- ); - })} -
- ) : null} - -
+ ({ + fileKey: item.fileKey, + fileName: item.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + onAdd={(fileList) => void handleFileUpload(fileList)} + onRemove={(index) => void handleFileRemove(index)} + />
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 1b6ba18b4..d0b906dae 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -6,17 +6,21 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { envoSbmsn: "envoSbmsn", rjct: "rjct", sncnAprv: "sncnAprv", + lclRply: "lclRply", rvwRply: "rvwRply", rqstTechRvw: "rqstTechRvw", fileSplmnt: "fileSplmnt", dsgntRvwr: "dsgntRvwr", pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt", pwrkCnsltnDocMng: "pwrkCnsltnDocMng", + rvwRsltInq: "rvwRsltInq", rvwStrg: "rvwStrg", rvwCmptn: "rvwCmptn", dmndSplmnt: "dmndSplmnt", atrzAprv: "atrzAprv", atrzRply: "atrzRply", + actnCmptn: "actnCmptn", + rqstActnRvw: "rqstActnRvw", } as const; export type FnrsCnsltnViewButtonAction = @@ -34,15 +38,24 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record([ - FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, + ...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply, ]); @@ -109,8 +126,14 @@ function resolveRoleUser01AtrzButtonActions( } return actions.filter((action) => { + const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); + if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false; + if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) { - if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) { + if ( + action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng || + action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq + ) { return options.hasCompletedDraft; } if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { @@ -118,9 +141,7 @@ function resolveRoleUser01AtrzButtonActions( } } - const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); - - return !needsPendingApproval || options?.hasPendingApprovalRequest !== false; + return true; }); } diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts index e925ab321..0faa372e9 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts @@ -86,7 +86,7 @@ export const fnrsCnsltnRegistSchema = z.object({ splmntDmndList: z.array( z.object({ splmntDmndSn: z.number().nullish(), - splmntArtclComCd: z.string().optional(), + splmntArtclComCd: z.string().nullish(), splmntArtclCdNm: z.string().nullish(), splmntDmndCn: z.string().optional(), splmntDmndDt: z.string().optional(), diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts index 6a81db9ee..39ebd7582 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts @@ -10,6 +10,7 @@ import type { FnrsCnsltnMngResponseDto, FnrsCnsltnSplmntDmndItem, } from "@/entities/fnrs-cnsltn"; +import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng"; import { mapFnrsSharDetToFormList, type SplmntDmndDialogConfirmPayload, @@ -129,6 +130,26 @@ export function buildFileSplmntReqPayload( }; } +export function buildRvwRsltSplmntReqPayload( + data: FnrsCnsltnMngDetResponseDto, + selected: PwrkCnsltnCnsltnResponseDto[], + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({ + splmntDmndCn: row.rvwOpnn, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem { return { splmntDmndSn: item.splmntDmndSn, diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index e3a6fad4f..00f14e430 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -21,6 +21,7 @@ import { } from "@/entities/elapr"; import { type DocPrgrsUpdateRequestDto, + type EnvoActnReviewReqDto, type FnrsCnsltnApplySaveRequestDto, type FnrsCnsltnFileVrfcRsltResponseDto, fnrsCnsltnKeys, @@ -28,7 +29,13 @@ import { fnrsCnsltnMngService, type FnrsExamineRequestDto, } from "@/entities/fnrs-cnsltn"; -import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng"; +import { + findCompletedPwrkDraft, + type PwrkActnResultSubmitDto, + type PwrkCnsltnCnsltnResponseDto, + pwrkCnsltnMngService, + usePwrkCnsltnMngByUpDocNo, +} from "@/entities/pwrk-cnsltn-mng"; import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; @@ -41,6 +48,7 @@ import { import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils"; import { buildFileSplmntReqPayload, + buildRvwRsltSplmntReqPayload, buildSavePayload, buildSupplementReqPayload, buildTechReviewReplyPayload, @@ -51,6 +59,7 @@ import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opi import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog"; import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog"; +import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog"; interface FloatingActionButtonsProps { data: FnrsCnsltnMngDetResponseDto; @@ -73,6 +82,7 @@ type ActiveDialog = | "fileSplmnt" | "elaprApproval" | "pwrkCnsltnDocMng" + | "rvwRsltInq" | null; function FloatingActionButtons({ @@ -102,13 +112,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, @@ -265,6 +278,19 @@ function FloatingActionButtons({ }); } + async function handleLclRplyClick() { + const payload: DocPrgrsUpdateRequestDto = { + docNo, + docSeComCd, + prgrsSttsComCd: "RB", + }; + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply, + request: payload, + successMessage: "지자체 회신이 완료되었습니다.", + }); + } + async function handleRjctClick() { const payload: DocPrgrsUpdateRequestDto = { docNo, @@ -311,7 +337,7 @@ function FloatingActionButtons({ data, checklistFormState, "ENV", - "EP", + "RM", "CP", getReviewOpinionFormData(), opnrNm, @@ -320,7 +346,7 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, request: payload, - successMessage: "검토결과 회신이 완료되었습니다.", + successMessage: "재원협의 결과 회신이 완료되었습니다.", }); techReviewOpinionRef?.current?.resetForm(); } @@ -336,6 +362,15 @@ function FloatingActionButtons({ }); } + async function handleRqstActnRvwClick() { + const payload: EnvoActnReviewReqDto = {}; + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw, + request: payload, + successMessage: "조치결과 검토의뢰가 완료되었습니다.", + }); + } + async function handleElaprTmpltConfirm({ aplnRows }: ElaprTmpltPickerConfirmPayload) { const payload: ElaprDmndSaveRequestDto = { elaprDocNo: docNo, @@ -435,6 +470,32 @@ function FloatingActionButtons({ }); } + async function handleRvwRsltSplmntConfirm(selected: PwrkCnsltnCnsltnResponseDto[]) { + setActiveDialog(null); + const requestPayload = buildRvwRsltSplmntReqPayload(data, selected, checklistFormState); + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, + request: requestPayload, + successMessage: "보완요청이 처리되었습니다.", + }); + } + + async function handleActnCmptnClick() { + if (!pwrkCnsltnDocNo) { + alert("실무협의 문서번호를 찾을 수 없습니다."); + return; + } + + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn, + request: { + pwrkCnsltnDocNo, + body: { isSubmit: true } satisfies PwrkActnResultSubmitDto, + }, + successMessage: "조치완료되었습니다.", + }); + } + return ( <>
@@ -444,6 +505,21 @@ function FloatingActionButtons({ {!isAiPending && ( <> + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && ( + + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && ( + + )} + {canEdit && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && ( @@ -471,7 +547,12 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && ( + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply) && ( + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( @@ -486,36 +567,41 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw) && ( + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && ( - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && ( + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && ( @@ -525,12 +611,12 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && ( @@ -588,8 +674,18 @@ function FloatingActionButtons({ setActiveDialog(null)} /> + + setActiveDialog(null)} + onConfirm={handleRvwRsltSplmntConfirm} + /> )} @@ -680,6 +776,36 @@ async function handleRequestExamine( } } +async function handleRequestActnReview( + context: ActionHandlerContext, +): Promise { + try { + const docNo = context.data.detDto?.fnrsCnsltnDocNo; + if (!docNo) throw new Error("문서번호가 없습니다."); + await fnrsCnsltnMngService.requestActnReview(docNo, context.request); + context.onSuccess?.(); + } catch (error) { + context.onError?.(error as Error); + } +} + +async function handleActnCmptn( + context: ActionHandlerContext<{ + pwrkCnsltnDocNo: string; + body: PwrkActnResultSubmitDto; + }>, +): Promise { + 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 { // TODO: 결재 승인 처리 API 호출 context.onSuccess?.(); @@ -698,17 +824,21 @@ const buttonActionHandlers: Record = envoSbmsn: handleChangePrgrsSttsComCd, rjct: handleChangePrgrsSttsComCd, sncnAprv: handleChangePrgrsSttsComCd, + lclRply: handleChangePrgrsSttsComCd, rvwRply: handleTechReview, rqstTechRvw: handleRequestExamine, fileSplmnt: handleDmndSplmnt, dsgntRvwr: handleDsgntRvwr, pwrkCnsltnDocCrt: () => Promise.resolve(), pwrkCnsltnDocMng: () => Promise.resolve(), + rvwRsltInq: () => Promise.resolve(), rvwStrg: handleTechReview, rvwCmptn: handleTechReview, dmndSplmnt: handleDmndSplmnt, atrzAprv: handleAtrzAprv, atrzRply: handleAtrzRply, + actnCmptn: handleActnCmptn, + rqstActnRvw: handleRequestActnReview, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx index 2e9ac175d..f87fbed9b 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx @@ -4,15 +4,10 @@ import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Paperclip, X } from "lucide-react"; import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { - attachService, - parseFilenameFromResponse, - useDownloadAttachmentByAtflId, -} from "@/entities/attach"; +import { AttachmentFileField } from "@/shared/ui"; +import { attachService, parseFilenameFromResponse } from "@/entities/attach"; import { pwrkCnsltnDocCrtRsltSaveRequestSchema, pwrkCnsltnDocCrtRsltService, @@ -42,9 +37,13 @@ const HangulGiangi = dynamic( const PWRK_ATCH_FILE_KEY_PREFIX = "pwrk-cnsltn-doc::"; +const APPROVER_ATRZ_SE_COM_CDS = new Set(["ATRZ03", "ATRZ99"]); + interface PwrkCnsltnDocMngDialogProps { open: boolean; fnrsCnsltnDocNo: string; + atrzSeComCdList: string[]; + canSave?: boolean; onClose: () => void; } @@ -59,10 +58,15 @@ function createPwrkAtchUploadKey(file: File) { return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`; } -function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDocMngDialogProps) { +function PwrkCnsltnDocMngDialog({ + open, + fnrsCnsltnDocNo, + atrzSeComCdList, + canSave: canSaveProp, + onClose, +}: PwrkCnsltnDocMngDialogProps) { const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); - const downloadByAtflId = useDownloadAttachmentByAtflId(); const [atchFileGroupId, setAtchFileGroupId] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -229,12 +233,9 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo } } - function handleAtchFileDownload(atflId?: string) { - if (!atflId) return; - downloadByAtflId.mutate(atflId); - } - const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]); + const canSaveByAtrz = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)); + const canSave = canSaveProp ?? canSaveByAtrz; return ( @@ -248,7 +249,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo {isLoading ? : null} {isError ? ( -

+

실무협의서 정보를 불러오지 못했습니다.

) : null} @@ -263,6 +264,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo documentBlob={documentBlob} documentKey={completedDraft.crtDmndUuid} documentFileName={documentFileName} + canSave={canSave} onSaveDocument={handleSaveDocument} onDownload={handleDownload} /> @@ -271,69 +273,22 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, onClose }: PwrkCnsltnDo {completedDraft ? (
-
-

첨부파일

- -
- {uploadedFiles.length > 0 ? ( -
- {uploadedFiles.map((item, index) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - -
- ); - })} -
- ) : ( +

첨부파일

+ {uploadedFiles.length === 0 ? (

등록된 첨부파일이 없습니다.

- )} + ) : null} + ({ + fileKey: item.fileKey, + fileName: item.fileName, + }))} + inputId="pwrk-cnsltn-doc-atch-file-input" + isUploadingByKey={isUploading} + addDisabled={!canSave || isAnyAtchUploading} + showAddButton={canSave} + onAdd={(files) => void handleAtchFileUpload(files)} + onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined} + />
) : null} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx new file mode 100644 index 000000000..e811873f4 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { useState } from "react"; + +import { + type PwrkCnsltnCnsltnResponseDto, + usePwrkCnsltnMngByUpDocNo, +} from "@/entities/pwrk-cnsltn-mng"; +import { + Button, + Checkbox, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@common/components"; + +interface PwrkCnsltnRvwRsltInqDialogProps { + open: boolean; + fnrsCnsltnDocNo: string; + isSubmitting?: boolean; + onClose: () => void; + onConfirm: (selected: PwrkCnsltnCnsltnResponseDto[]) => void; +} + +interface CnsltnListTableProps { + rows: PwrkCnsltnCnsltnResponseDto[]; + selectedRowKeys: Set; + onToggleAll: (checked: boolean) => void; + onToggleRow: (rowKey: string, checked: boolean) => void; +} + +function PwrkCnsltnRvwRsltInqDialog({ + open, + fnrsCnsltnDocNo, + isSubmitting = false, + onClose, + onConfirm, +}: PwrkCnsltnRvwRsltInqDialogProps) { + const [selectedRowKeys, setSelectedRowKeys] = useState>(new Set()); + const { + data: pwrkCnsltnData, + isLoading, + isError, + } = usePwrkCnsltnMngByUpDocNo(fnrsCnsltnDocNo, open); + + const cnsltnList = pwrkCnsltnData?.cnsltnList ?? []; + const selectedRows = cnsltnList.filter((row, index) => + selectedRowKeys.has(getCnsltnRowKey(row, index)), + ); + + function handleClose() { + setSelectedRowKeys(new Set()); + onClose(); + } + + function handleOpenChange(nextOpen: boolean) { + if (!nextOpen) handleClose(); + } + + function handleToggleAll(checked: boolean) { + if (!checked) { + setSelectedRowKeys(new Set()); + return; + } + + setSelectedRowKeys(new Set(cnsltnList.map((row, index) => getCnsltnRowKey(row, index)))); + } + + function handleToggleRow(rowKey: string, checked: boolean) { + setSelectedRowKeys((prev) => { + const next = new Set(prev); + if (checked) next.add(rowKey); + else next.delete(rowKey); + return next; + }); + } + + function handleConfirm() { + if (selectedRows.length === 0) return; + onConfirm(selectedRows); + } + + return ( + + + + 검토결과조회 + + + {isLoading ? ( +
+ + +
+ ) : null} + + {isError ? ( +

+ 실무협의 검토결과를 불러오지 못했습니다. +

+ ) : null} + + {!isLoading && !isError && pwrkCnsltnData ? ( +
+ {cnsltnList.length === 0 ? ( +

조회된 검토의견이 없습니다.

+ ) : ( + + )} +
+ ) : null} + + + + + +
+
+ ); +} + +function CnsltnListTable({ + rows, + selectedRowKeys, + onToggleAll, + onToggleRow, +}: CnsltnListTableProps) { + const selectedCount = rows.filter((row, index) => + selectedRowKeys.has(getCnsltnRowKey(row, index)), + ).length; + const isAllSelected = rows.length > 0 && selectedCount === rows.length; + const isSomeSelected = selectedCount > 0 && !isAllSelected; + + return ( + + + + + + + + + + onToggleAll(checked === true)} + aria-label="전체 선택" + /> + + 분류 + 검토의견 + + + + {rows.map((row, index) => { + const rowKey = getCnsltnRowKey(row, index); + const isSelected = selectedRowKeys.has(rowKey); + + return ( + + + onToggleRow(rowKey, checked === true)} + aria-label="행 선택" + /> + + +

{formatOptional(row.rmrk)}

+
+ +

{formatOptional(row.rvwOpnn)}

+
+
+ ); + })} +
+
+ ); +} + +function getCnsltnRowKey(row: PwrkCnsltnCnsltnResponseDto, index: number) { + return [row.pwrkCnsltnDocNo, row.verSn, row.dtlSn, row.rvwOpnnSn, String(index)] + .filter((value) => value !== undefined && value !== null && value !== "") + .join("::"); +} + +function formatOptional(value: string | number | undefined | null) { + if (value === undefined || value === null) return "-"; + const text = String(value).trim(); + return text ? text : "-"; +} + +export { PwrkCnsltnRvwRsltInqDialog }; +export type { PwrkCnsltnRvwRsltInqDialogProps }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx index 55fbb04ad..efe663ab3 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx @@ -12,7 +12,7 @@ interface FnrsDetailGateProps { function FnrsDetailGate({ id }: FnrsDetailGateProps) { const userInfo = useUserStore((state) => state.userInfo); - const { data, isLoading, isFetching, isError } = useFnrsCnsltnMng(id); + const { data, isLoading, isError } = useFnrsCnsltnMng(id); const canEdit = canEditPbadmsDoc({ docSeComCd: "FNRS", prgrsSttsComCd: data?.detDto?.prgrsSttsComCd, @@ -22,9 +22,9 @@ function FnrsDetailGate({ id }: FnrsDetailGateProps) { return ( diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx index d892187f6..45af2a9f7 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx @@ -32,7 +32,7 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { 증감 - (C-B) + (C-A) % diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx index bdbd4e622..caa4a3ada 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx @@ -6,13 +6,11 @@ import { Paperclip } from "lucide-react"; import { PROXY_HOST } from "@/shared/config"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { SectionCollapsible } from "@/shared/ui"; +import { AttachmentFileField, SectionCollapsible } from "@/shared/ui"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { attachService } from "@/entities/attach"; import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn"; import { - Button, RadioGroup, RadioGroupItem, Table, @@ -246,38 +244,20 @@ const TechReviewOpinionSection = forwardRef< 첨부파일 - {uploadedFile ? ( -
- - {uploadedFile.fileName} - - -
- ) : ( - - )} + void handleFileUpload(fileList)} + onRemove={() => void handleFileRemove()} + />
@@ -300,7 +280,10 @@ const TechReviewOpinionSection = forwardRef<
{kecoRvwList.length > 0 ? : null} + {/* + 재원협의에서 환경공단 검토의견은 자동 생성 {renderOpinionForm("KECO")} + */}
) : null} diff --git a/apps/as/src/pages/instl-sncn/lib/form-data-mapper.ts b/apps/as/src/pages/instl-sncn/lib/form-data-mapper.ts index e32309255..4dfdf8685 100644 --- a/apps/as/src/pages/instl-sncn/lib/form-data-mapper.ts +++ b/apps/as/src/pages/instl-sncn/lib/form-data-mapper.ts @@ -22,10 +22,10 @@ export function mapDetailToFormData(data: InstlSncnMngDetResponse): InstlSncnReg bizCn: detDto.bizCn, taskSeComCd: parseTaskSeComCdFromApi(detDto.taskSeComCd), sncnSeComCd: detDto.sncnSeComCd, - dsgnrNm: detDto.dsgnrNm, - dsgnrTelno: detDto.dsgnrTelno, - dsgnrOgdpNm: detDto.dsgnrOgdpNm, - dsgnrEmlAddr: detDto.dsgnrEmlAddr, + dsgnrNm: detDto.dsgnrNm ?? "", + dsgnrTelno: detDto.dsgnrTelno ?? "", + dsgnrOgdpNm: detDto.dsgnrOgdpNm ?? "", + dsgnrEmlAddr: detDto.dsgnrEmlAddr ?? "", aplcntNm: detDto.aplcntNm ?? "", aplcntMblTelno: detDto.aplcntMblTelno ?? "", aplcntOgdpNm: detDto.aplcntOgdpNm ?? "", diff --git a/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts b/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts index 7fe74a337..755d3df63 100644 --- a/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts +++ b/apps/as/src/pages/instl-sncn/lib/instl-sncn-regist-schema.ts @@ -78,7 +78,7 @@ export const instlSncnRegistSchema = z.object({ splmntDmndList: z.array( z.object({ splmntDmndSn: z.number().nullish(), - splmntArtclComCd: z.string().optional(), + splmntArtclComCd: z.string().nullish(), splmntArtclCdNm: z.string().nullish(), splmntDmndCn: z.string().optional(), splmntDmndDt: z.string().optional(), diff --git a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx index adf563c65..a52c9776d 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx @@ -225,7 +225,9 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps - +
+ +
{initialData?.detDto.instlSncnDocNo && ( 증감 - (C-B) + (C-A) % diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx index fe29ccaa7..4c267b9a0 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-form-page.tsx @@ -242,7 +242,9 @@ function SewerBscPlanFormPage({ - +
+ +
{initialData?.detDto?.sewerBscPlanDocNo && ( ; + onAdd: (files: FileList | null) => void; + onRemove?: (index: number) => void; + accept?: string; + multiple?: boolean; + addDisabled?: boolean; + showAddButton?: boolean; +} + +function AttachmentFileField({ + files, + inputId, + isUploadingByKey, + onAdd, + onRemove, + accept, + multiple = true, + addDisabled = false, + showAddButton = true, +}: AttachmentFileFieldProps) { + return ( +
+ {files.length > 0 ? ( +
+ {files.map((file, index) => ( + onRemove(index) : undefined} + /> + ))} +
+ ) : null} + {showAddButton ? ( + + ) : null} +
+ ); +} + +export { AttachmentFileField }; +export type { AttachmentFileFieldItem, AttachmentFileFieldProps }; diff --git a/apps/as/src/shared/ui/file-add-button.tsx b/apps/as/src/shared/ui/file-add-button.tsx new file mode 100644 index 000000000..36c41406e --- /dev/null +++ b/apps/as/src/shared/ui/file-add-button.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Button, cn } from "@common/components"; + +interface FileAddButtonProps { + inputId: string; + onFilesSelected: (files: FileList | null) => void; + accept?: string; + multiple?: boolean; + disabled?: boolean; + label?: string; +} + +function FileAddButton({ + inputId, + onFilesSelected, + accept, + multiple = true, + disabled = false, + label = "파일추가", +}: FileAddButtonProps) { + return ( + + ); +} + +export { FileAddButton }; +export type { FileAddButtonProps }; diff --git a/apps/as/src/shared/ui/index.ts b/apps/as/src/shared/ui/index.ts index 813bcafd5..71cd9a481 100644 --- a/apps/as/src/shared/ui/index.ts +++ b/apps/as/src/shared/ui/index.ts @@ -1,3 +1,7 @@ +export type { AttachmentFileFieldItem, AttachmentFileFieldProps } from "./attachment-file-field"; +export { AttachmentFileField } from "./attachment-file-field"; +export type { FileAddButtonProps } from "./file-add-button"; +export { FileAddButton } from "./file-add-button"; export { NoData } from "./no-data"; export { PageLayout } from "./page-layout"; export { SectionCollapsible } from "./section-collapsible"; @@ -7,3 +11,5 @@ export { } from "./section-collapsible-group"; export type { TextShimmerProps } from "./text-shimmer"; export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer"; +export type { UploadedFileChipProps } from "./uploaded-file-chip"; +export { UploadedFileChip } from "./uploaded-file-chip"; diff --git a/apps/as/src/shared/ui/uploaded-file-chip.tsx b/apps/as/src/shared/ui/uploaded-file-chip.tsx new file mode 100644 index 000000000..a7eb6fa22 --- /dev/null +++ b/apps/as/src/shared/ui/uploaded-file-chip.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Paperclip, X } from "lucide-react"; + +import { TextShimmer } from "./text-shimmer"; + +interface UploadedFileChipProps { + fileName: string; + isUploading?: boolean; + onRemove?: () => void; +} + +function UploadedFileChip({ fileName, isUploading = false, onRemove }: UploadedFileChipProps) { + return ( +
+ + + {fileName} + + {onRemove ? ( + + ) : null} +
+ ); +} + +export { UploadedFileChip }; +export type { UploadedFileChipProps }; diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx index 27d209d39..39123f09d 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx @@ -1,11 +1,17 @@ "use client"; -import { Download, Fullscreen, Save } from "lucide-react"; +import { useId } from "react"; -import { Button } from "@common/components"; +import { Download, FolderOpen, Fullscreen, Save } from "lucide-react"; + +import { Button, cn } from "@common/components"; + +const WEBHYP_DEV_MODE = process.env.NEXT_PUBLIC_WEBHYP_DEV_MODE === "true"; interface HangulGiangiToolbarProps { isBusy?: boolean; + canSave?: boolean; + onOpen: (file: File) => void; onSave: () => void; onDownload: () => void; onFullscreen: () => void; @@ -13,16 +19,58 @@ interface HangulGiangiToolbarProps { function HangulGiangiToolbar({ isBusy = false, + canSave = true, + onOpen, onSave, onDownload, onFullscreen, }: HangulGiangiToolbarProps) { + const openInputId = useId(); + return (
- + {WEBHYP_DEV_MODE && ( + + )} + {canSave && ( + + )} -
- ); - })} -
- )} -
- -
+ ({ + fileKey: item.fileKey, + fileName: item.file?.name ?? item.fileName, + }))} + inputId={`${file.esntlAtchFileId}-file-input`} + isUploadingByKey={isUploading} + accept={toFileAccept(file.esntlAtchFileRmrk) || undefined} + onAdd={(fileList) => + void handleEssentialFileUpload(file.esntlAtchFileId, fileList) + } + onRemove={(index) => + void removeEssentialFile(file.esntlAtchFileId, index) + } + /> ); @@ -495,55 +456,22 @@ function AttachmentsFormSection({

기타 첨부파일

- + void handleEtcFileUpload(files)} + />
{etcFiles.length > 0 ? (
- {etcFiles.map((item, idx) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - {item.file?.name ?? item.fileName} - - -
- ); - })} + {etcFiles.map((item, idx) => ( + void removeEtcFile(idx)} + /> + ))}
) : (
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx index 969a4613b..22aa35c82 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/business-cost-form-section.tsx @@ -18,7 +18,7 @@ import { } from "@common/components"; import { buildAcntgCostItems, recalculateBizCstTotals } from "../lib/business-cost-form-utils"; -import { calcDiffPercent } from "../lib/format-utils"; +import { calcDiffPercent } from "../lib/format-utils"; interface BusinessCostFormData { bizCstList: Array<{ @@ -95,7 +95,7 @@ function BusinessCostFormSection() { 증감 - (C-B) + (C-A) % diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx index 95aaaede2..752898721 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx @@ -2,10 +2,8 @@ import { useEffect, useId, useState } from "react"; -import { Paperclip, X } from "lucide-react"; - import { getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; +import { AttachmentFileField } from "@/shared/ui"; import { attachService } from "@/entities/attach"; import { CodeSelect } from "@/entities/code"; import { @@ -287,57 +285,17 @@ function SplmntDmndDialog({ /> - {row.uploadedFiles.length > 0 ? ( -
- {row.uploadedFiles.map((file, fileIndex) => { - const uploading = isUploading[file.fileKey] ?? false; - - return ( -
- - - {file.fileName} - - -
- ); - })} -
- ) : null} - + ({ + fileKey: file.fileKey, + fileName: file.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + addDisabled={isRowUploading} + onAdd={(fileList) => void handleFileUpload(row.rowKey, fileList)} + onRemove={(fileIndex) => void handleFileRemove(row.rowKey, fileIndex)} + />
{rows.length > 1 ? ( diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx index a8f38ecb4..eea7fb7af 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx @@ -3,15 +3,13 @@ import { useEffect, useId, useRef, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { Paperclip, X } from "lucide-react"; +import { Paperclip } from "lucide-react"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { SectionCollapsible } from "@/shared/ui"; +import { AttachmentFileField, SectionCollapsible } from "@/shared/ui"; import { attachService } from "@/entities/attach"; import { Badge, - Button, Table, TableBody, TableCell, @@ -303,63 +301,26 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) )} - {rowFiles.length > 0 ? ( -
- {rowFiles.map((file, fileIndex) => { - const uploading = isUploading[file.fileKey] ?? false; - - return ( -
- - - {file.fileName} - - {!isRowReadOnly ? ( - - ) : null} -
- ); - })} -
- ) : null} - {!isRowReadOnly ? ( - - ) : rowFiles.length === 0 ? ( + {isRowReadOnly && rowFiles.length === 0 ? ( - - ) : null} + ) : ( + ({ + fileKey: file.fileKey, + fileName: file.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + addDisabled={isRowUploading} + showAddButton={!isRowReadOnly} + onAdd={(fileList) => void handleFileUpload(index, fileList)} + onRemove={ + isRowReadOnly + ? undefined + : (fileIndex) => void handleFileRemove(index, fileIndex) + } + /> + )}