<!-- task=flutter_first_portal_migration/05+01,02,04_portal_wire_contract plan=0 tag=PORTAL_WIRE -->

# Plan - PORTAL_WIRE

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

구현 마지막 단계에서 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 반드시 실제 변경 내용과 검증 출력으로 채운다. 검증 명령을 실행하고 실제 stdout/stderr를 기록한 뒤 active 파일은 그대로 두고 리뷰 준비 상태만 보고한다. `complete.log` 작성, `.md` 파일 로그화, archive 이동은 code-review-skill 전용이다.

## 의존 관계 및 구현 순서

이 subtask directory는 `05+01,02,04_portal_wire_contract`이므로 같은 task group의 `01_flutter_portal_scaffold`, `02_proto_socket_browser_transport`, `04_control_plane_ws_endpoint`에 `complete.log`가 생긴 뒤 시작한다. 추가 의존성은 없다.

## 배경

Flutter Portal 앱, browser-compatible Dart WS transport, Control Plane WS endpoint가 생긴 뒤에는 실제 Dart protobuf 계약과 Portal client 경계를 연결해야 한다. 이 작업은 전체 운영 화면이 아니라 generated Dart proto, parser map, PortalHello handshake, UI 상태 반영까지의 최소 end-to-end compile path를 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/milestones/flutter-first-portal-migration.md`
- `proto/iop/control.proto`
- `proto/iop/runtime.proto`
- `proto/iop/node.proto`
- `proto/iop/job.proto`
- `Makefile`
- `../proto-socket/dart/lib/proto_socket.dart`
- `../proto-socket/dart/lib/src/ws_protobuf_client.dart`
- `../proto-socket/dart/lib/src/transport.dart`
- `../proto-socket/dart/pubspec.yaml`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/internal/wire/wire.go`
- `apps/web/src/lib/iop-client/client.ts`
- `apps/web/src/lib/iop-client/connection.ts`
- `apps/web/src/lib/iop-client/types.ts`

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

- Dart proto generation: 기존 Makefile은 Go generation만 제공한다. 새 `proto-dart` target이나 script가 필요하다.
- Portal parser map: 기존 TypeScript stub에는 실제 protobuf parser가 없다. 새 Dart unit test가 `PortalHelloResponse.fromBuffer` parser map 등록을 검증해야 한다.
- Flutter Web compile: `flutter build web`로 browser target import와 proto-socket web export를 검증한다.
- 실제 Control Plane process와 Flutter app 통합 smoke는 범위 밖이다. Control Plane WS 자체는 `04`에서 Go test로 검증한다.

### 심볼 참조

- `IopClientConfig`: `apps/web/src/lib/iop-client/client.ts:1`, `apps/web/src/lib/iop-client/types.ts:5`.
- `connectIopWire`: `apps/web/src/lib/iop-client/connection.ts:3`; 삭제 후 Dart `PortalWireClient`가 대체한다.
- `WsProtobufClient`: `../proto-socket/dart/lib/proto_socket.dart:9`; `02` 완료 후 conditional export를 사용해야 한다.
- `PortalHelloRequest`/`PortalHelloResponse`: `04` 완료 후 `proto/iop/control.proto`와 generated Go/Dart에서 사용한다.

### 분할 판단

분할 정책을 먼저 평가했다. 이 작업은 app call-site rollout이며 `01`, `02`, `04`의 foundation 변경에 의존한다. Docker 제거인 `03`과는 독립이므로 의존성에 넣지 않는다.

### 범위 결정 근거

- 전체 Portal 화면, Edge list, job list, auth는 범위 밖이다.
- Control Plane WS endpoint 구현은 `04`에서 완료된 것으로 가정한다.
- proto-socket Dart browser transport 구현은 `02`에서 완료된 것으로 가정한다.
- Docker/compose 배포는 `03`에서 완료된 것으로 가정한다.

### 빌드 등급

Build lane `cloud-G08`, review lane `cloud-G08`. Cross-repo Dart package, generated protobuf, Flutter Web compile, protocol parser map이 맞물리는 작업이다.

## 구현 체크리스트

- [ ] [PORTAL_WIRE-1] IOP proto Dart generation target과 generated Dart files를 추가한다.
- [ ] [PORTAL_WIRE-2] Flutter Portal `PortalWireClient`와 parser map을 구현한다.
- [ ] [PORTAL_WIRE-3] Portal UI/test가 WS endpoint 설정과 hello 상태를 반영하도록 연결한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [PORTAL_WIRE-1] Dart proto generation

문제: `Makefile:28`은 Go protobuf generation만 정의한다. Flutter Portal이 `proto/iop/control.proto`를 소비하려면 Dart generated files와 재생성 명령이 필요하다.

해결 방법: `Makefile`에 `proto-dart` target을 추가하거나 `scripts/proto-dart.sh`를 추가한다. `protoc-gen-dart`가 없으면 구현 환경에서는 `command -v protoc-gen-dart` 실패를 기록하고 설치/경로 조치를 `계획 대비 변경 사항`에 남긴다. Generated files는 `apps/portal/lib/gen/proto/iop/*.pb.dart` 아래 둔다.

Before:

```makefile
Makefile:28
proto:
	protoc \
		--go_out=. \
```

After:

```makefile
proto-dart:
	protoc \
		--dart_out=apps/portal/lib/gen \
		--proto_path=. \
		proto/iop/runtime.proto \
		proto/iop/node.proto \
		proto/iop/control.proto \
		proto/iop/job.proto
```

수정 파일 및 체크리스트:

- [ ] `Makefile`: `proto-dart` target을 추가한다.
- [ ] `apps/portal/pubspec.yaml`: `protobuf`와 local `proto_socket` path dependency를 추가한다.
- [ ] `apps/portal/lib/gen/proto/iop/*.pb.dart`: `make proto-dart`로 생성한다.
- [ ] `apps/portal/README.md`: Dart proto generation prerequisite를 기록한다.

테스트 작성: generation target 자체 unit test는 작성하지 않는다. `PORTAL_WIRE-2` parser map test가 generated code 사용을 검증한다.

중간 검증:

```bash
command -v protoc
command -v protoc-gen-dart
make proto-dart
```

기대 결과: `protoc`와 `protoc-gen-dart` 경로가 출력되고 Dart 생성물이 갱신된다. 현재 환경에서 `protoc-gen-dart`가 없으면 설치/경로 조치 또는 blocker를 review file에 기록한다.

### [PORTAL_WIRE-2] PortalWireClient와 parser map

문제: 기존 TypeScript stub `apps/web/src/lib/iop-client/connection.ts:5`는 browser transport가 확정되지 않아 연결을 던진다. Flutter Portal에는 typed proto-socket client가 필요하다.

해결 방법: `apps/portal/lib/iop_wire/portal_wire_client.dart`와 `parser_map.dart`를 추가한다. `PortalWireClient.hello()`는 `PortalHelloRequest`를 보내고 `PortalHelloResponse`를 받는다. 실제 WebSocket 생성은 `WsProtobufClient` conditional export를 사용한다.

Before:

```ts
apps/web/src/lib/iop-client/connection.ts:9
throw new Error("IOP protobuf-socket client is not connected yet.");
```

After:

```dart
final response = await client.sendRequest<PortalHelloRequest, PortalHelloResponse>(
  PortalHelloRequest(clientId: clientId, clientVersion: version),
  timeout: const Duration(seconds: 3),
);
```

수정 파일 및 체크리스트:

- [ ] `apps/portal/lib/iop_wire/parser_map.dart`: `PortalHelloRequest`, `PortalHelloResponse`, heartbeat parser를 등록한다.
- [ ] `apps/portal/lib/iop_wire/portal_wire_client.dart`: connect/hello/close 경계를 구현한다.
- [ ] `apps/portal/test/iop_wire/parser_map_test.dart`: parser map이 generated type name을 포함하는지 검증한다.
- [ ] `apps/portal/test/iop_wire/portal_wire_client_test.dart`: fake transport 또는 fake client로 hello result mapping을 검증한다.

테스트 작성: 작성한다. 실제 네트워크 없는 unit test를 우선한다.

중간 검증:

```bash
cd apps/portal && flutter test test/iop_wire
```

기대 결과: parser map과 client unit tests 통과.

### [PORTAL_WIRE-3] UI 연결 상태 반영

문제: `01`의 앱은 endpoint 표시와 placeholder 상태만 가진다. Milestone 완료 기준은 Flutter Web이 `dart:io` 없이 WebSocket binary frame 경로를 갖는 것이다.

해결 방법: Portal home에 wire URL과 hello 상태를 표시하고, 앱 시작 또는 버튼 클릭 시 `PortalWireClient.hello()`를 호출하는 얇은 state layer를 둔다. 테스트에서는 fake client를 주입해 connected/error 상태를 검증한다.

수정 파일 및 체크리스트:

- [ ] `apps/portal/lib/main.dart`: wire status 영역을 client abstraction에 연결한다.
- [ ] `apps/portal/lib/portal_config.dart`: wire URL default가 `ws://localhost:19080/portal`인지 유지한다.
- [ ] `apps/portal/test/widget_test.dart`: fake client로 ready/pending/error 상태를 검증한다.

테스트 작성: 작성한다. widget test에서 fake `PortalWireClient`를 주입해 네트워크 없이 상태 전환을 확인한다.

중간 검증:

```bash
cd apps/portal && flutter test
```

기대 결과: Portal 전체 Flutter tests 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `Makefile` | PORTAL_WIRE-1 |
| `apps/portal/pubspec.yaml` | PORTAL_WIRE-1 |
| `apps/portal/lib/gen/proto/iop/*.pb.dart` | PORTAL_WIRE-1 |
| `apps/portal/README.md` | PORTAL_WIRE-1 |
| `apps/portal/lib/iop_wire/parser_map.dart` | PORTAL_WIRE-2 |
| `apps/portal/lib/iop_wire/portal_wire_client.dart` | PORTAL_WIRE-2 |
| `apps/portal/test/iop_wire/*.dart` | PORTAL_WIRE-2 |
| `apps/portal/lib/main.dart` | PORTAL_WIRE-3 |
| `apps/portal/lib/portal_config.dart` | PORTAL_WIRE-3 |
| `apps/portal/test/widget_test.dart` | PORTAL_WIRE-3 |

## 최종 검증

```bash
command -v protoc
command -v protoc-gen-dart
make proto-dart
cd apps/portal && flutter pub get
cd apps/portal && flutter test
cd apps/portal && flutter build web --dart-define=IOP_CONTROL_PLANE_HTTP_URL=http://localhost:9080 --dart-define=IOP_CONTROL_PLANE_WIRE_URL=ws://localhost:19080/portal
git diff --check
```

기대 결과: Dart proto generation, Flutter tests, Flutter Web build가 통과한다. Flutter/Dart test cache는 허용하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
