199 lines
9.2 KiB
Markdown
199 lines
9.2 KiB
Markdown
# Astra Office Refactor — Design Doc
|
|
|
|
작성: 2026-05-16
|
|
대상 코드: `src/sidebarProvider.ts` (3860~4900 line 의 `_pixelOfficePanelHtml`),
|
|
`src/features/company/pixelOfficeState.ts`,
|
|
`media/sidebar.{html,js,css}` 의 mini Pixel Office 영역.
|
|
|
|
---
|
|
|
|
## 0. 한 줄 진단
|
|
|
|
이 기능은 "UI 스킨" 단계는 통과했지만, "회사 운영 가시화 시스템" 으로 키우려면 (a) 데이터 모델, (b) 파일 구조, (c) mini/full 공용 presenter 가 빠져 있다. 본 문서는 그 세 개를 한 번에 정렬하는 합의안.
|
|
|
|
---
|
|
|
|
## 1. 동시성 진실 (Truth)
|
|
|
|
> 백엔드 dispatcher 는 한 turn 동안 **정확히 한 명의 agent 만 활성**이다.
|
|
|
|
근거 — `src/features/company/dispatcher.ts:1-32`:
|
|
- 주석: "sequential dispatch keeps exactly one model resident at a time"
|
|
- 메인 loop `for (let i = startIdx; i < total; i++)` — 직렬
|
|
- callback 순서: `agent-start` → AI call → `agent-done` → 다음 agent
|
|
|
|
**결정**: 화면이 "여러 명이 동시에 일한다" 는 *연출* 은 가능하지만, **데이터 모델은 활성 agent 1명을 진실로 삼는다**. 비활성 agents 는 "지난 활동" 또는 "다음 차례" 로 표현.
|
|
|
|
이게 안 지켜지면 미래의 진짜 병렬 dispatch 도입 시 스키마를 또 갈아엎어야 한다.
|
|
|
|
---
|
|
|
|
## 2. 도메인 모델 — `OfficeSnapshot`
|
|
|
|
### 2.1 현재 모델 (얇음)
|
|
|
|
```ts
|
|
// pixelOfficeState.ts:49
|
|
interface AgentWorkState {
|
|
agentId: string;
|
|
agentName: string;
|
|
status: AgentStatus;
|
|
currentTask?: string;
|
|
currentStep?: string;
|
|
// ... 그 외 단일 슬롯 필드
|
|
}
|
|
```
|
|
|
|
문제: 한 시점의 active agent 만 표현. "직전 agent 가 무슨 일 했었나", "다음에 누가 차례", "현재 roster 가 누구누구", "회사 전반 phase" 같은 정보가 없다.
|
|
|
|
### 2.2 새 모델
|
|
|
|
```ts
|
|
type AgentSnapshot = {
|
|
agentId: string; // company roster id (built-in 또는 custom)
|
|
agentName: string; // 표시 이름
|
|
roleCategory: RoleCategory; // ceo | planner | researcher | designer | developer | qa | inspector | support | writer
|
|
status: AgentStatus;
|
|
currentStep?: string;
|
|
lastLog?: string; // 머리 위 말풍선 source
|
|
lastActivityAt: number; // 정렬용 epoch ms
|
|
};
|
|
|
|
type OfficeSnapshot = {
|
|
// 회사 전체 phase — 어디까지 진행됐는가
|
|
phase: 'idle' | 'intake' | 'planning' | 'executing' | 'reviewing' | 'awaiting-approval' | 'reporting' | 'done' | 'error';
|
|
// 활성 agent id — null 이면 idle. dispatcher 직렬성 보장.
|
|
activeAgentId: string | null;
|
|
// roster — 이 turn 에 참여 가능한 모든 agent (built-in + custom). 빈 책상 표시도 가능.
|
|
roster: AgentSnapshot[];
|
|
// 현재 요구사항/계약
|
|
task?: { goal: string; context?: string; format?: string; criteria?: string[]; openQuestions?: string[] };
|
|
// pipeline 진행도
|
|
pipeline?: { stages: Array<{ label: string; agentId?: string; status: 'done'|'active'|'pending' }>; index: number };
|
|
// 승인 대기
|
|
awaiting?: { kind: 'approval' | 'clarification'; questions: string[] };
|
|
// 누적 활동 (ticker용) — 최근 N개 ring buffer
|
|
activity: Array<{ ts: number; agentId: string; text: string; kind?: 'ok'|'warn'|'err'|'info' }>;
|
|
// 직전 emit 으로부터 새 활동 N개 — 말풍선 트리거
|
|
newBubbles: Array<{ agentId: string; text: string; type: 'event'|'warning'|'error'|'success'|'status' }>;
|
|
updatedAt: number;
|
|
};
|
|
```
|
|
|
|
### 2.3 마이그레이션
|
|
|
|
기존 `AgentWorkState` → `OfficeSnapshot` 매핑:
|
|
- `agentId`, `agentName`, `status` → `activeAgentId`, `roster[active].agentName`, `roster[active].status`, `phase` 매핑
|
|
- `bubbles[]` (별도 시퀀스) → `newBubbles` (snapshot 내부로 흡수)
|
|
- `activityItems` (별도 메시지) → `activity` (snapshot 내부로 흡수)
|
|
- `pipelineStages` → `pipeline.stages`
|
|
|
|
**점진 마이그레이션**: presenter 단계가 두 모델 모두 받게. 백엔드는 `OfficeSnapshot` 로 옮기고, presenter 가 webview 로 보내는 message 는 한 종류 (`officeSnapshot`) 로 통합. 옛 `pixelOfficeUpdate` / `pixelOfficeActivity` 는 1버전 동안 호환 보존.
|
|
|
|
---
|
|
|
|
## 3. 파일 구조
|
|
|
|
### 3.1 현재
|
|
|
|
```
|
|
src/sidebarProvider.ts (~4900 lines, 그 중 800+ 가 inline office HTML/CSS/JS)
|
|
src/features/company/pixelOfficeState.ts (200줄, 타입 + bubble text pool)
|
|
media/sidebar.js (mini Pixel Office 렌더링 코드가 섞여있음)
|
|
media/sidebar.html (mini Pixel Office DOM 마크업)
|
|
media/sidebar.css (mini Pixel Office 스타일)
|
|
```
|
|
|
|
### 3.2 목표
|
|
|
|
```
|
|
src/features/astraOffice/
|
|
├── index.ts # public API (createOfficeView, presenter exports)
|
|
├── schema.ts # OfficeSnapshot 타입 정의 + 런타임 validator
|
|
├── presenter.ts # 백엔드 이벤트 → OfficeSnapshot 변환기 (pure)
|
|
├── view/
|
|
│ ├── panelHtml.ts # full Astra Office webview HTML 생성 함수 (현재 _pixelOfficePanelHtml)
|
|
│ ├── officeView.css.ts # style 문자열 export
|
|
│ ├── runtime.ts # 브라우저 측 JS — 캐릭터/애니메이션/말풍선/ticker (string export)
|
|
│ ├── layoutEditor.ts # 브라우저 측 JS — 편집 모드/속성 패널 (string export)
|
|
│ └── layoutSchema.ts # 저장 layout 스키마 (v1/v2 validator, migration)
|
|
└── viewModel.ts # 공용 viewModel — mini/full 둘 다 입력으로 받음
|
|
|
|
media/sidebar.js (mini) # viewModel 기반 렌더로 정리 (별도 단계)
|
|
```
|
|
|
|
### 3.3 이번 세션 범위
|
|
|
|
- `view/panelHtml.ts`, `view/runtime.ts`, `view/layoutEditor.ts`, `view/officeView.css.ts` 추출
|
|
- `schema.ts` 작성 (타입 + validator)
|
|
- `presenter.ts` stub (실제 변환은 다음 세션에서 옛 코드와 wiring)
|
|
- `index.ts` re-export
|
|
|
|
sidebarProvider.ts 는 `import { renderAstraOfficeHtml } from './features/astraOffice'` 한 줄로 호출. 동작 변화 없음. mini view 통합은 다음 세션.
|
|
|
|
---
|
|
|
|
## 4. Company Roster 통합 (#3, 다음 세션)
|
|
|
|
현재: full view 의 `DEFAULT_STATIONS` 가 8개 role 을 하드코딩. company 의 custom agent 가 추가돼도 화면에 안 나타남.
|
|
|
|
해결: layout 의 `desks[].agentKey` 를 `agentId` 로 (roleCategory 가 아닌). presenter 가 OfficeSnapshot 보낼 때 roster 의 모든 active agent 를 포함. webview 는 desk 를 그릴 때 agentId 로 매칭. 매핑 없는 agent 는 default 좌석에 자동 배치.
|
|
|
|
alias map (`writer→planner` 등) 은 presenter 단계의 1회성 변환으로만 두고, 도메인 모델은 늘 정확한 `agentId` 를 들고 다닌다.
|
|
|
|
---
|
|
|
|
## 5. mini/full 공용 presenter (#4, 다음 세션)
|
|
|
|
현재: mini (media/sidebar.js) 와 full (sidebarProvider.ts inline) 이 각자 다른 규칙으로 message 를 해석. drift 발생 중.
|
|
|
|
목표:
|
|
```
|
|
백엔드 events
|
|
↓
|
|
presenter (pure)
|
|
↓ OfficeSnapshot
|
|
├→ mini view (media/sidebar.js : OfficeSnapshot → mini DOM)
|
|
└→ full view (features/astraOffice/view/runtime.ts : OfficeSnapshot → stage)
|
|
```
|
|
|
|
이번 세션은 `OfficeSnapshot` 만 정의. mini/full 의 wiring 은 다음 세션.
|
|
|
|
---
|
|
|
|
## 6. Layout schema validation (#5, 이번 세션)
|
|
|
|
현재: `workspaceState.get(key)` 가 `unknown` 반환. 깨진 데이터/이전 버전 데이터는 `_isV2Snap()` heuristic 으로만 분기.
|
|
|
|
해결: `view/layoutSchema.ts` 에 두 가지:
|
|
1. `validateLayout(raw): LayoutV2 | null` — invalid 면 null, valid 면 정규화된 v2 객체
|
|
2. `migrateLayout(raw): LayoutV2 | null` — v1 → v2 변환 + 누락 필드 기본값 채우기
|
|
|
|
webview 측에서 받는 즉시 한 번 통과시킨다. 통과 못 한 데이터는 default 로 fallback (시각적 reset).
|
|
|
|
---
|
|
|
|
## 7. 이번 세션 deliverable
|
|
|
|
- [x] 본 design doc (`docs/ASTRA_OFFICE_REFACTOR.md`)
|
|
- [ ] `src/features/astraOffice/` 디렉토리 생성 + 파일 분리 (sidebarProvider.ts 의 ~800줄 inline 추출)
|
|
- [ ] `schema.ts` — `OfficeSnapshot` 타입 + `validateOfficeSnapshot()`
|
|
- [ ] `view/layoutSchema.ts` — layout v2 validator + migration
|
|
- [ ] `presenter.ts` stub (export 인터페이스만, 내부 변환은 다음 세션)
|
|
- [ ] sidebarProvider.ts 는 `renderAstraOfficeHtml(cspSource, derivedBase)` 한 줄 호출로 정리. 동작 동등.
|
|
|
|
**관찰 가능한 변화 (사용자 입장)**: 없음. 코드 구조만 정리.
|
|
|
|
다음 세션에서:
|
|
- mini view 도 같은 presenter / viewModel 사용 (#4)
|
|
- company roster derive (#3)
|
|
- 옛 message types 제거 (`pixelOfficeUpdate` / `pixelOfficeActivity` → `officeSnapshot` 단일화)
|
|
|
|
---
|
|
|
|
## 8. 비결정 (열린 질문)
|
|
|
|
- mini view 와 full view 가 사실상 다른 UX (mini=status pane, full=2D stage) 인데 진짜로 *완전히 같은* viewModel 을 공유해야 하나? 현실적으로 mini 는 viewModel 의 subset (active agent + 최근 로그 + bubble 1개) 만 쓸 가능성. 다음 세션에서 mini 작업 시 결정.
|
|
- presenter 를 webview 로 push 하는 메시지 빈도. 현재 매 event 마다 broadcast → roster 가 커지면 비용 증가. coalescing window (e.g., 60ms) 가 필요할 수도. 다음 세션에서 결정.
|
|
- layout schema versioning — workspaceState 에 schema version key 를 별도로 저장할지, 객체 안에 `schema: 2` 만 넣고 끝낼지. 이번 세션은 후자 (현재 코드 호환), 별도 key 는 추후.
|