feat: implement multi-select status filter logic from reference hub

Support combined active chips for all/progress/hold/done and restore prior filters when toggling issue view.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-06 00:33:58 +09:00
parent 288e05f691
commit d14ff1997c
3 changed files with 291 additions and 149 deletions

View File

@@ -1,5 +1,10 @@
import { useState } from 'react';
import { isDetailWindowOpen } from '../../lib/dualMonitor';
import {
FILTER_ALL,
isStatusChipActive,
@@ -13,8 +18,11 @@ interface Stats {
interface Stats {
activeStatus: string;
onStatusChange: (status: string) => void;
total: number;
inProgress: number;
review: number;
done: number;
@@ -27,11 +35,16 @@ const STAT_ACCENT = {
interface DashboardHeaderProps {
quarter: string;
stats: Stats;
activeFilters: string[];
activeStatus,
onStatusChange,
issueFilterActive: boolean;
onToggleAll: () => void;
onToggleStatus: (key: CoreStatusFilter) => void;
onToggleIssue: () => void;
@@ -44,12 +57,48 @@ export function DashboardHeader({
const STAT_ACCENT = {
const statItems = [
{ label: '전체', value: stats.total, statusKey: '전체' as const },
{ label: '진행', value: stats.inProgress, statusKey: 'IN_PROGRESS' as const },
{ label: '보류', value: stats.review, statusKey: 'REVIEW' as const },
{ label: '완료', value: stats.done, statusKey: 'DONE' as const },
{ label: '이슈', value: stats.issues, statusKey: 'ISSUES' as const },
: 'text-[#ffdb3a]',
IN_PROGRESS: 'text-[#10b981]',
REVIEW: 'text-[#ff9f0a]',
DONE: 'text-[#b0b0b0]',
ISSUES: 'text-[#ff5252]',
} as const;
type StatKey = keyof typeof STAT_ACCENT;
export function DashboardHeader({
quarter,
stats,
activeFilters,
issueFilterActive,
onToggleAll,
onToggleStatus,
onToggleIssue,
onOpenDetailWindow,
onOpenTaskManager,
}: DashboardHeaderProps) {
const [detailViewActive, setDetailViewActive] = useState(isDetailWindowOpen);
const quarterLabel = quarter.replace(/^(\d{4})-Q(\d)$/, '$1 $2분기 업무');
@@ -75,10 +124,9 @@ export function DashboardHeader({
statusKey: StatKey;
statusKey={item.statusKey}
activeStatus={activeStatus}
onClick: () => void;
onClick={onStatusChange}
isActive: boolean;
}> = [
@@ -109,26 +157,21 @@ function StatDivider() {
isActive: isStatusChipActive('IN_PROGRESS', activeFilters, issueFilterActive),
},
statusKey: keyof typeof STAT_ACCENT;
activeStatus: string;
onClick: (key: string) => void;
{
label: '보류',
function StatClick({ label, value, statusKey, activeStatus, accent, onClick }: StatClickProps) {
const isActive = activeStatus === statusKey;
const handleActivate = () => onClick(isActive ? '전체' : statusKey);
value: stats.review,
statusKey: 'REVIEW',
onClick: () => onToggleStatus('REVIEW'),
onClick={handleActivate}
isActive: isStatusChipActive('REVIEW', activeFilters, issueFilterActive),
},
handleActivate();
{
label: '완료',
@@ -140,3 +183,4 @@ function StatClick({ label, value, statusKey, activeStatus, accent, onClick }: S
onClick: () => onToggleStatus('DONE'),
isActive: isStatusChipActive('DONE', activeFilters, issueFilterActive),

View File

@@ -0,0 +1,68 @@
import type { Task } from '../types';
export const FILTER_ALL = 'all' as const;
export const CORE_STATUS_FILTERS = ['IN_PROGRESS', 'REVIEW', 'DONE'] as const;
export type CoreStatusFilter = (typeof CORE_STATUS_FILTERS)[number];
export const DEFAULT_STATUS_FILTERS: string[] = [
FILTER_ALL,
...CORE_STATUS_FILTERS,
];
export function isCoreStatusFilter(key: string): key is CoreStatusFilter {
return (CORE_STATUS_FILTERS as readonly string[]).includes(key);
}
/** UI: 전체·진행·보류·완료 버튼 활성 표시 */
export function isStatusChipActive(
key: string,
filters: string[],
issueMode: boolean,
): boolean {
if (issueMode) return false;
if (key === FILTER_ALL) return filters.includes(FILTER_ALL);
if (isCoreStatusFilter(key)) {
return filters.includes(FILTER_ALL) || filters.includes(key);
}
return false;
}
export function toggleAllFilter(current: string[]): string[] {
if (current.includes(FILTER_ALL)) {
return ['IN_PROGRESS'];
}
return [...DEFAULT_STATUS_FILTERS];
}
export function toggleCoreFilter(current: string[], key: CoreStatusFilter): string[] {
if (current.includes(FILTER_ALL)) {
return CORE_STATUS_FILTERS.filter((k) => k !== key);
}
const withoutAll = current.filter((f) => f !== FILTER_ALL);
if (withoutAll.includes(key)) {
const next = withoutAll.filter((k) => k !== key);
return next.length === 0 ? [...DEFAULT_STATUS_FILTERS] : next;
}
const next = [...withoutAll, key];
if (CORE_STATUS_FILTERS.every((k) => next.includes(k))) {
return [FILTER_ALL, ...next];
}
return next;
}
export function taskMatchesStatusFilters(task: Task, filters: string[]): boolean {
if (filters.includes(FILTER_ALL)) return true;
return filters.some((key) => {
if (key === 'IN_PROGRESS') return task.status === 'IN_PROGRESS';
if (key === 'REVIEW') {
return task.status === 'REVIEW' || task.status === 'CANCELLED' || task.status === 'TODO';
}
if (key === 'DONE') return task.status === 'DONE';
return false;
});
}

View File

@@ -19,6 +19,13 @@ import { TaskManager } from '../components/dashboard/TaskManager';
import { useSocket } from '../contexts/SocketContext';
import { sendTaskSelected, openDetailWindow } from '../lib/dualMonitor';
import { isRoutineTask, displayFlagsForTaskType } from '../lib/taskType';
import {
DEFAULT_STATUS_FILTERS,
taskMatchesStatusFilters,
toggleAllFilter,
toggleCoreFilter,
type CoreStatusFilter,
} from '../lib/statusFilters';
const QUARTER = '2026-Q2';
const SECTIONS = ['인사관리', '학습성장', '운영지원', '전산관리'] as const;
@@ -31,7 +38,9 @@ const COLUMN_STYLES = [
] as const;
export default function DashboardPage() {
const [activeStatus, setActiveStatus] = useState('전체');
const [activeFilters, setActiveFilters] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
const [filtersBeforeIssue, setFiltersBeforeIssue] = useState<string[]>([...DEFAULT_STATUS_FILTERS]);
const [issueFilterActive, setIssueFilterActive] = useState(false);
const [showTaskManager, setShowTaskManager] = useState(false);
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
const [columnOrders, setColumnOrders] = useState<Record<string, string[]>>({});
@@ -188,12 +197,30 @@ export default function DashboardPage() {
};
const filtered = tasks.filter((t) => {
if (activeStatus === '전체') return true;
if (activeStatus === 'ISSUES') return !!t.issueNote;
if (activeStatus === 'REVIEW') return t.status === 'REVIEW' || t.status === 'CANCELLED' || t.status === 'TODO';
return t.status === activeStatus;
if (issueFilterActive) return !!t.issueNote;
return taskMatchesStatusFilters(t, activeFilters);
});
const handleToggleAll = () => {
setIssueFilterActive(false);
setActiveFilters((prev) => toggleAllFilter(prev));
};
const handleToggleStatus = (key: CoreStatusFilter) => {
setIssueFilterActive(false);
setActiveFilters((prev) => toggleCoreFilter(prev, key));
};
const handleToggleIssue = () => {
if (issueFilterActive) {
setIssueFilterActive(false);
setActiveFilters([...filtersBeforeIssue]);
return;
}
setFiltersBeforeIssue([...activeFilters]);
setIssueFilterActive(true);
};
const sectionOptions = SECTIONS.map((s) => ({
value: s,
label: colConfigs?.find((c) => c.key === s)?.title ?? s,
@@ -214,8 +241,11 @@ export default function DashboardPage() {
<DashboardHeader
quarter={QUARTER}
stats={stats}
activeStatus={activeStatus}
onStatusChange={setActiveStatus}
activeFilters={activeFilters}
issueFilterActive={issueFilterActive}
onToggleAll={handleToggleAll}
onToggleStatus={handleToggleStatus}
onToggleIssue={handleToggleIssue}
onOpenDetailWindow={() => { openDetailWindow(); }}
onOpenTaskManager={() => setShowTaskManager(true)}
/>