feat(WEB): Phase 2-3 V2 마이그레이션 완료 및 ServerErrorPage 적용

Phase 2 완료 (4개):
- 노무관리, 단가관리(건설), 입금, 출금

Phase 3 라우팅 구조 변경 완료 (22개):
- 거래처(영업), 팝업관리, 공정관리, 게시판관리, 대손추심, Q&A
- 현장관리, 실행내역, 견적관리, 견적(테스트)
- 입찰관리, 이슈관리, 현장설명회, 견적서(건설)
- 협력업체, 시공관리, 기성관리, 품목관리(건설)
- 회계 도메인: 거래처, 매출, 세금계산서, 매입

신규 컴포넌트:
- ErrorCard: 에러 페이지 UI 통일
- ServerErrorPage: V2 페이지 에러 처리 필수
- V2 Client 컴포넌트 및 Config 파일들

총 47개 상세 페이지 중 28개 완료, 19개 제외/불필요

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-19 17:31:28 +09:00
parent 1a6cde2d36
commit 1d7b028693
109 changed files with 6811 additions and 2562 deletions

View File

@@ -0,0 +1,174 @@
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { SearchX, Home, ArrowLeft, Map, AlertCircle, ServerCrash } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
type ErrorType = 'not-found' | 'network' | 'error';
interface ErrorCardProps {
type?: ErrorType;
title?: string;
description?: string;
tips?: string[];
showBackButton?: boolean;
showHomeButton?: boolean;
backButtonLabel?: string;
homeButtonLabel?: string;
homeButtonHref?: string;
onBack?: () => void;
}
const ERROR_CONFIG: Record<ErrorType, {
icon: typeof SearchX;
iconColor: string;
bgColor: string;
emoji: string;
defaultTitle: string;
defaultDescription: string;
defaultTips: string[];
}> = {
'not-found': {
icon: SearchX,
iconColor: 'text-yellow-600 dark:text-yellow-500',
bgColor: 'from-yellow-500/20 to-orange-500/20',
emoji: '?',
defaultTitle: '페이지를 찾을 수 없습니다',
defaultDescription: '요청하신 페이지가 존재하지 않거나 접근 권한이 없습니다.',
defaultTips: [
'메뉴에서 올바른 페이지를 선택했는지 확인',
'해당 페이지에 접근 권한이 있는지 확인',
'페이지가 아직 개발 중일 수 있습니다',
],
},
'network': {
icon: ServerCrash,
iconColor: 'text-orange-600 dark:text-orange-500',
bgColor: 'from-orange-500/20 to-red-500/20',
emoji: '!',
defaultTitle: '데이터를 불러올 수 없습니다',
defaultDescription: '서버와의 연결에 문제가 발생했습니다.',
defaultTips: [
'인터넷 연결 상태를 확인해주세요',
'잠시 후 다시 시도해주세요',
'문제가 지속되면 관리자에게 문의해주세요',
],
},
'error': {
icon: AlertCircle,
iconColor: 'text-red-600 dark:text-red-500',
bgColor: 'from-red-500/20 to-pink-500/20',
emoji: '!',
defaultTitle: '오류가 발생했습니다',
defaultDescription: '요청을 처리하는 중 문제가 발생했습니다.',
defaultTips: [
'페이지를 새로고침 해보세요',
'잠시 후 다시 시도해주세요',
'문제가 지속되면 관리자에게 문의해주세요',
],
},
};
export function ErrorCard({
type = 'not-found',
title,
description,
tips,
showBackButton = true,
showHomeButton = true,
backButtonLabel = '이전 페이지',
homeButtonLabel = '목록으로 이동',
homeButtonHref,
onBack,
}: ErrorCardProps) {
const router = useRouter();
const config = ERROR_CONFIG[type];
const Icon = config.icon;
const handleBack = () => {
if (onBack) {
onBack();
} else {
router.back();
}
};
return (
<div className="flex items-center justify-center min-h-[calc(100vh-200px)] p-4">
<Card className="w-full max-w-2xl border border-border/20 bg-card/50 backdrop-blur">
<CardHeader className="text-center pb-4">
<div className="flex justify-center mb-6">
<div className="relative">
<div className={`w-24 h-24 bg-gradient-to-br ${config.bgColor} rounded-2xl flex items-center justify-center`}>
<Icon className={`w-12 h-12 ${config.iconColor}`} strokeWidth={1.5} />
</div>
<div className="absolute -top-1 -right-1 w-8 h-8 bg-yellow-500 rounded-full flex items-center justify-center shadow-lg">
<span className="text-xl font-bold text-white">{config.emoji}</span>
</div>
</div>
</div>
<CardTitle className="text-2xl md:text-3xl font-bold text-foreground mb-2">
{title || config.defaultTitle}
</CardTitle>
<p className="text-muted-foreground">
{description || config.defaultDescription}
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* 안내 메시지 */}
<div className="bg-muted/50 rounded-xl p-6 space-y-3">
<div className="flex items-start gap-3">
<Map className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
<div className="space-y-2">
<p className="text-sm text-foreground font-medium">
:
</p>
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
{(tips || config.defaultTips).map((tip, index) => (
<li key={index}>{tip}</li>
))}
</ul>
</div>
</div>
</div>
{/* 액션 버튼 */}
<div className="flex flex-col sm:flex-row gap-3 pt-4">
{showBackButton && (
<Button
variant="outline"
className="flex-1 rounded-xl"
onClick={handleBack}
>
<ArrowLeft className="w-4 h-4 mr-2" />
{backButtonLabel}
</Button>
)}
{showHomeButton && homeButtonHref && (
<Button
asChild
className="flex-1 rounded-xl bg-primary hover:bg-primary/90"
>
<Link href={homeButtonHref}>
<Home className="w-4 h-4 mr-2" />
{homeButtonLabel}
</Link>
</Button>
)}
</div>
{/* 도움말 */}
<div className="pt-6 border-t border-border/20 text-center">
<p className="text-xs text-muted-foreground">
.
</p>
</div>
</CardContent>
</Card>
</div>
);
}