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 { User, LogOut, X } from 'lucide-react';
|
||||
import { User, LogOut, X, UserCog, Key } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { UserInfoDialog } from '@/components/user/UserInfoDialog';
|
||||
import { PasswordDialog } from '@/components/user/PasswordDialog';
|
||||
|
||||
interface UserInfoButtonProps {
|
||||
userName?: string;
|
||||
@@ -10,8 +12,21 @@ interface UserInfoButtonProps {
|
||||
|
||||
export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
||||
const [showLogoutDialog, setShowLogoutDialog] = useState(false);
|
||||
const [showUserInfoDialog, setShowUserInfoDialog] = useState(false);
|
||||
const [showPasswordDialog, setShowPasswordDialog] = 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 () => {
|
||||
setProcessing(true);
|
||||
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;
|
||||
|
||||
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">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setShowLogoutDialog(false)}
|
||||
@@ -79,16 +118,30 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
||||
{userDept && <p className="text-white/50 text-sm">{userDept}</p>}
|
||||
</div>
|
||||
<p className="text-white/70 text-center text-sm">
|
||||
로그아웃 하시겠습니까?
|
||||
원하시는 작업을 선택하세요.
|
||||
</p>
|
||||
</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
|
||||
onClick={handleLogout}
|
||||
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 ? (
|
||||
<span className="animate-spin mr-2">⏳</span>
|
||||
@@ -102,6 +155,19 @@ export function UserInfoButton({ userName, userDept }: UserInfoButtonProps) {
|
||||
</div>,
|
||||
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 className="bg-white/10 px-6 py-3 border-b border-white/5 flex items-center gap-4">
|
||||
<div className="w-8 text-center text-xs font-medium text-white/70 uppercase">상태</div>
|
||||
<div className="flex-1 text-xs font-medium text-white/70 uppercase">제품명 / 제조사</div>
|
||||
<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 uppercase">상태</div>
|
||||
<div className="flex-1 uppercase">제품명 / 제조사</div>
|
||||
<div className="flex items-center gap-6 shrink-0">
|
||||
<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-16 text-center text-xs font-medium text-white/70 uppercase">수량</div>
|
||||
<div className="w-32 text-left text-xs font-medium text-white/70 uppercase">사용자</div>
|
||||
<div className="w-48 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 uppercase">수량</div>
|
||||
<div className="w-32 text-left uppercase">사용자</div>
|
||||
<div className="w-48 text-left uppercase">시리얼 번호</div>
|
||||
</div>
|
||||
<div className="w-8"></div> {/* 액션/폴더 버튼 공간 */}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@ export function NoteEditModal({
|
||||
processing,
|
||||
onClose,
|
||||
onSave,
|
||||
initialEditMode = false,
|
||||
}: NoteEditModalProps) {
|
||||
const [pdate, setPdate] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -34,7 +33,6 @@ export function NoteEditModal({
|
||||
const [description, setDescription] = useState('');
|
||||
const [share, setShare] = useState(false);
|
||||
const [guid, setGuid] = useState('');
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
// 현재 로그인 사용자 정보 로드
|
||||
const [currentUserId, setCurrentUserId] = useState('');
|
||||
@@ -66,19 +64,17 @@ export function NoteEditModal({
|
||||
setDescription(editingItem.description || '');
|
||||
setShare(editingItem.share || false);
|
||||
setGuid(editingItem.guid || '');
|
||||
setIsEditMode(initialEditMode);
|
||||
} else {
|
||||
// 신규 메모 - 편집 모드로 시작
|
||||
// 신규 메모
|
||||
setPdate(new Date().toISOString().split('T')[0]);
|
||||
setTitle('');
|
||||
setUid(currentUserId);
|
||||
setDescription('');
|
||||
setShare(false);
|
||||
setGuid('');
|
||||
setIsEditMode(true);
|
||||
}
|
||||
}
|
||||
}, [isOpen, editingItem, currentUserId, initialEditMode]);
|
||||
}, [isOpen, editingItem, currentUserId]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -94,15 +90,15 @@ export function NoteEditModal({
|
||||
|
||||
return createPortal(
|
||||
<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">
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title">
|
||||
{!editingItem ? '새 메모 작성' : '메모 편집'}
|
||||
</h2>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -117,47 +113,37 @@ export function NoteEditModal({
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
날짜 {isEditMode && '*'}
|
||||
날짜
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<input
|
||||
type="date"
|
||||
value={pdate}
|
||||
onChange={(e) => setPdate(e.target.value)}
|
||||
required
|
||||
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"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white text-sm">{pdate}</div>
|
||||
)}
|
||||
<input
|
||||
type="date"
|
||||
value={pdate}
|
||||
onChange={(e) => setPdate(e.target.value)}
|
||||
required
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 작성자 */}
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
작성자 {isEditMode && '*'}
|
||||
작성자
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={uid}
|
||||
onChange={(e) => setUid(e.target.value)}
|
||||
required
|
||||
disabled={!canEdit || !canChangeUser}
|
||||
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">
|
||||
관리자만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white text-sm">{uid}</div>
|
||||
<input
|
||||
type="text"
|
||||
value={uid}
|
||||
onChange={(e) => setUid(e.target.value)}
|
||||
required
|
||||
disabled={!canEdit || !canChangeUser}
|
||||
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">
|
||||
관리자만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -166,33 +152,27 @@ export function NoteEditModal({
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
공유 설정
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="note-share"
|
||||
checked={share}
|
||||
onChange={(e) => setShare(e.target.checked)}
|
||||
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>
|
||||
</div>
|
||||
{!canChangeShare && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
본인 메모만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white text-sm">{share ? '공유됨' : '비공유'}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="note-share"
|
||||
checked={share}
|
||||
onChange={(e) => setShare(e.target.checked)}
|
||||
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>
|
||||
</div>
|
||||
{!canChangeShare && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
본인 메모만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</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>
|
||||
@@ -204,21 +184,17 @@ export function NoteEditModal({
|
||||
{/* 제목 */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
제목 {isEditMode && '*'}
|
||||
제목
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
required
|
||||
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"
|
||||
placeholder="메모 제목을 입력하세요"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white text-lg font-semibold">{title}</div>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
required
|
||||
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"
|
||||
placeholder="메모 제목을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
@@ -226,27 +202,21 @@ export function NoteEditModal({
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
내용
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={!canEdit}
|
||||
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"
|
||||
placeholder="메모 내용을 입력하세요"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white/10 border border-white/20 rounded-lg p-4 text-white whitespace-pre-wrap min-h-[400px]">
|
||||
{description || '내용이 없습니다.'}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={!canEdit}
|
||||
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"
|
||||
placeholder="메모 내용을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -255,7 +225,7 @@ export function NoteEditModal({
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
{isEditMode && canEdit && (
|
||||
{canEdit && (
|
||||
<button
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
|
||||
@@ -21,13 +21,13 @@ export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteV
|
||||
|
||||
return createPortal(
|
||||
<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">
|
||||
<h2 className="text-xl font-bold text-white">{note.title || '제목 없음'}</h2>
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title">{note.title || '제목 없음'}</h2>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -41,7 +41,7 @@ export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteV
|
||||
</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
|
||||
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"
|
||||
|
||||
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 { 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 { comms } from '@/communication';
|
||||
import { UserInfoDetail } from '@/types';
|
||||
@@ -12,119 +13,9 @@ interface UserInfoDialogProps {
|
||||
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) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showPasswordDialog, setShowPasswordDialog] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
@@ -154,16 +45,16 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
||||
}
|
||||
}, [isOpen, userId]);
|
||||
|
||||
// ESC 키로 닫기 (비밀번호 다이얼로그가 열려있지 않을 때만)
|
||||
// ESC 키로 닫기
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen && !showPasswordDialog) {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose, showPasswordDialog]);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const loadUserInfo = async () => {
|
||||
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) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
@@ -229,19 +107,19 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<>
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<div className="dialog-header flex items-center justify-between px-6 py-4">
|
||||
<h2 className="dialog-title">
|
||||
<User className="w-6 h-6" />
|
||||
사용자 정보
|
||||
</h2>
|
||||
<button
|
||||
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" />
|
||||
</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>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* 테마 설정 섹션 */}
|
||||
<div className="bg-white/5 rounded-lg p-4 mb-6">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<Palette className="w-4 h-4 text-purple-400" />
|
||||
테마 설정
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
onClick={() => handleThemeChange('dark')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex flex-col items-center gap-2',
|
||||
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-full h-2 rounded-full bg-gradient-to-r from-blue-600 to-purple-600"></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 flex-col items-center gap-2',
|
||||
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-full h-2 rounded-full bg-gradient-to-r from-pink-500 to-rose-500"></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 flex-col items-center gap-2',
|
||||
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-full h-2 rounded-full bg-gradient-to-r from-sky-400 to-blue-500"></div>
|
||||
<span className="text-sm font-medium">시원한 하늘</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col h-full gap-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 min-h-0">
|
||||
{/* 좌측 컬럼: 기본 정보 */}
|
||||
<div className="space-y-4 overflow-y-auto pr-2 custom-scrollbar">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<User className="w-4 h-4" />
|
||||
사원번호
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Id}
|
||||
disabled
|
||||
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">이름</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameK}
|
||||
onChange={(e) => handleInputChange('NameK', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="이름"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">영문이름</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.NameE}
|
||||
onChange={(e) => handleInputChange('NameE', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="English Name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Briefcase className="w-4 h-4" />
|
||||
직책
|
||||
</label>
|
||||
<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 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">
|
||||
<Mail className="w-4 h-4" />
|
||||
이메일
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.Email}
|
||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">공정</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.Process}
|
||||
onChange={(e) => handleInputChange('Process', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="공정"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 입/퇴사 정보 */}
|
||||
<div 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>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Mail className="w-4 h-4" />
|
||||
이메일
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.Email}
|
||||
onChange={(e) => handleInputChange('Email', e.target.value)}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 입/퇴사 정보 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
입사일
|
||||
</label>
|
||||
<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>
|
||||
<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 className="flex flex-col h-full gap-4">
|
||||
{/* 테마 설정 섹션 */}
|
||||
<div className="bg-white/5 rounded-lg p-4">
|
||||
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
|
||||
<Palette className="w-4 h-4 text-purple-400" />
|
||||
테마 설정
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<button
|
||||
onClick={() => handleThemeChange('dark')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'dark'
|
||||
? 'border-blue-500 bg-blue-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">기본 (Dark)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleThemeChange('PSH_PINK')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'PSH_PINK'
|
||||
? 'border-pink-500 bg-pink-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-pink-500 to-rose-500 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">발랄한 핑크</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleThemeChange('JW_SKY')}
|
||||
className={clsx(
|
||||
'px-4 py-3 rounded-lg border-2 transition-all flex items-center gap-3',
|
||||
theme === 'JW_SKY'
|
||||
? 'border-sky-500 bg-sky-500/20 text-white'
|
||||
: 'border-white/10 bg-white/5 text-white/50 hover:bg-white/10 hover:border-white/30'
|
||||
)}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-gradient-to-r from-sky-400 to-blue-500 border border-white/20 shrink-0"></div>
|
||||
<span className="text-sm font-medium">시원한 하늘</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고: 남은 높이 채우기 */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<label className="block text-sm text-white/70 mb-1 flex items-center gap-1">
|
||||
<FileText className="w-4 h-4" />
|
||||
비고
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.Memo}
|
||||
onChange={(e) => handleInputChange('Memo', e.target.value)}
|
||||
className="flex-1 w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 resize-none"
|
||||
placeholder="비고"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고 */}
|
||||
<div>
|
||||
<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 && (
|
||||
<div
|
||||
@@ -480,14 +353,7 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10">
|
||||
<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="dialog-footer flex items-center justify-end px-6 py-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -511,13 +377,9 @@ export function UserInfoDialog({ isOpen, onClose, userId, onSave }: UserInfoDial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
<PasswordDialog
|
||||
isOpen={showPasswordDialog}
|
||||
onClose={() => setShowPasswordDialog(false)}
|
||||
onConfirm={handleChangePassword}
|
||||
/>
|
||||
</>
|
||||
</>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user