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

@@ -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>