feat: showProgress toggle and auto display flags on task type move

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-02 16:53:57 +09:00
parent 395680ea20
commit 849897b141
9 changed files with 62 additions and 16 deletions

View File

@@ -56,6 +56,7 @@ model Task {
showDescription Boolean @default(true) showDescription Boolean @default(true)
showStatus Boolean @default(true) showStatus Boolean @default(true)
showIssue Boolean @default(true) showIssue Boolean @default(true)
showProgress Boolean @default(true)
keywords String? keywords String?
creatorId String creatorId String
assigneeId String? assigneeId String?

View File

@@ -57,7 +57,7 @@ router.post('/', async (req, res, next) => {
try { try {
const { title, description, status, priority, quarter, category, const { title, description, status, priority, quarter, category,
section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate, section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate,
showDescription, showStatus, showIssue, keywords } = showDescription, showStatus, showIssue, showProgress, keywords } =
req.body as Record<string, any>; req.body as Record<string, any>;
if (!title || !quarter) { if (!title || !quarter) {
@@ -83,6 +83,7 @@ router.post('/', async (req, res, next) => {
showDescription: showDescription !== undefined ? showDescription === 'true' || showDescription === true : true, showDescription: showDescription !== undefined ? showDescription === 'true' || showDescription === true : true,
showStatus: showStatus !== undefined ? showStatus === 'true' || showStatus === true : true, showStatus: showStatus !== undefined ? showStatus === 'true' || showStatus === true : true,
showIssue: showIssue !== undefined ? showIssue === 'true' || showIssue === true : true, showIssue: showIssue !== undefined ? showIssue === 'true' || showIssue === true : true,
showProgress: showProgress !== undefined ? showProgress === 'true' || showProgress === true : true,
keywords: keywords || null, keywords: keywords || null,
assigneeId: assigneeId || null, assigneeId: assigneeId || null,
creatorId: (req.body as Record<string, string>).creatorId ?? 'system', creatorId: (req.body as Record<string, string>).creatorId ?? 'system',
@@ -103,7 +104,7 @@ router.patch('/:id', async (req, res, next) => {
const { title, description, status, priority, quarter, category, const { title, description, status, priority, quarter, category,
section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate, section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate,
showDescription, showStatus, showIssue, keywords } = showDescription, showStatus, showIssue, showProgress, keywords } =
req.body as Record<string, any>; req.body as Record<string, any>;
const task = await prisma.task.update({ const task = await prisma.task.update({
@@ -127,6 +128,7 @@ router.patch('/:id', async (req, res, next) => {
...(showDescription !== undefined && { showDescription: showDescription === true || showDescription === 'true' }), ...(showDescription !== undefined && { showDescription: showDescription === true || showDescription === 'true' }),
...(showStatus !== undefined && { showStatus: showStatus === true || showStatus === 'true' }), ...(showStatus !== undefined && { showStatus: showStatus === true || showStatus === 'true' }),
...(showIssue !== undefined && { showIssue: showIssue === true || showIssue === 'true' }), ...(showIssue !== undefined && { showIssue: showIssue === true || showIssue === 'true' }),
...(showProgress !== undefined && { showProgress: showProgress === true || showProgress === 'true' }),
...(keywords !== undefined && { keywords: keywords || null }), ...(keywords !== undefined && { keywords: keywords || null }),
}, },
}); });

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { Task } from '../../types'; import type { Task } from '../../types';
import { normalizeTaskType } from '../../lib/taskType'; import { normalizeTaskType, displayFlagsForTaskType } from '../../lib/taskType';
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: 'TODO', label: '대기' }, { value: 'TODO', label: '대기' },
@@ -26,6 +26,7 @@ export interface TaskFormData {
showDescription: boolean; showDescription: boolean;
showStatus: boolean; showStatus: boolean;
showIssue: boolean; showIssue: boolean;
showProgress: boolean;
keywords: string; keywords: string;
} }
@@ -62,6 +63,7 @@ export function TaskModal({ mode, task, defaultSection = 'HR', defaultQuarter =
showDescription: task?.showDescription ?? true, showDescription: task?.showDescription ?? true,
showStatus: task?.showStatus ?? true, showStatus: task?.showStatus ?? true,
showIssue: task?.showIssue ?? true, showIssue: task?.showIssue ?? true,
showProgress: task?.showProgress ?? true,
keywords: task?.keywords ?? '', keywords: task?.keywords ?? '',
}); });
@@ -130,7 +132,14 @@ export function TaskModal({ mode, task, defaultSection = 'HR', defaultQuarter =
<label className="block text-sm font-bold text-gray-500 mb-1.5"> </label> <label className="block text-sm font-bold text-gray-500 mb-1.5"> </label>
<select <select
value={form.taskType} value={form.taskType}
onChange={(e) => set('taskType', e.target.value)} onChange={(e) => {
const newType = e.target.value;
setForm((prev) => ({
...prev,
taskType: newType,
...displayFlagsForTaskType(newType),
}));
}}
className="w-full border border-gray-200 rounded-xl px-4 py-2.5 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition bg-white" className="w-full border border-gray-200 rounded-xl px-4 py-2.5 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition bg-white"
> >
<option value="기반업무"></option> <option value="기반업무"></option>
@@ -165,10 +174,21 @@ export function TaskModal({ mode, task, defaultSection = 'HR', defaultQuarter =
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-bold text-gray-500 mb-1.5"> <div className="flex items-center justify-between mb-1.5">
<label className="text-sm font-bold text-gray-500">
<span className="ml-2 font-black text-gray-800">{form.progress}%</span>
</label> <span className="ml-2 font-black text-gray-800">{form.progress}%</span>
</label>
<label className="flex items-center gap-1.5 cursor-pointer select-none">
<input
type="checkbox"
checked={form.showProgress}
onChange={(e) => set('showProgress', e.target.checked)}
className="w-4 h-4 accent-blue-500 cursor-pointer"
/>
<span className="text-xs font-semibold text-gray-400"> </span>
</label>
</div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
type="range" type="range"

View File

@@ -196,6 +196,7 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
showDescription: data.showDescription, showDescription: data.showDescription,
showStatus: data.showStatus, showStatus: data.showStatus,
showIssue: data.showIssue, showIssue: data.showIssue,
showProgress: data.showProgress,
keywords: data.keywords || null, keywords: data.keywords || null,
quarter: data.quarter, quarter: data.quarter,
priority: 'MEDIUM', priority: 'MEDIUM',
@@ -215,6 +216,7 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
startDate: data.startDate || null, dueDate: data.dueDate || null, startDate: data.startDate || null, dueDate: data.dueDate || null,
showDate: data.showDate, showDescription: data.showDescription, showDate: data.showDate, showDescription: data.showDescription,
showStatus: data.showStatus, showIssue: data.showIssue, showStatus: data.showStatus, showIssue: data.showIssue,
showProgress: data.showProgress,
keywords: data.keywords || null, keywords: data.keywords || null,
}, },
}); });

View File

@@ -88,12 +88,14 @@ export function TaskCard({
<span className="min-w-0 flex-1 truncate text-2xl font-black leading-snug text-slate-900"> <span className="min-w-0 flex-1 truncate text-2xl font-black leading-snug text-slate-900">
{task.title} {task.title}
</span> </span>
<span className={`mt-0.5 min-w-[4rem] shrink-0 text-right text-2xl font-black ${ {task.showProgress !== false && (
task.progress >= 70 ? 'text-emerald-500' : <span className={`mt-0.5 min-w-[4rem] shrink-0 text-right text-2xl font-black ${
task.progress >= 40 ? 'text-blue-400' : 'text-orange-400' task.progress >= 70 ? 'text-emerald-500' :
}`}> task.progress >= 40 ? 'text-blue-400' : 'text-orange-400'
{task.progress}% }`}>
</span> {task.progress}%
</span>
)}
</div> </div>
<div className="mt-1 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">

View File

@@ -71,6 +71,7 @@ export function TaskManager({ tasks, sectionOptions, quarter, onClose }: TaskMan
showDescription: data.showDescription, showDescription: data.showDescription,
showStatus: data.showStatus, showStatus: data.showStatus,
showIssue: data.showIssue, showIssue: data.showIssue,
showProgress: data.showProgress,
keywords: data.keywords || null, keywords: data.keywords || null,
quarter: data.quarter, quarter: data.quarter,
priority: 'MEDIUM', priority: 'MEDIUM',
@@ -92,6 +93,7 @@ export function TaskManager({ tasks, sectionOptions, quarter, onClose }: TaskMan
showDescription: data.showDescription, showDescription: data.showDescription,
showStatus: data.showStatus, showStatus: data.showStatus,
showIssue: data.showIssue, showIssue: data.showIssue,
showProgress: data.showProgress,
keywords: data.keywords || null, keywords: data.keywords || null,
}, },
}); });

View File

@@ -14,3 +14,15 @@ export function normalizeTaskType(taskType: string | null | undefined): string {
if (taskType === '상시업무') return '기반업무'; if (taskType === '상시업무') return '기반업무';
return taskType; return taskType;
} }
/** 실행과제 카드에 표시할 필드 플래그 (기반업무는 모두 숨김) */
export function displayFlagsForTaskType(taskType: string | null | undefined) {
const visible = isProjectTask(taskType);
return {
showDate: visible,
showDescription: visible,
showStatus: visible,
showIssue: visible,
showProgress: visible,
};
}

View File

@@ -18,7 +18,7 @@ import { DepartmentColumn } from '../components/dashboard/DepartmentColumn';
import { TaskManager } from '../components/dashboard/TaskManager'; import { TaskManager } from '../components/dashboard/TaskManager';
import { useSocket } from '../contexts/SocketContext'; import { useSocket } from '../contexts/SocketContext';
import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor'; import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor';
import { isRoutineTask } from '../lib/taskType'; import { isRoutineTask, displayFlagsForTaskType } from '../lib/taskType';
const QUARTER = '2026-Q2'; const QUARTER = '2026-Q2';
const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const; const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const;
@@ -134,8 +134,10 @@ export default function DashboardPage() {
if (targetSection !== srcSection) updateData.section = targetSection; if (targetSection !== srcSection) updateData.section = targetSection;
if (areaType === 'routine' && !isRoutineTask(draggedTask.taskType)) { if (areaType === 'routine' && !isRoutineTask(draggedTask.taskType)) {
updateData.taskType = '기반업무'; updateData.taskType = '기반업무';
Object.assign(updateData, displayFlagsForTaskType('기반업무'));
} else if (areaType === 'project' && isRoutineTask(draggedTask.taskType)) { } else if (areaType === 'project' && isRoutineTask(draggedTask.taskType)) {
updateData.taskType = '실행과제'; updateData.taskType = '실행과제';
Object.assign(updateData, displayFlagsForTaskType('실행과제'));
} }
if (Object.keys(updateData).length > 0) { if (Object.keys(updateData).length > 0) {
patchTask.mutate({ id: activeId, data: updateData }); patchTask.mutate({ id: activeId, data: updateData });
@@ -154,7 +156,9 @@ export default function DashboardPage() {
const updateData: Record<string, any> = {}; const updateData: Record<string, any> = {};
if (dstSection !== srcSection) updateData.section = dstSection; if (dstSection !== srcSection) updateData.section = dstSection;
if (typeChanged) { if (typeChanged) {
updateData.taskType = isRoutineTask(overTask.taskType) ? '기반업무' : '실행과제'; const newType = isRoutineTask(overTask.taskType) ? '기반업무' : '실행과제';
updateData.taskType = newType;
Object.assign(updateData, displayFlagsForTaskType(newType));
} }
patchTask.mutate({ id: activeId, data: updateData }); patchTask.mutate({ id: activeId, data: updateData });
if (dstSection !== srcSection) return; if (dstSection !== srcSection) return;

View File

@@ -31,6 +31,7 @@ export interface Task {
showDescription: boolean; showDescription: boolean;
showStatus: boolean; showStatus: boolean;
showIssue: boolean; showIssue: boolean;
showProgress: boolean;
keywords: string | null; keywords: string | null;
creatorId: string; creatorId: string;
assigneeId: string | null; assigneeId: string | null;