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:
@@ -126,14 +126,14 @@ export function CustomEditDialog({ isOpen, onClose, onSaved, item }: CustomEditD
|
||||
/>
|
||||
|
||||
{/* 다이얼로그 콘텐트 */}
|
||||
<div className="relative w-full max-w-2xl bg-[#1a1c1e] border border-white/10 rounded-3xl shadow-2xl overflow-hidden animate-scale-in">
|
||||
<div className="px-6 py-5 border-b border-white/10 flex items-center justify-between bg-white/[0.02]">
|
||||
<div className="dialog-container relative w-full max-w-2xl border border-white/10 rounded-3xl overflow-hidden animate-scale-in transition-all duration-300">
|
||||
<div className="dialog-header px-6 py-5 border-b border-white/10 flex items-center justify-between bg-white/[0.02]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary-500/20 rounded-xl text-primary-400">
|
||||
<Building className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white tracking-tight">
|
||||
<h3 className="dialog-title tracking-tight">
|
||||
{item ? '업체 정보 수정' : '새 업체 등록'}
|
||||
</h3>
|
||||
<p className="text-white/30 text-[10px] uppercase font-bold tracking-widest mt-0.5">
|
||||
@@ -329,7 +329,7 @@ export function CustomEditDialog({ isOpen, onClose, onSaved, item }: CustomEditD
|
||||
</div>
|
||||
|
||||
{/* 푸터 버튼 */}
|
||||
<div className="px-6 py-5 bg-white/[0.02] border-t border-white/10 flex items-center justify-between">
|
||||
<div className="dialog-footer px-6 py-5 bg-white/[0.02] border-t border-white/10 flex items-center justify-between">
|
||||
<div>
|
||||
{item && (
|
||||
<button
|
||||
|
||||
@@ -140,12 +140,12 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative w-full max-w-4xl mx-4 glass-effect-solid rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
|
||||
<div className="dialog-container relative w-full max-w-4xl mx-4 rounded-2xl overflow-hidden animate-fade-in transition-all duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Star className="w-5 h-5 text-yellow-400" />
|
||||
<h2 className="text-lg font-semibold text-white">즐겨찾기</h2>
|
||||
<h2 className="dialog-title">즐겨찾기</h2>
|
||||
<span className="text-white/50 text-sm">({favorites.length}개)</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -190,7 +190,7 @@ export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-3 border-t border-white/10 bg-black/20">
|
||||
<div className="dialog-footer px-6 py-3 border-t border-white/10 bg-black/20">
|
||||
<p className="text-xs text-white/40 text-center">
|
||||
클릭하면 새 탭에서 열립니다
|
||||
</p>
|
||||
|
||||
@@ -356,10 +356,10 @@ export function HolidayRequestDialog({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-fade-in">
|
||||
<div className="bg-paper rounded-2xl shadow-[0_0_40px_rgba(var(--color-primary),0.4)] w-full max-w-4xl max-h-[90vh] overflow-y-auto border-2 border-primary">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-y-auto border-2 border-primary transition-all duration-300">
|
||||
{/* Header - Lively Gradient */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-primary-500 via-primary-400 to-primary-600">
|
||||
<h2 className="text-xl font-bold text-white flex items-center drop-shadow-md">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 bg-gradient-to-r from-primary-500 via-primary-400 to-primary-600">
|
||||
<h2 className="dialog-title text-white flex items-center drop-shadow-md">
|
||||
<Calendar className="w-6 h-6 mr-2 text-white animate-pulse" />
|
||||
{title}
|
||||
</h2>
|
||||
@@ -711,7 +711,7 @@ export function HolidayRequestDialog({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-primary-500/30 bg-primary-500/10">
|
||||
<div className="dialog-footer flex items-center justify-end gap-3 px-6 py-4 border-t border-primary-500/30 bg-primary-500/10">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-primary-200 hover:text-white hover:bg-primary-500/20 rounded-lg transition-colors font-medium"
|
||||
|
||||
@@ -61,13 +61,42 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 이미지를 Base64로 변환
|
||||
const convertToBase64 = (file: File): Promise<string> => {
|
||||
// 이미지를 압축하여 Base64로 변환
|
||||
const compressImage = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (event) => {
|
||||
const img = new Image();
|
||||
img.src = event.target?.result as string;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const MAX_WIDTH = 640;
|
||||
const MAX_HEIGHT = 480;
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
// 너비가 최대값보다 크면 리사이즈
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
|
||||
// 높이가 최대값보다 크면 리사이즈 (너비 리사이즈 후에도 높이가 클 수 있음)
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.drawImage(img, 0, 0, width, height);
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.8)); // Quality 0.8
|
||||
};
|
||||
img.onerror = (error) => reject(error);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,7 +108,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await convertToBase64(file);
|
||||
const base64 = await compressImage(file);
|
||||
setImageData(base64);
|
||||
|
||||
// 기존 품목인 경우 바로 저장
|
||||
@@ -209,14 +238,30 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
|
||||
{/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */}
|
||||
<div
|
||||
className="relative bg-slate-800 rounded-xl shadow-2xl w-full max-w-4xl mx-4 border border-white/10"
|
||||
className="dialog-container relative rounded-xl w-full max-w-4xl mx-4 transition-all duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
{isNew ? '품목 추가' : '품목 편집'}
|
||||
</h2>
|
||||
<div className="dialog-header flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="dialog-title">
|
||||
{isNew ? '품목 추가' : '품목 편집'}
|
||||
</h2>
|
||||
<label className="flex items-center gap-2 cursor-pointer group select-none">
|
||||
<div className={`w-10 h-5 rounded-full relative transition-colors ${editData.disable ? 'bg-red-500' : 'bg-white/20'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editData.disable}
|
||||
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`absolute left-1 top-1 w-3 h-3 bg-white rounded-full transition-transform ${editData.disable ? 'translate-x-[20px]' : ''}`} />
|
||||
</div>
|
||||
<span className={`text-sm transition-colors ${editData.disable ? 'text-red-400 font-medium' : 'text-white/50 group-hover:text-white/70'}`}>
|
||||
{editData.disable ? '비활성화됨' : '비활성화'}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-white/10 rounded text-white/70 hover:text-white transition-colors"
|
||||
@@ -237,7 +282,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
type="text"
|
||||
value={editData.sid}
|
||||
onChange={(e) => setEditData({ ...editData, sid: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
className={`w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg ${editData.disable ? 'text-red-400 font-bold' : 'text-white'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -310,7 +355,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* 공급처 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">공급처</label>
|
||||
@@ -332,17 +377,17 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 보관장소 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">보관장소</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.storage}
|
||||
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
{/* 보관장소 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">보관장소</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.storage}
|
||||
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메모 */}
|
||||
@@ -356,17 +401,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 비활성화 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable"
|
||||
checked={editData.disable}
|
||||
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10"
|
||||
/>
|
||||
<label htmlFor="disable" className="text-sm text-white/70">비활성화</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 이미지 영역 */}
|
||||
@@ -380,11 +415,10 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${
|
||||
isDragging
|
||||
? 'border-blue-400 bg-blue-500/20'
|
||||
: 'border-white/20 bg-white/5 hover:border-white/40'
|
||||
}`}
|
||||
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${isDragging
|
||||
? 'border-blue-400 bg-blue-500/20'
|
||||
: 'border-white/20 bg-white/5 hover:border-white/40'
|
||||
}`}
|
||||
>
|
||||
{imageLoading ? (
|
||||
<div className="text-center">
|
||||
@@ -468,7 +502,7 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-white/10">
|
||||
<div className="dialog-footer flex items-center justify-between p-4">
|
||||
<div>
|
||||
{!isNew && (
|
||||
<button
|
||||
|
||||
@@ -225,18 +225,18 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
|
||||
<div
|
||||
className="glass-effect rounded-3xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col shadow-2xl border border-white/10 animate-scale-in"
|
||||
className="dialog-container rounded-3xl w-full max-w-7xl max-h-[95vh] overflow-hidden flex flex-col transition-all duration-300 animate-scale-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
|
||||
<div className="dialog-header px-8 py-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2.5 bg-primary-500/20 rounded-xl">
|
||||
<Clock className="w-6 h-6 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white leading-tight">일별 근무시간 집계</h2>
|
||||
<h2 className="dialog-title text-xl">일별 근무시간 집계</h2>
|
||||
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Daily Working Hours Summary</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
|
||||
</div>
|
||||
|
||||
{/* 하단 범례 (Legend) */}
|
||||
<div className="px-8 py-4 border-t border-white/10 bg-white/5 flex items-center gap-6 overflow-x-auto custom-scrollbar no-scrollbar">
|
||||
<div className="dialog-footer px-8 py-4 flex items-center gap-6 overflow-x-auto custom-scrollbar no-scrollbar">
|
||||
<span className="text-[10px] font-black text-white/30 uppercase tracking-[0.2em] shrink-0 border-r border-white/10 pr-6 mr-2">Legend</span>
|
||||
<div className="flex items-center gap-6 text-[11px] font-bold whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -87,11 +87,11 @@ export function JobTypeSelectModal({
|
||||
// 검색 필터 적용
|
||||
const filteredTypes = searchKey
|
||||
? jobTypes.filter(
|
||||
(item) =>
|
||||
item.type?.toLowerCase().includes(searchKey.toLowerCase()) ||
|
||||
item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) ||
|
||||
item.process?.toLowerCase().includes(searchKey.toLowerCase())
|
||||
)
|
||||
(item) =>
|
||||
item.type?.toLowerCase().includes(searchKey.toLowerCase()) ||
|
||||
item.jobgrp?.toLowerCase().includes(searchKey.toLowerCase()) ||
|
||||
item.process?.toLowerCase().includes(searchKey.toLowerCase())
|
||||
)
|
||||
: jobTypes;
|
||||
|
||||
// 그룹핑
|
||||
@@ -200,12 +200,12 @@ export function JobTypeSelectModal({
|
||||
>
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div
|
||||
className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up max-h-[85vh] flex flex-col"
|
||||
className="dialog-container rounded-2xl w-full max-w-2xl animate-slide-up max-h-[85vh] flex flex-col transition-all duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-white">업무형태 선택</h2>
|
||||
<div className="dialog-header px-6 py-4 flex items-center justify-between">
|
||||
<h2 className="dialog-title">업무형태 선택</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
@@ -288,11 +288,10 @@ export function JobTypeSelectModal({
|
||||
return (
|
||||
<button
|
||||
key={typePath}
|
||||
className={`w-full px-8 py-1.5 text-left transition-colors ${
|
||||
isSelected
|
||||
className={`w-full px-8 py-1.5 text-left transition-colors ${isSelected
|
||||
? 'bg-primary-500/40 text-primary-200'
|
||||
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
}`}
|
||||
onClick={() => setSelectedPath(typePath)}
|
||||
onDoubleClick={() => typeNode.item && handleDoubleClick(typeNode.item)}
|
||||
>
|
||||
@@ -335,7 +334,7 @@ export function JobTypeSelectModal({
|
||||
)}
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-end space-x-3">
|
||||
<div className="dialog-footer px-6 py-4 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
|
||||
@@ -137,7 +137,7 @@ export function JobreportEditModal({
|
||||
const lastReport = await comms.getLastJobReportByProject(formData.pidx, formData.projectName);
|
||||
if (lastReport.Success && lastReport.Data) {
|
||||
const updatedFormData = { ...formData };
|
||||
|
||||
|
||||
if (lastReport.Data.requestpart) {
|
||||
updatedFormData.requestpart = lastReport.Data.requestpart;
|
||||
}
|
||||
@@ -261,14 +261,13 @@ export function JobreportEditModal({
|
||||
>
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div
|
||||
className="glass-effect rounded-2xl w-full max-w-3xl animate-slide-up max-h-[90vh] overflow-y-auto"
|
||||
className="dialog-container rounded-2xl w-full max-w-3xl animate-slide-up max-h-[90vh] overflow-y-auto transition-all duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className={`px-6 py-4 border-b border-white/10 flex items-center justify-between sticky top-0 backdrop-blur z-10 ${
|
||||
editingItem ? 'bg-slate-800/95' : 'bg-primary-600/30'
|
||||
}`}>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<div className={`dialog-header px-6 py-4 flex items-center justify-between sticky top-0 z-10 ${editingItem ? '' : ''
|
||||
}`}>
|
||||
<h2 className="dialog-title flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
{editingItem ? '업무일지 수정' : '업무일지 등록'}
|
||||
</h2>
|
||||
@@ -517,7 +516,7 @@ export function JobreportEditModal({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-between sticky bottom-0 bg-slate-800/95 backdrop-blur">
|
||||
<div className="dialog-footer px-6 py-4 flex justify-between sticky bottom-0 backdrop-blur">
|
||||
{/* 좌측: 삭제 버튼 (편집 모드일 때만) */}
|
||||
<div>
|
||||
{editingItem && (
|
||||
|
||||
@@ -52,10 +52,10 @@ export function JobreportTypeModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in">
|
||||
<div className="bg-gray-900 border border-white/10 rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-2xl overflow-hidden transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
|
||||
<h3 className="text-lg font-semibold text-white">업무형태별 집계</h3>
|
||||
<div className="dialog-header px-6 py-4 flex items-center justify-between">
|
||||
<h3 className="dialog-title">업무형태별 집계</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
@@ -128,7 +128,7 @@ export function JobreportTypeModal({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-end bg-white/5">
|
||||
<div className="dialog-footer px-6 py-4 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors"
|
||||
|
||||
@@ -97,14 +97,14 @@ export function ProjectSearchDialog({
|
||||
onMouseDown={onClose}
|
||||
>
|
||||
<div
|
||||
className="glass-effect rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col"
|
||||
className="dialog-container rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col transition-all duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="p-4 border-b border-white/10 flex items-center justify-between shrink-0">
|
||||
<div className="dialog-header p-4 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">프로젝트/항목 검색</h2>
|
||||
<h2 className="dialog-title">프로젝트/항목 검색</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -151,11 +151,10 @@ export function ProjectSearchDialog({
|
||||
onSelect({ idx: project.idx, name: project.name });
|
||||
onClose();
|
||||
}}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
|
||||
selectedProject?.idx === project.idx && selectedProject?.name === project.name
|
||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedProject?.idx === project.idx && selectedProject?.name === project.name
|
||||
? 'bg-primary-500/30 border border-primary-400/50'
|
||||
: 'bg-white/5 hover:bg-white/10 border border-transparent'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{/* 아이콘 */}
|
||||
<div className={`shrink-0 ${project.source === 'project' ? 'text-blue-400' : 'text-gray-400'}`}>
|
||||
@@ -174,11 +173,10 @@ export function ProjectSearchDialog({
|
||||
)}
|
||||
<span className="font-medium text-white truncate">{project.name}</span>
|
||||
{project.status && (
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
project.status === '진행' ? 'bg-green-500/20 text-green-400' :
|
||||
project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${project.status === '진행' ? 'bg-green-500/20 text-green-400' :
|
||||
project.status === '준비' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
)}
|
||||
@@ -209,7 +207,7 @@ export function ProjectSearchDialog({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="p-4 border-t border-white/10 flex items-center justify-between shrink-0">
|
||||
<div className="dialog-footer p-4 flex items-center justify-between shrink-0">
|
||||
<span className="text-sm text-white/50">
|
||||
{projects.length}건
|
||||
{selectedProject && ` | 선택: ${selectedProject.name}`}
|
||||
|
||||
@@ -116,10 +116,10 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in">
|
||||
<div className="bg-bg-paper rounded-2xl shadow-2xl w-full max-w-lg border border-white/10 overflow-hidden">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-lg overflow-hidden transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex justify-between items-center bg-white/5">
|
||||
<h2 className="text-xl font-bold text-white flex items-center">
|
||||
<div className="dialog-header px-6 py-4 flex justify-between items-center">
|
||||
<h2 className="dialog-title flex items-center">
|
||||
<Calendar className="w-5 h-5 mr-2 text-primary-400" />
|
||||
{title}
|
||||
</h2>
|
||||
@@ -262,7 +262,7 @@ export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }:
|
||||
</div>
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="flex justify-end space-x-3 pt-4 border-t border-white/10">
|
||||
<div className="dialog-footer flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -188,10 +188,10 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10000]">
|
||||
<div className="glass-effect-solid rounded-xl w-[900px] max-h-[90vh] flex flex-col">
|
||||
<div className="dialog-container rounded-xl w-[900px] max-h-[90vh] flex flex-col transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-warning-400" />
|
||||
근태 자료 오류 확인
|
||||
</h2>
|
||||
@@ -355,9 +355,8 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
|
||||
{ngList.map((item) => (
|
||||
<tr
|
||||
key={item.Date}
|
||||
className={`hover:bg-white/5 cursor-pointer ${
|
||||
item.IsMagam ? 'text-blue-400' : 'text-danger-400'
|
||||
}`}
|
||||
className={`hover:bg-white/5 cursor-pointer ${item.IsMagam ? 'text-blue-400' : 'text-danger-400'
|
||||
}`}
|
||||
onClick={() => toggleError(item.Date, item.IsMagam)}
|
||||
>
|
||||
<td className="px-2 py-2 text-center border-r border-white/10">
|
||||
@@ -396,7 +395,7 @@ export function KuntaeErrorCheckDialog({ isOpen, onClose }: KuntaeErrorCheckDial
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="px-6 py-4 border-t border-white/10">
|
||||
<div className="dialog-footer px-6 py-4">
|
||||
<button
|
||||
onClick={handleFix}
|
||||
disabled={isChecking || isFixing || selectedErrors.size === 0}
|
||||
|
||||
@@ -88,15 +88,15 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in" onClick={onClose}>
|
||||
<div className="glass-effect rounded-3xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl border border-white/10 animate-scale-in" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dialog-container rounded-3xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300 animate-scale-in" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="px-8 py-6 border-b border-white/10 flex items-center justify-between bg-white/5">
|
||||
<div className="dialog-header px-8 py-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2.5 bg-primary-500/20 rounded-xl">
|
||||
<ShieldCheck className="w-6 h-6 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white leading-tight">
|
||||
<h2 className="dialog-title">
|
||||
{formData.idx ? '라이선스 수정' : '라이선스 추가'}
|
||||
</h2>
|
||||
<p className="text-xs text-white/40 uppercase tracking-widest font-medium mt-0.5">Edit License Details</p>
|
||||
@@ -290,7 +290,7 @@ export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: L
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-8 py-6 border-t border-white/10 flex items-center justify-between bg-white/5">
|
||||
<div className="dialog-footer px-8 py-6 flex items-center justify-between">
|
||||
<div>
|
||||
{formData.idx && onDelete && (
|
||||
<button
|
||||
|
||||
@@ -132,12 +132,12 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-3xl glass-effect-solid rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div className="dialog-container relative w-full max-w-3xl rounded-2xl overflow-hidden transition-all duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">메일 테스트</h2>
|
||||
<h2 className="dialog-title">메일 테스트</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -238,7 +238,7 @@ export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10 bg-black/20">
|
||||
<div className="dialog-footer flex items-center justify-end gap-2 px-6 py-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={processing}
|
||||
|
||||
@@ -157,11 +157,11 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-800/95 backdrop-blur rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col shadow-2xl border border-white/10">
|
||||
<div className="dialog-container rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col overflow-hidden transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-primary-600/30 sticky top-0 z-10">
|
||||
<div className="dialog-header flex items-center justify-between p-4 sticky top-0 z-10">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">파트리스트</h2>
|
||||
<h2 className="dialog-title">파트리스트</h2>
|
||||
<p className="text-sm text-white/60">{projectName}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -225,9 +225,8 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
|
||||
return (
|
||||
<tr
|
||||
key={part.idx}
|
||||
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${
|
||||
isEditing ? 'bg-primary-500/10' : ''
|
||||
}`}
|
||||
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${isEditing ? 'bg-primary-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
@@ -500,7 +499,7 @@ export function PartListDialog({ projectIdx, projectName, onClose }: PartListDia
|
||||
|
||||
{/* 합계 */}
|
||||
{parts.length > 0 && (
|
||||
<div className="p-4 border-t border-white/10 bg-slate-900/50">
|
||||
<div className="dialog-footer p-4">
|
||||
<div className="flex justify-end gap-4 text-sm">
|
||||
<span className="text-white/70">
|
||||
총 <span className="text-white font-medium">{parts.length}</span>개 항목
|
||||
|
||||
@@ -108,9 +108,9 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-gray-900/95 border border-white/10 rounded-2xl shadow-2xl w-[1200px] h-[90vh] flex flex-col">
|
||||
<div className="dialog-container rounded-2xl w-[1200px] h-[90vh] flex flex-col overflow-hidden transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<select
|
||||
value={formData.status}
|
||||
@@ -473,7 +473,7 @@ export function ProjectDetailDialog({ project, onClose }: ProjectDetailDialogPro
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-white/10 shrink-0 bg-white/5">
|
||||
<div className="dialog-footer flex items-center justify-between px-6 py-3 shrink-0">
|
||||
<span className="text-sm text-white/50">PNO: {project.pno || '-'} | Jasmin: {project.jasmin || '-'} | 진행률: {formData.progress}%</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
|
||||
@@ -93,10 +93,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up mx-4 overflow-hidden flex flex-col max-h-[80vh]">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-2xl animate-slide-up mx-4 overflow-hidden flex flex-col max-h-[80vh] transition-all duration-300">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between bg-white/5">
|
||||
<h2 className="text-xl font-bold text-white flex items-center">
|
||||
<div className="dialog-header px-6 py-4 flex items-center justify-between">
|
||||
<h2 className="dialog-title flex items-center">
|
||||
<SettingsIcon className="w-5 h-5 mr-2 text-primary-400" />
|
||||
환경 설정
|
||||
</h2>
|
||||
@@ -263,7 +263,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end space-x-3">
|
||||
<div className="dialog-footer px-6 py-4 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/5 transition-colors"
|
||||
|
||||
@@ -232,15 +232,15 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-gray-900/95 border border-white/10 rounded-2xl shadow-2xl w-[1000px] max-h-[85vh] flex flex-col">
|
||||
<div className="dialog-container rounded-2xl w-[1000px] max-h-[85vh] flex flex-col overflow-hidden transition-all duration-300">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary-500/20 rounded-lg">
|
||||
<Users className="w-5 h-5 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">그룹정보</h2>
|
||||
<h2 className="dialog-title">그룹정보</h2>
|
||||
<p className="text-white/50 text-sm">부서/그룹 및 권한 관리</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,7 +350,7 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex items-center justify-between px-6 py-3 border-t border-white/10 shrink-0 bg-white/5">
|
||||
<div className="dialog-footer flex items-center justify-between px-6 py-3 shrink-0">
|
||||
<span className="text-sm text-white/50">{filteredItems.length}개 그룹</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -364,9 +364,9 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
|
||||
{/* 그룹 편집 모달 */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
|
||||
<div className="bg-gray-800 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title">
|
||||
{editingItem ? '그룹 수정' : '새 그룹'}
|
||||
</h2>
|
||||
<button
|
||||
@@ -472,10 +472,10 @@ export function UserGroupDialog({ isOpen, onClose }: UserGroupDialogProps) {
|
||||
{/* 권한 설정 모달 */}
|
||||
{showPermissionModal && editingItem && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
|
||||
<div className="bg-gray-800 rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="dialog-container rounded-2xl w-full max-w-md max-h-[90vh] overflow-hidden flex flex-col transition-all duration-300">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">권한 설정</h2>
|
||||
<h2 className="dialog-title">권한 설정</h2>
|
||||
<p className="text-white/60 text-sm">{editingItem.dept}</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Save, User, Mail, Building2, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
|
||||
import { X, Save, User, Mail, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { comms } from '@/communication';
|
||||
import { UserInfoDetail } from '@/types';
|
||||
@@ -109,276 +109,350 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10000]">
|
||||
<div className="dialog-container rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden transition-all duration-300">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[10000] animate-fade-in">
|
||||
<div className="dialog-container w-full max-w-4xl h-[85vh] flex flex-col shadow-2xl border border-white/10 bg-[#1a1b2e]/90 backdrop-blur-xl rounded-3xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title">
|
||||
<User className="w-6 h-6" />
|
||||
사용자 정보
|
||||
</h2>
|
||||
<div className="dialog-header px-8 py-6 border-b border-white/10 bg-white/5 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-primary-500/20 rounded-xl">
|
||||
<User className="w-6 h-6 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="dialog-title text-2xl">사용자 프로필</h2>
|
||||
<p className="text-white/40 text-xs mt-1">개인 정보 및 시스템 환경설정을 관리합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||
className="p-2 hover:bg-white/10 rounded-full text-white/40 hover:text-white transition-all transform hover:rotate-90"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)] custom-scrollbar">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
<div className="h-full flex flex-col items-center justify-center gap-4">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary-400"></div>
|
||||
<p className="text-white/40 text-sm">사용자 정보를 불러오는 중...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 min-h-0">
|
||||
{/* 좌측 컬럼: 기본 정보 */}
|
||||
<div className="space-y-4 overflow-y-auto pr-2 custom-scrollbar">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<User className="w-4 h-4" />
|
||||
사원번호
|
||||
<div className="flex flex-col lg:flex-row h-full">
|
||||
{/* Left Panel: Profile Info */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-8 border-r border-white/10 bg-white/[0.02]">
|
||||
<div className="space-y-8">
|
||||
{/* Identity Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-1 h-4 bg-primary-500 rounded-full"></div>
|
||||
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">기본 정보</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">사원번호</label>
|
||||
<div className="px-4 py-3.5 bg-white/5 border border-white/10 rounded-xl text-white/50 font-mono text-sm flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-white/20"></span>
|
||||
{formData.Id}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">직책</label>
|
||||
<div className="relative group">
|
||||
<Briefcase className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-primary-400 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Grade}
|
||||
onChange={(e) => handleInputChange('Grade', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-medium"
|
||||
placeholder="직책 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">이름 (한글)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameK}
|
||||
onChange={(e) => handleInputChange('NameK', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-bold tracking-tight"
|
||||
placeholder="이름 입력"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">이름 (영문)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameE}
|
||||
onChange={(e) => handleInputChange('NameE', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all font-medium"
|
||||
placeholder="English Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-1 h-4 bg-secondary-500 rounded-full"></div>
|
||||
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">연락처 정보</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">이메일</label>
|
||||
<div className="relative group">
|
||||
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 group-focus-within:text-secondary-400 transition-colors" />
|
||||
<input
|
||||
type="email"
|
||||
value={formData.Email}
|
||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all font-medium"
|
||||
placeholder="email@company.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">전화번호 (내선)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Tel}
|
||||
onChange={(e) => handleInputChange('Tel', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all"
|
||||
placeholder="내선번호"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">휴대전화</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Hp}
|
||||
onChange={(e) => handleInputChange('Hp', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-secondary-500/50 focus:bg-white/10 transition-all"
|
||||
placeholder="010-0000-0000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Employment Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-1 h-4 bg-indigo-500 rounded-full"></div>
|
||||
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">부서 및 인사</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">부서</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Dept}
|
||||
onChange={(e) => handleInputChange('Dept', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all"
|
||||
placeholder="소속 부서"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">공정</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Process}
|
||||
onChange={(e) => handleInputChange('Process', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all"
|
||||
placeholder="담당 공정"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">입사일</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 pointer-events-none" />
|
||||
<input
|
||||
type="date"
|
||||
value={formData.DateIn}
|
||||
onChange={(e) => handleInputChange('DateIn', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all [color-scheme:dark] font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] uppercase tracking-widest font-bold text-white/30 ml-1">퇴사일</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-white/20 pointer-events-none" />
|
||||
<input
|
||||
type="date"
|
||||
value={formData.DateO}
|
||||
onChange={(e) => handleInputChange('DateO', e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl pl-11 pr-4 py-3.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:bg-white/10 transition-all [color-scheme:dark] font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex flex-wrap gap-4">
|
||||
<label className={clsx(
|
||||
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
|
||||
formData.UseJobReport ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
|
||||
)}>
|
||||
<div className={clsx(
|
||||
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
|
||||
formData.UseJobReport ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
|
||||
)}>
|
||||
<Briefcase className="w-3 h-3" />
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.UseJobReport}
|
||||
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className={clsx("text-xs font-bold", formData.UseJobReport ? "text-primary-300" : "text-white/50")}>업무일지 사용</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Id}
|
||||
disabled
|
||||
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">이름</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameK}
|
||||
onChange={(e) => handleInputChange('NameK', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="이름"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">영문이름</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameE}
|
||||
onChange={(e) => handleInputChange('NameE', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="English Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Briefcase className="w-4 h-4" />
|
||||
직책
|
||||
|
||||
<label className={clsx(
|
||||
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
|
||||
formData.UseUserState ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
|
||||
)}>
|
||||
<div className={clsx(
|
||||
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
|
||||
formData.UseUserState ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
|
||||
)}>
|
||||
<User className="w-3 h-3" />
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.UseUserState}
|
||||
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className={clsx("text-xs font-bold", formData.UseUserState ? "text-primary-300" : "text-white/50")}>계정 사용</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Grade}
|
||||
onChange={(e) => handleInputChange('Grade', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="직책"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">공정</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Process}
|
||||
onChange={(e) => handleInputChange('Process', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="공정"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 이메일 */}
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Mail className="w-4 h-4" />
|
||||
이메일
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.Email}
|
||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 입/퇴사 정보 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
입사일
|
||||
<label className={clsx(
|
||||
"flex items-center gap-3 px-4 py-2.5 rounded-xl border transition-all cursor-pointer flex-1",
|
||||
formData.ExceptHoly ? "bg-primary-500/20 border-primary-500/30" : "bg-white/5 border-white/10 hover:bg-white/10"
|
||||
)}>
|
||||
<div className={clsx(
|
||||
"w-5 h-5 rounded-md border flex items-center justify-center transition-colors",
|
||||
formData.ExceptHoly ? "bg-primary-500 border-primary-500 text-white" : "border-white/30 text-transparent"
|
||||
)}>
|
||||
<Calendar className="w-3 h-3" />
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.ExceptHoly}
|
||||
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className={clsx("text-xs font-bold", formData.ExceptHoly ? "text-primary-300" : "text-white/50")}>휴가 제외</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.DateIn}
|
||||
onChange={(e) => handleInputChange('DateIn', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
퇴사일
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.DateO}
|
||||
onChange={(e) => handleInputChange('DateO', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 옵션 체크박스 */}
|
||||
<div className="flex flex-wrap gap-6 pt-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.UseJobReport}
|
||||
onChange={(e) => handleInputChange('UseJobReport', e.target.checked)}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
||||
/>
|
||||
<span className="text-white/70">업무일지 사용</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.UseUserState}
|
||||
onChange={(e) => handleInputChange('UseUserState', e.target.checked)}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
||||
/>
|
||||
<span className="text-white/70">계정 사용</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.ExceptHoly}
|
||||
onChange={(e) => handleInputChange('ExceptHoly', e.target.checked)}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10 text-blue-600"
|
||||
/>
|
||||
<span className="text-white/70">휴가 제외</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 컬럼: 테마 설정 + 비고 */}
|
||||
<div className="flex flex-col h-full gap-4">
|
||||
{/* 테마 설정 섹션 */}
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<Palette className="w-4 h-4 text-purple-400" />
|
||||
테마 설정
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
onClick={() => handleThemeChange('dark')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'dark'
|
||||
? 'border-blue-500 bg-blue-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">기본 (Dark)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleThemeChange('PSH_PINK')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'PSH_PINK'
|
||||
? 'border-pink-500 bg-pink-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-pink-500 to-rose-500 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">발랄한 핑크</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleThemeChange('JW_SKY')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'JW_SKY'
|
||||
? 'border-sky-500 bg-sky-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-sky-400 to-blue-500 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">시원한 하늘</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고: 남은 높이 채우기 */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
비고
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.Memo}
|
||||
onChange={(e) => handleInputChange('Memo', e.target.value)}
|
||||
className="flex-1 w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 resize-none"
|
||||
placeholder="비고"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메시지 */}
|
||||
{message && (
|
||||
<div
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-lg text-sm',
|
||||
message.type === 'success' ? 'bg-green-600/20 text-green-400' : 'bg-red-600/20 text-red-400'
|
||||
)}
|
||||
>
|
||||
{message.text}
|
||||
{/* Right Panel: Preferences & Memo */}
|
||||
<div className="w-full lg:w-96 flex flex-col h-full bg-[#131426]/50">
|
||||
<div className="p-6 md:p-8 space-y-8 flex-1 overflow-y-auto custom-scrollbar">
|
||||
{/* Theme Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Palette className="w-4 h-4 text-purple-400" />
|
||||
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">테마 설정</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{[
|
||||
{ id: 'dark', name: 'Standard Dark', desc: '기본 어두운 테마', gradient: 'from-blue-600 to-indigo-900', border: 'border-blue-500' },
|
||||
{ id: 'PSH_PINK', name: 'Vibrant Pink', desc: '발랄한 핑크 테마', gradient: 'from-pink-500 to-rose-500', border: 'border-pink-500' },
|
||||
{ id: 'JW_SKY', name: 'Fresh Sky', desc: '시원한 하늘 테마', gradient: 'from-sky-400 to-blue-500', border: 'border-sky-500' },
|
||||
].map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => handleThemeChange(t.id as Theme)}
|
||||
className={clsx(
|
||||
"group relative overflow-hidden rounded-xl border transition-all duration-300 text-left p-4",
|
||||
theme === t.id
|
||||
? `bg-white/5 ${t.border} ring-1 ring-white/20`
|
||||
: "bg-white/5 border-white/10 hover:border-white/30 hover:bg-white/10"
|
||||
)}
|
||||
>
|
||||
<div className={clsx("absolute inset-0 opacity-10 bg-gradient-to-br transition-opacity", t.gradient, theme === t.id ? "opacity-20" : "group-hover:opacity-15")} />
|
||||
<div className="relative z-10 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className={clsx("font-bold text-sm", theme === t.id ? "text-white" : "text-white/70 group-hover:text-white")}>{t.name}</h4>
|
||||
<p className="text-xs text-white/40 mt-0.5">{t.desc}</p>
|
||||
</div>
|
||||
<div className={clsx("w-8 h-8 rounded-full bg-gradient-to-br shadow-lg", t.gradient)} />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Memo Section */}
|
||||
<section className="flex-1 flex flex-col min-h-[200px]">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<FileText className="w-4 h-4 text-white/40" />
|
||||
<h3 className="text-sm font-bold text-white/80 uppercase tracking-wider">비고 & 메모</h3>
|
||||
</div>
|
||||
<div className="flex-1 relative group">
|
||||
<textarea
|
||||
value={formData.Memo}
|
||||
onChange={(e) => handleInputChange('Memo', e.target.value)}
|
||||
className="w-full h-full bg-white/5 border border-white/10 rounded-xl p-4 text-sm text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/10 transition-all resize-none leading-relaxed"
|
||||
placeholder="사용자에 대한 추가 정보를 입력하세요..."
|
||||
/>
|
||||
<div className="absolute bottom-4 right-4 text-[10px] text-white/20 font-mono">
|
||||
MEMO AREA
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="dialog-footer flex items-center justify-end px-6 py-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="dialog-footer px-8 py-5 border-t border-white/10 bg-[#131426] shrink-0 flex items-center justify-between">
|
||||
<div className="text-xs text-white/30 font-medium">
|
||||
{message && (
|
||||
<span className={clsx(
|
||||
"px-3 py-1.5 rounded-lg inline-flex items-center gap-2",
|
||||
message.type === 'success' ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
|
||||
)}>
|
||||
<div className={clsx("w-1.5 h-1.5 rounded-full", message.type === 'success' ? "bg-green-500" : "bg-red-500")} />
|
||||
{message.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white/70 hover:text-white transition-colors"
|
||||
className="px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-white/70 hover:text-white text-sm font-bold transition-all active:scale-95"
|
||||
>
|
||||
취소
|
||||
닫기
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors',
|
||||
saving
|
||||
? 'bg-blue-600/50 text-white/50 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
)}
|
||||
className="px-8 py-2.5 bg-primary-500 hover:bg-primary-600 border border-white/20 rounded-xl text-white text-sm font-bold transition-all shadow-lg shadow-primary-500/20 active:scale-95 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saving ? '저장 중...' : '저장'}
|
||||
{saving ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
{saving ? '저장 중...' : '저장 완료'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
);
|
||||
|
||||
@@ -98,18 +98,18 @@ export function UserSearchDialog({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="glass-effect rounded-xl w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col"
|
||||
className="dialog-container rounded-xl w-full max-w-lg max-h-[80vh] overflow-hidden flex flex-col transition-all duration-300"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="p-4 border-b border-white/10 flex items-center justify-between shrink-0">
|
||||
<div className="dialog-header p-4 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
<h2 className="dialog-title">{title}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -156,11 +156,10 @@ export function UserSearchDialog({
|
||||
onSelect(user);
|
||||
onClose();
|
||||
}}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
|
||||
selectedUser?.id === user.id
|
||||
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${selectedUser?.id === user.id
|
||||
? 'bg-primary-500/30 border border-primary-400/50'
|
||||
: 'bg-white/5 hover:bg-white/10 border border-transparent'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -186,7 +185,7 @@ export function UserSearchDialog({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="p-4 border-t border-white/10 flex items-center justify-between shrink-0">
|
||||
<div className="dialog-footer p-4 flex items-center justify-between shrink-0">
|
||||
<span className="text-sm text-white/50">
|
||||
{filteredUsers.length}명 {selectedUser && `| 선택: ${selectedUser.id} (${selectedUser.name})`}
|
||||
</span>
|
||||
|
||||
@@ -709,9 +709,9 @@ export function Dashboard() {
|
||||
{/* 업무일지 미등록 상세 모달 */}
|
||||
{showUnregisteredModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
||||
<div className="bg-bg-paper border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<div className="dialog-container border border-white/10 rounded-2xl w-full max-w-md overflow-hidden animate-scale-in transition-all duration-300">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
|
||||
<h3 className="dialog-title flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-danger-400" />
|
||||
업무일지 미등록 내역
|
||||
</h3>
|
||||
@@ -754,7 +754,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
|
||||
<div className="dialog-footer px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUnregisteredModal(false);
|
||||
@@ -898,8 +898,8 @@ export function Dashboard() {
|
||||
type="button"
|
||||
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${todoFormData.status === option.value
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
@@ -1058,8 +1058,8 @@ export function Dashboard() {
|
||||
type="button"
|
||||
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${todoFormData.status === option.value
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
@@ -6,9 +6,8 @@ import { ItemInfo, ItemDetail, SupplierStaff, PurchaseHistoryItem } from '@/type
|
||||
import { ItemEditDialog } from '@/components/items';
|
||||
|
||||
export function ItemsPage() {
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [items, setItems] = useState<ItemInfo[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||
const [selectedCategory] = useState<string>('all');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filter, setFilter] = useState('');
|
||||
@@ -24,20 +23,9 @@ export function ItemsPage() {
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories();
|
||||
// No categories to load
|
||||
}, []);
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const result = await comms.getItemCategories();
|
||||
if (result.Success && result.Data) {
|
||||
setCategories(result.Data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 로드 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadItems = async () => {
|
||||
if (!searchKey.trim()) {
|
||||
alert('검색어를 입력하세요');
|
||||
|
||||
@@ -802,9 +802,9 @@ export function Jobreport() {
|
||||
{/* 업무일지 미등록 상세 모달 */}
|
||||
{showUnregisteredModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
||||
<div className="bg-slate-900 border border-white/10 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-scale-in">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<div className="dialog-container border border-white/10 rounded-2xl w-full max-w-md overflow-hidden animate-scale-in transition-all duration-300">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4 border-b border-white/10 bg-white/5">
|
||||
<h3 className="dialog-title flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-danger-400" />
|
||||
업무일지 미등록 내역
|
||||
</h3>
|
||||
@@ -847,7 +847,7 @@ export function Jobreport() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
|
||||
<div className="dialog-footer px-6 py-4 border-t border-white/10 bg-white/5 flex justify-end">
|
||||
<button
|
||||
onClick={() => setShowUnregisteredModal(false)}
|
||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-colors text-sm font-medium"
|
||||
|
||||
@@ -555,14 +555,14 @@ function TodoModal({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md animate-fade-in" onClick={onClose}>
|
||||
<div className="bg-[#1a1b2e]/90 rounded-3xl shadow-2xl w-full max-w-2xl overflow-hidden border border-white/10 flex flex-col backdrop-blur-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dialog-container w-full max-w-2xl" onClick={(e) => e.stopPropagation()}>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-8 py-6 border-b border-white/10 bg-white/5">
|
||||
<div className="dialog-header">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 bg-primary-500/20 rounded-lg">
|
||||
<div className={`p-2 rounded-lg ${isEdit ? 'bg-primary-500/20' : 'bg-primary-500/20'}`}>
|
||||
{isEdit ? <Edit3 className="w-5 h-5 text-primary-400" /> : <Plus className="w-5 h-5 text-primary-400" />}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-white tracking-tight">{title}</h2>
|
||||
<h2 className="dialog-title">{title}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isEdit && onComplete && currentStatus !== '5' && (
|
||||
@@ -709,7 +709,7 @@ function TodoModal({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-8 py-6 border-t border-white/10 bg-white/5 flex items-center justify-between">
|
||||
<div className="dialog-footer">
|
||||
<div>
|
||||
{isEdit && onDelete && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user