feat: apply dark glassmorphism theme to License list and JobReport daily summary dialog

This commit is contained in:
backuppc
2025-12-30 14:16:46 +09:00
parent d11a86b58d
commit 959b1b685d
32 changed files with 3720 additions and 2162 deletions

View File

@@ -0,0 +1,277 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon } from 'lucide-react';
import clsx from 'clsx';
interface DateRangePickerProps {
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD
onChange: (startDate: string, endDate: string) => void;
align?: 'horizontal' | 'vertical';
}
export function DateRangePicker({ startDate, endDate, onChange, align = 'horizontal' }: DateRangePickerProps) {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Internal state for delayed update
const [localStart, setLocalStart] = useState(startDate);
const [localEnd, setLocalEnd] = useState(endDate);
// View states for the two calendars
const [leftViewDate, setLeftViewDate] = useState(new Date());
const [rightViewDate, setRightViewDate] = useState(new Date());
// Sync props to internal state and init view dates on open
useEffect(() => {
if (isOpen) {
setLocalStart(startDate);
setLocalEnd(endDate);
const sDate = startDate ? new Date(startDate) : new Date();
const eDate = endDate ? new Date(endDate) : new Date();
setLeftViewDate(new Date(sDate.getFullYear(), sDate.getMonth(), 1));
setRightViewDate(new Date(eDate.getFullYear(), eDate.getMonth(), 1));
}
}, [isOpen, startDate, endDate]);
// Close on click outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const setPreset = (type: 'today' | 'tomorrow' | 'thisWeek' | 'thisMonth' | 'lastMonth') => {
const today = new Date();
let newStart = today;
let newEnd = today;
switch (type) {
case 'today':
break;
case 'tomorrow':
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
newStart = tomorrow;
newEnd = tomorrow;
break;
case 'thisWeek':
const day = today.getDay();
const diff = today.getDate() - day; // Sunday start
newStart = new Date(today.setDate(diff));
newEnd = new Date(today.setDate(diff + 6));
break;
case 'thisMonth':
newStart = new Date(today.getFullYear(), today.getMonth(), 1);
newEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
break;
case 'lastMonth':
newStart = new Date(today.getFullYear(), today.getMonth() - 1, 1);
newEnd = new Date(today.getFullYear(), today.getMonth(), 0);
break;
}
const sStr = formatDate(newStart);
const eStr = formatDate(newEnd);
setLocalStart(sStr);
setLocalEnd(eStr);
// Update views to match selection
setLeftViewDate(new Date(newStart.getFullYear(), newStart.getMonth(), 1));
setRightViewDate(new Date(newEnd.getFullYear(), newEnd.getMonth(), 1));
};
const handleConfirm = () => {
onChange(localStart, localEnd);
setIsOpen(false);
};
const handleCancel = () => {
setIsOpen(false);
};
const toggleOpen = () => setIsOpen(!isOpen);
// --- Calendar Logic ---
const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate();
const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay();
const renderCalendarGrid = (viewDate: Date, setViewDate: (d: Date) => void, type: 'start' | 'end') => {
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstDay = getFirstDayOfMonth(year, month);
const days = [];
for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="h-8 w-8" />);
}
for (let day = 1; day <= daysInMonth; day++) {
const current = new Date(year, month, day);
const dateStr = formatDate(current);
// Only highlight the date corresponding to the specific calendar type
const isSelected = type === 'start'
? localStart === dateStr
: localEnd === dateStr;
days.push(
<button
key={day}
onClick={() => handleDateClick(dateStr, type)}
className={clsx(
"h-8 w-8 rounded-full flex items-center justify-center text-xs transition-all relative",
// Selected
isSelected ? "bg-blue-600 text-white font-bold hover:bg-blue-500 z-10 ring-2 ring-blue-400/50" : "text-white/80 hover:bg-white/10"
)}
>
{day}
</button>
);
}
return (
<div className="w-[240px]">
{/* Header */}
<div className="flex items-center justify-between mb-2 px-2">
<button onClick={() => {
const d = new Date(viewDate);
d.setMonth(d.getMonth() - 1);
setViewDate(d);
}} className="p-1 hover:bg-white/10 rounded text-white/70">
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-semibold text-white">
{year} {month + 1}
</span>
<button onClick={() => {
const d = new Date(viewDate);
d.setMonth(d.getMonth() + 1);
setViewDate(d);
}} className="p-1 hover:bg-white/10 rounded text-white/70">
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* Days Header */}
<div className="grid grid-cols-7 gap-1 text-center mb-1">
{['일', '월', '화', '수', '목', '금', '토'].map(day => (
<div key={day} className="text-[10px] text-white/40 font-medium py-1">
{day}
</div>
))}
</div>
{/* Days Grid */}
<div className="grid grid-cols-7 gap-1">
{days}
</div>
</div>
);
};
const handleDateClick = (dateStr: string, type: 'start' | 'end') => {
if (type === 'start') {
setLocalStart(dateStr);
// Validation: Start cannot be after End
if (localEnd && new Date(dateStr) > new Date(localEnd)) {
setLocalEnd(dateStr);
}
} else {
setLocalEnd(dateStr);
// Validation: End cannot be before Start
if (localStart && new Date(dateStr) < new Date(localStart)) {
setLocalStart(dateStr);
}
}
};
return (
<div className="relative inline-block" ref={containerRef}>
{/* Trigger */}
<div
onClick={toggleOpen}
className={clsx(
"bg-white/5 border border-white/10 rounded-lg px-3 py-2 cursor-pointer hover:bg-white/10 transition-colors w-fit",
align === 'vertical' ? 'flex flex-col items-start gap-1' : 'flex items-center gap-2'
)}
>
{align === 'horizontal' ? (
<>
<CalendarIcon className="w-4 h-4 text-white/50" />
<span className="text-white text-sm font-medium">
{startDate} <span className="text-white/50 mx-1">~</span> {endDate}
</span>
</>
) : (
<>
<div className="flex items-center gap-2">
<span className="text-white/50 text-xs"></span>
<span className="text-white text-sm font-medium tracking-wider">{startDate}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-white/50 text-xs"></span>
<span className="text-white text-sm font-medium tracking-wider">{endDate}</span>
</div>
</>
)}
</div>
{/* Popover */}
{isOpen && (
<div className="absolute top-full left-0 mt-2 z-[9999] bg-[#1e1e2e] border border-white/10 rounded-xl shadow-xl p-4 w-auto backdrop-blur-md">
{/* Presets Grid */}
<div className="flex flex-wrap gap-2 mb-4 justify-center">
<button onClick={() => setPreset('today')} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 rounded-full text-xs text-white/80 border border-white/5 transition-colors"></button>
<button onClick={() => setPreset('tomorrow')} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 rounded-full text-xs text-white/80 border border-white/5 transition-colors"></button>
<button onClick={() => setPreset('thisWeek')} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 rounded-full text-xs text-white/80 border border-white/5 transition-colors"></button>
<button onClick={() => setPreset('thisMonth')} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 rounded-full text-xs text-white/80 border border-white/5 transition-colors"></button>
<button onClick={() => setPreset('lastMonth')} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 rounded-full text-xs text-white/80 border border-white/5 transition-colors"></button>
</div>
<div className="h-px bg-white/10 mb-4" />
{/* Dual Calendars Container */}
<div className="flex flex-col md:flex-row gap-6">
{renderCalendarGrid(leftViewDate, setLeftViewDate, 'start')}
{/* Mobile/Desktop separator or spacer */}
<div className="hidden md:block w-px bg-white/10 self-stretch my-2"></div>
{renderCalendarGrid(rightViewDate, setRightViewDate, 'end')}
</div>
<div className="h-px bg-white/10 my-4" />
{/* Action Buttons */}
<div className="flex justify-end gap-2">
<button
onClick={handleCancel}
className="px-4 py-2 rounded-lg text-sm text-white/70 hover:bg-white/10 transition-colors"
>
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 rounded-lg text-sm bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors"
>
</button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,13 @@
export function DevelopmentNotice() {
return (
<div className="bg-gradient-to-r from-[#ff1493] via-[#ff69b4] to-[#ff1493] border-2 border-white/50 rounded-xl p-4 flex items-center justify-center shadow-[0_0_25px_rgba(255,20,147,0.6)] backdrop-blur-md mb-6">
<span className="text-white font-black flex items-center gap-3 drop-shadow-[0_2px_4px_rgba(0,0,0,0.3)] tracking-tight">
<span className="text-2xl animate-bounce"></span>
<span className="uppercase italic">Development Mode:</span> . .
<span className="text-2xl animate-bounce" style={{ animationDelay: '0.2s' }}></span>
</span>
</div>
);
}

View File

@@ -0,0 +1,213 @@
import { useState, useRef, useEffect, useMemo } from 'react';
import { Search, Check, ChevronDown, User } from 'lucide-react';
import clsx from 'clsx';
export interface UserInfo {
id: string;
name: string;
process?: string;
level?: number;
useJobReport?: boolean;
outdate?: string | null;
}
interface UserSelectorProps {
users: UserInfo[];
selectedIds: string[]; // Always array for internal consistency, or handle single/multi logic
onChange: (ids: string[]) => void;
multiSelect?: boolean;
placeholder?: string;
className?: string;
// Filter Options
minLevel?: number;
onlyJobReportUsers?: boolean;
includeResigned?: boolean;
}
export function UserSelector({
users,
selectedIds,
onChange,
multiSelect = false,
placeholder = '담당자 선택',
className
}: UserSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
// Filter and Sort users
const filteredAndSortedUsers = useMemo(() => {
let result = users;
// Filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
result = result.filter(user =>
user.name.toLowerCase().includes(query) ||
user.id.toLowerCase().includes(query)
);
}
// Sort: Process -> Name
return result.sort((a, b) => {
// Compare Process
const processA = a.process || '';
const processB = b.process || '';
if (processA !== processB) {
if (processA < processB) return -1;
if (processA > processB) return 1;
}
// Compare Name
return a.name.localeCompare(b.name);
});
}, [users, searchQuery]);
// Handle outside click
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSelect = (userId: string) => {
if (multiSelect) {
if (selectedIds.includes(userId)) {
onChange(selectedIds.filter(id => id !== userId));
} else {
onChange([...selectedIds, userId]);
}
} else {
// Single select: toggle or set
if (selectedIds.includes(userId)) {
onChange([]); // Deselect if clicked again? Or just prevent? Usually deselect is allowed.
} else {
onChange([userId]);
setIsOpen(false); // Close on selection for single mode
}
}
};
const selectedUserList = useMemo(() => users.filter(u => selectedIds.includes(u.id)), [users, selectedIds]);
const getDisplayText = () => {
if (selectedIds.length === 0) return placeholder;
if (selectedUserList.length === 0) {
return selectedIds[0];
}
if (selectedUserList.length === 1) {
return `${selectedUserList[0].name}(${selectedUserList[0].id})`;
}
return `${selectedUserList[0].name}${selectedUserList.length - 1}`;
};
return (
<div className={clsx("relative z-30", className)} ref={containerRef}>
{/* Trigger Button */}
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className={clsx(
"flex items-center justify-between w-full h-[60px] px-3 bg-white/5 border border-white/10 rounded-lg text-white transition-colors focus:outline-none focus:ring-2 focus:ring-primary-400 hover:bg-white/10",
isOpen && "ring-2 ring-primary-400"
)}
>
<div className="flex items-center gap-3 overflow-hidden text-left">
<User className="w-5 h-5 text-white/50 flex-shrink-0" />
<div className="flex flex-col justify-center gap-0.5">
{selectedUserList.length === 1 ? (
<>
<div className="text-sm font-medium leading-none flex items-center">
{selectedUserList[0].name}
<span className="text-white/40 text-xs ml-1 font-normal">({selectedUserList[0].id})</span>
</div>
{selectedUserList[0].process ? (
<div className="text-xs text-white/60 leading-none">
{selectedUserList[0].process}
</div>
) : (
<div className="text-xs text-white/30 leading-none">
-
</div>
)}
</>
) : (
<span className="truncate text-sm">
{getDisplayText()}
</span>
)}
</div>
</div>
<ChevronDown className="w-4 h-4 text-white/50 ml-2 flex-shrink-0" />
</button>
{/* Popover */}
{isOpen && (
<div className="absolute top-full mt-2 w-[360px] bg-[#1e293b] border border-white/10 rounded-xl shadow-2xl overflow-hidden animate-fade-in-up z-50">
<div className="p-2 space-y-2">
{/* Search Input */}
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-white/50" />
<input
type="text"
placeholder="사용자 검색"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-8 bg-black/20 border border-white/10 rounded pl-8 pr-2 text-xs text-white focus:outline-none focus:border-primary-500 placeholder-white/30"
autoFocus
/>
</div>
{/* User List */}
<div className="max-h-60 overflow-y-auto space-y-1 scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent pr-1">
{filteredAndSortedUsers.length === 0 ? (
<div className="text-center text-white/40 text-xs py-4">
.
</div>
) : (
filteredAndSortedUsers.map(user => {
const isSelected = selectedIds.includes(user.id);
return (
<button
key={user.id}
onClick={() => handleSelect(user.id)}
className={clsx(
"w-full flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors text-sm",
isSelected ? "bg-primary-500/20 text-primary-200" : "text-white/80 hover:bg-white/10"
)}
>
<div className={clsx(
"w-4 h-4 rounded border flex items-center justify-center transition-colors flex-shrink-0",
isSelected ? "bg-primary-500 border-primary-500" : "border-white/30"
)}>
{isSelected && <Check className="w-3 h-3 text-white" />}
</div>
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-1.5">
{user.process && (
<span className="text-[10px] text-primary-300 bg-primary-500/10 px-1 rounded flex-shrink-0">
{user.process}
</span>
)}
<span className="text-xs font-medium truncate">{user.name}</span>
</div>
<span className="text-[10px] text-white/50">{user.id}</span>
</div>
</button>
);
})
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,12 +0,0 @@
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,7 +1,7 @@
import { useState, useEffect } from 'react';
import { X, Save, Calendar, Clock, MapPin, User, FileText, AlertCircle } from 'lucide-react';
import { comms } from '../../communication';
import { DevelopmentNotice } from '../common/DevelopmentNotice';
import { DevelopmentNotice } from "../DevelopmentNotice";
import { HolidayRequest, CommonCode } from '@/types';
interface HolidayRequestDialogProps {
@@ -36,7 +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>({
@@ -356,14 +356,14 @@ export function HolidayRequestDialog({
return (
<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-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" />
<div className="bg-paper rounded-2xl shadow-[0_0_40px_rgba(var(--color-primary),0.4)] w-full max-w-4xl max-h-[90vh] overflow-y-auto border-2 border-primary">
{/* Header - Lively Gradient */}
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-primary-500 via-primary-400 to-primary-600">
<h2 className="text-xl font-bold text-white flex items-center drop-shadow-md">
<Calendar className="w-6 h-6 mr-2 text-white animate-pulse" />
{title}
</h2>
<button onClick={onClose} className="text-white/50 hover:text-white transition-colors">
<button onClick={onClose} className="text-white/80 hover:text-white transition-colors bg-white/10 hover:bg-white/20 rounded-full p-1">
<X className="w-5 h-5" />
</button>
</div>
@@ -378,10 +378,10 @@ export function HolidayRequestDialog({
{/* 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">
<div className="flex gap-4 p-4 bg-primary-500/10 rounded-lg border-2 border-primary-500/30">
<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 className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'day' ? 'border-primary-500' : 'border-primary-500/30'}`}>
{requestType === 'day' && <div className="w-2 h-2 rounded-full bg-primary-500" />}
</div>
<input
type="radio"
@@ -391,11 +391,11 @@ export function HolidayRequestDialog({
className="hidden"
disabled={isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체' && initialData?.cate !== '외출')}
/>
<span className="font-medium text-white/90"></span>
<span className="font-bold text-primary-400"></span>
</label>
<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 className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'time' ? 'border-primary-500' : 'border-primary-500/30'}`}>
{requestType === 'time' && <div className="w-2 h-2 rounded-full bg-primary-500" />}
</div>
<input
type="radio"
@@ -405,11 +405,11 @@ export function HolidayRequestDialog({
className="hidden"
disabled={isReadOnly || (formData.idx > 0 && initialData?.cate !== '대체')}
/>
<span className="font-medium text-white/90"></span>
<span className="font-bold text-primary-400"></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 className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${requestType === 'out' ? 'border-primary-500' : 'border-primary-500/30'}`}>
{requestType === 'out' && <div className="w-2 h-2 rounded-full bg-primary-500" />}
</div>
<input
type="radio"
@@ -419,24 +419,24 @@ export function HolidayRequestDialog({
className="hidden"
disabled={isReadOnly}
/>
<span className="font-medium text-white/90"></span>
<span className="font-bold text-primary-400"></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">
<label className="text-sm font-bold text-primary-400 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 bg-white/5 border border-white/10 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={isReadOnly || formData.idx > 0}
>
{users.map(user => (
<option key={user.id} value={user.id} className="bg-[#1e1e2e]">{user.name} ({user.id})</option>
<option key={user.id} value={user.id} className="bg-bg-paper">{user.name} ({user.id})</option>
))}
</select>
</div>
@@ -445,26 +445,26 @@ export function HolidayRequestDialog({
{/* 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">
<label className="text-sm font-bold text-primary-400 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 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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
disabled={isReadOnly}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<label className="text-sm font-bold text-primary-400 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 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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
disabled={isReadOnly}
/>
</div>
@@ -475,21 +475,21 @@ export function HolidayRequestDialog({
{/* 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">
<label className="text-sm font-bold text-primary-400 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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
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]">
<option key={key} value={val} className="bg-bg-paper">
{val}
</option>
);
@@ -500,12 +500,12 @@ export function HolidayRequestDialog({
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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 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">
<label className="text-sm font-bold text-primary-400 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
</label>
<input
@@ -513,7 +513,7 @@ export function HolidayRequestDialog({
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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
@@ -532,7 +532,7 @@ export function HolidayRequestDialog({
{/* 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">
<label className="text-sm font-bold text-primary-400 flex items-center gap-2">
<MapPin className="w-4 h-4" />
</label>
<input
@@ -540,7 +540,7 @@ export function HolidayRequestDialog({
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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
@@ -555,7 +555,7 @@ export function HolidayRequestDialog({
</datalist>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<label className="text-sm font-bold text-primary-400 flex items-center gap-2">
<User className="w-4 h-4" />
</label>
<input
@@ -563,7 +563,7 @@ export function HolidayRequestDialog({
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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
placeholder="입력 또는 선택"
disabled={isReadOnly}
/>
@@ -582,24 +582,24 @@ export function HolidayRequestDialog({
{/* 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>
<label className="text-sm font-bold text-primary-400"></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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5 disabled:text-primary-200/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>
<label className="text-sm font-bold text-primary-400"></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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5 disabled:text-primary-200/30 disabled:cursor-not-allowed"
disabled={isReadOnly || requestType === 'day'}
/>
</div>
@@ -609,53 +609,51 @@ export function HolidayRequestDialog({
{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">
<label className="text-sm font-bold text-primary-400 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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
disabled={isReadOnly}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70 flex items-center gap-2">
<label className="text-sm font-bold text-primary-400 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"
className="w-full px-3 py-2 bg-primary-500/10 border-2 border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-primary-500/5"
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>
<label className="text-sm font-medium text-primary-200"></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"
className="w-full flex-1 px-3 py-2 bg-primary-500/10 border border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none min-h-[200px] disabled:bg-primary-500/5"
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">
<div className="p-4 bg-primary-500/10 rounded-lg space-y-4 border border-primary-500/30 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'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 0 ? 'border-primary-400' : 'border-primary-500/30'}`}>
{formData.conf === 0 && <div className="w-2 h-2 rounded-full bg-primary-400" />}
</div>
<input
@@ -666,10 +664,10 @@ export function HolidayRequestDialog({
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
<span className="text-primary-200"></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'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 1 ? 'border-green-400' : 'border-primary-500/30'}`}>
{formData.conf === 1 && <div className="w-2 h-2 rounded-full bg-green-400" />}
</div>
<input
@@ -680,10 +678,10 @@ export function HolidayRequestDialog({
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
<span className="text-primary-200"></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'}`}>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.conf === 2 ? 'border-red-400' : 'border-primary-500/30'}`}>
{formData.conf === 2 && <div className="w-2 h-2 rounded-full bg-red-400" />}
</div>
<input
@@ -694,24 +692,18 @@ export function HolidayRequestDialog({
className="hidden"
disabled={userLevel < 5}
/>
<span className="text-white/70"></span>
<span className="text-primary-200"></span>
</label>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-white/70"> </label>
<label className="text-sm font-medium text-primary-200"> </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"
className="w-full px-3 py-2 bg-primary-500/10 border border-primary-500/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:bg-primary-500/5"
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>
@@ -719,10 +711,10 @@ export function HolidayRequestDialog({
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10 bg-white/5">
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-primary-500/30 bg-primary-500/10">
<button
onClick={onClose}
className="px-4 py-2 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors font-medium"
className="px-4 py-2 text-primary-200 hover:text-white hover:bg-primary-500/20 rounded-lg transition-colors font-medium"
>
</button>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
import { X, ChevronLeft, ChevronRight, Download, Calendar, Users, Clock } from 'lucide-react';
import { clsx } from 'clsx';
import { comms } from '@/communication';
import { HolidayItem } from '@/types';
@@ -177,16 +178,16 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
// 셀 색상 결정
const getCellStyle = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => {
if (!data) return 'text-gray-400';
if (!data) return 'text-white/20';
if (data.jobtype === '휴가') return 'text-red-500 font-medium';
if (isHoliday) return 'text-green-500 font-medium';
if (data.jobtype === '휴가') return 'text-danger-400 font-black bg-danger-500/10';
if (isHoliday) return 'text-success-400 font-bold bg-success-500/10 underline underline-offset-4';
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';
if (data.hrs > 8) return 'text-primary-400 font-bold underline underline-offset-2';
if (data.hrs < 8) return 'text-danger-500 font-medium';
if (data.ot > 0) return 'text-warning-400 font-bold italic';
return 'text-white';
return 'text-white/70';
};
// 셀 내용 포맷
@@ -201,10 +202,10 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
// 엑셀 내보내기 (간단한 CSV)
const exportToExcel = () => {
let csv = '사원명,' + dayColumns.map(c => `${c.day}(${c.dayOfWeek})`).join(',') + ',합계\n';
let csv = '사원명,' + dayColumns.map((c: DayColumn) => `${c.day}(${c.dayOfWeek})`).join(',') + ',합계\n';
userRows.forEach(row => {
const cells = dayColumns.map(col => {
userRows.forEach((row: UserRow) => {
const cells = dayColumns.map((col: DayColumn) => {
const data = row.dailyData.get(col.day);
return formatCellContent(data, col.isHoliday);
});
@@ -222,125 +223,207 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
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="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div
className="glass-effect rounded-3xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col shadow-2xl border border-white/10 animate-scale-in"
onClick={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<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">
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
<div className="flex items-center gap-6">
<div className="flex items-center gap-4">
<div className="p-2.5 bg-primary-500/20 rounded-xl">
<Clock className="w-6 h-6 text-primary-400" />
</div>
<div>
<h2 className="text-xl font-bold text-white leading-tight"> </h2>
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Daily Working Hours Summary</p>
</div>
</div>
{/* 월 선택 UI */}
<div className="flex items-center gap-1 bg-white/5 p-1 rounded-xl border border-white/10">
<button
onClick={() => changeMonth(-1)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 hover:bg-white/10 rounded-lg text-white/50 hover:text-white transition-all active:scale-95"
title="이전 달"
>
<ChevronLeft className="w-5 h-5 text-white" />
<ChevronLeft className="w-5 h-5" />
</button>
<span className="text-lg font-medium text-white min-w-[100px] text-center">
{currentMonth}
</span>
<div className="px-4 flex items-center gap-2">
<Calendar className="w-4 h-4 text-primary-400" />
<span className="text-sm font-bold text-white font-mono min-w-[80px] text-center italic tracking-wider">
{currentMonth}
</span>
</div>
<button
onClick={() => changeMonth(1)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 hover:bg-white/10 rounded-lg text-white/50 hover:text-white transition-all active:scale-95"
title="다음 달"
>
<ChevronRight className="w-5 h-5 text-white" />
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-3">
<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"
className="px-5 py-2.5 bg-green-500 hover:bg-green-600 text-white border border-green-500/20 rounded-xl transition-all font-bold flex items-center gap-2 shadow-lg shadow-green-500/20 active:scale-95"
>
<Download className="w-4 h-4" />
<span>CSV </span>
</button>
<button
onClick={onClose}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2.5 text-white/30 hover:text-white hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6 text-white" />
<X className="w-6 h-6" />
</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.toFixed(1)}+{row.totalOt.toFixed(1)}(*{row.totalHolidayOt.toFixed(1)})
</td>
{/* 테이블 컨텐츠 */}
<div className="flex-1 overflow-hidden flex flex-col p-6 bg-white/[0.02]">
<div className="flex-1 glass-effect rounded-2xl border border-white/10 flex flex-col overflow-hidden shadow-2xl">
{loading ? (
<div className="flex-1 flex flex-col items-center justify-center">
<RefreshCw className="w-12 h-12 text-primary-500/30 animate-spin mb-4" />
<p className="text-white/40 font-medium"> ...</p>
</div>
) : (
<div className="flex-1 overflow-auto custom-scrollbar">
<table className="w-full border-separate border-spacing-0">
<thead className="sticky top-0 z-20">
<tr>
<th className="px-6 py-4 text-left text-xs font-bold text-white/70 uppercase tracking-widest bg-gray-900 border-b border-r border-white/10 sticky left-0 z-30">
</th>
{dayColumns.map(col => (
<th
key={col.day}
className={clsx(
"px-2 py-3 text-center text-[10px] font-black uppercase tracking-tighter border-b border-r border-white/5 bg-gray-900/95 backdrop-blur-sm min-w-[50px]",
col.isHoliday ? 'text-danger-400 bg-danger-500/10' :
col.dayOfWeek === '일' ? 'text-danger-400' :
col.dayOfWeek === '토' ? 'text-primary-400' :
'text-white/40'
)}
title={col.holidayMemo}
>
<div className="flex flex-col items-center">
<span className="text-sm font-mono leading-none">{col.day}</span>
<span className="mt-1 opacity-60 leading-none">{col.dayOfWeek}</span>
</div>
</th>
))}
<th className="px-4 py-4 text-center text-xs font-bold text-primary-400 uppercase tracking-widest bg-gray-900 border-b border-white/10 sticky right-0 z-30 shadow-[-4px_0_12px_rgba(0,0,0,0.5)]">
(Hr + OT)
</th>
</tr>
))
)}
</tbody>
</table>
)}
</thead>
<tbody className="divide-y divide-white/[0.03]">
{userRows.length === 0 ? (
<tr>
<td colSpan={dayColumns.length + 2} className="px-4 py-32 text-center">
<Users className="w-20 h-20 text-white/5 mx-auto mb-4" />
<p className="text-white/20 font-medium"> .</p>
</td>
</tr>
) : (
userRows.map((row: UserRow) => (
<tr key={row.uid} className="hover:bg-white/[0.03] transition-colors group">
<td className="px-6 py-2.5 text-sm font-bold text-white bg-gray-900/50 border-r border-white/10 sticky left-0 z-10 backdrop-blur-md group-hover:bg-primary-500/10 transition-colors">
{row.uname}
</td>
{dayColumns.map((col: DayColumn) => {
const data = row.dailyData.get(col.day);
return (
<td
key={col.day}
className={clsx(
"px-2 py-2 text-center text-xs border-r border-white/[0.02] last:border-r-0",
getCellStyle(data, col.isHoliday)
)}
>
<span className="font-mono font-medium tracking-tighter italic opacity-90">
{formatCellContent(data, col.isHoliday)}
</span>
</td>
);
})}
<td className="px-4 py-2.5 text-center bg-gray-900/50 border-l border-white/10 sticky right-0 z-10 backdrop-blur-md shadow-[-4px_0_12px_rgba(0,0,0,0.5)] group-hover:bg-primary-500/10 transition-colors">
<div className="flex items-center justify-center gap-1.5 font-mono text-xs font-bold">
<span className="text-white tracking-widest">{row.totalHrs.toFixed(1)}</span>
<span className="text-white/20 italic">+</span>
<span className="text-primary-400 tracking-widest">{row.totalOt.toFixed(1)}</span>
{row.totalHolidayOt > 0 && (
<span className="text-warning-400 ml-1 text-[10px] bg-warning-500/10 px-1 rounded">
*{row.totalHolidayOt.toFixed(1)}
</span>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
</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>
{/* 하단 범례 (Legend) */}
<div className="px-8 py-4 border-t border-white/10 bg-white/5 flex items-center gap-6 overflow-x-auto custom-scrollbar no-scrollbar">
<span className="text-[10px] font-black text-white/30 uppercase tracking-[0.2em] shrink-0 border-r border-white/10 pr-6 mr-2">Legend</span>
<div className="flex items-center gap-6 text-[11px] font-bold whitespace-nowrap">
<div className="flex items-center gap-2">
<span className="w-5 h-5 rounded-md bg-white/5 flex items-center justify-center text-gray-500 font-mono italic">--</span>
<span className="text-white/40"> </span>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded-md bg-danger-500/20 text-danger-400 font-bold"></span>
<span className="text-white/40">/</span>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded-md bg-success-500/20 text-success-400 font-mono font-bold">*8+4</span>
<span className="text-white/40"> </span>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded-md bg-primary-500/20 text-primary-400 font-mono font-bold underline">10+0</span>
<span className="text-white/40"> </span>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded-md bg-danger-500/10 text-danger-500 font-mono font-bold">6+0</span>
<span className="text-white/40"> </span>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded-md bg-warning-500/20 text-warning-400 font-mono font-bold italic">8+2</span>
<span className="text-white/40">OT </span>
</div>
</div>
</div>
</div>
</div>
);
}
const RefreshCw = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24" height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
);

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { X, RefreshCw } from 'lucide-react';
import { comms } from '@/communication';
import { JobReportTypeItem } from '@/types';
import { DevelopmentNotice } from '@/components/DevelopmentNotice';
interface JobreportTypeModalProps {
isOpen: boolean;
@@ -65,6 +66,7 @@ export function JobreportTypeModal({
{/* 컨텐츠 */}
<div className="p-6">
<DevelopmentNotice />
<div className="mb-4 text-white/70 text-sm">
: {startDate} ~ {endDate}
</div>

View File

@@ -116,7 +116,7 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
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="bg-bg-paper 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">
@@ -142,7 +142,7 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
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>
<option key={opt} value={opt} className="bg-bg-paper">{opt}</option>
))}
</select>
</div>

View File

@@ -10,7 +10,7 @@ interface LayoutProps {
export function Layout({ isConnected, user }: LayoutProps) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900">
<div className="min-h-screen gradient-bg">
<div className="flex flex-col h-screen overflow-hidden">
{/* Top Navigation Header */}
<Header isConnected={isConnected} />

View File

@@ -0,0 +1,21 @@
import { useState } from 'react';
import { Settings } from 'lucide-react';
import { SettingsDialog } from '../settings/SettingsDialog';
export function SettingsButton() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
onClick={() => setIsOpen(true)}
className="flex items-center justify-center p-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors"
title="설정"
>
<Settings className="w-4 h-4" />
</button>
<SettingsDialog isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { Clock, Wifi, WifiOff } from 'lucide-react';
import { UserInfoButton } from './UserInfoButton';
import { SettingsButton } from './SettingsButton';
import { comms } from '@/communication';
interface StatusBarProps {
@@ -39,6 +40,8 @@ export function StatusBar({ userName, userDept, isConnected }: StatusBarProps) {
<footer className="glass-effect px-4 py-2 flex items-center justify-between text-sm">
{/* Left: User Info */}
<div className="flex items-center space-x-2">
{/* Settings Button */}
<SettingsButton />
<UserInfoButton userName={userName} userDept={userDept} />
</div>

View File

@@ -1,4 +1,5 @@
import { X, Save, Trash2 } from 'lucide-react';
import { X, Save, Trash2, ShieldCheck, Info, Package, Truck, User, Calendar, FileText, CheckCircle, XCircle } from 'lucide-react';
import { clsx } from 'clsx';
import { useState, useEffect } from 'react';
import type { LicenseItem } from '@/types';
@@ -86,65 +87,88 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
<div className="glass-effect rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto m-4" onClick={(e) => e.stopPropagation()}>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div className="glass-effect rounded-3xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl border border-white/10 animate-scale-in" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h2 className="text-xl font-semibold text-white">
{formData.idx ? '라이선스 수정' : '라이선스 추가'}
</h2>
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
<div className="flex items-center gap-4">
<div className="p-2.5 bg-primary-500/20 rounded-xl">
<ShieldCheck className="w-6 h-6 text-primary-400" />
</div>
<div>
<h2 className="text-xl font-bold text-white leading-tight">
{formData.idx ? '라이선스 수정' : '라이선스 추가'}
</h2>
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Edit License Details</p>
</div>
</div>
<button
onClick={onClose}
className="text-white/70 hover:text-white transition-colors"
className="p-2 text-white/30 hover:text-white hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Body */}
<div className="p-6 space-y-6">
<div className="flex-1 overflow-y-auto custom-scrollbar p-8 space-y-8">
{/* 기본 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
<span> </span>
<h3 className="text-sm font-bold text-primary-400 flex items-center gap-2 uppercase tracking-widest py-1 border-b border-white/5 mb-4">
<Info className="w-4 h-4" />
</h3>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-1 flex items-center">
<label className="flex items-center space-x-2 cursor-pointer">
<div className="grid grid-cols-12 gap-5">
<div className="col-span-12 md:col-span-2">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<button
type="button"
onClick={() => setFormData({ ...formData, expire: !formData.expire })}
className={clsx(
"w-full px-4 py-2.5 rounded-xl border transition-all flex items-center justify-center gap-2 font-bold text-sm",
formData.expire
? "bg-danger-500/20 border-danger-500/30 text-danger-400"
: "bg-success-500/20 border-success-500/30 text-success-400"
)}
>
{formData.expire ? (
<><XCircle className="w-4 h-4" /> </>
) : (
<><CheckCircle className="w-4 h-4" /> </>
)}
</button>
</div>
<div className="col-span-12 md:col-span-5">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"> *</label>
<div className="relative group">
<Package className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary-400 transition-colors" />
<input
type="checkbox"
checked={formData.expire || false}
onChange={(e) => setFormData({ ...formData, expire: e.target.checked })}
className="w-4 h-4"
type="text"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full pl-10 pr-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium"
placeholder="제품명을 입력하세요"
/>
<span className="text-sm text-white/70"></span>
</label>
</div>
</div>
<div className="col-span-5">
<label className="block text-sm text-white/70 mb-1"> *</label>
<input
type="text"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
/>
</div>
<div className="col-span-3">
<label className="block text-sm text-white/70 mb-1"></label>
<div className="col-span-12 md:col-span-2">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="text"
value={formData.version || ''}
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium"
placeholder="v1.0"
/>
</div>
<div className="col-span-3">
<label className="block text-sm text-white/70 mb-1"></label>
<div className="col-span-12 md:col-span-3">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="text"
value={formData.meterialNo || ''}
onChange={(e) => setFormData({ ...formData, meterialNo: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium font-mono"
placeholder="M-0000"
/>
</div>
</div>
@@ -152,26 +176,27 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
{/* 공급 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
<span> </span>
<h3 className="text-sm font-bold text-primary-400 flex items-center gap-2 uppercase tracking-widest py-1 border-b border-white/5 mb-4">
<Truck className="w-4 h-4" />
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-5">
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="text"
value={formData.supply || ''}
onChange={(e) => setFormData({ ...formData, supply: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="text"
value={formData.manu || ''}
onChange={(e) => setFormData({ ...formData, manu: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium"
/>
</div>
</div>
@@ -179,35 +204,36 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
{/* 사용 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
<span> </span>
<h3 className="text-sm font-bold text-primary-400 flex items-center gap-2 uppercase tracking-widest py-1 border-b border-white/5 mb-4">
<User className="w-4 h-4" />
</h3>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-2">
<label className="block text-sm text-white/70 mb-1"></label>
<div className="grid grid-cols-12 gap-5">
<div className="col-span-12 md:col-span-2">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="number"
value={formData.qty || 1}
onChange={(e) => setFormData({ ...formData, qty: parseInt(e.target.value) || 1 })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-bold text-center"
/>
</div>
<div className="col-span-4">
<label className="block text-sm text-white/70 mb-1"></label>
<div className="col-span-12 md:col-span-4">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<input
type="text"
value={formData.uids || ''}
onChange={(e) => setFormData({ ...formData, uids: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium"
/>
</div>
<div className="col-span-6">
<label className="block text-sm text-white/70 mb-1">S/N</label>
<div className="col-span-12 md:col-span-6">
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1">S/N</label>
<input
type="text"
value={formData.serialNo || ''}
onChange={(e) => setFormData({ ...formData, serialNo: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium font-mono"
/>
</div>
</div>
@@ -215,74 +241,82 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
{/* 기간 정보 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
<span> </span>
<h3 className="text-sm font-bold text-primary-400 flex items-center gap-2 uppercase tracking-widest py-1 border-b border-white/5 mb-4">
<Calendar className="w-4 h-4" />
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-5">
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<input
type="date"
value={formData.sdate || ''}
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
/>
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<div className="relative group">
<Calendar className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary-400 transition-colors pointer-events-none" />
<input
type="date"
value={formData.sdate || ''}
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
className="w-full pl-10 pr-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium custom-calendar-icon"
/>
</div>
</div>
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<input
type="date"
value={formData.edate || ''}
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
/>
<label className="block text-xs font-bold text-white/30 uppercase tracking-widest mb-2 px-1"></label>
<div className="relative group">
<Calendar className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary-400 transition-colors pointer-events-none" />
<input
type="date"
value={formData.edate || ''}
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
className="w-full pl-10 pr-4 py-2.5 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium custom-calendar-icon"
/>
</div>
</div>
</div>
</div>
{/* 비고 */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
<span></span>
<h3 className="text-sm font-bold text-primary-400 flex items-center gap-2 uppercase tracking-widest py-1 border-b border-white/5 mb-4">
<FileText className="w-4 h-4" />
</h3>
<textarea
value={formData.remark || ''}
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
rows={3}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500 resize-none"
rows={4}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-2xl text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all font-medium resize-none"
placeholder="추가 메모를 입력하세요..."
/>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-white/10">
<div className="px-8 py-6 border-t border-white/10 flex items-center justify-between bg-white/5">
<div>
{formData.idx && onDelete && (
<button
onClick={handleDelete}
disabled={saving}
className="flex items-center space-x-2 px-4 py-2 bg-red-500 hover:bg-red-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
className="flex items-center gap-2 px-5 py-2.5 bg-danger-500/10 hover:bg-danger-500/20 text-danger-400 border border-danger-500/20 rounded-xl transition-all font-bold group active:scale-95"
>
<Trash2 className="w-4 h-4" />
<Trash2 className="w-4 h-4 group-hover:shake" />
<span></span>
</button>
)}
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center gap-3">
<button
onClick={onClose}
disabled={saving}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-800 text-white rounded-lg transition-colors"
className="px-6 py-2.5 bg-white/5 hover:bg-white/10 text-white/70 hover:text-white border border-white/10 rounded-xl transition-all font-bold active:scale-95"
>
</button>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
className="flex items-center gap-2 px-8 py-2.5 bg-primary-500 hover:bg-primary-600 text-white border border-primary-500/20 rounded-xl transition-all font-bold shadow-lg shadow-primary-500/20 active:scale-95 disabled:opacity-50"
>
<Save className="w-4 h-4" />
<Save className={clsx("w-4 h-4", saving && "animate-spin")} />
<span>{saving ? '저장 중...' : '저장'}</span>
</button>
</div>

View File

@@ -4,12 +4,14 @@ import {
FolderOpen,
Download,
Search,
X,
ChevronLeft,
ChevronRight,
CheckCircle,
XCircle,
ShieldCheck,
RefreshCw,
} from 'lucide-react';
import { clsx } from 'clsx';
import { comms } from '@/communication';
import { LicenseEditDialog } from './LicenseEditDialog';
import type { LicenseItem } from '@/types';
@@ -229,149 +231,215 @@ export function LicenseList() {
};
return (
<div className="p-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white"> </h1>
<div className="flex items-center space-x-2">
<div className="flex flex-col h-full animate-fade-in bg-white/[0.02]">
{/* 리스트 헤더 */}
<div className="bg-white/5 border-b border-white/10 px-6 py-4 flex items-center justify-between backdrop-blur-md sticky top-0 z-20">
<div className="flex items-center gap-4">
<div className="p-2 bg-primary-500/20 rounded-lg">
<ShieldCheck className="w-6 h-6 text-primary-400" />
</div>
<div>
<h2 className="text-lg font-bold text-white leading-tight"> </h2>
<p className="text-[10px] text-white/40 uppercase tracking-wider font-medium">License Management</p>
</div>
</div>
<div className="flex items-center gap-3">
{/* 검색 바 */}
<div className="relative group w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary-400 transition-colors" />
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="검색어 입력..."
className="w-full bg-white/5 border border-white/10 rounded-xl pl-9 pr-8 py-1.5 text-xs text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all backdrop-blur-sm"
/>
{searchText && (
<button
onClick={() => setSearchText('')}
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-white/20 hover:text-white transition-colors"
>
<XCircle className="w-3.5 h-3.5" />
</button>
)}
</div>
{/* 건수 */}
<div className="flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-xl border border-white/10 h-[38px]">
<span className="text-primary-400 font-bold text-sm">{filteredList.length}</span>
<span className="text-white/40 text-[10px] uppercase"></span>
</div>
{/* 새로고침 */}
<button
onClick={handleAdd}
onClick={loadData}
disabled={loading}
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
className="p-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-white/70 hover:text-white transition-all disabled:opacity-50"
title="새로고침"
>
<Plus className="w-4 h-4" />
<span></span>
<RefreshCw className={clsx("w-4 h-4", loading && "animate-spin")} />
</button>
{/* CSV */}
<button
onClick={handleExportCSV}
disabled={loading}
className="flex items-center space-x-2 px-4 py-2 bg-green-500 hover:bg-green-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
className="p-2 bg-white/5 hover:bg-green-500/20 border border-white/10 rounded-xl text-white/70 hover:text-green-400 transition-all disabled:opacity-50"
title="CSV 내보내기"
>
<Download className="w-4 h-4" />
<span>CSV</span>
</button>
{/* 추가 */}
<button
onClick={handleAdd}
className="p-2 bg-primary-500 hover:bg-primary-600 border border-white/20 rounded-xl text-white transition-all shadow-lg shadow-primary-500/20 active:scale-95"
title="추가"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* Search */}
<div className="flex items-center space-x-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/50" />
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="검색 (제품명, 버전, 공급업체, 제조사, S/N, 자재번호, 비고)"
className="w-full pl-10 pr-10 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-blue-500"
/>
{searchText && (
<button
onClick={() => setSearchText('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/50 hover:text-white"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Table */}
<div className="glass-effect rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead className="bg-white/10">
<tr>
<th className="px-4 py-3 text-center text-sm font-semibold text-white border-r border-white/10 w-16"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '25%' }}></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '25%' }}></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10 w-20"></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '12%' }}></th>
<th className="px-4 py-3 text-left text-sm font-semibold text-white" style={{ width: '15%' }}>S/N</th>
</tr>
</thead>
<tbody>
{loading && (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-white/70">
...
</td>
</tr>
)}
{!loading && paginatedList.length === 0 && (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-white/70">
.
</td>
</tr>
)}
{!loading &&
paginatedList.map((item) => (
<tr
key={item.idx}
onClick={() => handleRowClick(item)}
className={`border-t border-white/10 hover:bg-white/10 cursor-pointer transition-colors ${
item.expire ? 'bg-red-500/10' : ''
}`}
>
<td className="px-4 py-3 text-center border-r border-white/10">
<div className="flex justify-center" title={item.expire ? '만료' : '유효'}>
{item.expire ? (
<XCircle className="w-5 h-5 text-red-500" />
) : (
<CheckCircle className="w-5 h-5 text-green-500" />
)}
</div>
</td>
<td className="px-4 py-3 text-sm text-white border-r border-white/10 max-w-xs">
<div className="flex items-center space-x-2">
<button
onClick={(e) => handleOpenFolder(item, e)}
className="p-1 text-yellow-400 hover:text-yellow-300 transition-colors flex-shrink-0"
title="폴더 열기"
>
<FolderOpen className="w-4 h-4" />
</button>
<span className="break-words">{item.name}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-white border-r border-white/10 break-words">{item.version}</td>
<td className="px-4 py-3 text-sm text-white border-r border-white/10">{item.qty}</td>
<td className="px-4 py-3 text-sm text-white border-r border-white/10 break-words max-w-[8rem]">{item.uids}</td>
<td className="px-4 py-3 text-sm text-white break-words">{item.serialNo}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-4 py-3 border-t border-white/10">
<div className="text-sm text-white/70">
{filteredList.length} {(currentPage - 1) * pageSize + 1}~
{Math.min(currentPage * pageSize, filteredList.length)}
</div>
<div className="flex items-center space-x-2">
<button
onClick={goToPreviousPage}
disabled={currentPage === 1}
className="p-2 text-white/70 hover:text-white disabled:text-white/30 disabled:cursor-not-allowed"
>
<ChevronLeft className="w-5 h-5" />
</button>
<span className="text-sm text-white">
{currentPage} / {totalPages}
</span>
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="p-2 text-white/70 hover:text-white disabled:text-white/30 disabled:cursor-not-allowed"
>
<ChevronRight className="w-5 h-5" />
</button>
<div className="flex-1 overflow-hidden flex flex-col p-6">
{/* 테이블 메인 섹션 */}
<div className="flex-1 glass-effect rounded-2xl border border-white/10 flex flex-col overflow-hidden shadow-2xl">
{/* 컬럼 헤더 */}
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4 sticky top-0 z-10">
<div className="w-12 text-center text-xs font-medium text-white/70 uppercase"></div>
<div className="flex-1 flex items-center gap-4">
<div className="w-1/4 text-xs font-medium text-white/70 uppercase"></div>
<div className="w-1/4 text-xs font-medium text-white/70 uppercase"></div>
<div className="w-20 text-xs font-medium text-white/70 uppercase text-center"></div>
<div className="w-32 text-xs font-medium text-white/70 uppercase"></div>
<div className="flex-1 text-xs font-medium text-white/70 uppercase">S/N</div>
</div>
</div>
)}
<div className="divide-y divide-white/5 overflow-y-auto custom-scrollbar flex-1">
{loading ? (
<div className="px-6 py-12 text-center">
<RefreshCw className="w-10 h-10 mx-auto mb-4 animate-spin text-primary-500/50" />
<p className="text-white/50 font-medium text-sm"> ...</p>
</div>
) : paginatedList.length === 0 ? (
<div className="px-6 py-20 text-center">
<div className="relative inline-block mb-4">
<ShieldCheck className="w-16 h-16 mx-auto text-white/10" />
</div>
<p className="text-white/30 font-medium"> .</p>
</div>
) : (
paginatedList.map((item) => (
<div
key={item.idx}
onClick={() => handleRowClick(item)}
className={clsx(
"px-6 py-3 hover:bg-white/[0.03] transition-all cursor-pointer group flex items-center gap-4 border-b border-white/[0.02]",
item.expire && "bg-danger-500/[0.03]"
)}
>
<div className="w-12 flex justify-center shrink-0">
<div className={clsx(
"w-8 h-8 rounded-lg flex items-center justify-center transition-all group-hover:scale-110",
item.expire ? "bg-danger-500/20 text-danger-400" : "bg-success-500/20 text-success-400"
)}>
{item.expire ? (
<XCircle className="w-4 h-4" />
) : (
<CheckCircle className="w-4 h-4" />
)}
</div>
</div>
<div className="flex-1 flex items-center gap-4 min-w-0">
<div className="w-1/4 min-w-0 flex items-center gap-2">
<button
onClick={(e) => handleOpenFolder(item, e)}
className="p-1 text-warning-400 hover:text-warning-300 transition-colors shrink-0"
title="폴더 열기"
>
<FolderOpen className="w-4 h-4" />
</button>
<span className="text-sm font-bold text-white group-hover:text-primary-400 transition-colors truncate">
{item.name}
</span>
</div>
<div className="w-1/4 min-w-0">
<span className="text-sm text-white/60 truncate" title={item.version}>{item.version || '-'}</span>
</div>
<div className="w-20 shrink-0 text-center">
<span className="text-sm font-medium text-white/70">{item.qty || 0}</span>
</div>
<div className="w-32 shrink-0">
<span className="text-sm text-white/50 truncate" title={item.uids}>{item.uids || '-'}</span>
</div>
<div className="flex-1 min-w-0">
<span className="text-xs font-mono text-white/40 truncate" title={item.serialNo}>{item.serialNo || '-'}</span>
</div>
</div>
</div>
))
)}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="bg-white/5 px-6 py-3 border-t border-white/10 flex items-center justify-between backdrop-blur-md">
<div className="flex items-center gap-2">
<span className="text-white/30 text-[10px] uppercase font-bold tracking-wider">Page</span>
<span className="text-white text-sm font-mono font-bold">{currentPage}</span>
<span className="text-white/20 text-xs italic">of</span>
<span className="text-white/60 text-sm font-mono">{totalPages}</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={goToPreviousPage}
disabled={currentPage === 1}
className="p-1.5 rounded-lg hover:bg-white/10 text-white/70 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-1 mx-2">
{totalPages <= 5 ? (
[...Array(totalPages)].map((_, i) => (
<button
key={i + 1}
onClick={() => setCurrentPage(i + 1)}
className={clsx(
"w-8 h-8 rounded-lg text-xs font-bold transition-all",
currentPage === i + 1
? "bg-primary-500 text-white shadow-lg shadow-primary-500/30"
: "text-white/40 hover:bg-white/10 hover:text-white"
)}
>
{i + 1}
</button>
))
) : (
<span className="text-white/40 text-xs px-2">{currentPage} / {totalPages}</span>
)}
</div>
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="p-1.5 rounded-lg hover:bg-white/10 text-white/70 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
<div className="flex items-center gap-2">
<span className="text-white/30 text-[10px] uppercase font-bold tracking-wider">Total</span>
<span className="text-primary-400 text-sm font-mono font-bold leading-none">{filteredList.length}</span>
</div>
</div>
)}
</div>
</div>
{/* Edit Dialog */}

View File

@@ -0,0 +1,290 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { X, Settings as SettingsIcon, Monitor, Palette, Save } from 'lucide-react';
import { useTheme } from '@/context/ThemeContext';
import { comms } from '@/communication';
import { SettingsModel } from '@/types';
interface SettingsDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const { theme, setTheme } = useTheme();
const [activeTab, setActiveTab] = useState<'general' | 'theme'>('theme');
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [settings, setSettings] = useState<SettingsModel | null>(null);
// 설정 로드
useEffect(() => {
if (isOpen) {
loadSettings();
}
}, [isOpen]);
const loadSettings = async () => {
setLoading(true);
console.log('Settings Load Start: comms.getSettings() call');
try {
const response = await comms.getSettings();
console.log('Settings response:', response);
// 백엔드에서 객체를 직접 반환함 (ApiResponse 래퍼 없음)
// Settings response: {Xml: {...}, Theme: "...", ...}
if (response) {
// @ts-ignore
const settingsData = response as SettingsModel;
setSettings(settingsData);
// DB에 저장된 테마가 있다면 현재 테마와 동기화
if (settingsData.Theme && settingsData.Theme !== theme) {
// DB 테마가 현재 로컬 테마와 다르다면?
// 일단 UI 상의 선택 상태는 DB 값으로 설정.
}
}
} catch (error) {
console.error('설정 로드 실패:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!settings) return;
setSaving(true);
try {
// 1. 백엔드 저장
const response = await comms.saveSettings(settings);
if (response.Success) {
// 2. 테마 변경 적용 (Context 업데이트)
if (settings.Theme && settings.Theme !== theme) {
setTheme(settings.Theme as any);
}
alert('설정이 저장되었습니다.');
onClose();
} else {
alert('저장 실패: ' + (response.Message || '알 수 없는 오류'));
}
} catch (error) {
console.error('설정 저장 실패:', error);
alert('저장 중 오류가 발생했습니다.');
} finally {
setSaving(false);
}
};
const handleThemeSelect = (selectedTheme: string) => {
if (settings) {
setSettings({ ...settings, Theme: selectedTheme });
}
};
const handleCheckboxChange = (field: keyof SettingsModel, checked: boolean) => {
if (settings) {
setSettings({ ...settings, [field]: checked });
}
};
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up mx-4 overflow-hidden flex flex-col max-h-[80vh]">
{/* Header */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
<h2 className="text-xl font-bold text-white flex items-center">
<SettingsIcon className="w-5 h-5 mr-2 text-primary-400" />
</h2>
<button onClick={onClose} className="text-white/70 hover:text-white transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Sidebar Tabs */}
<div className="w-48 border-r border-white/10 bg-black/20 p-4 space-y-2">
<button
onClick={() => setActiveTab('general')}
className={`w-full text-left px-4 py-3 rounded-lg flex items-center space-x-3 transition-colors ${activeTab === 'general' ? 'bg-primary-500/20 text-primary-300' : 'text-white/70 hover:bg-white/5'
}`}
>
<Monitor className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => setActiveTab('theme')}
className={`w-full text-left px-4 py-3 rounded-lg flex items-center space-x-3 transition-colors ${activeTab === 'theme' ? 'bg-primary-500/20 text-primary-300' : 'text-white/70 hover:bg-white/5'
}`}
>
<Palette className="w-4 h-4" />
<span></span>
</button>
</div>
{/* Content Area */}
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar bg-black/10">
{loading ? (
<div className="flex items-center justify-center h-full text-white/50">
<span className="animate-spin mr-2"></span> ...
</div>
) : !settings ? (
<div className="text-center text-red-400"> .</div>
) : (
<>
{activeTab === 'general' && (
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white/90 mb-4"> </h3>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.FullScreen}
onChange={(e) => handleCheckboxChange('FullScreen', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors"> </span>
</label>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.DupWindow}
onChange={(e) => handleCheckboxChange('DupWindow', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors"> </span>
</label>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.Disable8HourOver}
onChange={(e) => handleCheckboxChange('Disable8HourOver', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors">8 </span>
</label>
</div>
</div>
)}
{activeTab === 'theme' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-white/90"> </h3>
<div className="grid grid-cols-1 gap-4">
{/* Dark Theme */}
<button
onClick={() => handleThemeSelect('dark')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'dark' || (!settings.Theme && theme === 'dark')
? 'border-primary-500 bg-primary-500/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">Dark ()</span>
{settings.Theme === 'dark' && <div className="w-2 h-2 rounded-full bg-primary-500"></div>}
</div>
<div className="h-16 rounded-lg bg-[#1e1e2e] border border-white/10 flex overflow-hidden">
<div className="w-1/4 bg-[#181825] border-r border-white/5"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-white/10 rounded mb-2"></div>
<div className="h-8 w-full bg-[#1e1e2e] border border-primary-500/30 rounded"></div>
</div>
</div>
</button>
{/* PSH_PINK Theme */}
<button
onClick={() => handleThemeSelect('PSH_PINK')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'PSH_PINK'
? 'border-[#FF00FF] bg-[#FF00FF]/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">PSH Pink</span>
{settings.Theme === 'PSH_PINK' && <div className="w-2 h-2 rounded-full bg-[#FF00FF]"></div>}
</div>
<div className="h-16 rounded-lg bg-[#2D0A1E] border border-[#FF66FF]/30 flex overflow-hidden">
<div className="w-1/4 bg-[#1A0512] border-r border-[#FF66FF]/20"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-[#FF66FF]/20 rounded mb-2"></div>
<div className="h-8 w-full bg-[#3D0D28] border border-[#FF00FF] rounded relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[#FF00FF]/10 to-transparent"></div>
</div>
</div>
</div>
</button>
{/* JW_SKY Theme */}
<button
onClick={() => handleThemeSelect('JW_SKY')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'JW_SKY'
? 'border-sky-400 bg-sky-400/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">JW Sky</span>
{settings.Theme === 'JW_SKY' && <div className="w-2 h-2 rounded-full bg-sky-400"></div>}
</div>
<div className="h-16 rounded-lg bg-[#0F172A] border border-sky-400/30 flex overflow-hidden">
<div className="w-1/4 bg-[#020617] border-r border-sky-400/20"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-sky-400/20 rounded mb-2"></div>
<div className="h-8 w-full bg-[#1E293B] border border-sky-400 rounded"></div>
</div>
</div>
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/5 transition-colors"
>
</button>
<button
onClick={handleSave}
disabled={saving || loading}
className="px-6 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white shadow-lg shadow-primary-500/30 transition-all flex items-center disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? (
<span className="animate-spin mr-2"></span>
) : (
<Save className="w-4 h-4 mr-2" />
)}
</button>
</div>
</div>
</div>,
document.body
);
}

View File

@@ -0,0 +1,65 @@
import { SettingsModel } from '@/types';
interface SettingsGeneralProps {
settings: SettingsModel;
onChange: (field: keyof SettingsModel, checked: boolean) => void;
}
export function SettingsGeneral({ settings, onChange }: SettingsGeneralProps) {
return (
<div className="space-y-6">
<div className="bg-white/5 p-4 rounded-lg overflow-auto max-h-96">
<h3 className="text-white/90 font-medium mb-2"> (Debug)</h3>
<pre className="text-xs text-white/70 whitespace-pre-wrap font-mono">
{JSON.stringify(settings, null, 2)}
</pre>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white/90 mb-4"> </h3>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.FullScreen}
onChange={(e) => onChange('FullScreen', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors"> </span>
</label>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.DupWindow}
onChange={(e) => onChange('DupWindow', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors"> </span>
</label>
<label className="flex items-center space-x-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only peer"
checked={settings.Disable8HourOver}
onChange={(e) => onChange('Disable8HourOver', e.target.checked)}
/>
<div className="w-10 h-6 bg-white/20 rounded-full peer peer-checked:bg-primary-500 peer-focus:ring-2 peer-focus:ring-primary-500/50 transition-all"></div>
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-all peer-checked:translate-x-4"></div>
</div>
<span className="text-white/80 group-hover:text-white transition-colors">8 </span>
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,82 @@
import { SettingsModel } from '@/types';
import { Theme } from '@/context/ThemeContext';
interface SettingsThemeProps {
settings: SettingsModel;
currentTheme: Theme;
onSelect: (theme: string) => void;
}
export function SettingsTheme({ settings, currentTheme, onSelect }: SettingsThemeProps) {
return (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-white/90"> </h3>
<div className="grid grid-cols-1 gap-4">
{/* Dark Theme */}
<button
onClick={() => onSelect('dark')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'dark' || (!settings.Theme && currentTheme === 'dark')
? 'border-primary-500 bg-primary-500/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">Dark ()</span>
{(settings.Theme === 'dark') && <div className="w-2 h-2 rounded-full bg-primary-500"></div>}
</div>
<div className="h-16 rounded-lg bg-[#1e1e2e] border border-white/10 flex overflow-hidden">
<div className="w-1/4 bg-[#181825] border-r border-white/5"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-white/10 rounded mb-2"></div>
<div className="h-8 w-full bg-[#1e1e2e] border border-primary-500/30 rounded"></div>
</div>
</div>
</button>
{/* PSH_PINK Theme */}
<button
onClick={() => onSelect('PSH_PINK')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'PSH_PINK'
? 'border-[#FF00FF] bg-[#FF00FF]/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">PSH Pink</span>
{settings.Theme === 'PSH_PINK' && <div className="w-2 h-2 rounded-full bg-[#FF00FF]"></div>}
</div>
<div className="h-16 rounded-lg bg-[#2D0A1E] border border-[#FF66FF]/30 flex overflow-hidden">
<div className="w-1/4 bg-[#1A0512] border-r border-[#FF66FF]/20"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-[#FF66FF]/20 rounded mb-2"></div>
<div className="h-8 w-full bg-[#3D0D28] border border-[#FF00FF] rounded relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-[#FF00FF]/10 to-transparent"></div>
</div>
</div>
</div>
</button>
{/* JW_SKY Theme */}
<button
onClick={() => onSelect('JW_SKY')}
className={`relative group p-4 rounded-xl border-2 transition-all duration-300 text-left ${settings.Theme === 'JW_SKY'
? 'border-sky-400 bg-sky-400/10'
: 'border-white/10 hover:border-white/30 hover:bg-white/5'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">JW Sky</span>
{settings.Theme === 'JW_SKY' && <div className="w-2 h-2 rounded-full bg-sky-400"></div>}
</div>
<div className="h-16 rounded-lg bg-[#0F172A] border border-sky-400/30 flex overflow-hidden">
<div className="w-1/4 bg-[#020617] border-r border-sky-400/20"></div>
<div className="flex-1 p-2">
<div className="h-2 w-2/3 bg-sky-400/20 rounded mb-2"></div>
<div className="h-8 w-full bg-[#1E293B] border border-sky-400 rounded"></div>
</div>
</div>
</button>
</div>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { X, Save, Key, User, Mail, Building2, Briefcase, Calendar, FileText } from 'lucide-react';
import { X, Save, Key, User, Mail, Building2, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
import { clsx } from 'clsx';
import { comms } from '@/communication';
import { UserInfoDetail } from '@/types';
import { useTheme, Theme } from '@/context/ThemeContext';
interface UserInfoDialogProps {
isOpen: boolean;
@@ -125,6 +126,7 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
const [saving, setSaving] = useState(false);
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const { theme, setTheme } = useTheme();
const [formData, setFormData] = useState<UserInfoDetail>({
Id: '',
@@ -221,6 +223,10 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme);
};
if (!isOpen) return null;
return (
@@ -249,6 +255,52 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
</div>
) : (
<div className="space-y-6">
{/* 테마 설정 섹션 */}
<div className="bg-white/5 rounded-lg p-4 mb-6">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<Palette className="w-4 h-4 text-purple-400" />
</h3>
<div className="grid grid-cols-3 gap-3">
<button
onClick={() => handleThemeChange('dark')}
className={clsx(
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
theme === 'dark'
? 'border-blue-500 bg-blue-500/20 text-white'
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
)}
>
<div className="w-full h-2 rounded-full bg-gradient-to-r from-blue-600 to-purple-600"></div>
<span className="text-sm font-medium"> (Dark)</span>
</button>
<button
onClick={() => handleThemeChange('PSH_PINK')}
className={clsx(
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
theme === 'PSH_PINK'
? 'border-pink-500 bg-pink-500/20 text-white'
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
)}
>
<div className="w-full h-2 rounded-full bg-gradient-to-r from-pink-500 to-rose-500"></div>
<span className="text-sm font-medium"> </span>
</button>
<button
onClick={() => handleThemeChange('JW_SKY')}
className={clsx(
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
theme === 'JW_SKY'
? 'border-sky-500 bg-sky-500/20 text-white'
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
)}
>
<div className="w-full h-2 rounded-full bg-gradient-to-r from-sky-400 to-blue-500"></div>
<span className="text-sm font-medium"> </span>
</button>
</div>
</div>
{/* 기본 정보 */}
<div className="grid grid-cols-2 gap-4">
<div>