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.HolidayRequest.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Login.cs" /> <Compile Include="Web\MachineBridge\MachineBridge.Login.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Dashboard.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.Todo.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Common.cs" /> <Compile Include="Web\MachineBridge\MachineBridge.Common.cs" />
<Compile Include="Web\MachineBridge\MachineBridge.Jobreport.cs" /> <Compile Include="Web\MachineBridge\MachineBridge.Jobreport.cs" />

View File

@@ -83,6 +83,9 @@ namespace Project
[DisplayName("Tool Bar")] [DisplayName("Tool Bar")]
public eToolPosition HideToolbar { get; set; } public eToolPosition HideToolbar { get; set; }
[DisplayName("테마")]
public string Theme { get; set; }
public Setting() : this(Util.CurrentPath + "setting.xml") { } 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 try
{ {
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); List<byte> messageBytes = new List<byte>();
if (result.MessageType == WebSocketMessageType.Close) 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); await HandleMessage(msg, socket);
} }
} }
@@ -551,7 +560,7 @@ namespace Project.Web
case "USERLIST_GET_LIST": case "USERLIST_GET_LIST":
{ {
string process = json.process ?? "%"; string process = json.process ?? string.Empty;
string result = _bridge.UserList_GetList(process); string result = _bridge.UserList_GetList(process);
var response = new { type = "USERLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; var response = new { type = "USERLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response)); await Send(socket, JsonConvert.SerializeObject(response));
@@ -725,6 +734,25 @@ namespace Project.Web
} }
break; 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 ===== // ===== Kuntae API =====
case "GET_KUNTAE_LIST": 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="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="px-6 py-5 border-b border-white/10 flex items-center justify-between bg-white/[0.02]"> <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="flex items-center gap-3">
<div className="p-2 bg-primary-500/20 rounded-xl text-primary-400"> <div className="p-2 bg-primary-500/20 rounded-xl text-primary-400">
<Building className="w-5 h-5" /> <Building className="w-5 h-5" />
</div> </div>
<div> <div>
<h3 className="text-xl font-bold text-white tracking-tight"> <h3 className="dialog-title tracking-tight">
{item ? '업체 정보 수정' : '새 업체 등록'} {item ? '업체 정보 수정' : '새 업체 등록'}
</h3> </h3>
<p className="text-white/30 text-[10px] uppercase font-bold tracking-widest mt-0.5"> <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>
{/* 푸터 버튼 */} {/* 푸터 버튼 */}
<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> <div>
{item && ( {item && (
<button <button

View File

@@ -140,12 +140,12 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
/> />
{/* Dialog */} {/* 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 */} {/* 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"> <div className="flex items-center gap-3">
<Star className="w-5 h-5 text-yellow-400" /> <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> <span className="text-white/50 text-sm">({favorites.length})</span>
</div> </div>
<button <button
@@ -190,7 +190,7 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
</div> </div>
{/* Footer */} {/* 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 className="text-xs text-white/40 text-center">
</p> </p>

View File

@@ -356,10 +356,10 @@ export function HolidayRequestDialog({
return ( 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="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 */} {/* 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"> <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="text-xl font-bold text-white flex items-center drop-shadow-md"> <h2 className="dialog-title text-white flex items-center drop-shadow-md">
<Calendar className="w-6 h-6 mr-2 text-white animate-pulse" /> <Calendar className="w-6 h-6 mr-2 text-white animate-pulse" />
{title} {title}
</h2> </h2>
@@ -711,7 +711,7 @@ export function HolidayRequestDialog({
</div> </div>
{/* Footer */} {/* 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 <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-primary-200 hover:text-white hover:bg-primary-500/20 rounded-lg transition-colors font-medium" className="px-4 py-2 text-primary-200 hover:text-white hover:bg-primary-500/20 rounded-lg transition-colors font-medium"

View File

@@ -61,13 +61,42 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]); }, [isOpen, onClose]);
// 이미지를 Base64로 변환 // 이미지를 압축하여 Base64로 변환
const convertToBase64 = (file: File): Promise<string> => { const compressImage = (file: File): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file); 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 { try {
const base64 = await convertToBase64(file); const base64 = await compressImage(file);
setImageData(base64); setImageData(base64);
// 기존 품목인 경우 바로 저장 // 기존 품목인 경우 바로 저장
@@ -209,14 +238,30 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
{/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */} {/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */}
<div <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()} onMouseDown={(e) => e.stopPropagation()}
> >
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between p-4 border-b border-white/10"> <div className="dialog-header flex items-center justify-between p-4">
<h2 className="text-lg font-semibold text-white"> <div className="flex items-center gap-4">
{isNew ? '품목 추가' : '품목 편집'} <h2 className="dialog-title">
</h2> {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 <button
onClick={onClose} onClick={onClose}
className="p-1 hover:bg-white/10 rounded text-white/70 hover:text-white transition-colors" 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" type="text"
value={editData.sid} value={editData.sid}
onChange={(e) => setEditData({ ...editData, sid: e.target.value })} 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> </div>
@@ -310,7 +355,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
{/* 공급처 */} {/* 공급처 */}
<div> <div>
<label className="block text-sm font-medium text-white/70 mb-1"></label> <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" className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
/> />
</div> </div>
</div>
{/* 보관장소 */} {/* 보관장소 */}
<div> <div>
<label className="block text-sm font-medium text-white/70 mb-1"></label> <label className="block text-sm font-medium text-white/70 mb-1"></label>
<input <input
type="text" type="text"
value={editData.storage} value={editData.storage}
onChange={(e) => setEditData({ ...editData, storage: e.target.value })} 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" className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
/> />
</div>
</div> </div>
{/* 메모 */} {/* 메모 */}
@@ -356,17 +401,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
/> />
</div> </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> </div>
{/* 오른쪽: 이미지 영역 */} {/* 오른쪽: 이미지 영역 */}
@@ -380,11 +415,10 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={handleDrop} onDrop={handleDrop}
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${ className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${isDragging
isDragging ? 'border-blue-400 bg-blue-500/20'
? 'border-blue-400 bg-blue-500/20' : 'border-white/20 bg-white/5 hover:border-white/40'
: 'border-white/20 bg-white/5 hover:border-white/40' }`}
}`}
> >
{imageLoading ? ( {imageLoading ? (
<div className="text-center"> <div className="text-center">
@@ -468,7 +502,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
</div> </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> <div>
{!isNew && ( {!isNew && (
<button <button

View File

@@ -225,18 +225,18 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
return ( 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="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div <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()} 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-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="p-2.5 bg-primary-500/20 rounded-xl"> <div className="p-2.5 bg-primary-500/20 rounded-xl">
<Clock className="w-6 h-6 text-primary-400" /> <Clock className="w-6 h-6 text-primary-400" />
</div> </div>
<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> <p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Daily Working Hours Summary</p>
</div> </div>
</div> </div>
@@ -375,7 +375,7 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
</div> </div>
{/* 하단 범례 (Legend) */} {/* 하단 범례 (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> <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-6 text-[11px] font-bold whitespace-nowrap">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -87,11 +87,11 @@ export function JobTypeSelectModal({
// 검색 필터 적용 // 검색 필터 적용
const filteredTypes = searchKey const filteredTypes = searchKey
? jobTypes.filter( ? jobTypes.filter(
(item) => (item) =>
item.type?.toLowerCase().includes(searchKey.toLowerCase()) || item.type?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) || item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) ||
item.process?.toLowerCase().includes(searchKey.toLowerCase()) item.process?.toLowerCase().includes(searchKey.toLowerCase())
) )
: jobTypes; : jobTypes;
// 그룹핑 // 그룹핑
@@ -200,12 +200,12 @@ export function JobTypeSelectModal({
> >
<div className="flex items-center justify-center min-h-screen p-4"> <div className="flex items-center justify-center min-h-screen p-4">
<div <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()} onMouseDown={(e) => e.stopPropagation()}
> >
{/* 헤더 */} {/* 헤더 */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between"> <div className="dialog-header px-6 py-4 flex items-center justify-between">
<h2 className="text-xl font-semibold text-white"> </h2> <h2 className="dialog-title"> </h2>
<button <button
onClick={onClose} onClick={onClose}
className="text-white/70 hover:text-white transition-colors" className="text-white/70 hover:text-white transition-colors"
@@ -288,11 +288,10 @@ export function JobTypeSelectModal({
return ( return (
<button <button
key={typePath} key={typePath}
className={`w-full px-8 py-1.5 text-left transition-colors ${ className={`w-full px-8 py-1.5 text-left transition-colors ${isSelected
isSelected
? 'bg-primary-500/40 text-primary-200' ? 'bg-primary-500/40 text-primary-200'
: 'text-white/70 hover:bg-white/10 hover:text-white' : 'text-white/70 hover:bg-white/10 hover:text-white'
}`} }`}
onClick={() => setSelectedPath(typePath)} onClick={() => setSelectedPath(typePath)}
onDoubleClick={() => typeNode.item && handleDoubleClick(typeNode.item)} 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 <button
onClick={onClose} onClick={onClose}
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors" 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); const lastReport = await comms.getLastJobReportByProject(formData.pidx, formData.projectName);
if (lastReport.Success && lastReport.Data) { if (lastReport.Success && lastReport.Data) {
const updatedFormData = { ...formData }; const updatedFormData = { ...formData };
if (lastReport.Data.requestpart) { if (lastReport.Data.requestpart) {
updatedFormData.requestpart = 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="flex items-center justify-center min-h-screen p-4">
<div <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()} 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 ${ <div className={`dialog-header px-6 py-4 flex items-center justify-between sticky top-0 z-10 ${editingItem ? '' : ''
editingItem ? 'bg-slate-800/95' : 'bg-primary-600/30' }`}>
}`}> <h2 className="dialog-title flex items-center">
<h2 className="text-xl font-semibold text-white flex items-center">
<FileText className="w-5 h-5 mr-2" /> <FileText className="w-5 h-5 mr-2" />
{editingItem ? '업무일지 수정' : '업무일지 등록'} {editingItem ? '업무일지 수정' : '업무일지 등록'}
</h2> </h2>
@@ -517,7 +516,7 @@ export function JobreportEditModal({
</div> </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> <div>
{editingItem && ( {editingItem && (

View File

@@ -52,10 +52,10 @@ export function JobreportTypeModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"> <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"> <div className="dialog-header px-6 py-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white"> </h3> <h3 className="dialog-title"> </h3>
<button <button
onClick={onClose} onClick={onClose}
className="text-white/50 hover:text-white transition-colors" className="text-white/50 hover:text-white transition-colors"
@@ -128,7 +128,7 @@ export function JobreportTypeModal({
</div> </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 <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors" 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} onMouseDown={onClose}
> >
<div <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()} 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"> <div className="flex items-center gap-2">
<Folder className="w-5 h-5 text-primary-400" /> <Folder className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">/ </h2> <h2 className="dialog-title">/ </h2>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -151,11 +151,10 @@ export function ProjectSearchDialog({
onSelect({ idx: project.idx, name: project.name }); onSelect({ idx: project.idx, name: project.name });
onClose(); onClose();
}} }}
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${ 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
selectedProject?.idx === project.idx && selectedProject?.name === project.name
? 'bg-primary-500/30 border border-primary-400/50' ? 'bg-primary-500/30 border border-primary-400/50'
: 'bg-white/5 hover:bg-white/10 border border-transparent' : 'bg-white/5 hover:bg-white/10 border border-transparent'
}`} }`}
> >
{/* 아이콘 */} {/* 아이콘 */}
<div className={`shrink-0 ${project.source === 'project' ? 'text-blue-400' : 'text-gray-400'}`}> <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> <span className="font-medium text-white truncate">{project.name}</span>
{project.status && ( {project.status && (
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span className={`text-xs px-1.5 py-0.5 rounded ${project.status === '진행' ? 'bg-green-500/20 text-green-400' :
project.status === '진행' ? 'bg-green-500/20 text-green-400' : project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' :
project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-gray-500/20 text-gray-400'
'bg-gray-500/20 text-gray-400' }`}>
}`}>
{project.status} {project.status}
</span> </span>
)} )}
@@ -209,7 +207,7 @@ export function ProjectSearchDialog({
</div> </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"> <span className="text-sm text-white/50">
{projects.length} {projects.length}
{selectedProject && ` | 선택: ${selectedProject.name}`} {selectedProject && ` | 선택: ${selectedProject.name}`}

View File

@@ -116,10 +116,10 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
return ( 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="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"> <div className="dialog-header px-6 py-4 flex justify-between items-center">
<h2 className="text-xl font-bold text-white flex items-center"> <h2 className="dialog-title flex items-center">
<Calendar className="w-5 h-5 mr-2 text-primary-400" /> <Calendar className="w-5 h-5 mr-2 text-primary-400" />
{title} {title}
</h2> </h2>
@@ -262,7 +262,7 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
</div> </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 <button
type="button" type="button"
onClick={onClose} onClick={onClose}

View File

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

View File

@@ -88,15 +88,15 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
return ( 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="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 */} {/* 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="flex items-center gap-4">
<div className="p-2.5 bg-primary-500/20 rounded-xl"> <div className="p-2.5 bg-primary-500/20 rounded-xl">
<ShieldCheck className="w-6 h-6 text-primary-400" /> <ShieldCheck className="w-6 h-6 text-primary-400" />
</div> </div>
<div> <div>
<h2 className="text-xl font-bold text-white leading-tight"> <h2 className="dialog-title">
{formData.idx ? '라이선스 수정' : '라이선스 추가'} {formData.idx ? '라이선스 수정' : '라이선스 추가'}
</h2> </h2>
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Edit License Details</p> <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> </div>
{/* Footer */} {/* 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> <div>
{formData.idx && onDelete && ( {formData.idx && onDelete && (
<button <button

View File

@@ -132,12 +132,12 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
return ( return (
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"> <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 */} {/* 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"> <div className="flex items-center gap-3">
<Mail className="w-5 h-5 text-primary-400" /> <Mail className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white"> </h2> <h2 className="dialog-title"> </h2>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -238,7 +238,7 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
</div> </div>
{/* Footer */} {/* 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 <button
onClick={onClose} onClick={onClose}
disabled={processing} disabled={processing}

View File

@@ -157,11 +157,11 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
return ( return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"> <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> <div>
<h2 className="text-lg font-bold text-white"></h2> <h2 className="dialog-title"></h2>
<p className="text-sm text-white/60">{projectName}</p> <p className="text-sm text-white/60">{projectName}</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -225,9 +225,8 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
return ( return (
<tr <tr
key={part.idx} key={part.idx}
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${ className={`border-b border-white/5 hover:bg-white/5 transition-colors ${isEditing ? 'bg-primary-500/10' : ''
isEditing ? 'bg-primary-500/10' : '' }`}
}`}
> >
<td className="px-2 py-2"> <td className="px-2 py-2">
{isEditing ? ( {isEditing ? (
@@ -500,7 +499,7 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
{/* 합계 */} {/* 합계 */}
{parts.length > 0 && ( {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"> <div className="flex justify-end gap-4 text-sm">
<span className="text-white/70"> <span className="text-white/70">
<span className="text-white font-medium">{parts.length}</span> <span className="text-white font-medium">{parts.length}</span>

View File

@@ -108,9 +108,9 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <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"> <div className="flex items-center gap-4">
<select <select
value={formData.status} value={formData.status}
@@ -473,7 +473,7 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
</div> </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> <span className="text-sm text-white/50">PNO: {project.pno || '-'} | Jasmin: {project.jasmin || '-'} | : {formData.progress}%</span>
<button <button
onClick={onClose} onClick={onClose}

View File

@@ -93,10 +93,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
return createPortal( return createPortal(
<div className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm"> <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 */} {/* Header */}
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5"> <div className="dialog-header px-6 py-4 flex items-center justify-between">
<h2 className="text-xl font-bold text-white flex items-center"> <h2 className="dialog-title flex items-center">
<SettingsIcon className="w-5 h-5 mr-2 text-primary-400" /> <SettingsIcon className="w-5 h-5 mr-2 text-primary-400" />
</h2> </h2>
@@ -263,7 +263,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
</div> </div>
{/* Footer */} {/* 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 <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/5 transition-colors" 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 ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"> <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="flex items-center gap-3">
<div className="p-2 bg-primary-500/20 rounded-lg"> <div className="p-2 bg-primary-500/20 rounded-lg">
<Users className="w-5 h-5 text-primary-400" /> <Users className="w-5 h-5 text-primary-400" />
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-white"></h2> <h2 className="dialog-title"></h2>
<p className="text-white/50 text-sm">/ </p> <p className="text-white/50 text-sm">/ </p>
</div> </div>
</div> </div>
@@ -350,7 +350,7 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
</div> </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> <span className="text-sm text-white/50">{filteredItems.length} </span>
<button <button
onClick={onClose} onClick={onClose}
@@ -364,9 +364,9 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
{/* 그룹 편집 모달 */} {/* 그룹 편집 모달 */}
{showModal && ( {showModal && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"> <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="dialog-container rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
<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">
<h2 className="text-xl font-bold text-white"> <h2 className="dialog-title">
{editingItem ? '그룹 수정' : '새 그룹'} {editingItem ? '그룹 수정' : '새 그룹'}
</h2> </h2>
<button <button
@@ -472,10 +472,10 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
{/* 권한 설정 모달 */} {/* 권한 설정 모달 */}
{showPermissionModal && editingItem && ( {showPermissionModal && editingItem && (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"> <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="dialog-container rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
<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> <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> <p className="text-white/60 text-sm">{editingItem.dept}</p>
</div> </div>
<button <button

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom'; 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 { clsx } from 'clsx';
import { comms } from '@/communication'; import { comms } from '@/communication';
import { UserInfoDetail } from '@/types'; import { UserInfoDetail } from '@/types';
@@ -109,276 +109,350 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
return createPortal( return createPortal(
<> <>
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10000]"> <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 rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden transition-all duration-300"> <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 */} {/* Header */}
<div className="dialog-header flex items-center justify-between px-6 py-4"> <div className="dialog-header px-8 py-6 border-b border-white/10 bg-white/5 flex items-center justify-between shrink-0">
<h2 className="dialog-title"> <div className="flex items-center gap-4">
<User className="w-6 h-6" /> <div className="p-3 bg-primary-500/20 rounded-xl">
<User className="w-6 h-6 text-primary-400" />
</h2> </div>
<div>
<h2 className="dialog-title text-2xl"> </h2>
<p className="text-white/40 text-xs mt-1"> .</p>
</div>
</div>
<button <button
onClick={onClose} 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" /> <X className="w-6 h-6" />
</button> </button>
</div> </div>
{/* Content */} {/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)] custom-scrollbar"> <div className="flex-1 overflow-hidden">
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="h-full flex flex-col items-center justify-center gap-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div> <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>
) : ( ) : (
<div className="flex flex-col h-full gap-6"> <div className="flex flex-col lg:flex-row h-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 min-h-0"> {/* 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-4 overflow-y-auto pr-2 custom-scrollbar"> <div className="space-y-8">
<div className="grid grid-cols-2 gap-4"> {/* Identity Section */}
<div> <section className="space-y-4">
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1"> <div className="flex items-center gap-2 mb-2">
<User className="w-4 h-4" /> <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> </label>
<input
type="text" <label className={clsx(
value={formData.Id} "flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
disabled formData.UseUserState ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50" )}>
/> <div className={clsx(
</div> "w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
<div> formData.UseUserState ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
<label className="block text-sm text-white/70 mb-1"></label> )}>
<input <User className="w-3 h-3" />
type="text" </div>
value={formData.NameK} <input
onChange={(e) => handleInputChange('NameK', e.target.value)} type="checkbox"
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40" checked={formData.UseUserState}
placeholder="이름" onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
/> className="hidden"
</div> />
<div> <span className={clsx("text-xs font-bold", formData.UseUserState ? "text-primary-300" : "text-white/50")}> </span>
<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> </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> <label className={clsx(
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
<div> formData.ExceptHoly ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1"></label> )}>
<input <div className={clsx(
type="text" "w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
value={formData.Process} formData.ExceptHoly ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
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" <Calendar className="w-3 h-3" />
placeholder="공정" </div>
/> <input
</div> type="checkbox"
checked={formData.ExceptHoly}
{/* 이메일 */} onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
<div> className="hidden"
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1"> />
<Mail className="w-4 h-4" /> <span className={clsx("text-xs font-bold", formData.ExceptHoly ? "text-primary-300" : "text-white/50")}> </span>
</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> </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>
<div> </section>
<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>
</div> </div>
</div> </div>
{/* 메시지 */} {/* Right Panel: Preferences & Memo */}
{message && ( <div className="w-full lg:w-96 flex flex-col h-full bg-[#131426]/50">
<div <div className="p-6 md:p-8 space-y-8 flex-1 overflow-y-auto custom-scrollbar">
className={clsx( {/* Theme Section */}
'px-4 py-2 rounded-lg text-sm', <section className="space-y-4">
message.type === 'success' ? 'bg-green-600/20 text-green-400' : 'bg-red-600/20 text-red-400' <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>
{message.text} </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> </div>
)} )}
</div> </div>
{/* Footer */} {/* Footer */}
<div className="dialog-footer flex items-center justify-end px-6 py-4"> <div className="dialog-footer px-8 py-5 border-t border-white/10 bg-[#131426] shrink-0 flex items-center justify-between">
<div className="flex gap-2"> <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 <button
onClick={onClose} 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>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className={clsx( 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"
'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'
)}
> >
<Save className="w-4 h-4" /> {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 ? '저장 중...' : '저장'} {saving ? '저장 중...' : '저장 완료'}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div > </div>
</>, </>,
document.body document.body
); );

View File

@@ -98,18 +98,18 @@ export function UserSearchDialog({
return ( return (
<div <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} onClick={onClose}
> >
<div <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()} 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"> <div className="flex items-center gap-2">
<Users className="w-5 h-5 text-primary-400" /> <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> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -156,11 +156,10 @@ export function UserSearchDialog({
onSelect(user); onSelect(user);
onClose(); onClose();
}} }}
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${ className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedUser?.id === user.id
selectedUser?.id === user.id
? 'bg-primary-500/30 border border-primary-400/50' ? 'bg-primary-500/30 border border-primary-400/50'
: 'bg-white/5 hover:bg-white/10 border border-transparent' : 'bg-white/5 hover:bg-white/10 border border-transparent'
}`} }`}
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -186,7 +185,7 @@ export function UserSearchDialog({
</div> </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"> <span className="text-sm text-white/50">
{filteredUsers.length} {selectedUser && `| 선택: ${selectedUser.id} (${selectedUser.name})`} {filteredUsers.length} {selectedUser && `| 선택: ${selectedUser.id} (${selectedUser.name})`}
</span> </span>

View File

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

View File

@@ -6,9 +6,8 @@ import { ItemInfo, ItemDetail, SupplierStaff, PurchaseHistoryItem } from '@/type
import { ItemEditDialog } from '@/components/items'; import { ItemEditDialog } from '@/components/items';
export function ItemsPage() { export function ItemsPage() {
const [categories, setCategories] = useState<string[]>([]);
const [items, setItems] = useState<ItemInfo[]>([]); const [items, setItems] = useState<ItemInfo[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('all'); const [selectedCategory] = useState<string>('all');
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState(''); const [filter, setFilter] = useState('');
@@ -24,20 +23,9 @@ export function ItemsPage() {
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => { 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 () => { const loadItems = async () => {
if (!searchKey.trim()) { if (!searchKey.trim()) {
alert('검색어를 입력하세요'); alert('검색어를 입력하세요');

View File

@@ -802,9 +802,9 @@ export function Jobreport() {
{/* 업무일지 미등록 상세 모달 */} {/* 업무일지 미등록 상세 모달 */}
{showUnregisteredModal && ( {showUnregisteredModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"> <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="dialog-container border border-white/10 rounded-2xl w-full max-w-md overflow-hidden animate-scale-in transition-all duration-300">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5"> <div className="dialog-header 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"> <h3 className="dialog-title flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-danger-400" /> <AlertTriangle className="w-5 h-5 text-danger-400" />
</h3> </h3>
@@ -847,7 +847,7 @@ export function Jobreport() {
)} )}
</div> </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 <button
onClick={() => setShowUnregisteredModal(false)} 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" 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 ( 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="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="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" />} {isEdit ? <Edit3 className="w-5 h-5 text-primary-400" /> : <Plus className="w-5 h-5 text-primary-400" />}
</div> </div>
<h2 className="text-xl font-bold text-white tracking-tight">{title}</h2> <h2 className="dialog-title">{title}</h2>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isEdit && onComplete && currentStatus !== '5' && ( {isEdit && onComplete && currentStatus !== '5' && (
@@ -709,7 +709,7 @@ function TodoModal({
</div> </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> <div>
{isEdit && onDelete && ( {isEdit && onDelete && (
<button <button