# 함수 설계서: `validateFrameLevel` (#204) > 부모 설계서: [README.md](./README.md) · 상태: Draft > 작성: [AI] Architect · 구현: `lib/domain/frame/validate_frame_level.dart::validateFrameLevel` (TBD) > 테스트: `test/domain/frame/validate_frame_level_test.dart` (TBD) ## 1. 시그니처 ```dart FrameValidationResult validateFrameLevel( FrameInput input, { required Iterable knownPatterns, // catalog 에서 미리 load }); class FrameInput { final FrameLevel level; // L0/L1/L2/L3 final String? originalText; final String framedText; } class FrameValidationResult { final FrameValidationStatus status; // accept / warn / reject final List avoidanceHits; // R7 회피 키워드 검출 결과 final List suggestions; // L2/L3 변환 제안 } enum FrameValidationStatus { accept, warn, reject } class AvoidanceHit { final String keyword; final int startIndex; final int endIndex; final FramePattern source; } class FrameSuggestion { final FrameLevel level; // L2 or L3 final String text; final FramePattern source; } ``` ## 2. 책임 사용자가 입력한 frame 의 (1) level 이 L2/L3 가드를 통과하는지 (R3) 검사하고, (2) framed_text 안의 회피 키워드를 detect 해 변환 제안을 반환한다 (R7). ## 3. 입력 | 파라미터 | 타입 | 제약/검증 | 설명 | |----------|------|-----------|------| | `input.level` | `FrameLevel` enum | not null | L0 이면 reject (R3) | | `input.framedText` | `String` | not empty | 사용자 최종 문장 | | `input.originalText` | `String?` | nullable | L0 였던 원본 (audit) | | `knownPatterns` | `Iterable` | catalog 에서 load | 회피 키워드 사전 | ## 4. 출력 - **반환**: `FrameValidationResult`. - status: - `reject` — L0/L1 입력. UI 는 모달로 L2/L3 변환 제안 표시 후 사용자 선택. - `warn` — L2/L3 이지만 회피 키워드 감지 (예: "안 ..." , "끊기"). UI 는 inline warning + 변환 제안. 사용자가 '계속' 선택 가능. - `accept` — 통과. - avoidanceHits — UI 가 underline 표시. - suggestions — frame_pattern.l2_suggestion / l3_identity 에서 가져온 대체 텍스트. - **부수효과**: 없음. **순수 함수**. ## 5. 동작 / 알고리즘 ``` 1. if input.level in (L0, L1): suggestions = buildSuggestions(input.framedText, knownPatterns) return reject(suggestions, hits=[]) 2. hits = detectAvoidanceKeywords(input.framedText, knownPatterns) 3. if hits.isEmpty: return accept 4. # L2/L3 인데 회피 키워드 감지 — warn suggestions = buildSuggestions(input.framedText, knownPatterns, hits) return warn(suggestions, hits) function detectAvoidanceKeywords(text, patterns) -> List: hits = [] for p in patterns: idx = text.indexOf(p.avoidance_keyword) while idx >= 0: hits.push(AvoidanceHit(keyword=p.avoidance_keyword, start=idx, end=idx+keyword.length, source=p)) idx = text.indexOf(p.avoidance_keyword, idx+1) return hits.distinctBy((h)->(h.start, h.keyword)) function buildSuggestions(text, patterns, hits=null) -> List: relevant = hits != null ? hits.map(h => h.source).distinct() : patterns.filter(p => text.contains(p.avoidance_keyword)) return relevant.expand((p) -> [ FrameSuggestion(L2, p.l2_suggestion, p), if (p.l3_identity != null) FrameSuggestion(L3, p.l3_identity, p), ]).take(5).toList() # UI 부담 위해 최대 5 개 ``` > 회피 키워드 사전은 `frame_patterns.json` 시드 (~30 항목). 'general' 도메인이 default 매칭 풀, 특정 domain (food/drink/...) 은 habit 의 protocol category 로 필터링 가능 (v2 최적화). ## 6. 에러 & 실패 모드 | 조건 | 처리 | 반환 | |------|------|------| | `framedText` empty | reject + suggestions=[] | reject | | `level` enum 외 값 | 호출자 보장 — 도달 불가 | — | | `knownPatterns` empty | accept (사전 없으면 R7 skip) | accept | | 회피 키워드가 다른 단어의 부분 일치 (예: "안전" 안에 "안") | false positive 발생. 본 phase 는 substring 매칭. 단어 경계 처리는 v2 (한국어 형태소 분석 필요). | warn (사용자가 '계속' 선택) | ## 7. 엣지케이스 - "끊기" 가 "끊기는 게 아니라" 안에 포함 → hit. warn. 사용자가 '계속'. - L3 ("나는 무알콜인 사람이다") + 회피 키워드 "무알콜" — '무X' 패턴이 사전에 있다면 hit. 사용자가 '계속'. - L2 + 회피 키워드 없음 → accept. - L0 + 회피 키워드 없음 → reject + L2 일반 제안 ('이걸 대신 해보세요'). ## 8. 복잡도 / 성능 - 시간: O(N × M), N = patterns 수 (~30), M = text 길이 (≤ 100). worst ≈ 3000 char ops → < 100 µs. - 공간: O(hits). - 호출 빈도: habit 생성 폼에서 입력 변경 시 debounced (300 ms). 무시. ## 9. 의존성 - `FramePattern` 모델 (domain/models). - `FrameLevel` enum. - catalog DAO 가 startup 에 patterns 를 한 번 load 해서 Riverpod provider 로 공유. ## 10. 테스트 케이스 - [ ] accept: L2, "기상 직후 햇빛 10 분 받기" → status=accept, hits=[] - [ ] reject (L0): L0, "햇빛 안 빼먹기" → status=reject, suggestions 포함 L2/L3 - [ ] reject (L1): L1, "햇빛 챙기기" → status=reject (L2 으로 변환 제안) - [ ] warn: L2, "술 끊기 (조금만)" — "끊기" hit → status=warn, suggestions - [ ] accept (L3 클린): L3, "나는 아침 햇빛 사람이다" → accept - [ ] empty framedText → reject - [ ] empty patterns → accept (회피 검출 skip) - [ ] 다중 hits: "끊기 안 하기" → 2 개 hits, suggestions ≤ 5 ## 11. 추적성 - 인수조건: #204 AC-8 (R3 reject), AC 보강 (R7 warn). - 관련 메모리: `feedback_positive_framing.md` (L2/L3 강제). - 관련 schema: `frame_pattern.schema.json`, `habit.schema.json` (frame.level).