# 함수 설계서: `ChatWarmupController.start` (#311) > **부모 설계서**: ./README.md · **상태**: Approved (v0.4.1, 2026-06-15) > **작성**: [AI] Architect · **구현**: `app/lib/state/chat_warmup_provider.dart:start` · **테스트**: `app/test/state/chat_warmup_test.dart` ## 1. 시그니처 ```dart class ChatWarmupController extends StateNotifier { ChatWarmupController({ required this.llm, required this.lifecycle, }) : super(const ChatWarmupIdle()); final LlmService llm; final ModelLifecycle lifecycle; bool _disposed = false; Future start(); Future retry(); @override void dispose() { _disposed = true; super.dispose(); } } ``` ## 2. 책임 (단일 책임) 모델 ready 추정 → background `load()` → state 전이까지를 한 번의 트랜잭션으로 묶고, 모든 실패/취소 분기에서 안전하게 state 만 갱신한다. ## 3. 입력 | 파라미터 | 타입 | 제약/검증 | 설명 | |----------|------|-----------|------| | (ctor) `llm` | `LlmService` | non-null | `load`/`isLoaded` 만 사용. | | (ctor) `lifecycle` | `ModelLifecycle` | non-null | `quickCheck` 만 사용. | ## 4. 출력 - **반환**: `Future` — 완료 시점에 state 가 ready/failed/unavailable 중 하나로 확정. - **부수효과**: `state =` 설정. 다른 I/O 없음. ## 5. 동작 / 알고리즘 ``` 1. 현재 state 가 Loading 이면 즉시 return (재진입 가드, retry 외에는 발생 X). 2. state = ChatWarmupLoading() 임시 설정 (단, 아래 빠른 경로 확인 전이라 안전). → ChatScreen 재진입 시 깜빡임 방지 위해 isLoaded 빠른 경로를 먼저 확인: if (llm.isLoaded) { _safeSet(const ChatWarmupReady()); return; } 3. quickCheck = await lifecycle.quickCheck(); 4. quickCheck != ready: _safeSet(const ChatWarmupUnavailable()); return; 5. _safeSet(const ChatWarmupLoading()); // 본격 로드 시작 6. try { await llm.load(); } catch (e) { _safeSet(ChatWarmupFailed(_messageFor(e))); return; } 7. _safeSet(const ChatWarmupReady()); _safeSet(s) = if (_disposed) return; state = s; ``` `retry()` = `state = ChatWarmupIdle();` 후 `await start();`. ## 6. 에러 & 실패 모드 | 조건 | 처리 | 반환/예외 | |------|------|-----------| | `quickCheck` 가 DB lock 등으로 throw | `lifecycle.quickCheck` 내부 catch → `corrupt` 반환 | state = `Unavailable` (보수적) | | `llm.load()` 가 `FileSystemException('model file missing')` | _messageFor 가 매핑 → "AI 모델 파일을 찾을 수 없어요. 설정에서 다시 다운로드해주세요." | state = `Failed` | | `llm.load()` 가 기타 throw (native init 실패, OOM) | _messageFor → "AI 시작 중 오류가 발생했어요. 잠시 후 다시 시도해주세요." | state = `Failed` | | start() 진행 중 dispose() | `_disposed = true` → `_safeSet` 가 no-op | state 변경 안 함 (마지막 set 유지) | | concurrent start() 호출 | step 1 의 Loading 가드 — 외부에서는 retry() 만 사용하므로 정상 흐름에서 미발생 | early return | ## 7. 엣지케이스 - **ChatScreen 재진입 (이미 loaded)**: step 2 의 빠른 경로로 Loading 단계 skip → 라벨 깜빡임 없음. - **start() 진행 중 ChatScreen pop → push (빠른 재진입)**: 첫 인스턴스 dispose, 두 번째 인스턴스의 start() 가 새로 호출. `_disposed` 가 인스턴스별이라 race 없음. `llm._loadingFuture` 가 native init 중복 차단. - **opt-in 토글 race**: 사용자가 ChatScreen 진입과 동시에 SettingsScreen 에서 opt-out → ChatScreen 의 🤖 entry 가 hidden 되며 즉시 pop. dispose 가드로 안전. ## 8. 복잡도 / 성능 - 시간: O(1) + `lifecycle.quickCheck` O(1) (meta_kv 4 쿼리 + 1 stat) + `llm.load()` (수 초). - 공간: state object 1개. - 호출 빈도: ChatScreen mount 당 1회 (+retry 횟수). ## 9. 의존성 - `LlmService` (`load`, `isLoaded`) — 인터페이스 안정. - `ModelLifecycle.quickCheck` (신규). - `flutter_riverpod` `StateNotifier`. ## 10. 테스트 케이스 - [ ] 정상 happy: quickCheck=ready, load delay 100ms → 시퀀스 [Idle → Loading → Ready]. - [ ] 빠른 경로: isLoaded=true → 시퀀스 [Idle → Ready] (Loading 없음). - [ ] unavailable: quickCheck=missing → 시퀀스 [Idle → Unavailable], load 호출 안 됨. - [ ] failure: load throws FileSystemException → state = Failed + 매핑된 한국어 메시지. - [ ] failure: load throws StateError → state = Failed + generic 메시지. - [ ] retry: Failed → retry() → Loading → Ready. - [ ] unmount race: start() 진행 중 dispose() → state 변경 시도 무시 (마지막 state = Loading 유지). - [ ] DB 예외: quickCheck 가 throw → Unavailable. ## 11. 추적성 - 인수조건: AC1, AC2, AC3, AC4, AC5, AC6. - 관련 ADR: 없음.