Apply standardized dialog theme to PasswordDialog and Note dialogs
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { User, LogOut, X } from 'lucide-react';
|
import { User, LogOut, X, UserCog, Key } from 'lucide-react';
|
||||||
import { comms } from '@/communication';
|
import { comms } from '@/communication';
|
||||||
|
import { UserInfoDialog } from '@/components/user/UserInfoDialog';
|
||||||
|
import { PasswordDialog } from '@/components/user/PasswordDialog';
|
||||||
|
|
||||||
interface UserInfoButtonProps {
|
interface UserInfoButtonProps {
|
||||||
userName?: string;
|
userName?: string;
|
||||||
@@ -10,8 +12,21 @@ interface UserInfoButtonProps {
|
|||||||
|
|
||||||
export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
||||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||||
|
const [showUserInfoDialog, setShowUserInfoDialog] = useState(false);
|
||||||
|
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
|
||||||
|
// ESC 키로 다이얼로그 닫기
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && showLogoutDialog) {
|
||||||
|
setShowLogoutDialog(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [showLogoutDialog]);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
try {
|
try {
|
||||||
@@ -31,6 +46,30 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUserInfoClick = () => {
|
||||||
|
setShowLogoutDialog(false);
|
||||||
|
setShowUserInfoDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordClick = () => {
|
||||||
|
setShowLogoutDialog(false);
|
||||||
|
setShowPasswordDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = async (oldPw: string, newPw: string) => {
|
||||||
|
try {
|
||||||
|
const result = await comms.changePassword(oldPw, newPw);
|
||||||
|
if (result.Success) {
|
||||||
|
alert('비밀번호가 변경되었습니다.');
|
||||||
|
} else {
|
||||||
|
alert(result.Message || '비밀번호 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('비밀번호 변경 오류:', error);
|
||||||
|
alert('비밀번호 변경 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!userName) return null;
|
if (!userName) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,8 +97,8 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-white flex items-center">
|
<h2 className="text-lg font-semibold text-white flex items-center">
|
||||||
<LogOut className="w-5 h-5 mr-2" />
|
<User className="w-5 h-5 mr-2" />
|
||||||
로그아웃
|
사용자 메뉴
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowLogoutDialog(false)}
|
onClick={() => setShowLogoutDialog(false)}
|
||||||
@@ -79,16 +118,30 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
|||||||
{userDept && <p className="text-white/50 text-sm">{userDept}</p>}
|
{userDept && <p className="text-white/50 text-sm">{userDept}</p>}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-white/70 text-center text-sm">
|
<p className="text-white/70 text-center text-sm">
|
||||||
로그아웃 하시겠습니까?
|
원하시는 작업을 선택하세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 푸터 */}
|
{/* 푸터 */}
|
||||||
<div className="px-6 py-4 border-t border-white/10 flex justify-center">
|
<div className="px-6 py-4 border-t border-white/10 flex justify-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleUserInfoClick}
|
||||||
|
className="bg-transparent border border-white/30 hover:bg-white/10 text-white px-3 py-2 rounded-lg transition-colors flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<UserCog className="w-4 h-4 mr-2" />
|
||||||
|
정보수정
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePasswordClick}
|
||||||
|
className="bg-transparent border border-white/30 hover:bg-white/10 text-white px-3 py-2 rounded-lg transition-colors flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<Key className="w-4 h-4 mr-2" />
|
||||||
|
암호변경
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
className="bg-danger-500 hover:bg-danger-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
className="bg-danger-500 hover:bg-danger-600 text-white px-3 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50 text-sm"
|
||||||
>
|
>
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<span className="animate-spin mr-2">⏳</span>
|
<span className="animate-spin mr-2">⏳</span>
|
||||||
@@ -102,6 +155,19 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
|||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 사용자 정보 수정 다이얼로그 */}
|
||||||
|
<UserInfoDialog
|
||||||
|
isOpen={showUserInfoDialog}
|
||||||
|
onClose={() => setShowUserInfoDialog(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 비밀번호 변경 다이얼로그 */}
|
||||||
|
<PasswordDialog
|
||||||
|
isOpen={showPasswordDialog}
|
||||||
|
onClose={() => setShowPasswordDialog(false)}
|
||||||
|
onConfirm={handlePasswordChange}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,15 +299,15 @@ export function LicenseList() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 헤더 (메모장 디자인 통일) */}
|
{/* 컬럼 헤더 (메모장 디자인 통일) */}
|
||||||
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4">
|
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4 text-list-header text-white/opacity-header-muted font-list-header uppercase">
|
||||||
<div className="w-8 text-center text-xs font-medium text-white/70 uppercase">상태</div>
|
<div className="w-8 text-center uppercase">상태</div>
|
||||||
<div className="flex-1 text-xs font-medium text-white/70 uppercase">제품명 / 제조사</div>
|
<div className="flex-1 uppercase">제품명 / 제조사</div>
|
||||||
<div className="flex items-center gap-6 shrink-0">
|
<div className="flex items-center gap-6 shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-32 text-left text-xs font-medium text-white/70 uppercase">버전</div>
|
<div className="w-32 text-left uppercase">버전</div>
|
||||||
<div className="w-16 text-center text-xs font-medium text-white/70 uppercase">수량</div>
|
<div className="w-16 text-center uppercase">수량</div>
|
||||||
<div className="w-32 text-left text-xs font-medium text-white/70 uppercase">사용자</div>
|
<div className="w-32 text-left uppercase">사용자</div>
|
||||||
<div className="w-48 text-left text-xs font-medium text-white/70 uppercase">시리얼 번호</div>
|
<div className="w-48 text-left uppercase">시리얼 번호</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-8"></div> {/* 액션/폴더 버튼 공간 */}
|
<div className="w-8"></div> {/* 액션/폴더 버튼 공간 */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export function NoteEditModal({
|
|||||||
processing,
|
processing,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
initialEditMode = false,
|
|
||||||
}: NoteEditModalProps) {
|
}: NoteEditModalProps) {
|
||||||
const [pdate, setPdate] = useState('');
|
const [pdate, setPdate] = useState('');
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
@@ -34,7 +33,6 @@ export function NoteEditModal({
|
|||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [share, setShare] = useState(false);
|
const [share, setShare] = useState(false);
|
||||||
const [guid, setGuid] = useState('');
|
const [guid, setGuid] = useState('');
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
|
||||||
|
|
||||||
// 현재 로그인 사용자 정보 로드
|
// 현재 로그인 사용자 정보 로드
|
||||||
const [currentUserId, setCurrentUserId] = useState('');
|
const [currentUserId, setCurrentUserId] = useState('');
|
||||||
@@ -66,19 +64,17 @@ export function NoteEditModal({
|
|||||||
setDescription(editingItem.description || '');
|
setDescription(editingItem.description || '');
|
||||||
setShare(editingItem.share || false);
|
setShare(editingItem.share || false);
|
||||||
setGuid(editingItem.guid || '');
|
setGuid(editingItem.guid || '');
|
||||||
setIsEditMode(initialEditMode);
|
|
||||||
} else {
|
} else {
|
||||||
// 신규 메모 - 편집 모드로 시작
|
// 신규 메모
|
||||||
setPdate(new Date().toISOString().split('T')[0]);
|
setPdate(new Date().toISOString().split('T')[0]);
|
||||||
setTitle('');
|
setTitle('');
|
||||||
setUid(currentUserId);
|
setUid(currentUserId);
|
||||||
setDescription('');
|
setDescription('');
|
||||||
setShare(false);
|
setShare(false);
|
||||||
setGuid('');
|
setGuid('');
|
||||||
setIsEditMode(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen, editingItem, currentUserId, initialEditMode]);
|
}, [isOpen, editingItem, currentUserId]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -94,15 +90,15 @@ export function NoteEditModal({
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden border border-white/10">
|
<div className="dialog-container rounded-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden 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
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/50 hover:text-white transition-colors"
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -117,47 +113,37 @@ export function NoteEditModal({
|
|||||||
<div>
|
<div>
|
||||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||||
<Calendar className="w-4 h-4 mr-2" />
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
날짜 {isEditMode && '*'}
|
날짜
|
||||||
</label>
|
</label>
|
||||||
{isEditMode ? (
|
<input
|
||||||
<input
|
type="date"
|
||||||
type="date"
|
value={pdate}
|
||||||
value={pdate}
|
onChange={(e) => setPdate(e.target.value)}
|
||||||
onChange={(e) => setPdate(e.target.value)}
|
required
|
||||||
required
|
disabled={!canEdit}
|
||||||
disabled={!canEdit}
|
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-white text-sm">{pdate}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 작성자 */}
|
{/* 작성자 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||||
<User className="w-4 h-4 mr-2" />
|
<User className="w-4 h-4 mr-2" />
|
||||||
작성자 {isEditMode && '*'}
|
작성자
|
||||||
</label>
|
</label>
|
||||||
{isEditMode ? (
|
<input
|
||||||
<>
|
type="text"
|
||||||
<input
|
value={uid}
|
||||||
type="text"
|
onChange={(e) => setUid(e.target.value)}
|
||||||
value={uid}
|
required
|
||||||
onChange={(e) => setUid(e.target.value)}
|
disabled={!canEdit || !canChangeUser}
|
||||||
required
|
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={!canEdit || !canChangeUser}
|
placeholder="사용자 ID"
|
||||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
/>
|
||||||
placeholder="사용자 ID"
|
{!canChangeUser && (
|
||||||
/>
|
<p className="text-xs text-white/50 mt-1">
|
||||||
{!canChangeUser && (
|
관리자만 변경 가능
|
||||||
<p className="text-xs text-white/50 mt-1">
|
</p>
|
||||||
관리자만 변경 가능
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-white text-sm">{uid}</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,33 +152,27 @@ export function NoteEditModal({
|
|||||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||||
공유 설정
|
공유 설정
|
||||||
</label>
|
</label>
|
||||||
{isEditMode ? (
|
<div className="flex items-center gap-2">
|
||||||
<>
|
<input
|
||||||
<div className="flex items-center gap-2">
|
type="checkbox"
|
||||||
<input
|
id="note-share"
|
||||||
type="checkbox"
|
checked={share}
|
||||||
id="note-share"
|
onChange={(e) => setShare(e.target.checked)}
|
||||||
checked={share}
|
disabled={!canEdit || !canChangeShare}
|
||||||
onChange={(e) => setShare(e.target.checked)}
|
className="w-4 h-4 bg-white/20 border border-white/30 rounded focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={!canEdit || !canChangeShare}
|
/>
|
||||||
className="w-4 h-4 bg-white/20 border border-white/30 rounded focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
<label htmlFor="note-share" className="text-sm text-white/70 cursor-pointer">
|
||||||
/>
|
공유
|
||||||
<label htmlFor="note-share" className="text-sm text-white/70 cursor-pointer">
|
</label>
|
||||||
공유
|
</div>
|
||||||
</label>
|
{!canChangeShare && (
|
||||||
</div>
|
<p className="text-xs text-white/50 mt-1">
|
||||||
{!canChangeShare && (
|
본인 메모만 변경 가능
|
||||||
<p className="text-xs text-white/50 mt-1">
|
</p>
|
||||||
본인 메모만 변경 가능
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-white text-sm">{share ? '공유됨' : '비공유'}</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!canEdit && isEditMode && (
|
{!canEdit && (
|
||||||
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-red-300 text-xs">
|
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-red-300 text-xs">
|
||||||
타인의 자료는 수정할 수 없습니다.
|
타인의 자료는 수정할 수 없습니다.
|
||||||
</div>
|
</div>
|
||||||
@@ -204,21 +184,17 @@ export function NoteEditModal({
|
|||||||
{/* 제목 */}
|
{/* 제목 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||||
제목 {isEditMode && '*'}
|
제목
|
||||||
</label>
|
</label>
|
||||||
{isEditMode ? (
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={title}
|
||||||
value={title}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
required
|
||||||
required
|
disabled={!canEdit}
|
||||||
disabled={!canEdit}
|
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
placeholder="메모 제목을 입력하세요"
|
||||||
placeholder="메모 제목을 입력하세요"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-white text-lg font-semibold">{title}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내용 */}
|
{/* 내용 */}
|
||||||
@@ -226,27 +202,21 @@ export function NoteEditModal({
|
|||||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||||
내용
|
내용
|
||||||
</label>
|
</label>
|
||||||
{isEditMode ? (
|
<textarea
|
||||||
<textarea
|
value={description}
|
||||||
value={description}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
disabled={!canEdit}
|
||||||
disabled={!canEdit}
|
rows={18}
|
||||||
rows={18}
|
className="w-full bg-white/20 border border-white/30 rounded-lg p-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
||||||
className="w-full bg-white/20 border border-white/30 rounded-lg p-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
placeholder="메모 내용을 입력하세요"
|
||||||
placeholder="메모 내용을 입력하세요"
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white/10 border border-white/20 rounded-lg p-4 text-white whitespace-pre-wrap min-h-[400px]">
|
|
||||||
{description || '내용이 없습니다.'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* 하단 버튼 */}
|
{/* 하단 버튼 */}
|
||||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10 bg-white/5">
|
<div className="dialog-footer flex items-center justify-end gap-3 px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -255,7 +225,7 @@ export function NoteEditModal({
|
|||||||
>
|
>
|
||||||
닫기
|
닫기
|
||||||
</button>
|
</button>
|
||||||
{isEditMode && canEdit && (
|
{canEdit && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteV
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden border border-white/10">
|
<div className="dialog-container rounded-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden 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">{note.title || '제목 없음'}</h2>
|
<h2 className="dialog-title">{note.title || '제목 없음'}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/50 hover:text-white transition-colors"
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -41,7 +41,7 @@ export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteV
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 하단 버튼 */}
|
{/* 하단 버튼 */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10 bg-white/5">
|
<div className="dialog-footer flex items-center justify-between px-6 py-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="px-4 py-2 rounded-lg bg-danger-500 hover:bg-danger-600 text-white transition-colors flex items-center gap-2"
|
className="px-4 py-2 rounded-lg bg-danger-500 hover:bg-danger-600 text-white transition-colors flex items-center gap-2"
|
||||||
|
|||||||
190
Project/frontend/src/components/user/PasswordDialog.tsx
Normal file
190
Project/frontend/src/components/user/PasswordDialog.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Key, Lock, ShieldCheck, CheckCircle } from 'lucide-react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
|
interface PasswordDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (oldPassword: string, newPassword: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordDialog({ isOpen, onClose, onConfirm }: PasswordDialogProps) {
|
||||||
|
const [oldPassword, setOldPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
// ESC 키로 닫기
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!oldPassword) {
|
||||||
|
setError('기존 비밀번호를 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newPassword) {
|
||||||
|
setError('새 비밀번호를 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError('새 비밀번호가 일치하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newPassword.length < 4) {
|
||||||
|
setError('비밀번호는 4자 이상이어야 합니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirm(oldPassword, newPassword);
|
||||||
|
|
||||||
|
// 초기화는 부모에서 성공 시 처리하거나, 여기서 닫힐 때 처리
|
||||||
|
// 하지만 단순화를 위해 여기서 초기화
|
||||||
|
setTimeout(() => {
|
||||||
|
setOldPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
setError('');
|
||||||
|
}, 300);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="fixed inset-0 z-[10001] flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||||
|
<div className="dialog-container rounded-2xl w-full max-w-sm animate-slide-up overflow-hidden transition-all duration-300">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="dialog-header relative px-6 py-6">
|
||||||
|
<div className="flex items-center justify-between relative z-10">
|
||||||
|
<h3 className="dialog-title gap-3">
|
||||||
|
<div className="p-2 bg-yellow-500/20 rounded-xl border border-yellow-500/30">
|
||||||
|
<Key className="w-5 h-5 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
비밀번호 변경
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Decorative background glow */}
|
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-yellow-500/10 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="group">
|
||||||
|
<label className="block text-xs uppercase tracking-wider text-white/50 mb-1.5 font-medium ml-1">
|
||||||
|
기존 비밀번호
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/40 group-focus-within:text-yellow-500 transition-colors">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={oldPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setOldPassword(e.target.value);
|
||||||
|
if (error) setError('');
|
||||||
|
}}
|
||||||
|
className="w-full pl-10 pr-4 py-3 bg-black/20 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-yellow-500/50 focus:bg-black/40 transition-all font-mono"
|
||||||
|
placeholder="현재 비밀번호 입력"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="group">
|
||||||
|
<label className="block text-xs uppercase tracking-wider text-white/50 mb-1.5 font-medium ml-1">
|
||||||
|
새 비밀번호
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/40 group-focus-within:text-blue-500 transition-colors">
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNewPassword(e.target.value);
|
||||||
|
if (error) setError('');
|
||||||
|
}}
|
||||||
|
className="w-full pl-10 pr-4 py-3 bg-black/20 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:border-blue-500/50 focus:bg-black/40 transition-all font-mono"
|
||||||
|
placeholder="새로운 비밀번호"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="group">
|
||||||
|
<label className="block text-xs uppercase tracking-wider text-white/50 mb-1.5 font-medium ml-1">
|
||||||
|
비밀번호 확인
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-white/40 group-focus-within:text-green-500 transition-colors">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setConfirmPassword(e.target.value);
|
||||||
|
if (error) setError('');
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
"w-full pl-10 pr-4 py-3 bg-black/20 border rounded-xl text-white placeholder-white/30 focus:outline-none transition-all font-mono",
|
||||||
|
confirmPassword && newPassword === confirmPassword
|
||||||
|
? "border-green-500/50 focus:border-green-500/50 bg-green-500/5"
|
||||||
|
: "border-white/10 focus:border-green-500/50 focus:bg-black/40"
|
||||||
|
)}
|
||||||
|
placeholder="비밀번호 다시 입력"
|
||||||
|
/>
|
||||||
|
{confirmPassword && newPassword === confirmPassword && (
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-green-500 animate-in fade-in zoom-in duration-200">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm flex items-start gap-2 animate-in slide-in-from-top-1">
|
||||||
|
<span className="mt-0.5 block w-1.5 h-1.5 rounded-full bg-red-500 shrink-0"></span>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="dialog-footer flex justify-end gap-3 px-6 py-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2.5 rounded-xl border border-white/10 hover:bg-white/5 text-white/70 hover:text-white transition-all text-sm font-medium"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white shadow-lg shadow-blue-900/20 border border-blue-500/20 transition-all active:scale-[0.98] text-sm font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
비밀번호 변경
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { X, Save, Key, User, Mail, Building2, Briefcase, Calendar, FileText, Palette } from 'lucide-react';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { X, Save, User, Mail, Building2, 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';
|
||||||
@@ -12,119 +13,9 @@ interface UserInfoDialogProps {
|
|||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PasswordDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: (oldPassword: string, newPassword: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PasswordDialog({ isOpen, onClose, onConfirm }: PasswordDialogProps) {
|
|
||||||
const [oldPassword, setOldPassword] = useState('');
|
|
||||||
const [newPassword, setNewPassword] = useState('');
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
// ESC 키로 닫기
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && isOpen) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [isOpen, onClose]);
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!newPassword) {
|
|
||||||
setError('새 비밀번호를 입력하세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newPassword !== confirmPassword) {
|
|
||||||
setError('새 비밀번호가 일치하지 않습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onConfirm(oldPassword, newPassword);
|
|
||||||
setOldPassword('');
|
|
||||||
setNewPassword('');
|
|
||||||
setConfirmPassword('');
|
|
||||||
setError('');
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[10001]">
|
|
||||||
<div className="glass-effect-solid rounded-xl p-6 w-full max-w-sm">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
||||||
<Key className="w-5 h-5" />
|
|
||||||
비밀번호 변경
|
|
||||||
</h3>
|
|
||||||
<button onClick={onClose} className="text-white/60 hover:text-white">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-white/70 mb-1">기존 비밀번호</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={oldPassword}
|
|
||||||
onChange={(e) => setOldPassword(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="password"
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(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="password"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(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>
|
|
||||||
|
|
||||||
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white transition-colors"
|
|
||||||
>
|
|
||||||
변경
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDialogProps) {
|
export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDialogProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
@@ -154,16 +45,16 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
}
|
}
|
||||||
}, [isOpen, userId]);
|
}, [isOpen, userId]);
|
||||||
|
|
||||||
// ESC 키로 닫기 (비밀번호 다이얼로그가 열려있지 않을 때만)
|
// ESC 키로 닫기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape' && isOpen && !showPasswordDialog) {
|
if (e.key === 'Escape' && isOpen) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [isOpen, onClose, showPasswordDialog]);
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
const loadUserInfo = async () => {
|
const loadUserInfo = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -206,19 +97,6 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangePassword = async (oldPassword: string, newPassword: string) => {
|
|
||||||
try {
|
|
||||||
const response = await comms.changePassword(oldPassword, newPassword);
|
|
||||||
if (response.Success) {
|
|
||||||
setMessage({ type: 'success', text: '비밀번호가 변경되었습니다.' });
|
|
||||||
} else {
|
|
||||||
setMessage({ type: 'error', text: response.Message || '비밀번호 변경에 실패했습니다.' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setMessage({ type: 'error', text: '비밀번호 변경 중 오류가 발생했습니다.' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (field: keyof UserInfoDetail, value: string | boolean) => {
|
const handleInputChange = (field: keyof UserInfoDetail, value: string | boolean) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
@@ -229,19 +107,19 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<>
|
<>
|
||||||
<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-full max-w-2xl max-h-[90vh] overflow-hidden">
|
<div className="dialog-container rounded-xl w-full max-w-2xl max-h-[90vh] 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">
|
||||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
<h2 className="dialog-title">
|
||||||
<User className="w-6 h-6" />
|
<User className="w-6 h-6" />
|
||||||
사용자 정보
|
사용자 정보
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/60 hover:text-white transition-colors"
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-6 h-6" />
|
<X className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -254,216 +132,211 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<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="bg-white/5 rounded-lg p-4 mb-6">
|
{/* 좌측 컬럼: 기본 정보 */}
|
||||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
<div className="space-y-4 overflow-y-auto pr-2 custom-scrollbar">
|
||||||
<Palette className="w-4 h-4 text-purple-400" />
|
<div className="grid grid-cols-2 gap-4">
|
||||||
테마 설정
|
<div>
|
||||||
</h3>
|
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<User className="w-4 h-4" />
|
||||||
<button
|
사원번호
|
||||||
onClick={() => handleThemeChange('dark')}
|
</label>
|
||||||
className={clsx(
|
<input
|
||||||
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
|
type="text"
|
||||||
theme === 'dark'
|
value={formData.Id}
|
||||||
? 'border-blue-500 bg-blue-500/20 text-white'
|
disabled
|
||||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50"
|
||||||
)}
|
/>
|
||||||
>
|
</div>
|
||||||
<div className="w-full h-2 rounded-full bg-gradient-to-r from-blue-600 to-purple-600"></div>
|
<div>
|
||||||
<span className="text-sm font-medium">기본 (Dark)</span>
|
<label className="block text-sm text-white/70 mb-1">이름</label>
|
||||||
</button>
|
<input
|
||||||
<button
|
type="text"
|
||||||
onClick={() => handleThemeChange('PSH_PINK')}
|
value={formData.NameK}
|
||||||
className={clsx(
|
onChange={(e) => handleInputChange('NameK', e.target.value)}
|
||||||
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
|
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||||
theme === 'PSH_PINK'
|
placeholder="이름"
|
||||||
? '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>
|
||||||
)}
|
<div>
|
||||||
>
|
<label className="block text-sm text-white/70 mb-1">영문이름</label>
|
||||||
<div className="w-full h-2 rounded-full bg-gradient-to-r from-pink-500 to-rose-500"></div>
|
<input
|
||||||
<span className="text-sm font-medium">발랄한 핑크</span>
|
type="text"
|
||||||
</button>
|
value={formData.NameE}
|
||||||
<button
|
onChange={(e) => handleInputChange('NameE', e.target.value)}
|
||||||
onClick={() => handleThemeChange('JW_SKY')}
|
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||||
className={clsx(
|
placeholder="English Name"
|
||||||
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
|
/>
|
||||||
theme === 'JW_SKY'
|
</div>
|
||||||
? 'border-sky-500 bg-sky-500/20 text-white'
|
<div>
|
||||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||||
)}
|
<Briefcase className="w-4 h-4" />
|
||||||
>
|
직책
|
||||||
<div className="w-full h-2 rounded-full bg-gradient-to-r from-sky-400 to-blue-500"></div>
|
</label>
|
||||||
<span className="text-sm font-medium">시원한 하늘</span>
|
<input
|
||||||
</button>
|
type="text"
|
||||||
</div>
|
value={formData.Grade}
|
||||||
</div>
|
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 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" />
|
|
||||||
사원번호
|
|
||||||
</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">
|
|
||||||
<Building2 className="w-4 h-4" />
|
|
||||||
부서
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.Dept}
|
|
||||||
onChange={(e) => handleInputChange('Dept', 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">
|
|
||||||
<Briefcase className="w-4 h-4" />
|
|
||||||
직책
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.Grade}
|
|
||||||
onChange={(e) => handleInputChange('Grade', e.target.value)}
|
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
|
||||||
placeholder="직책"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-white/70 mb-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>
|
|
||||||
|
|
||||||
{/* 이메일 */}
|
<div>
|
||||||
<div>
|
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">공정</label>
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
<input
|
||||||
<Mail className="w-4 h-4" />
|
type="text"
|
||||||
이메일
|
value={formData.Process}
|
||||||
</label>
|
onChange={(e) => handleInputChange('Process', e.target.value)}
|
||||||
<input
|
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||||
type="email"
|
placeholder="공정"
|
||||||
value={formData.Email}
|
/>
|
||||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
</div>
|
||||||
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>
|
||||||
<div>
|
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
<Mail className="w-4 h-4" />
|
||||||
<Calendar className="w-4 h-4" />
|
이메일
|
||||||
입사일
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="email"
|
||||||
type="text"
|
value={formData.Email}
|
||||||
value={formData.DateIn}
|
onChange={(e) => handleInputChange('Email', 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 px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
placeholder="email@example.com"
|
||||||
placeholder="YYYY-MM-DD"
|
/>
|
||||||
/>
|
</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>
|
||||||
|
<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>
|
||||||
<div>
|
|
||||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
{/* 우측 컬럼: 테마 설정 + 비고 */}
|
||||||
<Calendar className="w-4 h-4" />
|
<div className="flex flex-col h-full gap-4">
|
||||||
퇴사일
|
{/* 테마 설정 섹션 */}
|
||||||
</label>
|
<div className="bg-white/5 rounded-lg p-4">
|
||||||
<input
|
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||||
type="text"
|
<Palette className="w-4 h-4 text-purple-400" />
|
||||||
value={formData.DateO}
|
테마 설정
|
||||||
onChange={(e) => handleInputChange('DateO', e.target.value)}
|
</h3>
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
<div className="grid grid-cols-1 gap-3">
|
||||||
placeholder="YYYY-MM-DD"
|
<button
|
||||||
/>
|
onClick={() => handleThemeChange('dark')}
|
||||||
|
className={clsx(
|
||||||
|
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||||
|
theme === 'dark'
|
||||||
|
? 'border-blue-500 bg-blue-500/20 text-white'
|
||||||
|
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 border border-white/20 shrink-0"></div>
|
||||||
|
<span className="text-sm font-medium">기본 (Dark)</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleThemeChange('PSH_PINK')}
|
||||||
|
className={clsx(
|
||||||
|
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||||
|
theme === 'PSH_PINK'
|
||||||
|
? 'border-pink-500 bg-pink-500/20 text-white'
|
||||||
|
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-pink-500 to-rose-500 border border-white/20 shrink-0"></div>
|
||||||
|
<span className="text-sm font-medium">발랄한 핑크</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleThemeChange('JW_SKY')}
|
||||||
|
className={clsx(
|
||||||
|
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||||
|
theme === 'JW_SKY'
|
||||||
|
? 'border-sky-500 bg-sky-500/20 text-white'
|
||||||
|
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-sky-400 to-blue-500 border border-white/20 shrink-0"></div>
|
||||||
|
<span className="text-sm font-medium">시원한 하늘</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 비고: 남은 높이 채우기 */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
비고
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.Memo}
|
||||||
|
onChange={(e) => handleInputChange('Memo', e.target.value)}
|
||||||
|
className="flex-1 w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 resize-none"
|
||||||
|
placeholder="비고"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 비고 */}
|
|
||||||
<div>
|
|
||||||
<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)}
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 resize-none"
|
|
||||||
placeholder="비고"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 옵션 체크박스 */}
|
|
||||||
<div className="flex flex-wrap gap-6">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 메시지 */}
|
{/* 메시지 */}
|
||||||
{message && (
|
{message && (
|
||||||
<div
|
<div
|
||||||
@@ -480,14 +353,7 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10">
|
<div className="dialog-footer flex items-center justify-end px-6 py-4">
|
||||||
<button
|
|
||||||
onClick={() => setShowPasswordDialog(true)}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-yellow-600/20 hover:bg-yellow-600/30 text-yellow-400 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<Key className="w-4 h-4" />
|
|
||||||
비밀번호 변경
|
|
||||||
</button>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -511,13 +377,9 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div >
|
||||||
|
|
||||||
<PasswordDialog
|
</>,
|
||||||
isOpen={showPasswordDialog}
|
document.body
|
||||||
onClose={() => setShowPasswordDialog(false)}
|
|
||||||
onConfirm={handleChangePassword}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,83 +4,191 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
/* Default Dark Theme (Purple/Blue base) */
|
/* =========================================================================
|
||||||
--bg-main: #111827; /* gray-900 like */
|
1. Default Dark Theme (Purple/Blue base) - "기본 어두운 테마"
|
||||||
--bg-paper: #1f2937; /* gray-800 like */
|
========================================================================= */
|
||||||
--bg-gradient-start: #1e3a8a; /* blue-900 */
|
|
||||||
--bg-gradient-mid: #581c87; /* purple-900 */
|
|
||||||
--bg-gradient-end: #312e81; /* indigo-900 */
|
|
||||||
|
|
||||||
--text-primary: #f9fafb; /* gray-50 */
|
|
||||||
--text-secondary: #9ca3af; /* gray-400 */
|
|
||||||
--text-muted: #6b7280; /* gray-500 */
|
|
||||||
|
|
||||||
--border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
--border-base: #374151; /* gray-700 */
|
|
||||||
|
|
||||||
--color-primary: 59, 130, 246; /* blue-500 (RGB) */
|
/* [Background Colors] - 메인 배경 및 컴포넌트 배경 */
|
||||||
--color-primary-light: 96, 165, 250; /* blue-400 */
|
--bg-main: #111827;
|
||||||
--color-primary-dark: 37, 99, 235; /* blue-600 */
|
/* 전체 앱의 기본 배경색 (Deep Dark Blue) */
|
||||||
|
--bg-paper: #1f2937;
|
||||||
--color-accent: 139, 92, 246; /* violet-500 */
|
/* 카드, 모달 등 컨테이너의 배경색 */
|
||||||
|
|
||||||
|
/* [Background Gradients] - 배경에 깊이감을 더하는 그라디언트 색상 */
|
||||||
|
--bg-gradient-start: #1e3a8a;
|
||||||
|
/* 시작점: Deep Blue */
|
||||||
|
--bg-gradient-mid: #581c87;
|
||||||
|
/* 중간점: Purple (보라빛 포인트) */
|
||||||
|
--bg-gradient-end: #312e81;
|
||||||
|
/* 끝점: Indigo */
|
||||||
|
|
||||||
|
/* [Text Colors] - 텍스트 계층 구조 */
|
||||||
|
--text-primary: #f9fafb;
|
||||||
|
/* 주요 텍스트 (거의 흰색, 가독성 최우선) */
|
||||||
|
--text-secondary: #9ca3af;
|
||||||
|
/* 보조 텍스트 (연한 회색, 설명 문구 등) */
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
/* 비활성 텍스트 (짙은 회색, 힌트 등) */
|
||||||
|
|
||||||
|
/* [Border Colors] - 테두리 및 구분선 */
|
||||||
|
--border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
/* 유리 효과(Glass)에 사용되는 반투명 테두리 */
|
||||||
|
--border-base: #374151;
|
||||||
|
/* 일반적인 불투명 테두리 (Solid Border) */
|
||||||
|
|
||||||
|
/* [Primary Brand Colors] - 포인트 컬러 (RGB 값으로 정의하여 투명도 조절 용이) */
|
||||||
|
/* Tailwind에서 alpha 값을 조절하기 위해 R, G, B 숫자로 정의함 */
|
||||||
|
--color-primary: 59, 130, 246;
|
||||||
|
/* Main: Blue-500 */
|
||||||
|
--color-primary-light: 96, 165, 250;
|
||||||
|
/* Light: Blue-400 (Hover 시 등) */
|
||||||
|
--color-primary-dark: 37, 99, 235;
|
||||||
|
/* Dark: Blue-600 (Click 시 등) */
|
||||||
|
|
||||||
|
--color-accent: 139, 92, 246;
|
||||||
|
/* Accent: Violet-500 (강조용 보라색) */
|
||||||
|
|
||||||
|
/* [Glassmorphism Tokens] - 유리 효과 제어 변수 */
|
||||||
|
/* 테마별로 투명도와 색상을 다르게 적용하여 가독성을 확보함 */
|
||||||
--glass-bg: rgba(255, 255, 255, 0.25);
|
--glass-bg: rgba(255, 255, 255, 0.25);
|
||||||
|
/* 유리 배경의 기본 투명도 */
|
||||||
--glass-border: rgba(255, 255, 255, 0.18);
|
--glass-border: rgba(255, 255, 255, 0.18);
|
||||||
|
/* 유리 테두리의 투명도 */
|
||||||
|
|
||||||
|
/* [Typography Tokens] *NEW* - 리스트 헤더 등 공통 타이포그래피 표준 */
|
||||||
|
/* 모든 리스트(업무일지, 월별근무표 등)의 헤더 스타일을 이곳에서 통합 관리합니다. */
|
||||||
|
--fs-list-header: 0.6875rem;
|
||||||
|
/* 11px: 목록 헤더 폰트 크기 (작고 깔끔하게) */
|
||||||
|
/* 11px: 목록 헤더 폰트 크기 (작고 깔끔하게) */
|
||||||
|
--fs-list-item: 0.8125rem;
|
||||||
|
/* 13px: 목록 본문 폰트 크기 */
|
||||||
|
--fw-list-header: 500;
|
||||||
|
/* 500(Medium): 목록 헤더 폰트 굵기 (너무 두껍지 않게) */
|
||||||
|
--header-muted-opacity: 0.5;
|
||||||
|
/* 0.5: 목록 헤더 텍스트 투명도 (본문보다 연하게 처리) */
|
||||||
|
|
||||||
|
/* [Dialog Tokens] *NEW* - 다이얼로그 전용 스타일 */
|
||||||
|
--dialog-bg: var(--glass-bg);
|
||||||
|
--dialog-border: var(--glass-border);
|
||||||
|
--dialog-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
--dialog-header-bg: transparent;
|
||||||
|
--dialog-header-border: var(--glass-border);
|
||||||
|
|
||||||
|
--dialog-footer-bg: transparent;
|
||||||
|
--dialog-footer-border: var(--glass-border);
|
||||||
|
|
||||||
|
--dialog-title-color: #f9fafb;
|
||||||
|
--dialog-title-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-pink {
|
.theme-pink {
|
||||||
/* "PSH_PINK" Theme - Magenta & Pink */
|
/* "PSH_PINK" Theme - Magenta & Pink */
|
||||||
/* Background: Pinkish base */
|
/* Background: Pinkish base */
|
||||||
--bg-main: #501025; /* Deep pink/wine */
|
--bg-main: #501025;
|
||||||
--bg-paper: #1f0510; /* Very dark pink */
|
/* Deep pink/wine */
|
||||||
--bg-gradient-start: #be185d; /* pink-700 */
|
--bg-paper: #1f0510;
|
||||||
--bg-gradient-mid: #9d174d; /* pink-800 */
|
/* Very dark pink */
|
||||||
--bg-gradient-end: #831843; /* pink-900 */
|
--bg-gradient-start: #be185d;
|
||||||
|
/* pink-700 */
|
||||||
|
--bg-gradient-mid: #9d174d;
|
||||||
|
/* pink-800 */
|
||||||
|
--bg-gradient-end: #831843;
|
||||||
|
/* pink-900 */
|
||||||
|
|
||||||
--text-primary: #fce7f3; /* pink-100 */
|
--text-primary: #fce7f3;
|
||||||
--text-secondary: #fbcfe8; /* pink-200 */
|
/* pink-100 */
|
||||||
--text-muted: #f9a8d4; /* pink-300 */
|
--text-secondary: #fbcfe8;
|
||||||
|
/* pink-200 */
|
||||||
|
--text-muted: #f9a8d4;
|
||||||
|
/* pink-300 */
|
||||||
|
|
||||||
--border-color: rgba(255, 192, 203, 0.4); /* Pink border */
|
--border-color: rgba(255, 192, 203, 0.4);
|
||||||
--border-base: #9d174d; /* pink-800 */
|
/* Pink border */
|
||||||
|
--border-base: #9d174d;
|
||||||
|
/* pink-800 */
|
||||||
|
|
||||||
/* Primary: Magenta (#FF00FF -> 255, 0, 255) */
|
/* Primary: Magenta (#FF00FF -> 255, 0, 255) */
|
||||||
--color-primary: 255, 0, 255; /* Magenta */
|
--color-primary: 255, 0, 255;
|
||||||
--color-primary-light: 255, 105, 180; /* HotPink */
|
/* Magenta */
|
||||||
--color-primary-dark: 199, 21, 133; /* MediumVioletRed */
|
--color-primary-light: 255, 105, 180;
|
||||||
|
/* HotPink */
|
||||||
|
--color-primary-dark: 199, 21, 133;
|
||||||
|
/* MediumVioletRed */
|
||||||
|
|
||||||
/* Accent: Pink (#FFC0CB -> 255, 192, 203) */
|
/* Accent: Pink (#FFC0CB -> 255, 192, 203) */
|
||||||
--color-accent: 255, 192, 203; /* Pink */
|
--color-accent: 255, 192, 203;
|
||||||
|
/* Pink */
|
||||||
|
|
||||||
--glass-bg: rgba(255, 0, 255, 0.1);
|
--glass-bg: rgba(255, 0, 255, 0.1);
|
||||||
--glass-border: rgba(255, 192, 203, 0.4);
|
--glass-border: rgba(255, 192, 203, 0.4);
|
||||||
|
|
||||||
|
/* Dialog Pink Overrides */
|
||||||
|
--dialog-bg: rgba(80, 16, 37, 0.9);
|
||||||
|
--dialog-border: rgba(236, 72, 153, 0.3);
|
||||||
|
--dialog-shadow: 0 25px 50px -12px rgba(131, 24, 67, 0.3);
|
||||||
|
|
||||||
|
--dialog-header-bg: linear-gradient(to right, rgba(236, 72, 153, 0.1), transparent);
|
||||||
|
--dialog-header-border: rgba(236, 72, 153, 0.2);
|
||||||
|
|
||||||
|
--dialog-footer-bg: rgba(236, 72, 153, 0.05);
|
||||||
|
--dialog-footer-border: rgba(236, 72, 153, 0.2);
|
||||||
|
|
||||||
|
--dialog-title-color: #fce7f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-sky {
|
.theme-sky {
|
||||||
/* "JW_SKY" Theme - Sky Blue & White/Blue */
|
/* "JW_SKY" Theme - Sky Blue & White/Blue */
|
||||||
--bg-main: #0c4a6e; /* sky-900 */
|
--bg-main: #0c4a6e;
|
||||||
--bg-paper: #082f49; /* sky-950 */
|
/* sky-900 */
|
||||||
--bg-gradient-start: #38bdf8; /* sky-400 */
|
--bg-paper: #082f49;
|
||||||
--bg-gradient-mid: #0ea5e9; /* sky-500 */
|
/* sky-950 */
|
||||||
--bg-gradient-end: #0284c7; /* sky-600 */
|
--bg-gradient-start: #38bdf8;
|
||||||
|
/* sky-400 */
|
||||||
|
--bg-gradient-mid: #0ea5e9;
|
||||||
|
/* sky-500 */
|
||||||
|
--bg-gradient-end: #0284c7;
|
||||||
|
/* sky-600 */
|
||||||
|
|
||||||
--text-primary: #f0f9ff; /* sky-50 */
|
--text-primary: #f0f9ff;
|
||||||
--text-secondary: #bae6fd; /* sky-200 */
|
/* sky-50 */
|
||||||
--text-muted: #7dd3fc; /* sky-300 */
|
--text-secondary: #bae6fd;
|
||||||
|
/* sky-200 */
|
||||||
|
--text-muted: #7dd3fc;
|
||||||
|
/* sky-300 */
|
||||||
|
|
||||||
--border-color: rgba(186, 230, 253, 0.3); /* sky-200 / 0.3 */
|
--border-color: rgba(186, 230, 253, 0.3);
|
||||||
--border-base: #0369a1; /* sky-700 */
|
/* sky-200 / 0.3 */
|
||||||
|
--border-base: #0369a1;
|
||||||
|
/* sky-700 */
|
||||||
|
|
||||||
|
--color-primary: 14, 165, 233;
|
||||||
|
/* sky-500 */
|
||||||
|
--color-primary-light: 56, 189, 248;
|
||||||
|
/* sky-400 */
|
||||||
|
--color-primary-dark: 2, 132, 199;
|
||||||
|
/* sky-600 */
|
||||||
|
|
||||||
|
--color-accent: 255, 255, 255;
|
||||||
|
/* White accent */
|
||||||
|
|
||||||
--color-primary: 14, 165, 233; /* sky-500 */
|
|
||||||
--color-primary-light: 56, 189, 248; /* sky-400 */
|
|
||||||
--color-primary-dark: 2, 132, 199; /* sky-600 */
|
|
||||||
|
|
||||||
--color-accent: 255, 255, 255; /* White accent */
|
|
||||||
|
|
||||||
--glass-bg: rgba(255, 255, 255, 0.2);
|
--glass-bg: rgba(255, 255, 255, 0.2);
|
||||||
--glass-border: rgba(255, 255, 255, 0.2);
|
--glass-border: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
/* Dialog Sky Overrides */
|
||||||
|
--dialog-bg: rgba(8, 47, 73, 0.9);
|
||||||
|
--dialog-border: rgba(14, 165, 233, 0.3);
|
||||||
|
--dialog-shadow: 0 25px 50px -12px rgba(12, 74, 110, 0.3);
|
||||||
|
|
||||||
|
--dialog-header-bg: linear-gradient(to right, rgba(14, 165, 233, 0.1), transparent);
|
||||||
|
--dialog-header-border: rgba(14, 165, 233, 0.2);
|
||||||
|
|
||||||
|
--dialog-footer-bg: rgba(14, 165, 233, 0.05);
|
||||||
|
--dialog-footer-border: rgba(14, 165, 233, 0.2);
|
||||||
|
|
||||||
|
--dialog-title-color: #f0f9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg-main);
|
background-color: var(--bg-main);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -145,3 +253,32 @@ select:focus option:checked {
|
|||||||
select option:hover {
|
select option:hover {
|
||||||
background-color: var(--bg-gradient-mid);
|
background-color: var(--bg-gradient-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dialog Utility Classes */
|
||||||
|
.dialog-container {
|
||||||
|
background: var(--dialog-bg);
|
||||||
|
border: 1px solid var(--dialog-border);
|
||||||
|
box-shadow: var(--dialog-shadow);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
background: var(--dialog-header-bg);
|
||||||
|
border-bottom: 1px solid var(--dialog-header-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
background: var(--dialog-footer-bg);
|
||||||
|
border-top: 1px solid var(--dialog-footer-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
color: var(--dialog-title-color);
|
||||||
|
font-weight: 600;
|
||||||
|
/* semibold */
|
||||||
|
font-size: var(--dialog-title-size);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
/* gap-2 */
|
||||||
|
}
|
||||||
@@ -116,10 +116,10 @@ export function Customs() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 헤더 (메모장 디자인 통일) */}
|
{/* 컬럼 헤더 (메모장 디자인 통일) */}
|
||||||
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4">
|
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4 text-list-header text-white/opacity-header-muted font-list-header uppercase">
|
||||||
<div className="w-8"></div>
|
<div className="w-8"></div>
|
||||||
<div className="flex-1 text-xs font-medium text-white/70 uppercase tracking-tighter">업체명 / 구분</div>
|
<div className="flex-1 uppercase">업체명 / 구분</div>
|
||||||
<div className="flex items-center gap-6 shrink-0 text-xs font-medium text-white/70 uppercase tracking-tighter">
|
<div className="flex items-center gap-6 shrink-0 uppercase">
|
||||||
<div className="w-24">대표자</div>
|
<div className="w-24">대표자</div>
|
||||||
<div className="w-36">연락처 / 이메일</div>
|
<div className="w-36">연락처 / 이메일</div>
|
||||||
<div className="w-24">담당자</div>
|
<div className="w-24">담당자</div>
|
||||||
|
|||||||
@@ -180,258 +180,287 @@ export function ItemsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col space-y-4 animate-fade-in pb-4">
|
||||||
{/* 헤더 */}
|
{/* 메인 컨테이너 */}
|
||||||
<div className="glass-effect rounded-xl p-4 mb-4">
|
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
|
||||||
<select
|
|
||||||
value={selectedCategory}
|
|
||||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
||||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white min-w-[150px]"
|
|
||||||
>
|
|
||||||
<option value="all" className="bg-slate-800">-- 전체 --</option>
|
|
||||||
{categories.map((c) => (
|
|
||||||
<option key={c} value={c} className="bg-slate-800">{c}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div className="flex-1 flex items-center gap-2">
|
|
||||||
<div className="relative flex-1 max-w-xs">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchKey}
|
|
||||||
onChange={(e) => setSearchKey(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && loadItems()}
|
|
||||||
placeholder="품목명/SID/모델 검색..."
|
|
||||||
className="w-full pl-9 pr-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={loadItems}
|
|
||||||
className="flex items-center gap-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white transition-colors"
|
|
||||||
>
|
|
||||||
<Search className="w-4 h-4" />
|
|
||||||
검색
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={filter}
|
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
|
||||||
placeholder="필터..."
|
|
||||||
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 w-32"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleAddNew}
|
|
||||||
className="flex items-center gap-1 px-3 py-2 bg-green-600 hover:bg-green-700 rounded-lg text-white transition-colors"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
추가
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 메인 컨텐츠: 목록 + 상세 패널 */}
|
|
||||||
<div className="flex-1 flex gap-4 min-h-0">
|
<div className="flex-1 flex gap-4 min-h-0">
|
||||||
{/* 품목 목록 (좌측) */}
|
{/* 통합 리스트 카드 (좌측) */}
|
||||||
<div className="flex-1 glass-effect rounded-xl overflow-hidden flex flex-col">
|
<div className="flex-1 glass-effect rounded-3xl overflow-hidden shadow-2xl border border-white/10 flex flex-col">
|
||||||
<div className="p-4 border-b border-white/10 flex items-center gap-2">
|
{/* 헤더 영역 */}
|
||||||
<Package className="w-5 h-5 text-white/70" />
|
<div className="px-6 py-4 border-b border-white/10 flex flex-col md:flex-row items-center justify-between gap-4 bg-white/[0.02] shrink-0">
|
||||||
<h2 className="text-lg font-semibold text-white">품목 목록</h2>
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-white/50">({filteredItems.length}건)</span>
|
<div className="p-2 bg-primary-500/20 rounded-lg">
|
||||||
|
<Package className="w-5 h-5 text-primary-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h3 className="text-lg font-bold text-white tracking-tight">품목 관리</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 bg-white/5 rounded-full border border-white/5 ml-2">
|
||||||
|
<span className="text-white/40 text-xs font-medium uppercase">Total</span>
|
||||||
|
<span className="text-primary-400 text-sm font-bold">{items.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 w-full md:w-auto">
|
||||||
|
|
||||||
|
|
||||||
|
<div className="relative group">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/40 group-focus-within:text-primary-400 transition-colors" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchKey}
|
||||||
|
onChange={(e) => setSearchKey(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && loadItems()}
|
||||||
|
placeholder="품목명, SID, 모델..."
|
||||||
|
className="w-64 bg-white/5 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-sm text-white placeholder-white/20 focus:outline-none focus:ring-1 focus:ring-primary-500/50 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative group">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-white/40" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
placeholder="결과 내 필터"
|
||||||
|
className="w-40 bg-white/5 border border-white/10 rounded-xl pl-8 pr-3 py-2 text-xs text-white placeholder-white/20 focus:outline-none focus:border-primary-500/30 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={loadItems}
|
||||||
|
className="p-2 bg-primary-500 hover:bg-primary-600 border border-white/20 rounded-xl text-white transition-all shadow-lg shadow-primary-500/20 active:scale-95"
|
||||||
|
title="검색 실행"
|
||||||
|
>
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleAddNew}
|
||||||
|
className="p-2 bg-success-500 hover:bg-success-600 border border-white/20 rounded-xl text-white transition-all shadow-lg shadow-success-500/20 active:scale-95 ml-2"
|
||||||
|
title="신규 품목 추가"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
|
|
||||||
|
{/* 메인 리스트 헤더 (Note style) */}
|
||||||
|
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-list-header font-list-header text-white/opacity-header-muted uppercase shrink-0">
|
||||||
|
<div className="w-28 px-4">SID</div>
|
||||||
|
<div className="w-24 px-4 text-center">분류</div>
|
||||||
|
<div className="flex-1 px-4">품명</div>
|
||||||
|
<div className="flex-1 px-4">모델</div>
|
||||||
|
<div className="w-24 px-4 text-right">단가</div>
|
||||||
|
<div className="w-32 px-4">공급처</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 리스트 본문 */}
|
||||||
|
<div className="flex-1 overflow-y-auto custom-scrollbar divide-y divide-white/5">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center h-32">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500"></div>
|
||||||
|
</div>
|
||||||
|
) : filteredItems.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-white/40">
|
||||||
|
<Package className="w-12 h-12 mb-4 opacity-20" />
|
||||||
|
<p>{items.length === 0 ? '검색어를 입력하세요.' : '검색 결과가 없습니다.'}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-sm">
|
filteredItems.map((item) => (
|
||||||
<thead className="bg-white/5 sticky top-0">
|
<div
|
||||||
<tr>
|
key={item.idx || 'new'}
|
||||||
<th className="px-3 py-2 text-left font-medium text-white/70 w-28">SID</th>
|
onClick={() => handleRowClick(item)}
|
||||||
<th className="px-3 py-2 text-left font-medium text-white/70 w-20">분류</th>
|
className={clsx(
|
||||||
<th className="px-3 py-2 text-left font-medium text-white/70">품명</th>
|
'flex items-center px-6 py-3 hover:bg-white/[0.02] transition-colors cursor-pointer group text-[length:var(--fs-list-item)]',
|
||||||
<th className="px-3 py-2 text-left font-medium text-white/70">모델</th>
|
item.disable && 'opacity-50 grayscale',
|
||||||
<th className="px-3 py-2 text-right font-medium text-white/70 w-24">단가</th>
|
selectedItemDetail?.idx === item.idx && 'bg-primary-500/10'
|
||||||
<th className="px-3 py-2 text-left font-medium text-white/70 w-24">공급처</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-white/5">
|
|
||||||
{filteredItems.map((item) => (
|
|
||||||
<tr
|
|
||||||
key={item.idx || 'new'}
|
|
||||||
onClick={() => handleRowClick(item)}
|
|
||||||
onDoubleClick={() => handleRowDoubleClick(item)}
|
|
||||||
className={clsx(
|
|
||||||
'hover:bg-white/10 transition-colors cursor-pointer',
|
|
||||||
item.disable && 'opacity-50',
|
|
||||||
selectedItemDetail?.idx === item.idx && 'bg-blue-600/30'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<td className="px-3 py-2 text-white font-mono">{item.sid}</td>
|
|
||||||
<td className="px-3 py-2 text-white/70">{item.cate}</td>
|
|
||||||
<td className="px-3 py-2 text-white">{item.name}</td>
|
|
||||||
<td className="px-3 py-2 text-white/70">{item.model}</td>
|
|
||||||
<td className="px-3 py-2 text-white text-right">{(item.price ?? 0).toLocaleString()}</td>
|
|
||||||
<td className="px-3 py-2 text-white/70">{item.supply}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{filteredItems.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="px-4 py-8 text-center text-white/50">
|
|
||||||
{items.length === 0 ? '검색어를 입력하고 검색 버튼을 클릭하세요.' : '검색 결과가 없습니다.'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
>
|
||||||
</table>
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // 부모 Row의 클릭 이벤트(상세보기) 방지하고
|
||||||
|
handleRowDoubleClick(item); // 편집 다이얼로그 오픈 (함수명은 DoubleClick이지만 동작은 클릭으로 변경)
|
||||||
|
}}
|
||||||
|
className="w-28 px-4 font-mono text-[yellow] group-hover:text-[#ff7f66] transition-colors underline decoration-primary-500/50 hover:decoration-primary-500 cursor-pointer text-[15px]"
|
||||||
|
>
|
||||||
|
{item.sid}
|
||||||
|
</div>
|
||||||
|
<div className="w-24 px-4 text-center">
|
||||||
|
<span className="bg-white/10 px-2 py-0.5 rounded text-xs text-white/70">
|
||||||
|
{item.cate}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-4 text-white font-medium group-hover:text-white transition-colors">
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-4 text-white/60">
|
||||||
|
{item.model}
|
||||||
|
</div>
|
||||||
|
<div className="w-24 px-4 text-right text-white/90 font-mono">
|
||||||
|
{(item.price ?? 0).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<div className="w-32 px-4 text-white/60 truncate">
|
||||||
|
{item.supply}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 상세 패널 (우측) */}
|
{/* 상세 패널 (우측) - Glass Effect & Typography Update */}
|
||||||
<div className="w-80 flex flex-col gap-4">
|
<div className="w-80 flex flex-col gap-4">
|
||||||
|
|
||||||
{/* 이미지 */}
|
{/* 이미지 */}
|
||||||
<div className="glass-effect rounded-xl p-3">
|
<div className="glass-effect rounded-3xl p-4 border border-white/10">
|
||||||
<div className="flex items-center gap-2 mb-2 border-b border-white/10 pb-2">
|
<div className="flex items-center gap-2 mb-3 border-b border-white/10 pb-2">
|
||||||
<Image className="w-4 h-4 text-white/70" />
|
<Image className="w-4 h-4 text-primary-400" />
|
||||||
<h3 className="text-sm font-medium text-white">품목 이미지</h3>
|
<h3 className="text-sm font-bold text-white tracking-tight">품목 이미지</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="aspect-[4/3] bg-white/5 rounded-lg flex items-center justify-center overflow-hidden">
|
<div className="aspect-[4/3] bg-black/20 rounded-xl flex items-center justify-center overflow-hidden border border-white/5 group relative">
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white/50"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-white/30"></div>
|
||||||
) : itemImage ? (
|
) : itemImage ? (
|
||||||
<img
|
<>
|
||||||
src={`data:image/jpeg;base64,${itemImage}`}
|
<img
|
||||||
alt="품목 이미지"
|
src={`data:image/jpeg;base64,${itemImage}`}
|
||||||
className="max-w-full max-h-full object-contain"
|
alt="품목 이미지"
|
||||||
/>
|
className="max-w-full max-h-full object-contain transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors pointer-events-none" />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-white/30 text-sm">이미지 없음</span>
|
<div className="flex flex-col items-center gap-2 text-white/20">
|
||||||
|
<Image className="w-8 h-8 opacity-50" />
|
||||||
|
<span className="text-xs">이미지 없음</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 공급처 담당자 */}
|
{/* 공급처 담당자 */}
|
||||||
<div className="glass-effect rounded-xl p-3">
|
<div className="glass-effect rounded-3xl p-4 border border-white/10">
|
||||||
<div className="flex items-center gap-2 mb-2 border-b border-white/10 pb-2">
|
<div className="flex items-center gap-2 mb-3 border-b border-white/10 pb-2">
|
||||||
<Users className="w-4 h-4 text-white/70" />
|
<Users className="w-4 h-4 text-accent-400" />
|
||||||
<h3 className="text-sm font-medium text-white">
|
<h3 className="text-sm font-bold text-white tracking-tight">
|
||||||
{selectedItemDetail?.supply ? `[${selectedItemDetail.supply}] 담당자` : '공급처 담당자'}
|
{selectedItemDetail?.supply ? `[${selectedItemDetail.supply}] 담당자` : '공급처 담당자'}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-32 overflow-auto">
|
<div className="max-h-32 overflow-y-auto custom-scrollbar">
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div className="text-center py-2">
|
<div className="text-center py-4">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/50 mx-auto"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/30 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
) : supplierStaff.length > 0 ? (
|
) : supplierStaff.length > 0 ? (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead className="bg-white/5">
|
<thead className="bg-white/5 sticky top-0 backdrop-blur-sm">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-2 py-1 text-left text-white/60">이름</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">이름</th>
|
||||||
<th className="px-2 py-1 text-left text-white/60">연락처</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">연락처</th>
|
||||||
<th className="px-2 py-1 text-left text-white/60">이메일</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">이메일</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/5">
|
<tbody className="divide-y divide-white/5">
|
||||||
{supplierStaff.map((staff) => (
|
{supplierStaff.map((staff) => (
|
||||||
<tr key={staff.idx}>
|
<tr key={staff.idx} className="hover:bg-white/5 transition-colors">
|
||||||
<td className="px-2 py-1 text-white">{staff.name}</td>
|
<td className="px-2 py-1.5 text-white/90">{staff.name}</td>
|
||||||
<td className="px-2 py-1 text-white/70">{staff.tel}</td>
|
<td className="px-2 py-1.5 text-white/70">{staff.tel}</td>
|
||||||
<td className="px-2 py-1 text-white/70 truncate max-w-[100px]" title={staff.email}>{staff.email}</td>
|
<td className="px-2 py-1.5 text-white/70 truncate max-w-[100px]" title={staff.email}>{staff.email}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-white/30 text-xs py-2">담당자 정보 없음</div>
|
<div className="text-center text-white/30 text-xs py-4 flex flex-col items-center gap-1">
|
||||||
|
<Users className="w-6 h-6 opacity-20" />
|
||||||
|
<span>담당자 정보 없음</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 최근 입고내역 */}
|
{/* 최근 입고내역 */}
|
||||||
<div className="glass-effect rounded-xl p-3 flex-1 min-h-0 flex flex-col">
|
<div className="glass-effect rounded-3xl p-4 border border-white/10 flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 mb-2 border-b border-white/10 pb-2">
|
<div className="flex items-center gap-2 mb-3 border-b border-white/10 pb-2 shrink-0">
|
||||||
<TrendingDown className="w-4 h-4 text-green-400" />
|
<TrendingDown className="w-4 h-4 text-success-400" />
|
||||||
<h3 className="text-sm font-medium text-white">최근 입고내역</h3>
|
<h3 className="text-sm font-bold text-white tracking-tight">최근 입고내역</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div className="text-center py-2">
|
<div className="text-center py-4">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/50 mx-auto"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/30 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
) : incomingHistory.length > 0 ? (
|
) : incomingHistory.length > 0 ? (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead className="bg-white/5 sticky top-0">
|
<thead className="bg-white/5 sticky top-0 backdrop-blur-sm z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left text-white/60">일자</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">일자</th>
|
||||||
<th className="px-1 py-1 text-left text-white/60">요청자</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">요청자</th>
|
||||||
<th className="px-1 py-1 text-right text-white/60">수량</th>
|
<th className="px-2 py-1.5 text-right text-list-header font-list-header text-white/opacity-header-muted uppercase">수량</th>
|
||||||
<th className="px-1 py-1 text-right text-white/60">금액</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase pl-4">상태</th>
|
||||||
<th className="px-1 py-1 text-left text-white/60">상태</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/5">
|
<tbody className="divide-y divide-white/5">
|
||||||
{incomingHistory.map((h) => (
|
{incomingHistory.map((h) => (
|
||||||
<tr key={h.idx}>
|
<tr key={h.idx} className="hover:bg-white/5 transition-colors">
|
||||||
<td className="px-1 py-1 text-white/80 whitespace-nowrap">{h.date}</td>
|
<td className="px-2 py-1.5 text-white/80 whitespace-nowrap font-mono text-[11px]">{h.date}</td>
|
||||||
<td className="px-1 py-1 text-white/70 truncate max-w-[50px]" title={h.request}>{h.request}</td>
|
<td className="px-2 py-1.5 text-white/70 truncate max-w-[50px]" title={h.request}>{h.request}</td>
|
||||||
<td className="px-1 py-1 text-white text-right">{h.qty.toLocaleString()}</td>
|
<td className="px-2 py-1.5 text-white text-right font-mono">{h.qty.toLocaleString()}</td>
|
||||||
<td className="px-1 py-1 text-white text-right">{h.price.toLocaleString()}</td>
|
<td className="px-2 py-1.5 text-white/70 pl-4">{h.state}</td>
|
||||||
<td className="px-1 py-1 text-white/70">{h.state}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-white/30 text-xs py-2">입고내역 없음</div>
|
<div className="text-center text-white/30 text-xs py-4 flex flex-col items-center gap-1">
|
||||||
|
<TrendingDown className="w-6 h-6 opacity-20" />
|
||||||
|
<span>입고내역 없음</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 발주내역 */}
|
{/* 발주내역 */}
|
||||||
<div className="glass-effect rounded-xl p-3 flex-1 min-h-0 flex flex-col">
|
<div className="glass-effect rounded-3xl p-4 border border-white/10 flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 mb-2 border-b border-white/10 pb-2">
|
<div className="flex items-center gap-2 mb-3 border-b border-white/10 pb-2 shrink-0">
|
||||||
<ShoppingCart className="w-4 h-4 text-blue-400" />
|
<ShoppingCart className="w-4 h-4 text-blue-400" />
|
||||||
<h3 className="text-sm font-medium text-white">발주내역</h3>
|
<h3 className="text-sm font-bold text-white tracking-tight">발주내역</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div className="text-center py-2">
|
<div className="text-center py-4">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/50 mx-auto"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white/30 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
) : orderHistory.length > 0 ? (
|
) : orderHistory.length > 0 ? (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead className="bg-white/5 sticky top-0">
|
<thead className="bg-white/5 sticky top-0 backdrop-blur-sm z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left text-white/60">일자</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">일자</th>
|
||||||
<th className="px-1 py-1 text-left text-white/60">요청자</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">요청자</th>
|
||||||
<th className="px-1 py-1 text-right text-white/60">수량</th>
|
<th className="px-2 py-1.5 text-right text-list-header font-list-header text-white/opacity-header-muted uppercase">수량</th>
|
||||||
<th className="px-1 py-1 text-right text-white/60">금액</th>
|
<th className="px-2 py-1.5 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase pl-4">상태</th>
|
||||||
<th className="px-1 py-1 text-left text-white/60">상태</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/5">
|
<tbody className="divide-y divide-white/5">
|
||||||
{orderHistory.map((h) => (
|
{orderHistory.map((h) => (
|
||||||
<tr key={h.idx}>
|
<tr key={h.idx} className="hover:bg-white/5 transition-colors">
|
||||||
<td className="px-1 py-1 text-white/80 whitespace-nowrap">{h.date}</td>
|
<td className="px-2 py-1.5 text-white/80 whitespace-nowrap font-mono text-[11px]">{h.date}</td>
|
||||||
<td className="px-1 py-1 text-white/70 truncate max-w-[50px]" title={h.request}>{h.request}</td>
|
<td className="px-2 py-1.5 text-white/70 truncate max-w-[50px]" title={h.request}>{h.request}</td>
|
||||||
<td className="px-1 py-1 text-white text-right">{h.qty.toLocaleString()}</td>
|
<td className="px-2 py-1.5 text-white text-right font-mono">{h.qty.toLocaleString()}</td>
|
||||||
<td className="px-1 py-1 text-white text-right">{h.price.toLocaleString()}</td>
|
<td className="px-2 py-1.5 text-white/70 pl-4">{h.state}</td>
|
||||||
<td className="px-1 py-1 text-white/70">{h.state}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-white/30 text-xs py-2">발주내역 없음</div>
|
<div className="text-center text-white/30 text-xs py-4 flex flex-col items-center gap-1">
|
||||||
|
<ShoppingCart className="w-6 h-6 opacity-20" />
|
||||||
|
<span>발주내역 없음</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -621,14 +621,14 @@ export function Jobreport() {
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-white/10">
|
<thead className="bg-white/10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-2 py-3 text-center text-xs font-medium text-white/70 uppercase w-10"></th>
|
<th className="px-2 py-3 text-center text-list-header font-list-header text-white/opacity-header-muted uppercase w-10"></th>
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase w-24">날짜</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase w-24">날짜</th>
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase" style={{ width: '35%' }}>프로젝트</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase" style={{ width: '35%' }}>프로젝트</th>
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">업무형태</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">업무형태</th>
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">상태</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">상태</th>
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">시간</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">시간</th>
|
||||||
{canViewOT && <th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">OT</th>}
|
{canViewOT && <th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">OT</th>}
|
||||||
<th className="px-2 py-3 text-left text-xs font-medium text-white/70 uppercase">담당자</th>
|
<th className="px-2 py-3 text-left text-list-header font-list-header text-white/opacity-header-muted uppercase">담당자</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-white/10">
|
<tbody className="divide-y divide-white/10">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { comms } from '@/communication';
|
import { comms } from '@/communication';
|
||||||
import { MailFormItem } from '@/types';
|
import { MailFormItem } from '@/types';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
|
import { DevelopmentNotice } from '@/components/DevelopmentNotice';
|
||||||
|
|
||||||
const initialFormData: Partial<MailFormItem> = {
|
const initialFormData: Partial<MailFormItem> = {
|
||||||
cate: '',
|
cate: '',
|
||||||
@@ -169,6 +170,7 @@ export function MailFormPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in pb-4 h-full">
|
<div className="space-y-6 animate-fade-in pb-4 h-full">
|
||||||
|
<DevelopmentNotice />
|
||||||
{/* 메일양식 메인 카드 */}
|
{/* 메일양식 메인 카드 */}
|
||||||
<div className="glass-effect rounded-3xl overflow-hidden shadow-2xl border border-white/10 flex flex-col h-full max-h-[calc(100vh-140px)]">
|
<div className="glass-effect rounded-3xl overflow-hidden shadow-2xl border border-white/10 flex flex-col h-full max-h-[calc(100vh-140px)]">
|
||||||
<div className="px-6 py-4 border-b border-white/10 flex flex-col md:flex-row items-center justify-between gap-4 bg-white/[0.02] shrink-0">
|
<div className="px-6 py-4 border-b border-white/10 flex flex-col md:flex-row items-center justify-between gap-4 bg-white/[0.02] shrink-0">
|
||||||
@@ -225,7 +227,7 @@ export function MailFormPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 리스트 헤더 */}
|
{/* 리스트 헤더 */}
|
||||||
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-[10px] font-bold text-white/30 uppercase tracking-widest shrink-0">
|
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-list-header font-list-header text-white/opacity-header-muted uppercase shrink-0">
|
||||||
<div className="w-24 px-4 text-center">분류</div>
|
<div className="w-24 px-4 text-center">분류</div>
|
||||||
<div className="flex-1 px-4">양식 정보</div>
|
<div className="flex-1 px-4">양식 정보</div>
|
||||||
<div className="w-64 px-4">메일 제목</div>
|
<div className="w-64 px-4">메일 제목</div>
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ export function MonthlyWorkPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-white tracking-tight">월별근무표</h3>
|
<h3 className="text-lg font-bold text-white tracking-tight">월별근무표</h3>
|
||||||
<p className="text-white/30 text-[10px] uppercase font-bold tracking-widest mt-0.5">
|
|
||||||
Monthly Company Schedule
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -163,7 +160,7 @@ export function MonthlyWorkPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-px h-3 bg-white/10 mx-1" />
|
<div className="w-px h-3 bg-white/10 mx-1" />
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
<div className="w-1.5 h-1.5 rounded-full bg-danger-400" />
|
||||||
<span className="text-[10px] text-white/30 uppercase font-bold">Free</span>
|
<span className="text-[10px] text-white/30 uppercase font-bold">Free</span>
|
||||||
<span className="text-xs font-bold text-white ml-0.5">{freeDays}</span>
|
<span className="text-xs font-bold text-white ml-0.5">{freeDays}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +200,7 @@ export function MonthlyWorkPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 리스트 헤더 */}
|
{/* 리스트 헤더 */}
|
||||||
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-[10px] font-bold text-white/30 uppercase tracking-widest shrink-0">
|
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-list-header font-list-header text-white/opacity-header-muted uppercase shrink-0">
|
||||||
<div className="w-32 px-4">날짜 (Date)</div>
|
<div className="w-32 px-4">날짜 (Date)</div>
|
||||||
<div className="w-20 px-4 text-center">요일</div>
|
<div className="w-20 px-4 text-center">요일</div>
|
||||||
<div className="w-24 px-4 text-center">휴일 지정</div>
|
<div className="w-24 px-4 text-center">휴일 지정</div>
|
||||||
@@ -228,7 +225,7 @@ export function MonthlyWorkPage() {
|
|||||||
key={day.idx}
|
key={day.idx}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-6 py-2.5 hover:bg-white/[0.03] transition-all group flex items-center",
|
"px-6 py-2.5 hover:bg-white/[0.03] transition-all group flex items-center",
|
||||||
day.dayOfWeek === 0 && "bg-red-500/[0.03]",
|
day.dayOfWeek === 0 && "bg-danger-500/[0.03]",
|
||||||
day.dayOfWeek === 6 && "bg-blue-500/[0.03]"
|
day.dayOfWeek === 6 && "bg-blue-500/[0.03]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -246,7 +243,7 @@ export function MonthlyWorkPage() {
|
|||||||
<div className="w-20 px-4 text-center">
|
<div className="w-20 px-4 text-center">
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
"text-xs font-bold",
|
"text-xs font-bold",
|
||||||
day.dayOfWeek === 0 ? "text-red-400" :
|
day.dayOfWeek === 0 ? "text-danger-400" :
|
||||||
day.dayOfWeek === 6 ? "text-blue-400" : "text-white/30"
|
day.dayOfWeek === 6 ? "text-blue-400" : "text-white/30"
|
||||||
)}>
|
)}>
|
||||||
{day.dayName}
|
{day.dayName}
|
||||||
@@ -260,7 +257,7 @@ export function MonthlyWorkPage() {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center w-8 h-8 rounded-xl border transition-all active:scale-90",
|
"flex items-center justify-center w-8 h-8 rounded-xl border transition-all active:scale-90",
|
||||||
day.free
|
day.free
|
||||||
? "bg-red-500/10 border-red-500/30 text-red-400 shadow-lg shadow-red-500/10"
|
? "bg-danger-500/10 border-danger-500/30 text-danger-400 shadow-lg shadow-danger-500/10"
|
||||||
: "bg-white/5 border-white/5 text-white/10 hover:border-white/10 hover:text-white/30"
|
: "bg-white/5 border-white/5 text-white/10 hover:border-white/10 hover:text-white/30"
|
||||||
)}
|
)}
|
||||||
title={day.free ? "휴일 해제" : "휴일 지정"}
|
title={day.free ? "휴일 해제" : "휴일 지정"}
|
||||||
@@ -295,7 +292,7 @@ export function MonthlyWorkPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-white/20 text-[9px] flex items-center gap-4">
|
<div className="text-white/20 text-[9px] flex items-center gap-4">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-1 h-1 rounded-full bg-red-400" />
|
<div className="w-1 h-1 rounded-full bg-danger-400" />
|
||||||
<span>Sunday (휴일)</span>
|
<span>Sunday (휴일)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -278,14 +278,14 @@ export function Note() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컬럼 헤더 (업무일지 디자인 통일) */}
|
{/* 컬럼 헤더 (업무일지 디자인 통일) */}
|
||||||
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4">
|
<div className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4 text-list-header text-white/opacity-header-muted font-list-header uppercase">
|
||||||
<div className="w-8 text-center text-xs font-medium text-white/70 uppercase">상태</div>
|
<div className="w-8 text-center uppercase">상태</div>
|
||||||
<div className="flex-1 text-xs font-medium text-white/70 uppercase">제목</div>
|
<div className="flex-1 uppercase">제목</div>
|
||||||
<div className="flex items-center gap-6 shrink-0">
|
<div className="flex items-center gap-6 shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-20 text-right text-xs font-medium text-white/70 uppercase">작성자</div>
|
<div className="w-20 text-right uppercase">작성자</div>
|
||||||
<div className="w-24 text-center text-xs font-medium text-white/70 uppercase">작성일</div>
|
<div className="w-24 text-center uppercase">작성일</div>
|
||||||
<div className="w-16 text-right text-xs font-medium text-white/70 uppercase">조회수</div>
|
<div className="w-16 text-right uppercase">조회수</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[88px]"></div> {/* 액션 버튼 공간 */}
|
<div className="w-[88px]"></div> {/* 액션 버튼 공간 */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ export function UserListPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 헤더 */}
|
{/* 테이블 헤더 */}
|
||||||
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-[10px] font-bold text-white/30 uppercase tracking-widest shrink-0">
|
<div className="bg-white/5 px-6 py-3 border-b border-white/5 flex items-center text-list-header font-list-header text-white/opacity-header-muted uppercase shrink-0">
|
||||||
<div className="w-12 text-center">ID</div>
|
<div className="w-12 text-center">ID</div>
|
||||||
<div className="w-32 px-4">성명/직책</div>
|
<div className="w-32 px-4">성명/직책</div>
|
||||||
<div className="flex-1 px-4">이메일/연락처</div>
|
<div className="flex-1 px-4">이메일/연락처</div>
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ export default {
|
|||||||
900: '#7f1d1d',
|
900: '#7f1d1d',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fontSize: {
|
||||||
|
'xs': 'var(--fs-list-header)',
|
||||||
|
'list-header': 'var(--fs-list-header)',
|
||||||
|
'list-item': 'var(--fs-list-item)',
|
||||||
|
},
|
||||||
|
opacity: {
|
||||||
|
'header-muted': 'var(--header-muted-opacity)',
|
||||||
|
},
|
||||||
|
fontWeight: {
|
||||||
|
'list-header': 'var(--fw-list-header)',
|
||||||
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'fade-in': 'fadeIn 0.5s ease-in-out',
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||||
'slide-up': 'slideUp 0.3s ease-out',
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
|||||||
Reference in New Issue
Block a user