<!-- task=mattermost_push_plugin_extraction/04+02_risk_hardening plan=0 tag=REFACTOR -->

# Push Plugin Risk Hardening Plan

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

구현을 끝내기 전 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제로 실행하고 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 둔 채 review ready를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다.

## 배경

The Milestone explicitly calls out DB singleton close, `PushNotificationDataHelper` DAO stubs, and Firebase Messaging manifest collisions as extraction risks. Once native code lives in the plugin, these risks should be fixed or intentionally documented before the app fully relies on the plugin. This subtask hardens plugin-owned Android native behavior without expanding into host app UX or server-side work.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/mattermost-push-plugin-extraction.md`
- `agent-ops/rules/project/domain/mobile/rules.md`
- `apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/PushNotificationDataHelper.kt`
- `apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/CustomPushNotificationHelper.java`
- `apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/MattermostDatabase.kt`
- `apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/db/ServerEntities.kt`
- `apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/DatabaseHelper.kt`
- `apps/mobile/android/app/src/main/AndroidManifest.xml`
- `apps/mobile/android/app/build.gradle.kts`
- `apps/mobile/push-notification-todo.md`

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

- DAO stub behavior has no existing tests. Add JVM-level tests only if plugin Android test harness can run them quickly; otherwise compile and document manual smoke data conditions.
- DB close regression has no direct test. Use static verification that plugin native source no longer calls `db.close()` on singleton DBs.
- Firebase manifest merge collision is not covered by unit tests. Use Android debug build and inspect merged manifest in final docs task.

### 심볼 참조

- `db?.close()` appears in current `PushNotificationDataHelper.kt:99`.
- `db.close()` appears in current `CustomPushNotificationHelper.java:497`.
- DAO stubs are `fetchTeamIfNeeded` at `PushNotificationDataHelper.kt:109`, `fetchMyChannel` at `115`, `fetchPosts` at `121`, `fetchThread` at `128`, `fetchNeededUsers` at `134`, `saveToDatabase` at `140`.
- `MattermostDatabase` currently declares only `ConfigEntity`, `UserEntity`, `SystemEntity` at `MattermostDatabase.kt:12-18`.

### 분할 판단

- Split decision policy was evaluated before choosing plan files.
- This subtask depends on `02+01_android_native`; `agent-task/mattermost_push_plugin_extraction/02+01_android_native/complete.log` must exist before implementation starts.
- It is split from native move because storage schema changes and manifest conflict checks are easier to review after the plugin native code compiles.

### 범위 결정 근거

- Do not alter host app imports or delete app files; `03+01,02_host_consumer` owns host migration.
- Do not implement iOS/macOS native push; Milestone explicitly excludes full iOS/macOS implementation.
- Do not add server API behavior. Network calls remain the migrated ACK/reply client paths only.

### 빌드 등급

- Build lane: `cloud-G08`; review lane: `cloud-G08`. Storage schema, singleton lifecycle, Android manifest merge, and auth-backed reply/ACK paths make this high-risk despite narrow file count.

## 구현 체크리스트

- [ ] [REFACTOR-1] Singleton Room DB close calls를 제거하고 DB lifecycle을 plugin-safe하게 정리한다.
- [ ] [REFACTOR-2] `PushNotificationDataHelper` DAO stubs를 구현하거나 명시적 follow-up 예외로 분류한다.
- [ ] [REFACTOR-3] Firebase Messaging manifest collision risks를 plugin manifest/README note/verification으로 닫는다.
- [ ] 중간 검증과 최종 검증 명령을 모두 실행하고 실제 출력을 `CODE_REVIEW-cloud-G08.md`에 붙여 넣는다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- Directory dependency: `04+02_risk_hardening` depends on `02+01_android_native`.
- Start only after `agent-task/mattermost_push_plugin_extraction/02+01_android_native/complete.log` exists.

### [REFACTOR-1] DB Singleton Lifecycle

#### 문제

`PushNotificationDataHelper.kt:99` and `CustomPushNotificationHelper.java:497` close Room DB instances obtained from singleton maps. Closing these singletons can break later notification processing.

Before (`apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/helpers/PushNotificationDataHelper.kt:98`):

```kotlin
} finally {
    db?.close()
    Log.i(TAG, "DONE fetching notification data")
}
```

After:

```kotlin
} finally {
    Log.i(TAG, "DONE fetching notification data")
}
```

#### 해결 방법

Remove singleton DB close calls from plugin native source. If a short-lived DB is introduced in plugin code, document ownership clearly and never close instances returned by `MattermostDatabase.getInstance`.

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

- [ ] Plugin `PushNotificationDataHelper.kt` removes singleton `close()`
- [ ] Plugin `CustomPushNotificationHelper.java` removes singleton `close()`
- [ ] Static search confirms no `db.close()` or `db?.close()` in plugin Android source

#### 테스트 작성

Skip runtime test; use static search plus Android build.

#### 중간 검증

```bash
cd /config/workspace/nomadcode && ! rg --sort path "db\\??\\.close\\(" ../mattermost-push-plugin/android/src/main
```

Expected: no output and command succeeds through `!`.

### [REFACTOR-2] DAO Stub Resolution

#### 문제

`PushNotificationDataHelper.kt:109-144` returns null for team/channel/post/thread/users/save operations, so notification enrichment is incomplete.

Before (`PushNotificationDataHelper.kt:115`):

```kotlin
private fun fetchMyChannel(db: MattermostDatabase, channelId: String): Bundle? {
    // TODO: db.channelDao().getChannel(channelId)
    Log.i(TAG, "fetchMyChannel: channelId=$channelId (Room DAO 구현 필요)")
    return null
}
```

After:

```kotlin
private fun fetchMyChannel(db: MattermostDatabase, channelId: String): Bundle? {
    val channel = db.channelDao().getChannel(channelId) ?: return null
    return Bundle().apply {
        putString("id", channel.id)
        putString("display_name", channel.displayName)
        putString("type", channel.type)
        putString("team_id", channel.teamId)
    }
}
```

#### 해결 방법

Add minimal Room entities/DAOs for `Channel`, `Post`, and `Team` in plugin `ServerEntities.kt`; register them in `MattermostDatabase` and bump DB version with destructive migration acceptable for the current local cache. Implement `fetchTeamIfNeeded`, `fetchMyChannel`, `fetchPosts`, `fetchThread`, and `saveToDatabase`. If `fetchNeededUsers` needs a missing membership table, keep it as an explicit documented follow-up with a reason in code and CODE_REVIEW.

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

- [ ] Plugin `ServerEntities.kt` adds `ChannelEntity`, `PostEntity`, `TeamEntity` and DAOs
- [ ] Plugin `MattermostDatabase.kt` registers new entities/DAOs and version bump
- [ ] Plugin `PushNotificationDataHelper.kt` implements non-user stubs
- [ ] Any remaining user-membership stub has explicit reason and follow-up note
- [ ] No app-owned DB files are edited in this subtask

#### 테스트 작성

Write if feasible: plugin Android JVM test for DAO insert/query if generated plugin example supports it without extra repo tooling. If not feasible, skip with reason and rely on Android build plus manual smoke criteria in `05+03,04_docs_verify`.

#### 중간 검증

```bash
cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
```

Expected: Room entities/DAO compile.

### [REFACTOR-3] Firebase Manifest Collision Guard

#### 문제

Host app currently removes Flutter firebase messaging components in `AndroidManifest.xml:41-54` while registering its own native service at `56-63`. After plugin extraction, plugin native service and any transitive `firebase_messaging` plugin manifest can collide.

Before (`apps/mobile/android/app/src/main/AndroidManifest.xml:41`):

```xml
        <!-- Flutter firebase_messaging 플러그인 컴포넌트 비활성화 (네이티브 서비스 사용) -->
        <receiver
            android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingReceiver"
            tools:node="remove" />
```

After:

```xml
<!-- Plugin manifest owns Mattermost FCM service; host merged manifest must contain one MESSAGING_EVENT service. -->
```

#### 해결 방법

Ensure plugin manifest declares exactly one Mattermost FCM service. If plugin Dart depends on `firebase_messaging`, document whether host must keep `tools:node="remove"` overrides or whether the plugin avoids the Flutter firebase messaging Android plugin. Add deterministic verification in this subtask for plugin source and leave merged host manifest inspection to `05+03,04_docs_verify`.

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

- [ ] Plugin `android/src/main/AndroidManifest.xml` declares Mattermost service and receivers once
- [ ] Plugin package docs or code comments explain `firebase_messaging` manifest handling
- [ ] Static search records plugin service declarations

#### 테스트 작성

Skip separate tests; manifest checked by build and static search.

#### 중간 검증

```bash
cd /config/workspace/nomadcode && rg --sort path "MESSAGING_EVENT|MattermostFirebaseMessagingService|FlutterFirebaseMessaging" ../mattermost-push-plugin/android/src/main
```

Expected: output shows plugin-owned Mattermost service and any intentional Firebase override notes.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `../mattermost-push-plugin/android/src/main/kotlin/com/tokilabs/mattermost_push_plugin/helpers/PushNotificationDataHelper.kt` | REFACTOR-1, REFACTOR-2 |
| `../mattermost-push-plugin/android/src/main/java/com/tokilabs/mattermost_push_plugin/helpers/CustomPushNotificationHelper.java` | REFACTOR-1 |
| `../mattermost-push-plugin/android/src/main/kotlin/com/tokilabs/mattermost_push_plugin/helpers/db/ServerEntities.kt` | REFACTOR-2 |
| `../mattermost-push-plugin/android/src/main/kotlin/com/tokilabs/mattermost_push_plugin/helpers/db/MattermostDatabase.kt` | REFACTOR-2 |
| `../mattermost-push-plugin/android/src/main/AndroidManifest.xml` | REFACTOR-3 |
| `../mattermost-push-plugin/README.md` | REFACTOR-3 |

## 최종 검증

```bash
cd /config/workspace/nomadcode && ! rg --sort path "db\\??\\.close\\(" ../mattermost-push-plugin/android/src/main
cd /config/workspace/nomadcode && rg --sort path "Room DAO 구현 필요|TODO: db\\.|TODO: Room DB" ../mattermost-push-plugin/android/src/main
cd /config/workspace/mattermost-push-plugin/example && flutter build apk --debug
```

Expected: first command succeeds with no output. Second command should have no output unless each remaining item is an intentional documented follow-up recorded in `CODE_REVIEW-cloud-G08.md`. Android build must be freshly run and succeed.

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