From 380575dae0da885ecadffaf952b6081dafde6784 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 16 Sep 2026 14:19:39 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=8B=A4=EB=AC=B4=ED=98=91?= =?UTF-8?q?=EC=9D=98=EC=84=9C=20=EA=B2=80=ED=86=A0=20=EB=A1=9C=EC=A7=81(RR?= =?UTF-8?q?,=20RK)=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/pwrk-cnsltn/ref-frontend-entities.md | 2 + .../as/docs/pwrk-cnsltn/ref-screen-buttons.md | 16 +++-- .../elapr/ui/elapr-approval-dialog.tsx | 22 ++++-- .../pages/fnrs-cnsltn/lib/button-config.ts | 43 ++++++++--- .../fnrs-cnsltn/lib/review-step-utils.ts | 5 +- .../ui/components/floating-action-buttons.tsx | 71 +++++++++++-------- 6 files changed, 103 insertions(+), 56 deletions(-) diff --git a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md index ddb0f5f3d..fdb817e08 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md +++ b/apps/as/docs/pwrk-cnsltn/ref-frontend-entities.md @@ -346,3 +346,5 @@ To-Be: - 검토자지정은 **`ER`(1차)** 과 **`EV`(2차)** 에서. - `ER`: `elaprCycl: 1`, `docPrgrsSttsComCd: "AC"`, 서식 선택 다이얼로그 - `EV`: `elaprCycl: 2`, `docPrgrsSttsComCd: "RK"`, 1차 결재선 시드 다이얼로그(서식 없음) + +`RK` / ATRZ02 FAB: `findDocPrgrsByDocNo` 전체 조회 후 pending은 `elaprCycl === 2`만 사용. 검토완료 후 `ElaprApprovalDialog`에 `elaprCycl={2}`. 보완요청은 TE와 동일 `SplmntDmndDialog`. diff --git a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md index 1c9d8565d..c0b7d2a08 100644 --- a/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md +++ b/apps/as/docs/pwrk-cnsltn/ref-screen-buttons.md @@ -70,7 +70,7 @@ API 컬럼은 [ref-endpoint.md](./ref-endpoint.md)와 동일. | 33 | 상세 | USER01 | 검토자 | `RK` | 검토완료 | `RK` | 다음 결재자 | `process` | | 34 | 상세 | USER01 | 승인자 | `RK` | 결재(승인) | `RK` | 다음 결재자 | `process` | | 35 | 상세 | USER01 | 최종승인자 | `RK` | 결재(회신) | `RR` | 환경청 | `process` 최종 | -| 36 | 상세 | USER03 | - | `RR` | 결과회신 | `EP` | 광역 | `updatePrgrsStts` | +| 36 | 상세 | USER03 | - | `RR` | 실무협의서 관리(조회), 검토결과 회신 | `EP` | 광역 | `tech-review` | | 37 | 작성/상세 | USER04 | - | `EP` | 결과회신 | `TA` | 기초 | `updatePrgrsStts` | | 38 | 상세 | USER05 | - | `TA` | 결과회신 | — | — | **API 없음. 조회** | @@ -121,7 +121,7 @@ ROLE_USER03 LE → 결과회신(자체종결), 기술검토 의뢰 TP → 재원협의 결과 회신 RA → 실무협의서 관리(조회), 조치결과 검토의뢰 - RR → 결과회신 + RR → 실무협의서 관리(조회), 검토결과 회신 (`tech-review` → `EP`) ``` ### 3-2. 공단 결재선 × 상태 @@ -141,14 +141,18 @@ TE / ATRZ03 승인자 → 결재(승인) TE / ATRZ99 최종 → 결재(회신) RK / ATRZ02 검토자 - → 실무협의서관리, 금액검증, 저장, 검토완료, 보완요청 -RK / ATRZ03 → 결재(승인) -RK / ATRZ99 → 결재(회신) + → (초안 완료) 실무협의서관리 + → (초안 완료 + 결재 대기 ATRZ_PRGRS_002, cycl2) 보완요청, 검토완료 + · 보완요청 UI: TE와 동일 SplmntDmndDialog +RK / ATRZ03 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(승인) +RK / ATRZ99 → (초안 완료) 실무협의서관리, (cycl2 pending) 결재(회신) + · 결재 UI: ElaprApprovalDialog (elaprCycl=2) 승인/반려 → PUT process ``` `FV`, `AC`, `AV`: 액션 빈 배열 + 진행 패널. -현행 `TE`에서 `hasPendingApprovalRequest === false`이면 버튼을 숨긴다. v3.0에서는 검토자 버튼(저장·웹기안기·보완)은 결재 대기와 무관하게 두고, **검토완료/결재**만 결재건 존재 여부에 묶는다. +`PENDING_APPROVAL_ACTIONS`: `dmndSplmnt`, `rvwCmptn`, `atrzAprv`, `atrzRply`만. +`pwrkCnsltnDocMng`는 결재 대기와 무관(초안 완료만). ATRZ02·ATRZ03·ATRZ99라도 pending 없으면 관리만 노출. --- diff --git a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index ad9abfa3e..0f7cfd056 100644 --- a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; @@ -48,6 +48,7 @@ interface ElaprApprovalDialogProps { onOpenChange: (open: boolean) => void; elaprDocNo: string; currentUserUuid: string; + elaprCycl?: number; onSuccess: () => void; } @@ -58,6 +59,7 @@ function ElaprApprovalDialog({ onOpenChange, elaprDocNo, currentUserUuid, + elaprCycl, onSuccess, }: ElaprApprovalDialogProps) { const queryClient = useQueryClient(); @@ -67,9 +69,15 @@ function ElaprApprovalDialog({ const [pickerOpen, setPickerOpen] = useState(false); const [pickingRowKey, setPickingRowKey] = useState(null); - const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null); + const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null, { + elaprCycl, + }); + const cyclDocPrgrsList = useMemo(() => { + if (elaprCycl == null) return elaprDocPrgrsList; + return elaprDocPrgrsList?.filter((row) => Number(row.elaprCycl) === elaprCycl); + }, [elaprCycl, elaprDocPrgrsList]); - const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId; + const elaprDmndId = cyclDocPrgrsList?.[0]?.elaprDmndId; const { rows, @@ -85,7 +93,7 @@ function ElaprApprovalDialog({ markDeleted, reorderRows, } = usePrgrsSttsRows({ - serverRows: elaprDocPrgrsList, + serverRows: cyclDocPrgrsList, elaprDmndId, }); @@ -93,14 +101,14 @@ function ElaprApprovalDialog({ const sensors = useDndSensors(); const currentUserPrgrs = findCurrentUserPendingApprovalRequest( - elaprDocPrgrsList, + cyclDocPrgrsList, currentUserUuid, ); const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId); const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined; const isBusy = isProcessing || savePrgrsSttsMutation.isPending; - const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, elaprDocPrgrsList ?? []); + const isPrgrsSttsUnsaved = hasUnsavedPrgrsSttsChanges(rows, cyclDocPrgrsList ?? []); function handleDragEnd(event: DragEndEvent) { const { active, over } = event; @@ -164,7 +172,7 @@ function ElaprApprovalDialog({ return; } - const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []); + const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, cyclDocPrgrsList ?? []); if (payload.length === 0) { alert("변경된 결재선이 없습니다."); return; 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 92bf5b83e..cb705cffe 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -53,10 +53,14 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record([ - ...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, + FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply, ]); @@ -129,15 +146,19 @@ function resolveRoleUser01AtrzButtonActions( 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 || - action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq - ) { - return options.hasCompletedDraft; + if (options?.hasCompletedDraft != null) { + if (prgrsSttsComCd === "TE" || prgrsSttsComCd === "RK") { + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) { + return options.hasCompletedDraft; + } } - if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { - return !options.hasCompletedDraft; + if (prgrsSttsComCd === "TE") { + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) { + return options.hasCompletedDraft; + } + if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { + return !options.hasCompletedDraft; + } } } diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts index bce365857..0ba84b926 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/review-step-utils.ts @@ -32,7 +32,10 @@ export function getEditableReviewStep( dtlInstCd?: string, prgrsSttsComCd?: string, ): ReviewStepCode | undefined { - if (dtlInstCd === "ROLE_USER03" && (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP")) { + if ( + dtlInstCd === "ROLE_USER03" && + (prgrsSttsComCd === "LE" || prgrsSttsComCd === "TP" || prgrsSttsComCd === "RR") + ) { return "ENV"; } 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 83a38980d..187a72fa3 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 @@ -113,30 +113,37 @@ function FloatingActionButtons({ const isRegister = data.detDto?.rgtr === userInfo?.userUnqId; const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); - const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; + const needsElaprPrgrsCheck = needsAtrzResolve && ["TE", "RK"].includes(prgrsSttsComCd); 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); + useElaprDocPrgrsByDocNo(needsElaprPrgrsCheck ? docNo : null); + const { data: pwrkCnsltnData, isLoading: isPwrkCnsltnLoading } = usePwrkCnsltnMngByUpDocNo( docNo, needsPwrkLookup, ); + const hasCompletedDraft = Boolean(findCompletedPwrkDraft(pwrkCnsltnData)); const pwrkCnsltnDocNo = pwrkCnsltnData?.pwrkCnsltnDocNo; + const elaprPrgrsForPending = + prgrsSttsComCd === "RK" + ? elaprDocPrgrsList.filter((row) => Number(row.elaprCycl) === 2) + : elaprDocPrgrsList; const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( - elaprDocPrgrsList, + elaprPrgrsForPending, userInfo?.userUnqId, ); const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; - if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; + if (needsElaprPrgrsCheck && isElaprDocPrgrsLoading) return []; if (needsPwrkDraftCheck && isPwrkCnsltnLoading) return []; return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { - hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + hasPendingApprovalRequest: needsElaprPrgrsCheck ? hasPendingApprovalRequest : undefined, hasCompletedDraft: needsPwrkDraftCheck ? hasCompletedDraft : undefined, }); }, [ @@ -148,7 +155,7 @@ function FloatingActionButtons({ isPwrkCnsltnLoading, isRvwLoginUserLoading, needsAtrzResolve, - needsTeApprovalCheck, + needsElaprPrgrsCheck, needsPwrkDraftCheck, prgrsSttsComCd, ]); @@ -336,11 +343,12 @@ function FloatingActionButtons({ return; } + const isResultReply = prgrsSttsComCd === "RR"; const payload = buildTechReviewReplyPayload( data, checklistFormState, "ENV", - "RM", + isResultReply ? "EP" : "RM", "CP", getReviewOpinionFormData(), opnrNm, @@ -349,7 +357,9 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, request: payload, - successMessage: "재원협의 결과 회신이 완료되었습니다.", + successMessage: isResultReply + ? "검토결과 회신이 완료되었습니다." + : "재원협의 결과 회신이 완료되었습니다.", }); techReviewOpinionRef?.current?.resetForm(); } @@ -432,15 +442,17 @@ function FloatingActionButtons({ } async function handleRvwCmptnClick() { - const { percent } = computeReviewStepChecklistProgress( - data.cklstResultList ?? [], - checklistFormState.results, - "KECO", - ); + if (prgrsSttsComCd === "TE") { + const { percent } = computeReviewStepChecklistProgress( + data.cklstResultList ?? [], + checklistFormState.results, + "KECO", + ); - if (percent !== 100) { - alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다."); - return; + if (percent !== 100) { + alert("모든 체크리스트 항목을 체크해야 검토완료가 가능합니다."); + return; + } } const payload = { @@ -448,7 +460,7 @@ function FloatingActionButtons({ data, checklistFormState, "KECO", - "TE", + prgrsSttsComCd, "SV", getReviewOpinionFormData(), opnrNm, @@ -585,7 +597,7 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( @@ -698,6 +710,7 @@ function FloatingActionButtons({ onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)} elaprDocNo={data.detDto?.fnrsCnsltnDocNo ?? fnrsCnsltnDocNo} currentUserUuid={userInfo?.userUnqId ?? ""} + elaprCycl={prgrsSttsComCd === "RK" ? 2 : undefined} onSuccess={() => { queryClient.invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo), @@ -709,7 +722,13 @@ function FloatingActionButtons({ open={activeDialog === "pwrkCnsltnDocMng"} fnrsCnsltnDocNo={docNo} atrzSeComCdList={atrzSeComCdList} - canSave={prgrsSttsComCd === "RA" ? false : undefined} + canSave={ + prgrsSttsComCd === "RA" || + prgrsSttsComCd === "RR" || + (needsElaprPrgrsCheck && !hasPendingApprovalRequest) + ? false + : undefined + } onClose={() => setActiveDialog(null)} /> @@ -840,16 +859,6 @@ async function handleActnCmptn( } } -async function handleAtrzAprv(context: ActionHandlerContext): Promise { - // TODO: 결재 승인 처리 API 호출 - context.onSuccess?.(); -} - -async function handleAtrzRply(context: ActionHandlerContext): Promise { - // TODO: 결재 회신 처리 API 호출 - context.onSuccess?.(); -} - const buttonActionHandlers: Record = { strg: handleSaveApply, aply: handleSaveApply, @@ -869,8 +878,8 @@ const buttonActionHandlers: Record = rvwStrg: handleTechReview, rvwCmptn: handleTechReview, dmndSplmnt: handleDmndSplmnt, - atrzAprv: handleAtrzAprv, - atrzRply: handleAtrzRply, + atrzAprv: () => Promise.resolve(), + atrzRply: () => Promise.resolve(), actnCmptn: handleActnCmptn, rqstActnRvw: handleRequestActnReview, };