근태(Holyday) API 추가 및 일별/업무형태별 집계 다이얼로그 구현, OT 시작/종료시간 필드 추가

This commit is contained in:
backuppc
2025-12-02 08:26:24 +09:00
parent adcdc40169
commit aa956cf063
14 changed files with 2012 additions and 240 deletions

View File

@@ -0,0 +1,350 @@
import { useState, useEffect } from 'react';
import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
import { comms } from '@/communication';
import { JobReportDayItem, HolidayItem } from '@/types';
interface JobReportDayDialogProps {
isOpen: boolean;
onClose: () => void;
initialMonth?: string; // YYYY-MM format
}
interface DayColumn {
day: number;
dayOfWeek: string;
isHoliday: boolean;
holidayMemo?: string;
}
interface UserRow {
uid: string;
uname: string;
processs: string;
dailyData: Map<number, { hrs: number; ot: number; jobtype: string }>;
totalHrs: number;
totalOt: number;
totalHolidayOt: number;
}
export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportDayDialogProps) {
const [currentMonth, setCurrentMonth] = useState(initialMonth || new Date().toISOString().substring(0, 7));
const [loading, setLoading] = useState(false);
const [dayColumns, setDayColumns] = useState<DayColumn[]>([]);
const [userRows, setUserRows] = useState<UserRow[]>([]);
const [currentUserId, setCurrentUserId] = useState<string>('');
const [currentUserLevel, setCurrentUserLevel] = useState<number>(0);
const [authLevel, setAuthLevel] = useState<number>(0);
const [canViewAll, setCanViewAll] = useState<boolean>(false);
// 요일 배열
const weekDays = ['일', '월', '화', '수', '목', '금', '토'];
// 권한 및 사용자 정보 로드
useEffect(() => {
if (isOpen) {
loadAuthAndUserInfo();
}
}, [isOpen]);
const loadAuthAndUserInfo = async () => {
try {
// 현재 로그인 사용자 정보 가져오기
const loginStatus = await comms.checkLoginStatus();
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
const userId = loginStatus.User.Id;
const userLevel = loginStatus.User.Level || 0;
setCurrentUserId(userId);
setCurrentUserLevel(userLevel);
// 업무일지(jobreport) 권한 가져오기
const authResponse = await comms.checkAuth('jobreport', 5);
const jobReportAuthLevel = authResponse.EffectiveLevel || 0;
setAuthLevel(jobReportAuthLevel);
// 유효 권한 레벨 = Max(사용자레벨, 권한레벨)
const effectiveLevel = Math.max(userLevel, jobReportAuthLevel);
setCanViewAll(effectiveLevel >= 5);
}
} catch (error) {
console.error('권한 정보 로드 오류:', error);
setCanViewAll(false);
}
};
// 데이터 로드
useEffect(() => {
if (isOpen && currentUserId) {
loadData();
}
}, [isOpen, currentMonth, currentUserId, canViewAll]);
const loadData = async () => {
setLoading(true);
try {
// 현재는 기존 API를 사용하여 월별 데이터를 가져옴
const startDate = `${currentMonth}-01`;
const year = parseInt(currentMonth.substring(0, 4));
const month = parseInt(currentMonth.substring(5, 7));
const lastDay = new Date(year, month, 0).getDate();
const endDate = `${currentMonth}-${String(lastDay).padStart(2, '0')}`;
const [jobReportResponse, holidayResponse] = await Promise.all([
comms.getJobReportList(startDate, endDate, '', ''),
comms.getHolidayList(currentMonth)
]);
if (jobReportResponse.Success && jobReportResponse.Data) {
processData(jobReportResponse.Data, holidayResponse.Data || [], lastDay);
}
} catch (error) {
console.error('데이터 로드 오류:', error);
} finally {
setLoading(false);
}
};
const processData = (items: any[], holidays: HolidayItem[], lastDay: number) => {
// 날짜 컬럼 생성
const columns: DayColumn[] = [];
const year = parseInt(currentMonth.substring(0, 4));
const month = parseInt(currentMonth.substring(5, 7));
for (let day = 1; day <= lastDay; day++) {
const date = new Date(year, month - 1, day);
const dayOfWeek = weekDays[date.getDay()];
const dateStr = `${currentMonth}-${String(day).padStart(2, '0')}`;
const holiday = holidays.find(h => h.pdate === dateStr);
columns.push({
day,
dayOfWeek,
isHoliday: holiday?.free || false,
holidayMemo: holiday?.memo
});
}
setDayColumns(columns);
// 사용자별 데이터 집계
const userMap = new Map<string, UserRow>();
items.forEach(item => {
const uid = item.id || item.uid;
const uname = item.name || item.uname || uid;
const pdate = item.pdate?.substring(0, 10);
if (!pdate || !pdate.startsWith(currentMonth)) return;
// 권한 체크: 권한 레벨이 5 미만이고 본인이 아니면 스킵
if (!canViewAll && uid !== currentUserId) return;
const day = parseInt(pdate.substring(8, 10));
if (!userMap.has(uid)) {
userMap.set(uid, {
uid,
uname,
processs: item.userprocess || item.process || '',
dailyData: new Map(),
totalHrs: 0,
totalOt: 0,
totalHolidayOt: 0
});
}
const userRow = userMap.get(uid)!;
const existing = userRow.dailyData.get(day);
const hrs = (existing?.hrs || 0) + (item.hrs || 0);
const ot = (existing?.ot || 0) + (item.ot || 0);
const jobtype = item.type || existing?.jobtype || '';
userRow.dailyData.set(day, { hrs, ot, jobtype });
userRow.totalHrs += item.hrs || 0;
// 휴일 OT 계산
const column = columns[day - 1];
if (column?.isHoliday) {
userRow.totalHolidayOt += item.ot || 0;
} else {
userRow.totalOt += item.ot || 0;
}
});
setUserRows(Array.from(userMap.values()).sort((a, b) => a.uname.localeCompare(b.uname)));
};
// 월 변경
const changeMonth = (delta: number) => {
const [year, month] = currentMonth.split('-').map(Number);
const newDate = new Date(year, month - 1 + delta, 1);
setCurrentMonth(`${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}`);
};
// 셀 색상 결정
const getCellStyle = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => {
if (!data) return 'text-gray-400';
if (data.jobtype === '휴가') return 'text-red-500 font-medium';
if (isHoliday) return 'text-green-500 font-medium';
if (data.hrs > 8) return 'text-blue-500 font-medium';
if (data.hrs < 8) return 'text-red-500';
if (data.ot > 0) return 'text-purple-500 font-medium';
return 'text-white';
};
// 셀 내용 포맷
const formatCellContent = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => {
if (!data) return '--';
if (data.jobtype === '휴가' && data.hrs + data.ot === 8) return '휴가';
const prefix = isHoliday ? '*' : '';
return `${prefix}${data.hrs}+${data.ot}`;
};
// 엑셀 내보내기 (간단한 CSV)
const exportToExcel = () => {
let csv = '사원명,' + dayColumns.map(c => `${c.day}(${c.dayOfWeek})`).join(',') + ',합계\n';
userRows.forEach(row => {
const cells = dayColumns.map(col => {
const data = row.dailyData.get(col.day);
return formatCellContent(data, col.isHoliday);
});
const summary = `${row.totalHrs}+${row.totalOt}(*${row.totalHolidayOt})`;
csv += `${row.uname},${cells.join(',')},${summary}\n`;
});
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `업무일지_일별집계_${currentMonth}.csv`;
link.click();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-gradient-to-br from-gray-900 to-gray-800 rounded-2xl shadow-2xl w-full max-w-7xl max-h-[90vh] flex flex-col">
{/* 헤더 */}
<div className="flex items-center justify-between p-6 border-b border-white/10">
<div className="flex items-center gap-4">
<h2 className="text-2xl font-bold text-white"> </h2>
<div className="flex items-center gap-2">
<button
onClick={() => changeMonth(-1)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<span className="text-lg font-medium text-white min-w-[100px] text-center">
{currentMonth}
</span>
<button
onClick={() => changeMonth(1)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
>
<ChevronRight className="w-5 h-5 text-white" />
</button>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={exportToExcel}
className="px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors flex items-center gap-2"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
>
<X className="w-6 h-6 text-white" />
</button>
</div>
</div>
{/* 테이블 */}
<div className="flex-1 overflow-auto p-6">
{loading ? (
<div className="flex items-center justify-center h-full">
<div className="text-white/50"> ...</div>
</div>
) : (
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-gray-800 z-10">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-white/70 uppercase border border-white/10 bg-gray-800">
</th>
{dayColumns.map(col => (
<th
key={col.day}
className={`px-2 py-2 text-center text-xs font-medium uppercase border border-white/10 ${col.isHoliday ? 'bg-green-900/30 text-green-400' :
col.dayOfWeek === '일' ? 'bg-red-900/30 text-red-400' :
col.dayOfWeek === '토' ? 'bg-blue-900/30 text-blue-400' :
'bg-gray-800 text-white/70'
}`}
title={col.holidayMemo}
>
{col.day}<br />({col.dayOfWeek})
</th>
))}
<th className="px-3 py-2 text-center text-xs font-medium text-white/70 uppercase border border-white/10 bg-gray-800">
</th>
</tr>
</thead>
<tbody>
{userRows.length === 0 ? (
<tr>
<td colSpan={dayColumns.length + 2} className="px-4 py-8 text-center text-white/50">
.
</td>
</tr>
) : (
userRows.map(row => (
<tr key={row.uid} className="hover:bg-white/5 transition-colors">
<td className="px-3 py-2 text-sm text-white border border-white/10 whitespace-nowrap">
{row.uname}
</td>
{dayColumns.map(col => {
const data = row.dailyData.get(col.day);
return (
<td
key={col.day}
className={`px-2 py-2 text-center text-sm border border-white/10 ${getCellStyle(data, col.isHoliday)}`}
>
{formatCellContent(data, col.isHoliday)}
</td>
);
})}
<td className="px-3 py-2 text-center text-sm text-white border border-white/10 font-medium whitespace-nowrap">
{row.totalHrs}+{row.totalOt}(*{row.totalHolidayOt})
</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
{/* 범례 */}
<div className="px-6 py-4 border-t border-white/10 bg-gray-800/50">
<div className="flex flex-wrap gap-4 text-xs text-white/70">
<div><span className="text-gray-400">--</span> : </div>
<div><span className="text-red-500"></span> : </div>
<div><span className="text-green-500">*8+2</span> : </div>
<div><span className="text-blue-500">9+0</span> : 8 </div>
<div><span className="text-red-500">7+0</span> : 8 </div>
<div><span className="text-purple-500">8+2</span> : 8+OT</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -18,6 +18,8 @@ export interface JobreportFormData {
description: string;
hrs: number;
ot: number;
otStart: string; // 초과근무 시작시간 (HH:mm 형식)
otEnd: string; // 초과근무 종료시간 (HH:mm 형식)
jobgrp: string;
tag: string;
}
@@ -39,6 +41,8 @@ export const initialFormData: JobreportFormData = {
description: '',
hrs: 8,
ot: 0,
otStart: '18:00',
otEnd: '20:00',
jobgrp: '',
tag: '',
};
@@ -339,11 +343,10 @@ export function JobreportEditModal({
<button
type="button"
onClick={() => setShowJobTypeModal(true)}
className={`w-full border rounded-lg px-4 py-2 text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-primary-400 transition-colors ${
formData.type
? 'bg-white/20 border-white/30 text-white'
: 'bg-pink-500/30 border-pink-400/50 text-pink-200'
}`}
className={`w-full border rounded-lg px-4 py-2 text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-primary-400 transition-colors ${formData.type
? 'bg-white/20 border-white/30 text-white'
: 'bg-pink-500/30 border-pink-400/50 text-pink-200'
}`}
>
<span>{getJobTypeDisplayText()}</span>
<ChevronDown className="w-4 h-4 text-white/50" />
@@ -411,6 +414,34 @@ export function JobreportEditModal({
</div>
</div>
{/* 5행: 초과근무 시간대 (OT > 0일 때만 표시) */}
{formData.ot > 0 && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/70 text-sm font-medium mb-2">
</label>
<input
type="time"
value={formData.otStart}
onChange={(e) => handleFieldChange('otStart', e.target.value)}
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-2">
</label>
<input
type="time"
value={formData.otEnd}
onChange={(e) => handleFieldChange('otEnd', e.target.value)}
className="w-full bg-white/20 border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
)}
{/* 업무내용 */}
<div>
<label className="block text-white/70 text-sm font-medium mb-2">

View File

@@ -0,0 +1,140 @@
import { useEffect, useState } from 'react';
import { X, RefreshCw } from 'lucide-react';
import { comms } from '@/communication';
import { JobReportTypeItem } from '@/types';
interface JobreportTypeModalProps {
isOpen: boolean;
onClose: () => void;
startDate: string;
endDate: string;
userId: string;
}
export function JobreportTypeModal({
isOpen,
onClose,
startDate,
endDate,
userId,
}: JobreportTypeModalProps) {
const [loading, setLoading] = useState(false);
const [items, setItems] = useState<JobReportTypeItem[]>([]);
useEffect(() => {
if (isOpen && startDate && endDate) {
loadData();
}
}, [isOpen, startDate, endDate, userId]);
const loadData = async () => {
setLoading(true);
try {
const response = await comms.getJobReportTypeList(startDate, endDate, userId);
if (response.Success && response.Data) {
setItems(response.Data);
} else {
setItems([]);
}
} catch (error) {
console.error('업무형태별 집계 로드 오류:', error);
setItems([]);
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
const totalHrs = items.reduce((acc, item) => acc + (item.hrs || 0), 0);
const totalOt = items.reduce((acc, item) => acc + (item.ot || 0), 0);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
<div className="bg-gray-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
<h3 className="text-lg font-semibold text-white"> </h3>
<button
onClick={onClose}
className="text-white/50 hover:text-white transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* 컨텐츠 */}
<div className="p-6">
<div className="mb-4 text-white/70 text-sm">
: {startDate} ~ {endDate}
</div>
<div className="overflow-hidden rounded-xl border border-white/10">
<table className="w-full">
<thead className="bg-white/10">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase"></th>
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase"></th>
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase"></th>
<th className="px-4 py-3 text-right text-xs font-medium text-white/70 uppercase">OT</th>
</tr>
</thead>
<tbody className="divide-y divide-white/10">
{loading ? (
<tr>
<td colSpan={4} className="px-4 py-8 text-center">
<div className="flex items-center justify-center">
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
<span className="text-white/50"> ...</span>
</div>
</td>
</tr>
) : items.length === 0 ? (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-white/50">
.
</td>
</tr>
) : (
<>
{items.map((item, index) => (
<tr key={index} className="hover:bg-white/5 transition-colors">
<td className="px-4 py-3 text-white text-sm">{item.type || '-'}</td>
<td className="px-4 py-3 text-white text-sm text-right">{item.count}</td>
<td className="px-4 py-3 text-white text-sm text-right">{item.hrs}h</td>
<td className="px-4 py-3 text-white text-sm text-right">
{item.ot > 0 ? <span className="text-warning-400">{item.ot}h</span> : '-'}
</td>
</tr>
))}
{/* 합계 */}
<tr className="bg-white/5 font-semibold">
<td className="px-4 py-3 text-white text-sm"></td>
<td className="px-4 py-3 text-white text-sm text-right">
{items.reduce((acc, item) => acc + item.count, 0)}
</td>
<td className="px-4 py-3 text-white text-sm text-right">{totalHrs}h</td>
<td className="px-4 py-3 text-white text-sm text-right">
{totalOt > 0 ? <span className="text-warning-400">{totalOt}h</span> : '-'}
</td>
</tr>
</>
)}
</tbody>
</table>
</div>
</div>
{/* 푸터 */}
<div className="px-6 py-4 border-t border-white/10 flex justify-end bg-white/5">
<button
onClick={onClose}
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
>
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,295 @@
import React, { useState, useEffect } from 'react';
import { X, Save, Calendar, Clock, FileText, User } from 'lucide-react';
import { KuntaeModel } from '@/types';
interface KuntaeEditModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: KuntaeFormData) => Promise<void>;
initialData?: KuntaeModel | null;
mode: 'add' | 'edit' | 'copy';
}
export interface KuntaeFormData {
idx?: number;
cate: string;
sdate: string;
edate: string;
term: number;
crtime: number;
termDr: number;
drTime: number;
contents: string;
uid: string;
}
const CATE_OPTIONS = ['연차', '대체', '공가', '경조', '병가', '오전반차', '오후반차', '조퇴', '외출', '지각', '결근', '휴직', '교육', '출장', '재택', '특근', '당직', '기타'];
export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }: KuntaeEditModalProps) {
const [formData, setFormData] = useState<KuntaeFormData>({
cate: '연차',
sdate: new Date().toISOString().split('T')[0],
edate: new Date().toISOString().split('T')[0],
term: 1,
crtime: 0,
termDr: 0,
drTime: 0,
contents: '',
uid: '',
});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (isOpen) {
if (initialData) {
setFormData({
idx: mode === 'edit' ? initialData.idx : undefined,
cate: initialData.cate || '연차',
sdate: initialData.sdate || new Date().toISOString().split('T')[0],
edate: initialData.edate || new Date().toISOString().split('T')[0],
term: initialData.term || 0,
crtime: initialData.crtime || 0,
termDr: initialData.termDr || 0,
drTime: initialData.DrTime || 0,
contents: initialData.contents || '',
uid: initialData.uid || '',
});
} else {
// 초기화
setFormData({
cate: '연차',
sdate: new Date().toISOString().split('T')[0],
edate: new Date().toISOString().split('T')[0],
term: 1,
crtime: 0,
termDr: 0,
drTime: 0,
contents: '',
uid: '', // 상위 컴포넌트에서 현재 사용자 ID를 주입받거나 여기서 처리해야 함
});
}
}
}, [isOpen, initialData, mode]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'term' || name === 'crtime' || name === 'termDr' || name === 'drTime'
? parseFloat(value) || 0
: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
await onSave(formData);
onClose();
} catch (error) {
console.error('Save error:', error);
alert('저장 중 오류가 발생했습니다.');
} finally {
setSaving(false);
}
};
// 날짜 변경 시 기간 자동 계산 (단순 1일 차이)
useEffect(() => {
if (formData.sdate && formData.edate) {
const start = new Date(formData.sdate);
const end = new Date(formData.edate);
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
// 연차/휴가 등일 때만 자동 계산
if (['연차', '공가', '경조', '병가', '교육', '출장'].includes(formData.cate)) {
setFormData(prev => ({ ...prev, term: diffDays }));
}
}
}, [formData.sdate, formData.edate, formData.cate]);
if (!isOpen) return null;
const title = mode === 'add' ? '근태 등록' : mode === 'edit' ? '근태 수정' : '근태 복사 등록';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
<div className="bg-[#1e1e2e] rounded-2xl shadow-2xl w-full max-w-lg border border-white/10 overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center bg-white/5">
<h2 className="text-xl font-bold text-white flex items-center">
<Calendar className="w-5 h-5 mr-2 text-primary-400" />
{title}
</h2>
<button onClick={onClose} className="text-white/50 hover:text-white transition-colors">
<X className="w-6 h-6" />
</button>
</div>
{/* 폼 */}
<form onSubmit={handleSubmit} className="p-6 space-y-4">
{/* 구분 및 사용자 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/70 text-sm font-medium mb-1"></label>
<select
name="cate"
value={formData.cate}
onChange={handleChange}
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
>
{CATE_OPTIONS.map(opt => (
<option key={opt} value={opt} className="bg-[#1e1e2e]">{opt}</option>
))}
</select>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-1"> ID</label>
<div className="relative">
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
<input
type="text"
name="uid"
value={formData.uid}
onChange={handleChange}
readOnly={mode === 'edit'} // 수정 시에는 사용자 변경 불가
className={`w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 ${mode === 'edit' ? 'opacity-50 cursor-not-allowed' : ''}`}
placeholder="사번 입력"
/>
</div>
</div>
</div>
{/* 기간 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/70 text-sm font-medium mb-1"></label>
<input
type="date"
name="sdate"
value={formData.sdate}
onChange={handleChange}
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-1"></label>
<input
type="date"
name="edate"
value={formData.edate}
onChange={handleChange}
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
{/* 사용량 (일/시간) */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/70 text-sm font-medium mb-1"> ()</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
<input
type="number"
name="term"
value={formData.term}
onChange={handleChange}
step="0.5"
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-1"> ()</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/50" />
<input
type="number"
name="crtime"
value={formData.crtime}
onChange={handleChange}
step="0.5"
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
</div>
{/* 발생량 (일/시간) - 대체근무 등 발생 시 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-white/70 text-sm font-medium mb-1"> ()</label>
<input
type="number"
name="termDr"
value={formData.termDr}
onChange={handleChange}
step="0.5"
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-1"> ()</label>
<input
type="number"
name="drTime"
value={formData.drTime}
onChange={handleChange}
step="0.5"
className="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
/>
</div>
</div>
{/* 내용 */}
<div>
<label className="block text-white/70 text-sm font-medium mb-1"></label>
<div className="relative">
<FileText className="absolute left-3 top-3 w-4 h-4 text-white/50" />
<textarea
name="contents"
value={formData.contents}
onChange={handleChange}
rows={3}
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none"
placeholder="근태 사유 또는 내용 입력"
/>
</div>
</div>
{/* 버튼 */}
<div className="flex justify-end space-x-3 pt-4 border-t border-white/10">
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors"
>
</button>
<button
type="submit"
disabled={saving}
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
>
{saving ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}