feat(agent-office): AgentCard component with state dot + badge
- 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) <noreply@anthropic.com>
This commit is contained in:
30
src/pages/agent-office/components/AgentCard.jsx
Normal file
30
src/pages/agent-office/components/AgentCard.jsx
Normal file
@@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`ao-card${active ? ' active' : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ '--card-accent': meta.color }}
|
||||||
|
>
|
||||||
|
<span className={dotClass} title={state} />
|
||||||
|
{notificationCount > 0 && (
|
||||||
|
<span className="ao-card-badge">{badgeText}</span>
|
||||||
|
)}
|
||||||
|
<div className="ao-card-image">
|
||||||
|
<img src={meta.image} alt={meta.displayName} />
|
||||||
|
</div>
|
||||||
|
<div className="ao-card-name">{meta.displayName}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/pages/agent-office/components/AgentCard.test.jsx
Normal file
60
src/pages/agent-office/components/AgentCard.test.jsx
Normal file
@@ -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(<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={0} onClick={() => {}} />);
|
||||||
|
expect(screen.getByText('주식 트레이더')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('working 상태일 때 dot에 working 클래스 부여', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentCard agentId="stock" agentState={{ state: 'working' }} notificationCount={0} onClick={() => {}} />
|
||||||
|
);
|
||||||
|
const dot = container.querySelector('.ao-card-dot');
|
||||||
|
expect(dot).toHaveClass('working');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('agentState 없으면 idle로 fallback', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentCard agentId="stock" agentState={undefined} notificationCount={0} onClick={() => {}} />
|
||||||
|
);
|
||||||
|
const dot = container.querySelector('.ao-card-dot');
|
||||||
|
expect(dot).toHaveClass('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notificationCount > 0이면 뱃지 표시', () => {
|
||||||
|
render(<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={3} onClick={() => {}} />);
|
||||||
|
expect(screen.getByText('3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notificationCount === 0이면 뱃지 없음', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={0} onClick={() => {}} />
|
||||||
|
);
|
||||||
|
expect(container.querySelector('.ao-card-badge')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('notificationCount > 9이면 9+ 표시', () => {
|
||||||
|
render(<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={15} onClick={() => {}} />);
|
||||||
|
expect(screen.getByText('9+')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('클릭 시 onClick 호출', () => {
|
||||||
|
const onClick = vi.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={0} onClick={onClick} />
|
||||||
|
);
|
||||||
|
fireEvent.click(container.querySelector('.ao-card'));
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('active prop 시 카드에 active 클래스 부여', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentCard agentId="stock" agentState={{ state: 'idle' }} notificationCount={0} active onClick={() => {}} />
|
||||||
|
);
|
||||||
|
expect(container.querySelector('.ao-card')).toHaveClass('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user