From bda556b808b0f968ec4d93eef81268c62dafb7a8 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 26 Oct 2026 18:38:00 +0900 Subject: [PATCH] =?UTF-8?q?[as=20fr]=20=ED=95=98=EC=88=98=EB=8F=84=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=20=EB=AC=B4=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=B3=91=EB=A0=AC=20Join=20=EA=B2=B0=EC=9E=AC?= =?UTF-8?q?=EB=A1=9C=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/sewer-bsc-plan/lib/button-config.ts | 66 +++++++++- .../lib/sewer-bsc-plan-regist-schema.ts | 16 +++ .../ui/components/floating-action-buttons.tsx | 120 ++++++++++++++++-- .../ui/sewer-bsc-plan-view-page.tsx | 5 +- 4 files changed, 189 insertions(+), 18 deletions(-) diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts b/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts index 062dcf7e1..d6b72beb4 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/button-config.ts @@ -54,14 +54,49 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< }, }; +/** + * Fork-Join (병렬 결재 6인 체제: 관로2 + 시설2 + 정책병합2) 전용 상태별 버튼 액션 매핑 + * - Stage 2: 관로/시설 실무검토자(ATRZ02), 부서승인자(ATRZ03) + * - Stage 3: 정책총괄 종합병합자(ATRZ04), 최종회신자(ATRZ99) + * - 하수도기본계획은 100% 무조건 Fork-Join 결재 라인으로 동작 + */ +const ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + ER: { + ATRZ01: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dsgntRvwr], + }, + TE: { + ATRZ02: [ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt, + ], + ATRZ03: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply], + ATRZ04: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv], + ATRZ99: [SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply], + }, + TP: {}, +}; + function resolveButtonConfigs( actions: SewerBscPlanViewButtonAction[], ): SewerBscPlanViewButtonAction[] { return actions.map((action) => SEWER_BSC_PLAN_VIEW_BUTTON_ACTION[action]); } -interface GetButtonConfigsOptions { +const PENDING_APPROVAL_ACTIONS = new Set([ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply, +]); + +export interface GetButtonConfigsOptions { hasPendingApprovalRequest?: boolean; + isForkJoin?: boolean; + branchSeCd?: string; + branchSttsCd?: string; } function resolveRoleUser01AtrzButtonActions( @@ -69,11 +104,28 @@ function resolveRoleUser01AtrzButtonActions( atrzSeComCdList: string[], options?: GetButtonConfigsOptions, ): SewerBscPlanViewButtonAction[] { - if (prgrsSttsComCd === "TE" && options?.hasPendingApprovalRequest === false) { - return []; + let effectiveStatus = prgrsSttsComCd; + const isForkJoin = options?.isForkJoin ?? true; + + if (isForkJoin && options?.branchSttsCd) { + switch (options.branchSttsCd) { + case "SUPPLEMENT": + // 본인 브랜치가 보완요청 중이면 검토/승인 버튼 숨김 + return []; + case "COMPLETED": + // 본인 브랜치 승인 완료 후 타 브랜치 대기 중이면 검토/승인 버튼 숨김 + return []; + case "WAITING": + return []; + case "IN_PROGRESS": + effectiveStatus = "TE"; + break; + } } - const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; + const atrzStatusMap = isForkJoin + ? ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS[effectiveStatus] + : ROLE_USER01_ATRZ_STATUS_ACTIONS[effectiveStatus]; if (!atrzStatusMap || atrzSeComCdList.length === 0) return []; const seen = new Set(); @@ -90,7 +142,11 @@ function resolveRoleUser01AtrzButtonActions( } } - return actions; + return actions.filter((action) => { + const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); + if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false; + return true; + }); } export function needsRoleUser01AtrzResolve( diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts index 9d2dfc30a..e47555f81 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/sewer-bsc-plan-regist-schema.ts @@ -101,3 +101,19 @@ export const sewerBscPlanRegistSchema = z.object({ }); export type SewerBscPlanRegistFormData = z.infer; + +/** + * 하수도기본계획 전자결재 전용 공종코드 정규화 헬퍼 + * 하수도기본계획은 변경구분(CP, AP, MP)과 무관하게 상시 관로/시설/총괄 병렬 6인 결재 대상 + */ +export function normalizeTaskSeComCdForElapr(_taskSeComCd?: string | null): string { + return "SF_SP"; +} + +/** + * 전자결재 복합병렬결재(Fork-Join) 대상 업무 여부 판단 + * 하수도기본계획은 100% 무조건 Fork-Join 대상 + */ +export function isForkJoinTask(_taskSeComCd?: string | null): boolean { + return true; +} diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/components/floating-action-buttons.tsx index 364de08b9..2feaec62f 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/components/floating-action-buttons.tsx @@ -17,6 +17,7 @@ import { type ElaprTmpltPickerConfirmPayload, ElaprTmpltPickerDialog, hasCurrentUserPendingApprovalRequest, + useElaprBranchesByDocNo, useElaprDocPrgrsByDocNo, } from "@/entities/elapr"; import { instlSncnService } from "@/entities/instl-sncn"; @@ -42,6 +43,10 @@ import { buildTechReviewReplyPayload, toSplmntDmndSaveItem, } from "../../lib/payload-utils"; +import { + isForkJoinTask, + normalizeTaskSeComCdForElapr, +} from "../../lib/sewer-bsc-plan-regist-schema"; import { type ChecklistFormState } from "../../model/view-types"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; @@ -85,31 +90,92 @@ function FloatingActionButtons({ const docNo = data.detDto?.sewerBscPlanDocNo ?? sewerBscPlanDocNo; const isRegister = data.detDto?.rgtr === userInfo?.userUnqId; + // 하수도기본계획은 100% 무조건 Fork-Join (6인 체제) + const isForkJoin = isForkJoinTask(data.detDto?.taskSeComCd); + const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null); + const { data: elaprBranchList = [], isLoading: isElaprBranchLoading } = + useElaprBranchesByDocNo(needsTeApprovalCheck && isForkJoin ? docNo : null); const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( elaprDocPrgrsList, userInfo?.userUnqId, ); + const currentUserPrgrsRow = elaprDocPrgrsList.find( + (row) => row.aprvrUuid?.toLowerCase() === userInfo?.userUnqId?.toLowerCase(), + ); + const userPendingPrgrs = elaprDocPrgrsList.find( + (row) => + row.aprvrUuid?.toLowerCase() === userInfo?.userUnqId?.toLowerCase() && + row.atrzPrgrsSttsComCd === "ATRZ_PRGRS_002", + ); + const userBranchId = userPendingPrgrs?.branchId ?? currentUserPrgrsRow?.branchId; + const userBranchSeCd = (userPendingPrgrs?.branchSeCd ?? currentUserPrgrsRow?.branchSeCd) as + | "SP" + | "SF" + | "TOTAL_AGG" + | "MAIN" + | undefined; + const userPendingBranchSeCd = userPendingPrgrs?.branchSeCd ?? userBranchSeCd; + const userPendingAtrzSeComCd = userPendingPrgrs?.atrzSeComCd; + + const effectiveAtrzSeComCdList = useMemo(() => { + if (isForkJoin && userPendingAtrzSeComCd) { + return [userPendingAtrzSeComCd]; + } + if (isForkJoin && currentUserPrgrsRow?.atrzSeComCd) { + return [currentUserPrgrsRow.atrzSeComCd]; + } + return atrzSeComCdList; + }, [atrzSeComCdList, currentUserPrgrsRow?.atrzSeComCd, isForkJoin, userPendingAtrzSeComCd]); + + const userBranchSttsCd = useMemo(() => { + if (!isForkJoin || !userBranchSeCd) return undefined; + + const matchedBranch = userBranchId + ? elaprBranchList.find((b) => b.branchId === userBranchId) + : elaprBranchList.findLast((b) => b.branchSeCd === userBranchSeCd); + if (matchedBranch?.branchSttsCd) { + return matchedBranch.branchSttsCd; + } + + if (prgrsSttsComCd === "TE") { + return "IN_PROGRESS"; + } + if (prgrsSttsComCd === "TP") { + return "COMPLETED"; + } + + return undefined; + }, [elaprBranchList, isForkJoin, prgrsSttsComCd, userBranchId, userBranchSeCd]); + const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; - return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { + if (isForkJoin && isElaprBranchLoading) return []; + return getButtonConfigs(dtlInstCd, prgrsSttsComCd, effectiveAtrzSeComCdList, { hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + isForkJoin, + branchSeCd: userBranchSeCd, + branchSttsCd: userBranchSttsCd, }); }, [ - atrzSeComCdList, dtlInstCd, + effectiveAtrzSeComCdList, hasPendingApprovalRequest, + isElaprBranchLoading, isElaprDocPrgrsLoading, + isForkJoin, isRvwLoginUserLoading, needsAtrzResolve, needsTeApprovalCheck, prgrsSttsComCd, + userBranchSeCd, + userBranchSttsCd, ]); const canEdit = @@ -254,6 +320,9 @@ function FloatingActionButtons({ dmndCn: data.activeDtlDto?.bizOtlnCn ?? "", elaprCycl: 1, docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, + taskSeComCd: "SF_SP", + forkJoinSeCd: "FORK_JOIN", + crntStageNo: 2, elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), }; await executeAction({ @@ -264,6 +333,9 @@ function FloatingActionButtons({ await queryClient.invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo], }); + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH, "findBranchesByDocNo", docNo], + }); } async function handleRvwStrgClick() { @@ -389,16 +461,33 @@ function FloatingActionButtons({ 보완요청 )} - {shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv) && ( - - )} - {shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply) && ( - - )} + {shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv) && + (!isForkJoin || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && ( + + )} + {shouldShowButton(SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply) && + (!isForkJoin || + (userPendingBranchSeCd !== "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ03") || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ99")) && ( + + )} @@ -406,6 +495,7 @@ function FloatingActionButtons({ open={activeDialog === "elaprTmplt"} onClose={() => setActiveDialog(null)} docSeComCd={docSeComCd} + taskSeComCd="SF_SP" onConfirm={handleElaprTmpltConfirm} /> @@ -424,6 +514,12 @@ function FloatingActionButtons({ currentUserUuid={userInfo?.userUnqId ?? ""} onSuccess={() => { queryClient.invalidateQueries({ queryKey: sewerBscPlanKeys.all }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo], + }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH, "findBranchesByDocNo", docNo], + }); }} /> diff --git a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx index 2b9048280..c57287d0f 100644 --- a/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx +++ b/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx @@ -127,7 +127,10 @@ function SewerBscPlanViewPage({ id, propChgCycl }: SewerBscPlanViewPageProps) { /> - +