<!-- task=flutter_first_portal_migration/03+01_web_delete_flutter_deploy plan=0 tag=WEB_DEPLOY -->

# Code Review Reference - WEB_DEPLOY

> **[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.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-24
task=flutter_first_portal_migration/03+01_web_delete_flutter_deploy, plan=0, tag=WEB_DEPLOY

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

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

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/flutter_first_portal_migration/03+01_web_delete_flutter_deploy/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다.
4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [WEB_DEPLOY-1] `apps/web` TypeScript/Next.js 제품 경로와 npm 배포 파일을 삭제한다. | [x] |
| [WEB_DEPLOY-2] `bin/web.sh`, compose `web` service, Dockerfile을 Flutter Web 산출물 서빙 흐름으로 교체한다. | [x] |
| [WEB_DEPLOY-3] 문서와 정적 검색 검증을 Flutter-first 경로로 갱신한다. | [x] |

## 구현 체크리스트

- [x] [WEB_DEPLOY-1] `apps/web` TypeScript/Next.js 제품 경로와 npm 배포 파일을 삭제한다.
- [x] [WEB_DEPLOY-2] `bin/web.sh`, compose `web` service, Dockerfile을 Flutter Web 산출물 서빙 흐름으로 교체한다.
- [x] [WEB_DEPLOY-3] 문서와 정적 검색 검증을 Flutter-first 경로로 갱신한다.
- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [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_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/flutter_first_portal_migration/03+01_web_delete_flutter_deploy/`를 `agent-task/archive/YYYY/MM/flutter_first_portal_migration/03+01_web_delete_flutter_deploy/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/flutter_first_portal_migration/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

- `apps/portal/Dockerfile` static server stage 구성에 nginx config 파일이 필요해 `apps/portal/nginx.conf`를 plan 외 신규 파일로 추가했다. plan에서는 "static server stage 추가" 까지만 명시했고, nginx의 listen 포트를 3000으로 맞추기 위한 표준선 결정이다.
- `apps/web` 디렉터리를 통째로 삭제했다. plan 체크리스트의 개별 파일 삭제 + `apps/web/README.md`는 directory 제거 시 함께 삭제한다는 항목과 일치한다.
- `bin/web.sh`의 환경변수 이름을 `CONTROL_PLANE_INTERNAL_HTTP_URL` / `NEXT_PUBLIC_*` 에서 `IOP_CONTROL_PLANE_HTTP_URL` / `IOP_CONTROL_PLANE_WIRE_URL`로 재정렬했다. plan은 이 두 이름을 `--dart-define` 전달 변수로 명시했고, Flutter Web에는 Next 전용 변수가 의미 없다.
- Docker가 환경에 없어 `docker compose config` / `docker compose build web`은 실행하지 못했다. plan의 "Docker CLI가 없을 수 있다. 사용 가능하면 실행, 없으면 blocker로 남긴다" 지침에 따라 blocker로 기록했다.

## 주요 설계 결정

- compose `web` build context는 `..`(repo parent), dockerfile은 `iop/apps/portal/Dockerfile`로 두어 sibling `../proto-socket/dart` path dependency가 추가될 때 그대로 빌드에 접근할 수 있게 했다. 이는 control-plane Dockerfile의 기존 패턴과 동일하다.
- builder 단계는 `ghcr.io/cirruslabs/flutter:stable`을 사용했다. Flutter SDK를 별도 설치하지 않고 `flutter build web --release`를 그대로 실행할 수 있고, runner 단계는 `nginx:1.27-alpine`으로 static asset만 서빙한다.
- runner의 nginx 설정은 SPA 라우팅을 위해 `try_files $uri $uri/ /index.html;` fallback을 사용한다.
- `bin/web.sh`는 `flutter run -d web-server` + `--dart-define` 흐름으로 바꿔, Docker 이미지가 굳이 빌드되지 않더라도 호스트에서 dev server를 그대로 띄울 수 있게 했다.
- `IOP_CONTROL_PLANE_WIRE_URL`의 기본값을 `ws://localhost:19080/portal`로 두어 Portal-Control Plane 통신이 proto-socket WebSocket 표준선에 맞도록 했다.
- `.gitignore`에서 `apps/web/*` 항목 전체를 제거하고 Flutter Portal artifact ignore만 남겼다.

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

- `apps/web`에 제품 TypeScript/Next/npm 경로가 남지 않았는지 확인한다.
- `bin/web.sh`가 Flutter `web-server`와 `--dart-define` 흐름을 쓰는지 확인한다.
- compose `web` service가 repo parent context와 `apps/portal/Dockerfile`을 사용하는지 확인한다.
- Docker가 없는 환경이면 blocker 기록이 명확한지, Docker가 있으면 `docker compose build web` 실제 출력인지 확인한다.

## 검증 결과

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

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.

### WEB_DEPLOY-1 중간 검증
```bash
$ rg --sort path "next|react|typescript|NEXT_PUBLIC|npm run|apps/web" README.md docs apps bin docker-compose.yml Makefile
apps/edge/internal/input/a2a/server.go:func (s *Server) withAuth(next http.HandlerFunc) http.HandlerFunc {
apps/edge/internal/input/a2a/server.go:		next(w, r)
apps/edge/internal/node/registry.go:	nextIndex int
apps/edge/internal/node/registry.go:			entry.Index = r.nextIndex
apps/edge/internal/node/registry.go:		if entry.Index >= r.nextIndex {
apps/edge/internal/node/registry.go:			r.nextIndex = entry.Index + 1
apps/edge/internal/transport/heartbeat_test.go:// wait-timer state check), keeping wait > interval gives the peer's next
apps/node/internal/adapters/cli/opencode_sse.go:		next := func() (string, bool) {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			if v, ok := next(); ok {
apps/node/internal/adapters/cli/opencode_sse.go:			case "session.next.text.delta":
apps/node/internal/adapters/cli/opencode_sse.go:			case "session.next.step.ended":
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.text.delta",
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.text.delta",
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.step.ended",
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.text.delta",
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.text.delta",
apps/node/internal/adapters/cli/opencode_sse_blackbox_test.go:		"type": "session.next.text.delta",
apps/node/internal/adapters/cli/persistent.go:			// Drain output so the process remains usable for the next run.
apps/node/internal/adapters/cli/status/parser.go:			next := strings.TrimSpace(lines[i+1])
apps/node/internal/adapters/cli/status/parser.go:			if strings.HasPrefix(next, "(") {
apps/node/internal/adapters/cli/status/parser.go:				reset = strings.TrimSpace(reset + " " + next)
apps/node/internal/adapters/cli/status/parser.go:// the next opposing header so the session header is never paired with the
apps/node/internal/adapters/cli/status/parser_test.go:	// The week section, starting at its header, gets the next available
apps/node/internal/transport/client.go:	// gives the peer's next heartbeat an extra chance to overwrite any stray
apps/node/internal/transport/heartbeat_test.go:// heartbeat response was already processed, so the peer's next heartbeat (one
apps/portal/windows/CMakeLists.txt:# Support files are copied into place next to the executable, so that it can
```
모든 매치는 `next` 영어 단어가 Go 변수명/주석/이벤트 상수/Flutter Windows CMake 보일러플레이트에 포함된 무관 케이스다. Next.js, React, TypeScript, NEXT_PUBLIC, npm run, apps/web 토큰은 없다.

### WEB_DEPLOY-2 중간 검증
```bash
$ make portal-build-web
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
Resolving dependencies...
Downloading packages...
  matcher 0.12.19 (0.12.20 available)
  meta 1.17.0 (1.18.2 available)
  test_api 0.7.10 (0.7.12 available)
  vector_math 2.2.0 (2.3.0 available)
Got dependencies!
4 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
Compiling lib/main.dart for the Web...
Wasm dry run succeeded. Consider building and testing your application with the `--wasm` flag. See docs for more info: https://docs.flutter.dev/platform-integration/web/wasm
Use --no-wasm-dry-run to disable these warnings.
Compiling lib/main.dart for the Web...                             22.3s
✓ Built build/web
```

### WEB_DEPLOY-3 중간 검증
```bash
$ rg --sort path "Next\.js|React|TypeScript|NEXT_PUBLIC|npm|apps/web" README.md docs apps bin docker-compose.yml Makefile
apps/node/internal/adapters/factory_internal_test.go:				Command:      "/config/.npm-global/bin/cline",
apps/node/internal/adapters/factory_internal_test.go:	if prof.Command != "/config/.npm-global/bin/cline" {
```
두 매치는 cline CLI를 npm으로 글로벌 설치한 외부 경로(`.npm-global`)이 테스트 fixture에 들어간 것으로 Next.js/React/TypeScript와 무관하다.

### 최종 검증
```bash
$ make portal-test
cd apps/portal && flutter test
Resolving dependencies...
Downloading packages...
  matcher 0.12.19 (0.12.20 available)
  meta 1.17.0 (1.18.2 available)
  test_api 0.7.10 (0.7.12 available)
  vector_math 2.2.0 (2.3.0 available)
Got dependencies!
4 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
00:00 +0: loading /config/workspace/iop/apps/portal/test/widget_test.dart
00:00 +0: /config/workspace/iop/apps/portal/test/widget_test.dart: Portal App basic rendering test
00:00 +1: /config/workspace/iop/apps/portal/test/widget_test.dart: Portal App basic rendering test
00:00 +2: All tests passed!

$ make portal-build-web
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
Resolving dependencies...
Downloading packages...
  matcher 0.12.19 (0.12.20 available)
  meta 1.17.0 (1.18.2 available)
  test_api 0.7.10 (0.7.12 available)
  vector_math 2.2.0 (2.3.0 available)
Got dependencies!
4 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.
Compiling lib/main.dart for the Web...
Wasm dry run succeeded. Consider building and testing your application with the `--wasm` flag. See docs for more info: https://docs.flutter.dev/platform-integration/web/wasm
Use --no-wasm-dry-run to disable these warnings.
Compiling lib/main.dart for the Web...                             22.3s
✓ Built build/web

$ rg --sort path "Next\.js|React|TypeScript|NEXT_PUBLIC|npm|apps/web" README.md docs apps bin docker-compose.yml Makefile
apps/node/internal/adapters/factory_internal_test.go:				Command:      "/config/.npm-global/bin/cline",
apps/node/internal/adapters/factory_internal_test.go:	if prof.Command != "/config/.npm-global/bin/cline" {
```
두 매치는 의도된 예외(외부 cline CLI 설치 경로)이다.

```bash
$ command -v docker
(no output)
$ echo "exit=$?"
exit=1
```
Docker CLI가 환경에 없다. blocker로 기록한다.

```bash
$ docker compose config
(not executed: docker missing)

$ docker compose build web
(not executed: docker missing)
```
Docker 실행 검증은 blocker로 남긴다. compose `web` 서비스 변경은 정적 검토에 기반한 것이며, Docker가 있는 환경에서 `docker compose build web` 통과 여부는 후속 환경에서 확인이 필요하다.

```bash
$ git diff --check
(no output)
$ echo "exit=$?"
exit=0
```
whitespace 오류 없음.

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## Section Ownership

| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus areas |
| 검증 결과 | Implementing agent | Paste actual stdout/stderr only |
| 코드리뷰 결과 | Review agent appends | Not included in stub |

## 코드리뷰 결과

종합 판정: FAIL

차원별 평가:
- correctness: Fail
- completeness: Fail
- test coverage: Pass
- API contract: Fail
- code quality: Pass
- plan deviation: Pass
- verification trust: Fail

발견된 문제:
- Required: `apps/portal/Dockerfile:13`에서 `apps/portal`의 `pubspec.yaml`과 `pubspec.lock`만 복사한 뒤 `apps/portal/Dockerfile:14`에서 `flutter pub get`을 실행합니다. 하지만 `apps/portal/pubspec.yaml:39`는 `proto_socket`을 `../../../proto-socket/dart` path dependency로 선언하므로, Docker 이미지 안의 작업 디렉터리 `/workspace/iop/apps/portal` 기준으로 `/workspace/proto-socket/dart`가 먼저 존재해야 합니다. 현재 Dockerfile은 sibling `proto-socket/dart`를 전혀 복사하지 않아 Docker가 있는 환경에서 `docker compose build web`이 `Path dependency ... does not exist` 류의 오류로 실패할 구조입니다. `proto-socket/dart`의 manifest와 source를 `/workspace/proto-socket/dart`로 복사한 뒤 `flutter pub get`과 `flutter build web`을 실행하도록 고치고, Docker가 있으면 `docker compose config`와 `docker compose build web`까지 확인하세요.

다음 단계:
- FAIL: Required issue를 해결하는 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 즉시 작성한다.
