feat: React 프론트엔드 기능 대폭 확장
- 월별근무표: 휴일/근무일 관리, 자동 초기화 - 메일양식: 템플릿 CRUD, To/CC/BCC 설정 - 그룹정보: 부서 관리, 비트 연산 기반 권한 설정 - 업무일지: 수정 성공 메시지 제거, 오늘 근무시간 필터링 수정 - 웹소켓 메시지 type 충돌 버그 수정 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
719
Project/frontend/src/pages/Todo.tsx
Normal file
719
Project/frontend/src/pages/Todo.tsx
Normal file
@@ -0,0 +1,719 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Edit2,
|
||||
Trash2,
|
||||
Flag,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
X,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { TodoModel, TodoStatus, TodoPriority } from '@/types';
|
||||
|
||||
// 상태/중요도 유틸리티 함수들
|
||||
const getStatusText = (status: string): string => {
|
||||
switch (status) {
|
||||
case '0': return '대기';
|
||||
case '1': return '진행';
|
||||
case '2': return '취소';
|
||||
case '3': return '보류';
|
||||
case '5': return '완료';
|
||||
default: return '대기';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string): string => {
|
||||
switch (status) {
|
||||
case '0': return 'bg-gray-500/20 text-gray-300 border-gray-500/30';
|
||||
case '1': return 'bg-primary-500/20 text-primary-300 border-primary-500/30';
|
||||
case '2': return 'bg-danger-500/20 text-danger-300 border-danger-500/30';
|
||||
case '3': return 'bg-warning-500/20 text-warning-300 border-warning-500/30';
|
||||
case '5': return 'bg-success-500/20 text-success-300 border-success-500/30';
|
||||
default: return 'bg-white/10 text-white/50 border-white/20';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityText = (seqno: number): string => {
|
||||
switch (seqno) {
|
||||
case 1: return '중요';
|
||||
case 2: return '매우 중요';
|
||||
case 3: return '긴급';
|
||||
default: return '보통';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityClass = (seqno: number): string => {
|
||||
switch (seqno) {
|
||||
case 1: return 'bg-primary-500/20 text-primary-300';
|
||||
case 2: return 'bg-warning-500/20 text-warning-300';
|
||||
case 3: return 'bg-danger-500/20 text-danger-300';
|
||||
default: return 'bg-white/10 text-white/50';
|
||||
}
|
||||
};
|
||||
|
||||
// 폼 데이터 타입
|
||||
interface TodoFormData {
|
||||
title: string;
|
||||
remark: string;
|
||||
expire: string;
|
||||
seqno: TodoPriority;
|
||||
flag: boolean;
|
||||
request: string;
|
||||
status: TodoStatus;
|
||||
}
|
||||
|
||||
const initialFormData: TodoFormData = {
|
||||
title: '',
|
||||
remark: '',
|
||||
expire: '',
|
||||
seqno: 0,
|
||||
flag: false,
|
||||
request: '',
|
||||
status: '0',
|
||||
};
|
||||
|
||||
export function Todo() {
|
||||
const [todos, setTodos] = useState<TodoModel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'active' | 'hold' | 'completed' | 'cancelled'>('active');
|
||||
|
||||
// 모달 상태
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingTodo, setEditingTodo] = useState<TodoModel | null>(null);
|
||||
const [formData, setFormData] = useState<TodoFormData>(initialFormData);
|
||||
|
||||
// 할일 목록 로드
|
||||
const loadTodos = useCallback(async () => {
|
||||
try {
|
||||
const response = await comms.getTodos();
|
||||
if (response.Success && response.Data) {
|
||||
setTodos(response.Data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 목록 로드 오류:', error);
|
||||
alert('할일 목록을 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadTodos();
|
||||
}, [loadTodos]);
|
||||
|
||||
// 필터링된 할일 목록
|
||||
const activeTodos = todos.filter(todo => todo.status === '0' || todo.status === '1'); // 대기 + 진행
|
||||
const holdTodos = todos.filter(todo => todo.status === '3'); // 보류
|
||||
const completedTodos = todos.filter(todo => todo.status === '5'); // 완료
|
||||
const cancelledTodos = todos.filter(todo => todo.status === '2'); // 취소
|
||||
|
||||
// 새 할일 추가
|
||||
const handleAdd = async () => {
|
||||
if (!formData.remark.trim()) {
|
||||
alert('할일 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.createTodo(
|
||||
formData.title,
|
||||
formData.remark,
|
||||
formData.expire || null,
|
||||
formData.seqno,
|
||||
formData.flag,
|
||||
formData.request || null,
|
||||
formData.status
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowAddModal(false);
|
||||
setFormData(initialFormData);
|
||||
loadTodos();
|
||||
} else {
|
||||
alert(response.Message || '할일 추가에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 추가 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 할일 수정 모달 열기
|
||||
const openEditModal = async (todo: TodoModel) => {
|
||||
try {
|
||||
const response = await comms.getTodo(todo.idx);
|
||||
if (response.Success && response.Data) {
|
||||
const data = response.Data;
|
||||
setEditingTodo(data);
|
||||
setFormData({
|
||||
title: data.title || '',
|
||||
remark: data.remark || '',
|
||||
expire: data.expire ? data.expire.split('T')[0] : '',
|
||||
seqno: data.seqno as TodoPriority,
|
||||
flag: data.flag || false,
|
||||
request: data.request || '',
|
||||
status: data.status as TodoStatus,
|
||||
});
|
||||
setShowEditModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 조회 오류:', error);
|
||||
alert('할일 정보를 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 할일 수정
|
||||
const handleUpdate = async () => {
|
||||
if (!editingTodo || !formData.remark.trim()) {
|
||||
alert('할일 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.updateTodo(
|
||||
editingTodo.idx,
|
||||
formData.title,
|
||||
formData.remark,
|
||||
formData.expire || null,
|
||||
formData.seqno,
|
||||
formData.flag,
|
||||
formData.request || null,
|
||||
formData.status
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowEditModal(false);
|
||||
setEditingTodo(null);
|
||||
setFormData(initialFormData);
|
||||
loadTodos();
|
||||
} else {
|
||||
alert(response.Message || '할일 수정에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 수정 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 할일 삭제
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.deleteTodo(id);
|
||||
if (response.Success) {
|
||||
loadTodos();
|
||||
} else {
|
||||
alert(response.Message || '할일 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 삭제 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 상태 빠른 변경
|
||||
const handleQuickStatusChange = async (todo: TodoModel, newStatus: TodoStatus) => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.updateTodo(
|
||||
todo.idx,
|
||||
todo.title,
|
||||
todo.remark,
|
||||
todo.expire,
|
||||
todo.seqno,
|
||||
todo.flag,
|
||||
todo.request,
|
||||
newStatus
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
loadTodos();
|
||||
} else {
|
||||
alert(response.Message || '상태 변경에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('상태 변경 오류:', error);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 헤더 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
내 할일 목록
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData(initialFormData);
|
||||
setShowAddModal(true);
|
||||
}}
|
||||
className="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
새 할일 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 탭 메뉴 */}
|
||||
<div className="px-6 py-2 border-b border-white/10">
|
||||
<div className="flex space-x-1 bg-white/5 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('active')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'active'
|
||||
? 'text-white bg-white/20 shadow-sm'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<Zap className="w-4 h-4" />
|
||||
<span>진행중</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-primary-500/30 text-primary-200 rounded-full">
|
||||
{activeTodos.length}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('hold')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'hold'
|
||||
? 'text-white bg-white/20 shadow-sm'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<span>보류</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-warning-500/30 text-warning-200 rounded-full">
|
||||
{holdTodos.length}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('completed')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'completed'
|
||||
? 'text-white bg-white/20 shadow-sm'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>완료</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-success-500/30 text-success-200 rounded-full">
|
||||
{completedTodos.length}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('cancelled')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||
activeTab === 'cancelled'
|
||||
? 'text-white bg-white/20 shadow-sm'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<X className="w-4 h-4" />
|
||||
<span>취소</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-danger-500/30 text-danger-200 rounded-full">
|
||||
{cancelledTodos.length}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 할일 테이블 */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-white/10">
|
||||
<tr>
|
||||
{activeTab === 'active' && (
|
||||
<th className="px-2 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-10 border-r border-white/10"></th>
|
||||
)}
|
||||
<th className="px-3 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-16 border-r border-white/10">상태</th>
|
||||
<th className="px-3 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-16 border-r border-white/10">플래그</th>
|
||||
<th className="px-4 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider border-r border-white/10">제목</th>
|
||||
<th className="px-3 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-24 border-r border-white/10">요청자</th>
|
||||
<th className="px-3 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-20 border-r border-white/10">중요도</th>
|
||||
<th className="px-3 py-4 text-center text-xs font-medium text-white/70 uppercase tracking-wider w-24">
|
||||
{activeTab === 'completed' ? '완료일' : '만료일'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/10">
|
||||
{(activeTab === 'active' ? activeTodos : activeTab === 'hold' ? holdTodos : activeTab === 'completed' ? completedTodos : cancelledTodos).map((todo) => (
|
||||
<TodoRow
|
||||
key={todo.idx}
|
||||
todo={todo}
|
||||
showOkdate={activeTab === 'completed'}
|
||||
showCompleteButton={activeTab === 'active'}
|
||||
onEdit={() => openEditModal(todo)}
|
||||
onComplete={() => handleQuickStatusChange(todo, '5')}
|
||||
/>
|
||||
))}
|
||||
{(activeTab === 'active' ? activeTodos : activeTab === 'hold' ? holdTodos : activeTab === 'completed' ? completedTodos : cancelledTodos).length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={activeTab === 'active' ? 7 : 6} className="px-6 py-8 text-center text-white/50">
|
||||
{activeTab === 'active' ? '진행중인 할일이 없습니다' : activeTab === 'hold' ? '보류된 할일이 없습니다' : activeTab === 'completed' ? '완료된 할일이 없습니다' : '취소된 할일이 없습니다'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 로딩 인디케이터 */}
|
||||
{processing && (
|
||||
<div className="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm flex items-center">
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
처리 중...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 모달 */}
|
||||
{showAddModal && (
|
||||
<TodoModal
|
||||
title="새 할일 추가"
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onSubmit={handleAdd}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
submitText="추가"
|
||||
processing={processing}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 수정 모달 */}
|
||||
{showEditModal && editingTodo && (
|
||||
<TodoModal
|
||||
title="할일 수정"
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
onSubmit={handleUpdate}
|
||||
onClose={() => {
|
||||
setShowEditModal(false);
|
||||
setEditingTodo(null);
|
||||
}}
|
||||
submitText="수정"
|
||||
processing={processing}
|
||||
isEdit={true}
|
||||
onComplete={() => {
|
||||
handleQuickStatusChange(editingTodo, '5');
|
||||
setShowEditModal(false);
|
||||
setEditingTodo(null);
|
||||
}}
|
||||
onDelete={() => {
|
||||
handleDelete(editingTodo.idx);
|
||||
setShowEditModal(false);
|
||||
setEditingTodo(null);
|
||||
}}
|
||||
currentStatus={editingTodo.status}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 할일 행 컴포넌트
|
||||
interface TodoRowProps {
|
||||
todo: TodoModel;
|
||||
showOkdate: boolean;
|
||||
showCompleteButton?: boolean;
|
||||
onEdit: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
function TodoRow({ todo, showOkdate, showCompleteButton = true, onEdit, onComplete }: TodoRowProps) {
|
||||
const isExpired = todo.expire && new Date(todo.expire) < new Date();
|
||||
|
||||
const handleComplete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (confirm('이 할일을 완료 처리하시겠습니까?')) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
className="hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={onEdit}
|
||||
>
|
||||
{showCompleteButton && (
|
||||
<td className="px-2 py-4 text-center border-r border-white/10">
|
||||
<button
|
||||
onClick={handleComplete}
|
||||
className="p-1.5 bg-success-500/20 hover:bg-success-500/40 text-success-300 rounded-full transition-colors"
|
||||
title="완료 처리"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-3 py-4 text-center whitespace-nowrap border-r border-white/10">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${getStatusClass(todo.status)}`}>
|
||||
{getStatusText(todo.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-4 text-center whitespace-nowrap border-r border-white/10">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
todo.flag ? 'bg-warning-500/20 text-warning-300' : 'bg-white/10 text-white/50'
|
||||
}`}>
|
||||
{todo.flag ? <Flag className="w-3 h-3 mr-1" /> : null}
|
||||
{todo.flag ? '고정' : '일반'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-left text-white border-r border-white/10">{todo.title || '제목 없음'}</td>
|
||||
<td className="px-3 py-4 text-center text-white/80 border-r border-white/10">{todo.request || '-'}</td>
|
||||
<td className="px-3 py-4 text-center whitespace-nowrap border-r border-white/10">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${getPriorityClass(todo.seqno)}`}>
|
||||
{getPriorityText(todo.seqno)}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`px-3 py-4 text-center whitespace-nowrap ${showOkdate ? 'text-success-400' : (isExpired ? 'text-danger-400' : 'text-white/80')}`}>
|
||||
{showOkdate
|
||||
? (todo.okdate ? new Date(todo.okdate).toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' }) : '-')
|
||||
: (todo.expire ? new Date(todo.expire).toLocaleDateString('ko-KR', { year: '2-digit', month: '2-digit', day: '2-digit' }) : '-')
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// 할일 모달 컴포넌트
|
||||
interface TodoModalProps {
|
||||
title: string;
|
||||
formData: TodoFormData;
|
||||
setFormData: React.Dispatch<React.SetStateAction<TodoFormData>>;
|
||||
onSubmit: () => void;
|
||||
onClose: () => void;
|
||||
submitText: string;
|
||||
processing: boolean;
|
||||
isEdit?: boolean;
|
||||
onComplete?: () => void;
|
||||
onDelete?: () => void;
|
||||
currentStatus?: string;
|
||||
}
|
||||
|
||||
function TodoModal({
|
||||
title,
|
||||
formData,
|
||||
setFormData,
|
||||
onSubmit,
|
||||
onClose,
|
||||
submitText,
|
||||
processing,
|
||||
isEdit = false,
|
||||
onComplete,
|
||||
onDelete,
|
||||
currentStatus,
|
||||
}: TodoModalProps) {
|
||||
const statusOptions: { value: TodoStatus; label: string }[] = [
|
||||
{ value: '0', label: '대기' },
|
||||
{ value: '1', label: '진행' },
|
||||
{ value: '3', label: '보류' },
|
||||
{ value: '2', label: '취소' },
|
||||
{ value: '5', label: '완료' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" onClick={onClose}>
|
||||
<div className="flex items-center justify-center min-h-screen p-4">
|
||||
<div
|
||||
className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
{title}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">제목 (선택사항)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||
placeholder="할일 제목을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">만료일 (선택사항)</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.expire}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, expire: e.target.value }))}
|
||||
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">내용 *</label>
|
||||
<textarea
|
||||
value={formData.remark}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))}
|
||||
rows={3}
|
||||
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||
placeholder="할일 내용을 입력하세요 (필수)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">요청자</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.request}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, request: e.target.value }))}
|
||||
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||
placeholder="업무 요청자를 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">진행상태</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{statusOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, status: option.value }))}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${
|
||||
formData.status === option.value
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">중요도</label>
|
||||
<select
|
||||
value={formData.seqno}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, seqno: parseInt(e.target.value) as TodoPriority }))}
|
||||
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||
>
|
||||
<option value={0}>보통</option>
|
||||
<option value={1}>중요</option>
|
||||
<option value={2}>매우 중요</option>
|
||||
<option value={3}>긴급</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center text-white/70 text-sm font-medium cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.flag}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, flag: e.target.checked }))}
|
||||
className="mr-2 text-primary-500 focus:ring-primary-400 focus:ring-offset-0 rounded"
|
||||
/>
|
||||
플래그 (상단 고정)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-between">
|
||||
{/* 왼쪽: 삭제 버튼 (편집 모드일 때만) */}
|
||||
<div>
|
||||
{isEdit && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
disabled={processing}
|
||||
className="bg-danger-500 hover:bg-danger-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
삭제
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 취소, 완료, 수정 버튼 */}
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
{isEdit && onComplete && currentStatus !== '5' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onComplete}
|
||||
disabled={processing}
|
||||
className="bg-success-500 hover:bg-success-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
완료 처리
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={processing}
|
||||
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{submitText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user