<!-- task=m-architecture-refactor-foundation/08_client_state_split plan=0 tag=REFACTOR -->

# PLAN - REFACTOR client-state-split

## 이 파일을 읽는 구현 에이전트에게

구현 완료의 마지막 단계는 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 출력 원문을 붙이고, active 파일은 그대로 둔 채 리뷰 준비 상태를 보고한다. 종료 처리, 로그 아카이브, `complete.log` 작성, task directory 이동은 code-review skill 전용이다.

구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret/서비스 준비, 또는 안전하게 진행할 수 없는 충돌이 생기면 직접 질문하지 말고 `CODE_REVIEW-cloud-G08.md`의 `사용자 리뷰 요청` 섹션을 증거와 함께 채운 뒤 멈춘다. `request_user_input`, 채팅 선택지 제시, `USER_REVIEW.md` 생성, `complete.log` 작성, active 파일 아카이브는 구현 에이전트가 하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청 사유가 아니다.

## 배경

Flutter client는 `main.dart`가 Firebase/Mattermost bootstrap, wire connection, fleet/status/events state, selected Edge normalization, panel composition을 모두 가진다. `control_plane_status_client.dart`도 DTO와 HTTP repository를 한 파일에 같이 둔다. 이 작업은 외부 통합을 선택 초기화 가능하게 만들고, 상태/DTO/repository/panel 경계를 나눠 client 변경의 blast radius를 줄인다.

## 사용자 리뷰 요청 흐름

구현 중 차단 결정은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`와 동일한 형식을 사용한다. 구현 에이전트는 사용자에게 직접 묻지 않으며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- Task ids:
  - `client-state-split`: Flutter client의 bootstrap, status state, DTO, repository, panel widget 경계를 분리하고 Firebase/Mattermost 같은 외부 통합은 선택적으로 초기화되게 한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/client/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/client-smoke.md`
- `.gitignore`
- `go.mod`
- `apps/client/pubspec.yaml`
- `apps/client/lib/main.dart`
- `apps/client/lib/control_plane_status_client.dart`
- `apps/client/lib/control_plane_status_widgets.dart`
- `apps/client/test/widget_test.dart`
- `packages/flutter/iop_console/test/iop_console_shell_test.dart`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md` 존재 및 정독 완료. Client 변경은 `agent-test/local/client-smoke.md`를 baseline으로 사용한다.
- Flutter client 또는 `packages/flutter/iop_console` 검증은 원격 runner `ssh toki@toki-labs.com`의 `/Users/toki/agent-work/iop` 기준이다. 현재 workspace 변경분이 원격 checkout에 동기화되지 않았으면 원격 검증 완료로 보지 않는다.
- 적용 명령: `cd apps/client && flutter test`, `cd apps/client && flutter analyze --no-fatal-infos`.
- `packages/flutter/iop_console` 자체 API는 변경하지 않을 계획이므로 `cd packages/flutter/iop_console && flutter test`는 필수에서 제외한다. 구현 중 package API/import가 바뀌면 계획 대비 변경 사항에 기록하고 해당 명령을 추가 실행한다.
- `<확인 필요>` 값 없음. agent-test fallback 없음.

### 테스트 커버리지 공백

- 기존 `apps/client/test/widget_test.dart`는 app rendering, Edge/Nodes/Logs/Runtime panel behavior, selected Edge disappearance, mobile layout을 widget 수준에서 검증한다.
- bootstrap optional init은 현재 테스트가 없다. Firebase/Mattermost init을 선택적으로 끄는 unit/widget test를 추가해야 한다.
- 상태 분리를 `ChangeNotifier`/controller로 만들면 selected Edge disappearance와 stale cache cleanup을 widget test 밖의 unit test로 추가해야 한다.
- DTO/repository 파일 이동은 public class명을 유지하면 기존 widget fake repository가 회귀 검증을 맡는다.

### 심볼 참조

- 제거/이름 변경 예정 없음. 파일 이동 후 import 변경 예정.
- 이동 예정 call site:
  - `IopClientApp`: `apps/client/lib/main.dart:69`, `apps/client/test/widget_test.dart:380`, `410`, `425`, `446`, `485`, `515`, `544`, `617`, `742`, `779`, `849`
  - `ClientHomePage`: `apps/client/lib/main.dart:95`, `104`
  - `ControlPlaneStatusRepository`: `apps/client/lib/main.dart:72`, `107`, `125`, `apps/client/lib/control_plane_status_widgets.dart:822`, `apps/client/test/widget_test.dart:66`
  - DTO classes in `control_plane_status_client.dart`: existing widget tests import `package:iop_client/control_plane_status_client.dart`.

### 분할 판단

- split decision policy를 평가했다. cp-client Epic은 multi-plan으로 분리한다.
- shared task group: `agent-task/m-architecture-refactor-foundation/`
- sibling subtask directories:
  - `06_cp_api_split`: Control Plane API file boundary.
  - `07+06_fleet_polling`: depends on `06_cp_api_split`.
  - `08_client_state_split`: 이 plan. 선행 의존성 없음.
- 이 plan 자체는 단일 plan으로 유지한다. bootstrap, state controller, DTO/repository imports, panel composition이 서로 compile-time import graph를 공유해 한 번에 맞춰야 한다.

### 범위 결정 근거

- Control Plane Go server, fleet polling policy, wire protocol은 변경하지 않는다.
- `packages/flutter/iop_console` API와 product shell UX는 변경하지 않는다.
- generated proto files under `apps/client/lib/gen/**`는 변경하지 않는다.
- Mattermost plugin implementation 내부는 optional init boundary에 필요한 import 위치 변경 외에는 변경하지 않는다.

### 빌드 등급

- build lane: `local-G08`. Flutter app import graph와 widget state ownership을 바꾸는 넓은 refactor다.
- review lane: `cloud-G08`. UI behavior, optional external init, remote runner verification evidence가 필요하다.

## 구현 체크리스트

- [ ] bootstrap을 옵션화해 Firebase/Mattermost 초기화를 기본 on, test/embedded 경로에서는 off로 선택할 수 있게 한다.
- [ ] fleet/status/events selected Edge state를 controller/repository state 경계로 분리하고 stale cache cleanup을 unit test로 검증한다.
- [ ] DTO와 HTTP repository를 별도 파일로 나누고 기존 public import 호환을 유지한다.
- [ ] `ClientHomePage` shell composition과 panel widgets 파일 경계를 나누되 기존 visible behavior를 유지한다.
- [ ] 변경분을 원격 runner에 동기화한 뒤 `cd apps/client && flutter test`, `cd apps/client && flutter analyze --no-fatal-infos`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REFACTOR-1] Optional Bootstrap 경계

### 문제

`apps/client/lib/main.dart:57`부터 `66`까지 bootstrap이 fullscreen, Firebase, Mattermost host init, `runApp`을 고정 순서로 직접 실행한다. 외부 통합을 끌 수 없어 테스트/embedded 경로에서 Firebase 또는 Mattermost 준비 여부가 app bootstrap과 강하게 결합된다.

Before:

```dart
// apps/client/lib/main.dart:57
Future<void> bootstrapIopClient() async {
  await applyFullscreenMode();
  await Firebase.initializeApp();
  await _mattermostHost.initialize();
}
```

### 해결 방법

- 새 파일 `apps/client/lib/client_bootstrap.dart`를 만든다.
- `IopClientBootstrapOptions`를 추가한다: `initializeFirebase`, `initializeMattermost`, `mattermostHost`.
- production `main()`은 기존처럼 default options를 사용해 Firebase/Mattermost를 초기화한다.
- test/embedded path는 `runIopClient(options: const IopClientBootstrapOptions(initializeFirebase: false, initializeMattermost: false))` 형태로 끌 수 있게 한다.
- 기존 `IopClientApp(mattermostHost: ...)` injection은 유지한다.

### 수정 파일 및 체크리스트

- [ ] `apps/client/lib/main.dart`: production entrypoint만 남기고 bootstrap helper를 이동한다.
- [ ] `apps/client/lib/client_bootstrap.dart`: options/default host/bootstrap/run helper 추가.
- [ ] `apps/client/test/widget_test.dart`: optional bootstrap test 추가 또는 기존 direct `IopClientApp` tests가 영향 없음을 확인한다.

### 테스트 작성

- 작성: `apps/client/test/widget_test.dart`, `runIopClient can skip external integrations` 또는 별도 `client_bootstrap_test.dart`.
- assertion: Firebase/Mattermost init off options가 `IopClientApp` construction path를 막지 않는다.
- fixture: fake bootstrap hook 또는 no-op Mattermost host wrapper. 실제 Firebase secret/service를 요구하지 않는다.

### 중간 검증

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

## [REFACTOR-2] Status State Controller 분리

### 문제

`_ClientHomePageState`가 `apps/client/lib/main.dart:120`부터 `139`까지 Edge/status/events state map을 들고, `196`부터 `294`까지 fetch/selection/cache cleanup을 직접 수행한다. UI widget lifecycle과 repository state transition이 결합되어 stale selection 관련 회귀가 widget test에만 의존한다.

Before:

```dart
// apps/client/lib/main.dart:196
Future<void> _fetchEdges() async {
  if (!mounted) return;
  setState(() {
    _loadingEdges = true;
    _edgesError = null;
  });
```

### 해결 방법

- 새 파일 `apps/client/lib/control_plane_status_controller.dart`를 만든다.
- `ControlPlaneStatusController extends ChangeNotifier`를 추가한다.
- controller가 `edges`, `selectedEdgeId`, `edgeStatuses`, `edgeEvents`, loading/error state와 `fetchEdges`, `selectEdge`, `refreshSelectedStatus`, `refreshSelectedEvents`를 소유한다.
- `ClientHomePage`는 controller를 생성/dispose하고 `AnimatedBuilder` 또는 `ListenableBuilder`로 panels에 state를 전달한다.
- selected Edge가 사라질 때 stale status/events/loading/error cache를 제거하는 기존 동작을 controller unit test로 고정한다.

### 수정 파일 및 체크리스트

- [ ] `apps/client/lib/main.dart` 또는 새 `client_home_page.dart`: widget state를 controller delegation으로 축소한다.
- [ ] `apps/client/lib/control_plane_status_controller.dart`: state machine 추가.
- [ ] `apps/client/test/control_plane_status_controller_test.dart`: selected Edge disappearance/stale cache cleanup tests 추가.
- [ ] `apps/client/test/widget_test.dart`: 기존 Edge/Nodes/Logs/Runtime behavior tests 유지.

### 테스트 작성

- 작성: `TestWidgetsFlutterBinding.ensureInitialized()`가 필요 없는 Dart unit style test.
- test name: `removes stale selected edge caches when fleet refresh drops edge`.
- assertion: 이전 selected Edge의 status/event/loading/error maps가 제거되고 새 첫 Edge가 선택된다.
- fixture: fake `ControlPlaneStatusRepository`.

### 중간 검증

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

## [REFACTOR-3] DTO와 Repository 파일 분리

### 문제

`apps/client/lib/control_plane_status_client.dart:4`부터 `383`까지 DTO/view classes가 있고, `385`부터 `487`까지 repository interface와 HTTP implementation이 같은 파일에 있다. Widget tests와 UI panels가 DTO와 transport implementation을 같은 import로 가져와 경계가 흐리다.

Before:

```dart
// apps/client/lib/control_plane_status_client.dart:385
abstract class ControlPlaneStatusRepository {
  Future<List<EdgeRegistryView>> fetchEdges();
  Future<EdgeStatusResponseView> fetchEdgeStatus(String edgeId);
  Future<List<EdgeNodeEventView>> fetchEdgeEvents(String edgeId);
```

### 해결 방법

- 새 파일 `apps/client/lib/control_plane_status_dto.dart`에 DTO/view classes를 이동한다.
- 새 파일 `apps/client/lib/control_plane_status_repository.dart`에 repository interface와 HTTP implementation을 이동한다.
- 기존 `control_plane_status_client.dart`는 compatibility barrel로 남겨 `export 'control_plane_status_dto.dart'; export 'control_plane_status_repository.dart';`를 제공한다.
- existing tests/imports는 필요 최소 변경만 한다.

### 수정 파일 및 체크리스트

- [ ] `apps/client/lib/control_plane_status_dto.dart`: DTO/view classes 추가.
- [ ] `apps/client/lib/control_plane_status_repository.dart`: repository interface/HTTP implementation 추가.
- [ ] `apps/client/lib/control_plane_status_client.dart`: compatibility export로 축소.
- [ ] `apps/client/lib/main.dart`, `control_plane_status_widgets.dart`, tests: import 경로를 필요한 만큼 정리한다.

### 테스트 작성

- 새 테스트 작성 없음. 기존 `FakeControlPlaneStatusRepository`와 widget tests가 public class/import compatibility를 검증한다.

### 중간 검증

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

## [REFACTOR-4] Home Shell과 Panel Widget 파일 경계

### 문제

`apps/client/lib/main.dart:340`부터 `414`까지 `IopConsoleShell` composition과 panel state binding을 직접 수행한다. `apps/client/lib/control_plane_status_widgets.dart`는 `EdgesPanel`, `NodesPanel`, `ExecutionLogsPanel`, `RuntimePanel`까지 1506 lines를 포함한다.

Before:

```dart
// apps/client/lib/main.dart:349
return IopConsoleShell(
  config: config,
  capabilities: iopDefaultCapabilityPack,
  overview: IopConsoleOverview(
```

### 해결 방법

- 새 파일 `apps/client/lib/client_home_page.dart`를 만들고 `ClientHomePage`와 shell composition을 이동한다.
- `control_plane_status_widgets.dart`는 compatibility export barrel로 남긴다.
- panel files를 분리한다:
  - `apps/client/lib/widgets/edges_panel.dart`
  - `apps/client/lib/widgets/nodes_panel.dart`
  - `apps/client/lib/widgets/execution_logs_panel.dart`
  - `apps/client/lib/widgets/runtime_panel.dart`
- public widget class명은 유지한다.
- UI text/behavior는 변경하지 않는다.

### 수정 파일 및 체크리스트

- [ ] `apps/client/lib/main.dart`: app root와 production entrypoint만 남긴다.
- [ ] `apps/client/lib/client_home_page.dart`: shell composition 추가.
- [ ] `apps/client/lib/control_plane_status_widgets.dart`: exports로 축소.
- [ ] `apps/client/lib/widgets/*.dart`: panels 이동.
- [ ] `apps/client/test/widget_test.dart`: 기존 visible behavior tests 유지.

### 테스트 작성

- 새 widget test 작성 없음. 기존 `widget_test.dart`의 rendering, panel navigation, selected Edge disappearance, runtime command gating, mobile layout 검증이 회귀를 담당한다.

### 중간 검증

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/main.dart` | REFACTOR-1, REFACTOR-2, REFACTOR-4 |
| `apps/client/lib/client_bootstrap.dart` | REFACTOR-1 |
| `apps/client/lib/client_home_page.dart` | REFACTOR-2, REFACTOR-4 |
| `apps/client/lib/control_plane_status_controller.dart` | REFACTOR-2 |
| `apps/client/lib/control_plane_status_dto.dart` | REFACTOR-3 |
| `apps/client/lib/control_plane_status_repository.dart` | REFACTOR-3 |
| `apps/client/lib/control_plane_status_client.dart` | REFACTOR-3 |
| `apps/client/lib/control_plane_status_widgets.dart` | REFACTOR-4 |
| `apps/client/lib/widgets/edges_panel.dart` | REFACTOR-4 |
| `apps/client/lib/widgets/nodes_panel.dart` | REFACTOR-4 |
| `apps/client/lib/widgets/execution_logs_panel.dart` | REFACTOR-4 |
| `apps/client/lib/widgets/runtime_panel.dart` | REFACTOR-4 |
| `apps/client/test/control_plane_status_controller_test.dart` | REFACTOR-2 |
| `apps/client/test/widget_test.dart` | REFACTOR-1, REFACTOR-3, REFACTOR-4 |

## 최종 검증

원격 runner 기준이며, local 변경분을 `/Users/toki/agent-work/iop`에 동기화한 뒤 실행한다. 동기화하지 못했으면 검증을 완료로 표시하지 말고 실제 차단 사유를 `CODE_REVIEW-cloud-G08.md`에 기록한다.

```bash
cd apps/client && flutter test
cd apps/client && flutter analyze --no-fatal-infos
```

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
