왜 · 원칙
코칭 엔진은 지금 흩어진 lib 파일 ~15개로 잘 돌지만, ① 신규 기능(선호 계량화·푸드체이닝·노출)이 토대를 공유하고 ② 통합맵대로 6+1 모듈로 정리하면 응집도·테스트·확장이 쉬워집니다. 한 번에 갈아엎지 않고 아래 원칙으로 한 칸씩 옮깁니다.
- behavior 보존 — 겉보기 동작 동일. 리네임·파일 이동·배럴부터(오늘
callClaude→callLLM이 그 1번 사례: tsc 0·테스트 369/369 그린). - additive·역호환 — 기존 호출부를 깨지 않고 새 진입점을 더한 뒤 옮긴다. 폴백 유지.
- 한 모듈씩 · 테스트 그린 유지 — 매 단계
vitest(prebuild 게이트)·tsc통과해야 다음. - 라이브 무중단 — 크론·편지 발행 경로는 마지막에, 카나리아(아린)→전자녀 순.
- DB 선행 — 데이터 무결성(멱등성·정규화)이 안 잡히면 모듈만 정리해도 모래 위. Phase 0 먼저.
증상 개선 — 구현 · 기능 · 설계 변경 방향 (2026-06-18 적대감사)
★ 최상위 단일 근본원인 — 일간 두뇌 블록(route.ts:628-629)이 brainPick.scenarioId만 있으면 precomputed = planFor({forceScenarioId})로 재할당한다. 이 경로(coach.ts:445-451)는 주간 닻(타깃·lever·채근캡·teaching arc)을 전혀 모른 채 raw signals로 타깃을 새로 뽑는다 → 주간이 잠근 모든 것이 일간에서 무력화. 즉 이 문서가 그린 '조립→뇌(판단)→손'이 코드에선 '뇌가 작전층(주간)을 덮어쓰기'로 역전돼 있다. 리팩터 전략(P0~P4)의 역할 = 이 역전을 끝내는 두 구조 결함을 단계 마일스톤으로 박는다 — ⓐ 판단 권한 이중화(주간 닻 vs 일간 두뇌, 계약 부재)와 ⓑ 주간 진행 폐루프 부재(advanceProgress·curriculum_progress·focusHistory 미배선으로 닻 고착). 개별 캡·필터·게이트는 각 모듈 소관이라 여기서 직접 구현하지 않고 들어갈 단계·의존 순서·완료정의(계약)만 조율한다.
| 증상/문제 | 근본원인 (파일:라인 · 분류) | 구현·기능·설계 변경 방향 | 우선 |
|---|---|---|---|
| 치킨 반복·음식 잔소리 N연속·괴식·앵무새 4증상의 상류 — 일간 두뇌가 주간 닻(타깃 콩류·lever environment·push=0)을 통째 폐기 | route.ts:628-629 vs 604-610 · CODE_BUG | P2 판단 권한 단일화의 1번 산출물. planFor에 lockedPlan/anchor 인자를 더해 닻이 잠근 target·lever·push캡을 보존한 채 두뇌는 시나리오만 교체. 비-food 레버 주엔 food override를 결정론 캡(주당 N회) 안에서만 허용. 셀렉터 파사드 계약으로 명문화하고 P2 완료정의 = "두뇌 override가 닻 plan을 폐기하지 않음"을 테스트로 보증. 캡 수치·결핍필터 구현은 ①신호선택기 소관. | P2(실질1) |
판단 권한 이중화의 구조 측면 — planFor(forceScenarioId)가 타깃 풀을 주간 닻 대신 새로 산출(치킨이 타깃이 됨) | coach.ts:445-450 · 386-388 · CODE_BUG | 위 계약의 lib 측면. planFor가 호출자가 잠근 풀(lockedTargetPool)을 받게 하고, 닻이 있으면 mission_target+잔여결핍을, 없으면 refExposable(결핍군 필터)을 단일 진실원천으로 받는다. 거부 결핍필터를 일간/폴백/두뇌 3경로가 공유하는 단일 수정점으로 둔다는 원칙만 전략이 정의(구현은 ①). | P2 |
| 주간 종합이 매주 environment·push0·식탁 behavior_goal로 고착 — teaching arc 진행·재앵커·stall 전환이 daily lever를 못 바꿈(앵무새 상류) | route.ts:520-534 · coachWeekly.ts:96-108(applyFocusFatigue dormant) · DESIGN_GAP | P3 폐루프 닫기. cron에 advanceProgress 호출 + curriculum_progress upsert(현재 write 0건) 배선, runWeeklyPlanning에 progress·focusHistory 주입으로 피로 캡 활성화. 그래야 "환경이 안 먹히면 음식으로 진행"(curriculum.ts:200-215 limping/stall 피벗)이 데이터로 닫혀 닻 고착이 풀린다. 새 테이블 불필요 — 기존 curriculum_progress(sql/2026-06-12) 재사용. | P3 |
| 두뇌가 상위 제약(lever·push캡)을 입력 단계에서 보지 못해 override를 남발 | route.ts:619-626 buildBrainContext 입력 · DESIGN_GAP | P3 CoachContext 단일 계약화. 두뇌 입력을 산발적 signals + wk3 + nutritionMirror 조합에서 동결된 CoachContext 객체로 전환 — 닻의 lever·mission_target·target_pool·ledger.pushUsed·budget.push + 최근 food override 이력을 계약 필드로 포함. 상위 제약을 못 본 채 시나리오를 갈아끼우는 것을 입력 단계에서 구조적 차단. | P3 |
weekly_plans.status가 supersede/archive 없이 매주 active만 적재 → W22~W25 4행 동시 active(변별력 없는 표시 전용 데드값) | route.ts:538 · 559 · CODE_BUG | P0 무결성에 1줄 추가. 신규 닻 upsert 직후 같은 child_id의 직전 week_key active 행을 status='archived'로 강등 → 1-active-per-child 불변식·어드민 가시성 확보. loadAnchor는 week_key로만 로드하므로 daily 동작 무영향·저위험. | P0 |
planFor 분리는 "파일 이동"이 아니라 "닻이 상위 권한이고 두뇌는 닻 안에서만 시나리오를 고른다"는 단일 판단 권한 계약을 코드로 강제하는 것이 본질이다. P3의 child_daily_state 배치는 "캐시"가 아니라 닫히지 않은 주간 진행 폐루프를 닫는 단일 진실이어야 한다.route.ts:647 STRUCTURAL 가드가 nutrient-gap override로 탈출하는 것과 동일 함정).weekly_plans 1-active-per-child 불변식 SQL/한 줄 · 저위험week_key active 행 → archived 강등. daily는 week_key 로드라 동작 무영향.planFor 판단부를 ①로 분리 = 계약 박기(lockedPlan/anchor 인자·시나리오만 교체·비-food 주 override 캡). 완료정의 = 두뇌가 re-exposure-timing(치킨)을 골라도 발행 plan.target이 닻(콩류)/환경 프레임으로 유지됨을 골든 테스트로 보증.advanceProgress→curriculum_progress upsert + focusHistory 주입(피로 캡 활성)으로 닻 고착 해소. 두뇌 입력을 CoachContext 동결 객체로 전환해 상위 제약(lever·push캡)을 입력 단계에서 노출.route.ts:545) → upsert 결과 검사·폴백 필수. 테스트: limping/stall 시 environment→food 피벗이 닻 goals에 영속되는지. 롤백: 배치 단계 플래그 OFF→재계산 경로 복귀.목표 상태 — Before / After
CoachContext 단일 계약, 야간 배치 2단계.단계 로드맵 — P0 → P4
의존 순서대로. P0(무결성)·P1(선호 토대)이 신규 기능의 전제라 먼저.
meal_logs(child_id,log_date,slot)·coach_letters(child_id,letter_date)·daily_questions·user_menu_overrides 등)·핵심 FK·name UNIQUE(ingredients.name). PII 테이블 RLS·DDL 형상관리.meal_log_items(끼니→음식 1행·수용 5단계·거부사유) 추가 + 기록 UX 원탭 포착 + child_food_pref(다축 롤업) + 콜드스타트 게이트. 읽기 전용 계산부터(라이브 무영향).child_daily_state 야간 머티리얼라이즈(1단계) → 편지 워커(2단계)가 CoachContext 읽기. 아린 카나리아→전자녀. ⭐추천 두 트랙(supply 결핍·challenge 사촌/공출현) + weekly_plans.reco_balance(주간 밸런스) + 뇌 decision.recoTrack(일간 회전). ⭐BMI·성장곡선 연동 3대영양소 매크로 트랙(저체중·성장더딤 → 탄·단·지 식재료 우선) + weekly_plans 3대영양소 카테고리(격주·2주 연속 금지 — 측정 빈도 낮아 과잉 잔소리 방지).JSON → SQL 전환 결정표 (무엇을 SQL/캐시/코드로)
"전부 SQL"이 아니라 SSOT=DB · 핫패스/클라=파생 캐시 · 로직=코드. 정적 JSON 스냅샷을 쓰던 이유(빌드 판정·클라 사용·속도)는 인정하되, 자라는 데이터는 DB를 SSOT로 옮겨 푸시 없이 갱신되게 한다.
| 데이터/자산 | 지금 | → 가야 할 곳 | 이유 · 단계 |
|---|---|---|---|
| food-graph (사촌·궁합) | 정적 JSON | SQL(ingredient_edges) SSOT + 캐시 | 매일 자람(공출현 복리)·푸시없이 갱신·관계질의. graphSource.warmGraphFromSql+COACH_GRAPH_SQL 이미 배선(켜기만) → P3 |
| learned_menus (메뉴→식재료) | DB(배열) | DB 유지 + menu_ingredients junction을 SSOT로 | 배열→junction 정규화(SSOT 이중·stale 해소) → P1~P2 |
| 도감 등급·vocab (ingredients-light) | 정적 JSON(빌드) | SQL(ingredients 컬럼) SSOT + 생성 캐시/캐시 API | DB가 진실, 클라·핫패스는 캐시. gen-grade가 JSON 재생성 대신 컬럼 업데이트 → P4 |
| meal_logs 식재료 | 배열 text[] | 정규화(meal_log_items) | 1NF·per-식재료 집계(선호/노출) → P1 |
| cooking-amounts·cuisine-guide·curated 콘텐츠 | 정적 JSON | JSON 유지 | 거의 안 변함·ROI 낮음·관계질의 불요 |
| 매퍼 규칙(menuMapCore boostFromName)·시나리오·유닛 | 코드 | 코드 유지 | 절차 로직(테이블화 부적합)·푸시 필요는 당연. MENU_MAP 사전 부분만 테이블화 검토 |
COACH_GRAPH_SQL ON·ingredient_edges 재집계·정합검증) → P4(vocab/등급 DB SSOT·질의대상 jsonb 정형화). ⚠️ graphSource ON 조건 = SQL 테이블 적재 + 엣지 재집계 + JSON과 정합 확인(지금 SQL==JSON이라 켜도 무효).모듈별 리팩터 카드
| 모듈 | 합치는 것 · 작업 | 리스크 | 테스트 |
|---|---|---|---|
| ① 신호선택기 | coachScenarios+coachBrain+coachWeekly → 셀렉터 파사드(선택 책임 3중복 일원화) | 높음 라이브 두뇌 경로 | coach-hybrid-rotation·coach-guards |
| ② 추천엔진 | coachRecos + 휴면(coachMaterials·recoEvidence) 흡수/삭제 결정 · 근거문구 일원화 · comboMatrix=괴식 유틸로 배선 | 중간 휴면 정리·게이트 | coach-recos·coach-combo-cells-gate·coach-materials |
| ③ 사실·거울 | coachFacts+reexposure → 배럴(meal_logs 사실화 한곳) | 낮음 배럴 | coach-data·coach-guards |
| ④ 영양평가 | nutrition+snack → 배럴(isProcessed 공유) | 낮음 | (영양 신호 테스트) |
| ⑤ 진척·커리큘럼 | progress+curriculumUnits+coachDaily → 배럴 | 낮음 | curriculum.test |
| ⑥ 작문기(손) | coach.ts에서 판단부(planFor)→①로 분리, 작문(compose/generate/verify)+coachChronic만 남김 | 높음 coach.ts 분해·라이브 발행 | 전 coach 스위트·아린 카나리아 |
| ⑦ 네트워크 조회 | foodGraph+kitGuide+ingredientFreq+graphSource · freq 일원화 | 중간 graphSource SQL 토대 | graph-source·coach-tray-cooccur |
graphSource·cookingMatrix·comboMatrix·remapMenus)는 모듈로 흡수하지 않음 — 별도 계층/유틸 유지(통합맵 '합치면 안 되는 것').리스크 · 롤백
- 매 단계 게이트: tsc 0 + vitest 그린 + (라이브 경로면) 아린 카나리아 1~3일. 실패 시 그 커밋만 revert.
- 라이브 발행 경로(①·⑥)는 마지막: 데이터·계산부(P0·P1·③④⑤·⑦)를 먼저 안정화한 뒤 손댄다.
- 휴면 코드 결정 명시: coachMaterials·comboMatrix·recoEvidence는 '라이브 승격 vs 삭제'를 P2에서 한 번에 결정(어정쩡한 잔존 금지).
- DB 마이그레이션 additive: 새 테이블·제약은 추가만, 기존 컬럼(배열)은 캐시로 유지하다 소비처 전환 후 제거.
- 과잉정규화 경고: 정당한 jsonb 스냅샷(context·metrics·child_daily_state)·집계 캐시는 건드리지 않는다.
meal_log_items 정규화 + 수용 5단계 포착 UX 범위 ④ ⑥ coach.ts 분해를 언제(가장 큰 작업).