Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into dataflowMng

This commit is contained in:
hyeonsu
2025-09-05 16:19:02 +09:00
16 changed files with 1134 additions and 53 deletions

View File

@@ -5,6 +5,7 @@ import { CompanyToolbar } from "./CompanyToolbar";
import { CompanyTable } from "./CompanyTable";
import { CompanyFormModal } from "./CompanyFormModal";
import { CompanyDeleteDialog } from "./CompanyDeleteDialog";
import { DiskUsageSummary } from "./DiskUsageSummary";
/**
* 회사 관리 메인 컴포넌트
@@ -18,6 +19,11 @@ export function CompanyManagement() {
isLoading,
error,
// 디스크 사용량 관련
diskUsageInfo,
isDiskUsageLoading,
loadDiskUsage,
// 모달 상태
modalState,
deleteState,
@@ -46,6 +52,9 @@ export function CompanyManagement() {
return (
<div className="space-y-6">
{/* 디스크 사용량 요약 */}
<DiskUsageSummary diskUsageInfo={diskUsageInfo} isLoading={isDiskUsageLoading} onRefresh={loadDiskUsage} />
{/* 툴바 - 검색, 필터, 등록 버튼 */}
<CompanyToolbar
searchFilter={searchFilter}

View File

@@ -1,8 +1,9 @@
import { Edit, Trash2 } from "lucide-react";
import { Edit, Trash2, HardDrive, FileText } from "lucide-react";
import { Company } from "@/types/company";
import { COMPANY_TABLE_COLUMNS } from "@/constants/company";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
interface CompanyTableProps {
companies: Company[];
@@ -15,6 +16,32 @@ interface CompanyTableProps {
* 회사 목록 테이블 컴포넌트
*/
export function CompanyTable({ companies, isLoading, onEdit, onDelete }: CompanyTableProps) {
// 디스크 사용량 포맷팅 함수
const formatDiskUsage = (company: Company) => {
if (!company.diskUsage) {
return (
<div className="text-muted-foreground flex items-center gap-1">
<HardDrive className="h-3 w-3" />
<span className="text-xs"> </span>
</div>
);
}
const { fileCount, totalSizeMB } = company.diskUsage;
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<FileText className="h-3 w-3 text-blue-500" />
<span className="text-xs font-medium">{fileCount} </span>
</div>
<div className="flex items-center gap-1">
<HardDrive className="h-3 w-3 text-green-500" />
<span className="text-xs">{totalSizeMB.toFixed(1)} MB</span>
</div>
</div>
);
};
// 상태에 따른 Badge 색상 결정
console.log(companies);
// 로딩 상태 렌더링
@@ -94,6 +121,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
{column.label}
</TableHead>
))}
<TableHead className="w-[140px]"> </TableHead>
<TableHead className="w-[120px]"></TableHead>
</TableRow>
</TableHeader>
@@ -103,6 +131,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
<TableCell className="font-mono text-sm">{company.company_code}</TableCell>
<TableCell className="font-medium">{company.company_name}</TableCell>
<TableCell>{company.writer}</TableCell>
<TableCell className="py-2">{formatDiskUsage(company)}</TableCell>
<TableCell>
<div className="flex gap-2">
<Button

View File

@@ -0,0 +1,138 @@
import { RefreshCw, HardDrive, FileText, Building2, Clock } from "lucide-react";
import { AllDiskUsageInfo } from "@/types/company";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
interface DiskUsageSummaryProps {
diskUsageInfo: AllDiskUsageInfo | null;
isLoading: boolean;
onRefresh: () => void;
}
/**
* 디스크 사용량 요약 정보 컴포넌트
*/
export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUsageSummaryProps) {
if (!diskUsageInfo) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle className="text-sm font-medium"> </CardTitle>
<CardDescription> </CardDescription>
</div>
<Button variant="outline" size="sm" onClick={onRefresh} disabled={isLoading} className="h-8 w-8 p-0">
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</CardHeader>
<CardContent>
<div className="text-muted-foreground flex items-center justify-center py-6">
<div className="text-center">
<HardDrive className="mx-auto mb-2 h-8 w-8" />
<p className="text-sm"> ...</p>
</div>
</div>
</CardContent>
</Card>
);
}
const { summary, lastChecked } = diskUsageInfo;
const lastCheckedDate = new Date(lastChecked);
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle className="text-sm font-medium"> </CardTitle>
<CardDescription> </CardDescription>
</div>
<Button
variant="outline"
size="sm"
onClick={onRefresh}
disabled={isLoading}
className="h-8 w-8 p-0"
title="새로고침"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{/* 총 회사 수 */}
<div className="flex items-center space-x-2">
<Building2 className="h-4 w-4 text-blue-500" />
<div>
<p className="text-muted-foreground text-xs"> </p>
<p className="text-lg font-semibold">{summary.totalCompanies}</p>
</div>
</div>
{/* 총 파일 수 */}
<div className="flex items-center space-x-2">
<FileText className="h-4 w-4 text-green-500" />
<div>
<p className="text-muted-foreground text-xs"> </p>
<p className="text-lg font-semibold">{summary.totalFiles.toLocaleString()}</p>
</div>
</div>
{/* 총 용량 */}
<div className="flex items-center space-x-2">
<HardDrive className="h-4 w-4 text-orange-500" />
<div>
<p className="text-muted-foreground text-xs"> </p>
<p className="text-lg font-semibold">{summary.totalSizeMB.toFixed(1)} MB</p>
</div>
</div>
{/* 마지막 업데이트 */}
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-gray-500" />
<div>
<p className="text-muted-foreground text-xs"> </p>
<p className="text-xs font-medium">
{lastCheckedDate.toLocaleString("ko-KR", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div>
</div>
{/* 용량 기준 상태 표시 */}
<div className="mt-4 border-t pt-4">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs"> </span>
<Badge
variant={summary.totalSizeMB > 1000 ? "destructive" : summary.totalSizeMB > 500 ? "secondary" : "default"}
>
{summary.totalSizeMB > 1000 ? "용량 주의" : summary.totalSizeMB > 500 ? "보통" : "여유"}
</Badge>
</div>
{/* 간단한 진행 바 */}
<div className="mt-2 h-2 w-full rounded-full bg-gray-200">
<div
className={`h-2 rounded-full transition-all duration-300 ${
summary.totalSizeMB > 1000 ? "bg-red-500" : summary.totalSizeMB > 500 ? "bg-yellow-500" : "bg-green-500"
}`}
style={{
width: `${Math.min((summary.totalSizeMB / 2000) * 100, 100)}%`,
}}
/>
</div>
<div className="text-muted-foreground mt-1 flex justify-between text-xs">
<span>0 MB</span>
<span>2,000 MB ( )</span>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -32,6 +32,9 @@ import {
Download,
Eye,
X,
ZoomIn,
ZoomOut,
RotateCw,
} from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { getCurrentUser, UserInfo } from "@/lib/api/client";
@@ -84,6 +87,49 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
const [editFormData, setEditFormData] = useState<Record<string, any>>({});
const [editingRowData, setEditingRowData] = useState<Record<string, any> | null>(null);
const [isEditing, setIsEditing] = useState(false);
// 이미지 미리보기 상태
const [previewImage, setPreviewImage] = useState<FileInfo | null>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
// 이미지 미리보기 핸들러들
const handlePreviewImage = useCallback((fileInfo: FileInfo) => {
setPreviewImage(fileInfo);
setShowPreviewModal(true);
setZoom(1);
setRotation(0);
}, []);
const closePreviewModal = useCallback(() => {
setShowPreviewModal(false);
setPreviewImage(null);
setZoom(1);
setRotation(0);
}, []);
const handleZoom = useCallback((direction: "in" | "out") => {
setZoom((prev) => {
if (direction === "in") {
return Math.min(prev + 0.25, 3);
} else {
return Math.max(prev - 0.25, 0.25);
}
});
}, []);
const handleRotate = useCallback(() => {
setRotation((prev) => (prev + 90) % 360);
}, []);
const formatFileSize = useCallback((bytes: number): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}, []);
const [showFileModal, setShowFileModal] = useState(false);
const [currentFileData, setCurrentFileData] = useState<FileColumnData | null>(null);
const [currentFileColumn, setCurrentFileColumn] = useState<DataTableColumn | null>(null);
@@ -1812,10 +1858,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
variant="outline"
size="sm"
className="w-full"
onClick={() => {
// TODO: 이미지 미리보기 모달 구현
alert("이미지 미리보기 기능은 준비 중입니다.");
}}
onClick={() => handlePreviewImage(fileInfo)}
>
<Eye className="mr-1 h-4 w-4" />
@@ -1904,6 +1947,65 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
</DialogFooter>
</DialogContent>
</Dialog>
{/* 이미지 미리보기 다이얼로그 */}
<Dialog open={showPreviewModal} onOpenChange={closePreviewModal}>
<DialogContent className="max-h-[90vh] max-w-4xl overflow-hidden">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span className="truncate">{previewImage?.name}</span>
<div className="flex items-center space-x-2">
<Button size="sm" variant="outline" onClick={() => handleZoom("out")} disabled={zoom <= 0.25}>
<ZoomOut className="h-4 w-4" />
</Button>
<span className="min-w-[60px] text-center text-sm text-gray-500">{Math.round(zoom * 100)}%</span>
<Button size="sm" variant="outline" onClick={() => handleZoom("in")} disabled={zoom >= 3}>
<ZoomIn className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={handleRotate}>
<RotateCw className="h-4 w-4" />
</Button>
{previewImage && (
<Button
size="sm"
variant="outline"
onClick={() => {
handleDownloadFile(previewImage);
}}
>
<Download className="h-4 w-4" />
</Button>
)}
</div>
</DialogTitle>
</DialogHeader>
<div className="flex flex-1 items-center justify-center overflow-auto rounded-lg bg-gray-50 p-4">
{previewImage && (
<img
src={`${process.env.NEXT_PUBLIC_API_URL}/files/preview/${previewImage.id}?serverFilename=${previewImage.serverFilename}`}
alt={previewImage.name}
className="max-h-full max-w-full object-contain transition-transform duration-200"
style={{
transform: `scale(${zoom}) rotate(${rotation}deg)`,
}}
onError={() => {
console.error("이미지 로딩 실패:", previewImage);
toast.error("이미지를 불러올 수 없습니다.");
}}
/>
)}
</div>
{previewImage && (
<div className="flex items-center justify-between border-t pt-3 text-sm text-gray-500">
<div>: {formatFileSize(previewImage.size)}</div>
<div>: {previewImage.type}</div>
<div>: {new Date(previewImage.uploadedAt).toLocaleDateString("ko-KR")}</div>
</div>
)}
</DialogContent>
</Dialog>
</Card>
);
};

View File

@@ -826,6 +826,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
return (
<div className="mt-2 space-y-2">
<div className="text-sm font-medium text-gray-700">
({fileData.length})
</div>
{fileData.map((fileInfo: any, index: number) => {
const isImage = fileInfo.type?.startsWith('image/');