<!-- task=m-architecture-refactor-foundation/06_cp_api_split plan=0 tag=REFACTOR -->

# PLAN - REFACTOR cp-api-split

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

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

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

## 배경

`apps/control-plane/cmd/control-plane/main.go`가 CLI, config, server lifecycle, HTTP handler, DTO 변환, Edge/fleet orchestration을 한 파일에 갖고 있다. cp-client Epic의 첫 작업은 HTTP API 경계를 먼저 나눠 이후 fleet polling 정책과 client 상태 분리를 안전하게 얹는 기반을 만드는 것이다. 이번 계획은 동작 변경 없이 Go package `main` 내부 파일 경계를 분리한다.

## 사용자 리뷰 요청 흐름

구현 중 차단 결정은 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:
  - `cp-api-split`: `apps/control-plane/cmd/control-plane/main.go`의 server lifecycle, HTTP handler, DTO conversion, Edge command/fleet orchestration을 분리한다.
- 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/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/control-plane/internal/wire/edge_registry.go`
- `apps/control-plane/internal/wire/edge_server.go`
- `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`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md` 존재 및 정독 완료. Control Plane 변경은 `agent-test/local/control-plane-smoke.md`를 baseline으로 사용한다.
- 적용 명령: 변경한 Control Plane 패키지는 `go test ./apps/control-plane/...`를 실행한다. fresh execution이 필요하므로 계획 명령은 `go test -count=1 ./apps/control-plane/...`로 고정한다.
- HTTP lifecycle 경계를 건드리므로 health/readiness는 새 `httptest` 단위 테스트로 확인한다. Docker compose/curl smoke는 이 계획의 범위가 실제 프로세스 기동 변경이 아니므로 생략한다.
- `agent-test/local/client-smoke.md`도 읽었지만 이 plan은 Flutter 파일을 변경하지 않으므로 client 검증 명령은 적용하지 않는다.
- `<확인 필요>` 값 없음. agent-test fallback 없음.

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

- 기존 `main_test.go`는 config, log field, Edge registry handler, command handler, fleet handler를 직접 호출해 검증한다.
- `run`이 health/readiness handler를 등록하는 경로는 현재 직접 테스트가 없다. `newHTTPMux` 또는 동등 helper를 추가하고 `/healthz`, `/readyz` 단위 테스트를 작성한다.
- 파일 경계 분리 자체는 동작 변경이 아니므로 기존 handler 테스트가 회귀 검증을 담당한다.

### 심볼 참조

- 제거/이름 변경 예정 없음.
- 이동 예정 call site:
  - `run`: `apps/control-plane/cmd/control-plane/main.go:84`
  - `registerEdgeRegistryHandlers`: `apps/control-plane/cmd/control-plane/main.go:182`, `apps/control-plane/cmd/control-plane/main_test.go:260`, `338`, `401`, `467`, `515`, `530`, `574`, `600`, `616`, `634`, `660`
  - `registerFleetHandlers`: `apps/control-plane/cmd/control-plane/main.go:183`, `apps/control-plane/cmd/control-plane/main_test.go:746`, `818`
  - DTO/view types are package-private and referenced by existing tests in `main_test.go`.

### 분할 판단

- split decision policy를 평가했다. cp-client Epic은 multi-plan으로 분리한다.
- shared task group: `agent-task/m-architecture-refactor-foundation/`
- sibling subtask directories:
  - `06_cp_api_split`: 이 plan. 선행 의존성 없음.
  - `07+06_fleet_polling`: `06_cp_api_split` PASS 후 `complete.log` 필요. 현재 missing.
  - `08_client_state_split`: Flutter client 독립 작업. 선행 의존성 없음.
- 이 plan 자체는 단일 plan으로 유지한다. server lifecycle, handlers, DTO 이동은 같은 package compile/test 루프 안에서 함께 맞춰야 하며, 더 쪼개면 중간 상태에서 package-private type 참조가 흔들린다.

### 범위 결정 근거

- `/fleet/status` bounded concurrency/cache/timeout 정책은 `07+06_fleet_polling`에서 처리한다.
- Flutter bootstrap/state/DTO/repository/widget 분리는 `08_client_state_split`에서 처리한다.
- `apps/control-plane/internal/wire/**` protocol behavior는 변경하지 않는다.
- public JSON field명, HTTP path, status code는 유지한다.

### 빌드 등급

- build lane: `local-G07`. Internal Go refactor지만 server/handler/DTO 파일 경계를 한 번에 나누며 package compile surface가 넓다.
- review lane: `cloud-G07`. Roadmap task 완료 판정과 API boundary 회귀 확인이 필요하다.

## 구현 체크리스트

- [ ] `main.go`에서 CLI/config entrypoint와 server lifecycle 책임을 분리하고 health/readiness mux 단위 테스트를 추가한다.
- [ ] DTO/view type과 proto/registry 변환 함수를 별도 파일로 이동하되 JSON 계약과 secret 비노출 동작을 유지한다.
- [ ] Edge registry handler와 fleet handler 등록/command helper를 별도 파일로 이동하되 기존 handler 테스트를 모두 통과시킨다.
- [ ] `gofmt`와 `go test -count=1 ./apps/control-plane/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [REFACTOR-1] Server Lifecycle 경계 분리

### 문제

`apps/control-plane/cmd/control-plane/main.go:137`부터 `209`까지 `run`이 mux 생성, health/readiness handler, wire 서버 생성, Edge 서버 생성, HTTP server lifecycle을 모두 처리한다. 이 구조에서는 API handler 분리와 fleet orchestration 정책 추가가 모두 `run` 주변에 붙는다.

Before:

```go
// apps/control-plane/cmd/control-plane/main.go:137
func run(ctx context.Context, cfg controlPlaneConfig, logger *zap.Logger) error {
	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok\n"))
	})
	mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ready\n"))
	})
```

### 해결 방법

- 새 파일 `apps/control-plane/cmd/control-plane/server.go`를 만든다.
- `run`, HTTP server lifecycle helper, `newHTTPMux`를 `server.go`로 이동한다.
- `main.go`에는 `main`, `rootCmd`, `serveCmd`, config/env/log field helpers만 남긴다.
- `newHTTPMux(registry, requestStatus, sendCommand)` 또는 동등 helper에서 health/readiness/Edge/fleet handlers를 등록한다.
- `main_test.go`에 `TestHTTPMuxHealthAndReadiness`를 추가해 `/healthz`, `/readyz` body와 status를 검증한다.

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

- [ ] `apps/control-plane/cmd/control-plane/main.go`: `run` 및 HTTP lifecycle 코드를 제거하고 entrypoint/config 중심으로 축소한다.
- [ ] `apps/control-plane/cmd/control-plane/server.go`: `run`, mux 생성, HTTP server lifecycle을 배치한다.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: health/readiness mux 테스트를 추가한다.

### 테스트 작성

- 작성: `apps/control-plane/cmd/control-plane/main_test.go`, `TestHTTPMuxHealthAndReadiness`.
- assertion: `/healthz`는 `200 ok\n`, `/readyz`는 `200 ready\n`.
- fixture: `wire.NewEdgeRegistry()`, nil requester/sender.

### 중간 검증

```bash
gofmt -w apps/control-plane/cmd/control-plane/main.go apps/control-plane/cmd/control-plane/server.go apps/control-plane/cmd/control-plane/main_test.go
go test -count=1 ./apps/control-plane/...
```

## [REFACTOR-2] DTO Conversion 파일 분리

### 문제

`apps/control-plane/cmd/control-plane/main.go:211`부터 `373`까지 JSON DTO type이 있고, `557`부터 `723`까지 registry/proto 변환 함수가 있다. Handler 코드와 DTO 변환 코드가 같은 파일에 있어 HTTP surface 변경과 serialization 계약 검토가 섞인다.

Before:

```go
// apps/control-plane/cmd/control-plane/main.go:243
type edgeStatusResponseView struct {
	RequestID            string                       `json:"request_id"`
	EdgeID               string                       `json:"edge_id"`
	EdgeName             string                       `json:"edge_name"`
	ObservedTimeUnixNano int64                        `json:"observed_time_unix_nano"`
	Nodes                []edgeNodeSnapshotView       `json:"nodes"`
	Capabilities         []edgeCapabilitySummaryView  `json:"capabilities,omitempty"`
	DomainAgents         []edgeDomainAgentSummaryView `json:"domain_agents,omitempty"`
	Metadata             map[string]string            `json:"metadata,omitempty"`
	Error                string                       `json:"error,omitempty"`
}
```

### 해결 방법

- 새 파일 `apps/control-plane/cmd/control-plane/http_views.go`를 만든다.
- DTO structs와 변환 함수 `edgeRegistryViewFromState`, `edgeStatusResponseViewFromProto`, `edgeCapabilityViews`, `edgeDomainAgentViews`, `edgeCommandResponseViewFromProto`, `edgeCommandRecordViewFromRecord`, `fleetEdgeViewFromState`, `newNodeConfigSummaryView`, `edgeNodeEventViewFromRecord`를 이동한다.
- JSON tag와 nil/empty slice behavior는 변경하지 않는다.

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

- [ ] `apps/control-plane/cmd/control-plane/main.go`: DTO/conversion block 제거.
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: DTO/conversion block 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: 기존 secret 비노출 및 fleet leak 테스트가 그대로 통과하는지 확인한다.

### 테스트 작성

- 새 테스트 작성 없음. 기존 `TestEdgeRegistryHTTPHandlersGetLiveStatus`가 raw config secret 비노출을, `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`가 fleet response leak 금지를 검증한다.

### 중간 검증

```bash
gofmt -w apps/control-plane/cmd/control-plane/main.go apps/control-plane/cmd/control-plane/http_views.go apps/control-plane/cmd/control-plane/main_test.go
go test -count=1 ./apps/control-plane/...
```

## [REFACTOR-3] HTTP Handler 파일 분리

### 문제

`apps/control-plane/cmd/control-plane/main.go:382`부터 `550`까지 Edge registry handlers, single Edge command helper, fleet handlers가 연속 배치되어 있다. `07+06_fleet_polling`에서 fleet 정책을 추가하려면 현재 handler block을 먼저 독립 파일로 떼어야 한다.

Before:

```go
// apps/control-plane/cmd/control-plane/main.go:501
func registerFleetHandlers(mux *http.ServeMux, registry *wire.EdgeRegistry, requestStatus edgeStatusRequester, sendCommand edgeCommandSender) {
	mux.HandleFunc("/fleet/status", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			methodNotAllowed(w, http.MethodGet)
			return
		}
```

### 해결 방법

- 새 파일 `apps/control-plane/cmd/control-plane/http_edge_handlers.go`를 만들고 `edgeStatusRequester`, `edgeCommandSender`, `registerEdgeRegistryHandlers`, `handleEdgeCommandPost`, `methodNotAllowed`, `writeJSON`을 배치한다.
- 새 파일 `apps/control-plane/cmd/control-plane/http_fleet_handlers.go`를 만들고 `registerFleetHandlers`를 배치한다.
- `writeJSON` 위치는 Edge/fleet 양쪽에서 공유되도록 같은 package 파일에 둔다.
- 기존 tests가 package-private handler functions를 그대로 호출하도록 symbol명은 유지한다.

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

- [ ] `apps/control-plane/cmd/control-plane/main.go`: handler block 제거.
- [ ] `apps/control-plane/cmd/control-plane/http_edge_handlers.go`: Edge handlers와 shared HTTP helpers 추가.
- [ ] `apps/control-plane/cmd/control-plane/http_fleet_handlers.go`: fleet handlers 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: 기존 호출명이 유지되어 변경을 최소화한다.

### 테스트 작성

- 새 테스트 작성 없음. 기존 Edge/fleet handler tests가 전체 회귀를 담당한다.

### 중간 검증

```bash
gofmt -w apps/control-plane/cmd/control-plane/main.go apps/control-plane/cmd/control-plane/http_edge_handlers.go apps/control-plane/cmd/control-plane/http_fleet_handlers.go apps/control-plane/cmd/control-plane/main_test.go
go test -count=1 ./apps/control-plane/...
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/control-plane/cmd/control-plane/main.go` | REFACTOR-1, REFACTOR-2, REFACTOR-3 |
| `apps/control-plane/cmd/control-plane/server.go` | REFACTOR-1 |
| `apps/control-plane/cmd/control-plane/http_views.go` | REFACTOR-2 |
| `apps/control-plane/cmd/control-plane/http_edge_handlers.go` | REFACTOR-3 |
| `apps/control-plane/cmd/control-plane/http_fleet_handlers.go` | REFACTOR-3 |
| `apps/control-plane/cmd/control-plane/main_test.go` | REFACTOR-1, REFACTOR-2, REFACTOR-3 |

## 최종 검증

```bash
gofmt -w apps/control-plane/cmd/control-plane/main.go apps/control-plane/cmd/control-plane/server.go apps/control-plane/cmd/control-plane/http_views.go apps/control-plane/cmd/control-plane/http_edge_handlers.go apps/control-plane/cmd/control-plane/http_fleet_handlers.go apps/control-plane/cmd/control-plane/main_test.go
go test -count=1 ./apps/control-plane/...
```

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