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,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>
);
}