<!-- task=m-artifact-browser/02+01_job_select_ui plan=0 tag=JOBSELECT-UI -->

# Plan - JOBSELECT-UI

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고 실제 변경 내용, 설계 결정, 검증 stdout/stderr를 review stub에 채운 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 구현 중 사용자만 결정할 수 있는 선택, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 기록하고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.

## 배경

`01_job_data`가 Jenkins job 조회 기반을 만든 뒤에는 [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:10)의 sample build-first 화면을 job-first 화면으로 바꿔야 한다. `job-select` Epic의 세 Task는 job 목록 표시, 선택 후 build 목록 진입, no jobs/permission 상태 표시가 한 UI 흐름으로 연결된다. 이 subtask PASS는 `job-select` Epic의 roadmap task 3개를 완료 대상으로 한다.

## 사용자 리뷰 요청 흐름

구현-time blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자에게 묻지 말고, code-review가 요청의 정당성을 검증해 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/artifact-browser.md`
- Task ids:
  - `job-list`: Jenkins job 목록을 조회하고 검색 가능한 리스트로 표시한다.
  - `job-select`: 사용자가 job을 선택하면 해당 job의 build 목록으로 진입한다.
  - `job-empty`: 접근 가능한 job이 없거나 권한이 없을 때 상태를 구분해 표시한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.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`
- `lib/src/features/builds/builds_page.dart`
- `lib/src/models/jenkins_build.dart`
- `lib/src/services/jenkins_client.dart`
- `test/widget_test.dart`
- `pubspec.yaml`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`

### 테스트 환경 규칙

- test_env: `local`
- `agent-test/local/rules.md` 존재 및 전체 읽음.
- 매칭 profile: `agent-test/local/artifact-flow-smoke.md`
- 적용 명령:
  - remote lint: `ssh toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter analyze'\'''`
  - remote unit: `ssh toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test'\'''`
- 실제 Jenkins endpoint가 제공되면 수동 검증은 보조 검증이다. 이 subtask의 필수 완료 근거는 fake loader/widget test다.

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

- job list UI render/search: 현재 테스트 없음. 새 widget test 필요.
- job selection state transition: 현재 테스트 없음. 새 widget test 필요.
- no jobs/permission empty state: 현재 테스트 없음. 새 widget test 필요.
- compact viewport overflow: 기존 `test/widget_test.dart`가 shell page overflow를 넓게 확인하지만 job selector 상태별 보장은 없다. job selector 전용 compact test 필요.

### 심볼 참조

- renamed/removed symbol: none.
- `_sampleBuilds`: `lib/src/features/builds/builds_page.dart` private helper only. 제거하면 같은 파일만 영향.
- `BuildsPage`: `lib/src/features/app_shell.dart`, `test/widget_test.dart`
- `JenkinsBuild`: `lib/src/features/builds/builds_page.dart`, `lib/src/models/jenkins_build.dart`, `lib/src/services/jenkins_client.dart`

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- 공유 task group: `m-artifact-browser`
- sibling subtasks:
  - `01_job_data`: job model/client. 의존 없음.
  - `02+01_job_select_ui`: job selector UI. predecessor `01`.
- predecessor `01`: active `agent-task/m-artifact-browser/01_job_data/complete.log` 없음, archive `agent-task/archive/*/*/m-artifact-browser/01_*/complete.log` 확인 필요. 현재 계획 작성 시점에는 미충족이다. 구현 시작 전 `01_job_data` PASS complete.log가 있어야 한다.

### 범위 결정 근거

- 포함: BuildsPage의 job-first list/search/select/empty UI, fake loader 주입, widget tests.
- 제외: 실제 Jenkins credential 저장/복원, WebView login, build-list Epic 구현, artifact download/install handoff.
- 제외 이유: 현재 Epic anchor는 `job-select`이며 build 목록과 설치 CTA는 별도 Epic이다.

### 빌드 등급

- lane/grade: `local-G06`
- 근거: UI 상태 전환과 widget tests가 필요하고 `01_job_data` dependency가 있지만, 변경 범위는 artifact-flow 내부로 제한되고 deterministic fake loader로 검증 가능하다.

## 구현 체크리스트

- [ ] 구현 시작 전 `01_job_data`의 active 또는 archived `complete.log`를 확인한다.
- [ ] `BuildsPage`를 job-first 상태 UI로 바꾸고 fake/injected job loader를 받을 수 있게 한다.
- [ ] job 검색 TextField와 job list row/card를 추가해 name/fullName/url 기준으로 필터링한다.
- [ ] job 선택 시 선택된 job summary와 build 목록 진입 placeholder를 표시하되, 실제 build list 구현은 후속 Epic 범위로 남긴다.
- [ ] empty/no permission/network failure 상태를 구분해 화면에 표시한다.
- [ ] widget test로 job list render/search/select/empty/forbidden 상태와 compact viewport overflow 없음까지 검증한다.
- [ ] remote Mac runner에서 `flutter analyze`와 `flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask directory 이름은 `02+01_job_select_ui`이며 runtime dependency source of truth는 predecessor index `01`이다.
- 구현 시작 전 다음 중 하나가 있어야 한다:
  - active: `agent-task/m-artifact-browser/01_*/complete.log`
  - archive: `agent-task/archive/*/*/m-artifact-browser/01_*/complete.log`
- predecessor가 없으면 구현하지 말고 review stub의 `사용자 리뷰 요청`이 아니라 task dependency 미충족으로 보고한다.

## [JOBSELECT-UI-1] BuildsPage job-first 상태 구조

### 문제

[builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:10)는 `_sampleBuilds()`를 즉시 만들고 build row를 표시한다. `job-select` Epic은 build 목록보다 먼저 job 목록을 보여줘야 한다.

### 해결 방법

`BuildsPage`를 `StatefulWidget`으로 바꾸고 job loader를 주입 가능하게 만든다. credentials 연결은 선행 Milestone 책임이므로 이 plan에서는 fake/injected loader와 "Jenkins 로그인 필요" 기본 상태를 둔다.

Before:

```dart
// lib/src/features/builds/builds_page.dart:5
class BuildsPage extends StatelessWidget {
  const BuildsPage({super.key});

  @override
  Widget build(BuildContext context) {
    final builds = _sampleBuilds();
```

After:

```dart
typedef JenkinsJobLoader = Future<List<JenkinsJob>> Function();

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

  final JenkinsJobLoader? jobLoader;

  @override
  State<BuildsPage> createState() => _BuildsPageState();
}

class _BuildsPageState extends State<BuildsPage> {
  // load/search/select state here
}
```

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

- [ ] `lib/src/features/builds/builds_page.dart`: stateful job-first UI 구조로 전환.
- [ ] `test/widget_test.dart` 또는 새 `test/builds_page_test.dart`: 기본 shell marker를 새 job-first text로 갱신.

### 테스트 작성

- 작성: `test/builds_page_test.dart`
- 테스트명 후보: `renders login required state without job loader`
- assertion: 기본 `BuildsPage`가 실제 Jenkins 호출 없이 로그인/설정 필요 상태를 표시한다.

### 중간 검증

```bash
ssh toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test test/builds_page_test.dart'\'''
```

기대 결과: BuildsPage widget test 통과.

## [JOBSELECT-UI-2] 검색 가능한 job list와 선택 전환

### 문제

[builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:17)는 job URL 입력과 build 검색 입력만 있고 Jenkins job list row/card가 없다. `[job-list]`와 `[job-select]`를 충족하려면 job 검색과 선택 상태가 필요하다.

### 해결 방법

`JenkinsJob` list를 받아 name/fullName/url로 필터링하고, row tap 또는 button으로 selected job을 설정한다. 선택 후에는 build-list Epic 구현 전까지 선택된 job summary와 "build 목록 준비" placeholder를 표시한다.

Before:

```dart
// lib/src/features/builds/builds_page.dart:17
Wrap(
  spacing: 12,
  runSpacing: 10,
  crossAxisAlignment: WrapCrossAlignment.center,
  children: [
```

After:

```dart
TextField(
  key: const ValueKey('job-search-field'),
  decoration: const InputDecoration(
    labelText: 'Job 검색',
    prefixIcon: Icon(Icons.search),
  ),
  onChanged: _setQuery,
)
```

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

- [ ] `lib/src/features/builds/builds_page.dart`: job search field 추가.
- [ ] `lib/src/features/builds/builds_page.dart`: job row/card와 selected job summary 추가.
- [ ] `test/builds_page_test.dart`: render/search/select widget tests 추가.

### 테스트 작성

- 작성: `test/builds_page_test.dart`
- 테스트명 후보:
  - `renders searchable Jenkins jobs`
  - `filters jobs by name full name and url`
  - `selects a job and shows build list placeholder`
- assertion: fake loader가 반환한 job만 표시되고 query에 따라 필터링되며, tap 후 selected summary가 보인다.

### 중간 검증

```bash
ssh toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test test/builds_page_test.dart'\'''
```

기대 결과: job list/search/select widget tests 통과.

## [JOBSELECT-UI-3] empty/permission/network 상태 표시

### 문제

현재 [jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:65)의 failure는 exception으로 올라오지만, [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:58)는 empty/error state UI가 없다. `[job-empty]`는 job 없음과 권한 없음을 구분해야 한다.

### 해결 방법

loader 결과가 빈 리스트이면 "접근 가능한 job 없음" 상태를 보여준다. `JenkinsClientException.statusCode == 401 || 403`이면 권한/인증 상태, 그 외 exception은 네트워크/조회 실패 상태로 분리한다.

Before:

```dart
// lib/src/features/builds/builds_page.dart:58
Expanded(
  child: ListView.separated(
    itemCount: builds.length,
```

After:

```dart
Widget _buildJobState(BuildContext context) {
  return switch (_state) {
    JobLoadState.empty => const _JobStatus(...),
    JobLoadState.forbidden => const _JobStatus(...),
    JobLoadState.failure => const _JobStatus(...),
    _ => _JobList(...),
  };
}
```

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

- [ ] `lib/src/features/builds/builds_page.dart`: empty/forbidden/failure state 모델 또는 enum 추가.
- [ ] `lib/src/features/builds/builds_page.dart`: status UI는 icon + concise text로 표현하고 overflow를 피한다.
- [ ] `test/builds_page_test.dart`: empty/forbidden/failure states와 compact viewport test 추가.

### 테스트 작성

- 작성: `test/builds_page_test.dart`
- 테스트명 후보:
  - `shows empty state when no jobs are returned`
  - `shows permission state for forbidden Jenkins response`
  - `renders job selector without overflow at compact viewport`
- assertion: 상태별 marker text가 나오고 `tester.takeException()`이 null이다.

### 중간 검증

```bash
ssh toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && flutter test test/builds_page_test.dart'\'''
```

기대 결과: state widget tests 통과.

## 수정 파일 요약

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

## 최종 검증

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

기대 결과: analyzer issue 없음, 모든 test 통과.

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