Refactor/Fix: Standardize Dialog Themes & Fix WebSocket Fragmentation. Detail: UserInfoDialog design refresh, standardized all dialogs, fixed backend WebSocketServer fragmentation bug.

This commit is contained in:
backuppc
2025-12-30 17:35:02 +09:00
parent 8528d0206c
commit 5fe21528fc
27 changed files with 590 additions and 411 deletions

View File

@@ -374,6 +374,7 @@
<Compile Include="Web\MachineBridge\MachineBridge.HolidayRequest.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Login.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Dashboard.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Settings.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Todo.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Common.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Jobreport.cs" />

View File

@@ -83,6 +83,9 @@ namespace Project
[DisplayName("Tool Bar")]
public eToolPosition HideToolbar { get; set; }
[DisplayName("테마")]
public string Theme { get; set; }
public Setting() : this(Util.CurrentPath + "setting.xml") { }

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Project;
namespace Project.Web
{
public partial class MachineBridge
{
public string GetSettings()
{
try
{
return JsonConvert.SerializeObject(Pub.setting);
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { Error = ex.Message });
}
}
public string SaveSettings(string jsonSettings)
{
try
{
if (string.IsNullOrEmpty(jsonSettings))
{
return JsonConvert.SerializeObject(new { Success = false, Message = "Empty settings data" });
}
var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(jsonSettings);
// Update properties using reflection or manual mapping
// Since Setting class inherits arUtil.Setting and might have complex types, manual mapping for known properties or generic reflection is better.
// For now, let's target 'Theme' specifically as requested, and generic handling for others if possible?
// Actually, Pub.setting is an instance. We can try to deserialize INTO it, or update properties.
if (dict.ContainsKey("Theme"))
{
Pub.setting.Theme = dict["Theme"]?.ToString();
}
// Add other setting properties here as needed in the future
// or implement a generic property updater
Pub.setting.Save(); // Save to XML
return JsonConvert.SerializeObject(new { Success = true });
}
catch (Exception ex)
{
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
}
}
}
}

View File

@@ -124,14 +124,23 @@ namespace Project.Web
{
try
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
List<byte> messageBytes = new List<byte>();
WebSocketReceiveResult result;
do
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
return;
}
messageBytes.AddRange(new ArraySegment<byte>(buffer, 0, result.Count));
}
else if (result.MessageType == WebSocketMessageType.Text)
while (!result.EndOfMessage);
if (result.MessageType == WebSocketMessageType.Text)
{
string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
string msg = Encoding.UTF8.GetString(messageBytes.ToArray());
await HandleMessage(msg, socket);
}
}
@@ -551,7 +560,7 @@ namespace Project.Web
case "USERLIST_GET_LIST":
{
string process = json.process ?? "%";
string process = json.process ?? string.Empty;
string result = _bridge.UserList_GetList(process);
var response = new { type = "USERLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
@@ -725,6 +734,25 @@ namespace Project.Web
}
break;
// ===== Settings API =====
case "GET_SETTINGS":
{
string result = _bridge.GetSettings();
var response = new { type = "SETTINGS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "SAVE_SETTINGS":
{
string settingsData = JsonConvert.SerializeObject(json.settings);
string result = _bridge.SaveSettings(settingsData);
var response = new { type = "SETTINGS_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== Kuntae API =====
case "GET_KUNTAE_LIST":
{

View File

@@ -126,14 +126,14 @@ export function CustomEditDialog({ isOpen, onClose, onSaved, item }: CustomEditD
/>
{/* 다이얼로그 콘텐트 */}
<div className="relative w-full max-w-2xl bg-[#1a1c1e] border border-white/10 rounded-3xl shadow-2xl overflow-hidden animate-scale-in">
<div className="px-6 py-5 border-b border-white/10 flex items-center justify-between bg-white/[0.02]">
<div className="dialog-container relative w-full max-w-2xl border border-white/10 rounded-3xl overflow-hidden animate-scale-in transition-all duration-300">
<div className="dialog-header px-6 py-5 border-b border-white/10 flex items-center justify-between bg-white/[0.02]">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary-500/20 rounded-xl text-primary-400">
<Building className="w-5 h-5" />
</div>
<div>
<h3 className="text-xl font-bold text-white tracking-tight">
<h3 className="dialog-title tracking-tight">
{item ? '업체 정보 수정' : '새 업체 등록'}
</h3>
<p className="text-white/30 text-[10px] uppercase font-bold tracking-widest mt-0.5">
@@ -329,7 +329,7 @@ export function CustomEditDialog({ isOpen, onClose, onSaved, item }: CustomEditD
</div>
{/* 푸터 버튼 */}
<div className="px-6 py-5 bg-white/[0.02] border-t border-white/10 flex items-center justify-between">
<div className="dialog-footer px-6 py-5 bg-white/[0.02] border-t border-white/10 flex items-center justify-between">
<div>
{item && (
<button

View File

@@ -140,12 +140,12 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
/>
{/* Dialog */}
<div className="relative w-full max-w-4xl mx-4 glass-effect-solid rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
<div className="dialog-container relative w-full max-w-4xl mx-4 rounded-2xl overflow-hidden animate-fade-in transition-all duration-300">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10">
<div className="flex items-center gap-3">
<Star className="w-5 h-5 text-yellow-400" />
<h2 className="text-lg font-semibold text-white"></h2>
<h2 className="dialog-title"></h2>
<span className="text-white/50 text-sm">({favorites.length})</span>
</div>
<button
@@ -190,7 +190,7 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
</div>
{/* Footer */}
<div className="px-6 py-3 border-t border-white/10 bg-black/20">
<div className="dialog-footer px-6 py-3 border-t border-white/10 bg-black/20">
<p className="text-xs text-white/40 text-center">
</p>

View File

@@ -356,10 +356,10 @@ 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-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">
<div className="dialog-container rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto border-2 border-primary transition-all duration-300">
{/* 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">
<div className="dialog-header flex items-center justify-between px-6 py-4 bg-gradient-to-r from-primary-500 via-primary-400 to-primary-600">
<h2 className="dialog-title text-white flex items-center drop-shadow-md">
<Calendar className="w-6 h-6 mr-2 text-white animate-pulse" />
{title}
</h2>
@@ -711,7 +711,7 @@ export function HolidayRequestDialog({
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-primary-500/30 bg-primary-500/10">
<div className="dialog-footer 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-primary-200 hover:text-white hover:bg-primary-500/20 rounded-lg transition-colors font-medium"

View File

@@ -61,13 +61,42 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
// 이미지를 Base64로 변환
const convertToBase64 = (file: File): Promise<string> => {
// 이미지를 압축하여 Base64로 변환
const compressImage = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
reader.onload = (event) => {
const img = new Image();
img.src = event.target?.result as string;
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 640;
const MAX_HEIGHT = 480;
let width = img.width;
let height = img.height;
// 너비가 최대값보다 크면 리사이즈
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
// 높이가 최대값보다 크면 리사이즈 (너비 리사이즈 후에도 높이가 클 수 있음)
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL('image/jpeg', 0.8)); // Quality 0.8
};
img.onerror = (error) => reject(error);
};
reader.onerror = (error) => reject(error);
});
};
@@ -79,7 +108,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
}
try {
const base64 = await convertToBase64(file);
const base64 = await compressImage(file);
setImageData(base64);
// 기존 품목인 경우 바로 저장
@@ -209,14 +238,30 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
{/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */}
<div
className="relative bg-slate-800 rounded-xl shadow-2xl w-full max-w-4xl mx-4 border border-white/10"
className="dialog-container relative rounded-xl w-full max-w-4xl mx-4 transition-all duration-300"
onMouseDown={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<h2 className="text-lg font-semibold text-white">
{isNew ? '품목 추가' : '품목 편집'}
</h2>
<div className="dialog-header flex items-center justify-between p-4">
<div className="flex items-center gap-4">
<h2 className="dialog-title">
{isNew ? '품목 추가' : '품목 편집'}
</h2>
<label className="flex items-center gap-2 cursor-pointer group select-none">
<div className={`w-10 h-5 rounded-full relative transition-colors ${editData.disable ? 'bg-red-500' : 'bg-white/20'}`}>
<input
type="checkbox"
checked={editData.disable}
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
className="sr-only"
/>
<div className={`absolute left-1 top-1 w-3 h-3 bg-white rounded-full transition-transform ${editData.disable ? 'translate-x-[20px]' : ''}`} />
</div>
<span className={`text-sm transition-colors ${editData.disable ? 'text-red-400 font-medium' : 'text-white/50 group-hover:text-white/70'}`}>
{editData.disable ? '비활성화됨' : '비활성화'}
</span>
</label>
</div>
<button
onClick={onClose}
className="p-1 hover:bg-white/10 rounded text-white/70 hover:text-white transition-colors"
@@ -237,7 +282,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
type="text"
value={editData.sid}
onChange={(e) => setEditData({ ...editData, sid: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
className={`w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg ${editData.disable ? 'text-red-400 font-bold' : 'text-white'}`}
/>
</div>
@@ -310,7 +355,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-3 gap-4">
{/* 공급처 */}
<div>
<label className="block text-sm font-medium text-white/70 mb-1"></label>
@@ -332,17 +377,17 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
/>
</div>
</div>
{/* 보관장소 */}
<div>
<label className="block text-sm font-medium text-white/70 mb-1"></label>
<input
type="text"
value={editData.storage}
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
/>
{/* 보관장소 */}
<div>
<label className="block text-sm font-medium text-white/70 mb-1"></label>
<input
type="text"
value={editData.storage}
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
/>
</div>
</div>
{/* 메모 */}
@@ -356,17 +401,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
/>
</div>
{/* 비활성화 */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="disable"
checked={editData.disable}
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
className="w-4 h-4 rounded border-white/20 bg-white/10"
/>
<label htmlFor="disable" className="text-sm text-white/70"></label>
</div>
</div>
{/* 오른쪽: 이미지 영역 */}
@@ -380,11 +415,10 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${
isDragging
? 'border-blue-400 bg-blue-500/20'
: 'border-white/20 bg-white/5 hover:border-white/40'
}`}
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${isDragging
? 'border-blue-400 bg-blue-500/20'
: 'border-white/20 bg-white/5 hover:border-white/40'
}`}
>
{imageLoading ? (
<div className="text-center">
@@ -468,7 +502,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
</div>
{/* 푸터 */}
<div className="flex items-center justify-between p-4 border-t border-white/10">
<div className="dialog-footer flex items-center justify-between p-4">
<div>
{!isNew && (
<button

View File

@@ -225,18 +225,18 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
return (
<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"
className="dialog-container rounded-3xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col transition-all duration-300 animate-scale-in"
onClick={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
<div className="dialog-header px-8 py-6 flex items-center justify-between">
<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>
<h2 className="dialog-title text-xl"> </h2>
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Daily Working Hours Summary</p>
</div>
</div>
@@ -375,7 +375,7 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
</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">
<div className="dialog-footer px-8 py-4 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">

View File

@@ -87,11 +87,11 @@ export function JobTypeSelectModal({
// 검색 필터 적용
const filteredTypes = searchKey
? jobTypes.filter(
(item) =>
item.type?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.process?.toLowerCase().includes(searchKey.toLowerCase())
)
(item) =>
item.type?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.process?.toLowerCase().includes(searchKey.toLowerCase())
)
: jobTypes;
// 그룹핑
@@ -200,12 +200,12 @@ export function JobTypeSelectModal({
>
<div className="flex items-center justify-center min-h-screen p-4">
<div
className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up max-h-[85vh] flex flex-col"
className="dialog-container rounded-2xl w-full max-w-2xl animate-slide-up max-h-[85vh] flex flex-col transition-all duration-300"
onMouseDown={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
<h2 className="text-xl font-semibold text-white"> </h2>
<div className="dialog-header px-6 py-4 flex items-center justify-between">
<h2 className="dialog-title"> </h2>
<button
onClick={onClose}
className="text-white/70 hover:text-white transition-colors"
@@ -288,11 +288,10 @@ export function JobTypeSelectModal({
return (
<button
key={typePath}
className={`w-full px-8 py-1.5 text-left transition-colors ${
isSelected
className={`w-full px-8 py-1.5 text-left transition-colors ${isSelected
? 'bg-primary-500/40 text-primary-200'
: 'text-white/70 hover:bg-white/10 hover:text-white'
}`}
}`}
onClick={() => setSelectedPath(typePath)}
onDoubleClick={() => typeNode.item && handleDoubleClick(typeNode.item)}
>
@@ -335,7 +334,7 @@ export function JobTypeSelectModal({
)}
{/* 푸터 */}
<div className="px-6 py-4 border-t border-white/10 flex justify-end space-x-3">
<div className="dialog-footer px-6 py-4 flex justify-end space-x-3">
<button
onClick={onClose}
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"

View File

@@ -137,7 +137,7 @@ export function JobreportEditModal({
const lastReport = await comms.getLastJobReportByProject(formData.pidx, formData.projectName);
if (lastReport.Success && lastReport.Data) {
const updatedFormData = { ...formData };
if (lastReport.Data.requestpart) {
updatedFormData.requestpart = lastReport.Data.requestpart;
}
@@ -261,14 +261,13 @@ export function JobreportEditModal({
>
<div className="flex items-center justify-center min-h-screen p-4">
<div
className="glass-effect rounded-2xl w-full max-w-3xl animate-slide-up max-h-[90vh] overflow-y-auto"
className="dialog-container rounded-2xl w-full max-w-3xl animate-slide-up max-h-[90vh] overflow-y-auto transition-all duration-300"
onMouseDown={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className={`px-6 py-4 border-b border-white/10 flex items-center justify-between sticky top-0 backdrop-blur z-10 ${
editingItem ? 'bg-slate-800/95' : 'bg-primary-600/30'
}`}>
<h2 className="text-xl font-semibold text-white flex items-center">
<div className={`dialog-header px-6 py-4 flex items-center justify-between sticky top-0 z-10 ${editingItem ? '' : ''
}`}>
<h2 className="dialog-title flex items-center">
<FileText className="w-5 h-5 mr-2" />
{editingItem ? '업무일지 수정' : '업무일지 등록'}
</h2>
@@ -517,7 +516,7 @@ export function JobreportEditModal({
</div>
{/* 푸터 */}
<div className="px-6 py-4 border-t border-white/10 flex justify-between sticky bottom-0 bg-slate-800/95 backdrop-blur">
<div className="dialog-footer px-6 py-4 flex justify-between sticky bottom-0 backdrop-blur">
{/* 좌측: 삭제 버튼 (편집 모드일 때만) */}
<div>
{editingItem && (

View File

@@ -52,10 +52,10 @@ export function JobreportTypeModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
<div className="bg-gray-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden">
<div className="dialog-container rounded-2xl w-full max-w-2xl overflow-hidden transition-all duration-300">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
<h3 className="text-lg font-semibold text-white"> </h3>
<div className="dialog-header px-6 py-4 flex items-center justify-between">
<h3 className="dialog-title"> </h3>
<button
onClick={onClose}
className="text-white/50 hover:text-white transition-colors"
@@ -128,7 +128,7 @@ export function JobreportTypeModal({
</div>
{/* 푸터 */}
<div className="px-6 py-4 border-t border-white/10 flex justify-end bg-white/5">
<div className="dialog-footer px-6 py-4 flex justify-end">
<button
onClick={onClose}
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"

View File

@@ -97,14 +97,14 @@ export function ProjectSearchDialog({
onMouseDown={onClose}
>
<div
className="glass-effect rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col"
className="dialog-container rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col transition-all duration-300"
onMouseDown={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="p-4 border-b border-white/10 flex items-center justify-between shrink-0">
<div className="dialog-header p-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2">
<Folder className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">/ </h2>
<h2 className="dialog-title">/ </h2>
</div>
<button
onClick={onClose}
@@ -151,11 +151,10 @@ export function ProjectSearchDialog({
onSelect({ idx: project.idx, name: project.name });
onClose();
}}
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
selectedProject?.idx === project.idx && selectedProject?.name === project.name
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedProject?.idx === project.idx && selectedProject?.name === project.name
? 'bg-primary-500/30 border border-primary-400/50'
: 'bg-white/5 hover:bg-white/10 border border-transparent'
}`}
}`}
>
{/* 아이콘 */}
<div className={`shrink-0 ${project.source === 'project' ? 'text-blue-400' : 'text-gray-400'}`}>
@@ -174,11 +173,10 @@ export function ProjectSearchDialog({
)}
<span className="font-medium text-white truncate">{project.name}</span>
{project.status && (
<span className={`text-xs px-1.5 py-0.5 rounded ${
project.status === '진행' ? 'bg-green-500/20 text-green-400' :
project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-gray-500/20 text-gray-400'
}`}>
<span className={`text-xs px-1.5 py-0.5 rounded ${project.status === '진행' ? 'bg-green-500/20 text-green-400' :
project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-gray-500/20 text-gray-400'
}`}>
{project.status}
</span>
)}
@@ -209,7 +207,7 @@ export function ProjectSearchDialog({
</div>
{/* 푸터 */}
<div className="p-4 border-t border-white/10 flex items-center justify-between shrink-0">
<div className="dialog-footer p-4 flex items-center justify-between shrink-0">
<span className="text-sm text-white/50">
{projects.length}
{selectedProject && ` | 선택: ${selectedProject.name}`}

View File

@@ -116,10 +116,10 @@ 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-bg-paper rounded-2xl shadow-2xl w-full max-w-lg border border-white/10 overflow-hidden">
<div className="dialog-container rounded-2xl w-full max-w-lg overflow-hidden transition-all duration-300">
{/* 헤더 */}
<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">
<div className="dialog-header px-6 py-4 flex justify-between items-center">
<h2 className="dialog-title flex items-center">
<Calendar className="w-5 h-5 mr-2 text-primary-400" />
{title}
</h2>
@@ -262,7 +262,7 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
</div>
{/* 버튼 */}
<div className="flex justify-end space-x-3 pt-4 border-t border-white/10">
<div className="dialog-footer flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={onClose}

View File

@@ -188,10 +188,10 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10000]">
<div className="glass-effect-solid rounded-xl w-[900px] max-h-[90vh] flex flex-col">
<div className="dialog-container rounded-xl w-[900px] max-h-[90vh] flex flex-col transition-all duration-300">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<div className="dialog-header flex items-center justify-between px-6 py-4">
<h2 className="dialog-title flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-warning-400" />
</h2>
@@ -355,9 +355,8 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
{ngList.map((item) => (
<tr
key={item.Date}
className={`hover:bg-white/5 cursor-pointer ${
item.IsMagam ? 'text-blue-400' : 'text-danger-400'
}`}
className={`hover:bg-white/5 cursor-pointer ${item.IsMagam ? 'text-blue-400' : 'text-danger-400'
}`}
onClick={() => toggleError(item.Date, item.IsMagam)}
>
<td className="px-2 py-2 text-center border-r border-white/10">
@@ -396,7 +395,7 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
</div>
{/* 하단 버튼 */}
<div className="px-6 py-4 border-t border-white/10">
<div className="dialog-footer px-6 py-4">
<button
onClick={handleFix}
disabled={isChecking || isFixing || selectedErrors.size === 0}

View File

@@ -88,15 +88,15 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
return (
<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()}>
<div className="dialog-container rounded-3xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300 animate-scale-in" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
<div className="dialog-header px-8 py-6 flex items-center justify-between">
<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">
<h2 className="dialog-title">
{formData.idx ? '라이선스 수정' : '라이선스 추가'}
</h2>
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Edit License Details</p>
@@ -290,7 +290,7 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
</div>
{/* Footer */}
<div className="px-8 py-6 border-t border-white/10 flex items-center justify-between bg-white/5">
<div className="dialog-footer px-8 py-6 flex items-center justify-between">
<div>
{formData.idx && onDelete && (
<button

View File

@@ -132,12 +132,12 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
return (
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="relative w-full max-w-3xl glass-effect-solid rounded-2xl shadow-2xl overflow-hidden">
<div className="dialog-container relative w-full max-w-3xl rounded-2xl overflow-hidden transition-all duration-300">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<div className="dialog-header flex items-center justify-between px-6 py-4">
<div className="flex items-center gap-3">
<Mail className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white"> </h2>
<h2 className="dialog-title"> </h2>
</div>
<button
onClick={onClose}
@@ -238,7 +238,7 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10 bg-black/20">
<div className="dialog-footer flex items-center justify-end gap-2 px-6 py-4">
<button
onClick={onClose}
disabled={processing}

View File

@@ -157,11 +157,11 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-slate-800/95 backdrop-blur rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col shadow-2xl border border-white/10">
<div className="dialog-container rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col overflow-hidden transition-all duration-300">
{/* 헤더 */}
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-primary-600/30 sticky top-0 z-10">
<div className="dialog-header flex items-center justify-between p-4 sticky top-0 z-10">
<div>
<h2 className="text-lg font-bold text-white"></h2>
<h2 className="dialog-title"></h2>
<p className="text-sm text-white/60">{projectName}</p>
</div>
<div className="flex items-center gap-2">
@@ -225,9 +225,8 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
return (
<tr
key={part.idx}
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${
isEditing ? 'bg-primary-500/10' : ''
}`}
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${isEditing ? 'bg-primary-500/10' : ''
}`}
>
<td className="px-2 py-2">
{isEditing ? (
@@ -500,7 +499,7 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
{/* 합계 */}
{parts.length > 0 && (
<div className="p-4 border-t border-white/10 bg-slate-900/50">
<div className="dialog-footer p-4">
<div className="flex justify-end gap-4 text-sm">
<span className="text-white/70">
<span className="text-white font-medium">{parts.length}</span>

View File

@@ -108,9 +108,9 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-gray-900/95 border border-white/10 rounded-2xl shadow-2xl w-[1200px] h-[90vh] flex flex-col">
<div className="dialog-container rounded-2xl w-[1200px] h-[90vh] flex flex-col overflow-hidden transition-all duration-300">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
<div className="dialog-header flex items-center justify-between px-6 py-4 shrink-0">
<div className="flex items-center gap-4">
<select
value={formData.status}
@@ -473,7 +473,7 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
</div>
{/* 푸터 */}
<div className="flex items-center justify-between px-6 py-3 border-t border-white/10 shrink-0 bg-white/5">
<div className="dialog-footer flex items-center justify-between px-6 py-3 shrink-0">
<span className="text-sm text-white/50">PNO: {project.pno || '-'} | Jasmin: {project.jasmin || '-'} | : {formData.progress}%</span>
<button
onClick={onClose}

View File

@@ -93,10 +93,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
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]">
<div className="dialog-container rounded-2xl w-full max-w-2xl animate-slide-up mx-4 overflow-hidden flex flex-col max-h-[80vh] transition-all duration-300">
{/* 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">
<div className="dialog-header px-6 py-4 flex items-center justify-between">
<h2 className="dialog-title flex items-center">
<SettingsIcon className="w-5 h-5 mr-2 text-primary-400" />
</h2>
@@ -263,7 +263,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end space-x-3">
<div className="dialog-footer px-6 py-4 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"

View File

@@ -232,15 +232,15 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-gray-900/95 border border-white/10 rounded-2xl shadow-2xl w-[1000px] max-h-[85vh] flex flex-col">
<div className="dialog-container rounded-2xl w-[1000px] max-h-[85vh] flex flex-col overflow-hidden transition-all duration-300">
{/* 헤더 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
<div className="dialog-header flex items-center justify-between px-6 py-4 shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary-500/20 rounded-lg">
<Users className="w-5 h-5 text-primary-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white"></h2>
<h2 className="dialog-title"></h2>
<p className="text-white/50 text-sm">/ </p>
</div>
</div>
@@ -350,7 +350,7 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
</div>
{/* 푸터 */}
<div className="flex items-center justify-between px-6 py-3 border-t border-white/10 shrink-0 bg-white/5">
<div className="dialog-footer flex items-center justify-between px-6 py-3 shrink-0">
<span className="text-sm text-white/50">{filteredItems.length} </span>
<button
onClick={onClose}
@@ -364,9 +364,9 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
{/* 그룹 편집 모달 */}
{showModal && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
<div className="bg-gray-800 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<h2 className="text-xl font-bold text-white">
<div className="dialog-container rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
<div className="dialog-header flex items-center justify-between px-6 py-4">
<h2 className="dialog-title">
{editingItem ? '그룹 수정' : '새 그룹'}
</h2>
<button
@@ -472,10 +472,10 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
{/* 권한 설정 모달 */}
{showPermissionModal && editingItem && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
<div className="bg-gray-800 rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<div className="dialog-container rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
<div className="dialog-header flex items-center justify-between px-6 py-4">
<div>
<h2 className="text-xl font-bold text-white"> </h2>
<h2 className="dialog-title"> </h2>
<p className="text-white/60 text-sm">{editingItem.dept}</p>
</div>
<button

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { X, Save, User, Mail, Building2, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
import { X, Save, User, Mail, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
import { clsx } from 'clsx';
import { comms } from '@/communication';
import { UserInfoDetail } from '@/types';
@@ -109,276 +109,350 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
return createPortal(
<>
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10000]">
<div className="dialog-container rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden transition-all duration-300">
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[10000] animate-fade-in">
<div className="dialog-container w-full max-w-4xl h-[85vh] flex flex-col shadow-2xl border border-white/10 bg-[#1a1b2e]/90 backdrop-blur-xl rounded-3xl overflow-hidden">
{/* Header */}
<div className="dialog-header flex items-center justify-between px-6 py-4">
<h2 className="dialog-title">
<User className="w-6 h-6" />
</h2>
<div className="dialog-header px-8 py-6 border-b border-white/10 bg-white/5 flex items-center justify-between shrink-0">
<div className="flex items-center gap-4">
<div className="p-3 bg-primary-500/20 rounded-xl">
<User className="w-6 h-6 text-primary-400" />
</div>
<div>
<h2 className="dialog-title text-2xl"> </h2>
<p className="text-white/40 text-xs mt-1"> .</p>
</div>
</div>
<button
onClick={onClose}
className="text-text-secondary hover:text-text-primary transition-colors"
className="p-2 hover:bg-white/10 rounded-full text-white/40 hover:text-white transition-all transform hover:rotate-90"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)] custom-scrollbar">
<div className="flex-1 overflow-hidden">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
<div className="h-full flex flex-col items-center justify-center gap-4">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary-400"></div>
<p className="text-white/40 text-sm"> ...</p>
</div>
) : (
<div className="flex flex-col h-full gap-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 min-h-0">
{/* 좌측 컬럼: 기본 정보 */}
<div className="space-y-4 overflow-y-auto pr-2 custom-scrollbar">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<User className="w-4 h-4" />
<div className="flex flex-col lg:flex-row h-full">
{/* Left Panel: Profile Info */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-8 border-r border-white/10 bg-white/[0.02]">
<div className="space-y-8">
{/* Identity Section */}
<section className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-4 bg-primary-500 rounded-full"></div>
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider"> </h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<div className="px-4 py-3.5 bg-white/5 border border-white/10 rounded-xl text-white/50 font-mono text-sm flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-white/20"></span>
{formData.Id}
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<div className="relative group">
<Briefcase className="absolute left-4 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={formData.Grade}
onChange={(e) => handleInputChange('Grade', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-medium"
placeholder="직책 입력"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"> ()</label>
<input
type="text"
value={formData.NameK}
onChange={(e) => handleInputChange('NameK', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-bold tracking-tight"
placeholder="이름 입력"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"> ()</label>
<input
type="text"
value={formData.NameE}
onChange={(e) => handleInputChange('NameE', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-medium"
placeholder="English Name"
/>
</div>
</div>
</section>
{/* Contact Section */}
<section className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-4 bg-secondary-500 rounded-full"></div>
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider"> </h3>
</div>
<div className="space-y-5">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<div className="relative group">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-secondary-400 transition-colors" />
<input
type="email"
value={formData.Email}
onChange={(e) => handleInputChange('Email', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all font-medium"
placeholder="email@company.com"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"> ()</label>
<input
type="text"
value={formData.Tel}
onChange={(e) => handleInputChange('Tel', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all"
placeholder="내선번호"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<input
type="text"
value={formData.Hp}
onChange={(e) => handleInputChange('Hp', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all"
placeholder="010-0000-0000"
/>
</div>
</div>
</div>
</section>
{/* Employment Section */}
<section className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-1 h-4 bg-indigo-500 rounded-full"></div>
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider"> </h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<input
type="text"
value={formData.Dept}
onChange={(e) => handleInputChange('Dept', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all"
placeholder="소속 부서"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<input
type="text"
value={formData.Process}
onChange={(e) => handleInputChange('Process', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all"
placeholder="담당 공정"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-5">
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<div className="relative">
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 pointer-events-none" />
<input
type="date"
value={formData.DateIn}
onChange={(e) => handleInputChange('DateIn', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all [color-scheme:dark] font-mono"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1"></label>
<div className="relative">
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 pointer-events-none" />
<input
type="date"
value={formData.DateO}
onChange={(e) => handleInputChange('DateO', e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all [color-scheme:dark] font-mono"
/>
</div>
</div>
</div>
<div className="pt-2 flex flex-wrap gap-4">
<label className={clsx(
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
formData.UseJobReport ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
)}>
<div className={clsx(
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
formData.UseJobReport ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
)}>
<Briefcase className="w-3 h-3" />
</div>
<input
type="checkbox"
checked={formData.UseJobReport}
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
className="hidden"
/>
<span className={clsx("text-xs font-bold", formData.UseJobReport ? "text-primary-300" : "text-white/50")}> </span>
</label>
<input
type="text"
value={formData.Id}
disabled
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<input
type="text"
value={formData.NameK}
onChange={(e) => handleInputChange('NameK', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="이름"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1"></label>
<input
type="text"
value={formData.NameE}
onChange={(e) => handleInputChange('NameE', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="English Name"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<Briefcase className="w-4 h-4" />
<label className={clsx(
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
formData.UseUserState ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
)}>
<div className={clsx(
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
formData.UseUserState ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
)}>
<User className="w-3 h-3" />
</div>
<input
type="checkbox"
checked={formData.UseUserState}
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
className="hidden"
/>
<span className={clsx("text-xs font-bold", formData.UseUserState ? "text-primary-300" : "text-white/50")}> </span>
</label>
<input
type="text"
value={formData.Grade}
onChange={(e) => handleInputChange('Grade', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="직책"
/>
</div>
</div>
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1"></label>
<input
type="text"
value={formData.Process}
onChange={(e) => handleInputChange('Process', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="공정"
/>
</div>
{/* 이메일 */}
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<Mail className="w-4 h-4" />
</label>
<input
type="email"
value={formData.Email}
onChange={(e) => handleInputChange('Email', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="email@example.com"
/>
</div>
{/* 입/퇴사 정보 */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<Calendar className="w-4 h-4" />
<label className={clsx(
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
formData.ExceptHoly ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
)}>
<div className={clsx(
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
formData.ExceptHoly ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
)}>
<Calendar className="w-3 h-3" />
</div>
<input
type="checkbox"
checked={formData.ExceptHoly}
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
className="hidden"
/>
<span className={clsx("text-xs font-bold", formData.ExceptHoly ? "text-primary-300" : "text-white/50")}> </span>
</label>
<input
type="text"
value={formData.DateIn}
onChange={(e) => handleInputChange('DateIn', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="YYYY-MM-DD"
/>
</div>
<div>
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<Calendar className="w-4 h-4" />
</label>
<input
type="text"
value={formData.DateO}
onChange={(e) => handleInputChange('DateO', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
placeholder="YYYY-MM-DD"
/>
</div>
</div>
{/* 옵션 체크박스 */}
<div className="flex flex-wrap gap-6 pt-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.UseJobReport}
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
/>
<span className="text-white/70"> </span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.UseUserState}
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
/>
<span className="text-white/70"> </span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.ExceptHoly}
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
/>
<span className="text-white/70"> </span>
</label>
</div>
</div>
{/* 우측 컬럼: 테마 설정 + 비고 */}
<div className="flex flex-col h-full gap-4">
{/* 테마 설정 섹션 */}
<div className="bg-white/5 rounded-lg p-4">
<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-1 gap-3">
<button
onClick={() => handleThemeChange('dark')}
className={clsx(
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
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-6 h-6 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 border border-white/20 shrink-0"></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 items-center gap-3',
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-6 h-6 rounded-full bg-gradient-to-r from-pink-500 to-rose-500 border border-white/20 shrink-0"></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 items-center gap-3',
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-6 h-6 rounded-full bg-gradient-to-r from-sky-400 to-blue-500 border border-white/20 shrink-0"></div>
<span className="text-sm font-medium"> </span>
</button>
</div>
</div>
{/* 비고: 남은 높이 채우기 */}
<div className="flex-1 flex flex-col">
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
<FileText className="w-4 h-4" />
</label>
<textarea
value={formData.Memo}
onChange={(e) => handleInputChange('Memo', e.target.value)}
className="flex-1 w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 resize-none"
placeholder="비고"
/>
</div>
</section>
</div>
</div>
{/* 메시지 */}
{message && (
<div
className={clsx(
'px-4 py-2 rounded-lg text-sm',
message.type === 'success' ? 'bg-green-600/20 text-green-400' : 'bg-red-600/20 text-red-400'
)}
>
{message.text}
{/* Right Panel: Preferences & Memo */}
<div className="w-full lg:w-96 flex flex-col h-full bg-[#131426]/50">
<div className="p-6 md:p-8 space-y-8 flex-1 overflow-y-auto custom-scrollbar">
{/* Theme Section */}
<section className="space-y-4">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-4 h-4 text-purple-400" />
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider"> </h3>
</div>
<div className="grid grid-cols-1 gap-3">
{[
{ id: 'dark', name: 'Standard Dark', desc: '기본 어두운 테마', gradient: 'from-blue-600 to-indigo-900', border: 'border-blue-500' },
{ id: 'PSH_PINK', name: 'Vibrant Pink', desc: '발랄한 핑크 테마', gradient: 'from-pink-500 to-rose-500', border: 'border-pink-500' },
{ id: 'JW_SKY', name: 'Fresh Sky', desc: '시원한 하늘 테마', gradient: 'from-sky-400 to-blue-500', border: 'border-sky-500' },
].map((t) => (
<button
key={t.id}
onClick={() => handleThemeChange(t.id as Theme)}
className={clsx(
"group relative overflow-hidden rounded-xl border transition-all duration-300 text-left p-4",
theme === t.id
? `bg-white/5 ${t.border} ring-1 ring-white/20`
: "bg-white/5 border-white/10 hover:border-white/30 hover:bg-white/10"
)}
>
<div className={clsx("absolute inset-0 opacity-10 bg-gradient-to-br transition-opacity", t.gradient, theme === t.id ? "opacity-20" : "group-hover:opacity-15")} />
<div className="relative z-10 flex items-center justify-between">
<div>
<h4 className={clsx("font-bold text-sm", theme === t.id ? "text-white" : "text-white/70 group-hover:text-white")}>{t.name}</h4>
<p className="text-xs text-white/40 mt-0.5">{t.desc}</p>
</div>
<div className={clsx("w-8 h-8 rounded-full bg-gradient-to-br shadow-lg", t.gradient)} />
</div>
</button>
))}
</div>
</section>
{/* Memo Section */}
<section className="flex-1 flex flex-col min-h-[200px]">
<div className="flex items-center gap-2 mb-4">
<FileText className="w-4 h-4 text-white/40" />
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider"> & </h3>
</div>
<div className="flex-1 relative group">
<textarea
value={formData.Memo}
onChange={(e) => handleInputChange('Memo', e.target.value)}
className="w-full h-full bg-white/5 border border-white/10 rounded-xl p-4 text-sm text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all resize-none leading-relaxed"
placeholder="사용자에 대한 추가 정보를 입력하세요..."
/>
<div className="absolute bottom-4 right-4 text-[10px] text-white/20 font-mono">
MEMO AREA
</div>
</div>
</section>
</div>
)}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="dialog-footer flex items-center justify-end px-6 py-4">
<div className="flex gap-2">
<div className="dialog-footer px-8 py-5 border-t border-white/10 bg-[#131426] shrink-0 flex items-center justify-between">
<div className="text-xs text-white/30 font-medium">
{message && (
<span className={clsx(
"px-3 py-1.5 rounded-lg inline-flex items-center gap-2",
message.type === 'success' ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
)}>
<div className={clsx("w-1.5 h-1.5 rounded-full", message.type === 'success' ? "bg-green-500" : "bg-red-500")} />
{message.text}
</span>
)}
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white/70 hover:text-white transition-colors"
className="px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white/70 hover:text-white text-sm font-bold transition-all active:scale-95"
>
</button>
<button
onClick={handleSave}
disabled={saving}
className={clsx(
'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors',
saving
? 'bg-blue-600/50 text-white/50 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 text-white'
)}
className="px-8 py-2.5 bg-primary-500 hover:bg-primary-600 border border-white/20 rounded-xl text-white text-sm font-bold transition-all shadow-lg shadow-primary-500/20 active:scale-95 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="w-4 h-4" />
{saving ? '저장 중...' : '저장'}
{saving ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
{saving ? '저장 중...' : '저장 완료'}
</button>
</div>
</div>
</div>
</div >
</div>
</>,
document.body
);

View File

@@ -98,18 +98,18 @@ export function UserSearchDialog({
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm"
onClick={onClose}
>
<div
className="glass-effect rounded-xl w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col"
className="dialog-container rounded-xl w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col transition-all duration-300"
onClick={(e) => e.stopPropagation()}
>
{/* 헤더 */}
<div className="p-4 border-b border-white/10 flex items-center justify-between shrink-0">
<div className="dialog-header p-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">{title}</h2>
<h2 className="dialog-title">{title}</h2>
</div>
<button
onClick={onClose}
@@ -156,11 +156,10 @@ export function UserSearchDialog({
onSelect(user);
onClose();
}}
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
selectedUser?.id === user.id
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedUser?.id === user.id
? 'bg-primary-500/30 border border-primary-400/50'
: 'bg-white/5 hover:bg-white/10 border border-transparent'
}`}
}`}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
@@ -186,7 +185,7 @@ export function UserSearchDialog({
</div>
{/* 푸터 */}
<div className="p-4 border-t border-white/10 flex items-center justify-between shrink-0">
<div className="dialog-footer p-4 flex items-center justify-between shrink-0">
<span className="text-sm text-white/50">
{filteredUsers.length} {selectedUser && `| 선택: ${selectedUser.id} (${selectedUser.name})`}
</span>

View File

@@ -709,9 +709,9 @@ export function Dashboard() {
{/* 업무일지 미등록 상세 모달 */}
{showUnregisteredModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-bg-paper border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<div className="dialog-container border border-white/10 rounded-2xl w-full max-w-md overflow-hidden animate-scale-in transition-all duration-300">
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
<h3 className="dialog-title flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-danger-400" />
</h3>
@@ -754,7 +754,7 @@ export function Dashboard() {
)}
</div>
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
<div className="dialog-footer px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
<button
onClick={() => {
setShowUnregisteredModal(false);
@@ -898,8 +898,8 @@ export function Dashboard() {
type="button"
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${todoFormData.status === option.value
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
}`}
>
{option.label}
@@ -1058,8 +1058,8 @@ export function Dashboard() {
type="button"
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${todoFormData.status === option.value
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
}`}
>
{option.label}

View File

@@ -6,9 +6,8 @@ import { ItemInfo, ItemDetail, SupplierStaff, PurchaseHistoryItem } from '@/type
import { ItemEditDialog } from '@/components/items';
export function ItemsPage() {
const [categories, setCategories] = useState<string[]>([]);
const [items, setItems] = useState<ItemInfo[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [selectedCategory] = useState<string>('all');
const [searchKey, setSearchKey] = useState('');
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState('');
@@ -24,20 +23,9 @@ export function ItemsPage() {
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
loadCategories();
// No categories to load
}, []);
const loadCategories = async () => {
try {
const result = await comms.getItemCategories();
if (result.Success && result.Data) {
setCategories(result.Data);
}
} catch (error) {
console.error('카테고리 로드 실패:', error);
}
};
const loadItems = async () => {
if (!searchKey.trim()) {
alert('검색어를 입력하세요');

View File

@@ -802,9 +802,9 @@ export function Jobreport() {
{/* 업무일지 미등록 상세 모달 */}
{showUnregisteredModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-slate-900 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<div className="dialog-container border border-white/10 rounded-2xl w-full max-w-md overflow-hidden animate-scale-in transition-all duration-300">
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
<h3 className="dialog-title flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-danger-400" />
</h3>
@@ -847,7 +847,7 @@ export function Jobreport() {
)}
</div>
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
<div className="dialog-footer px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
<button
onClick={() => setShowUnregisteredModal(false)}
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm font-medium"

View File

@@ -555,14 +555,14 @@ function TodoModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md animate-fade-in" onClick={onClose}>
<div className="bg-[#1a1b2e]/90 rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden border border-white/10 flex flex-col backdrop-blur-xl" onClick={(e) => e.stopPropagation()}>
<div className="dialog-container w-full max-w-2xl" onClick={(e) => e.stopPropagation()}>
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-6 border-b border-white/10 bg-white/5">
<div className="dialog-header">
<div className="flex items-center gap-4">
<div className="p-2 bg-primary-500/20 rounded-lg">
<div className={`p-2 rounded-lg ${isEdit ? 'bg-primary-500/20' : 'bg-primary-500/20'}`}>
{isEdit ? <Edit3 className="w-5 h-5 text-primary-400" /> : <Plus className="w-5 h-5 text-primary-400" />}
</div>
<h2 className="text-xl font-bold text-white tracking-tight">{title}</h2>
<h2 className="dialog-title">{title}</h2>
</div>
<div className="flex items-center gap-3">
{isEdit && onComplete && currentStatus !== '5' && (
@@ -709,7 +709,7 @@ function TodoModal({
</div>
{/* 푸터 */}
<div className="px-8 py-6 border-t border-white/10 bg-white/5 flex items-center justify-between">
<div className="dialog-footer">
<div>
{isEdit && onDelete && (
<button