"""학습 패턴 분류 (Phase 1). attempts 시간 순 리스트 → pattern_state 7 분류: unattempted / unsure / chronic_wrong / regressed / recovered / stable / unstable 윈도우 = 최근 3회 attempts + 과거 correct/wrong 존재 여부. recent3 만으로는 regressed/recovered 판정 불가능 — 과거 이력 함께 필요. 우선순위 (위→아래, 첫 매칭 박힘): 1. unattempted (attempts == 0) 2. unsure (latest == 'unsure') 3. chronic_wrong (recent3 wrong ≥ 2) 4. regressed (latest wrong + 이전 correct 이력) 5. recovered (latest 2 연속 correct + 이전 wrong 이력) 6. stable (recent3 모두 correct, 3회 충족) 7. unstable (그 외) UI 라벨 (가칭): unattempted="미학습" / unsure="모르겠음" / chronic_wrong="반복 오답 경향" regressed="퇴행" / recovered="회복" / stable="안정" / unstable="불안정" chronic_wrong 은 [wrong,wrong,correct] 같이 latest correct 케이스도 잡힘. "지금 틀림" 의미 X, "최근 3회 오답 비중 높음" 의미 — UI 라벨 주의. """ from __future__ import annotations from dataclasses import dataclass PATTERN_UNATTEMPTED = "unattempted" PATTERN_UNSURE = "unsure" PATTERN_CHRONIC_WRONG = "chronic_wrong" PATTERN_REGRESSED = "regressed" PATTERN_RECOVERED = "recovered" PATTERN_STABLE = "stable" PATTERN_UNSTABLE = "unstable" WINDOW_SIZE = 3 @dataclass class _AttemptOutcome: """compute_pattern_state 입력용 가벼운 dataclass. 시간 순 (oldest → newest).""" outcome: str # correct / wrong / unsure / skipped def compute_pattern_state(attempts: list[_AttemptOutcome]) -> tuple[str, int]: """attempts (oldest → newest) → (pattern_state, window_size). window_size = min(WINDOW_SIZE, len(attempts)). """ if not attempts: return PATTERN_UNATTEMPTED, 0 latest = attempts[-1].outcome if latest == "unsure": return PATTERN_UNSURE, min(WINDOW_SIZE, len(attempts)) recent_window = attempts[-WINDOW_SIZE:] wrong_in_window = sum(1 for a in recent_window if a.outcome == "wrong") # chronic_wrong: 최근 3회 중 wrong ≥ 2 (또는 attempts 가 2회뿐이고 둘 다 wrong) if wrong_in_window >= 2: return PATTERN_CHRONIC_WRONG, len(recent_window) # regressed: latest wrong + 이전 correct 이력 1건 이상 if latest == "wrong" and any(a.outcome == "correct" for a in attempts[:-1]): return PATTERN_REGRESSED, len(recent_window) # recovered: latest 2 연속 correct + 그 이전 wrong 이력 if ( len(attempts) >= 3 and attempts[-1].outcome == "correct" and attempts[-2].outcome == "correct" and any(a.outcome == "wrong" for a in attempts[:-2]) ): return PATTERN_RECOVERED, len(recent_window) # stable: 최근 3회 모두 correct (3회 충족) if len(recent_window) >= WINDOW_SIZE and all(a.outcome == "correct" for a in recent_window): return PATTERN_STABLE, WINDOW_SIZE # 그 외: unstable (1~2회 correct, wrong 1회 + correct 이력 없음, 등) return PATTERN_UNSTABLE, len(recent_window) def attempts_from_rows(rows) -> list[_AttemptOutcome]: """SQL row (outcome 컬럼) → _AttemptOutcome 리스트. 호출자가 정렬은 미리. rows 는 oldest → newest 정렬되어 들어와야 함 (answered_at asc). """ return [_AttemptOutcome(outcome=r.outcome) for r in rows]