Files
life-helper/docs/design/311-llm-warmup/fn-chat_warmup_controller.md
joungmin 1fa4f24a8a [02-Architect] #311 design spec + UX-Reviewer persona for LLM warm-up
- docs/design/311-llm-warmup/README.md — 기능 설계서. ChatWarmupController (5-state) + GemmaLlmService _loadingFuture concurrent guard + ModelLifecycle.quickCheck (lightweight ready).
- docs/design/311-llm-warmup/UX-REVIEW.md — UX-Reviewer parallel pass. Strong 4 + Suggest 2 권고. 입력창 enabled 유지 (타이핑 가능) + hintText 만 교체 + 상태-행동 분리.
- docs/design/311-llm-warmup/fn-chat_warmup_controller.md — start/retry 상세 + 빠른 경로 (isLoaded 시 Loading skip).
- docs/design/311-llm-warmup/fn-concurrent_load_guard.md — _loadingFuture 패턴 + whenComplete cleanup.
- .claude/agents/ux-reviewer.md — 신규 페르소나 (02-Architect 단계 내 parallel reviewer, 카테고리 부여 X).

AC 8 → 12 (UX 신규 4건 통합). OQ 3건 모두 해소. ADR 없음 (backward-compatible 추가).

Refs #311 #260
2026-06-15 11:41:03 +09:00

4.7 KiB

함수 설계서: ChatWarmupController.start (#311)

부모 설계서: ./README.md · 상태: Draft 작성: [AI] Architect · 구현: app/lib/state/chat_warmup_provider.dart:start · 테스트: app/test/state/chat_warmup_test.dart

1. 시그니처

class ChatWarmupController extends StateNotifier<ChatWarmupState> {
  ChatWarmupController({
    required this.llm,
    required this.lifecycle,
  }) : super(const ChatWarmupIdle());

  final LlmService llm;
  final ModelLifecycle lifecycle;
  bool _disposed = false;

  Future<void> start();
  Future<void> 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<void> — 완료 시점에 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: 없음.