import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import {
ShoppingCart,
FileCheck,
AlertTriangle,
CheckCircle,
Flag,
RefreshCw,
ClipboardList,
Clock,
FileText,
Share2,
Lock,
List,
Plus,
X,
Loader2,
Edit2,
Trash2,
} from 'lucide-react';
import { comms } from '@/communication';
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;
value: number | string;
icon: React.ReactNode;
color: string;
onClick?: () => void;
}
function StatCard({ title, value, icon, color, onClick }: StatCardProps) {
return (
);
}
export function Dashboard() {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// 통계 데이터
const [purchaseNR, setPurchaseNR] = useState(0);
const [purchaseCR, setPurchaseCR] = useState(0);
const [todoCount, setTodoCount] = useState(0);
const [todayWorkHrs, setTodayWorkHrs] = useState(0);
// 목록 데이터
const [urgentTodos, setUrgentTodos] = useState([]);
const [purchaseNRList, setPurchaseNRList] = useState([]);
const [purchaseCRList, setPurchaseCRList] = useState([]);
const [recentNotes, setRecentNotes] = useState([]);
// 모달 상태
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(null);
const [editingNote, setEditingNote] = useState(null);
const [processing, setProcessing] = useState(false);
// 할일 추가 모달 상태
const [showTodoAddModal, setShowTodoAddModal] = useState(false);
const [showTodoEditModal, setShowTodoEditModal] = useState(false);
const [editingTodo, setEditingTodo] = useState(null);
const [todoFormData, setTodoFormData] = useState({
title: '',
remark: '',
expire: '',
seqno: 0 as TodoPriority,
flag: false,
request: '',
status: '0' as TodoStatus,
});
const loadDashboardData = useCallback(async () => {
try {
// 오늘 날짜 (로컬 시간 기준)
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
// 현재 로그인 사용자 ID 가져오기
let currentUserId = '';
try {
const loginStatus = await comms.checkLoginStatus();
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
currentUserId = loginStatus.User.Id;
}
} catch (error) {
console.error('로그인 정보 로드 오류:', error);
}
// 병렬로 데이터 로드
const [
purchaseCount,
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);
setPurchaseCR(purchaseCount.CR);
if (urgentTodosResponse.Success && urgentTodosResponse.Data) {
setUrgentTodos(urgentTodosResponse.Data.slice(0, 5));
}
if (allTodosResponse.Success && allTodosResponse.Data) {
// 진행, 대기 상태의 할일만 카운트 (보류, 취소 제외)
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: 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 {
setLoading(false);
setRefreshing(false);
}
}, []);
const loadNRList = async () => {
try {
const list = await comms.getPurchaseNRList();
setPurchaseNRList(list);
setShowNRModal(true);
} catch (error) {
console.error('NR 목록 로드 오류:', error);
}
};
const loadCRList = async () => {
try {
const list = await comms.getPurchaseCRList();
setPurchaseCRList(list);
setShowCRModal(true);
} catch (error) {
console.error('CR 목록 로드 오류:', error);
}
};
useEffect(() => {
loadDashboardData();
// 30초마다 자동 새로고침
const interval = setInterval(loadDashboardData, 30000);
return () => clearInterval(interval);
}, [loadDashboardData]);
const handleRefresh = () => {
setRefreshing(true);
loadDashboardData();
};
const getStatusText = (status: string) => {
switch (status) {
case '0': return '대기';
case '1': return '진행';
case '2': return '취소';
case '3': return '보류';
case '5': return '완료';
default: return '대기';
}
};
const getStatusClass = (status: string) => {
switch (status) {
case '0': return 'bg-gray-500/20 text-gray-300';
case '1': return 'bg-primary-500/20 text-primary-300';
case '2': return 'bg-danger-500/20 text-danger-300';
case '3': return 'bg-warning-500/20 text-warning-300';
case '5': return 'bg-success-500/20 text-success-300';
default: return 'bg-white/10 text-white/50';
}
};
const getPriorityText = (seqno: number) => {
switch (seqno) {
case 1: return '중요';
case 2: return '매우 중요';
case 3: return '긴급';
default: return '보통';
}
};
const getPriorityClass = (seqno: number) => {
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';
}
};
if (loading) {
return (
);
}
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);
}
};
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 (
{/* 메인 컨텐츠 */}
{/* 헤더 */}
오늘의 현황
{/* 통계 카드 */}
}
color="text-primary-400"
onClick={loadNRList}
/>
}
color="text-success-400"
onClick={loadCRList}
/>
}
color="text-warning-400"
onClick={() => navigate('/todo')}
/>
}
color="text-cyan-400"
onClick={() => navigate('/jobreport')}
/>
{/* 할일 목록 */}
할일
{urgentTodos.length > 0 ? (
urgentTodos.map((todo) => (
handleTodoEdit(todo)}
>
{todo.flag && (
)}
{todo.title || '제목 없음'}
{todo.remark}
{getPriorityText(todo.seqno)}
{getStatusText(todo.status)}
{todo.expire && (
{new Date(todo.expire).toLocaleDateString('ko-KR')}
)}
))
) : (
)}
{/* NR 모달 */}
{showNRModal && (
setShowNRModal(false)}>
)}
{/* CR 모달 */}
{showCRModal && (
setShowCRModal(false)}>
)}
{/* 메모 보기 모달 */}
setShowNoteModal(false)}
onEdit={(note) => {
setShowNoteModal(false);
setEditingNote(note);
setShowNoteEditModal(true);
}}
onDelete={handleNoteDelete}
/>
{/* 메모 편집 모달 */}
setShowNoteEditModal(false)}
onSave={handleNoteSave}
initialEditMode={true}
/>
{/* 메모 추가 모달 */}
setShowNoteAddModal(false)}
onSave={handleNoteSave}
initialEditMode={true}
/>
{/* 할일 추가 모달 */}
{showTodoAddModal && (
setShowTodoAddModal(false)}>
e.stopPropagation()}
>
{/* 헤더 */}
할일 추가
{/* 내용 */}
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="업무 요청자를 입력하세요"
/>
{[
{ value: '0', label: '대기' },
{ value: '1', label: '진행' },
{ value: '3', label: '보류' },
{ value: '2', label: '취소' },
{ value: '5', label: '완료' },
].map((option) => (
))}
{/* 푸터 */}
)}
{/* 할일 수정 모달 */}
{showTodoEditModal && editingTodo && (
setShowTodoEditModal(false)}>
e.stopPropagation()}
>
{/* 헤더 */}
할일 수정
{/* 내용 */}
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="업무 요청자를 입력하세요"
/>
{[
{ value: '0', label: '대기' },
{ value: '1', label: '진행' },
{ value: '3', label: '보류' },
{ value: '2', label: '취소' },
{ value: '5', label: '완료' },
].map((option) => (
))}
{/* 푸터 */}
{/* 왼쪽: 삭제 버튼 */}
{/* 오른쪽: 취소, 완료, 수정 버튼 */}
{editingTodo.status !== '5' && (
)}
)}
{/* 우측 사이드바 - 메모 리스트 */}
최근 메모
{recentNotes.length > 0 ? (
recentNotes.map((note) => (
handleNoteClick(note)}
>
{note.share ? (
) : (
)}
{(note.title || '제목 없음').length > 15 ? `${(note.title || '제목 없음').substring(0, 15)}...` : (note.title || '제목 없음')}
))
) : (
)}
);
}
// 모달 컴포넌트
interface ModalProps {
title: string;
onClose: () => void;
children: React.ReactNode;
}
function Modal({ title, onClose, children }: ModalProps) {
return (
);
}
// 구매 테이블 컴포넌트
function PurchaseTable({ data }: { data: PurchaseItem[] }) {
if (data.length === 0) {
return (
대기 중인 구매요청이 없습니다
);
}
return (
| 요청일 |
공정 |
품명 |
규격 |
수량 |
단가 |
금액 |
{data.map((item, idx) => (
| {item.pdate} |
{item.process} |
{item.pumname} |
{item.pumscale} |
{item.pumqtyreq?.toLocaleString()} {item.pumunit}
|
{item.pumprice?.toLocaleString()}
|
{item.pumamt?.toLocaleString()}
|
))}
);
}