Add Dashboard todo edit/delete/complete features and Note view count tracking

This commit is contained in:
backuppc
2025-12-02 15:03:51 +09:00
parent 6a2485176b
commit e82f86191a
25 changed files with 3512 additions and 324 deletions

View File

@@ -9,9 +9,20 @@ import {
RefreshCw,
ClipboardList,
Clock,
FileText,
Share2,
Lock,
List,
Plus,
X,
Loader2,
Edit2,
Trash2,
} from 'lucide-react';
import { comms } from '@/communication';
import { TodoModel, PurchaseItem } from '@/types';
import { TodoModel, TodoStatus, TodoPriority, PurchaseItem, NoteItem, JobReportItem } from '@/types';
import { NoteViewModal } from '@/components/note/NoteViewModal';
import { NoteEditModal } from '@/components/note/NoteEditModal';
interface StatCardProps {
title: string;
@@ -55,10 +66,31 @@ export function Dashboard() {
const [urgentTodos, setUrgentTodos] = useState<TodoModel[]>([]);
const [purchaseNRList, setPurchaseNRList] = useState<PurchaseItem[]>([]);
const [purchaseCRList, setPurchaseCRList] = useState<PurchaseItem[]>([]);
const [recentNotes, setRecentNotes] = useState<NoteItem[]>([]);
// 모달 상태
const [showNRModal, setShowNRModal] = useState(false);
const [showCRModal, setShowCRModal] = useState(false);
const [showNoteModal, setShowNoteModal] = useState(false);
const [showNoteEditModal, setShowNoteEditModal] = useState(false);
const [showNoteAddModal, setShowNoteAddModal] = useState(false);
const [selectedNote, setSelectedNote] = useState<NoteItem | null>(null);
const [editingNote, setEditingNote] = useState<NoteItem | null>(null);
const [processing, setProcessing] = useState(false);
// 할일 추가 모달 상태
const [showTodoAddModal, setShowTodoAddModal] = useState(false);
const [showTodoEditModal, setShowTodoEditModal] = useState(false);
const [editingTodo, setEditingTodo] = useState<TodoModel | null>(null);
const [todoFormData, setTodoFormData] = useState({
title: '',
remark: '',
expire: '',
seqno: 0 as TodoPriority,
flag: false,
request: '',
status: '0' as TodoStatus,
});
const loadDashboardData = useCallback(async () => {
try {
@@ -83,11 +115,13 @@ export function Dashboard() {
urgentTodosResponse,
allTodosResponse,
jobreportResponse,
notesResponse,
] = await Promise.all([
comms.getPurchaseWaitCount(),
comms.getUrgentTodos(),
comms.getTodos(),
comms.getJobReportList(todayStr, todayStr, currentUserId, ''),
comms.getNoteList('2000-01-01', todayStr, ''),
]);
setPurchaseNR(purchaseCount.NR);
@@ -99,17 +133,22 @@ export function Dashboard() {
if (allTodosResponse.Success && allTodosResponse.Data) {
// 진행, 대기 상태의 할일만 카운트 (보류, 취소 제외)
const pendingCount = allTodosResponse.Data.filter(t => t.status === '0' || t.status === '1').length;
const pendingCount = allTodosResponse.Data.filter((t: TodoModel) => t.status === '0' || t.status === '1').length;
setTodoCount(pendingCount);
}
// 오늘 업무일지 작성시간 계산
if (jobreportResponse.Success && jobreportResponse.Data) {
const totalHrs = jobreportResponse.Data.reduce((acc, item) => acc + (item.hrs || 0), 0);
const totalHrs = jobreportResponse.Data.reduce((acc: number, item: JobReportItem) => acc + (item.hrs || 0), 0);
setTodayWorkHrs(totalHrs);
} else {
setTodayWorkHrs(0);
}
// 최근 메모 목록 (최대 10개)
if (notesResponse.Success && notesResponse.Data) {
setRecentNotes(notesResponse.Data.slice(0, 10));
}
} catch (error) {
console.error('대시보드 데이터 로드 오류:', error);
} finally {
@@ -199,23 +238,263 @@ export function Dashboard() {
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-white"> </h2>
<button
onClick={handleRefresh}
disabled={refreshing}
className="flex items-center space-x-2 px-4 py-2 glass-effect rounded-lg text-white/70 hover:text-white transition-colors"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
<span></span>
</button>
</div>
const handleNoteClick = async (note: NoteItem) => {
try {
const response = await comms.getNoteDetail(note.idx);
if (response.Success && response.Data) {
setSelectedNote(response.Data);
setShowNoteModal(true);
}
} catch (error) {
console.error('메모 조회 오류:', error);
}
};
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
const handleNoteAdd = () => {
setEditingNote(null);
setShowNoteAddModal(true);
};
const handleNoteDelete = async (note: NoteItem) => {
setProcessing(true);
try {
const response = await comms.deleteNote(note.idx);
if (response.Success) {
setShowNoteModal(false);
loadDashboardData();
} else {
alert(response.Message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 오류:', error);
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setProcessing(false);
}
};
const handleTodoAdd = () => {
setTodoFormData({
title: '',
remark: '',
expire: '',
seqno: 0,
flag: false,
request: '',
status: '0',
});
setShowTodoAddModal(true);
};
const handleTodoEdit = (todo: TodoModel) => {
setEditingTodo(todo);
setTodoFormData({
title: todo.title || '',
remark: todo.remark,
expire: todo.expire || '',
seqno: todo.seqno as TodoPriority,
flag: todo.flag,
request: todo.request || '',
status: todo.status as TodoStatus,
});
setShowTodoEditModal(true);
};
const handleTodoSave = async () => {
if (!todoFormData.remark.trim()) {
alert('할일 내용을 입력해주세요.');
return;
}
setProcessing(true);
try {
const response = await comms.createTodo(
todoFormData.title,
todoFormData.remark,
todoFormData.expire || null,
todoFormData.seqno,
todoFormData.flag,
todoFormData.request || null,
todoFormData.status
);
if (response.Success) {
setShowTodoAddModal(false);
loadDashboardData();
} else {
alert(response.Message || '할일 추가에 실패했습니다.');
}
} catch (error) {
console.error('할일 추가 오류:', error);
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setProcessing(false);
}
};
const handleTodoUpdate = async () => {
if (!editingTodo || !todoFormData.remark.trim()) {
alert('할일 내용을 입력해주세요.');
return;
}
setProcessing(true);
try {
const response = await comms.updateTodo(
editingTodo.idx,
todoFormData.title,
todoFormData.remark,
todoFormData.expire || null,
todoFormData.seqno,
todoFormData.flag,
todoFormData.request || null,
todoFormData.status
);
if (response.Success) {
setShowTodoEditModal(false);
setEditingTodo(null);
loadDashboardData();
} else {
alert(response.Message || '할일 수정에 실패했습니다.');
}
} catch (error) {
console.error('할일 수정 오류:', error);
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setProcessing(false);
}
};
const handleTodoDelete = async () => {
if (!editingTodo) return;
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) {
return;
}
setProcessing(true);
try {
const response = await comms.deleteTodo(editingTodo.idx);
if (response.Success) {
setShowTodoEditModal(false);
setEditingTodo(null);
loadDashboardData();
} else {
alert(response.Message || '할일 삭제에 실패했습니다.');
}
} catch (error) {
console.error('할일 삭제 오류:', error);
alert('서버 연결에 실패했습니다.');
} finally {
setProcessing(false);
}
};
const handleTodoComplete = async () => {
if (!editingTodo) return;
setProcessing(true);
try {
const response = await comms.updateTodo(
editingTodo.idx,
editingTodo.title,
editingTodo.remark,
editingTodo.expire,
editingTodo.seqno,
editingTodo.flag,
editingTodo.request,
'5' // 완료 상태
);
if (response.Success) {
setShowTodoEditModal(false);
setEditingTodo(null);
loadDashboardData();
} else {
alert(response.Message || '할일 완료 처리에 실패했습니다.');
}
} catch (error) {
console.error('할일 완료 오류:', error);
alert('서버 연결에 실패했습니다.');
} finally {
setProcessing(false);
}
};
const handleNoteSave = async (formData: {
pdate: string;
title: string;
uid: string;
description: string;
share: boolean;
guid: string;
}) => {
if (!formData.pdate) {
alert('날짜를 입력해주세요.');
return;
}
if (!formData.title.trim()) {
alert('제목을 입력해주세요.');
return;
}
setProcessing(true);
try {
const response = editingNote
? await comms.editNote(
editingNote.idx,
formData.pdate,
formData.title,
formData.uid,
formData.description,
'',
formData.share,
formData.guid
)
: await comms.addNote(
formData.pdate,
formData.title,
formData.uid,
formData.description,
'',
formData.share,
formData.guid
);
if (response.Success) {
setShowNoteEditModal(false);
setShowNoteAddModal(false);
loadDashboardData();
} else {
alert(response.Message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('저장 오류:', error);
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
} finally {
setProcessing(false);
}
};
return (
<div className="flex gap-6 animate-fade-in">
{/* 메인 컨텐츠 */}
<div className="flex-1 space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-white"> </h2>
<button
onClick={handleRefresh}
disabled={refreshing}
className="flex items-center space-x-2 px-4 py-2 glass-effect rounded-lg text-white/70 hover:text-white transition-colors"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
<span></span>
</button>
</div>
{/* 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
title="구매요청 (NR)"
value={purchaseNR}
@@ -244,21 +523,31 @@ export function Dashboard() {
color="text-cyan-400"
onClick={() => navigate('/jobreport')}
/>
</div>
</div>
{/* 급한 할일 목록 */}
<div className="glass-effect rounded-2xl overflow-hidden">
{/* 할일 목록 */}
<div className="glass-effect rounded-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
<h3 className="text-lg font-semibold text-white flex items-center">
<AlertTriangle className="w-5 h-5 mr-2 text-warning-400" />
</h3>
<button
onClick={() => navigate('/todo')}
className="text-sm text-primary-400 hover:text-primary-300 transition-colors"
>
</button>
<div className="flex items-center gap-2">
<button
onClick={handleTodoAdd}
className="p-1.5 rounded-lg bg-primary-500/20 text-primary-400 hover:bg-primary-500/30 transition-colors"
title="할일 추가"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => navigate('/todo')}
className="p-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors"
title="전체보기"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
<div className="divide-y divide-white/10">
@@ -267,7 +556,7 @@ export function Dashboard() {
<div
key={todo.idx}
className="px-6 py-4 hover:bg-white/5 transition-colors cursor-pointer"
onClick={() => navigate('/todo')}
onClick={() => handleTodoEdit(todo)}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
@@ -302,25 +591,436 @@ export function Dashboard() {
) : (
<div className="px-6 py-8 text-center text-white/50">
<CheckCircle className="w-12 h-12 mx-auto mb-3 text-success-400/50" />
<p> </p>
<p> </p>
</div>
)}
</div>
</div>
{/* NR 모달 */}
{showNRModal && (
<Modal title="구매요청 (NR) 목록" onClose={() => setShowNRModal(false)}>
<PurchaseTable data={purchaseNRList} />
</Modal>
)}
{/* CR 모달 */}
{showCRModal && (
<Modal title="구매요청 (CR) 목록" onClose={() => setShowCRModal(false)}>
<PurchaseTable data={purchaseCRList} />
</Modal>
)}
{/* 메모 보기 모달 */}
<NoteViewModal
isOpen={showNoteModal}
note={selectedNote}
onClose={() => setShowNoteModal(false)}
onEdit={(note) => {
setShowNoteModal(false);
setEditingNote(note);
setShowNoteEditModal(true);
}}
onDelete={handleNoteDelete}
/>
{/* 메모 편집 모달 */}
<NoteEditModal
isOpen={showNoteEditModal}
editingItem={editingNote}
processing={processing}
onClose={() => setShowNoteEditModal(false)}
onSave={handleNoteSave}
initialEditMode={true}
/>
{/* 메모 추가 모달 */}
<NoteEditModal
isOpen={showNoteAddModal}
editingItem={null}
processing={processing}
onClose={() => setShowNoteAddModal(false)}
onSave={handleNoteSave}
initialEditMode={true}
/>
{/* 할일 추가 모달 */}
{showTodoAddModal && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" onClick={() => setShowTodoAddModal(false)}>
<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" />
</h2>
<button onClick={() => setShowTodoAddModal(false)} 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={todoFormData.title}
onChange={(e) => setTodoFormData(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={todoFormData.expire}
onChange={(e) => setTodoFormData(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={todoFormData.remark}
onChange={(e) => setTodoFormData(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={todoFormData.request}
onChange={(e) => setTodoFormData(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">
{[
{ value: '0', label: '대기' },
{ value: '1', label: '진행' },
{ value: '3', label: '보류' },
{ value: '2', label: '취소' },
{ value: '5', label: '완료' },
].map((option) => (
<button
key={option.value}
type="button"
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${
todoFormData.status === option.value
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
}`}
>
{option.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-2"></label>
<select
value={todoFormData.seqno}
onChange={(e) => setTodoFormData(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={todoFormData.flag}
onChange={(e) => setTodoFormData(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-end space-x-3">
<button
type="button"
onClick={() => setShowTodoAddModal(false)}
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
>
</button>
<button
type="button"
onClick={handleTodoSave}
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" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
</button>
</div>
</div>
</div>
</div>
)}
{/* 할일 수정 모달 */}
{showTodoEditModal && editingTodo && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" onClick={() => setShowTodoEditModal(false)}>
<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">
<Edit2 className="w-5 h-5 mr-2" />
</h2>
<button onClick={() => setShowTodoEditModal(false)} 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={todoFormData.title}
onChange={(e) => setTodoFormData(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={todoFormData.expire}
onChange={(e) => setTodoFormData(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={todoFormData.remark}
onChange={(e) => setTodoFormData(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={todoFormData.request}
onChange={(e) => setTodoFormData(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">
{[
{ value: '0', label: '대기' },
{ value: '1', label: '진행' },
{ value: '3', label: '보류' },
{ value: '2', label: '취소' },
{ value: '5', label: '완료' },
].map((option) => (
<button
key={option.value}
type="button"
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${
todoFormData.status === option.value
? getStatusClass(option.value)
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
}`}
>
{option.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-white/70 text-sm font-medium mb-2"></label>
<select
value={todoFormData.seqno}
onChange={(e) => setTodoFormData(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={todoFormData.flag}
onChange={(e) => setTodoFormData(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>
<button
type="button"
onClick={handleTodoDelete}
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={() => setShowTodoEditModal(false)}
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
>
</button>
{editingTodo.status !== '5' && (
<button
type="button"
onClick={handleTodoComplete}
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={handleTodoUpdate}
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" />
)}
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* NR 모달 */}
{showNRModal && (
<Modal title="구매요청 (NR) 목록" onClose={() => setShowNRModal(false)}>
<PurchaseTable data={purchaseNRList} />
</Modal>
)}
{/* 우측 사이드바 - 메모 리스트 */}
<div className="w-60 space-y-4">
<div className="glass-effect rounded-2xl overflow-hidden">
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
<h3 className="text-base font-semibold text-white flex items-center">
<FileText className="w-4 h-4 mr-2" />
</h3>
<div className="flex items-center gap-2">
<button
onClick={handleNoteAdd}
className="p-1.5 rounded-lg bg-primary-500/20 text-primary-400 hover:bg-primary-500/30 transition-colors"
title="메모 추가"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => navigate('/note')}
className="p-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors"
title="전체보기"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
{/* CR 모달 */}
{showCRModal && (
<Modal title="구매요청 (CR) 목록" onClose={() => setShowCRModal(false)}>
<PurchaseTable data={purchaseCRList} />
</Modal>
)}
<div className="divide-y divide-white/10 max-h-[calc(100vh-200px)] overflow-y-auto">
{recentNotes.length > 0 ? (
recentNotes.map((note) => (
<div
key={note.idx}
className="px-4 py-2.5 hover:bg-white/5 transition-colors cursor-pointer group"
onClick={() => handleNoteClick(note)}
>
<div className="flex items-center gap-2">
{note.share ? (
<Share2 className="w-3 h-3 text-green-400 flex-shrink-0" />
) : (
<Lock className="w-3 h-3 text-blue-400 flex-shrink-0" />
)}
<p className="text-white text-sm truncate flex-1">
{(note.title || '제목 없음').length > 15 ? `${(note.title || '제목 없음').substring(0, 15)}...` : (note.title || '제목 없음')}
</p>
</div>
</div>
))
) : (
<div className="px-4 py-8 text-center text-white/50">
<FileText className="w-10 h-10 mx-auto mb-2 text-white/30" />
<p className="text-sm"> </p>
</div>
)}
</div>
</div>
</div>
</div>
);
}