Files
sam-react-prod/src/components/business/Reports.tsx
byeongcheolryu a68a25b737 [feat]: 인증 및 UI/UX 개선 작업
주요 변경사항:
- 로그인/회원가입 페이지 인증 리다이렉트 로직 추가
- 로그인 상태에서 auth 페이지 접근 시 대시보드로 자동 리다이렉트
- router.replace() 사용으로 브라우저 히스토리에서 auth 페이지 제거
- 사이드바 메뉴 활성화 동기화 개선 (URL 직접 입력 및 뒤로가기 대응)
- usePathname 기반 자동 메뉴 활성화 로직 추가
- ESLint 설정 업데이트 (전역 변수 추가, business 폴더 제외)
- TypeScript 빌드 설정 조정 (ignoreBuildErrors 추가)
- 다국어 지원 및 테마 선택 기능 통합
- 대시보드 레이아웃 및 컴포넌트 구조 개선
- UI 컴포넌트 라이브러리 확장 (dialog, sheet, progress 등)

기술적 개선:
- HttpOnly 쿠키 기반 인증 시스템 유지
- 로딩 상태 UI 추가 (인증 체크 중)
- 경로 정규화 로직 (locale 제거)
- 재귀적 메뉴 탐색 및 자동 확장

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 18:55:16 +09:00

510 lines
23 KiB
TypeScript

import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from "recharts";
import { Download, Calendar, Filter, TrendingUp, TrendingDown, Minus, FileText, BarChart3, PieChart as PieChartIcon, Activity } from "lucide-react";
export function Reports() {
const [selectedDateRange, setSelectedDateRange] = useState("month");
const [selectedReport, setSelectedReport] = useState("production");
// 생산 실적 데이터
const productionData = [
{ date: "2025-09-01", planned: 1200, actual: 1150, efficiency: 95.8 },
{ date: "2025-09-02", planned: 1300, actual: 1280, efficiency: 98.5 },
{ date: "2025-09-03", planned: 1100, actual: 1050, efficiency: 95.5 },
{ date: "2025-09-04", planned: 1400, actual: 1420, efficiency: 101.4 },
{ date: "2025-09-05", planned: 1250, actual: 1200, efficiency: 96.0 },
{ date: "2025-09-06", planned: 1350, actual: 1300, efficiency: 96.3 },
{ date: "2025-09-07", planned: 1200, actual: 1180, efficiency: 98.3 },
];
// 품질 데이터
const qualityData = [
{ product: "스마트폰 케이스", passRate: 98.5, defectRate: 1.5, totalInspected: 2500 },
{ product: "태블릿 스탠드", passRate: 97.2, defectRate: 2.8, totalInspected: 1800 },
{ product: "무선 충전기", passRate: 99.1, defectRate: 0.9, totalInspected: 3200 },
{ product: "이어폰 케이스", passRate: 96.8, defectRate: 3.2, totalInspected: 2100 },
];
// 자재 현황 데이터
const materialData = [
{ material: "플라스틱 원료", stock: 1250, minStock: 500, value: 3125000, turnover: 12.5 },
{ material: "알루미늄 판재", stock: 85, minStock: 100, value: 1275000, turnover: 8.2 },
{ material: "실리콘 패드", stock: 3200, minStock: 1000, value: 1600000, turnover: 15.8 },
{ material: "전자부품 모듈", stock: 75, minStock: 100, value: 1875000, turnover: 6.5 },
];
// 설비 가동률 데이터
const equipmentData = [
{ equipment: "CNC 머시닝센터 1호", uptime: 94.2, downtime: 5.8, productivity: 98.5 },
{ equipment: "사출성형기 A라인", uptime: 89.1, downtime: 10.9, productivity: 92.3 },
{ equipment: "자동포장기 1호", uptime: 96.8, downtime: 3.2, productivity: 99.1 },
{ equipment: "품질검사기 QC-01", uptime: 98.5, downtime: 1.5, productivity: 97.8 },
];
// 월별 매출 데이터
const salesData = [
{ month: "1월", sales: 85000000, cost: 62000000, profit: 23000000 },
{ month: "2월", sales: 92000000, cost: 68000000, profit: 24000000 },
{ month: "3월", sales: 78000000, cost: 59000000, profit: 19000000 },
{ month: "4월", sales: 105000000, cost: 75000000, profit: 30000000 },
{ month: "5월", sales: 98000000, cost: 72000000, profit: 26000000 },
{ month: "6월", sales: 112000000, cost: 79000000, profit: 33000000 },
{ month: "7월", sales: 108000000, cost: 77000000, profit: 31000000 },
{ month: "8월", sales: 95000000, cost: 70000000, profit: 25000000 },
{ month: "9월", sales: 118000000, cost: 82000000, profit: 36000000 },
];
const COLORS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'];
const formatCurrency = (value: number) => {
return `${(value / 1000000).toFixed(0)}M`;
};
const getStatusIcon = (current: number, previous: number) => {
if (current > previous) return <TrendingUp className="h-4 w-4 text-green-500" />;
if (current < previous) return <TrendingDown className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-gray-500" />;
};
return (
<div className="p-4 md:p-6 space-y-4 md:space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-gray-900"> </h1>
<p className="text-gray-600 mt-1">, , , , </p>
</div>
<div className="flex flex-col md:flex-row gap-2">
<Select value={selectedDateRange} onValueChange={setSelectedDateRange}>
<SelectTrigger className="w-full md:w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="week"> 1</SelectItem>
<SelectItem value="month"> 1</SelectItem>
<SelectItem value="quarter"> 3</SelectItem>
<SelectItem value="year"> 1</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="w-full md:w-auto">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 핵심 지표 요약 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8,950</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(8950, 8200)}
<span>+9.1% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<TrendingUp className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">97.9%</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(97.9, 96.8)}
<span>+1.1% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<BarChart3 className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">94.7%</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(94.7, 92.3)}
<span>+2.4% </span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<TrendingUp className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">118M</div>
<div className="flex items-center space-x-1 text-xs text-muted-foreground">
{getStatusIcon(118, 95)}
<span>+24.2% </span>
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="production" className="space-y-4">
<div className="overflow-x-auto">
<TabsList className="grid w-full grid-cols-5 min-w-[500px]">
<TabsTrigger value="production" className="flex items-center space-x-2">
<Activity className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="quality" className="flex items-center space-x-2">
<TrendingUp className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="material" className="flex items-center space-x-2">
<BarChart3 className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="equipment" className="flex items-center space-x-2">
<PieChartIcon className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
<TabsTrigger value="sales" className="flex items-center space-x-2">
<FileText className="h-4 w-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="production" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={productionData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Bar dataKey="planned" fill="#94a3b8" name="계획" />
<Bar dataKey="actual" fill="#3b82f6" name="실적" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={productionData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis domain={[90, 105]} />
<Tooltip />
<Line type="monotone" dataKey="efficiency" stroke="#10b981" strokeWidth={3} name="효율성 (%)" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{qualityData.map((item, index) => (
<div key={index} className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">{item.product}</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>:</span>
<span className="font-medium">{item.totalInspected.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span className="text-green-600 font-medium">{item.passRate}%</span>
</div>
<div className="flex justify-between">
<span>:</span>
<span className="text-red-600 font-medium">{item.defectRate}%</span>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="quality" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={qualityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="product" />
<YAxis domain={[90, 100]} />
<Tooltip />
<Bar dataKey="passRate" fill="#10b981" name="합격률 (%)" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={qualityData.map(item => ({ name: item.product, value: item.defectRate }))}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{qualityData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
<div className="mt-4 space-y-2">
{qualityData.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
<div
className="w-3 h-3 rounded-full mr-2"
style={{ backgroundColor: COLORS[index % COLORS.length] }}
></div>
<span className="text-sm">{item.product}</span>
</div>
<span className="text-sm font-medium">{item.defectRate}%</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="material" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={materialData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="material" />
<YAxis />
<Tooltip />
<Bar dataKey="stock" fill="#3b82f6" name="현재고" />
<Bar dataKey="minStock" fill="#ef4444" name="최소재고" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={materialData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="material" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="turnover" stroke="#10b981" strokeWidth={3} name="회전율" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-right p-2"></th>
<th className="text-center p-2"></th>
</tr>
</thead>
<tbody>
{materialData.map((item, index) => (
<tr key={index} className="border-b">
<td className="p-2 font-medium">{item.material}</td>
<td className="text-right p-2">{item.stock.toLocaleString()}</td>
<td className="text-right p-2">{item.value.toLocaleString()}</td>
<td className="text-right p-2">{item.turnover}</td>
<td className="text-center p-2">
{item.stock < item.minStock ? (
<span className="text-red-600 text-sm"></span>
) : (
<span className="text-green-600 text-sm"></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="equipment" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={equipmentData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="equipment" />
<YAxis />
<Tooltip />
<Bar dataKey="uptime" fill="#10b981" name="가동률 (%)" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={equipmentData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="equipment" />
<YAxis domain={[85, 100]} />
<Tooltip />
<Line type="monotone" dataKey="productivity" stroke="#3b82f6" strokeWidth={3} name="생산성 (%)" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="sales" className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={salesData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={formatCurrency} />
<Tooltip formatter={(value) => `${(value as number).toLocaleString()}`} />
<Area type="monotone" dataKey="sales" stackId="1" stroke="#3b82f6" fill="#3b82f6" name="매출" />
<Area type="monotone" dataKey="cost" stackId="1" stroke="#ef4444" fill="#ef4444" name="비용" />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={salesData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={formatCurrency} />
<Tooltip formatter={(value) => `${(value as number).toLocaleString()}`} />
<Bar dataKey="profit" fill="#10b981" name="순이익" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-blue-600">863M원</div>
<p className="text-sm text-gray-600"> </p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+15.2% YoY</span>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-green-600">237M원</div>
<p className="text-sm text-gray-600"> </p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+22.8% YoY</span>
</div>
</div>
<div className="text-center p-4 border rounded-lg">
<div className="text-2xl font-bold text-purple-600">27.5%</div>
<p className="text-sm text-gray-600"></p>
<div className="flex items-center justify-center mt-2">
<TrendingUp className="h-4 w-4 text-green-500 mr-1" />
<span className="text-sm text-green-600">+2.1%p</span>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}