From 1dd86b200874de0883b2c203faed22bf422371ac Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 26 Oct 2026 18:38:27 +0900 Subject: [PATCH] =?UTF-8?q?[as=20fr]=20=EC=84=A4=EA=B3=84=EC=9E=90?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EB=B3=91=EB=A0=AC=EC=A1=B0=EC=9D=B8=20?= =?UTF-8?q?=EA=B2=B0=EC=9E=AC=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/design-cnstn/lib/button-config.ts | 62 ++++++++- .../lib/design-cnstn-regist-schema.ts | 23 ++++ .../ui/components/floating-action-buttons.tsx | 126 ++++++++++++++++-- .../design-cnstn/ui/design-view-page.tsx | 6 +- 4 files changed, 198 insertions(+), 19 deletions(-) diff --git a/apps/as/src/pages/design-cnstn/lib/button-config.ts b/apps/as/src/pages/design-cnstn/lib/button-config.ts index 2727092f2..0f01d4f0d 100644 --- a/apps/as/src/pages/design-cnstn/lib/button-config.ts +++ b/apps/as/src/pages/design-cnstn/lib/button-config.ts @@ -43,14 +43,48 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record< }, }; +/** + * Fork-Join (병렬 결재 6인 체제: 관로2 + 시설2 + 정책병합2) 전용 상태별 버튼 액션 매핑 + * - Stage 2: 관로/시설 실무검토자(ATRZ02), 부서승인자(ATRZ03) + * - Stage 3: 정책총괄 종합병합자(ATRZ04), 최종회신자(ATRZ99) + * - 일반 순차 결재 라인과 분리하여 독립적으로 관리 + */ +const ROLE_USER01_FORK_JOIN_ATRZ_STATUS_ACTIONS: Record< + string, + Record +> = { + CT: { + ATRZ01: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.rqstRvw], + }, + CS: { + ATRZ02: [ + DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwStrg, + DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn, + ], + ATRZ03: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply], // Stage 2 부서승인 ➔ 병합에 회신 + ATRZ04: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv], // Stage 3 정책총괄 ➔ 병합 + ATRZ99: [DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply], // Stage 3 정책총괄 ➔ 최종 회신 + }, + CP: {}, +}; + function resolveButtonConfigs( actions: DesignCnstnViewButtonAction[], ): DesignCnstnViewButtonAction[] { return actions.map((action) => DESIGN_CNSTN_VIEW_BUTTON_ACTION[action]); } -interface GetButtonConfigsOptions { +const PENDING_APPROVAL_ACTIONS = new Set([ + DESIGN_CNSTN_VIEW_BUTTON_ACTION.rvwCmptn, + DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv, + DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply, +]); + +export interface GetButtonConfigsOptions { hasPendingApprovalRequest?: boolean; + isForkJoin?: boolean; + branchSeCd?: string; + branchSttsCd?: string; } function resolveRoleUser01AtrzButtonActions( @@ -58,11 +92,25 @@ function resolveRoleUser01AtrzButtonActions( atrzSeComCdList: string[], options?: GetButtonConfigsOptions, ): DesignCnstnViewButtonAction[] { - if (prgrsSttsComCd === "CS" && options?.hasPendingApprovalRequest === false) { - return []; + let effectiveStatus = prgrsSttsComCd; + + if (options?.isForkJoin && options?.branchSttsCd) { + switch (options.branchSttsCd) { + case "SUPPLEMENT": + return []; + case "COMPLETED": + return []; + case "WAITING": + return []; + case "IN_PROGRESS": + effectiveStatus = "CS"; + break; + } } - const atrzStatusMap = ROLE_USER01_ATRZ_STATUS_ACTIONS[prgrsSttsComCd]; + const atrzStatusMap = options?.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(); @@ -79,7 +127,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/design-cnstn/lib/design-cnstn-regist-schema.ts b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts index a5dda2d38..a0218d61c 100644 --- a/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts +++ b/apps/as/src/pages/design-cnstn/lib/design-cnstn-regist-schema.ts @@ -62,3 +62,26 @@ export function parseTaskSeComCdFromApi(taskSeComCd?: string | null): string[] { if (!taskSeComCd) return []; return taskSeComCd.split(",").filter(Boolean); } + +/** + * 설계자문 저장 시 코드가 "SF,SP", "SP,SF" 등의 복수 코드로 분리 저장된 경우, + * 전자결재 복합병렬결재(Fork-Join) 화면 및 프로세스 규격에 맞춰 "SF_SP"로 정규화합니다. + */ +export function normalizeTaskSeComCdForElapr(taskSeComCd?: string | null): string { + if (!taskSeComCd) return ""; + const trimmed = taskSeComCd.trim(); + if (trimmed === "SF_SP") return "SF_SP"; + + const codes = trimmed.split(",").map((c) => c.trim()).filter(Boolean); + if (codes.includes("SF") && codes.includes("SP")) { + return "SF_SP"; + } + return trimmed; +} + +/** + * 전자결재 복합병렬결재(Fork-Join) 대상 업무 여부 판단 + */ +export function isForkJoinTask(taskSeComCd?: string | null): boolean { + return normalizeTaskSeComCdForElapr(taskSeComCd) === "SF_SP"; +} diff --git a/apps/as/src/pages/design-cnstn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/design-cnstn/ui/components/floating-action-buttons.tsx index 94088f228..025fcb98f 100644 --- a/apps/as/src/pages/design-cnstn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/design-cnstn/ui/components/floating-action-buttons.tsx @@ -23,6 +23,7 @@ import { type ElaprTmpltPickerConfirmPayload, ElaprTmpltPickerDialog, hasCurrentUserPendingApprovalRequest, + useElaprBranchesByDocNo, useElaprDocPrgrsByDocNo, } from "@/entities/elapr"; import { Button } from "@common/components"; @@ -33,6 +34,10 @@ import { getButtonConfigs, needsRoleUser01AtrzResolve, } from "../../lib/button-config"; +import { + isForkJoinTask, + normalizeTaskSeComCdForElapr, +} from "../../lib/design-cnstn-regist-schema"; import { buildSavePayload, buildTechReviewPayload } from "../../lib/save-payload-utils"; import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section"; @@ -72,31 +77,92 @@ function FloatingActionButtons({ const docNo = data.detDto?.designCnstnDocNo ?? designCnstnDocNo; const isRegister = data.detDto?.rgtr === userInfo?.userUnqId; + const normalizedTaskSeComCd = normalizeTaskSeComCdForElapr(data.detDto?.taskSeComCd); + const isForkJoin = isForkJoinTask(data.detDto?.taskSeComCd); + const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); const needsCsApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "CS"; const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = useElaprDocPrgrsByDocNo(needsCsApprovalCheck ? docNo : null); + const { data: elaprBranchList = [], isLoading: isElaprBranchLoading } = + useElaprBranchesByDocNo(needsCsApprovalCheck && 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 === "CS") { + return "IN_PROGRESS"; + } + if (prgrsSttsComCd === "CP") { + return "COMPLETED"; + } + + return undefined; + }, [elaprBranchList, isForkJoin, prgrsSttsComCd, userBranchId, userBranchSeCd]); + const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; if (needsCsApprovalCheck && isElaprDocPrgrsLoading) return []; - return getButtonConfigs(dtlInstCd, prgrsSttsComCd, atrzSeComCdList, { + if (isForkJoin && isElaprBranchLoading) return []; + return getButtonConfigs(dtlInstCd, prgrsSttsComCd, effectiveAtrzSeComCdList, { hasPendingApprovalRequest: needsCsApprovalCheck ? hasPendingApprovalRequest : undefined, + isForkJoin, + branchSeCd: userBranchSeCd, + branchSttsCd: userBranchSttsCd, }); }, [ - atrzSeComCdList, dtlInstCd, + effectiveAtrzSeComCdList, hasPendingApprovalRequest, + isElaprBranchLoading, isElaprDocPrgrsLoading, + isForkJoin, isRvwLoginUserLoading, needsAtrzResolve, needsCsApprovalCheck, prgrsSttsComCd, + userBranchSeCd, + userBranchSttsCd, ]); const canEdit = @@ -191,6 +257,9 @@ function FloatingActionButtons({ dmndCn: data.detDto?.bizCn ?? "", elaprCycl: 1, docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd, + taskSeComCd: normalizedTaskSeComCd, + forkJoinSeCd: isForkJoin ? "FORK_JOIN" : "NONE", + crntStageNo: isForkJoin ? 2 : 1, elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows), }; await executeAction({ @@ -201,6 +270,11 @@ function FloatingActionButtons({ await queryClient.invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo], }); + if (isForkJoin) { + await queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH, "findBranchesByDocNo", docNo], + }); + } } async function handleRvwStrgClick() { @@ -281,16 +355,33 @@ function FloatingActionButtons({ 검토완료 )} - {shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv) && ( - - )} - {shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply) && ( - - )} + {shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzAprv) && + (!isForkJoin || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ04")) && ( + + )} + {shouldShowButton(DESIGN_CNSTN_VIEW_BUTTON_ACTION.atrzRply) && + (!isForkJoin || + (userPendingBranchSeCd !== "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ03") || + (userPendingBranchSeCd === "TOTAL_AGG" && userPendingAtrzSeComCd === "ATRZ99")) && ( + + )} @@ -298,18 +389,27 @@ function FloatingActionButtons({ open={activeDialog === "elaprTmplt"} onClose={() => setActiveDialog(null)} docSeComCd={docSeComCd} + taskSeComCd={normalizedTaskSeComCd} onConfirm={handleElaprTmpltConfirm} /> setActiveDialog(open ? "elaprApproval" : null)} - elaprDocNo={data.detDto?.designCnstnDocNo ?? designCnstnDocNo} + elaprDocNo={docNo} currentUserUuid={userInfo?.userUnqId ?? ""} onSuccess={() => { queryClient.invalidateQueries({ queryKey: designCnstnKeys.detail(designCnstnDocNo), }); + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", docNo], + }); + if (isForkJoin) { + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH, "findBranchesByDocNo", docNo], + }); + } }} /> diff --git a/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx b/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx index 906d9172c..28395247a 100644 --- a/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx +++ b/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx @@ -13,6 +13,7 @@ import { AttachmentsViewSection, ProgressStatusSection } from "@/widgets/pbadms- import { Button, cn, Skeleton } from "@common/components"; import { needsRoleUser01AtrzResolve } from "../lib/button-config"; +import { normalizeTaskSeComCdForElapr } from "../lib/design-cnstn-regist-schema"; import { getEditableReviewStep } from "../lib/review-step-utils"; import { PROGRESS_STATUS_COLUMNS } from "../model/columns"; @@ -108,7 +109,10 @@ function DesignViewPage({ id }: DesignViewPageProps) { /> - +