feat: 휴가 신청 다이얼로그 개선 (관리자 상용구, UI 가시성, 색상 통일, 공통 알림 컴포넌트 추가)

This commit is contained in:
backuppc
2025-12-29 17:33:50 +09:00
parent 9d6b27fae4
commit d11a86b58d
6 changed files with 947 additions and 606 deletions

View File

@@ -0,0 +1,12 @@
export function DevelopmentNotice() {
return (
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4 flex items-center justify-center animate-pulse">
<span className="text-yellow-400 font-medium flex items-center gap-2">
<span className="text-xl"></span>
. .
</span>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { X, Save, Calendar, Clock, MapPin, User, FileText, AlertCircle } from 'lucide-react';
import { comms } from '@/communication';
import { comms } from '../../communication';
import { DevelopmentNotice } from '../common/DevelopmentNotice';
import { HolidayRequest, CommonCode } from '@/types';
interface HolidayRequestDialogProps {
@@ -35,6 +36,7 @@ export function HolidayRequestDialog({
backup: []
});
const [users, setUsers] = useState<Array<{ id: string; name: string }>>([]);
const [adminComments, setAdminComments] = useState<CommonCode[]>([]); // Code 54
// Form State
const [formData, setFormData] = useState<HolidayRequest>({
@@ -59,7 +61,9 @@ export function HolidayRequestDialog({
sendmail: false
});
const [balanceMessage, setBalanceMessage] = useState('');
const [requestType, setRequestType] = useState<'day' | 'time' | 'out'>('day'); // day: 휴가, time: 대체, out: 외출
const isReadOnly = formData.conf === 1 && userLevel < 5;
useEffect(() => {
if (isOpen) {
@@ -67,9 +71,40 @@ export function HolidayRequestDialog({
if (userLevel >= 5) {
loadUsers();
}
if (initialData) {
setFormData({ ...initialData });
const confValue = initialData.conf;
const convertedConf = Number(initialData.conf ?? 0);
console.log('Dialog Debug:', {
initialData,
rawConf: confValue,
typeOfConf: typeof confValue,
convertedConf,
finalFormDataConf: convertedConf
});
setFormData({
idx: initialData.idx,
gcode: initialData.gcode || '',
uid: initialData.uid || currentUserId || '',
cate: initialData.cate || '',
sdate: initialData.sdate ? initialData.sdate.split('T')[0] : new Date().toISOString().split('T')[0],
edate: initialData.edate ? initialData.edate.split('T')[0] : new Date().toISOString().split('T')[0],
HolyDays: initialData.HolyDays || 0,
HolyTimes: initialData.HolyTimes || 0,
HolyReason: initialData.HolyReason || (initialData as any).holyReason || '',
HolyLocation: initialData.HolyLocation || (initialData as any).holyLocation || '',
HolyBackup: initialData.HolyBackup || (initialData as any).holyBackup || '',
Remark: initialData.Remark || (initialData as any).remark || '',
wuid: initialData.wuid || '',
wdate: initialData.wdate || '',
Response: initialData.Response || (initialData as any).response || '',
conf: convertedConf,
stime: initialData.stime || '09:00',
etime: initialData.etime || '18:00',
sendmail: initialData.sendmail || false,
conf_id: initialData.conf_id || '',
conf_time: initialData.conf_time || ''
});
// Determine request type based on data
if (initialData.cate === '외출') {
setRequestType('out');
@@ -106,21 +141,39 @@ export function HolidayRequestDialog({
}
}, [isOpen, initialData, currentUserId]);
// Handle ESC key
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (isOpen && e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]);
const loadCodes = async () => {
try {
const [cateRes, reasonRes, locationRes, backupRes] = await Promise.all([
comms.getCommonList('50'),
comms.getCommonList('51'),
comms.getCommonList('52'),
comms.getCommonList('53')
]);
// Execute sequentially to avoid WebSocket response race conditions
const cateRes = await comms.getCommonList('50');
const reasonRes = await comms.getCommonList('51');
const locationRes = await comms.getCommonList('52');
const backupRes = await comms.getCommonList('53');
console.log('Fetched Common Codes:', {
cate: cateRes,
reason: reasonRes,
location: locationRes,
backup: backupRes
});
setCodes({
cate: cateRes || [],
reason: reasonRes || [],
location: locationRes || [],
backup: backupRes || []
});
// Set default category if new
if (!initialData && cateRes && cateRes.length > 0) {
setFormData(prev => ({ ...prev, cate: cateRes[0].svalue }));
@@ -145,12 +198,104 @@ export function HolidayRequestDialog({
}
};
useEffect(() => {
const fetchBalance = async () => {
// Only for new requests
if (formData.idx !== 0 || !isOpen || !formData.sdate || !formData.uid) return;
try {
const year = formData.sdate.substring(0, 4);
const response = await comms.getHolydayBalance(year, formData.uid);
// C# logic replication for message formatting
// [기준:YYYY-MM-DD] => [분류] N일 남음(N%사용), ...
const basedate = new Date(formData.sdate);
basedate.setDate(basedate.getDate() - 1);
const baseDateStr = basedate.toISOString().split('T')[0]; // Format manually if needed, but ISO YYYY-MM-DD ok
if (!response.Success || !response.Data) {
setBalanceMessage(`[기준:${baseDateStr}] => 등록된 근태자료가 없습니다`);
return;
}
const items: string[] = [];
response.Data.forEach(item => {
// Days
if (item.TotalGenDays !== 0 || item.TotalUseDays !== 0) { // Actually checks if Total != 0 or Remain != 0 in C#
const remain = item.TotalGenDays - item.TotalUseDays;
// C# checks: if (val[0] != "0" || val[2] != "0") (Total or Remain)
// Here checking Total != 0 is usually sufficient.
if (item.TotalGenDays > 0) {
const perc = (item.TotalUseDays / item.TotalGenDays) * 100;
items.push(`[${item.cate}] ${remain.toFixed(1)}일 남음(${perc.toFixed(1)}%사용)`);
}
}
// Times
if (item.TotalGenHours !== 0) {
const remain = item.TotalGenHours - item.TotalUseHours;
if (item.TotalGenHours > 0) {
const perc = (item.TotalUseHours / item.TotalGenHours) * 100;
items.push(`[${item.cate}] ${remain.toFixed(1)}시간 남음(${perc.toFixed(1)}%사용)`);
}
}
});
if (items.length === 0) {
setBalanceMessage(`[기준:${baseDateStr}] => 등록된 근태자료가 없습니다`);
} else {
setBalanceMessage(`[기준:${baseDateStr}] => ${items.join(',')}`);
}
} catch (error) {
console.error('Failed to fetch balance:', error);
}
};
fetchBalance();
}, [formData.sdate, formData.uid, formData.idx, isOpen]);
const handleTypeChange = (type: 'day' | 'time' | 'out') => {
setRequestType(type);
setFormData(prev => {
let newCate = prev.cate;
if (type === 'time') newCate = '대체';
else if (type === 'out') newCate = '외출';
// If switching back to 'day', we might want to reset cate if it was fixed to '대체' or '외출',
// or just leave it and let the user change it via Select.
// But typically '대체'/'외출' are not valid options for 'day' (general holiday).
// So let's reset to empty or first option if we have codes.
else if (prev.cate === '대체' || prev.cate === '외출') newCate = codes.cate[0]?.svalue || '';
return { ...prev, cate: newCate };
});
};
const handleSave = async () => {
// Validation
if (!formData.cate && requestType === 'day') {
alert('구분을 선택하세요.');
return;
}
if (requestType === 'day' && (!formData.HolyDays || formData.HolyDays <= 0)) {
alert('일수를 입력하세요.');
return;
}
if ((requestType === 'time' || requestType === 'out') && (!formData.HolyTimes || formData.HolyTimes <= 0)) {
alert('시간을 입력하세요.');
return;
}
if (requestType === 'out') {
if (!formData.stime) {
alert('시작 시간을 입력하세요.');
return;
}
if (!formData.etime) {
alert('종료 시간을 입력하세요.');
return;
}
}
if (formData.sdate > formData.edate) {
alert('종료일이 시작일보다 빠를 수 없습니다.');
return;
@@ -161,8 +306,7 @@ export function HolidayRequestDialog({
if (requestType === 'out') {
dataToSave.cate = '외출';
dataToSave.HolyDays = 0;
// Calculate times if needed, or rely on user input?
// WinForms doesn't seem to auto-calc times for 'out', just saves stime/etime.
// Calculate times if needed
} else if (requestType === 'time') {
dataToSave.cate = '대체';
dataToSave.HolyDays = 0;
@@ -171,15 +315,24 @@ export function HolidayRequestDialog({
dataToSave.HolyTimes = 0;
dataToSave.stime = '';
dataToSave.etime = '';
// Calculate days
const start = new Date(dataToSave.sdate);
const end = new Date(dataToSave.edate);
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
dataToSave.HolyDays = diffDays;
// dataToSave.HolyDays is already set from formData (manual input)
}
// New request specific handling
if (formData.idx === 0) {
// Validate Remark is not empty (as per user request)
if (!dataToSave.Remark || dataToSave.Remark.trim() === '') {
alert('비고를 입력해주세요');
return; // Needs to focus textarea but we can just return
}
// Append balance message if not already present (simplified check)
if (balanceMessage && !dataToSave.Remark.includes(balanceMessage)) {
dataToSave.Remark = dataToSave.Remark.trim() + '\r\n' + balanceMessage;
}
}
setLoading(true);
try {
const response = await comms.saveHolidayRequest(dataToSave);
@@ -199,303 +352,384 @@ export function HolidayRequestDialog({
if (!isOpen) return null;
const title = formData.idx === 0 ? '휴가/외출 신청' : '신청 내역 수정';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in">
<div className="bg-[#1e1e2e] rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto border border-white/10">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<h2 className="text-xl font-bold text-gray-800">
{formData.idx === 0 ? '휴가/외출 신청' : '신청 내역 수정'}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 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="p-2 hover:bg-gray-100 rounded-full transition-colors">
<X className="w-5 h-5 text-gray-500" />
<button onClick={onClose} className="text-white/50 hover:text-white transition-colors">
<X className="w-5 h-5" />
</button>
</div>
{/* 개발중 알림 */}
<div className="px-6 pt-6">
<DevelopmentNotice />
</div>
{/* Body */}
<div className="p-6 space-y-6">
{/* Request Type */}
<div className="flex gap-4 p-4 bg-gray-50 rounded-lg">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="type"
checked={requestType === 'day'}
onChange={() => setRequestType('day')}
className="w-4 h-4 text-blue-600"
disabled={formData.idx > 0 && initialData?.cate !== '대체' && initialData?.cate !== '외출'}
/>
<span className="font-medium text-gray-700"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="type"
checked={requestType === 'time'}
onChange={() => setRequestType('time')}
className="w-4 h-4 text-blue-600"
disabled={formData.idx > 0 && initialData?.cate !== '대체'}
/>
<span className="font-medium text-gray-700">()</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="type"
checked={requestType === 'out'}
onChange={() => setRequestType('out')}
className="w-4 h-4 text-blue-600"
disabled={formData.idx > 0 && initialData?.cate !== '외출'}
/>
<span className="font-medium text-gray-700"></span>
</label>
</div>
{/* User Selection (Admin only) */}
{userLevel >= 5 && (
<div className="grid grid-cols-1 gap-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<User className="w-4 h-4" />
</label>
<select
value={formData.uid}
onChange={(e) => setFormData({ ...formData, uid: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={formData.idx > 0}
>
{users.map(user => (
<option key={user.id} value={user.id}>{user.name} ({user.id})</option>
))}
</select>
</div>
)}
{/* Date & Time */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
</label>
<input
type="date"
value={formData.sdate}
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="w-4 h-4" />
</label>
<input
type="date"
value={formData.edate}
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
{(requestType === 'time' || requestType === 'out') && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Clock className="w-4 h-4" />
</label>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Left Column: Inputs */}
<div className="space-y-6">
{/* Request Type */}
<div className="flex gap-4 p-4 bg-white/5 rounded-lg border border-white/5">
<label className={`flex items-center gap-2 ${(isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체' && initialData?.cate !== '외출')) ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'day' ? 'border-green-400' : 'border-white/30'}`}>
{requestType === 'day' && <div className="w-2 h-2 rounded-full bg-green-400" />}
</div>
<input
type="time"
value={formData.stime}
onChange={(e) => setFormData({ ...formData, stime: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
type="radio"
name="type"
checked={requestType === 'day'}
onChange={() => handleTypeChange('day')}
className="hidden"
disabled={isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체' && initialData?.cate !== '외출')}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Clock className="w-4 h-4" />
</label>
<input
type="time"
value={formData.etime}
onChange={(e) => setFormData({ ...formData, etime: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
)}
{/* Category & Reason */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<FileText className="w-4 h-4" />
<span className="font-medium text-white/90"></span>
</label>
{requestType === 'day' ? (
<label className={`flex items-center gap-2 ${(isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체')) ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'time' ? 'border-green-400' : 'border-white/30'}`}>
{requestType === 'time' && <div className="w-2 h-2 rounded-full bg-green-400" />}
</div>
<input
type="radio"
name="type"
checked={requestType === 'time'}
onChange={() => handleTypeChange('time')}
className="hidden"
disabled={isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체')}
/>
<span className="font-medium text-white/90"></span>
</label>
<label className={`flex items-center gap-2 ${isReadOnly ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'out' ? 'border-green-400' : 'border-white/30'}`}>
{requestType === 'out' && <div className="w-2 h-2 rounded-full bg-green-400" />}
</div>
<input
type="radio"
name="type"
checked={requestType === 'out'}
onChange={() => handleTypeChange('out')}
className="hidden"
disabled={isReadOnly}
/>
<span className="font-medium text-white/90"></span>
</label>
</div>
{/* User Selection (Admin only) */}
{userLevel >= 5 && (
<div className="grid grid-cols-1 gap-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<User className="w-4 h-4" />
</label>
<select
value={formData.cate}
onChange={(e) => setFormData({ ...formData, cate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={formData.uid}
onChange={(e) => setFormData({ ...formData, uid: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
disabled={isReadOnly || formData.idx > 0}
>
{codes.cate.map(code => (
<option key={code.code} value={code.svalue}>{code.svalue}</option>
{users.map(user => (
<option key={user.id} value={user.id} className="bg-[#1e1e2e]">{user.name} ({user.id})</option>
))}
</select>
) : (
</div>
)}
{/* Date & Time */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<Calendar className="w-4 h-4" />
</label>
<input
type="text"
value={requestType === 'out' ? '외출' : '대체'}
disabled
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500"
type="date"
value={formData.sdate}
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={isReadOnly}
/>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
</label>
<select
value={formData.HolyReason}
onChange={(e) => setFormData({ ...formData, HolyReason: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
{codes.reason.map(code => (
<option key={code.code} value={code.svalue}>{code.svalue}</option>
))}
</select>
</div>
</div>
{/* Location & Backup */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<MapPin className="w-4 h-4" />
</label>
<select
value={formData.HolyLocation}
onChange={(e) => setFormData({ ...formData, HolyLocation: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
{codes.location.map(code => (
<option key={code.code} value={code.svalue}>{code.svalue}</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 flex items-center gap-2">
<User className="w-4 h-4" />
</label>
<select
value={formData.HolyBackup}
onChange={(e) => setFormData({ ...formData, HolyBackup: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value=""></option>
{codes.backup.map(code => (
<option key={code.code} value={code.svalue}>{code.svalue}</option>
))}
</select>
</div>
</div>
{/* Days & Times (Manual Override) */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<input
type="number"
step="0.5"
value={formData.HolyDays}
onChange={(e) => setFormData({ ...formData, HolyDays: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={requestType !== 'day'}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<input
type="number"
step="0.5"
value={formData.HolyTimes}
onChange={(e) => setFormData({ ...formData, HolyTimes: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={requestType === 'day'}
/>
</div>
</div>
{/* Remark */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"></label>
<textarea
value={formData.Remark}
onChange={(e) => setFormData({ ...formData, Remark: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent h-20 resize-none"
/>
</div>
{/* Admin Response & Confirmation */}
{userLevel >= 5 && (
<div className="p-4 bg-blue-50 rounded-lg space-y-4 border border-blue-100">
<h3 className="font-semibold text-blue-800"> </h3>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="conf"
checked={formData.conf === 0}
onChange={() => setFormData({ ...formData, conf: 0 })}
className="w-4 h-4 text-blue-600"
/>
<span className="text-gray-700"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="conf"
checked={formData.conf === 1}
onChange={() => setFormData({ ...formData, conf: 1 })}
className="w-4 h-4 text-green-600"
/>
<span className="text-gray-700"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="conf"
checked={formData.conf === 2}
onChange={() => setFormData({ ...formData, conf: 2 })}
className="w-4 h-4 text-red-600"
/>
<span className="text-gray-700"></span>
</label>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700"> </label>
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<Calendar className="w-4 h-4" />
</label>
<input
type="text"
value={formData.Response}
onChange={(e) => setFormData({ ...formData, Response: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
type="date"
value={formData.edate}
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={isReadOnly}
/>
</div>
</div>
)}
{/* Category & Reason */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<FileText className="w-4 h-4" />
</label>
{requestType === 'day' ? (
<select
value={formData.cate}
onChange={(e) => setFormData({ ...formData, cate: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={isReadOnly}
>
{codes.cate.map(code => {
const val = code.memo || (code as any).Memo || code.svalue || (code as any).SValue || (code as any).value || (code as any).Value || (code as any).name || (code as any).Name;
const key = code.code || (code as any).Code || (code as any).key || (code as any).Key || val;
return (
<option key={key} value={val} className="bg-[#1e1e2e]">
{val}
</option>
);
})}
</select>
) : (
<input
type="text"
value={formData.cate}
readOnly
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50 cursor-not-allowed"
/>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
</label>
<input
type="text"
list="reason-list"
value={formData.HolyReason || ''}
onChange={(e) => setFormData({ ...formData, HolyReason: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
<datalist id="reason-list">
{codes.reason.map(code => {
const val = code.memo || (code as any).Memo || code.svalue || (code as any).SValue || (code as any).value || (code as any).Value || (code as any).name || (code as any).Name;
const key = code.code || (code as any).Code || (code as any).key || (code as any).Key || val;
return (
<option key={key} value={val} />
);
})}
</datalist>
</div>
</div>
{/* Location & Backup */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<MapPin className="w-4 h-4" />
</label>
<input
type="text"
list="location-list"
value={formData.HolyLocation || ''}
onChange={(e) => setFormData({ ...formData, HolyLocation: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
<datalist id="location-list">
{codes.location.map(code => {
const val = code.memo || (code as any).Memo || code.svalue || (code as any).SValue || (code as any).value || (code as any).Value || (code as any).name || (code as any).Name;
const key = code.code || (code as any).Code || (code as any).key || (code as any).Key || val;
return (
<option key={key} value={val} />
);
})}
</datalist>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<User className="w-4 h-4" />
</label>
<input
type="text"
list="backup-list"
value={formData.HolyBackup || ''}
onChange={(e) => setFormData({ ...formData, HolyBackup: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
<datalist id="backup-list">
{codes.backup.map(code => {
const val = code.memo || (code as any).Memo || code.svalue || (code as any).SValue || (code as any).value || (code as any).Value || (code as any).name || (code as any).Name;
const key = code.code || (code as any).Code || (code as any).key || (code as any).Key || val;
return (
<option key={key} value={val} />
);
})}
</datalist>
</div>
</div>
{/* Days & Times (Manual Override) */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white/70"></label>
<input
type="number"
step="0.5"
value={formData.HolyDays}
onChange={(e) => setFormData({ ...formData, HolyDays: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10 disabled:text-white/30 disabled:cursor-not-allowed"
disabled={isReadOnly || requestType !== 'day'}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70"></label>
<input
type="number"
step="0.5"
value={formData.HolyTimes}
onChange={(e) => setFormData({ ...formData, HolyTimes: parseFloat(e.target.value) })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10 disabled:text-white/30 disabled:cursor-not-allowed"
disabled={isReadOnly || requestType === 'day'}
/>
</div>
</div>
{/* Outing Time (Only for 'out') */}
{requestType === 'out' && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<Clock className="w-4 h-4" />
</label>
<input
type="time"
value={formData.stime}
onChange={(e) => setFormData({ ...formData, stime: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={isReadOnly}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<Clock className="w-4 h-4" />
</label>
<input
type="time"
value={formData.etime}
onChange={(e) => setFormData({ ...formData, etime: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={isReadOnly}
/>
</div>
</div>
)}
</div>
{/* Right Column: Remark */}
<div className="md:col-span-1 h-full">
<div className="flex flex-col h-full space-y-2">
<label className="text-sm font-medium text-white/70"></label>
<textarea
value={formData.Remark}
onChange={(e) => setFormData({ ...formData, Remark: e.target.value })}
className="w-full flex-1 px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none min-h-[200px] disabled:bg-white/10"
placeholder="비고 사항을 입력하세요..."
disabled={isReadOnly}
/>
{/* Admin Response & Confirmation (Moved to Right) */}
<div className="p-4 bg-primary-500/10 rounded-lg space-y-4 border border-primary-500/20 mt-4">
<h3 className="font-semibold text-primary-400"> </h3>
<div className="flex gap-4">
<label className={`flex items-center gap-2 ${userLevel < 5 ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 0 ? 'border-primary-400' : 'border-white/30'}`}>
{formData.conf === 0 && <div className="w-2 h-2 rounded-full bg-primary-400" />}
</div>
<input
type="radio"
name="conf"
checked={formData.conf === 0}
onChange={() => setFormData({ ...formData, conf: 0 })}
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
</label>
<label className={`flex items-center gap-2 ${userLevel < 5 ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 1 ? 'border-green-400' : 'border-white/30'}`}>
{formData.conf === 1 && <div className="w-2 h-2 rounded-full bg-green-400" />}
</div>
<input
type="radio"
name="conf"
checked={formData.conf === 1}
onChange={() => setFormData({ ...formData, conf: 1 })}
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
</label>
<label className={`flex items-center gap-2 ${userLevel < 5 ? 'cursor-not-allowed' : 'cursor-pointer'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 2 ? 'border-red-400' : 'border-white/30'}`}>
{formData.conf === 2 && <div className="w-2 h-2 rounded-full bg-red-400" />}
</div>
<input
type="radio"
name="conf"
checked={formData.conf === 2}
onChange={() => setFormData({ ...formData, conf: 2 })}
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
</label>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70"> </label>
<input
type="text"
list="adminCommentsList"
value={formData.Response}
onChange={(e) => setFormData({ ...formData, Response: e.target.value })}
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-white/10"
disabled={userLevel < 5}
/>
<datalist id="adminCommentsList">
{adminComments.map((item) => (
<option key={(item as any).code || (item as any).Code} value={(item as any).memo || (item as any).Memo || (item as any).svalue || (item as any).SValue} />
))}
</datalist>
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-100 bg-gray-50 rounded-b-xl">
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10 bg-white/5">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:bg-gray-200 rounded-lg transition-colors font-medium"
className="px-4 py-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors font-medium"
>
</button>
<button
onClick={handleSave}
disabled={loading}
className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg shadow-lg shadow-blue-500/30 transition-all font-medium disabled:opacity-50"
disabled={loading || isReadOnly}
className="flex items-center gap-2 px-6 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg shadow-lg shadow-primary-500/30 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
{loading ? '저장 중...' : '저장'}