feat(agent-office): ActivityItem (task/log 행 + 상태 뱃지)
This commit is contained in:
60
src/pages/agent-office/components/ActivityItem.jsx
Normal file
60
src/pages/agent-office/components/ActivityItem.jsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// src/pages/agent-office/components/ActivityItem.jsx
|
||||||
|
import { AGENT_META } from '../constants.js';
|
||||||
|
|
||||||
|
const STATUS_STYLE = {
|
||||||
|
succeeded: { bg: '#065f46', fg: '#34d399', label: '✓ 완료' },
|
||||||
|
failed: { bg: '#7f1d1d', fg: '#fca5a5', label: '✗ 실패' },
|
||||||
|
working: { bg: '#1e3a5f', fg: '#60a5fa', label: '⏳ 진행' },
|
||||||
|
pending: { bg: '#92400e', fg: '#fbbf24', label: '⏳ 대기' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_STYLE = {
|
||||||
|
error: { icon: '❌', cls: 'level-error' },
|
||||||
|
warning: { icon: '⚠️', cls: 'level-warning' },
|
||||||
|
info: { icon: '·', cls: 'level-info' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTime(ts) {
|
||||||
|
if (!ts) return '';
|
||||||
|
const d = new Date(ts);
|
||||||
|
const now = new Date();
|
||||||
|
const isToday = d.toDateString() === now.toDateString();
|
||||||
|
const time = d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
return isToday ? time : `${d.getMonth() + 1}/${d.getDate()} ${time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ActivityItem({ item, onSelectAgent }) {
|
||||||
|
const meta = AGENT_META[item.agent_id];
|
||||||
|
const color = meta?.color || '#6b7280';
|
||||||
|
const name = meta?.displayName || item.agent_id;
|
||||||
|
const isTask = item.type === 'task';
|
||||||
|
const status = STATUS_STYLE[item.status] || STATUS_STYLE.pending;
|
||||||
|
const level = LEVEL_STYLE[item.level] || LEVEL_STYLE.info;
|
||||||
|
const highlight = isTask && (item.status === 'pending' || item.status === 'working');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`ao-activity-item ${isTask ? 'is-task' : 'is-log'} ${highlight ? 'is-highlight' : ''}`}
|
||||||
|
onClick={() => onSelectAgent(item.agent_id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span className="ao-activity-dot" style={{ background: color }} aria-hidden="true" />
|
||||||
|
<div className="ao-activity-body">
|
||||||
|
<div className="ao-activity-line">
|
||||||
|
<span className="ao-activity-agent" style={{ color }}>{name}</span>
|
||||||
|
{isTask
|
||||||
|
? <span className="ao-activity-badge" style={{ background: status.bg, color: status.fg }}>{status.label}</span>
|
||||||
|
: <span className={`ao-activity-level ${level.cls}`}>{level.icon}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="ao-activity-msg">{item.message}</div>
|
||||||
|
</div>
|
||||||
|
<div className="ao-activity-meta">
|
||||||
|
<span className="ao-activity-time">{formatTime(item.created_at)}</span>
|
||||||
|
{isTask && item.duration_seconds != null && (
|
||||||
|
<span className="ao-activity-dur">{item.duration_seconds}s</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/pages/agent-office/components/ActivityItem.test.jsx
Normal file
30
src/pages/agent-office/components/ActivityItem.test.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import ActivityItem from './ActivityItem.jsx';
|
||||||
|
|
||||||
|
describe('ActivityItem', () => {
|
||||||
|
it('task 항목은 상태 뱃지와 duration을 렌더한다', () => {
|
||||||
|
render(<ActivityItem item={{ type: 'task', agent_id: 'stock', task_id: 'a', message: 'holdings_brief', created_at: '2026-06-10T23:30:00Z', status: 'succeeded', duration_seconds: 2 }} onSelectAgent={() => {}} />);
|
||||||
|
expect(screen.getByText('holdings_brief')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/완료/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('2s')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('log 항목은 level 아이콘을 렌더한다', () => {
|
||||||
|
render(<ActivityItem item={{ type: 'log', agent_id: 'lotto', task_id: 'b', message: 'signal_check', created_at: '2026-06-10T23:15:00Z', level: 'error' }} onSelectAgent={() => {}} />);
|
||||||
|
expect(screen.getByText('signal_check')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('❌')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('클릭 시 onSelectAgent(agent_id)를 호출한다', () => {
|
||||||
|
const onSelect = vi.fn();
|
||||||
|
render(<ActivityItem item={{ type: 'task', agent_id: 'insta', task_id: 'c', message: '발급', created_at: '2026-06-10T23:00:00Z', status: 'pending' }} onSelectAgent={onSelect} />);
|
||||||
|
fireEvent.click(screen.getByText('발급').closest('.ao-activity-item'));
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('insta');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('미지정 agent_id는 id를 그대로 표시한다', () => {
|
||||||
|
render(<ActivityItem item={{ type: 'log', agent_id: 'unknown', task_id: 'd', message: 'x', created_at: '2026-06-10T23:00:00Z', level: 'info' }} onSelectAgent={() => {}} />);
|
||||||
|
expect(screen.getByText('unknown')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user