<!-- task=m-logcat-console/04+03_export_tools plan=0 tag=EXPORT -->

# Plan - EXPORT

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

`CODE_REVIEW-local-G04.md`의 구현 에이전트 소유 섹션을 채우는 것까지가 구현이다. 코드를 수정한 뒤 검증 명령을 실행하고 실제 출력과 구현 메모를 review stub에 남긴다. 최종 판정, log rename, `complete.log`, task directory archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 또는 범위 충돌 없이는 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다.

## 배경

`filter` Epic에서 query, level, clear/pause는 직접 구현되어 남은 Task는 `export`다. MVP 범위에서는 macOS 파일 저장 경로와 권한까지 결정하지 않고, 현재 표시 중인 실제 logcat 라인을 clipboard로 복사하는 방향이 가장 작고 검증 가능하다. 이 계획은 `export` Task를 clipboard export로 완료시키는 구현 단위다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active `CODE_REVIEW-local-G04.md`의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 정당성을 확인한 뒤에만 `USER_REVIEW.md`를 작성한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
- Task ids:
  - `export`: 현재 로그를 파일 저장 또는 clipboard 복사할 수 있는 방향을 정리한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
- `agent-ops/rules/project/domain/device-console/rules.md`
- `agent-ops/rules/project/domain/app-shell/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/device-console-smoke.md`
- `lib/src/features/console/console_page.dart`
- `lib/src/services/adb_service.dart`
- `test/console_page_test.dart`
- `test/widget_test.dart`
- `pubspec.yaml`
- `.gitignore`
- `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present/read. 기본 완료 evidence는 standard remote Mac runner 기준이다.
- matched profile: `agent-test/local/device-console-smoke.md` present/read.
- 적용 명령: remote runner checkout에서 `flutter analyze`, `flutter test`.
- 보조 고정 검증: `dart format --output=none --set-exit-if-changed lib/src/features/console/console_page.dart test/console_page_test.dart`, `git diff --check -- lib/src/features/console/console_page.dart test/console_page_test.dart`.
- `<확인 필요>` 값 없음. 신규 패키지는 추가하지 않는다. Clipboard는 Flutter SDK `package:flutter/services.dart`를 사용한다.

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

- 현재 표시 로그를 clipboard로 복사하는 동작: 기존 테스트 없음. `test/console_page_test.dart`에 regression widget test를 추가한다.
- 로그가 없거나 sample placeholder만 있는 상태에서 export가 비활성/무동작이어야 하는 동작: 기존 테스트 없음. 같은 테스트 파일에 no live log disabled case를 추가한다.
- 파일 저장: 이번 계획 범위 제외이므로 테스트하지 않는다.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- 공유 task group: `agent-task/m-logcat-console`.
- sibling subtask: `04+03_export_tools`; predecessor `03`은 `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`로 충족된다.
- 이 subtask 안에서는 clipboard action, UI button, widget tests가 하나의 `ConsolePage` ownership boundary 안에 있어 추가 분할하지 않는다.

### 범위 결정 근거

- 포함: `ConsolePage`에 실제 logcat 표시 라인을 clipboard로 복사하는 export action과 버튼을 추가한다.
- 포함: export widget tests를 `test/console_page_test.dart`에 추가한다.
- 제외: macOS file save dialog, default export directory, persisted export history, log redaction policy. 이들은 platform/product 선택이 커져 별도 작업이 맞다.
- 제외: `AdbService` process contract. export는 이미 화면에 보관된 lines만 사용한다.

### 빌드 등급

- build: `local-G04`, review: `local-G04`. 단일 UI state/action과 widget test 중심이며, 원격 `flutter analyze`/`flutter test`로 검증 가능하다.

## 구현 체크리스트

- [ ] `ConsolePage`에 실제 live log 표시 라인 clipboard export action과 복사 버튼을 추가한다.
- [ ] sample placeholder만 있는 상태나 표시 라인이 없는 상태에서는 export가 비활성/무동작이 되게 한다.
- [ ] `test/console_page_test.dart`에 clipboard export와 empty/no-live 상태 회귀 테스트를 추가한다.
- [ ] 원격 Mac runner에서 포맷, 공백, analyze, test 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

`04+03_export_tools`는 sibling index `03` 완료 뒤에만 구현한다. `03`은 `agent-task/archive/2026/06/m-logcat-console/03+02_auto_scroll/complete.log`에서 PASS와 `Roadmap Completion: auto-scroll`이 확인되었다.

### [EXPORT-1] Clipboard Export Action

#### 문제

현재 console toolbar는 pause와 clear만 제공한다. `export` Task가 요구하는 clipboard 또는 file save 방향이 UI에 없다.

```dart
// lib/src/features/console/console_page.dart:260
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton.filledTonal(
                    tooltip: _isPaused ? '재개' : '일시정지',
                    onPressed: () {
                      setState(() {
                        _isPaused = !_isPaused;
                        if (!_isPaused) {
                          WidgetsBinding.instance.addPostFrameCallback(
                            (_) => _scrollToBottom(),
                          );
                        }
                      });
                    },
                    icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause),
                  ),
                  const SizedBox(width: 8),
                  IconButton.outlined(
                    tooltip: '지우기',
                    onPressed: () {
                      setState(_logLines.clear);
                    },
                    icon: const Icon(Icons.delete_outline),
                  ),
                ],
              ),
```

#### 해결 방법

`package:flutter/services.dart`를 import하고 `_copyVisibleLogsToClipboard`를 추가한다. 실제 device+starter가 있는 live mode에서 `_displayLines`가 비어 있지 않을 때만 복사 버튼을 활성화한다. 복사 텍스트는 현재 표시 라인을 `\n`으로 join한다.

```dart
import 'package:flutter/services.dart';

Future<void> _copyVisibleLogsToClipboard() async {
  if (widget.device == null || widget.logcatStarter == null) return;
  final lines = _displayLines;
  if (lines.isEmpty) return;

  await Clipboard.setData(ClipboardData(text: lines.join('\n')));
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('로그 ${lines.length}줄 복사됨')),
  );
}
```

버튼은 clear 버튼 옆에 `Icons.content_copy`로 둔다. tooltip은 `로그 복사`를 사용한다.

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

- [ ] `lib/src/features/console/console_page.dart`: `Clipboard` import 추가.
- [ ] `lib/src/features/console/console_page.dart`: `_copyVisibleLogsToClipboard` 추가.
- [ ] `lib/src/features/console/console_page.dart`: live display lines가 없으면 export button disabled.
- [ ] `lib/src/features/console/console_page.dart`: 복사 성공 후 `SnackBar`로 짧은 결과 표시.

#### 테스트 작성

`EXPORT-2`에서 widget tests를 추가한다.

#### 중간 검증

```bash
dart format lib/src/features/console/console_page.dart test/console_page_test.dart
```

Expected: files formatted without error.

### [EXPORT-2] Export Widget Tests

#### 문제

`console_page_test.dart`는 filter와 clear/pause를 검증하지만 clipboard export는 아직 검증하지 않는다.

```dart
// test/console_page_test.dart:369
  testWidgets('clear button removes captured live log lines', (tester) async {
    final fakeProcess = _FakeProcess();
    final session = AdbLogcatSession(fakeProcess);
    const device = AdbDevice(
      serial: 'R5CT90A1B2C',
      state: 'device',
      model: 'SM_S918N',
      product: 'dm3q',
    );
```

#### 해결 방법

`package:flutter/services.dart`를 import한다. `SystemChannels.platform` mock handler로 `Clipboard.setData` payload를 캡처하고, live log emit 후 `로그 복사` 버튼을 눌러 현재 표시 라인만 복사되는지 확인한다. query filter가 적용된 상태에서 복사해 filtered export contract도 함께 고정한다.

```dart
final copiedTexts = <String>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
    .setMockMethodCallHandler(SystemChannels.platform, (call) async {
  if (call.method == 'Clipboard.setData') {
    copiedTexts.add((call.arguments as Map<Object?, Object?>)['text']! as String);
  }
  return null;
});
addTearDown(() {
  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
      .setMockMethodCallHandler(SystemChannels.platform, null);
});
```

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

- [ ] `test/console_page_test.dart`: services import 추가.
- [ ] `test/console_page_test.dart`: filtered visible live logs copy test 추가.
- [ ] `test/console_page_test.dart`: no live logs/sample placeholder export disabled test 추가.

#### 테스트 작성

- path: `test/console_page_test.dart`
- test name: `copies currently visible live log lines to clipboard`
- assertion goal: query filter 적용 뒤 visible line만 clipboard payload에 들어간다.
- test name: `keeps export disabled without live log lines`
- assertion goal: no device/sample placeholder 상태나 live line empty 상태에서 `로그 복사` button `onPressed`가 null이다.

#### 중간 검증

```bash
flutter test test/console_page_test.dart
```

Expected: `All tests passed!`

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/features/console/console_page.dart` | EXPORT-1 |
| `test/console_page_test.dart` | EXPORT-2 |

## 최종 검증

```bash
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && dart format lib/src/features/console/console_page.dart test/console_page_test.dart && dart format --output=none --set-exit-if-changed lib/src/features/console/console_page.dart test/console_page_test.dart'\'''
```

Expected: format applies or reports 0 changed on second command.

```bash
ssh -o BatchMode=yes -o ConnectTimeout=10 toki@toki-labs.com 'zsh -lc '\''cd "$HOME/docker/services/code-server/data/volume/workspace/appsok" && git diff --check -- lib/src/features/console/console_page.dart test/console_page_test.dart'\'''
```

Expected: no output, 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 && flutter test'\'''
```

Expected: `No issues found!` and `All tests passed!`.

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