From 3f2938df1c214e0c52cf9db5375ce993b06a577e Mon Sep 17 00:00:00 2001 From: jangwoong Date: Mon, 12 Oct 2026 17:45:55 +0900 Subject: [PATCH] =?UTF-8?q?[as=20elapr]=20=EC=A0=84=EC=9E=90=EA=B2=B0?= =?UTF-8?q?=EC=9E=AC=20forkjoin=20=ED=86=B5=ED=95=A9=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/entities/elapr/index.ts | 10 + .../src/entities/elapr/lib/dmnd-save-utils.ts | 15 +- .../elapr/model/elapr-apln-row-types.ts | 5 + .../elapr/model/elapr-dmnd-interface.ts | 9 + .../elapr/model/elapr-forkjoin-types.ts | 89 ++++ .../elapr/model/elapr-prgrs-stts-interface.ts | 6 + .../elapr/ui/elapr-approval-dialog.tsx | 81 +++- .../ui/elapr-doc-prgrs-forkjoin-step.tsx | 235 ++++++++++ .../elapr/ui/elapr-doc-prgrs-status.tsx | 39 +- .../elapr/ui/elapr-forkjoin-picker-grid.tsx | 412 ++++++++++++++++++ .../elapr/ui/elapr-tmplt-picker-dialog.tsx | 145 +++++- .../lib/fnrs-cnsltn-regist-schema.ts | 24 + .../ui/components/floating-action-buttons.tsx | 46 +- .../pages/fnrs-cnsltn/ui/fnrs-view-page.tsx | 6 +- .../model/rvw-mnpw-block-config.ts | 1 + .../rvw-mnpw-mng/ui/rvw-mnpw-asmt-panel.tsx | 1 + 16 files changed, 1097 insertions(+), 27 deletions(-) create mode 100644 apps/as/src/entities/elapr/model/elapr-forkjoin-types.ts create mode 100644 apps/as/src/entities/elapr/ui/elapr-doc-prgrs-forkjoin-step.tsx create mode 100644 apps/as/src/entities/elapr/ui/elapr-forkjoin-picker-grid.tsx diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index 2210dfc17..e08a68366 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -91,6 +91,14 @@ export type { ElaprDmndService, ElaprDocPrgrsResponseDto, } from "./model/elapr-dmnd-interface"; +export type { + AtrzSttsComCd, + BranchSeCd, + BranchSttsCd, + ElaprBranchDto, + ElaprForkJoinDataMap, + ElaprForkJoinStepDto, +} from "./model/elapr-forkjoin-types"; export type { ElaprApproverChangeRequestDto, ElaprApproverInsertRequestDto, @@ -115,11 +123,13 @@ export type { ReviewerOption } from "./model/reviewer-option"; export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog"; export { ElaprAplnPickerGrid } from "./ui/elapr-apln-picker-grid"; export { ElaprApprovalDialog } from "./ui/elapr-approval-dialog"; +export { ElaprDocPrgrsForkJoinStep } from "./ui/elapr-doc-prgrs-forkjoin-step"; export { type ElaprDocPrgrsPickerConfirmPayload, ElaprDocPrgrsPickerDialog, } from "./ui/elapr-doc-prgrs-picker-dialog"; export { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status"; +export { ElaprForkJoinPickerGrid } from "./ui/elapr-forkjoin-picker-grid"; export { type ElaprTmpltPickerConfirmPayload, ElaprTmpltPickerDialog, diff --git a/apps/as/src/entities/elapr/lib/dmnd-save-utils.ts b/apps/as/src/entities/elapr/lib/dmnd-save-utils.ts index 7eee2894d..53b7a4535 100644 --- a/apps/as/src/entities/elapr/lib/dmnd-save-utils.ts +++ b/apps/as/src/entities/elapr/lib/dmnd-save-utils.ts @@ -8,6 +8,9 @@ function toPrgrsSttsItem( aprvrUuid: string | undefined, atrzSeComCd: string | undefined, atrzSeqNo: number | undefined, + branchId?: string, + branchSeCd?: string, + aprvrDeptNm?: string, ): ElaprPrgrsSttsSaveRequestDto { if (!aprvrUuid?.trim()) { throw new Error("결재자가 지정되지 않은 행이 있습니다."); @@ -23,6 +26,9 @@ function toPrgrsSttsItem( aprvrUuid, atrzSeComCd, atrzSeqNo, + branchId, + branchSeCd, + aprvrDeptNm, }; } @@ -38,6 +44,13 @@ export function buildElaprPrgrsSttsListFromAplnRows( rows: ElaprAplnRow[], ): ElaprPrgrsSttsSaveRequestDto[] { return getVisibleRows(rows).map((row) => - toPrgrsSttsItem(row.aprvrUuid, row.atrzSeComCd, row.atrzSeqNo), + toPrgrsSttsItem( + row.aprvrUuid, + row.atrzSeComCd, + row.atrzSeqNo, + row.branchId, + row.branchSeCd, + row.aprvrDeptNm, + ), ); } diff --git a/apps/as/src/entities/elapr/model/elapr-apln-row-types.ts b/apps/as/src/entities/elapr/model/elapr-apln-row-types.ts index b3532df8a..378c6c5e1 100644 --- a/apps/as/src/entities/elapr/model/elapr-apln-row-types.ts +++ b/apps/as/src/entities/elapr/model/elapr-apln-row-types.ts @@ -7,6 +7,11 @@ export interface ElaprAplnRow extends Omit { aplnId?: string; /** 결재자 표시명 — 응답 DTO 또는 선택 시 로컬 유지 */ aprvrNm?: string; + /** 소속 브랜치 ID 및 구분코드 */ + branchId?: string; + branchSeCd?: "MAIN" | "PIPE" | "FACILITY" | "TOTAL_AGG"; + aprvrDeptNm?: string; + dcarbYn?: string; isNew?: boolean; isDirty?: boolean; /** 소프트 삭제 — 최종 저장 시 mode:D */ diff --git a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts index 3b0b0d7e5..4728f203d 100644 --- a/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-dmnd-interface.ts @@ -1,5 +1,6 @@ import type { ElaprDocPrgrsClientResponseDto, PageableParams, PageResponse } from "@/shared/model"; +import type { ElaprBranchDto } from "./elapr-forkjoin-types"; import type { ElaprPrgrsSttsSaveRequestDto } from "./elapr-prgrs-stts-interface"; /** 백엔드 ElaprDmndSaveRequestDto */ @@ -7,6 +8,9 @@ export interface ElaprDmndSaveRequestDto { elaprDocNo: string; dmndTtl: string; dmndCn: string; + taskSeComCd?: string | null; + forkJoinSeCd?: string | null; + crntStageNo?: number | null; elaprCycl?: number | null; docPrgrsSttsComCd?: string | null; elaprPrgrsSttsList?: ElaprPrgrsSttsSaveRequestDto[]; @@ -19,8 +23,13 @@ export interface ElaprDmndResponseDto { elaprDocNo: string; dmndTtl: string; dmndCn: string; + taskSeComCd?: string; + forkJoinSeCd?: string; + crntStageNo?: number; + elaprBranchList?: ElaprBranchDto[]; atrzSttsComCd: string; atrzSttsComNm?: string; + elaprCycl?: number; dmndDt?: string; updtDt?: string; } diff --git a/apps/as/src/entities/elapr/model/elapr-forkjoin-types.ts b/apps/as/src/entities/elapr/model/elapr-forkjoin-types.ts new file mode 100644 index 000000000..766ca3675 --- /dev/null +++ b/apps/as/src/entities/elapr/model/elapr-forkjoin-types.ts @@ -0,0 +1,89 @@ +/** + * 공공하수도 재원협의 복합사업(SF+SP) Fork-Join 병렬결재 모델 및 타입 정의 (10월 12일 개정: 6인 체제) + */ + +/** 전자결재 마스터 진행상태 공통코드 (ATRZSTTS: tb_as_elapr_dmnd.atrz_stts_com_cd) */ +export type AtrzSttsComCd = + | "ATRZ_STTS_001" // 대기중 + | "ATRZ_STTS_002" // 결재진행중 + | "ATRZ_STTS_003" // 결재완료 + | "ATRZ_STTS_004" // 반려 + | "ATRZ_STTS_005" // 회수 + | "ATRZ_STTS_006"; // 취소 + +/** 결재 구분 공통코드 (ATRZSE: tb_as_elapr_prgrs_stts.atrz_se_com_cd) */ +export type AtrzSeComCd = + | "ATRZ01" // 접수 (민원/신청 접수) + | "ATRZ02" // 검토 (실무 기술검토) + | "ATRZ03" // 승인 (부서 승인 / 병합에 회신) + | "ATRZ04" // 병합 (정책총괄 AI 병합의견 종합 심의 - 10-12 신설) + | "ATRZ99"; // 최종승인(회신) (환경청에 최종 회신) + +/** 브랜치 구분코드 */ +export type BranchSeCd = "MAIN" | "PIPE" | "FACILITY" | "TOTAL_AGG"; + +/** 브랜치 진행상태코드 */ +export type BranchSttsCd = + | "WAITING" + | "IN_PROGRESS" + | "COMPLETED" + | "REJECTED" + | "CANCELLED"; + +/** 브랜치 단위 정보 DTO */ +export interface ElaprBranchDto { + branchId: string; + elaprDmndId: string; + stageSeqNo: number; // 2: 병렬검토, 3: 합류취합 + branchSeCd: BranchSeCd; + branchNm: string; + branchSttsCd: BranchSttsCd; + joinReqYn: "Y" | "N"; + strtDt?: string; + endDt?: string; +} + +/** 결재 진행 스텝 DTO (브랜치 확장) */ +export interface ElaprForkJoinStepDto { + elaprPrgrsId: string; + elaprDmndId?: string; + branchId?: string; + branchSeCd?: BranchSeCd; + aprvrUuid: string; + aprvrNm?: string; + aprvrDeptNm?: string; + ognzNm?: string; + jbpsSeNm?: string; + atrzSeComCd: AtrzSeComCd | string; // ATRZ01: 접수, ATRZ02: 검토, ATRZ03: 승인, ATRZ04: 병합, ATRZ99: 최종승인(회신) + atrzSeComNm?: string; + atrzSeqNo: number; + atrzPrgrsSttsComCd: string; // ATRZ_PRGRS_001: 대기, ATRZ_PRGRS_002: 요청, ATRZ_PRGRS_003: 완료, ATRZ_PRGRS_004: 반려, ATRZ_PRGRS_999: 결재안함 + atrzPrgrsSttsComNm?: string; + rjctRsn?: string; + atrzDt?: string; + dcarbYn?: string; + elaprCycl?: number; +} + +/** 3개 트랙으로 분할된 결재선 데이터 맵 (10-12 개정: 총 6인 체제) */ +export interface ElaprForkJoinDataMap { + pipeTrack: ElaprForkJoinStepDto[]; // 관로부 (2명: 검토(02) ➔ 승인(03)) + facilityTrack: ElaprForkJoinStepDto[]; // 시설부 (2명: 검토(02) ➔ 승인(03)) + aggTrack: ElaprForkJoinStepDto[]; // 정책부 종합병합 (2명: 병합(04) ➔ 최종승인(99)) +} + +/** 결재 승인 요청 DTO */ +export interface ElaprApproveRequestDto { + elaprDmndId: string; + branchId: string; + elaprPrgrsId: string; + atrzOpnn?: string; +} + +/** 결재 단계별 반려 요청 DTO (10-12 개정) */ +export interface ElaprRejectRequestDto { + elaprDmndId: string; + branchId: string; + elaprPrgrsId: string; + rjctRsn: string; // 반려 사유 필수 +} diff --git a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts index 1633b95f8..0204ac17d 100644 --- a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts @@ -14,6 +14,9 @@ export type ElaprPrgrsSttsSaveMode = export interface ElaprPrgrsSttsResponseDto { elaprPrgrsId: string; elaprDmndId?: string; + branchId?: string; + branchSeCd?: string; + aprvrDeptNm?: string; aprvrUuid?: string; atrzSeComCd?: string; atrzSeComNm?: string; @@ -30,6 +33,9 @@ export interface ElaprPrgrsSttsSaveRequestDto { mode?: ElaprPrgrsSttsSaveMode; elaprPrgrsId?: string; elaprDmndId?: string; + branchId?: string; + branchSeCd?: string; + aprvrDeptNm?: string; aprvrUuid?: string; atrzSeComCd?: string; atrzSeqNo?: number; 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 adca68c16..2187d38ef 100644 --- a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -49,6 +49,7 @@ interface ElaprApprovalDialogProps { elaprDocNo: string; currentUserUuid: string; elaprCycl?: number; + taskSeComCd?: string | null; onSuccess: () => void; } @@ -60,6 +61,7 @@ function ElaprApprovalDialog({ elaprDocNo, currentUserUuid, elaprCycl, + taskSeComCd, onSuccess, }: ElaprApprovalDialogProps) { const queryClient = useQueryClient(); @@ -104,7 +106,29 @@ function ElaprApprovalDialog({ const currentUserPrgrs = findCurrentUserPendingApprovalRequest(cyclDocPrgrsList, currentUserUuid); const canApprove = Boolean(currentUserPrgrs?.elaprPrgrsId); const canReject = - currentUserPrgrs?.atrzSeComCd === "ATRZ03" || currentUserPrgrs?.atrzSeComCd === "ATRZ99"; + currentUserPrgrs?.atrzSeComCd === "ATRZ03" || + currentUserPrgrs?.atrzSeComCd === "ATRZ04" || + currentUserPrgrs?.atrzSeComCd === "ATRZ99" || + (currentUserPrgrs?.atrzSeqNo ?? 1) > 1; + + const branchSeCd = (currentUserPrgrs as { branchSeCd?: string } | undefined)?.branchSeCd; + const isForkJoin = + taskSeComCd === "SF_SP" || + branchSeCd === "PIPE" || + branchSeCd === "FACILITY" || + branchSeCd === "TOTAL_AGG"; + + const isLastInBranch = + currentUserPrgrs?.atrzSeComCd === "ATRZ99" || currentUserPrgrs?.atrzSeComCd === "ATRZ03"; + + const approveButtonLabel = useMemo(() => { + if (!isForkJoin) return "결재"; + if (branchSeCd === "TOTAL_AGG" && isLastInBranch) return "결재 (환경청에 최종회신)"; + if (branchSeCd === "TOTAL_AGG") return "결재 (병합)"; + if ((branchSeCd === "PIPE" || branchSeCd === "FACILITY") && isLastInBranch) + return "결재 (병합에 회신)"; + return "결재 (검토)"; + }, [isForkJoin, branchSeCd, isLastInBranch]); const pickingRow = pickingRowKey ? rows.find((r) => r.rowKey === pickingRowKey) : undefined; const isBusy = isProcessing || savePrgrsSttsMutation.isPending; @@ -239,7 +263,11 @@ function ElaprApprovalDialog({ atrzPrgrsSttsComCd: "ATRZ_PRGRS_004", rjctRsn: rjctRsn.trim(), }); - alert("반려 처리가 완료되었습니다."); + alert( + isForkJoin + ? "결재가 반려 처리되었습니다.\n직전 결재자가 '결재 대기중'으로 변경되었습니다. 상대 부서 결재는 정상 진행됩니다." + : "반려 처리가 완료되었습니다.", + ); queryClient.invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], }); @@ -272,6 +300,42 @@ function ElaprApprovalDialog({
+ {isForkJoin && ( +
+
+ + {branchSeCd === "PIPE" + ? "🌊 하수관로 전문기술검토 (Stage 2)" + : branchSeCd === "FACILITY" + ? "🏭 하수처리시설 전문기술검토 (Stage 2)" + : branchSeCd === "TOTAL_AGG" + ? isLastInBranch + ? "⚖️ 정책총괄 최종승인 및 회신 (Stage 3)" + : "⚖️ 정책총괄 AI 종합병합 (Stage 3)" + : "복합사업 결재"} + + + {branchSeCd === "TOTAL_AGG" + ? isLastInBranch + ? "환경청 대외 최종 회신 단계" + : "AI 의견 종합 및 병합 심의 단계" + : isLastInBranch + ? "병합부서(정책총괄) 내부 회신(이관) 단계" + : "실무 기술검토 단계"} + +
+

+ {branchSeCd === "TOTAL_AGG" + ? isLastInBranch + ? "ℹ️ [중요] 본 승인은 환경청에 최종 회신하는 결재입니다. 승인 즉시 공문이 대외 발송되며 원천문서 상태가 TP(기술검토회신) 또는 TR(최종실무협의완료)로 전이됩니다." + : "ℹ️ [안내] 본 승인은 'AI의견 종합 및 병합 심의' 단계입니다. 양 부서 검토의견을 종합 반영하여 정책실장(최종 전결권자)에게 이관됩니다." + : isLastInBranch + ? "ℹ️ [안내] 본 승인은 '병합에 회신'하는 내부 이관 단계입니다. 환경청으로 직접 공문이 나가지 않으며, 상대방 부서 검토 완료 시 종합병합 단계로 자동 합류(Join)됩니다." + : "ℹ️ [안내] 본 단계는 실무 기술검토 단계입니다. 검토 완료 후 부서 승인자에게 이관됩니다."} +

+
+ )} +

결재 진행 단계

@@ -372,6 +436,17 @@ function ElaprApprovalDialog({ {canApprove && (
+ {isForkJoin && canReject && ( +
+
⚠️ [단계별 반려 안내 (10월 12일 개정)]
+

현재 복합사업 병렬 검토가 진행 중입니다. 본 단계에서 반려 처리 시:

+
    +
  • 귀하(현재 결재자)는 반려(ATRZ_PRGRS_004) 처리됩니다.
  • +
  • 이전(직전) 결재자만 '결재 대기중(ATRZ_PRGRS_002)'으로 변경되어 보완 및 재상신 기회가 부여됩니다.
  • +
  • 동시에 진행 중인 상대방 부서 결재는 중단되지 않고 독립 정상 진행됩니다.
  • +
+
+ )}

반려 의견