- Vite + React 프로젝트 구조 설정 - 불필요한 PDF 파일 삭제 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
17 KiB
17 KiB
공통 컴포넌트 가이드
이 문서는 SAM MES 시스템의 통일된 레이아웃과 반응형 디자인을 위한 공통 컴포넌트 사용 가이드입니다.
📌 중요: 페이지 타이틀 구조 통일에 대한 상세 가이드는 TITLE_STRUCTURE_STANDARDIZATION.md를 참고하세요.
📋 목차
개요
모든 페이지는 통일된 레이아웃과 디자인 스타일을 사용하여 일관된 사용자 경험을 제공합니다.
- ✅ 완전한 반응형 디자인 (데스크톱/태블릿/모바일)
- ✅ 데스크톱: 테이블 형식
- ✅ 모바일: 카드 형식
- ✅ 통일된 헤더, 검색, 통계 패턴
공통 컴포넌트 목록
1. PageLayout
페이지의 기본 레이아웃을 제공합니다.
import { PageLayout } from "./common/PageLayout";
function MyPage() {
return (
<PageLayout maxWidth="2xl">
{/* 페이지 내용 */}
</PageLayout>
);
}
Props:
maxWidth:"sm" | "md" | "lg" | "xl" | "2xl" | "full"(기본값: "full")sm: max-w-3xlmd: max-w-5xllg: max-w-6xlxl: max-w-7xl2xl: max-w-[1600px]full: w-full (전체 너비, 기본값)
- 자동으로 padding과 spacing 적용 (p-4 md:p-6, space-y-4 md:space-y-6)
2. PageHeader
페이지 헤더 (제목, 설명, 액션 버튼)
import { PageHeader } from "./common/PageHeader";
import { FileText, Plus } from "lucide-react";
<PageHeader
title="견적 관리"
description="작성된 견적서 목록을 확인하고 관리합니다"
icon={FileText}
actions={
<Button onClick={handleNew}>
<Plus className="w-4 h-4 mr-2" />
신규 작성
</Button>
}
/>
Props:
title: 페이지 제목 (필수)description: 페이지 설명 (선택)icon: Lucide 아이콘 컴포넌트 (선택)actions: 액션 버튼들 (선택)
3. StatCards
통계 카드를 그리드로 표시
import { StatCards } from "./common/StatCards";
import { FileText } from "lucide-react";
const stats = [
{
label: "전체 견적",
value: 150,
icon: FileText,
iconColor: "text-blue-600"
},
{
label: "금일 작성",
value: 12,
icon: FileText,
iconColor: "text-green-600",
trend: {
value: "+15%",
isPositive: true
}
}
];
<StatCards stats={stats} />
Props:
stats: 통계 데이터 배열label: 라벨value: 값 (숫자 또는 문자열)icon: Lucide 아이콘 (선택)iconColor: 아이콘 색상 클래스 (선택)trend: 추세 정보 (선택)
4. SearchFilter
검색 및 필터 바
import { SearchFilter } from "./common/SearchFilter";
<SearchFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="거래처, 작성자로 검색..."
filterButton={true}
onFilterClick={handleFilter}
extraActions={
<Button>
<Download className="w-4 h-4 mr-2" />
내보내기
</Button>
}
/>
Props:
searchValue: 검색어 (필수)onSearchChange: 검색어 변경 핸들러 (필수)searchPlaceholder: 검색 placeholder (선택)filterButton: 필터 버튼 표시 여부 (기본값: true)onFilterClick: 필터 클릭 핸들러 (선택)extraActions: 추가 액션 버튼들 (선택)
5. EmptyState
빈 상태 표시
import { EmptyState } from "./common/EmptyState";
import { FileText } from "lucide-react";
<EmptyState
icon={FileText}
title="작성된 견적서가 없습니다"
description="첫 견적서를 작성하여 시작하세요"
action={{
label: "첫 견적서 작성하기",
onClick: handleNew
}}
/>
Props:
icon: Lucide 아이콘 컴포넌트 (필수)title: 제목 (필수)description: 설명 (선택)action: 액션 버튼 정보 (선택)label: 버튼 텍스트onClick: 클릭 핸들러
6. MobileCard
모바일용 카드 컴포넌트
import { MobileCard } from "./common/MobileCard";
import { FileText, Eye, Edit, Trash2 } from "lucide-react";
<MobileCard
title="샘플 거래처"
subtitle="작성자: 김철수"
icon={<FileText className="w-4 h-4 text-blue-600" />}
badge={{
label: "진행중",
variant: "outline"
}}
fields={[
{ label: "접수일", value: "2025-01-15" },
{ label: "금액", value: "1,500,000원" },
{ label: "상태", value: "완료", badge: true, badgeVariant: "secondary" }
]}
actions={[
{
label: "상세",
icon: <Eye className="w-3 h-3 mr-1" />,
variant: "outline",
onClick: handleView
},
{
label: "수정",
icon: <Edit className="w-3 h-3 mr-1" />,
variant: "outline",
onClick: handleEdit
}
]}
/>
Props:
title: 제목 (필수)subtitle: 부제목 (선택)icon: 아이콘 ReactNode (선택)badge: 배지 정보 (선택)fields: 필드 배열 (필수)actions: 액션 버튼 배열 (선택)
사용 예시
List 페이지 전체 구조
import { useState } from "react";
import { PageLayout } from "./common/PageLayout";
import { PageHeader } from "./common/PageHeader";
import { StatCards } from "./common/StatCards";
import { SearchFilter } from "./common/SearchFilter";
import { EmptyState } from "./common/EmptyState";
import { MobileCard } from "./common/MobileCard";
import { Card, CardContent } from "./ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
import { Plus, FileText, Eye, Edit, Trash2 } from "lucide-react";
export function MyListPage() {
const [searchTerm, setSearchTerm] = useState("");
const [items, setItems] = useState([]);
const stats = [
{ label: "전체", value: items.length, icon: FileText, iconColor: "text-blue-600" },
{ label: "금일", value: 12, icon: FileText, iconColor: "text-green-600" }
];
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<PageLayout>
<PageHeader
title="항목 관리"
description="항목 목록을 확인하고 관리합니다"
icon={FileText}
actions={
<Button onClick={handleNew}>
<Plus className="w-4 h-4 mr-2" />
신규 작성
</Button>
}
/>
<SearchFilter
searchValue={searchTerm}
onSearchChange={setSearchTerm}
searchPlaceholder="검색..."
/>
<StatCards stats={stats} />
{filteredItems.length === 0 ? (
<EmptyState
icon={FileText}
title="항목이 없습니다"
action={{ label: "첫 항목 작성", onClick: handleNew }}
/>
) : (
<>
{/* 데스크톱 테이블 */}
<Card className="hidden md:block">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>이름</TableHead>
<TableHead>상태</TableHead>
<TableHead className="text-right">관리</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredItems.map(item => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>
<Badge>{item.status}</Badge>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
<Edit className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* 모바일 카드 */}
<div className="md:hidden">
{filteredItems.map(item => (
<MobileCard
key={item.id}
title={item.name}
badge={{ label: item.status }}
fields={[
{ label: "생성일", value: item.date }
]}
actions={[
{
label: "수정",
icon: <Edit className="w-3 h-3 mr-1" />,
onClick: () => handleEdit(item)
}
]}
/>
))}
</div>
</>
)}
</PageLayout>
);
}
Write 페이지 전체 구조
import { PageLayout } from "./common/PageLayout";
import { PageHeader } from "./common/PageHeader";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
import { ChevronDown, ChevronUp, Save, FileEdit } from "lucide-react";
export function MyWritePage({ onSave, onCancel }) {
const [isBasicInfoOpen, setIsBasicInfoOpen] = useState(true);
return (
<PageLayout>
<PageHeader
title="항목 작성"
description="항목 정보를 입력하세요"
icon={FileEdit}
actions={
<>
<Button variant="outline" onClick={onCancel}>
취소
</Button>
<Button onClick={handleSave}>
<Save className="w-4 h-4 mr-2" />
저장하기
</Button>
</>
}
/>
{/* 접이식 섹션 */}
<Collapsible open={isBasicInfoOpen} onOpenChange={setIsBasicInfoOpen}>
<Card>
<CollapsibleTrigger className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 cursor-pointer hover:bg-muted/50">
<CardTitle>기본 정보</CardTitle>
{isBasicInfoOpen ? (
<ChevronUp className="w-5 h-5 text-muted-foreground" />
) : (
<ChevronDown className="w-5 h-5 text-muted-foreground" />
)}
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>이름</Label>
<Input />
</div>
<div>
<Label>상태</Label>
<Input />
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</PageLayout>
);
}
디자인 원칙
반응형 브레이크포인트
- 모바일:
< 768px- 카드 뷰, 단일 컬럼 - 태블릿:
768px - 1024px- 2-3 컬럼 그리드 - 데스크톱:
> 1024px- 테이블 뷰, 4 컬럼 그리드
간격 (Spacing)
필수 규칙:
- 모든 페이지는
PageLayout으로 감싸기 (자동으로 통일된 padding/spacing 적용) - 페이지 padding:
p-4 md:p-6(PageLayout 자동 적용) - 섹션 간격:
space-y-4 md:space-y-6(PageLayout 자동 적용) - 그리드 간격:
gap-4 - Card 내부 padding:
p-4 md:p-6 - Table이 포함된 Card:
CardContent에p-0사용 (테이블이 꽉 차도록)
색상 패턴
통계 카드 아이콘 색상:
- 파랑:
text-blue-600- 전체/기본 - 초록:
text-green-600- 성공/금일 - 주황:
text-orange-600- 경고/이번주 - 보라:
text-purple-600- 정보/평균
타이포그래피
페이지 타이틀 구조 (필수 규칙):
- ⛔ 절대 금지: 직접
<h1>태그나 커스텀 제목 작성 - ✅ 필수 사용:
PageHeader컴포넌트 사용 - PageHeader는 자동으로 올바른 타이포그래피 적용 (
text-xl md:text-2xl)
잘못된 예시 (사용 금지):
<h1 className="text-3xl font-bold">주문 관리</h1>
<h1 className="text-2xl md:text-3xl font-bold">차량관리</h1>
올바른 예시 (필수):
<PageHeader
title="주문 관리"
description="고객 주문 접수 및 진행 상황 관리"
icon={ShoppingCart}
actions={<Button>등록</Button>}
/>
타이포그래피 규칙:
- 페이지 제목:
text-xl md:text-2xl(PageHeader가 자동 적용) - 카드 제목:
text-base - 본문:
text-sm - 설명:
text-sm text-muted-foreground
아이콘 크기
- 헤더 아이콘:
w-6 h-6 - 버튼 아이콘:
w-4 h-4 - 작은 아이콘:
w-3 h-3 - 통계 아이콘:
w-8 h-8 md:w-10 md:h-10
체크리스트
새 페이지를 만들 때 다음을 확인하세요:
필수 사항
PageLayout으로 감싸기 (maxWidth는 특별한 경우가 아니면 기본값 "full" 사용)PageHeader사용 (title, description, icon, actions) - 커스텀 h1 태그 절대 금지- 페이지 타이틀은 반드시
PageHeader컴포넌트 사용 -text-3xl,text-2xl md:text-3xl직접 사용 금지 - 통계가 필요하면
StatCards사용 - 검색이 필요하면
SearchFilter사용 - 빈 상태에
EmptyState사용
반응형 디자인
- 데스크톱:
Card+Table(테이블을 감싸는 Card만 표시) - 모바일:
MobileCard(md:hidden) - 반응형 그리드:
grid-cols-1 md:grid-cols-2 lg:grid-cols-4
레이아웃 규칙
- Table이 포함된 Card의 CardContent는
p-0사용 - Dialog width:
max-w-[95vw] sm:max-w-lg md:max-w-2xl또는max-w-[95vw] sm:max-w-xl md:max-w-4xl - Dialog에
DialogDescription포함 - 모든 액션 버튼에 아이콘 추가
- Card 내부 padding:
p-4 md:p-6(Table이 있는 경우 제외)
디자인 시스템 통일 작업
✅ 완료된 페이지 (PageLayout + PageHeader 적용)
/components/VehicleManagement.tsx- 차량관리/components/EquipmentManagement.tsx- 설비관리/components/ItemManagement.tsx- 품목관리/components/OrderManagement.tsx- 주문관리 ⭐ NEW/components/QuoteManagement3List.tsx- 견적관리 목록/components/QuoteManagement3Write.tsx- 견적관리 작성
⚠️ 업데이트 필요 페이지 (커스텀 h1 사용 중)
다음 페이지들은 아직 커스텀 <h1 className="text-3xl font-bold"> 또는 <h1 className="text-2xl md:text-3xl font-bold">를 사용하고 있어 PageHeader 컴포넌트로 변경이 필요합니다:
품질관리:
- (대부분 완료)
전자결재:
/components/ApprovalManagement.tsx- 전자결재
회계관리:
/components/AccountingManagement.tsx- 회계 관리/components/ShippingManagement.tsx- 출고관리
인사관리:
/components/HRManagement.tsx- 인사관리
시스템관리:
/components/SystemManagement.tsx- 시스템 관리
자재관리:
/components/MaterialManagement.tsx- 자재 관리
대시보드:
/components/Dashboard.tsx- CEO/생산관리자/작업자 대시보드
업데이트 방법
기존 커스텀 헤더 코드:
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-foreground">주문 관리</h1>
<p className="text-muted-foreground mt-1">고객 주문 접수 및 진행 상황 관리</p>
</div>
<div className="flex space-x-3">
<Button>등록</Button>
</div>
</div>
변경 후 PageHeader 사용:
import { PageHeader } from "./common/PageHeader";
import { ShoppingCart } from "lucide-react";
<PageHeader
title="주문 관리"
description="고객 주문 접수 및 진행 상황 관리"
icon={ShoppingCart}
actions={
<Button>
<Plus className="w-4 h-4 mr-2" />
등록
</Button>
}
/>
업데이트 체크리스트
각 페이지 업데이트 시 확인:
PageLayout으로 전체 페이지 감싸기- 커스텀
<h1>태그 제거 PageHeader컴포넌트 import 및 사용- 적절한 lucide-react 아이콘 추가
- actions prop에 버튼들 이동
- description prop에 설명 추가
text-3xl또는text-2xl md:text-3xl클래스 완전 제거
참고 파일
완벽하게 구현된 예시:
/components/VehicleManagement.tsx- 차량관리 (PageLayout + PageHeader 완벽 구현)/components/EquipmentManagement.tsx- 설비관리 (PageLayout + PageHeader 완벽 구현)/components/ItemManagement.tsx- 품목관리 (PageLayout + PageHeader 완벽 구현)/components/QuoteManagement3List.tsx- 견적관리 목록/components/QuoteManagement3Write.tsx- 견적관리 작성/components/common/- 모든 공통 컴포넌트