[as] feat: 실무협의서 검토 로직(RR, RK) 작업

This commit is contained in:
2026-09-16 14:19:39 +09:00
parent 5e54039028
commit 380575dae0
6 changed files with 103 additions and 56 deletions
@@ -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`.
+10 -6
View File
@@ -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 없으면 관리만 노출.
---
@@ -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<string | null>(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;
@@ -53,10 +53,14 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record<string, Record<string, FnrsCnsltnViewBu
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw,
],
RR: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply,
],
},
};
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "TE", "RK", "EV"] as const;
const ROLE_USER01_ATRZ_STATUS_CODES = ["ER", "EV", "TE", "RK"] as const;
const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
string,
@@ -84,7 +88,19 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
ATRZ99: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply],
},
RK: {
ATRZ02: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng],
ATRZ02: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt,
],
ATRZ03: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv,
],
ATRZ99: [
FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng,
FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply,
],
},
};
@@ -98,7 +114,8 @@ interface GetButtonConfigsOptions {
}
const PENDING_APPROVAL_ACTIONS = new Set<FnrsCnsltnViewButtonAction>([
...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;
}
}
}
@@ -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";
}
@@ -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) && (
<Button color="green" onClick={handleRvwRplyClick} disabled={isProcessing}>
재원협의 결과 회신
{prgrsSttsComCd === "RR" ? "검토결과 회신" : "재원협의 결과 회신"}
</Button>
)}
{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<void> {
// TODO: 결재 승인 처리 API 호출
context.onSuccess?.();
}
async function handleAtrzRply(context: ActionHandlerContext): Promise<void> {
// TODO: 결재 회신 처리 API 호출
context.onSuccess?.();
}
const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> = {
strg: handleSaveApply,
aply: handleSaveApply,
@@ -869,8 +878,8 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
rvwStrg: handleTechReview,
rvwCmptn: handleTechReview,
dmndSplmnt: handleDmndSplmnt,
atrzAprv: handleAtrzAprv,
atrzRply: handleAtrzRply,
atrzAprv: () => Promise.resolve(),
atrzRply: () => Promise.resolve(),
actnCmptn: handleActnCmptn,
rqstActnRvw: handleRequestActnReview,
};