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:
byeongcheolryu
2025-12-19 19:12:34 +09:00
parent d742c0ce26
commit c6b605200d
213 changed files with 32644 additions and 775 deletions

View File

@@ -0,0 +1,216 @@
'use client';
import { useState, useCallback } from 'react';
import { CreditCard, Download, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import type { SubscriptionInfo } from './types';
import { PLAN_LABELS } from './types';
// ===== Mock 데이터 =====
const mockSubscription: SubscriptionInfo = {
lastPaymentDate: '2025-12-01',
nextPaymentDate: '2025-12-01',
subscriptionAmount: 500000,
plan: 'premium',
userCount: 100,
userLimit: null, // 무제한
storageUsed: 5.5,
storageLimit: 10,
apiCallsUsed: 8500,
apiCallsLimit: 10000,
};
// ===== 날짜 포맷 함수 =====
const formatDate = (dateStr: string): string => {
const date = new Date(dateStr);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}${month}${day}`;
};
// ===== 금액 포맷 함수 =====
const formatCurrency = (amount: number): string => {
return new Intl.NumberFormat('ko-KR').format(amount) + '원';
};
export function SubscriptionManagement() {
const [subscription] = useState<SubscriptionInfo>(mockSubscription);
const [showCancelDialog, setShowCancelDialog] = useState(false);
// ===== 자료 내보내기 =====
const handleExportData = useCallback(() => {
// TODO: 실제 자료 다운로드 처리
console.log('자료 내보내기');
}, []);
// ===== 서비스 해지 =====
const handleCancelService = useCallback(() => {
// TODO: 실제 서비스 해지 처리
console.log('서비스 해지 처리');
setShowCancelDialog(false);
}, []);
// ===== Progress 계산 =====
const storageProgress = (subscription.storageUsed / subscription.storageLimit) * 100;
const apiProgress = (subscription.apiCallsUsed / subscription.apiCallsLimit) * 100;
return (
<>
<PageLayout>
{/* ===== 페이지 헤더 ===== */}
<PageHeader
title="구독관리"
description="구독 정보를 관리합니다"
icon={CreditCard}
actions={
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleExportData}>
<Download className="w-4 h-4 mr-2" />
</Button>
<Button
variant="outline"
className="border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300"
onClick={() => setShowCancelDialog(true)}
>
</Button>
</div>
}
/>
<div className="space-y-6">
{/* ===== 구독 정보 카드 영역 ===== */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 최근 결제일시 */}
<Card>
<CardContent className="pt-6">
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="text-2xl font-bold">
{formatDate(subscription.lastPaymentDate)}
</div>
</CardContent>
</Card>
{/* 다음 결제일시 */}
<Card>
<CardContent className="pt-6">
<div className="text-sm text-muted-foreground mb-1"> </div>
<div className="text-2xl font-bold">
{formatDate(subscription.nextPaymentDate)}
</div>
</CardContent>
</Card>
{/* 구독금액 */}
<Card>
<CardContent className="pt-6">
<div className="text-sm text-muted-foreground mb-1"></div>
<div className="text-2xl font-bold">
{formatCurrency(subscription.subscriptionAmount)}
</div>
</CardContent>
</Card>
</div>
{/* ===== 구독 정보 영역 ===== */}
<Card>
<CardContent className="pt-6">
<div className="text-sm text-muted-foreground mb-2"> </div>
{/* 플랜명 */}
<h3 className="text-xl font-bold mb-6">
{PLAN_LABELS[subscription.plan]}
</h3>
{/* 사용량 정보 */}
<div className="space-y-6">
{/* 사용자 수 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
</div>
<div className="flex-1">
<Progress value={subscription.userLimit ? (subscription.userCount / subscription.userLimit) * 100 : 30} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[100px] text-right">
{subscription.userCount} / {subscription.userLimit ? `${subscription.userLimit}` : '무제한'}
</div>
</div>
{/* 저장 공간 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
</div>
<div className="flex-1">
<Progress value={storageProgress} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[100px] text-right">
{subscription.storageUsed} TB /{subscription.storageLimit} TB
</div>
</div>
{/* AI API 호출 */}
<div className="flex items-center gap-4">
<div className="w-24 text-sm text-muted-foreground flex-shrink-0">
AI API
</div>
<div className="flex-1">
<Progress value={apiProgress} className="h-2" />
</div>
<div className="text-sm text-blue-600 min-w-[100px] text-right">
{subscription.apiCallsUsed.toLocaleString()} /{subscription.apiCallsLimit.toLocaleString()}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</PageLayout>
{/* ===== 서비스 해지 확인 다이얼로그 ===== */}
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-500" />
</AlertDialogTitle>
<AlertDialogDescription className="text-left">
.
<br />
<span className="font-medium text-red-600">
?
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleCancelService}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View File

@@ -0,0 +1,31 @@
export interface SubscriptionInfo {
// 결제 정보
lastPaymentDate: string;
nextPaymentDate: string;
subscriptionAmount: number;
// 구독 플랜 정보
plan: 'free' | 'basic' | 'premium' | 'enterprise';
// 사용량 정보
userCount: number;
userLimit: number | null; // null = 무제한
storageUsed: number; // TB 단위
storageLimit: number; // TB 단위
apiCallsUsed: number;
apiCallsLimit: number;
}
export const PLAN_LABELS: Record<SubscriptionInfo['plan'], string> = {
free: '무료',
basic: '베이직',
premium: '프리미엄',
enterprise: '엔터프라이즈',
};
export const PLAN_COLORS: Record<SubscriptionInfo['plan'], string> = {
free: 'bg-gray-100 text-gray-800',
basic: 'bg-blue-100 text-blue-800',
premium: 'bg-purple-100 text-purple-800',
enterprise: 'bg-amber-100 text-amber-800',
};