From 16301098565cdba0c954044a49b011d541abeda4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 17 May 2026 20:52:34 +0900 Subject: [PATCH] feat(agent-office): AgentCard component with state dot + badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state→color mapping via STATE_COLORS - notification badge with 9+ overflow - active prop for selected card border Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agent-office/components/AgentCard.jsx | 30 ++++++++++ .../components/AgentCard.test.jsx | 60 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/pages/agent-office/components/AgentCard.jsx create mode 100644 src/pages/agent-office/components/AgentCard.test.jsx diff --git a/src/pages/agent-office/components/AgentCard.jsx b/src/pages/agent-office/components/AgentCard.jsx new file mode 100644 index 0000000..50e1089 --- /dev/null +++ b/src/pages/agent-office/components/AgentCard.jsx @@ -0,0 +1,30 @@ +// src/pages/agent-office/components/AgentCard.jsx +import { AGENT_META, STATE_COLORS, DEFAULT_STATE_COLOR } from '../constants.js'; + +export default function AgentCard({ agentId, agentState, notificationCount = 0, active = false, onClick }) { + const meta = AGENT_META[agentId]; + if (!meta) return null; + + const state = agentState?.state || 'idle'; + const stateInfo = STATE_COLORS[state] || DEFAULT_STATE_COLOR; + const dotClass = `ao-card-dot ${state}${stateInfo.pulse ? ' pulse' : ''}`; + const badgeText = notificationCount > 9 ? '9+' : String(notificationCount); + + return ( + + ); +} diff --git a/src/pages/agent-office/components/AgentCard.test.jsx b/src/pages/agent-office/components/AgentCard.test.jsx new file mode 100644 index 0000000..2d51148 --- /dev/null +++ b/src/pages/agent-office/components/AgentCard.test.jsx @@ -0,0 +1,60 @@ +// src/pages/agent-office/components/AgentCard.test.jsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import AgentCard from './AgentCard.jsx'; + +describe('AgentCard', () => { + it('에이전트의 displayName을 표시', () => { + render( {}} />); + expect(screen.getByText('주식 트레이더')).toBeInTheDocument(); + }); + + it('working 상태일 때 dot에 working 클래스 부여', () => { + const { container } = render( + {}} /> + ); + const dot = container.querySelector('.ao-card-dot'); + expect(dot).toHaveClass('working'); + }); + + it('agentState 없으면 idle로 fallback', () => { + const { container } = render( + {}} /> + ); + const dot = container.querySelector('.ao-card-dot'); + expect(dot).toHaveClass('idle'); + }); + + it('notificationCount > 0이면 뱃지 표시', () => { + render( {}} />); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + it('notificationCount === 0이면 뱃지 없음', () => { + const { container } = render( + {}} /> + ); + expect(container.querySelector('.ao-card-badge')).toBeNull(); + }); + + it('notificationCount > 9이면 9+ 표시', () => { + render( {}} />); + expect(screen.getByText('9+')).toBeInTheDocument(); + }); + + it('클릭 시 onClick 호출', () => { + const onClick = vi.fn(); + const { container } = render( + + ); + fireEvent.click(container.querySelector('.ao-card')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('active prop 시 카드에 active 클래스 부여', () => { + const { container } = render( + {}} /> + ); + expect(container.querySelector('.ao-card')).toHaveClass('active'); + }); +});