style: apply soft dashboard card visual design

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
EENE Dashboard
2026-06-01 15:42:14 +09:00
parent f6a7fab234
commit 0a1cb3efe7
4 changed files with 43 additions and 40 deletions

View File

@@ -21,22 +21,22 @@ export function DashboardHeader({ quarter, stats, activeStatus, onStatusChange,
const todayStr = `${today.getMonth() + 1}${today.getDate()}`;
return (
<header className="bg-[#1e3260] text-white px-5 py-3 shrink-0 relative flex items-center">
<header className="relative flex shrink-0 items-center bg-[#1e3260] px-6 py-3 text-white shadow-[0_10px_28px_rgba(15,23,42,0.22)]">
{/* ── 왼쪽: 팀명 + 분기 ── */}
<div className="flex items-center gap-3 shrink-0">
<div className="shrink-0">
<span className="text-base font-bold tracking-tight"></span>
<span className="ml-1.5 text-xs text-blue-300 font-medium">EE&amp;E</span>
<span className="text-base font-black tracking-tight"></span>
<span className="ml-1.5 text-xs font-semibold text-blue-200/90">EE&amp;E</span>
</div>
<div className="w-px h-5 bg-white/20" />
<span className="text-sm font-semibold text-blue-200 shrink-0">
<span className="shrink-0 text-sm font-bold text-blue-100/90">
{quarter.replace(/^(\d{4})-Q(\d)$/, '$1년 $2분기')}
</span>
</div>
{/* ── 가운데: 상태 필터 버튼 (절대 중앙 정렬) ── */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-1.5 text-sm">
<div className="absolute left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-2xl bg-white/8 px-2 py-1 text-sm ring-1 ring-white/10 backdrop-blur">
<StatButton label="전체" value={stats.total} statusKey="전체" activeStatus={activeStatus} onClick={onStatusChange} color="text-white" activeColor="bg-white/20" />
<StatButton label="진행" value={stats.inProgress} statusKey="IN_PROGRESS" activeStatus={activeStatus} onClick={onStatusChange} color="text-blue-300" activeColor="bg-blue-500/40" />
<StatButton label="보류" value={stats.review} statusKey="REVIEW" activeStatus={activeStatus} onClick={onStatusChange} color="text-orange-300" activeColor="bg-orange-500/40" />
@@ -55,20 +55,20 @@ export function DashboardHeader({ quarter, stats, activeStatus, onStatusChange,
<button
onClick={onOpenDetailWindow}
title="우측 모니터에 상세 창 열기"
className="flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors border border-white/10 hover:border-white/30"
className="flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/10 px-3 py-1.5 text-xs font-bold text-white/75 transition-colors hover:border-white/30 hover:bg-white/20"
>
<span>🖥</span>
<span> </span>
</button>
<button
onClick={onOpenTaskManager}
className="flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors border border-white/10 hover:border-white/30"
className="flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/10 px-3 py-1.5 text-xs font-bold text-white/75 transition-colors hover:border-white/30 hover:bg-white/20"
>
<span>📋</span>
<span></span>
</button>
<div className="w-px h-5 bg-white/20" />
<span className="text-xs text-white/50">{todayStr}</span>
<span className="text-xs font-semibold text-white/50">{todayStr}</span>
</div>
</header>
@@ -90,13 +90,13 @@ function StatButton({ label, value, statusKey, activeStatus, onClick, color, act
return (
<button
onClick={() => onClick(isActive ? '전체' : statusKey)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border transition-all cursor-pointer select-none ${
className={`flex cursor-pointer select-none items-center gap-1.5 rounded-xl border px-3 py-1.5 transition-all ${
isActive
? `${activeColor} border-white/30 shadow-inner`
: 'border-white/10 hover:border-white/30 hover:bg-white/10 active:scale-95'
: 'border-white/10 bg-white/5 hover:border-white/30 hover:bg-white/10 active:scale-95'
}`}
>
<span className="text-white/60 text-xs font-semibold">{label}</span>
<span className="text-xs font-semibold text-white/60">{label}</span>
<span className={`font-black text-sm ${color}`}>{value}</span>
</button>
);

View File

@@ -212,19 +212,19 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
return (
<>
<div className="flex flex-col bg-gray-50 rounded-2xl overflow-hidden border border-gray-200 min-h-0">
<div className="flex min-h-0 flex-col overflow-hidden rounded-[1.6rem] border border-white/80 bg-white/70 shadow-[0_18px_45px_rgba(15,23,42,0.10)] ring-1 ring-slate-200/60 backdrop-blur">
{/* 컬럼 헤더 (noHeader 시 숨김) */}
{!noHeader && (
<div
className="h-10 shrink-0 select-none flex items-center justify-center px-4 gap-2 relative"
className="relative flex h-10 shrink-0 select-none items-center justify-center gap-2 px-4 shadow-sm"
style={headerStyle}
onContextMenu={(e) => { e.preventDefault(); setCtxMenu({ x: e.clientX, y: e.clientY, type: 'header' }); }}
>
<span className="text-white font-black text-base tracking-tight truncate">{displayTitle}</span>
<span className="truncate text-base font-black tracking-tight text-white drop-shadow-sm">{displayTitle}</span>
{titleEnState && (
<span className="text-white/60 text-xs font-medium truncate hidden xl:block">{titleEnState}</span>
)}
<span className="absolute right-3 shrink-0 text-xs font-black bg-white/20 text-white px-2 py-0.5 rounded-full">
<span className="absolute right-3 shrink-0 rounded-full bg-white/25 px-2 py-0.5 text-xs font-black text-white ring-1 ring-white/20">
{tasks.length}
</span>
</div>
@@ -232,13 +232,13 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
{/* 프로젝트 카드 목록 (스크롤 영역) */}
<div
className="flex-1 overflow-y-auto p-4 min-h-0"
className="min-h-0 flex-1 overflow-y-auto bg-gradient-to-b from-slate-50/80 to-white/70 p-4"
onContextMenu={handleListContextMenu}
>
{(() => {
const projectTasks = orderedTasks.filter((t) => t.taskType !== '상시업무');
return projectTasks.length === 0 ? (
<div className="flex items-center justify-center h-40 text-2xl text-gray-300">
<div className="flex h-40 items-center justify-center text-2xl text-slate-300">
</div>
) : (
@@ -257,10 +257,10 @@ export function DepartmentColumn({ title: initialTitle, titleEn, subtitle: initi
{(() => {
const routineTasks = orderedTasks.filter((t) => t.taskType === '상시업무');
return (
<div className="shrink-0 border-t border-gray-200" style={{ height: 300 }}>
<div className="shrink-0 border-t border-slate-200/80 bg-white/75" style={{ height: 300 }}>
<div className="h-full overflow-y-auto p-4">
{routineTasks.length === 0 ? (
<div className="flex items-center justify-center h-full text-base text-gray-300">
<div className="flex h-full items-center justify-center text-base text-slate-300">
</div>
) : (

View File

@@ -12,11 +12,11 @@ import type { Task } from '../../types';
const STATUS_STYLE: Record<string, string> = {
IN_PROGRESS: 'bg-blue-500 text-white',
REVIEW: 'bg-orange-400 text-white',
TODO: 'bg-gray-300 text-gray-700',
DONE: 'bg-emerald-500 text-white',
CANCELLED: 'bg-gray-200 text-gray-500',
IN_PROGRESS: 'bg-blue-500 text-white shadow-blue-500/20',
REVIEW: 'bg-amber-400 text-white shadow-amber-400/20',
TODO: 'bg-slate-200 text-slate-600 shadow-slate-300/20',
DONE: 'bg-emerald-500 text-white shadow-emerald-500/20',
CANCELLED: 'bg-slate-200 text-slate-400 shadow-slate-300/20',
};
const STATUS_LABEL: Record<string, string> = {
@@ -163,7 +163,7 @@ export function TaskCard({
style={dragStyle}
{...dragAttributes}
{...dragListeners}
className="bg-white rounded-2xl px-5 py-3 mb-3 shadow-sm border border-gray-100 hover:shadow-md hover:border-gray-200 transition-all select-none cursor-grab active:cursor-grabbing overflow-hidden"
className="mb-3 cursor-grab select-none overflow-hidden rounded-[1.35rem] border border-white/80 bg-white px-5 py-4 shadow-[0_10px_28px_rgba(15,23,42,0.08)] ring-1 ring-slate-200/60 transition-all hover:-translate-y-0.5 hover:shadow-[0_18px_34px_rgba(15,23,42,0.14)] active:cursor-grabbing"
onPointerDown={(e) => {
// 우클릭(button !== 0)을 dnd-kit에 전달하지 않음
// dnd-kit은 pointerdown→pointerup 시 synthetic click을 발화하는데
@@ -177,11 +177,11 @@ export function TaskCard({
{/* ── 상단: 제목 + 진행률 ── */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2 min-w-0 flex-1">
<span className="text-2xl font-bold text-gray-900 leading-snug min-w-0 truncate">
<span className="min-w-0 truncate text-2xl font-black leading-snug text-slate-900">
{task.title}
</span>
</div>
<span className={`shrink-0 text-2xl font-black mt-0.5 min-w-[4rem] text-right ${
<span className={`mt-0.5 min-w-[4rem] shrink-0 text-right text-2xl font-black ${
task.progress >= 70 ? 'text-emerald-500' :
task.progress >= 40 ? 'text-blue-400' :
'text-orange-400'
@@ -193,26 +193,26 @@ export function TaskCard({
{/* ── 기간 + 상태 ── */}
<div className="mt-1 flex items-center gap-2">
<span className="text-sm text-gray-400 font-medium flex-1 truncate">
<span className="flex-1 truncate text-sm font-semibold text-slate-400">
{task.showDate && (task.startDate || task.dueDate)
? `${task.startDate ? fmtDate(task.startDate) : '?'} ~ ${task.dueDate ? fmtDate(task.dueDate) : '?'}`
: ''}
</span>
<span className={`text-sm font-bold px-2.5 py-0.5 rounded-full ${STATUS_STYLE[task.status]} shrink-0`}>
<span className={`shrink-0 rounded-full px-2.5 py-0.5 text-sm font-black shadow-sm ${STATUS_STYLE[task.status]}`}>
{STATUS_LABEL[task.status]}
</span>
</div>
{/* ── description ── */}
{task.description && (
<div className="mt-1.5 text-2xl text-gray-700 truncate">
<div className="mt-2 truncate text-2xl text-slate-700">
{task.description}
</div>
)}
{/* ── 이슈 메모 ── */}
{task.issueNote && (
<div className="mt-1 flex gap-2 text-2xl text-red-500 min-w-0">
<div className="mt-1.5 flex min-w-0 gap-2 rounded-xl bg-red-50/80 px-2 py-1 text-2xl text-red-500">
<span className="shrink-0"></span>
<span className="truncate">{task.issueNote}</span>
</div>

View File

@@ -73,7 +73,10 @@ export default function DashboardPage() {
}
return (
<div className="relative flex flex-col h-screen bg-slate-100 overflow-hidden" style={{ fontSize: '18px' }}>
<div
className="relative flex h-screen flex-col overflow-hidden bg-[#eef2f5]"
style={{ fontSize: '18px' }}
>
<DashboardHeader
quarter={QUARTER}
stats={stats}
@@ -83,29 +86,29 @@ export default function DashboardPage() {
onOpenTaskManager={() => setShowTaskManager(true)}
/>
<main className="relative flex-1 overflow-hidden min-h-0 flex">
<main className="relative flex min-h-0 flex-1 overflow-hidden px-5 py-5">
{/* ── 맨 왼쪽 구분 라벨 컬럼 ── */}
<div className="shrink-0 w-8 flex flex-col border-r border-gray-200 bg-gray-50">
<div className="mr-4 flex w-16 shrink-0 flex-col overflow-hidden rounded-[2rem] bg-white shadow-[0_16px_40px_rgba(15,23,42,0.12)] ring-1 ring-slate-200/70">
{/* 헤더 높이 맞춤 (DepartmentColumn 헤더 h-10) */}
<div className="h-10 shrink-0 border-b border-gray-200" />
<div className="h-10 shrink-0 border-b border-slate-100 bg-slate-50" />
{/* 실행과제 영역 (flex-1) */}
<div className="flex-1 flex items-center justify-center bg-blue-50/60 border-b border-gray-200">
<div className="flex flex-1 items-center justify-center border-b border-slate-100 bg-gradient-to-b from-slate-50 to-white">
<span
className="text-xs font-black text-gray-800 tracking-widest select-none"
className="select-none text-sm font-black tracking-[0.35em] text-slate-800"
style={{ writingMode: 'vertical-rl' }}
></span>
</div>
{/* 기반업무 영역 (고정 300px) */}
<div className="shrink-0 flex items-center justify-center bg-amber-50/60" style={{ height: 300 }}>
<div className="flex shrink-0 items-center justify-center bg-gradient-to-b from-white to-slate-50" style={{ height: 300 }}>
<span
className="text-xs font-black text-gray-800 tracking-widest select-none"
className="select-none text-sm font-black tracking-[0.35em] text-slate-800"
style={{ writingMode: 'vertical-rl' }}
></span>
</div>
</div>
<div className="grid flex-1 h-full grid-cols-4 overflow-hidden min-h-0">
<div className="grid h-full min-h-0 flex-1 grid-cols-4 gap-4 overflow-hidden">
<DepartmentColumn
title="인사관리"
titleEn="HR Management"