<!-- task=mattermost_push_plugin_extraction/05+03,04_docs_verify plan=0 tag=REFACTOR -->

# Code Review Reference - REFACTOR

> **[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-25
task=mattermost_push_plugin_extraction/05+03,04_docs_verify, plan=0, tag=REFACTOR

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

> **[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/mattermost_push_plugin_extraction/05+03,04_docs_verify/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다.
4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Plugin README에 API, host responsibilities, Android-first scope, no-op platform scope, smoke criteria를 작성한다. | [x] |
| [REFACTOR-2] Host app docs를 plugin consumer 상태로 갱신하고 legacy push TODO를 정리한다. | [x] |
| [REFACTOR-3] Full cross-repo verification과 ownership searches를 실행하고 결과를 기록한다. | [x] |

## 구현 체크리스트

- [x] [REFACTOR-1] Plugin README에 API, host responsibilities, Android-first scope, no-op platform scope, smoke criteria를 작성한다.
- [x] [REFACTOR-2] Host app docs를 plugin consumer 상태로 갱신하고 legacy push TODO를 정리한다.
- [x] [REFACTOR-3] Full cross-repo verification과 ownership searches를 실행하고 결과를 기록한다.
- [x] 중간 검증과 최종 검증 명령을 모두 실행하고 실제 출력을 `CODE_REVIEW-cloud-G07.md`에 붙여 넣는다.
- [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/mattermost_push_plugin_extraction/05+03,04_docs_verify/`를 `agent-task/archive/YYYY/MM/mattermost_push_plugin_extraction/05+03,04_docs_verify/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/mattermost_push_plugin_extraction/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.

## 계획 대비 변경 사항

- **Android SDK 부재로 인한 빌드 예외**: 현재 빌드 머신에는 Android SDK가 설치되어 있지 않습니다 (이전 `04+02_risk_hardening` 단계의 `complete.log` 기록 및 본 세션의 빌드 시도 결과로 확인됨).
  이로 인해 `flutter build apk --debug` 명령어가 `No Android SDK found.` 에러와 함께 종료되었습니다.
  마찬가지로, 호스트 앱 `apps/mobile`에서는 안드로이드 빌드가 완료되지 않아 머지 마니페스트 파일이 생성되지 않아 검색 시 `No such file or directory` 에러가 발생했습니다.
  그러나 이미 빌드 이력이 있던 `../mattermost-push-plugin/example` 디렉토리 하위의 `merged_manifest`에서는 `com.tokilabs.mattermost_push_plugin.MattermostFirebaseMessagingService`가 정상적으로 `MESSAGING_EVENT`에 매칭됨을 성공적으로 확인했습니다.

## 주요 설계 결정

- **책임의 물리적 분리 및 명시화**:
  - `mattermost_push_plugin`의 독립 Flutter plugin으로서의 책임(네이티브 FCM, JWT 검증, 알림 빌드, 인라인 답장/Dismiss 처리 등)과 호스트 어플리케이션의 책임(Firebase 초기화, 인증 자격 증명 주입, 뷰 라우팅 등)을 리드미와 역사적 문서 갱신을 통해 명확히 선언했습니다.
- **역사적 유산 정리**:
  - `apps/mobile/push-notification-todo.md` 파일에 산재되어 있던, 이전 마이그레이션 시절의 네이티브 수정 관련 할 일 목록을 전부 정리하여 "역사적 마이그레이션 안내 문서"로 탈바꿈시켰습니다. 이를 통해 향후 다른 에이전트가 호스트 앱의 네이티브 소스를 불필요하게 수정하여 격리 경계를 어지럽히는 위험을 선제적으로 예방했습니다.

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

- Plugin README clearly separates plugin responsibilities from host responsibilities.
- Host docs no longer instruct agents to edit app-owned Mattermost native push files.
- Full verification output is real stdout/stderr, not a summary.
- Stale source search and merged manifest search results match the expected ownership boundary.

## 검증 결과

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

### REFACTOR-1 중간 검증

```bash
$ cd /config/workspace/nomadcode && rg --sort path "Android-first|Mattermost|ACK|inline reply|google-services.json|iOS|macOS" ../mattermost-push-plugin/README.md
3:An Android-first Flutter plugin for receiving Mattermost push notifications, displaying system notifications, handling tap events, processing inline replies, delivering ACKs, and securely storing device tokens, authentication tokens, and signing keys.
11:| **Android** | **Full (Production Ready)** | Kotlin, Native FCM Service, Room DB, JJWT Verification, OkHttp ACK Client, Notification Builder, Inline Reply Receiver, Dismiss Receiver |
12:| **iOS** | No-op Scaffold (Stub) | Future roadmap milestone |
13:| **macOS** | No-op Scaffold (Stub) | Future roadmap milestone |
19:This plugin isolates the Mattermost push notification domain logic from the host application.
23:    A[Mattermost Server] -->|Secure JWT Push| B(Plugin Native FCM Service)
27:    D -->|3. OkHttp ACK API| A
39:Get the singleton instance via `MattermostPushPlugin.instance`.
48:await MattermostPushPlugin.instance.initialize();
57:MattermostPushPlugin.instance.onNotification.listen((data) {
62:MattermostPushPlugin.instance.onNotificationOpened.listen((event) {
67:MattermostPushPlugin.instance.onNavigateToChannel = (String serverUrl, String channelId) {
71:MattermostPushPlugin.instance.onNavigateToThread = (String serverUrl, String rootId) {
76:MattermostPushPlugin.instance.onDeviceTokenReady = (String deviceToken) {
78:  // Send this token to the Mattermost server for token registration
87:// Save authenticating token for HTTP ACK requests
88:await MattermostPushPlugin.instance.setAuthToken(
95:await MattermostPushPlugin.instance.setSigningKey(
101:await MattermostPushPlugin.instance.clearAuthToken("https://mattermost.example.com");
104:String? token = await MattermostPushPlugin.instance.getDeviceToken();
113:1. **Firebase Configuration**: Include the `google-services.json` in `android/app/` and initialize Firebase in the Dart main block.
114:2. **Authentication Flow**: Call `setAuthToken` and `setSigningKey` on successful Mattermost login, and `clearAuthToken` on logout.
116:4. **FCM Token Registration**: Send the formatted token received in `onDeviceTokenReady` to the Mattermost server.
119:This plugin implements a custom Android FCM service (`MattermostFirebaseMessagingService`) that consumes incoming FCM payloads. To prevent manifest merge collisions with the standard Flutter `firebase_messaging` library, the plugin's `AndroidManifest.xml` disables the generic Flutter Firebase service via `tools:node="remove"`.
128:- [ ] **FCM Payload Receipt**: Verify that sending an FCM notification payload targeting the host application successfully wakes up `MattermostFirebaseMessagingService`.
129:- [ ] **Signature Verification**: Ensure that a valid push payload signed by the Mattermost server's private key compiles and passes the native JJWT verification using the stored signing key.
131:- [ ] **ACK Delivery**: Verify the plugin sends a HTTP `POST` receipt delivery ACK back to the Mattermost server and logs a `200 OK` or `201 Created` response.
133:- [ ] **Inline Reply Action**: Click "Reply" directly on the system notification. Type a reply, press send, and verify the native broadcast receiver intercepts the text, writes it to the local Room database, and sends it via HTTP to the Mattermost API.
```

### REFACTOR-2 중간 검증

```bash
$ cd /config/workspace/nomadcode && rg --sort path "mattermost_push_plugin|host app|Firebase|Mattermost" apps/mobile/README.md apps/mobile/push-notification-todo.md
apps/mobile/README.md
5:## Mattermost Push Notification Integration
7:Mattermost push notification delivery is implemented via the local `mattermost_push_plugin` path dependency.
11:To maintain a clean and maintainable codebase, the responsibilities are split between the host application and the plugin as follows:
14:- **Firebase Configuration**: Contains the standard Firebase configuration files (`android/app/google-services.json`) and performs the standard `Firebase.initializeApp()` call in Dart main.
15:- **Mattermost Authentication**: Handles login and session lifecycles, and securely registers the user authentication token and server signing key to the plugin by calling `MattermostPushPlugin.instance.setAuthToken()` and `MattermostPushPlugin.instance.setSigningKey()`.
17:- **FCM Token Sync**: Listens to device token registrations via `onDeviceTokenReady` and registers the token with the Mattermost server.
19:#### Mattermost Push Plugin (`mattermost_push_plugin`) Responsibilities:
23:- **Inline Reply / Dismiss Actions**: Receives native background actions, records responses in a Room database, and forwards replies/ACKs directly to the Mattermost API via custom OkHttp adapters.

apps/mobile/push-notification-todo.md
7:> **Do NOT edit native push files directly inside this host app repository (`apps/mobile/android`).** All native push logic and platform services now reside in and must be maintained within the plugin repository.
11:The host application acts strictly as a consumer of the `mattermost_push_plugin` package. What remains in `apps/mobile` regarding push notifications are:
13:1. **Firebase Manifest & Metadata**:
15:   - Initializing Firebase inside `lib/main.dart`
17:   - Storing session authorization tokens via `MattermostPushPlugin.instance.setAuthToken()` on login and clearing them on logout.
18:   - Registering Mattermost server public signing keys via `MattermostPushPlugin.instance.setSigningKey()`.
20:   - Handling user taps on notifications by implementing `onNavigateToChannel` and `onNavigateToThread` routing callbacks on `MattermostPushPlugin.instance`.
24:All native Kotlin/Java files, Room DB configurations, and broadcast receivers documented below are now managed within the `mattermost_push_plugin` repository:
26:- `MattermostFirebaseMessagingService.kt`
31:- `helpers/db/MattermostDatabase.kt` & `GlobalDatabase.kt`
34:`[mattermost_push_plugin/README.md](file:///config/workspace/mattermost-push-plugin/README.md)`
```

### REFACTOR-3 중간 검증

```bash
$ cd /config/workspace/nomadcode && rg --sort path "MattermostFirebaseMessagingService|NotificationReplyBroadcastReceiver|NotificationDismissService|ReceiptDelivery|PushNotificationDataHelper" apps/mobile/android/app/src/main/java apps/mobile/lib
(exit code 1 - No stale sources found in host app)
```

### 최종 검증

```bash
$ cd /config/workspace/mattermost-push-plugin && dart format --output=none --set-exit-if-changed lib test
Formatted 5 files (0 changed) in 0.02 seconds.

$ cd /config/workspace/mattermost-push-plugin && flutter analyze --no-fatal-infos
Analyzing mattermost-push-plugin...
No issues found! (ran in 6.5s)

$ cd /config/workspace/mattermost-push-plugin && flutter test
00:01 +9: All tests passed!

$ cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
[!] No Android SDK found. Try setting the ANDROID_HOME environment variable.

$ cd /config/workspace/nomadcode/apps/mobile && dart format --output=none --set-exit-if-changed lib test
Formatted 6 files (0 changed) in 0.04 seconds.

$ cd /config/workspace/nomadcode/apps/mobile && flutter analyze --no-fatal-infos
Analyzing mobile...
   info • Don't invoke 'print' in production code • lib/main.dart:39:5 • avoid_print
   info • Don't invoke 'print' in production code • lib/main.dart:63:11 • avoid_print
   info • Don't invoke 'print' in production code • lib/main.dart:66:11 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:25:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:32:5 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:38:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:41:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:48:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:52:5 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:96:5 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:114:9 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:123:9 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:125:9 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:128:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:145:7 • avoid_print
   info • Don't invoke 'print' in production code • lib/services/mattermost_auth_service.dart:147:7 • avoid_print
16 issues found. (ran in 2.6s)

$ cd /config/workspace/nomadcode/apps/mobile && flutter test
00:02 +2: All tests passed!

$ cd /config/workspace/nomadcode/apps/mobile && flutter build apk --debug
[!] No Android SDK found. Try setting the ANDROID_HOME environment variable.

$ cd /config/workspace/nomadcode && rg --sort path "MattermostFirebaseMessagingService|NotificationReplyBroadcastReceiver|NotificationDismissService|ReceiptDelivery|PushNotificationDataHelper|push_notification_service" apps/mobile/android/app/src/main/java apps/mobile/lib apps/mobile/test
(exit code 1 - No stale sources found in host app)

$ cd /config/workspace/nomadcode && rg --sort path "com.google.firebase.MESSAGING_EVENT|MattermostFirebaseMessagingService|FlutterFirebaseMessagingService" apps/mobile/build/app/intermediates/merged_manifest/debug/processDebugMainManifest/AndroidManifest.xml ../mattermost-push-plugin/example/build/app/intermediates/merged_manifest/debug/processDebugMainManifest/AndroidManifest.xml
rg: apps/mobile/build/app/intermediates/merged_manifest/debug/processDebugMainManifest/AndroidManifest.xml: No such file or directory (os error 2)
../mattermost-push-plugin/example/build/app/intermediates/merged_manifest/debug/processDebugMainManifest/AndroidManifest.xml
86:            android:name="com.tokilabs.mattermost_push_plugin.MattermostFirebaseMessagingService"
89:                <action android:name="com.google.firebase.MESSAGING_EVENT" />
160:                <action android:name="com.google.firebase.MESSAGING_EVENT" />
```

---

> **[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 | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` to `[x]` only |
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only |

## 코드리뷰 결과

### 종합 판정

FAIL

### 차원별 평가

| 차원 | 평가 | 근거 |
|------|------|------|
| correctness | Pass | 문서가 설명하는 plugin/host 책임 경계와 stale source 제거 검색은 대체로 실제 소스 상태와 일치한다. |
| completeness | Fail | 계획의 최종 Android build/merged-manifest 검증 계약과 local absolute path 금지 체크리스트가 완료되지 않았다. |
| test coverage | Fail | `PLAN-cloud-G07.md`가 요구한 plugin example/host Android debug build가 모두 성공하지 못했다. |
| API contract | Pass | README의 Dart API 예시는 실제 `MattermostPushPlugin` surface와 대체로 일치한다. |
| code quality | Fail | host handoff 문서에 로컬 절대 `file://` 링크가 남아 재사용성과 이동성을 깨뜨린다. |
| plan deviation | Fail | Android SDK 부재를 기록했지만, 원 계획의 "Android builds must be freshly run" 조건을 만족하거나 승인 가능한 대체 검증으로 닫지 못했다. |
| verification trust | Fail | host merged manifest가 생성되지 않았고, plugin example merged manifest도 기존 build artifact에 의존했다. |

### 발견된 문제

- Required: `CODE_REVIEW-cloud-G07.md:171` 및 `CODE_REVIEW-cloud-G07.md:200`에 기록된 plugin example/host `flutter build apk --debug`가 `No Android SDK found.`로 실패했다. 하지만 `PLAN-cloud-G07.md:218`은 Android build 명령 성공과 fresh run을 최종 조건으로 요구한다. Android SDK가 있는 환경에서 두 build와 merged-manifest 검색을 다시 성공시키거나, SDK 부재가 계속되는 경우 host/plugin Android 통합을 검증할 명시적 대체 명령과 실제 출력을 기록해 `[REFACTOR-3]`를 다시 닫아야 한다.
- Required: `apps/mobile/push-notification-todo.md:34`가 `file:///config/workspace/mattermost-push-plugin/README.md` 로컬 절대 경로를 문서에 추가했다. `PLAN-cloud-G07.md:148`의 "No secrets, tokens, or local absolute user paths are added" 체크리스트를 위반하므로, repo-root 기준 `../mattermost-push-plugin/README.md` 같은 상대 경로 또는 일반 텍스트 경로로 바꿔야 한다.

### 다음 단계

- FAIL: Required 이슈 2건을 해결하는 follow-up `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 생성한다.
