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:
@@ -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" />
|
||||||
|
|||||||
@@ -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") { }
|
||||||
|
|||||||
58
Project/Web/MachineBridge/MachineBridge.Settings.cs
Normal file
58
Project/Web/MachineBridge/MachineBridge.Settings.cs
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||||
if (result.MessageType == WebSocketMessageType.Close)
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
|
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (result.MessageType == WebSocketMessageType.Text)
|
messageBytes.AddRange(new ArraySegment<byte>(buffer, 0, result.Count));
|
||||||
|
}
|
||||||
|
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":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<h2 className="dialog-title">
|
||||||
{isNew ? '품목 추가' : '품목 편집'}
|
{isNew ? '품목 추가' : '품목 편집'}
|
||||||
</h2>
|
</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,7 +377,6 @@ 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>
|
||||||
@@ -344,6 +388,7 @@ 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>
|
||||||
@@ -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,8 +415,7 @@ 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'
|
||||||
}`}
|
}`}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,8 +288,7 @@ 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'
|
||||||
}`}
|
}`}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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="text-xl font-semibold text-white flex items-center">
|
<h2 className="dialog-title 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 && (
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,8 +151,7 @@ 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'
|
||||||
}`}
|
}`}
|
||||||
@@ -174,8 +173,7 @@ 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'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,8 +355,7 @@ 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)}
|
||||||
>
|
>
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,8 +225,7 @@ 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">
|
||||||
@@ -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>개 항목
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</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>
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-white/70 mb-1">이름</label>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<input
|
<div className="space-y-1.5">
|
||||||
type="text"
|
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">사원번호</label>
|
||||||
value={formData.NameK}
|
<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">
|
||||||
onChange={(e) => handleInputChange('NameK', e.target.value)}
|
<span className="w-2 h-2 rounded-full bg-white/20"></span>
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
{formData.Id}
|
||||||
placeholder="이름"
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
||||||
<div>
|
<div className="space-y-1.5">
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">직책</label>
|
||||||
<Briefcase className="w-4 h-4" />
|
<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" />
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.Grade}
|
value={formData.Grade}
|
||||||
onChange={(e) => handleInputChange('Grade', e.target.value)}
|
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"
|
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="직책"
|
placeholder="직책 입력"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
<div>
|
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">이름 (한글)</label>
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">공정</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.Process}
|
value={formData.NameK}
|
||||||
onChange={(e) => handleInputChange('Process', e.target.value)}
|
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"
|
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="공정"
|
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.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 */}
|
||||||
<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">
|
||||||
<Mail className="w-4 h-4" />
|
<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>
|
||||||
</label>
|
</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
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.Email}
|
value={formData.Email}
|
||||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
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"
|
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@example.com"
|
placeholder="email@company.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* 입/퇴사 정보 */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-1.5">
|
||||||
<div>
|
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">전화번호 (내선)</label>
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
|
||||||
<Calendar className="w-4 h-4" />
|
|
||||||
입사일
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
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}
|
value={formData.DateIn}
|
||||||
onChange={(e) => handleInputChange('DateIn', e.target.value)}
|
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"
|
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"
|
||||||
placeholder="YYYY-MM-DD"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
<div className="space-y-1.5">
|
||||||
<Calendar className="w-4 h-4" />
|
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">퇴사일</label>
|
||||||
퇴사일
|
<div className="relative">
|
||||||
</label>
|
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="date"
|
||||||
value={formData.DateO}
|
value={formData.DateO}
|
||||||
onChange={(e) => handleInputChange('DateO', e.target.value)}
|
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"
|
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"
|
||||||
placeholder="YYYY-MM-DD"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 옵션 체크박스 */}
|
<div className="pt-2 flex flex-wrap gap-4">
|
||||||
<div className="flex flex-wrap gap-6 pt-2">
|
<label className={clsx(
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
"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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.UseJobReport}
|
checked={formData.UseJobReport}
|
||||||
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
|
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
|
||||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<span className="text-white/70">업무일지 사용</span>
|
<span className={clsx("text-xs font-bold", formData.UseJobReport ? "text-primary-300" : "text-white/50")}>업무일지 사용</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.UseUserState}
|
checked={formData.UseUserState}
|
||||||
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
|
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
|
||||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<span className="text-white/70">계정 사용</span>
|
<span className={clsx("text-xs font-bold", formData.UseUserState ? "text-primary-300" : "text-white/50")}>계정 사용</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
|
||||||
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={formData.ExceptHoly}
|
checked={formData.ExceptHoly}
|
||||||
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
|
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
|
||||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<span className="text-white/70">휴가 제외</span>
|
<span className={clsx("text-xs font-bold", formData.ExceptHoly ? "text-primary-300" : "text-white/50")}>휴가 제외</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 우측 컬럼: 테마 설정 + 비고 */}
|
{/* Right Panel: Preferences & Memo */}
|
||||||
<div className="flex flex-col h-full gap-4">
|
<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">
|
||||||
<div className="bg-white/5 rounded-lg p-4">
|
{/* Theme Section */}
|
||||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
<section className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Palette className="w-4 h-4 text-purple-400" />
|
<Palette className="w-4 h-4 text-purple-400" />
|
||||||
테마 설정
|
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">테마 설정</h3>
|
||||||
</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>
|
||||||
|
|
||||||
{/* 비고: 남은 높이 채우기 */}
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div className="flex-1 flex flex-col">
|
{[
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
{ id: 'dark', name: 'Standard Dark', desc: '기본 어두운 테마', gradient: 'from-blue-600 to-indigo-900', border: 'border-blue-500' },
|
||||||
<FileText className="w-4 h-4" />
|
{ 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' },
|
||||||
</label>
|
].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
|
<textarea
|
||||||
value={formData.Memo}
|
value={formData.Memo}
|
||||||
onChange={(e) => handleInputChange('Memo', e.target.value)}
|
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"
|
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="비고"
|
placeholder="사용자에 대한 추가 정보를 입력하세요..."
|
||||||
/>
|
/>
|
||||||
|
<div className="absolute bottom-4 right-4 text-[10px] text-white/20 font-mono">
|
||||||
|
MEMO AREA
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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}
|
|
||||||
</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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,8 +156,7 @@ 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'
|
||||||
}`}
|
}`}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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('검색어를 입력하세요');
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user