diff --git a/src/pages/agent-office/components/ActivityItem.jsx b/src/pages/agent-office/components/ActivityItem.jsx new file mode 100644 index 0000000..9d74252 --- /dev/null +++ b/src/pages/agent-office/components/ActivityItem.jsx @@ -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 ( +
onSelectAgent(item.agent_id)} + role="button" + tabIndex={0} + > +
+ ); +} diff --git a/src/pages/agent-office/components/ActivityItem.test.jsx b/src/pages/agent-office/components/ActivityItem.test.jsx new file mode 100644 index 0000000..48f495b --- /dev/null +++ b/src/pages/agent-office/components/ActivityItem.test.jsx @@ -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( {}} />); + expect(screen.getByText('holdings_brief')).toBeInTheDocument(); + expect(screen.getByText(/완료/)).toBeInTheDocument(); + expect(screen.getByText('2s')).toBeInTheDocument(); + }); + + it('log 항목은 level 아이콘을 렌더한다', () => { + render( {}} />); + expect(screen.getByText('signal_check')).toBeInTheDocument(); + expect(screen.getByText('❌')).toBeInTheDocument(); + }); + + it('클릭 시 onSelectAgent(agent_id)를 호출한다', () => { + const onSelect = vi.fn(); + render(); + fireEvent.click(screen.getByText('발급').closest('.ao-activity-item')); + expect(onSelect).toHaveBeenCalledWith('insta'); + }); + + it('미지정 agent_id는 id를 그대로 표시한다', () => { + render( {}} />); + expect(screen.getByText('unknown')).toBeInTheDocument(); + }); +});