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

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
import { comms } from '@/communication';
import { JobReportDayItem, HolidayItem } from '@/types';
import { HolidayItem } from '@/types';
interface JobReportDayDialogProps {
isOpen: boolean;
@@ -32,8 +32,6 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
const [dayColumns, setDayColumns] = useState<DayColumn[]>([]);
const [userRows, setUserRows] = useState<UserRow[]>([]);
const [currentUserId, setCurrentUserId] = useState<string>('');
const [currentUserLevel, setCurrentUserLevel] = useState<number>(0);
const [authLevel, setAuthLevel] = useState<number>(0);
const [canViewAll, setCanViewAll] = useState<boolean>(false);
// 요일 배열
@@ -54,12 +52,10 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
const userId = loginStatus.User.Id;
const userLevel = loginStatus.User.Level || 0;
setCurrentUserId(userId);
setCurrentUserLevel(userLevel);
// 업무일지(jobreport) 권한 가져오기
const authResponse = await comms.checkAuth('jobreport', 5);
const jobReportAuthLevel = authResponse.EffectiveLevel || 0;
setAuthLevel(jobReportAuthLevel);
// 유효 권한 레벨 = Max(사용자레벨, 권한레벨)
const effectiveLevel = Math.max(userLevel, jobReportAuthLevel);

View File

@@ -115,6 +115,15 @@ const dropdownMenus: DropdownMenuConfig[] = [
{ type: 'link', path: '/mail-form', icon: Mail, label: '메일양식' },
],
},
{
label: '문서',
icon: FileText,
items: [
{ type: 'link', path: '/note', icon: FileText, label: '메모장' },
{ type: 'link', path: '/patch-list', icon: FileText, label: '패치 내역' },
{ type: 'link', path: '/mail-list', icon: Mail, label: '메일 내역' },
],
},
];
function DropdownNavMenu({

View File

@@ -0,0 +1,283 @@
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { X, Save, User, Calendar } from 'lucide-react';
import { NoteItem } from '@/types';
import { comms } from '@/communication';
interface NoteEditModalProps {
isOpen: boolean;
editingItem: NoteItem | null;
processing: boolean;
onClose: () => void;
onSave: (formData: {
pdate: string;
title: string;
uid: string;
description: string;
share: boolean;
guid: string;
}) => void;
initialEditMode?: boolean;
}
export function NoteEditModal({
isOpen,
editingItem,
processing,
onClose,
onSave,
initialEditMode = false,
}: NoteEditModalProps) {
const [pdate, setPdate] = useState('');
const [title, setTitle] = useState('');
const [uid, setUid] = useState('');
const [description, setDescription] = useState('');
const [share, setShare] = useState(false);
const [guid, setGuid] = useState('');
const [isEditMode, setIsEditMode] = useState(false);
// 현재 로그인 사용자 정보 로드
const [currentUserId, setCurrentUserId] = useState('');
const [currentUserLevel, setCurrentUserLevel] = useState(0);
useEffect(() => {
const loadUserInfo = async () => {
try {
const loginStatus = await comms.checkLoginStatus();
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
setCurrentUserId(loginStatus.User.Id);
setCurrentUserLevel(loginStatus.User.Level);
}
} catch (error) {
console.error('로그인 정보 로드 오류:', error);
}
};
loadUserInfo();
}, []);
// 모달이 열릴 때 폼 데이터 초기화
useEffect(() => {
if (isOpen) {
if (editingItem) {
// 기존 메모
setPdate(editingItem.pdate ? editingItem.pdate.split('T')[0] : '');
setTitle(editingItem.title || '');
setUid(editingItem.uid || currentUserId);
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]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({ pdate, title, uid, description, share, guid });
};
if (!isOpen) return null;
// 권한 체크: 자신의 메모이거나 관리자만 수정 가능
const canEdit = !editingItem || editingItem.uid === currentUserId || currentUserLevel >= 5;
const canChangeUser = currentUserLevel >= 5;
const canChangeShare = !editingItem || editingItem.uid === currentUserId;
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="flex items-center justify-between px-6 py-4 border-b border-white/10">
<h2 className="text-xl font-bold text-white">
{!editingItem ? '새 메모 작성' : '메모 편집'}
</h2>
<button
onClick={onClose}
className="text-white/50 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 본문 - 좌우 레이아웃 */}
<form onSubmit={handleSubmit} className="overflow-y-auto max-h-[calc(90vh-140px)]">
<div className="flex gap-6 p-6">
{/* 좌측 - 정보 영역 */}
<div className="w-64 flex-shrink-0 space-y-4">
{/* 날짜 */}
<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>
)}
</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>
)}
</div>
{/* 공유 여부 */}
<div>
<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>
{!canEdit && isEditMode && (
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-red-300 text-xs">
.
</div>
)}
</div>
{/* 우측 - 제목 및 내용 영역 */}
<div className="flex-1 space-y-4">
{/* 제목 */}
<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>
)}
</div>
{/* 내용 */}
<div className="flex-1">
<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>
)}
</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">
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
disabled={processing}
>
</button>
{isEditMode && canEdit && (
<button
type="submit"
onClick={handleSubmit}
disabled={processing}
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors flex items-center disabled:opacity-50"
>
{processing ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></div>
...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
</>
)}
</button>
)}
</div>
</div>
</div>,
document.body
);
}

View File

@@ -0,0 +1,74 @@
import { createPortal } from 'react-dom';
import { X, Trash2 } from 'lucide-react';
import { NoteItem } from '@/types';
interface NoteViewModalProps {
isOpen: boolean;
note: NoteItem | null;
onClose: () => void;
onEdit: (note: NoteItem) => void;
onDelete?: (note: NoteItem) => void;
}
export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteViewModalProps) {
if (!isOpen || !note) return null;
const handleDelete = () => {
if (window.confirm('정말 삭제하시겠습니까?')) {
onDelete?.(note);
}
};
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="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>
<button
onClick={onClose}
className="text-white/50 hover:text-white transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 본문 - 메모 내용만 표시 */}
<div className="overflow-y-auto max-h-[calc(80vh-140px)] p-6">
<div className="bg-white/10 border border-white/20 rounded-lg p-4 text-white whitespace-pre-wrap min-h-[300px]">
{note.description || '내용이 없습니다.'}
</div>
</div>
{/* 하단 버튼 */}
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10 bg-white/5">
<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"
>
<Trash2 className="w-4 h-4" />
</button>
<div className="flex items-center gap-3">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
>
</button>
<button
onClick={() => {
onClose();
onEdit(note);
}}
className="px-4 py-2 bg-success-500 hover:bg-success-600 text-white rounded-lg transition-colors"
>
</button>
</div>
</div>
</div>
</div>,
document.body
);
}