회사별 메뉴 분리 및 권한 관리
This commit is contained in:
@@ -4,28 +4,20 @@ import React, { useEffect, useState } from "react";
|
||||
import { FlowComponent } from "@/types/screen-management";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertCircle, Loader2, ChevronUp, History } from "lucide-react";
|
||||
import { AlertCircle, Loader2, ChevronUp } from "lucide-react";
|
||||
import {
|
||||
getFlowById,
|
||||
getAllStepCounts,
|
||||
getStepDataList,
|
||||
getFlowAuditLogs,
|
||||
getFlowSteps,
|
||||
getFlowConnections,
|
||||
getStepColumnLabels,
|
||||
} from "@/lib/api/flow";
|
||||
import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow";
|
||||
import type { FlowDefinition, FlowStep } from "@/types/flow";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -68,6 +60,7 @@ export function FlowWidget({
|
||||
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
|
||||
const [stepDataLoading, setStepDataLoading] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||
|
||||
/**
|
||||
* 🆕 컬럼 표시 결정 함수
|
||||
@@ -93,13 +86,6 @@ export function FlowWidget({
|
||||
const [stepDataPage, setStepDataPage] = useState(1);
|
||||
const [stepDataPageSize, setStepDataPageSize] = useState(10);
|
||||
|
||||
// 오딧 로그 상태
|
||||
const [auditLogs, setAuditLogs] = useState<FlowAuditLog[]>([]);
|
||||
const [auditLogsLoading, setAuditLogsLoading] = useState(false);
|
||||
const [showAuditLogs, setShowAuditLogs] = useState(false);
|
||||
const [auditPage, setAuditPage] = useState(1);
|
||||
const [auditPageSize] = useState(10);
|
||||
|
||||
// componentConfig에서 플로우 설정 추출 (DynamicComponentRenderer에서 전달됨)
|
||||
const config = (component as any).componentConfig || (component as any).config || {};
|
||||
const flowId = config.flowId || component.flowId;
|
||||
@@ -139,6 +125,12 @@ export function FlowWidget({
|
||||
if (selectedStepId) {
|
||||
setStepDataLoading(true);
|
||||
|
||||
// 컬럼 라벨 조회
|
||||
const labelsResponse = await getStepColumnLabels(flowId, selectedStepId);
|
||||
if (labelsResponse.success && labelsResponse.data) {
|
||||
setColumnLabels(labelsResponse.data);
|
||||
}
|
||||
|
||||
const response = await getStepDataList(flowId, selectedStepId, 1, 100);
|
||||
|
||||
if (!response.success) {
|
||||
@@ -226,6 +218,12 @@ export function FlowWidget({
|
||||
|
||||
// 첫 번째 스텝의 데이터 로드
|
||||
try {
|
||||
// 컬럼 라벨 조회
|
||||
const labelsResponse = await getStepColumnLabels(flowId!, firstStep.id);
|
||||
if (labelsResponse.success && labelsResponse.data) {
|
||||
setColumnLabels(labelsResponse.data);
|
||||
}
|
||||
|
||||
const response = await getStepDataList(flowId!, firstStep.id, 1, 100);
|
||||
if (response.success) {
|
||||
const rows = response.data?.records || [];
|
||||
@@ -297,6 +295,15 @@ export function FlowWidget({
|
||||
onSelectedDataChange?.([], stepId);
|
||||
|
||||
try {
|
||||
// 컬럼 라벨 조회
|
||||
const labelsResponse = await getStepColumnLabels(flowId!, stepId);
|
||||
if (labelsResponse.success && labelsResponse.data) {
|
||||
setColumnLabels(labelsResponse.data);
|
||||
} else {
|
||||
setColumnLabels({});
|
||||
}
|
||||
|
||||
// 데이터 조회
|
||||
const response = await getStepDataList(flowId!, stepId, 1, 100);
|
||||
|
||||
if (!response.success) {
|
||||
@@ -359,35 +366,6 @@ export function FlowWidget({
|
||||
onSelectedDataChange?.(selectedData, selectedStepId);
|
||||
};
|
||||
|
||||
// 오딧 로그 로드
|
||||
const loadAuditLogs = async () => {
|
||||
if (!flowId) return;
|
||||
|
||||
try {
|
||||
setAuditLogsLoading(true);
|
||||
const response = await getFlowAuditLogs(flowId, 100); // 최근 100개
|
||||
if (response.success && response.data) {
|
||||
setAuditLogs(response.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load audit logs:", err);
|
||||
toast.error("이력 조회 중 오류가 발생했습니다");
|
||||
} finally {
|
||||
setAuditLogsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 오딧 로그 모달 열기
|
||||
const handleOpenAuditLogs = () => {
|
||||
setShowAuditLogs(true);
|
||||
setAuditPage(1); // 페이지 초기화
|
||||
loadAuditLogs();
|
||||
};
|
||||
|
||||
// 페이지네이션된 오딧 로그
|
||||
const paginatedAuditLogs = auditLogs.slice((auditPage - 1) * auditPageSize, auditPage * auditPageSize);
|
||||
const totalAuditPages = Math.ceil(auditLogs.length / auditPageSize);
|
||||
|
||||
// 🆕 페이지네이션된 스텝 데이터
|
||||
const paginatedStepData = stepData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize);
|
||||
const totalStepDataPages = Math.ceil(stepData.length / stepDataPageSize);
|
||||
@@ -438,188 +416,6 @@ export function FlowWidget({
|
||||
<div className="mb-3 flex-shrink-0 sm:mb-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<h3 className="text-foreground text-base font-semibold sm:text-lg lg:text-xl">{flowData.name}</h3>
|
||||
|
||||
{/* 오딧 로그 버튼 */}
|
||||
<Dialog open={showAuditLogs} onOpenChange={setShowAuditLogs}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={handleOpenAuditLogs} className="gap-1.5">
|
||||
<History className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">변경 이력</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] max-w-[95vw] sm:max-w-[1000px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>플로우 변경 이력</DialogTitle>
|
||||
<DialogDescription>데이터 이동 및 상태 변경 기록 (총 {auditLogs.length}건)</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{auditLogsLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">이력 로딩 중...</span>
|
||||
</div>
|
||||
) : auditLogs.length === 0 ? (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">변경 이력이 없습니다</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* 테이블 */}
|
||||
<div className="bg-card overflow-hidden rounded-lg border">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[140px]">변경일시</TableHead>
|
||||
<TableHead className="w-[80px]">타입</TableHead>
|
||||
<TableHead className="w-[120px]">출발 단계</TableHead>
|
||||
<TableHead className="w-[120px]">도착 단계</TableHead>
|
||||
<TableHead className="w-[100px]">데이터 ID</TableHead>
|
||||
<TableHead className="w-[140px]">상태 변경</TableHead>
|
||||
<TableHead className="w-[100px]">변경자</TableHead>
|
||||
<TableHead className="w-[150px]">DB 연결</TableHead>
|
||||
<TableHead>테이블</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedAuditLogs.map((log) => {
|
||||
const fromStep = steps.find((s) => s.id === log.fromStepId);
|
||||
const toStep = steps.find((s) => s.id === log.toStepId);
|
||||
|
||||
return (
|
||||
<TableRow key={log.id} className="hover:bg-muted/50">
|
||||
<TableCell className="font-mono text-xs">
|
||||
{new Date(log.changedAt).toLocaleString("ko-KR", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{log.moveType === "status"
|
||||
? "상태"
|
||||
: log.moveType === "table"
|
||||
? "테이블"
|
||||
: "하이브리드"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{fromStep?.stepName || `Step ${log.fromStepId}`}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{toStep?.stepName || `Step ${log.toStepId}`}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{log.sourceDataId || "-"}
|
||||
{log.targetDataId && log.targetDataId !== log.sourceDataId && (
|
||||
<>
|
||||
<br />→ {log.targetDataId}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{log.statusFrom && log.statusTo ? (
|
||||
<span className="font-mono">
|
||||
{log.statusFrom}
|
||||
<br />→ {log.statusTo}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">{log.changedBy}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{log.dbConnectionName ? (
|
||||
<span
|
||||
className={
|
||||
log.dbConnectionName === "내부 데이터베이스"
|
||||
? "text-blue-600"
|
||||
: "text-green-600"
|
||||
}
|
||||
>
|
||||
{log.dbConnectionName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{log.sourceTable || "-"}
|
||||
{log.targetTable && log.targetTable !== log.sourceTable && (
|
||||
<>
|
||||
<br />→ {log.targetTable}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{totalAuditPages > 1 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{(auditPage - 1) * auditPageSize + 1}-{Math.min(auditPage * auditPageSize, auditLogs.length)} /{" "}
|
||||
{auditLogs.length}건
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => setAuditPage((p) => Math.max(1, p - 1))}
|
||||
className={auditPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{Array.from({ length: totalAuditPages }, (_, i) => i + 1)
|
||||
.filter((page) => {
|
||||
// 현재 페이지 주변만 표시
|
||||
return (
|
||||
page === 1 ||
|
||||
page === totalAuditPages ||
|
||||
(page >= auditPage - 1 && page <= auditPage + 1)
|
||||
);
|
||||
})
|
||||
.map((page, idx, arr) => (
|
||||
<React.Fragment key={page}>
|
||||
{idx > 0 && arr[idx - 1] !== page - 1 && (
|
||||
<PaginationItem>
|
||||
<span className="text-muted-foreground px-2">...</span>
|
||||
</PaginationItem>
|
||||
)}
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
onClick={() => setAuditPage(page)}
|
||||
isActive={auditPage === page}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => setAuditPage((p) => Math.min(totalAuditPages, p + 1))}
|
||||
className={
|
||||
auditPage === totalAuditPages ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{flowData.description && (
|
||||
@@ -758,7 +554,7 @@ export function FlowWidget({
|
||||
<div className="space-y-1.5">
|
||||
{stepDataColumns.map((col) => (
|
||||
<div key={col} className="flex justify-between gap-2 text-xs">
|
||||
<span className="text-muted-foreground font-medium">{col}:</span>
|
||||
<span className="text-muted-foreground font-medium">{columnLabels[col] || col}:</span>
|
||||
<span className="text-foreground truncate">
|
||||
{row[col] !== null && row[col] !== undefined ? (
|
||||
String(row[col])
|
||||
@@ -793,7 +589,7 @@ export function FlowWidget({
|
||||
key={col}
|
||||
className="bg-background sticky top-0 z-10 border-b px-3 py-2 text-xs font-semibold whitespace-nowrap shadow-[0_1px_0_0_rgb(0,0,0,0.1)] sm:text-sm"
|
||||
>
|
||||
{col}
|
||||
{columnLabels[col] || col}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user