<!-- task=flutter_first_portal_migration/04_control_plane_ws_endpoint plan=0 tag=CP_WS -->

# Plan - CP_WS

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

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

## 배경

Control Plane은 wire endpoint를 예약만 하고 실제 proto-socket listener를 시작하지 않는다. Flutter Portal이 붙을 수 있으려면 최소한 Portal-Control Plane WS endpoint와 검증 가능한 handshake 계약이 필요하다. 이 작업은 전체 운영 API가 아니라 WS listener와 `PortalHello` request-response만 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/milestones/flutter-first-portal-migration.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/control-plane/internal/wire/wire.go`
- `apps/control-plane/README.md`
- `configs/control-plane.yaml`
- `go.mod`
- `Makefile`
- `proto/iop/control.proto`
- `proto/iop/runtime.proto`
- `proto/gen/iop/control.pb.go`
- `../proto-socket/go/ws_server.go`
- `../proto-socket/go/ws_client.go`
- `../proto-socket/go/test/ws_test.go`

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

- Config loading: `main_test.go` covers DB/Redis defaults and env overrides, not wire listen env.
- WS endpoint: no IOP control-plane WS test exists. Add `apps/control-plane/internal/wire/portal_test.go` or cmd-level test that starts random port WS server and sends `PortalHelloRequest`.
- Full `run` lifecycle: current tests do not start `run`. Add narrow server test in wire package rather than brittle process test.

### 심볼 참조

- `wire.Endpoint`: `apps/control-plane/internal/wire/wire.go:11`, `apps/control-plane/cmd/control-plane/main.go:135`.
- `Protocol`: `apps/control-plane/internal/wire/wire.go:6`, `apps/control-plane/cmd/control-plane/main.go:137`.
- `WireListen`: `apps/control-plane/cmd/control-plane/main.go:26`, `:89`, `:135`, `configs/control-plane.yaml:3`, `docker-compose.yml:44`.

### 분할 판단

분할 정책을 먼저 평가했다. 이 작업은 Go Control Plane protocol/schema 변경이며 Flutter app과 Docker 제거와 독립이다. `05+01,02,04_portal_wire_contract`가 이 endpoint를 소비한다.

### 범위 결정 근거

- Edge 연결 관리, Edge 상태 조회, 권한, 감사, 저장은 범위 밖이다.
- Portal UI client integration은 `05+01,02,04_portal_wire_contract`에서 한다.
- TLS/WSS production certificate handling은 범위 밖이다. 이 작업은 local WS endpoint와 구조만 만든다.

### 빌드 등급

Build lane `cloud-G08`, review lane `cloud-G08`. Protocol/schema와 server lifecycle을 건드리고 tests가 새로 필요한 작업이다.

## 구현 체크리스트

- [ ] [CP_WS-1] `PortalHelloRequest`/`PortalHelloResponse` proto 계약과 Go 생성물을 추가한다.
- [ ] [CP_WS-2] Control Plane proto-socket WS server를 구현하고 `run` lifecycle에 연결한다.
- [ ] [CP_WS-3] WS handshake와 config override 테스트를 추가한다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [CP_WS-1] PortalHello proto 계약

문제: `proto/iop/control.proto:7`은 legacy placeholder만 있고 Portal-Control Plane handshake 계약이 없다.

해결 방법: `control.proto`에 최소 `PortalHelloRequest`와 `PortalHelloResponse`를 추가한다. `make proto`로 Go 생성물을 갱신한다.

Before:

```proto
proto/iop/control.proto:7
// PolicyRule is a placeholder for future edge-level routing/resource constraints.
message PolicyRule {
```

After:

```proto
message PortalHelloRequest {
  string client_id = 1;
  string client_version = 2;
}

message PortalHelloResponse {
  bool ready = 1;
  string protocol = 2;
  int64 server_time_unix_nano = 3;
  string message = 4;
}
```

수정 파일 및 체크리스트:

- [ ] `proto/iop/control.proto`: 새 메시지를 추가한다.
- [ ] `proto/gen/iop/control.pb.go`: `make proto`로 갱신한다.

테스트 작성: generated code 자체 테스트는 작성하지 않는다. `CP_WS-3`의 request-response test가 새 메시지를 사용한다.

중간 검증:

```bash
make proto
```

기대 결과: protoc와 protoc-gen-go가 성공하고 Go 생성물이 최신이다.

### [CP_WS-2] Control Plane WS server

문제: `apps/control-plane/cmd/control-plane/main.go:146`은 proto-socket listener 시작을 TODO로 남긴다. `apps/control-plane/internal/wire/wire.go:11`의 `Endpoint`는 listen 문자열만 담는다.

해결 방법: `apps/control-plane/internal/wire/portal.go`를 추가해 proto-socket `NewWsServer`를 감싸는 server를 구현한다. `main.run`은 HTTP server와 WS server를 함께 시작하고, context 취소 시 둘 다 닫는다. WS path는 `/portal`로 고정한다.

Before:

```go
apps/control-plane/cmd/control-plane/main.go:146
// TODO(control-plane): start the protobuf-socket listener here and use it
// for Portal-Control Plane and Control Plane-Edge message sessions.
```

After:

```go
portalServer, err := wire.NewPortalServer(cfg.Server.WireListen, logger)
if err != nil {
    return fmt.Errorf("wire server: %w", err)
}
if err := portalServer.Start(ctx); err != nil {
    return fmt.Errorf("start wire server: %w", err)
}
defer func() { _ = portalServer.Stop() }()
```

수정 파일 및 체크리스트:

- [ ] `apps/control-plane/internal/wire/portal.go`: listen parsing, parser map, server start/stop, hello handler를 구현한다.
- [ ] `apps/control-plane/internal/wire/wire.go`: `Endpoint`가 path나 protocol 상수를 노출해야 하면 최소로 확장한다.
- [ ] `apps/control-plane/cmd/control-plane/main.go`: wire server lifecycle과 `IOP_WIRE_LISTEN` env override를 연결한다.
- [ ] `configs/control-plane.yaml`: 기존 `wire_listen` 기본값을 유지한다.

테스트 작성: 작성한다. `CP_WS-3`에서 wire server를 직접 시작해 request-response를 검증한다.

중간 검증:

```bash
go test -count=1 ./apps/control-plane/internal/wire
```

기대 결과: wire package tests 통과.

### [CP_WS-3] Tests

문제: `apps/control-plane/cmd/control-plane/main_test.go:1`은 config와 log field만 검증한다. WS endpoint가 실제로 binary proto-socket request-response를 처리하는지 보장하지 않는다.

해결 방법: wire package test에서 random free port를 열고 `proto_socket.DialWsWithHeartbeat`로 `/portal`에 연결해 `PortalHelloRequest`를 보내고 `PortalHelloResponse.ready=true`, `protocol=protobuf-socket`을 검증한다. cmd test에는 `IOP_WIRE_LISTEN` env override 검증을 추가한다.

수정 파일 및 체크리스트:

- [ ] `apps/control-plane/internal/wire/portal_test.go`: hello request-response test를 추가한다.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: `IOP_WIRE_LISTEN` override test를 추가한다.

테스트 작성: 작성한다. WS request-response와 env override 모두 regression test다.

중간 검증:

```bash
go test -count=1 ./apps/control-plane/...
```

기대 결과: control-plane 전체 tests 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `proto/iop/control.proto` | CP_WS-1 |
| `proto/gen/iop/control.pb.go` | CP_WS-1 |
| `apps/control-plane/internal/wire/portal.go` | CP_WS-2 |
| `apps/control-plane/internal/wire/wire.go` | CP_WS-2 |
| `apps/control-plane/cmd/control-plane/main.go` | CP_WS-2 |
| `apps/control-plane/internal/wire/portal_test.go` | CP_WS-3 |
| `apps/control-plane/cmd/control-plane/main_test.go` | CP_WS-3 |

## 최종 검증

```bash
command -v protoc
command -v protoc-gen-go
make proto
go test -count=1 ./apps/control-plane/...
go test ./...
git diff --check
```

기대 결과: protoc/protoc-gen-go 경로가 출력되고, proto generation과 Go tests가 통과한다. `go test ./...`는 최종 전체 회귀 확인으로 cache output을 허용한다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
