feat(WEB): DynamicItemForm 필드 타입 확장 및 컴포넌트 레지스트리 추가
- DynamicFieldRenderer에 신규 필드 타입 추가 (Currency, File, MultiSelect, Radio, Reference, Toggle, UnitValue, Computed) - DynamicTableSection 및 TableCellRenderer 추가 - 필드 프리셋 및 설정 구조 분리 - 컴포넌트 레지스트리 개발 도구 페이지 추가 - UniversalListPage 개선 - 근태관리 코드 정리 - 즐겨찾기 기능 및 동적 필드 타입 백엔드 스펙 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,6 @@ import {
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type StatCard,
|
||||
type TabOption,
|
||||
type FilterFieldConfig,
|
||||
type FilterValues,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
@@ -250,19 +249,6 @@ export function AttendanceManagement() {
|
||||
},
|
||||
], [stats]);
|
||||
|
||||
// 탭 옵션 (mergedRecords 기반)
|
||||
const tabs: TabOption[] = useMemo(() => [
|
||||
{ value: 'all', label: '전체', count: mergedRecords.length, color: 'gray' },
|
||||
{ value: 'notYetIn', label: '미출근', count: stats.notYetInCount, color: 'gray' },
|
||||
{ value: 'onTime', label: '정시 출근', count: stats.onTimeCount, color: 'green' },
|
||||
{ value: 'late', label: '지각', count: stats.lateCount, color: 'yellow' },
|
||||
{ value: 'absent', label: '결근', count: stats.absentCount, color: 'red' },
|
||||
{ value: 'vacation', label: '휴가', count: stats.vacationCount, color: 'blue' },
|
||||
{ value: 'businessTrip', label: '출장', count: mergedRecords.filter(r => r.status === 'businessTrip').length, color: 'purple' },
|
||||
{ value: 'fieldWork', label: '외근', count: mergedRecords.filter(r => r.status === 'fieldWork').length, color: 'orange' },
|
||||
{ value: 'overtime', label: '연장근무', count: mergedRecords.filter(r => r.status === 'overtime').length, color: 'indigo' },
|
||||
], [mergedRecords.length, stats]);
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = useMemo(() => [
|
||||
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
|
||||
@@ -441,8 +427,6 @@ export function AttendanceManagement() {
|
||||
|
||||
columns: tableColumns,
|
||||
|
||||
tabs: tabs,
|
||||
defaultTab: activeTab,
|
||||
|
||||
filterConfig: filterConfig,
|
||||
initialFilters: filterValues,
|
||||
@@ -480,63 +464,11 @@ export function AttendanceManagement() {
|
||||
|
||||
searchPlaceholder: '이름, 부서 검색...',
|
||||
|
||||
// 엑셀 다운로드 설정 (fetchAllUrl로 전체 데이터 조회)
|
||||
// 엑셀 다운로드 설정 (프론트 mergedRecords 사용 - 미출근 직원 포함)
|
||||
excelDownload: {
|
||||
columns: excelColumns,
|
||||
filename: '근태현황',
|
||||
sheetName: '근태',
|
||||
fetchAllUrl: '/api/proxy/attendances',
|
||||
fetchAllParams: () => {
|
||||
const params: Record<string, string> = {};
|
||||
if (startDate) params.date_from = startDate;
|
||||
if (endDate) params.date_to = endDate;
|
||||
return params;
|
||||
},
|
||||
mapResponse: (result: unknown) => {
|
||||
const res = result as { data?: { data?: Record<string, unknown>[] } };
|
||||
const items = res.data?.data ?? [];
|
||||
return items.map((item) => {
|
||||
const user = item.user as Record<string, unknown> | undefined;
|
||||
const profiles = (user?.tenant_profiles ?? []) as Record<string, unknown>[];
|
||||
const profile = profiles[0] as Record<string, unknown> | undefined;
|
||||
const dept = profile?.department as Record<string, unknown> | undefined;
|
||||
const legacyProfile = user?.tenant_profile as Record<string, unknown> | undefined;
|
||||
const legacyDept = legacyProfile?.department as Record<string, unknown> | undefined;
|
||||
const jsonDetails = (item.json_details ?? {}) as Record<string, unknown>;
|
||||
const breakMins = item.break_minutes as number | null;
|
||||
const overtimeMins = jsonDetails.overtime_minutes as number | undefined;
|
||||
|
||||
return {
|
||||
id: String(item.id),
|
||||
employeeId: String(item.user_id),
|
||||
employeeName: (user?.name ?? '') as string,
|
||||
department: (dept?.name ?? legacyDept?.name ?? '') as string,
|
||||
position: (profile?.position_key ?? '') as string,
|
||||
rank: ((legacyProfile?.rank ?? '') as string),
|
||||
baseDate: item.base_date as string,
|
||||
checkIn: (item.check_in ?? jsonDetails.check_in ?? null) as string | null,
|
||||
checkOut: (item.check_out ?? jsonDetails.check_out ?? null) as string | null,
|
||||
breakTime: breakMins != null
|
||||
? `${Math.floor(breakMins / 60)}:${(breakMins % 60).toString().padStart(2, '0')}`
|
||||
: (jsonDetails.break_time as string || null),
|
||||
overtimeHours: overtimeMins
|
||||
? (() => {
|
||||
const h = Math.floor(overtimeMins / 60);
|
||||
const m = overtimeMins % 60;
|
||||
if (h > 0 && m > 0) return `${h}시간 ${m}분`;
|
||||
if (h > 0) return `${h}시간`;
|
||||
return `${m}분`;
|
||||
})()
|
||||
: null,
|
||||
workMinutes: (jsonDetails.work_minutes || null) as number | null,
|
||||
reason: (jsonDetails.reason || null) as AttendanceRecord['reason'],
|
||||
status: item.status as string,
|
||||
remarks: (item.remarks ?? null) as string | null,
|
||||
createdAt: item.created_at as string,
|
||||
updatedAt: item.updated_at as string,
|
||||
} as AttendanceRecord;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
itemsPerPage: itemsPerPage,
|
||||
@@ -551,11 +483,6 @@ export function AttendanceManagement() {
|
||||
);
|
||||
},
|
||||
|
||||
tabFilter: (item, activeTab) => {
|
||||
if (activeTab === 'all') return true;
|
||||
return item.status === activeTab;
|
||||
},
|
||||
|
||||
customFilterFn: (items, filterValues) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
let filtered = items;
|
||||
@@ -700,8 +627,6 @@ export function AttendanceManagement() {
|
||||
}), [
|
||||
mergedRecords,
|
||||
tableColumns,
|
||||
tabs,
|
||||
activeTab,
|
||||
filterConfig,
|
||||
filterValues,
|
||||
statCards,
|
||||
|
||||
Reference in New Issue
Block a user