feat(agent-office): 모바일 반응형 세로 스택 + 작업 시간 표기 개선

- 768px 이하에서 대시보드 세로 스택 + 에이전트 카드 아코디언 토글
- waiting/알림 있을 때 자동 펼침 및 좌측 강조 바
- 픽셀 오피스 캔버스 모바일 높이 140px로 축소 후 상단 배치
- 최근 작업 시간: completed_at 우선 + 오늘/어제/MM-DD HH:MM 포맷

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 00:19:32 +09:00
parent be46da0a1f
commit 104a34912f
2 changed files with 115 additions and 4 deletions

View File

@@ -70,6 +70,21 @@
flex-shrink: 0; flex-shrink: 0;
} }
.ao-col-chevron {
display: none;
color: #666;
font-size: 0.8rem;
margin-left: 4px;
}
.ao-col-body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.ao-col-name { .ao-col-name {
font-weight: bold; font-weight: bold;
font-size: 0.9rem; font-size: 0.9rem;
@@ -303,3 +318,71 @@
font-size: 0.7rem; font-size: 0.7rem;
margin-left: 4px; margin-left: 4px;
} }
/* Mobile: vertical stack + accordion */
@media (max-width: 768px) {
.ao-page {
height: auto;
min-height: 100vh;
}
.ao-dashboard {
flex-direction: column;
gap: 1px;
overflow: visible;
flex: none;
}
.ao-col {
flex: none;
overflow: visible;
}
.ao-col-header {
cursor: pointer;
user-select: none;
padding: 12px 14px;
}
.ao-col-chevron {
display: inline;
}
.ao-col--collapsed .ao-col-body {
display: none;
}
.ao-col--attention {
box-shadow: inset 3px 0 0 #fbbf24;
}
.ao-col-tasks {
max-height: 260px;
}
.ao-office-section {
height: 140px;
order: -1;
border-top: none;
border-bottom: 2px solid #2a2a4a;
}
.ao-title {
font-size: 1rem;
letter-spacing: 1px;
}
.ao-header {
padding: 8px 12px;
}
.ao-col-commands {
gap: 6px;
}
.ao-cmd-btn,
.ao-btn {
padding: 6px 12px;
font-size: 0.8rem;
}
}

View File

@@ -35,9 +35,12 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [activeCommand, setActiveCommand] = useState(null); const [activeCommand, setActiveCommand] = useState(null);
const [tokenUsage, setTokenUsage] = useState(null); const [tokenUsage, setTokenUsage] = useState(null);
const [expanded, setExpanded] = useState(false);
const state = agentState || { state: 'offline' }; const state = agentState || { state: 'offline' };
const commands = AGENT_COMMANDS[agentId] || []; const commands = AGENT_COMMANDS[agentId] || [];
const needsAttention = state.state === 'waiting' || notification > 0;
const isOpen = expanded || needsAttention;
useEffect(() => { useEffect(() => {
getAgentTasks(agentId, 10) getAgentTasks(agentId, 10)
@@ -89,11 +92,31 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
setActiveCommand(null); setActiveCommand(null);
}; };
const formatTime = (t) => t ? t.replace('T', ' ').slice(11, 19) : ''; const formatTaskTime = (task) => {
const iso = task.completed_at || task.created_at;
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return '';
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
const sameDay = d.toDateString() === now.toDateString();
const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1);
const isYesterday = d.toDateString() === yesterday.toDateString();
if (sameDay) return `오늘 ${hm}`;
if (isYesterday) return `어제 ${hm}`;
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${hm}`;
};
const handleHeaderClick = (e) => {
e.stopPropagation();
setExpanded(v => !v);
onClearNotification();
};
return ( return (
<div className="ao-col" onClick={onClearNotification}> <div className={`ao-col ${isOpen ? 'ao-col--open' : 'ao-col--collapsed'} ${needsAttention ? 'ao-col--attention' : ''}`} onClick={onClearNotification}>
<div className="ao-col-header" style={{ borderColor: meta.color }}> <div className="ao-col-header" style={{ borderColor: meta.color }} onClick={handleHeaderClick}>
<span className="ao-col-name" style={{ color: meta.color }}>{meta.name}</span> <span className="ao-col-name" style={{ color: meta.color }}>{meta.name}</span>
{tokenUsage && tokenUsage.total_tokens > 0 && ( {tokenUsage && tokenUsage.total_tokens > 0 && (
<span <span
@@ -105,8 +128,12 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
)} )}
<span className={`ao-col-state ao-col-state--${state.state}`}>{state.state}</span> <span className={`ao-col-state ao-col-state--${state.state}`}>{state.state}</span>
{notification > 0 && <span className="ao-col-badge">{notification}</span>} {notification > 0 && <span className="ao-col-badge">{notification}</span>}
<span className="ao-col-chevron" aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
</div> </div>
<div className="ao-col-body">
{state.detail && ( {state.detail && (
<div className="ao-col-detail">{state.detail}</div> <div className="ao-col-detail">{state.detail}</div>
)} )}
@@ -153,7 +180,7 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
<span className="ao-col-task-badge" style={{ background: badge.bg }}>{badge.label}</span> <span className="ao-col-task-badge" style={{ background: badge.bg }}>{badge.label}</span>
</div> </div>
<div className="ao-col-task-time"> <div className="ao-col-task-time">
{formatTime(task.created_at)} {formatTaskTime(task)}
{task.result_data?.telegram_sent !== undefined && ( {task.result_data?.telegram_sent !== undefined && (
<span className="ao-doc-tg-status">{task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'}</span> <span className="ao-doc-tg-status">{task.result_data.telegram_sent ? ' TG OK' : ' TG Fail'}</span>
)} )}
@@ -169,6 +196,7 @@ const AgentColumn = ({ agentId, meta, agentState, notification, onCommand, onApp
})} })}
</div> </div>
</div> </div>
</div>
); );
}; };