From 9e2da003f14ef93fd7d4b30a94475c3ff4205035 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 15 Jul 2026 13:36:20 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=A0=84=EA=B2=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B2=B0=EC=9E=AC?= =?UTF-8?q?=ED=98=84=ED=99=A9=20=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/app/providers/index.tsx | 4 +- apps/as/src/entities/elapr/index.ts | 11 +- .../elapr}/lib/approval-utils.ts | 9 +- .../elapr/lib/prgrs-stts-save-utils.ts | 12 +- .../entities/elapr/lib/use-prgrs-stts-rows.ts | 30 ++ .../elapr/model/elapr-prgrs-stts-interface.ts | 2 + .../elapr/ui}/elapr-approval-dialog.tsx | 58 +-- .../elapr/ui/elapr-doc-prgrs-status.tsx | 95 ++++ .../elapr/ui}/sortable-prgrs-stts-row.tsx | 21 +- .../ui/components/elapr-approval-dialog.tsx | 418 ------------------ .../ui/components/floating-action-buttons.tsx | 3 +- .../ui/components/sortable-prgrs-stts-row.tsx | 179 -------- .../pages/fnrs-cnsltn/ui/fnrs-view-page.tsx | 5 + .../pages/instl-sncn/lib/approval-utils.ts | 28 -- .../ui/components/floating-action-buttons.tsx | 3 +- .../pages/instl-sncn/ui/instl-form-page.tsx | 1 + .../pages/instl-sncn/ui/instl-view-page.tsx | 5 + apps/as/src/shared/model/pbadms-sprt.ts | 1 + 18 files changed, 213 insertions(+), 672 deletions(-) rename apps/as/src/{pages/fnrs-cnsltn => entities/elapr}/lib/approval-utils.ts (78%) rename apps/as/src/{pages/instl-sncn/ui/components => entities/elapr/ui}/elapr-approval-dialog.tsx (89%) create mode 100644 apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx rename apps/as/src/{pages/instl-sncn/ui/components => entities/elapr/ui}/sortable-prgrs-stts-row.tsx (87%) delete mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx delete mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/sortable-prgrs-stts-row.tsx delete mode 100644 apps/as/src/pages/instl-sncn/lib/approval-utils.ts diff --git a/apps/as/src/app/providers/index.tsx b/apps/as/src/app/providers/index.tsx index 20ec0b72e..2a0ac6e69 100644 --- a/apps/as/src/app/providers/index.tsx +++ b/apps/as/src/app/providers/index.tsx @@ -27,7 +27,9 @@ export function Providers({ children }: ProvidersProps) { - +
+ +
); } diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index b72bcdb1f..adec3e931 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -47,6 +47,12 @@ export { findCurrentUserPendingApprovalRequest, hasCurrentUserPendingApprovalRequest, } from "./lib/approval-request-utils"; +export { + APPROVER_STATUS_MAP, + type ApproverStatusConfig, + getApproverStatus, + sortByAtrzSeqNo, +} from "./lib/approval-utils"; export { buildElaprPrgrsSttsListFromApln, buildElaprPrgrsSttsListFromAplnRows, @@ -105,9 +111,12 @@ export type { 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 { ElaprDocPrgrsStatus } from "./ui/elapr-doc-prgrs-status"; export { - ElaprTmpltPickerDialog, type ElaprTmpltPickerConfirmPayload, + ElaprTmpltPickerDialog, } from "./ui/elapr-tmplt-picker-dialog"; export { ElaprTmpltSelectTable } from "./ui/elapr-tmplt-select-table"; export { SortableAplnPickerRow } from "./ui/sortable-apln-picker-row"; +export { SortablePrgrsSttsRow } from "./ui/sortable-prgrs-stts-row"; diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts b/apps/as/src/entities/elapr/lib/approval-utils.ts similarity index 78% rename from apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts rename to apps/as/src/entities/elapr/lib/approval-utils.ts index d60d20b1c..00d7d7b2a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/approval-utils.ts +++ b/apps/as/src/entities/elapr/lib/approval-utils.ts @@ -1,4 +1,4 @@ -import type { ElaprDocPrgrsClientResponseDto } from "@/entities/fnrs-cnsltn"; +import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model"; export interface ApproverStatusConfig { label: string; @@ -7,9 +7,10 @@ export interface ApproverStatusConfig { export const APPROVER_STATUS_MAP: Record = { ATRZ_PRGRS_001: { label: "대기", badgeClass: "bg-gray-100 text-gray-700" }, - ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-800" }, - ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-800" }, - ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-800" }, + ATRZ_PRGRS_002: { label: "요청", badgeClass: "bg-yellow-100 text-yellow-700" }, + ATRZ_PRGRS_003: { label: "승인", badgeClass: "bg-green-100 text-green-700" }, + ATRZ_PRGRS_004: { label: "반려", badgeClass: "bg-red-100 text-red-700" }, + ATRZ_PRGRS_999: { label: "결재안함", badgeClass: "bg-blue-100 text-blue-700" }, }; export function sortByAtrzSeqNo( diff --git a/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts index db6ee3e5c..6ea409344 100644 --- a/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts +++ b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts @@ -4,6 +4,7 @@ import { } from "../model/elapr-prgrs-stts-interface"; export const PRGRS_STTS_WAITING_CD = "ATRZ_PRGRS_001"; +export const PRGRS_STTS_APPROVED_CD = "ATRZ_PRGRS_003"; /** 서버 행 비교용 최소 필드 */ export interface PrgrsSttsServerRow { @@ -11,6 +12,7 @@ export interface PrgrsSttsServerRow { aprvrUuid?: string; atrzSeComCd?: string; atrzSeqNo?: number; + dcarbYn?: string; } /** 결재선 그리드 로컬 행 — 진행상태 API 응답 + UI 보조 필드 */ @@ -29,6 +31,7 @@ export interface ElaprPrgrsSttsRow { ognzNm?: string; jbpsSeNm?: string; jbgdSeNm?: string; + dcarbYn?: string; isNew?: boolean; isDirty?: boolean; /** 소프트 삭제 — 최종 저장 시 mode:D */ @@ -41,6 +44,10 @@ export function isPrgrsSttsRowEditable(row: ElaprPrgrsSttsRow): boolean { return row.isNew === true || row.atrzPrgrsSttsComCd === PRGRS_STTS_WAITING_CD; } +export function isPrgrsSttsRowDcarbable(row: ElaprPrgrsSttsRow): boolean { + return row.atrzPrgrsSttsComCd !== PRGRS_STTS_APPROVED_CD; +} + function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] { return rows.filter((row) => !row.isDeleted); } @@ -85,7 +92,8 @@ function rowDiffersFromServer(row: ElaprPrgrsSttsRow, serverRow: PrgrsSttsServer return ( row.aprvrUuid !== serverRow.aprvrUuid || row.atrzSeComCd !== serverRow.atrzSeComCd || - row.atrzSeqNo !== serverRow.atrzSeqNo + row.atrzSeqNo !== serverRow.atrzSeqNo || + (row.dcarbYn ?? "N") !== (serverRow.dcarbYn ?? "N") ); } @@ -149,6 +157,7 @@ export function buildPrgrsSttsSavePayload( atrzSeComCd: row.atrzSeComCd, atrzSeqNo: row.atrzSeqNo, atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + dcarbYn: row.dcarbYn ?? "N", }); continue; } @@ -164,6 +173,7 @@ export function buildPrgrsSttsSavePayload( atrzSeComCd: row.atrzSeComCd, atrzSeqNo: row.atrzSeqNo, atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + dcarbYn: row.dcarbYn ?? "N", }); } } diff --git a/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts b/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts index 40120db78..f4a884e75 100644 --- a/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts +++ b/apps/as/src/entities/elapr/lib/use-prgrs-stts-rows.ts @@ -34,6 +34,7 @@ function toRow(item: ElaprDocPrgrsClientResponseDto): ElaprPrgrsSttsRow { ognzNm: item.ognzNm, jbpsSeNm: item.jbpsSeNm, jbgdSeNm: item.jbgdSeNm, + dcarbYn: item.dcarbYn ?? "N", isNew: false, isDirty: false, isDeleted: false, @@ -71,6 +72,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp aprvrUuid: "", aprvrNm: "", atrzPrgrsSttsComCd: PRGRS_STTS_WAITING_CD, + dcarbYn: "N", isNew: true, isDirty: true, isDeleted: false, @@ -104,6 +106,33 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp [editingRowKey], ); + const setDcarbYn = useCallback( + (rowKey: string, checked: boolean) => { + if (editingRowKey != null && editingRowKey !== rowKey) return; + + setRows((prev) => { + const target = prev.find((r) => r.rowKey === rowKey); + if (!target || target.isDeleted) return prev; + + const nextYn = checked ? "Y" : "N"; + + return prev.map((row) => { + if (row.isDeleted) return row; + + if (row.rowKey === rowKey) { + if ((row.dcarbYn ?? "N") === nextYn) return row; + return { ...row, dcarbYn: nextYn, isDirty: true }; + } + + if (!checked) return row; + if ((row.dcarbYn ?? "N") === "N") return row; + return { ...row, dcarbYn: "N", isDirty: true }; + }); + }); + }, + [editingRowKey], + ); + const cancelEdit = useCallback(() => { if (!editingRowKey) return; @@ -202,6 +231,7 @@ export function usePrgrsSttsRows({ serverRows, elaprDmndId }: UsePrgrsSttsRowsOp addRow, startEdit, updateRow, + setDcarbYn, cancelEdit, commitEdit, markDeleted, 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 46dad56ec..7c24ff8eb 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 @@ -22,6 +22,7 @@ export interface ElaprPrgrsSttsResponseDto { atrzPrgrsSttsComNm?: string; rjctRsn?: string; atrzDt?: string; + dcarbYn?: string; } /** 백엔드 ElaprPrgrsSttsSaveRequestDto */ @@ -33,6 +34,7 @@ export interface ElaprPrgrsSttsSaveRequestDto { atrzSeComCd?: string; atrzSeqNo?: number; atrzPrgrsSttsComCd?: string; + dcarbYn?: string; } /** 백엔드 ElaprPrgrsSttsSearchDto */ diff --git a/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx similarity index 89% rename from apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx rename to apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index 53b6337c8..c4cb3956d 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -9,31 +9,13 @@ import { AxiosError } from "axios"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib"; -import { - AprvrPickerDialog, - buildPrgrsSttsSavePayload, - elaprPrgrsSttsService, - findCurrentUserPendingApprovalRequest, - hasUnsavedPrgrsSttsChanges, - isPrgrsSttsRowEditable, - type ReviewerOption, - useDndSensors, - useElaprDocPrgrsByDocNo, - usePrgrsSttsRows, - useSaveElaprPrgrsStts, - validateAllPrgrsSttsRows, - validateAtrzOrder, - validatePrgrsSttsRow, -} from "@/entities/elapr"; import { Button, - Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, - Label, Skeleton, Table, TableBody, @@ -43,6 +25,22 @@ import { Textarea, } from "@common/components"; +import { elaprPrgrsSttsService } from "../api/elapr-prgrs-stts-service"; +import { useElaprDocPrgrsByDocNo, useSaveElaprPrgrsStts } from "../api/queries"; +import { validateAtrzOrder } from "../lib/apln-validators"; +import { findCurrentUserPendingApprovalRequest } from "../lib/approval-request-utils"; +import { + buildPrgrsSttsSavePayload, + hasUnsavedPrgrsSttsChanges, + isPrgrsSttsRowEditable, + validateAllPrgrsSttsRows, + validatePrgrsSttsRow, +} from "../lib/prgrs-stts-save-utils"; +import { useDndSensors } from "../lib/use-dnd-sensors"; +import { usePrgrsSttsRows } from "../lib/use-prgrs-stts-rows"; +import type { ReviewerOption } from "../model/reviewer-option"; + +import { AprvrPickerDialog } from "./aprvr-picker-dialog"; import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row"; interface ElaprApprovalDialogProps { @@ -68,7 +66,6 @@ function ElaprApprovalDialog({ const [isProcessing, setIsProcessing] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); const [pickingRowKey, setPickingRowKey] = useState(null); - const [finalApprovalYn, setFinalApprovalYn] = useState(false); const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null); @@ -82,6 +79,7 @@ function ElaprApprovalDialog({ addRow, startEdit, updateRow, + setDcarbYn, cancelEdit, commitEdit, markDeleted, @@ -199,7 +197,6 @@ function ElaprApprovalDialog({ try { await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, { atrzPrgrsSttsComCd: "ATRZ_PRGRS_003", - finalApprovalYn, }); alert("결재가 완료되었습니다."); queryClient.invalidateQueries({ @@ -312,12 +309,13 @@ function ElaprApprovalDialog({ > + - - - - + + + + @@ -325,6 +323,7 @@ function ElaprApprovalDialog({ 결재순서 결재구분 결재자 + 전결자 지정 결재상태 관리 @@ -353,6 +352,7 @@ function ElaprApprovalDialog({ setPickingRowKey(row.rowKey); setPickerOpen(true); }} + onToggleDcarb={(checked) => setDcarbYn(row.rowKey, checked)} /> ))} @@ -383,16 +383,6 @@ function ElaprApprovalDialog({ {canApprove && (
-
- setFinalApprovalYn(checked === true)} - /> - -
diff --git a/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx new file mode 100644 index 000000000..731e39a07 --- /dev/null +++ b/apps/as/src/entities/elapr/ui/elapr-doc-prgrs-status.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { Fragment } from "react"; + +import { ChevronRight } from "lucide-react"; + +import type { ElaprDocPrgrsClientResponseDto } from "@/shared/model"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { cn, Skeleton } from "@common/components"; + +import { useElaprDocPrgrsByDocNo } from "../api/queries"; +import { getApproverStatus, sortByAtrzSeqNo } from "../lib/approval-utils"; + +const REJECT_STATUS_CD = "ATRZ_PRGRS_004"; + +interface ElaprDocPrgrsStatusProps { + elaprDocNo: string | null | undefined; +} + +function ElaprDocPrgrsStep({ item }: { item: ElaprDocPrgrsClientResponseDto }) { + const status = getApproverStatus(item.atrzPrgrsSttsComCd); + const isRejected = item.atrzPrgrsSttsComCd === REJECT_STATUS_CD; + const rjctRsn = item.rjctRsn?.trim(); + const metaParts = [item.ognzNm, item.jbpsSeNm].filter(Boolean); + + const statusBadge = ( + + {status.label} + + ); + + return ( +
+ {isRejected && rjctRsn ? ( + + {statusBadge} + + {rjctRsn} + + + ) : ( + statusBadge + )} +
+
+ [{item.atrzSeComNm || "-"}] + {item.aprvrNm || item.aprvrUuid || "-"} +
+ {metaParts.length > 0 && ( + {metaParts.join(" · ")} + )} +
+
+ ); +} + +function ElaprDocPrgrsStatus({ elaprDocNo }: ElaprDocPrgrsStatusProps) { + const { data, isLoading } = useElaprDocPrgrsByDocNo(elaprDocNo); + const steps = sortByAtrzSeqNo(data ?? []); + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (steps.length === 0) { + return
결재 정보가 없습니다.
; + } + + return ( +
+ {steps.map((item, index) => ( + +
+ {index > 0 && } +
+ +
+ ))} +
+ ); +} + +export { ElaprDocPrgrsStatus }; diff --git a/apps/as/src/pages/instl-sncn/ui/components/sortable-prgrs-stts-row.tsx b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx similarity index 87% rename from apps/as/src/pages/instl-sncn/ui/components/sortable-prgrs-stts-row.tsx rename to apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx index b002f558d..601d272f2 100644 --- a/apps/as/src/pages/instl-sncn/ui/components/sortable-prgrs-stts-row.tsx +++ b/apps/as/src/entities/elapr/ui/sortable-prgrs-stts-row.tsx @@ -6,9 +6,9 @@ import { MoveVertical } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; import { findCodeLabel, useCodeList } from "@/entities/code"; -import { type ElaprPrgrsSttsRow, isPrgrsSttsRowEditable } from "@/entities/elapr"; import { Button, + Checkbox, cn, Select, SelectContent, @@ -19,7 +19,12 @@ import { TableRow, } from "@common/components"; -import { getApproverStatus } from "../../lib/approval-utils"; +import { getApproverStatus } from "../lib/approval-utils"; +import { + type ElaprPrgrsSttsRow, + isPrgrsSttsRowDcarbable, + isPrgrsSttsRowEditable, +} from "../lib/prgrs-stts-save-utils"; interface SortablePrgrsSttsRowProps { row: ElaprPrgrsSttsRow; @@ -33,6 +38,7 @@ interface SortablePrgrsSttsRowProps { onDelete: () => void; onChange: (patch: Partial) => void; onPickAprvr: () => void; + onToggleDcarb: (checked: boolean) => void; } function SortablePrgrsSttsRow({ @@ -47,6 +53,7 @@ function SortablePrgrsSttsRow({ onDelete, onChange, onPickAprvr, + onToggleDcarb, }: SortablePrgrsSttsRowProps) { const { data: atrzSeCodes } = useCodeList("ATRZSE"); const selectableAtrzSeCodes = atrzSeCodes?.filter((code) => code.lwpoCd !== "ATRZ01"); @@ -63,6 +70,7 @@ function SortablePrgrsSttsRow({ }; const isEditable = isPrgrsSttsRowEditable(row); + const isDcarbable = isPrgrsSttsRowDcarbable(row); const rowDisabled = isLocked && !isEditing; const atrzSeLabel = row.atrzSeComNm || findCodeLabel(atrzSeCodes, row.atrzSeComCd); const status = getApproverStatus(row.atrzPrgrsSttsComCd); @@ -139,6 +147,15 @@ function SortablePrgrsSttsRow({ )}
+ + {isDcarbable && !rowDisabled && ( + onToggleDcarb(checked === true)} + aria-label="전결자 지정" + /> + )} + {isRejectedRow && rjctRsn ? ( diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx deleted file mode 100644 index 53b6337c8..000000000 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/elapr-approval-dialog.tsx +++ /dev/null @@ -1,418 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { closestCenter, DndContext, type DragEndEvent } from "@dnd-kit/core"; -import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; -import { useQueryClient } from "@tanstack/react-query"; -import { AxiosError } from "axios"; - -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib"; -import { - AprvrPickerDialog, - buildPrgrsSttsSavePayload, - elaprPrgrsSttsService, - findCurrentUserPendingApprovalRequest, - hasUnsavedPrgrsSttsChanges, - isPrgrsSttsRowEditable, - type ReviewerOption, - useDndSensors, - useElaprDocPrgrsByDocNo, - usePrgrsSttsRows, - useSaveElaprPrgrsStts, - validateAllPrgrsSttsRows, - validateAtrzOrder, - validatePrgrsSttsRow, -} from "@/entities/elapr"; -import { - Button, - Checkbox, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - Label, - Skeleton, - Table, - TableBody, - TableHead, - TableHeader, - TableRow, - Textarea, -} from "@common/components"; - -import { SortablePrgrsSttsRow } from "./sortable-prgrs-stts-row"; - -interface ElaprApprovalDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - elaprDocNo: string; - currentUserUuid: string; - onSuccess: () => void; -} - -const UNSAVED_PRGRS_STTS_MESSAGE = "결재선 변경 이력이 있습니다.\n결재선 저장을 해 주세요."; - -function ElaprApprovalDialog({ - open, - onOpenChange, - elaprDocNo, - currentUserUuid, - onSuccess, -}: ElaprApprovalDialogProps) { - const queryClient = useQueryClient(); - const alert = useAlertStore((state) => state.setMessage); - const [rjctRsn, setRjctRsn] = useState(""); - const [isProcessing, setIsProcessing] = useState(false); - const [pickerOpen, setPickerOpen] = useState(false); - const [pickingRowKey, setPickingRowKey] = useState(null); - const [finalApprovalYn, setFinalApprovalYn] = useState(false); - - const { data: elaprDocPrgrsList, isLoading } = useElaprDocPrgrsByDocNo(open ? elaprDocNo : null); - - const elaprDmndId = elaprDocPrgrsList?.[0]?.elaprDmndId; - - const { - rows, - visibleRows, - editingRowKey, - isEditingLocked, - addRow, - startEdit, - updateRow, - cancelEdit, - commitEdit, - markDeleted, - reorderRows, - } = usePrgrsSttsRows({ - serverRows: elaprDocPrgrsList, - elaprDmndId, - }); - - const savePrgrsSttsMutation = useSaveElaprPrgrsStts(); - const sensors = useDndSensors(); - - const currentUserPrgrs = findCurrentUserPendingApprovalRequest( - elaprDocPrgrsList, - 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 ?? []); - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event; - if (!over || active.id === over.id) return; - - reorderRows(String(active.id), String(over.id)); - } - - function handleRowSave(rowKey: string) { - const row = rows.find((r) => r.rowKey === rowKey); - if (!row) return; - - const fieldError = validatePrgrsSttsRow(row); - if (fieldError) { - alert(fieldError); - return; - } - - commitEdit(rowKey); - } - - function handleReviewerSelect(reviewer: ReviewerOption) { - if (!pickingRowKey) return; - updateRow(pickingRowKey, { - aprvrUuid: reviewer.uuid, - aprvrNm: reviewer.name, - ognzNm: reviewer.deptNm, - }); - setPickingRowKey(null); - } - - function assertPrgrsSttsSaved(): boolean { - if (isEditingLocked) { - alert(UNSAVED_PRGRS_STTS_MESSAGE); - return false; - } - - if (isPrgrsSttsUnsaved) { - alert(UNSAVED_PRGRS_STTS_MESSAGE); - return false; - } - - return true; - } - - async function handleSavePrgrsStts() { - if (!elaprDmndId) { - alert("결재 요청 정보가 없습니다."); - return; - } - - const validationError = validateAllPrgrsSttsRows(rows, isEditingLocked); - if (validationError) { - alert(validationError); - return; - } - - const orderError = validateAtrzOrder(rows); - if (orderError) { - alert(orderError); - return; - } - - const payload = buildPrgrsSttsSavePayload(rows, elaprDmndId, elaprDocPrgrsList ?? []); - if (payload.length === 0) { - alert("변경된 결재선이 없습니다."); - return; - } - - setIsProcessing(true); - try { - await savePrgrsSttsMutation.mutateAsync(payload); - alert("결재선이 저장되었습니다."); - await queryClient.invalidateQueries({ - queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], - }); - onSuccess(); - } catch (error: unknown) { - let errorMessage = "결재선 저장 중 오류가 발생했습니다."; - if (error instanceof AxiosError) { - errorMessage = error.response?.data?.message ?? errorMessage; - } - alert(errorMessage); - } finally { - setIsProcessing(false); - } - } - - async function handleApprove() { - if (!currentUserPrgrs?.elaprPrgrsId) return; - if (!assertPrgrsSttsSaved()) return; - - setIsProcessing(true); - try { - await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, { - atrzPrgrsSttsComCd: "ATRZ_PRGRS_003", - finalApprovalYn, - }); - alert("결재가 완료되었습니다."); - queryClient.invalidateQueries({ - queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], - }); - onSuccess(); - onOpenChange(false); - } catch (error: unknown) { - let errorMessage = ""; - if (error instanceof AxiosError) { - errorMessage = error.response?.data?.message ?? ""; - } else { - errorMessage = "결재 처리 중 오류가 발생했습니다."; - } - alert(errorMessage); - } finally { - setIsProcessing(false); - } - } - - async function handleReject() { - if (!currentUserPrgrs?.elaprPrgrsId) return; - if (!rjctRsn.trim()) { - alert("반려 사유를 입력해주세요."); - return; - } - if (!assertPrgrsSttsSaved()) return; - - setIsProcessing(true); - try { - await elaprPrgrsSttsService.process(currentUserPrgrs.elaprPrgrsId, { - atrzPrgrsSttsComCd: "ATRZ_PRGRS_004", - rjctRsn: rjctRsn.trim(), - }); - alert("반려 처리가 완료되었습니다."); - queryClient.invalidateQueries({ - queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_DMND, "findDocPrgrsByDocNo", elaprDocNo], - }); - onSuccess(); - onOpenChange(false); - } catch (error: unknown) { - let errorMessage = ""; - if (error instanceof AxiosError) { - errorMessage = error.response?.data?.message ?? ""; - } else { - errorMessage = "반려 처리 중 오류가 발생했습니다."; - } - alert(errorMessage); - } finally { - setIsProcessing(false); - } - } - - function handleClose() { - setRjctRsn(""); - setPickingRowKey(null); - onOpenChange(false); - } - - return ( - - - - 전자결재 - - -
-
-
-

결재 진행 단계

-
- - {elaprDmndId && ( - - )} -
-
- - {isLoading ? ( -
- - -
- ) : visibleRows.length === 0 ? ( -
- {elaprDmndId - ? "등록된 결재선이 없습니다. [결재선 추가]로 결재자를 등록하세요." - : "결재 정보가 없습니다."} -
- ) : ( - -
- - - - - - - - - - - - 결재순서 - 결재구분 - 결재자 - 결재상태 - 관리 - - - - r.rowKey)} - strategy={verticalListSortingStrategy} - > - {visibleRows.map((row, index) => ( - startEdit(row.rowKey)} - onSave={() => handleRowSave(row.rowKey)} - onCancel={cancelEdit} - onDelete={() => markDeleted(row.rowKey)} - onChange={(patch) => updateRow(row.rowKey, patch)} - onPickAprvr={() => { - setPickingRowKey(row.rowKey); - setPickerOpen(true); - }} - /> - ))} - - -
- - )} - - - {canApprove && ( -
-

반려 의견

-