diff --git a/apps/as/src/pages/instl-sncn/lib/button-config.ts b/apps/as/src/pages/instl-sncn/lib/button-config.ts index 826fa498a..3d014eeb6 100644 --- a/apps/as/src/pages/instl-sncn/lib/button-config.ts +++ b/apps/as/src/pages/instl-sncn/lib/button-config.ts @@ -44,7 +44,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record([ + INSTL_SNCN_VIEW_BUTTON_ACTION.rvwStrg, + INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, INSTL_SNCN_VIEW_BUTTON_ACTION.rvwCmptn, INSTL_SNCN_VIEW_BUTTON_ACTION.atrzAprv, INSTL_SNCN_VIEW_BUTTON_ACTION.atrzRply, diff --git a/apps/as/src/pages/instl-sncn/lib/review-step-utils.ts b/apps/as/src/pages/instl-sncn/lib/review-step-utils.ts index 7b50a9817..266f48d22 100644 --- a/apps/as/src/pages/instl-sncn/lib/review-step-utils.ts +++ b/apps/as/src/pages/instl-sncn/lib/review-step-utils.ts @@ -8,6 +8,7 @@ import { } from "../model/view-types"; export function getProgressStepCount(prgrsSttsComCd?: string): number { + if (prgrsSttsComCd === "SL") return 3; const idx = INSTL_PROGRESS_ORDER.indexOf(prgrsSttsComCd as (typeof INSTL_PROGRESS_ORDER)[number]); if (idx <= INSTL_PROGRESS_ORDER.indexOf("SV")) return 1; if (idx <= INSTL_PROGRESS_ORDER.indexOf("LE")) return 2; @@ -54,7 +55,7 @@ export function getEditableReviewStep( return "ENV"; } - if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "TE") { + if (dtlInstCd === "ROLE_USER01" && (prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL")) { return "KECO"; } diff --git a/apps/as/src/pages/instl-sncn/lib/save-payload-utils.ts b/apps/as/src/pages/instl-sncn/lib/save-payload-utils.ts index 93627f453..6d2875240 100644 --- a/apps/as/src/pages/instl-sncn/lib/save-payload-utils.ts +++ b/apps/as/src/pages/instl-sncn/lib/save-payload-utils.ts @@ -79,6 +79,7 @@ export function buildSupplementReqPayload( data: InstlSncnMngDetResponse, payload: SplmntDmndDialogConfirmPayload, checklistFormState: ChecklistFormState, + branchSeCd?: string, ): InstlSncnApplySaveRequestDto { const existingItems: InstlSncnSplmntDmndSaveDto[] = (data.splmntDmndList ?? []).map((item) => toSplmntDmndSaveItem(item), @@ -97,6 +98,7 @@ export function buildSupplementReqPayload( return instlSncnApplySaveSchema.parse({ ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), splmntDmndList: [...existingItems, ...newItems], + branchSeCd, }); } diff --git a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx index cf3c94584..df792f620 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/instl-sncn/ui/components/floating-action-buttons.tsx @@ -91,12 +91,13 @@ function FloatingActionButtons({ const normalizedTaskSeComCd = normalizeTaskSeComCdForElapr(data.detDto.taskSeComCd); const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); - const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; + const needsApprovalCheck = + needsAtrzResolve && (prgrsSttsComCd === "TE" || (isForkJoin && prgrsSttsComCd === "SL")); const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = - useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null); + useElaprDocPrgrsByDocNo(needsApprovalCheck ? docNo : null); const { data: elaprBranchList = [], isLoading: isElaprBranchLoading } = useElaprBranchesByDocNo( - isForkJoin && needsTeApprovalCheck ? docNo : null, + isForkJoin && (needsApprovalCheck || prgrsSttsComCd === "SL") ? docNo : null, ); const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( @@ -132,12 +133,61 @@ function FloatingActionButtons({ return atrzSeComCdList; }, [atrzSeComCdList, currentUserPrgrsRow?.atrzSeComCd, isForkJoin, userPendingAtrzSeComCd]); + const supplementDmndBranch = useMemo(() => { + if (!isForkJoin) return undefined; + + // 1순위: 전자결재 브랜치(tb_as_elapr_branch) 테이블에서 상태가 SUPPLEMENT인 브랜치 확인 + const splmntBranch = elaprBranchList.find((b) => b.branchSttsCd === "SUPPLEMENT"); + if (splmntBranch?.branchSeCd && ["SP", "SF"].includes(splmntBranch.branchSeCd)) { + return splmntBranch.branchSeCd as "SP" | "SF"; + } + + // 2순위: 미완료 보완요청 항목의 등록자 UUID 매칭 + const uncompletedItems = data?.splmntDmndList?.filter((item) => item.cmptnYn !== "Y") ?? []; + if (uncompletedItems.length > 0) { + for (const splmnt of uncompletedItems) { + const matchedPrgrs = elaprDocPrgrsList.find( + (p) => p.aprvrUuid?.toLowerCase() === splmnt.rgtr?.toLowerCase(), + ); + if (matchedPrgrs?.branchSeCd && ["SP", "SF"].includes(matchedPrgrs.branchSeCd)) { + return matchedPrgrs.branchSeCd as "SP" | "SF"; + } + } + } + return undefined; + }, [data?.splmntDmndList, elaprBranchList, elaprDocPrgrsList, isForkJoin]); + const userBranchSttsCd = useMemo(() => { if (!isForkJoin || !userBranchSeCd) return undefined; const matchedBranch = userBranchId ? elaprBranchList.find((b) => b.branchId === userBranchId) : elaprBranchList.findLast((b) => b.branchSeCd === userBranchSeCd); + + // 1. 이미 백엔드 브랜치 상태가 SUPPLEMENT이면 즉시 SUPPLEMENT + if (matchedBranch?.branchSttsCd === "SUPPLEMENT") { + return "SUPPLEMENT"; + } + + // 2. 문서 상태가 SL일 때: 보완요청 발생 부서(supplementDmndBranch 또는 본인 등록 건) 우선 판정 + if (prgrsSttsComCd === "SL") { + const hasMyUncompletedSplmnt = data?.splmntDmndList?.some( + (item) => item.cmptnYn !== "Y" && item.rgtr?.toLowerCase() === userInfo?.userUnqId?.toLowerCase(), + ); + if (hasMyUncompletedSplmnt || (supplementDmndBranch && userBranchSeCd === supplementDmndBranch)) { + return "SUPPLEMENT"; + } + + // 보완요청을 하지 않은 타 브랜치(시설부 등)는 결재 진행 중 상태 유지 + const branchRows = elaprDocPrgrsList.filter((row) => row.branchSeCd === userBranchSeCd); + const hasUnfinished = branchRows.some( + (row) => + row.atrzPrgrsSttsComCd !== "ATRZ_PRGRS_003" && row.atrzPrgrsSttsComCd !== "ATRZ_PRGRS_999", + ); + return hasUnfinished ? "IN_PROGRESS" : "COMPLETED"; + } + + // 3. SL이 아닌 일반 상태(TE 등)에서는 matchedBranch 상태 적용 if (matchedBranch?.branchSttsCd) { return matchedBranch.branchSttsCd; } @@ -150,14 +200,24 @@ function FloatingActionButtons({ } return undefined; - }, [elaprBranchList, isForkJoin, prgrsSttsComCd, userBranchId, userBranchSeCd]); + }, [ + data?.splmntDmndList, + elaprBranchList, + elaprDocPrgrsList, + isForkJoin, + prgrsSttsComCd, + supplementDmndBranch, + userBranchId, + userBranchSeCd, + userInfo?.userUnqId, + ]); const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; - if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; + if (needsApprovalCheck && isElaprDocPrgrsLoading) return []; if (isForkJoin && isElaprBranchLoading) return []; return getButtonConfigs(dtlInstCd, prgrsSttsComCd, effectiveAtrzSeComCdList, { - hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + hasPendingApprovalRequest: needsApprovalCheck ? hasPendingApprovalRequest : undefined, isForkJoin, branchSeCd: userBranchSeCd, branchSttsCd: userBranchSttsCd, @@ -170,8 +230,8 @@ function FloatingActionButtons({ isElaprDocPrgrsLoading, isForkJoin, isRvwLoginUserLoading, + needsApprovalCheck, needsAtrzResolve, - needsTeApprovalCheck, prgrsSttsComCd, userBranchSeCd, userBranchSttsCd, @@ -387,11 +447,12 @@ function FloatingActionButtons({ } async function handleRvwStrgClick() { + const targetPrgrsStts = prgrsSttsComCd === "SL" ? "SL" : "TE"; const payload = buildTechReviewReplyPayload( data, checklistFormState, "KECO", - "TE", + targetPrgrsStts, "SV", getReviewOpinionFormData(), opnrNm, @@ -406,13 +467,13 @@ function FloatingActionButtons({ } async function handleRvwCmptnClick() { - + const targetPrgrsStts = prgrsSttsComCd === "SL" ? "SL" : "TE"; const payload = { ...buildTechReviewReplyPayload( data, checklistFormState, "KECO", - "TE", + targetPrgrsStts, "SV", getReviewOpinionFormData(), opnrNm, @@ -434,7 +495,13 @@ function FloatingActionButtons({ async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) { setActiveDialog(null); - const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState); + const branchSeCd = userBranchSeCd ?? userPendingBranchSeCd; + const requestPayload = buildSupplementReqPayload( + data, + payload, + checklistFormState, + branchSeCd, + ); await executeAction({ action: INSTL_SNCN_VIEW_BUTTON_ACTION.dmndSplmnt, request: requestPayload, 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 d6b72beb4..b5a4aac66 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 @@ -34,7 +34,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record([ + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwStrg, + SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt, SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.rvwCmptn, SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzAprv, SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.atrzRply, diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts index df63265a4..5be72ee20 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/payload-utils.ts @@ -124,6 +124,7 @@ export function buildSupplementReqPayload( data: SewerBscPlanDetResponseDto, payload: SplmntDmndDialogConfirmPayload, checklistFormState: ChecklistFormState, + branchSeCd?: string, ): SewerBscPlanApplySaveRequestDto { const existingItems: SewerBscPlanSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => toSplmntDmndSaveItem(item), @@ -140,6 +141,7 @@ export function buildSupplementReqPayload( return sewerBscPlanApplySaveSchema.parse({ ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), splmntDmndList: [...existingItems, ...newItems], + branchSeCd, }); } diff --git a/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts b/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts index 5fab31348..89744c5bc 100644 --- a/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts +++ b/apps/as/src/pages/sewer-bsc-plan/lib/review-step-utils.ts @@ -9,6 +9,7 @@ import { } from "../model/view-types"; export function getProgressStepCount(prgrsSttsComCd?: string): number { + if (prgrsSttsComCd === "SL") return 3; const idx = PBADMS_PROGRESS_ORDER.indexOf( prgrsSttsComCd as (typeof PBADMS_PROGRESS_ORDER)[number], ); @@ -57,7 +58,7 @@ export function getEditableReviewStep( return "ENV"; } - if (dtlInstCd === "ROLE_USER01" && prgrsSttsComCd === "TE") { + if (dtlInstCd === "ROLE_USER01" && (prgrsSttsComCd === "TE" || prgrsSttsComCd === "SL")) { return "KECO"; } 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 2feaec62f..469fbbf66 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 @@ -94,11 +94,12 @@ function FloatingActionButtons({ const isForkJoin = isForkJoinTask(data.detDto?.taskSeComCd); const needsAtrzResolve = needsRoleUser01AtrzResolve(dtlInstCd, prgrsSttsComCd); - const needsTeApprovalCheck = needsAtrzResolve && prgrsSttsComCd === "TE"; + const needsApprovalCheck = + needsAtrzResolve && (prgrsSttsComCd === "TE" || (isForkJoin && prgrsSttsComCd === "SL")); const { data: elaprDocPrgrsList = [], isLoading: isElaprDocPrgrsLoading } = - useElaprDocPrgrsByDocNo(needsTeApprovalCheck ? docNo : null); + useElaprDocPrgrsByDocNo(needsApprovalCheck ? docNo : null); const { data: elaprBranchList = [], isLoading: isElaprBranchLoading } = - useElaprBranchesByDocNo(needsTeApprovalCheck && isForkJoin ? docNo : null); + useElaprBranchesByDocNo(isForkJoin && (needsApprovalCheck || prgrsSttsComCd === "SL") ? docNo : null); const hasPendingApprovalRequest = hasCurrentUserPendingApprovalRequest( elaprDocPrgrsList, @@ -133,12 +134,61 @@ function FloatingActionButtons({ return atrzSeComCdList; }, [atrzSeComCdList, currentUserPrgrsRow?.atrzSeComCd, isForkJoin, userPendingAtrzSeComCd]); + const supplementDmndBranch = useMemo(() => { + if (!isForkJoin) return undefined; + + // 1순위: 전자결재 브랜치(tb_as_elapr_branch) 테이블에서 상태가 SUPPLEMENT인 브랜치 확인 + const splmntBranch = elaprBranchList.find((b) => b.branchSttsCd === "SUPPLEMENT"); + if (splmntBranch?.branchSeCd && ["SP", "SF"].includes(splmntBranch.branchSeCd)) { + return splmntBranch.branchSeCd as "SP" | "SF"; + } + + // 2순위: 미완료 보완요청 항목의 등록자 UUID 매칭 + const uncompletedItems = data?.splmntDmndList?.filter((item) => item.cmptnYn !== "Y") ?? []; + if (uncompletedItems.length > 0) { + for (const splmnt of uncompletedItems) { + const matchedPrgrs = elaprDocPrgrsList.find( + (p) => p.aprvrUuid?.toLowerCase() === splmnt.rgtr?.toLowerCase(), + ); + if (matchedPrgrs?.branchSeCd && ["SP", "SF"].includes(matchedPrgrs.branchSeCd)) { + return matchedPrgrs.branchSeCd as "SP" | "SF"; + } + } + } + return undefined; + }, [data?.splmntDmndList, elaprBranchList, elaprDocPrgrsList, isForkJoin]); + const userBranchSttsCd = useMemo(() => { if (!isForkJoin || !userBranchSeCd) return undefined; const matchedBranch = userBranchId ? elaprBranchList.find((b) => b.branchId === userBranchId) : elaprBranchList.findLast((b) => b.branchSeCd === userBranchSeCd); + + // 1. 이미 백엔드 브랜치 상태가 SUPPLEMENT이면 즉시 SUPPLEMENT + if (matchedBranch?.branchSttsCd === "SUPPLEMENT") { + return "SUPPLEMENT"; + } + + // 2. 문서 상태가 SL일 때: 보완요청 발생 부서(supplementDmndBranch 또는 본인 등록 건) 우선 판정 + if (prgrsSttsComCd === "SL") { + const hasMyUncompletedSplmnt = data?.splmntDmndList?.some( + (item) => item.cmptnYn !== "Y" && item.rgtr?.toLowerCase() === userInfo?.userUnqId?.toLowerCase(), + ); + if (hasMyUncompletedSplmnt || (supplementDmndBranch && userBranchSeCd === supplementDmndBranch)) { + return "SUPPLEMENT"; + } + + // 보완요청을 하지 않은 타 브랜치(시설부 등)는 결재 진행 중 상태 유지 + const branchRows = elaprDocPrgrsList.filter((row) => row.branchSeCd === userBranchSeCd); + const hasUnfinished = branchRows.some( + (row) => + row.atrzPrgrsSttsComCd !== "ATRZ_PRGRS_003" && row.atrzPrgrsSttsComCd !== "ATRZ_PRGRS_999", + ); + return hasUnfinished ? "IN_PROGRESS" : "COMPLETED"; + } + + // 3. SL이 아닌 일반 상태(TE 등)에서는 matchedBranch 상태 적용 if (matchedBranch?.branchSttsCd) { return matchedBranch.branchSttsCd; } @@ -151,14 +201,24 @@ function FloatingActionButtons({ } return undefined; - }, [elaprBranchList, isForkJoin, prgrsSttsComCd, userBranchId, userBranchSeCd]); + }, [ + data?.splmntDmndList, + elaprBranchList, + elaprDocPrgrsList, + isForkJoin, + prgrsSttsComCd, + supplementDmndBranch, + userBranchId, + userBranchSeCd, + userInfo?.userUnqId, + ]); const actionButtons = useMemo(() => { if (needsAtrzResolve && isRvwLoginUserLoading) return []; - if (needsTeApprovalCheck && isElaprDocPrgrsLoading) return []; + if (needsApprovalCheck && isElaprDocPrgrsLoading) return []; if (isForkJoin && isElaprBranchLoading) return []; return getButtonConfigs(dtlInstCd, prgrsSttsComCd, effectiveAtrzSeComCdList, { - hasPendingApprovalRequest: needsTeApprovalCheck ? hasPendingApprovalRequest : undefined, + hasPendingApprovalRequest: needsApprovalCheck ? hasPendingApprovalRequest : undefined, isForkJoin, branchSeCd: userBranchSeCd, branchSttsCd: userBranchSttsCd, @@ -171,8 +231,8 @@ function FloatingActionButtons({ isElaprDocPrgrsLoading, isForkJoin, isRvwLoginUserLoading, + needsApprovalCheck, needsAtrzResolve, - needsTeApprovalCheck, prgrsSttsComCd, userBranchSeCd, userBranchSttsCd, @@ -339,11 +399,12 @@ function FloatingActionButtons({ } async function handleRvwStrgClick() { + const targetPrgrsStts = prgrsSttsComCd === "SL" ? "SL" : "TE"; const payload = buildTechReviewReplyPayload( data, checklistFormState, "KECO", - "TE", + targetPrgrsStts, "SV", getReviewOpinionFormData(), opnrNm, @@ -369,12 +430,13 @@ function FloatingActionButtons({ return; } + const targetPrgrsStts = prgrsSttsComCd === "SL" ? "SL" : "TE"; const payload = { ...buildTechReviewReplyPayload( data, checklistFormState, "KECO", - "TE", + targetPrgrsStts, "SV", getReviewOpinionFormData(), opnrNm, @@ -396,7 +458,13 @@ function FloatingActionButtons({ async function handleDmndSplmntConfirm(payload: SplmntDmndDialogConfirmPayload) { setActiveDialog(null); - const requestPayload = buildSupplementReqPayload(data, payload, checklistFormState); + const branchSeCd = userBranchSeCd ?? userPendingBranchSeCd; + const requestPayload = buildSupplementReqPayload( + data, + payload, + checklistFormState, + branchSeCd, + ); await executeAction({ action: SEWER_BSC_PLAN_VIEW_BUTTON_ACTION.dmndSplmnt, request: requestPayload,