<!-- task=m-artifact-browser/01_job_data plan=0 tag=JOBSELECT-DATA -->

# Plan - JOBSELECT-DATA

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

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

## 배경

`Artifact 탐색과 다운로드` Milestone의 `job-select` Epic은 로그인 이후 접근 가능한 Jenkins job을 먼저 보여주는 흐름이다. 현재 [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:10)는 `_sampleBuilds()`만 렌더링하고, [jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:12)는 build 조회만 제공한다. UI를 연결하기 전에 Jenkins job 모델과 Remote API 조회 기반이 필요하다.

## 사용자 리뷰 요청 흐름

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

## 분석 결과

### 읽은 파일

- `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 API parsing 변경이므로 fixture 또는 mock 기반 테스트 추가가 필요하다.
- 실제 Jenkins endpoint/credential은 이 subtask의 필수 검증에 포함하지 않는다. mock HTTP로 API shape를 고정한다.

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

- job 목록 JSON parsing: 현재 테스트 없음. 새 unit test 필요.
- Jenkins job 조회 URI와 credential header: 현재 테스트 없음. 새 unit test 필요.
- 권한 없음/empty 응답 분류: 현재 테스트 없음. 이 subtask에서는 client exception과 empty list 반환을 테스트하고, 사용자-facing empty UI는 `02+01_job_select_ui`에서 테스트한다.

### 심볼 참조

- renamed/removed symbol: none.
- `JenkinsBuild`: `lib/src/features/builds/builds_page.dart`, `lib/src/models/jenkins_build.dart`, `lib/src/services/jenkins_client.dart`
- `JenkinsClient.fetchRecentBuilds`: `lib/src/services/jenkins_client.dart`
- `BuildsPage`: `lib/src/features/app_shell.dart`, `test/widget_test.dart`

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- 공유 task group: `m-artifact-browser`
- `01_job_data`: job model, client API, mock HTTP tests. 의존 없음.
- `02+01_job_select_ui`: `01_job_data`의 `complete.log` 이후 UI/search/select/empty states 연결. predecessor `01`은 현재 active/archive `complete.log`가 없어 미충족이며, 구현은 `01_job_data` 완료 후 시작해야 한다.
- 분할 이유: client/model foundation과 UI rollout은 API-vs-call-site 경계가 있고 테스트 전략도 unit vs widget으로 나뉜다.

### 범위 결정 근거

- 포함: Jenkins job 모델, job list API 요청/파싱, mock HTTP unit tests.
- 제외: 실제 Jenkins endpoint 수동 검증, WebView login/session restore, build list UI, artifact download, USB install handoff.
- 제외 이유: `job-select` Epic의 데이터 foundation만 다루며 credential 수명주기와 후속 build/install Epic 범위를 침범하지 않는다.

### 빌드 등급

- lane/grade: `local-G05`
- 근거: 변경 파일과 call-site가 작고 mock HTTP로 deterministic 검증 가능하지만, 새 API와 parser 테스트가 필요하다.

## 구현 체크리스트

- [ ] `JenkinsJob` 모델을 추가하고 name/fullName/url/color/folder 여부를 안전하게 parse한다.
- [ ] `JenkinsClient.fetchJobs`를 추가해 Jenkins base URL의 `/api/json`에서 접근 가능한 jobs를 조회한다.
- [ ] HTTP status 실패는 기존 `JenkinsClientException` 흐름을 유지하고, empty/missing jobs는 빈 리스트로 반환한다.
- [ ] mock HTTP 기반 unit test를 추가해 job parsing, query tree, auth header, empty response, 403 failure를 검증한다.
- [ ] remote Mac runner에서 `flutter analyze`와 `flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## [JOBSELECT-DATA-1] Jenkins job model 추가

### 문제

[jenkins_build.dart](/config/workspace/appsok/lib/src/models/jenkins_build.dart:1)는 build/artifact 모델만 있고 job 선택 화면이 사용할 job 모델이 없다. [builds_page.dart](/config/workspace/appsok/lib/src/features/builds/builds_page.dart:26)의 `Jenkins job URL` 입력은 raw URL 중심이라 job 목록 선택 모델과 맞지 않는다.

### 해결 방법

기존 model 파일을 우선 사용해 `JenkinsJob`을 추가한다. 새 파일 생성은 피하고, job selection에 필요한 최소 필드만 둔다.

Before:

```dart
// lib/src/models/jenkins_build.dart:1
class JenkinsBuild {
```

After:

```dart
class JenkinsJob {
  const JenkinsJob({
    required this.name,
    required this.url,
    this.fullName,
    this.color,
    this.isFolder = false,
  });

  final String name;
  final Uri url;
  final String? fullName;
  final String? color;
  final bool isFolder;

  factory JenkinsJob.fromJson(Map<String, Object?> json) {
    final rawClass = json['_class'];
    return JenkinsJob(
      name: json['name'] as String,
      fullName: json['fullName'] as String?,
      url: Uri.parse(json['url'] as String),
      color: json['color'] as String?,
      isFolder: rawClass is String && rawClass.contains('Folder'),
    );
  }
}

class JenkinsBuild {
```

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

- [ ] `lib/src/models/jenkins_build.dart`: `JenkinsJob` 모델 추가.
- [ ] `test/jenkins_client_test.dart`: `JenkinsJob.fromJson` normal/folder/minimal parsing 테스트 추가.

### 테스트 작성

- 작성: `test/jenkins_client_test.dart`
- 테스트명 후보:
  - `parses jenkins jobs from api response`
  - `marks folder jobs from _class`
- assertion: name/fullName/url/color/isFolder가 JSON에서 안전하게 매핑된다.

### 중간 검증

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

기대 결과: 새 Jenkins client/model unit test가 통과한다.

## [JOBSELECT-DATA-2] JenkinsClient job 목록 조회 추가

### 문제

[jenkins_client.dart](/config/workspace/appsok/lib/src/services/jenkins_client.dart:12)는 `fetchRecentBuilds`만 제공한다. `job-select` Epic의 `[job-list]`는 base URL에서 접근 가능한 job 목록을 조회해야 한다.

### 해결 방법

`fetchJobs`를 추가한다. `JenkinsCredentials`와 `_headers`를 재사용하고, base URL path 뒤에 `api/json`을 붙인다. tree는 MVP job selector에 필요한 필드로 제한한다.

Before:

```dart
// lib/src/services/jenkins_client.dart:12
Future<List<JenkinsBuild>> fetchRecentBuilds({
```

After:

```dart
Future<List<JenkinsJob>> fetchJobs({
  required Uri baseUrl,
  required JenkinsCredentials credentials,
}) async {
  final uri = baseUrl.replace(
    path: _appendPath(baseUrl.path, 'api/json'),
    queryParameters: const {
      'tree': 'jobs[name,fullName,url,color,_class]',
    },
  );
  final response = await _client.get(uri, headers: _headers(credentials));

  _throwIfFailed(response);

  final json = jsonDecode(response.body) as Map<String, Object?>;
  final jobs = json['jobs'];
  if (jobs is! List) {
    return const [];
  }

  return jobs
      .whereType<Map<String, Object?>>()
      .map(JenkinsJob.fromJson)
      .toList();
}

Future<List<JenkinsBuild>> fetchRecentBuilds({
```

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

- [ ] `lib/src/services/jenkins_client.dart`: `fetchJobs` 추가.
- [ ] `test/jenkins_client_test.dart`: mock HTTP로 URI path/query/header/body parsing 테스트 추가.

### 테스트 작성

- 작성: `test/jenkins_client_test.dart`
- 테스트명 후보:
  - `fetchJobs requests root api json with job tree`
  - `fetchJobs returns empty list when jobs is missing`
  - `fetchJobs throws JenkinsClientException on forbidden response`
- fixture: private endpoint나 secret 없이 `https://jenkins.example/`와 fake credential `user/token`.

### 중간 검증

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

기대 결과: 새 unit test가 모두 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/models/jenkins_build.dart` | JOBSELECT-DATA-1 |
| `lib/src/services/jenkins_client.dart` | JOBSELECT-DATA-2 |
| `test/jenkins_client_test.dart` | JOBSELECT-DATA-1, JOBSELECT-DATA-2 |

## 최종 검증

```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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
