feat: showProgress toggle and auto display flags on task type move
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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?
|
||||||
|
|||||||
@@ -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 }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user