feat: Agent Office — AI 에이전트 가상 오피스 (#2)

## Summary
- Canvas 2D 픽셀아트 오피스 렌더링 (SpriteSheet + TileMap + AgentSprite)
- WebSocket 실시간 에이전트 상태 동기화 (useAgentManager)
- ChatPanel (명령/승인) + TaskHistory (작업 이력) UI
- 다크 테마 + glassmorphism 패널

## Changes (7 commits)
- API helpers + route + Lab entry
- Canvas engine: SpriteSheet, TileMap, AgentSprite, OfficeRenderer
- React hooks: useAgentManager, useOfficeCanvas
- Components: ChatPanel, TaskHistory
- Main page + CSS
- Code review fixes: claude agent 참조 제거, rejected 배지 추가

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-04-11 13:35:35 +09:00
parent 7fc2d3aaf7
commit 25715a2198
14 changed files with 1206 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
import { useRef, useEffect, useCallback } from 'react';
import { OfficeRenderer } from '../canvas/OfficeRenderer';
import officeMap from '../assets/office-map.json';
export function useOfficeCanvas(containerRef, onAgentClick) {
const rendererRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const canvas = document.createElement('canvas');
canvas.style.display = 'block';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.imageRendering = 'pixelated';
containerRef.current.appendChild(canvas);
const renderer = new OfficeRenderer(canvas, officeMap);
rendererRef.current = renderer;
const resize = () => {
const rect = containerRef.current.getBoundingClientRect();
renderer.resize(rect.width, rect.height);
};
resize();
renderer.start();
renderer.setOnClick((agentId) => {
if (onAgentClick) onAgentClick(agentId);
});
const handleClick = (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
renderer.handleClick(x, y);
};
canvas.addEventListener('click', handleClick);
window.addEventListener('resize', resize);
return () => {
renderer.stop();
canvas.removeEventListener('click', handleClick);
window.removeEventListener('resize', resize);
if (containerRef.current && canvas.parentNode === containerRef.current) {
containerRef.current.removeChild(canvas);
}
};
}, [containerRef, onAgentClick]);
const updateAgentState = useCallback((agentId, state, detail) => {
rendererRef.current?.updateAgentState(agentId, state, detail);
}, []);
const moveAgent = useCallback((agentId, target) => {
rendererRef.current?.moveAgent(agentId, target);
}, []);
return { updateAgentState, moveAgent };
}