feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
346
src/components/reports/ComprehensiveAnalysis/index.tsx
Normal file
346
src/components/reports/ComprehensiveAnalysis/index.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Check, AlertTriangle, Info, AlertCircle, BarChart3 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import {
|
||||
comprehensiveAnalysisMockData,
|
||||
formatAmount,
|
||||
} from '../mockData';
|
||||
import type { CheckPoint, AmountCard, TodayIssueItem } from '../types';
|
||||
|
||||
// 체크포인트 아이콘
|
||||
const CheckPointIcon = ({ type }: { type: CheckPoint['type'] }) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <Check className="h-4 w-4 text-green-600" />;
|
||||
case 'warning':
|
||||
return <AlertTriangle className="h-4 w-4 text-amber-600" />;
|
||||
case 'error':
|
||||
return <AlertCircle className="h-4 w-4 text-red-600" />;
|
||||
case 'info':
|
||||
default:
|
||||
return <Info className="h-4 w-4 text-blue-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 체크포인트 컴포넌트
|
||||
const CheckPointItem = ({ checkpoint }: { checkpoint: CheckPoint }) => {
|
||||
return (
|
||||
<div className="flex items-start gap-2 py-1">
|
||||
<div className="mt-0.5">
|
||||
<CheckPointIcon type={checkpoint.type} />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{checkpoint.message}
|
||||
{checkpoint.highlight && (
|
||||
<span className="text-red-600 font-medium"> {checkpoint.highlight}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 금액 카드 컴포넌트 (기존 스타일 적용)
|
||||
const AmountCardItem = ({ card }: { card: AmountCard }) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4 md:p-6">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-2">
|
||||
{card.label}
|
||||
</p>
|
||||
<p className="text-2xl md:text-3xl font-bold">{formatAmount(card.amount)}</p>
|
||||
{(card.subAmount !== undefined || card.previousAmount !== undefined) && (
|
||||
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
|
||||
{card.subAmount !== undefined && (
|
||||
<span>{card.subLabel}: {formatAmount(card.subAmount)}</span>
|
||||
)}
|
||||
{card.previousAmount !== undefined && (
|
||||
<span>{card.previousLabel}: {formatAmount(card.previousAmount)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 섹션 타이틀 컴포넌트
|
||||
const SectionTitle = ({
|
||||
title,
|
||||
badge,
|
||||
actionButton,
|
||||
}: {
|
||||
title: string;
|
||||
badge?: 'warning' | 'success' | 'info';
|
||||
actionButton?: { label: string; onClick: () => void };
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
badge === 'warning' ? 'bg-amber-500' :
|
||||
badge === 'success' ? 'bg-green-500' :
|
||||
badge === 'info' ? 'bg-blue-500' : 'bg-red-500'
|
||||
)} />
|
||||
<h3 className="text-base font-semibold text-foreground">{title}</h3>
|
||||
</div>
|
||||
{actionButton && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={actionButton.onClick}
|
||||
>
|
||||
{actionButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 오늘의 이슈 테이블 행
|
||||
const IssueTableRow = ({
|
||||
item,
|
||||
onApprove,
|
||||
onReject,
|
||||
}: {
|
||||
item: TodayIssueItem;
|
||||
onApprove: (id: string) => void;
|
||||
onReject: (id: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-3 border-b last:border-b-0">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<span className="text-sm font-medium min-w-[80px]">{item.category}</span>
|
||||
<span className="text-sm text-muted-foreground flex-1">{item.description}</span>
|
||||
{item.requiresApproval && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white h-7 px-3"
|
||||
onClick={() => onApprove(item.id)}
|
||||
>
|
||||
승인
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-3"
|
||||
onClick={() => onReject(item.id)}
|
||||
>
|
||||
반려
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground min-w-[50px] text-right">{item.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ComprehensiveAnalysis() {
|
||||
const router = useRouter();
|
||||
const data = comprehensiveAnalysisMockData;
|
||||
|
||||
const [issueFilter, setIssueFilter] = useState('전체필터');
|
||||
|
||||
const handleReceivableDetail = () => {
|
||||
router.push('/ko/accounting/receivables-status');
|
||||
};
|
||||
|
||||
const handleApprove = (id: string) => {
|
||||
console.log('승인:', id);
|
||||
// TODO: API 연동
|
||||
};
|
||||
|
||||
const handleReject = (id: string) => {
|
||||
console.log('반려:', id);
|
||||
// TODO: API 연동
|
||||
};
|
||||
|
||||
// 필터링된 이슈 목록
|
||||
const filteredIssues = issueFilter === '전체필터'
|
||||
? data.todayIssue.items
|
||||
: data.todayIssue.items.filter(item => item.category === issueFilter);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="보고서 및 분석"
|
||||
description="종합 경영 분석 현황을 확인합니다."
|
||||
icon={BarChart3}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 오늘의 이슈 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SectionTitle title="오늘의 이슈" badge="warning" />
|
||||
<Select value={issueFilter} onValueChange={setIssueFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder="필터 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data.todayIssue.filterOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 이슈 테이블 */}
|
||||
<div className="border rounded-lg p-4">
|
||||
{filteredIssues.map((item) => (
|
||||
<IssueTableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 당월 예상 지출 내역 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="당월 예상 지출 내역" badge="warning" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{data.monthlyExpense.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.monthlyExpense.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 카드/가지급금 관리 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="카드/가지급금 관리" badge="warning" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{data.cardManagement.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.cardManagement.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 접대비 현황 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="접대비 현황" badge="warning" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
{data.entertainment.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.entertainment.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 복리후생비 현황 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="복리후생비 현황" badge="warning" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{data.welfare.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.welfare.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 미수금 현황 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="미수금 현황"
|
||||
badge="warning"
|
||||
actionButton={{
|
||||
label: data.receivable.detailButtonLabel,
|
||||
onClick: handleReceivableDetail,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{data.receivable.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.receivable.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 채권추심 현황 섹션 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="채권추심 현황" badge="info" />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{data.debtCollection.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.debtCollection.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user