feat: apply dark glassmorphism theme to License list and JobReport daily summary dialog
This commit is contained in:
290
Project/frontend/src/components/settings/SettingsDialog.tsx
Normal file
290
Project/frontend/src/components/settings/SettingsDialog.tsx
Normal 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
|
||||
);
|
||||
}
|
||||
65
Project/frontend/src/components/settings/SettingsGeneral.tsx
Normal file
65
Project/frontend/src/components/settings/SettingsGeneral.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
82
Project/frontend/src/components/settings/SettingsTheme.tsx
Normal file
82
Project/frontend/src/components/settings/SettingsTheme.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user