<!-- task=m-artifact-browser/03+02_build_list_ui plan=0 tag=UI -->

# Plan - UI

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

구현 완료 전 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채우는 것이 필수다. 검증을 실행하고 실제 stdout/stderr를 붙여 넣은 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. archive, `complete.log` 작성, 파일명 변경은 code-review 스킬 전용이다. 사용자만 결정할 수 있는 차단, 사용자 소유 외부 환경/secret/service 준비, 또는 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 메울 수 있는 evidence 공백만으로는 사용자 리뷰 요청을 만들지 않는다.

## 배경

`build-list` Epic의 데이터 기반 작업인 `job-query`와 `apk-only`는 모델/서비스와 mock HTTP/parser 테스트로 바로 처리되었다. 남은 작업은 선택된 Jenkins job의 build 목록을 실제 화면으로 보여주는 UI 묶음이다. 현재 화면은 job 선택 뒤 placeholder만 표시하므로, build 주 정보, 상태 badge, 요약 metadata, 검색, empty/error 상태를 한 번에 설계해야 한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 직접 사용자 prompt는 금지되며, code-review가 blocker 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 담당한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- Task ids:
  - `requested-by`: Jenkins cause의 userName/userId를 우선하고 없으면 trigger source 또는 commit author를 fallback으로 사용해 빌드한 사람을 1차 정보로 표시한다.
  - `state-badges`: success/failure/running/aborted 상태를 구분해 표시한다.
  - `summary-row`: build number, branch, flavor, result, 시간, artifact 이름은 보조 정보로 요약 표시한다.
  - `search-filter`: 빌드한 사람, build number, branch, flavor, artifact 이름 기준 검색/필터를 제공한다.
  - `empty-error`: 권한 없음, 네트워크 실패, artifact 없음 상태를 구분해 사용자에게 보여준다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- `agent-ops/rules/project/domain/artifact-flow/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/artifact-flow-smoke.md`
- `agent-task/archive/2026/06/m-artifact-browser/01_job_data/complete.log`
- `agent-task/archive/2026/06/m-artifact-browser/02+01_job_select_ui/complete.log`
- `pubspec.yaml`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/services/jenkins_client.dart`
- `test/builds_page_test.dart`
- `test/jenkins_client_test.dart`

### 테스트 환경 규칙

- 선택 환경: `local`.
- `agent-test/local/rules.md`: 존재하고 읽음. 기본 완료 evidence는 standard remote Mac runner `toki@toki-labs.com`, checkout `$HOME/docker/services/code-server/data/volume/workspace/appsok`.
- 적용 profile: `agent-test/local/artifact-flow-smoke.md` 읽음.
- 적용 명령: remote runner에서 `flutter analyze`, `flutter test`. Jenkins API parsing 변경 시 mock/fixture 테스트 추가.
- `<확인 필요>` 값: 없음.
- fallback: 없음. profile 명령이 충분하다.

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

- build 목록 로딩 UI: 현재 `test/builds_page_test.dart`는 job 선택 뒤 `build-list-placeholder`만 확인한다. 새 widget test 필요.
- requestedBy 1차 표시: `JenkinsBuild.requestedBy` parser test는 있으나 화면 표시 test 없음.
- state badge: 기존 테스트 없음.
- summary row metadata: 기존 테스트 없음.
- build 검색/필터: job 검색 테스트만 있고 build 검색 테스트 없음.
- forbidden/failure/artifact 없음 상태: job 상태 테스트만 있고 build 상태 테스트 없음.

### 심볼 참조

- renamed/removed symbol: none.
- 추가 예정 symbol은 `JenkinsBuildLoader`, build load state enum, build row/status widgets 정도이며 기존 call site 제거는 없다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `agent-task/m-artifact-browser/`.
- 완료된 sibling: `01_job_data`는 `agent-task/archive/2026/06/m-artifact-browser/01_job_data/complete.log`로 만족.
- 완료된 sibling: `02+01_job_select_ui`는 `agent-task/archive/2026/06/m-artifact-browser/02+01_job_select_ui/complete.log`로 만족.
- 새 subtask: `03+02_build_list_ui`; predecessor index `02`는 위 archive complete.log로 만족.
- UI state, row rendering, filtering, empty/error는 같은 `BuildsPage` state machine과 widget tests를 공유한다. 별도 subtask로 쪼개면 loader/result model과 테스트 fixture가 중복되어 리뷰 비용이 커지므로 하나의 dependent subtask로 둔다.

### 범위 결정 근거

- 포함: `lib/src/features/builds/builds_page.dart`, `test/builds_page_test.dart`.
- 참조만: `lib/src/models/jenkins_build.dart`, `lib/src/services/jenkins_client.dart`.
- 제외: `install-ready` Epic, artifact 다운로드/취소/임시파일, USB 설치 handoff, real Jenkins credential wiring, app shell navigation 구조.
- 제외 이유: 이번 Roadmap Targets는 build 목록 표시와 상태/검색뿐이며, 설치 CTA와 다운로드는 다음 Epic의 별도 상태와 device-console 경계가 필요하다.

### 빌드 등급

- `local-G06`: 단일 feature 화면과 widget tests 중심의 bounded UI 작업이다. 외부 Jenkins 실서비스 없이 mock loader로 검증 가능하지만, 상태 조합과 compact viewport overflow 위험이 있어 중간 등급으로 둔다.

## 구현 체크리스트

- [ ] `BuildsPage`에 선택된 job 기준 build loader와 build load state를 추가하고 job 선택/뒤로가기 시 stale build 결과가 남지 않게 한다.
- [ ] build row에서 `requestedBy`를 1차 정보로 표시하고 success/failure/running/aborted badge와 build number, branch, flavor, 시간, APK artifact 이름 요약을 보조 정보로 표시한다.
- [ ] 빌드한 사람, build number, branch, flavor, artifact 이름 기준 build 검색/필터를 추가하고 검색 결과 없음 상태를 표시한다.
- [ ] build 조회의 401/403 권한 없음, 일반 네트워크/서버 실패, APK artifact 없음 상태를 구분해 표시한다.
- [ ] `test/builds_page_test.dart`에 loaded row, badge/summary, build search, forbidden/failure/artifact-empty, compact viewport 회귀 테스트를 추가한다.
- [ ] remote runner에서 `flutter analyze`, `flutter test`를 실행해 통과 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [UI-1] Build Loader State

- 문제: [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:10)는 `jobLoader`만 받고, [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:183)는 선택된 job 화면에서 placeholder만 보여준다.
- 해결 방법:

Before:

```dart
10 class BuildsPage extends StatefulWidget {
11   const BuildsPage({super.key, this.jobLoader});
12
13   final JenkinsJobLoader? jobLoader;
```

```dart
183         const Expanded(
184           child: Center(
185             child: Text(
186               'build 목록 준비 중',
187               key: ValueKey('build-list-placeholder'),
188             ),
189           ),
```

After:

```dart
typedef JenkinsBuildLoader = Future<List<JenkinsBuild>> Function(
  JenkinsJob job,
);

class BuildsPage extends StatefulWidget {
  const BuildsPage({super.key, this.jobLoader, this.buildLoader});

  final JenkinsJobLoader? jobLoader;
  final JenkinsBuildLoader? buildLoader;
}
```

`_BuildLoadState { idle, loading, loaded, empty, forbidden, failure }`와 `_loadBuilds(job)`를 추가한다. `_JobSelected`는 builds/state/query를 받아 loading, list, status를 렌더링한다.

- 수정 파일 및 체크리스트:
  - [ ] `lib/src/features/builds/builds_page.dart`: `JenkinsBuildLoader` typedef와 build state fields 추가.
  - [ ] `lib/src/features/builds/builds_page.dart`: job 선택 시 `_loadBuilds(job)` 실행.
  - [ ] `lib/src/features/builds/builds_page.dart`: back 동작에서 selected job/build state/query 정리.
- 테스트 작성: `test/builds_page_test.dart`에 build loader 호출과 placeholder 제거/loaded state 테스트를 작성한다.
- 중간 검증: `flutter test test/builds_page_test.dart`가 통과해야 한다.

### [UI-2] Build Rows, Badges, And Filtering

- 문제: [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:154)는 selected job detail만 표시하고 build row, badge, summary, build 검색 UI가 없다.
- 해결 방법:
  - `_BuildRow`를 추가해 `build.requestedBy ?? '알 수 없는 사용자'`를 주 텍스트로 표시한다.
  - 상태 badge는 `result == null`이면 running, `SUCCESS`, `FAILURE`, `ABORTED`는 각각 구분하고 기타 result는 원문을 축약 표시한다.
  - summary에는 `#<number>`, branch, flavor, formatted startedAt, `build.apkArtifacts.map((a) => a.fileName)`를 한 줄 또는 wrap 가능한 보조 텍스트로 표시한다.
  - selected job 화면 상단에 `ValueKey('build-search-field')` 검색 필드를 추가한다.
  - 검색 대상은 requestedBy, number string, branch, flavor, APK artifact fileName/relativePath다.
- 수정 파일 및 체크리스트:
  - [ ] `lib/src/features/builds/builds_page.dart`: `_BuildRow`, badge helper, summary helper 추가.
  - [ ] `lib/src/features/builds/builds_page.dart`: build 검색 field와 `_setBuildQuery` 추가.
  - [ ] `lib/src/features/builds/builds_page.dart`: long text에 `maxLines`, `overflow`, stable padding을 적용해 compact viewport overflow를 피한다.
- 테스트 작성: `test/builds_page_test.dart`에 requestedBy primary text, badge labels, summary metadata, artifact 이름, search filter 테스트를 작성한다.
- 중간 검증: `flutter test test/builds_page_test.dart`가 통과해야 한다.

### [UI-3] Build Empty And Error States

- 문제: [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:97)는 job empty/forbidden/failure만 구분하고, build 조회의 권한 없음/네트워크 실패/artifact 없음 상태는 없다.
- 해결 방법:
  - build loader가 `JenkinsClientException` 401/403을 던지면 `권한 없음` build 상태를 표시한다.
  - 그 외 exception은 `build 조회 실패` 상태를 표시한다.
  - loader가 빈 list를 반환하면 `.apk artifact 없음` 상태를 표시한다.
  - 검색 결과만 비었을 때는 별도 `검색 결과 없음`을 표시해 artifact 없음과 구분한다.
- 수정 파일 및 체크리스트:
  - [ ] `lib/src/features/builds/builds_page.dart`: build-specific `_BuildStatus` 또는 기존 `_JobStatus` 재사용 helper 추가.
  - [ ] `lib/src/features/builds/builds_page.dart`: empty/error 상태 copy가 job 상태와 혼동되지 않게 문구 분리.
  - [ ] `test/builds_page_test.dart`: forbidden/failure/empty/search-empty/widget compact viewport 테스트 추가.
- 테스트 작성: `test/builds_page_test.dart`에 403, 500, empty list, unmatched search, compact viewport 케이스를 작성한다.
- 중간 검증: `flutter test test/builds_page_test.dart`와 `flutter test`가 통과해야 한다.

## 의존 관계 및 구현 순서

- `03+02_build_list_ui`는 sibling predecessor `02+01_job_select_ui` 완료가 필요하다.
- predecessor `02`는 `agent-task/archive/2026/06/m-artifact-browser/02+01_job_select_ui/complete.log`로 satisfied.
- 구현 순서: UI-1 -> UI-2 -> UI-3. 이 순서는 같은 파일의 state 기반 의존성 때문이며 runtime dependency는 directory name의 `+02`가 source of truth다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/builds/builds_page.dart` | UI-1, UI-2, UI-3 |
| `test/builds_page_test.dart` | UI-1, UI-2, UI-3 |

## 최종 검증

```bash
git diff --check
```

예상: 출력 없음, exit 0.

```bash
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'\'''
```

예상: `No issues found!`.

```bash
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'\'''
```

예상: `All tests passed!`. Flutter test cache output은 Dart/Flutter test runner의 정상 PASS 출력이면 허용한다.

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