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:
@@ -4,7 +4,7 @@ import { ReactNode, Fragment, useState, RefObject } from "react";
|
||||
import { LucideIcon, Trash2 } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
@@ -80,6 +80,9 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
icon?: LucideIcon;
|
||||
headerActions?: ReactNode;
|
||||
|
||||
// 탭 콘텐츠 (헤더 액션 아래, 검색 위에 표시되는 커스텀 탭)
|
||||
tabsContent?: ReactNode;
|
||||
|
||||
// 통계 카드
|
||||
stats?: StatCard[];
|
||||
|
||||
@@ -88,20 +91,30 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
versionHistoryTitle?: string;
|
||||
|
||||
// 검색 및 필터
|
||||
searchValue: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
searchValue?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
searchPlaceholder?: string;
|
||||
extraFilters?: ReactNode; // Select, DatePicker 등 추가 필터
|
||||
hideSearch?: boolean; // 검색창 숨김 여부
|
||||
|
||||
// 탭 (품목 유형, 상태 등)
|
||||
tabs: TabOption[];
|
||||
activeTab: string;
|
||||
onTabChange: (value: string) => void;
|
||||
// 탭 (품목 유형, 상태 등) - optional
|
||||
tabs?: TabOption[];
|
||||
activeTab?: string;
|
||||
onTabChange?: (value: string) => void;
|
||||
|
||||
// 테이블 헤더 액션 (탭 옆에 표시될 셀렉트박스 등)
|
||||
tableHeaderActions?: ReactNode;
|
||||
|
||||
// 테이블 앞에 표시될 컨텐츠 (계정과목명 + 저장 버튼 등)
|
||||
beforeTableContent?: ReactNode;
|
||||
|
||||
// 테이블 컬럼
|
||||
tableColumns: TableColumn[];
|
||||
tableTitle?: string; // "전체 목록 (100개)" 같은 타이틀
|
||||
|
||||
// 테이블 하단 푸터 (합계 등)
|
||||
tableFooter?: ReactNode;
|
||||
|
||||
// 데이터
|
||||
data: T[]; // 데스크톱용 페이지네이션된 데이터
|
||||
totalCount?: number; // 전체 데이터 개수 (역순 번호 계산용)
|
||||
@@ -117,6 +130,10 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
getItemId: (item: T) => string; // 아이템에서 ID 추출
|
||||
onBulkDelete?: () => void; // 일괄 삭제 핸들러
|
||||
|
||||
// 테이블 표시 옵션
|
||||
showCheckbox?: boolean; // 체크박스 표시 여부 (기본: true)
|
||||
showRowNumber?: boolean; // 번호 컬럼 표시 여부 (기본: true, tableColumns에 번호 포함 시)
|
||||
|
||||
// 렌더링 함수
|
||||
renderTableRow: (item: T, index: number, globalIndex: number) => ReactNode;
|
||||
renderMobileCard: (item: T, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => ReactNode;
|
||||
@@ -133,6 +150,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
description,
|
||||
icon,
|
||||
headerActions,
|
||||
tabsContent,
|
||||
stats,
|
||||
versionHistory,
|
||||
versionHistoryTitle = "수정 이력",
|
||||
@@ -140,11 +158,15 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
onSearchChange,
|
||||
searchPlaceholder = "검색...",
|
||||
extraFilters,
|
||||
hideSearch = false,
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
tableHeaderActions,
|
||||
beforeTableContent,
|
||||
tableColumns,
|
||||
tableTitle,
|
||||
tableFooter,
|
||||
data,
|
||||
totalCount,
|
||||
allData,
|
||||
@@ -156,6 +178,8 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
onToggleSelectAll,
|
||||
getItemId,
|
||||
onBulkDelete,
|
||||
showCheckbox = true, // 기본값 true
|
||||
showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리)
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
pagination,
|
||||
@@ -187,9 +211,22 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
title={title}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={headerActions}
|
||||
/>
|
||||
|
||||
{/* 헤더 액션 (달력, 버튼 등) - 타이틀 아래 배치 */}
|
||||
{headerActions && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{headerActions}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 커스텀 탭 콘텐츠 (헤더 아래, 검색 위) */}
|
||||
{tabsContent && (
|
||||
<div className="flex items-center">
|
||||
{tabsContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 통계 카드 - 태블릿/데스크톱 */}
|
||||
{stats && stats.length > 0 && (
|
||||
<div className="hidden md:block">
|
||||
@@ -206,54 +243,66 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
)}
|
||||
|
||||
{/* 검색 및 필터 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SearchFilter
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChange}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
filterButton={false}
|
||||
extraActions={extraFilters}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{!hideSearch && (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SearchFilter
|
||||
searchValue={searchValue || ''}
|
||||
onSearchChange={onSearchChange || (() => {})}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
filterButton={false}
|
||||
extraActions={extraFilters}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 테이블 앞 컨텐츠 (계정과목명 + 저장 버튼 등) */}
|
||||
{beforeTableContent && (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
{beforeTableContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 목록 카드 */}
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
||||
<Tabs value={activeTab || 'default'} onValueChange={onTabChange} className="w-full">
|
||||
{/* 데스크톱 (1280px+) - TabChip 탭 */}
|
||||
<div className="hidden xl:block mb-4">
|
||||
<div className="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tabs.map((tab) => (
|
||||
{tabs && tabs.map((tab) => (
|
||||
<TabChip
|
||||
key={tab.value}
|
||||
label={tab.label}
|
||||
count={tab.count}
|
||||
active={activeTab === tab.value}
|
||||
onClick={() => onTabChange(tab.value)}
|
||||
onClick={() => onTabChange?.(tab.value)}
|
||||
color={tab.color as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{selectedItems.size >= 2 && onBulkDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택 삭제 ({selectedItems.size})
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 테이블 헤더 액션 (필터/정렬 셀렉트박스 등) */}
|
||||
{tableHeaderActions}
|
||||
{selectedItems.size >= 2 && onBulkDelete && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteClick}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
선택 삭제 ({selectedItems.size})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
{tabs.map((tab) => (
|
||||
{(tabs || [{ value: 'default', label: '', count: 0 }]).map((tab) => (
|
||||
<TabsContent key={tab.value} value={tab.value} className="mt-0">
|
||||
{/* 모바일/태블릿/소형 노트북 (~1279px) - 선택 삭제 버튼 */}
|
||||
{selectedItems.size >= 2 && onBulkDelete && (
|
||||
@@ -312,12 +361,14 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] min-w-[50px] max-w-[50px] text-center">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
{showCheckbox && (
|
||||
<TableHead className="w-[50px] min-w-[50px] max-w-[50px] text-center">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
{tableColumns.map((column) => {
|
||||
// "actions" 컬럼은 항상 렌더링하되, 선택된 항목이 없을 때는 빈 헤더로 표시
|
||||
return (
|
||||
@@ -335,7 +386,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={tableColumns.length + 1}
|
||||
colSpan={tableColumns.length + (showCheckbox ? 1 : 0)}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
검색 결과가 없습니다.
|
||||
@@ -355,6 +406,11 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
{tableFooter && (
|
||||
<TableFooter>
|
||||
{tableFooter}
|
||||
</TableFooter>
|
||||
)}
|
||||
</Table>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user