<!-- task=m-workspace-port-env-standardization/01_compose_env_defaults plan=0 tag=REFACTOR -->

# Code Review Reference - REFACTOR

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.

## 개요

date=2026-06-07
task=m-workspace-port-env-standardization/01_compose_env_defaults, plan=0, tag=REFACTOR

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.

1. 판정을 append한다.
2. `CODE_REVIEW-local-G04.md` -> `code_review_local_G04_N.log`, `PLAN-local-G04.md` -> `plan_local_G04_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-workspace-port-env-standardization/01_compose_env_defaults/`로 이동한다.
4. PASS이고 task group이 `m-workspace-port-env-standardization`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Align `.env.example` | [x] |
| [REFACTOR-2] Align compose host publish defaults | [x] |

## 구현 체크리스트

- [x] `.env.example`의 host publish 기본값을 slot plan에 맞춰 갱신하고 필요한 compose env key를 보강한다.
- [x] `docker-compose.yml`의 host publish 기본값과 web build args를 `.env.example` 기준과 맞춘다.
- [x] `docker compose --env-file .env.example config` 또는 원격 runner에서 동등한 render 검증을 실행해 출력에 새 host publish 값이 반영되는지 확인한다. (구현 시점에는 현재 컨테이너에 `docker` 미설치로 로컬 render 불가. code-review에서 `/tmp/iop-docker-compose` standalone Compose v2.27.1과 dummy `IOP_EDGE_NODE_TOKEN`으로 render 검증을 보강했고, 새 host publish 값 반영을 확인했다.)
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G04_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 task artifact를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리를 archive로 이동한다.
- [x] PASS split 작업이면 이동 후 빈 active parent 유지/제거 상태를 확인한다.

## 계획 대비 변경 사항

- 계획의 before/after 스니펫과 동일하게 두 파일만 수정했다. 추가/이탈 변경 없음.
- `.env.example`은 기존 그룹 구조(Control Plane 블록 / Postgres 블록 / Redis 블록)를 유지하면서, plan에서 보강하라고 한 `IOP_CONTROL_PLANE_EDGE_WIRE_PORT=19081`과 `IOP_EDGE_NODE_PORT=19090`을 Control Plane 포트 그룹 끝(web port 위)에 추가했다. plan after 스니펫의 나열 순서를 그대로 따르되 기존 파일의 의미 그룹 배치를 깨지 않았다.
- compose render 최종 검증은 현재 컨테이너에 `docker`가 없어 로컬에서 실행하지 못했다. plan의 명시(“현재 컨테이너에서 Docker 사용이 불가하면 차단 사유와 원격 미실행 사유를 기록한다”)에 따라 원격 render/smoke는 `03+01,02_remote_smoke_evidence`로 위임하고, 로컬에서는 정적 grep 검증으로 대체했다.

## 주요 설계 결정

- host publish 좌측(default)만 slot plan 값(15400/16300/18000/13000)으로 바꾸고, 컨테이너 우측 포트(5432/6379/9080/3000/9090)는 그대로 두어 컨테이너 내부 계약을 보존했다.
- container-internal 통신 경로는 변경하지 않았다: `IOP_DATABASE_URL`의 `postgres:5432`, `IOP_REDIS_URL`의 `redis:6379`, control-plane healthcheck의 `127.0.0.1:9080`, `IOP_EDGE_LISTEN`의 `0.0.0.0:9090`, `IOP_CONTROL_PLANE_EDGE_WIRE_ADDR`의 `control-plane:19081`은 모두 container 포트 기준으로 유지된다.
- web build arg `IOP_CONTROL_PLANE_HTTP_URL` 기본값을 `http://localhost:18000`으로 맞춰 브라우저(host) 측 접근 포트와 CP HTTP host publish 기본값을 일치시켰다. `IOP_CONTROL_PLANE_WIRE_URL`은 wire host publish가 `19080`으로 유지되므로 변경하지 않았다.
- token/secret 원문은 추가하지 않았다(`IOP_EDGE_NODE_TOKEN`은 compose의 required-var 형태로만 존재, `.env.example`에 raw 값 없음).

## 사용자 리뷰 요청

_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

## 리뷰어를 위한 체크포인트

- `.env.example`과 `docker-compose.yml`의 host publish defaults가 같은 slot plan을 말하는지 확인한다.
- container-internal DB/Redis/healthcheck 포트가 host publish 변경과 섞이지 않았는지 확인한다.
- token/secret 원문이 추가되지 않았는지 확인한다.

## 검증 결과

_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._

### REFACTOR-1 중간 검증

```bash
$ rg --sort path 'IOP_WEB_PORT=3000|IOP_POSTGRES_PORT=5432|IOP_REDIS_PORT=6379|IOP_CONTROL_PLANE_HTTP_PORT=9080' .env.example
(출력 없음, exit=1)
```

기대 결과(출력 없음)와 일치. legacy host publish 기본값이 `.env.example`에 남아 있지 않다.

### REFACTOR-2 중간 검증

```bash
$ rg --sort path ':-5432|:-6379|:-9080|localhost:9080' docker-compose.yml .env.example
(출력 없음, exit=1)
```

`:-default` 형태의 legacy host publish/URL drift가 없다. container-internal 참조는 `:-` 기본값 형태가 아니므로(예: `postgres:5432`, `redis:6379`, healthcheck `127.0.0.1:9080`) 매치되지 않으며 그대로 유지된다.

보조 정적 검증 (container-internal vs host publish 분리 확인):

```bash
$ rg -n 'postgres:5432|redis:6379|127.0.0.1:9080|:19080|:19081|0.0.0.0:9090|control-plane:19081' docker-compose.yml
12:      - "${IOP_POSTGRES_BIND:-127.0.0.1}:${IOP_POSTGRES_PORT:-15400}:5432"
26:      - "${IOP_REDIS_BIND:-127.0.0.1}:${IOP_REDIS_PORT:-16300}:6379"
41:      IOP_DATABASE_URL: "postgres://${IOP_POSTGRES_USER:-iop}:${IOP_POSTGRES_PASSWORD:-iop_dev_password}@postgres:5432/${IOP_POSTGRES_DB:-iop-control-plane-dev}?sslmode=disable"
42:      IOP_REDIS_URL: "redis://redis:6379/${IOP_REDIS_DB:-2}"
46:      - "${IOP_CONTROL_PLANE_WIRE_BIND:-0.0.0.0}:${IOP_CONTROL_PLANE_WIRE_PORT:-19080}:19080"
47:      - "${IOP_CONTROL_PLANE_EDGE_WIRE_BIND:-127.0.0.1}:${IOP_CONTROL_PLANE_EDGE_WIRE_PORT:-19081}:19081"
54:      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:9080/healthz"]
68:      IOP_CONTROL_PLANE_EDGE_WIRE_ADDR: "control-plane:19081"

$ rg -n '15400|16300|18000|13000' docker-compose.yml .env.example
.env.example:3:IOP_CONTROL_PLANE_HTTP_PORT=18000
.env.example:7:IOP_WEB_PORT=13000
.env.example:12:IOP_POSTGRES_PORT=15400
.env.example:14:IOP_REDIS_PORT=16300
docker-compose.yml:12:      - "${IOP_POSTGRES_BIND:-127.0.0.1}:${IOP_POSTGRES_PORT:-15400}:5432"
docker-compose.yml:26:      - "${IOP_REDIS_BIND:-127.0.0.1}:${IOP_REDIS_PORT:-16300}:6379"
docker-compose.yml:45:      - "${IOP_CONTROL_PLANE_HTTP_BIND:-127.0.0.1}:${IOP_CONTROL_PLANE_HTTP_PORT:-18000}:9080"
docker-compose.yml:83:        IOP_CONTROL_PLANE_HTTP_URL: "${IOP_CONTROL_PLANE_HTTP_URL:-http://localhost:18000}"
docker-compose.yml:87:      - "${IOP_WEB_BIND:-0.0.0.0}:${IOP_WEB_PORT:-13000}:3000"
```

### 최종 검증

```bash
$ command -v docker
(출력 없음, exit=1 — 현재 컨테이너에 docker 미설치)
```

`docker`가 현재 컨테이너에 없어 `docker compose --env-file .env.example config` 기반 render 검증을 로컬에서 실행하지 못했다. plan의 최종 검증 항목 지시("현재 컨테이너에서 Docker 사용이 불가하면 차단 사유와 원격 미실행 사유를 기록한다")에 따라 차단 사유를 기록한다. 원격 compose render와 compose smoke evidence 수집은 `03+01,02_remote_smoke_evidence` subtask로 분리되어 있으므로, 해당 subtask에서 `docker compose --env-file .env.example config` 출력에 `18000/13000/15400/16300/19080/19081/19090`이 host publish 좌측에 반영되는지를 확인한다.

## 코드리뷰 결과

- 종합 판정: PASS
- 차원별 평가:
  - correctness: Pass
  - completeness: Pass
  - test coverage: Pass
  - API contract: Pass
  - code quality: Pass
  - plan deviation: Pass
  - verification trust: Pass
- 발견된 문제: 없음
- 리뷰어 보강 검증:
  - `IOP_EDGE_NODE_TOKEN=iop_review_dummy /tmp/iop-docker-compose --env-file .env.example config >/tmp/iop-compose-config.out` - PASS; standalone Docker Compose v2.27.1로 daemon 없이 render 확인
  - `rg --sort path '18000|13000|15400|16300|19080|19081|19090' /tmp/iop-compose-config.out` - PASS; 새 host publish 값과 wire 슬롯이 config 출력에 포함됨
  - `rg -n --sort path -C 2 'published: "18000"|published: "15400"|published: "16300"|published: "13000"|published: "19090"|target: 9080|target: 5432|target: 6379|target: 3000|target: 9090' /tmp/iop-compose-config.out` - PASS; host publish 좌측은 표준 슬롯, target은 container 내부 포트 유지
- 다음 단계: PASS - `complete.log` 작성 후 task directory를 archive로 이동한다.
