🛠️ ENGINE · 리팩터 전략 (개발 전)

코칭 엔진 리팩터 전략
흩어진 lib 파일 → 7 통합 모듈

모듈 개편(11→6+조회1)·CoachContext 계약·아이 현황 스토어로 가는 안전한 이행 순서. behavior 보존·한 모듈씩·라이브 무중단. DB 감사(무결성)·신규 기능(선호 계량화·푸드체이닝)을 한 로드맵에 흡수.

0

왜 · 원칙

코칭 엔진은 지금 흩어진 lib 파일 ~15개로 잘 돌지만, ① 신규 기능(선호 계량화·푸드체이닝·노출)이 토대를 공유하고 ② 통합맵대로 6+1 모듈로 정리하면 응집도·테스트·확장이 쉬워집니다. 한 번에 갈아엎지 않고 아래 원칙으로 한 칸씩 옮깁니다.

🔧

증상 개선 — 구현 · 기능 · 설계 변경 방향 (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_BUGP2 판단 권한 단일화의 1번 산출물. planForlockedPlan/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_GAPP3 폐루프 닫기. cron에 advanceProgress 호출 + curriculum_progress upsert(현재 write 0건) 배선, runWeeklyPlanningprogress·focusHistory 주입으로 피로 캡 활성화. 그래야 "환경이 안 먹히면 음식으로 진행"(curriculum.ts:200-215 limping/stall 피벗)이 데이터로 닫혀 닻 고착이 풀린다. 새 테이블 불필요 — 기존 curriculum_progress(sql/2026-06-12) 재사용.P3
두뇌가 상위 제약(lever·push캡)을 입력 단계에서 보지 못해 override를 남발route.ts:619-626 buildBrainContext 입력 · DESIGN_GAPP3 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_BUGP0 무결성에 1줄 추가. 신규 닻 upsert 직후 같은 child_id의 직전 week_key active 행을 status='archived'로 강등 → 1-active-per-child 불변식·어드민 가시성 확보. loadAnchorweek_key로만 로드하므로 daily 동작 무영향·저위험.P0
이 모듈(리팩터 전략)이 근본적으로 무엇을 바꿔야 하는가 — 앵무새의 구조적 뿌리는 개별 모듈 버그가 아니라 판단 권한이 둘(주간 닻 vs 일간 두뇌)로 쪼개져 있는데 둘 사이 계약이 없고, 주간→일간 진행 폐루프가 끊겨 닻이 영영 고착되는 아키텍처 결함이다. P2의 planFor 분리는 "파일 이동"이 아니라 "닻이 상위 권한이고 두뇌는 닻 안에서만 시나리오를 고른다"는 단일 판단 권한 계약을 코드로 강제하는 것이 본질이다. P3의 child_daily_state 배치는 "캐시"가 아니라 닫히지 않은 주간 진행 폐루프를 닫는 단일 진실이어야 한다.
⚠️ 의존 순서 경고 — 개별 캡·필터·게이트(거부 결핍필터·food 쿨다운·동군 곁들임 게이트·growth 배선·mirror 쿨다운·구조 유사도 검증)는 P2 권한 단일화가 선 뒤에 각 모듈 단계에서 박아야 효과가 남는다. 권한 단일화 전에 캡만 넣으면 두뇌 override가 그 캡을 다시 무력화한다(=현재 route.ts:647 STRUCTURAL 가드가 nutrient-gap override로 탈출하는 것과 동일 함정).
P0 · weekly_plans 1-active-per-child 불변식 SQL/한 줄 · 저위험
신규 닻 upsert 직후 직전 week_key active 행 → archived 강등. daily는 week_key 로드라 동작 무영향.
리스크: 거의 없음(표시 전용 값 정리). 테스트: 같은 child 2주 적재 후 active=1 확인. 롤백: archive update만 제거(daily 무관).
P2 · 판단 권한 단일화 (닻 상위 · 두뇌 종속 계약) ⭐ 앵무새 구조 근본 · 실질 1순위
planFor 판단부를 ①로 분리 = 계약 박기(lockedPlan/anchor 인자·시나리오만 교체·비-food 주 override 캡). 완료정의 = 두뇌가 re-exposure-timing(치킨)을 골라도 발행 plan.target이 닻(콩류)/환경 프레임으로 유지됨을 골든 테스트로 보증.
리스크: 높음 — 라이브 두뇌 발행 경로. 테스트: 닻 lever=environment·target=콩류 골든 케이스 + 아린 카나리아 1~3일. 롤백: override 게이트 커밋만 revert(planFromWeekly 폴백 유지).
P3 · 주간 진행 폐루프 닫기 + CoachContext 단일 계약 신규 배선
advanceProgresscurriculum_progress upsert + focusHistory 주입(피로 캡 활성)으로 닻 고착 해소. 두뇌 입력을 CoachContext 동결 객체로 전환해 상위 제약(lever·push캡)을 입력 단계에서 노출.
리스크: 중간 — upsert 조용한 거부 선례(route.ts:545) → upsert 결과 검사·폴백 필수. 테스트: limping/stall 시 environment→food 피벗이 닻 goals에 영속되는지. 롤백: 배치 단계 플래그 OFF→재계산 경로 복귀.
P4 · 정합·정리 후속
권한 단일화·폐루프가 선 뒤, 산발적 signals 잔재 제거 · 휴면 게이트 승격/삭제 확정 · 계약 위반 정적 검사.
리스크: 낮음. P0~P3 안정화 이후에만.
원칙 단서(why 보강) — behavior 보존은 원칙이지만 판단 권한 이중화·폐루프 부재 둘은 보존 대상이 아니라 교정 대상이다. 이 둘만은 동작이 바뀌어야 정상(닻이 일간을 통제). 리네임·배럴의 behavior 보존과 명확히 구분한다.
1

목표 상태 — Before / After

BEFORE (지금)
lib 파일 ~15개가 평면으로(coachRecos·coachFacts·coachBrain·coachWeekly·nutrition·snack·progress·curriculumUnits·foodGraph·kitGuide·ingredientFreq·reexposure·coachDaily·coachChronic + 휴면 coachMaterials·comboMatrix·recoEvidence). 식재료=배열, 멱등성 제약 없음.
AFTER (목표)
코칭 엔진 1 + 콘텐츠 모듈 6(①신호선택기 ②추천엔진 ③사실·거울 ④영양평가 ⑤진척·커리큘럼 ⑥작문기) + 조회 헬퍼 ⑦ + 데이터 레이어(추천 네트워크·아이 현황 스토어). 식재료=정규화 행, CoachContext 단일 계약, 야간 배치 2단계.
상세 통합맵·입출력·뇌/손 프롬프트 매핑 = 통합 아키텍처 지도 §4. 데이터 무결성 진단 = DB 스키마 감사(별도).
2

단계 로드맵 — P0 → P4

의존 순서대로. P0(무결성)·P1(선호 토대)이 신규 기능의 전제라 먼저.

Phase 0 · DB 무결성 핫픽스 선행 · 위험 차단
DB 감사 HIGH 정리: 멱등성 UNIQUE 제약(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 형상관리.
왜 먼저: 제약이 없으면 정규화·집계가 중복 위에 서고, 선호 계량(끼니수·거부율)이 부풀음. 코드 변경 0, SQL만.
Phase 1 · 선호 계량화 모듈 + 끼니 정규화 + 수용 포착 신규 토대
meal_log_items(끼니→음식 1행·수용 5단계·거부사유) 추가 + 기록 UX 원탭 포착 + child_food_pref(다축 롤업) + 콜드스타트 게이트. 읽기 전용 계산부터(라이브 무영향).
왜: 선호 계량화·푸드체이닝·노출 추적 셋의 공통 토대. 이게 비면 뒤가 허상(Day1 '잘 먹는 소고기' 문제).
Phase 2 · 모듈 경계 정리 (코드 재배치) behavior 보존
①신호선택기 셀렉터 파사드(selectScenario·pickActionByBrain·planFromWeekly 단일 진입) · ②추천엔진 휴면 코드 정리(coachMaterials·recoEvidence 흡수/삭제 결정, comboMatrix→괴식 유틸) · ③④⑤ 배럴(index)로 묶기 · ⑥작문기 = coach.ts에서 판단부(planFor)는 ①로 분리하고 작문만 남김.
왜: 파일 이동·배럴·rename은 동작 동일(tsc/test로 보증). ⑥의 coach.ts 분해가 가장 큼 → 마지막·테스트 두껍게.
Phase 3 · 푸드체이닝 모듈 + 아이 현황 스토어 + 배치 2단계 신규 기능
⑦ 조회 헬퍼 위에 푸드체이닝 모듈(2축 체인·candidates/recommended) + child_daily_state 야간 머티리얼라이즈(1단계) → 편지 워커(2단계)가 CoachContext 읽기. 아린 카나리아→전자녀. ⭐추천 두 트랙(supply 결핍·challenge 사촌/공출현) + weekly_plans.reco_balance(주간 밸런스) + 뇌 decision.recoTrack(일간 회전). ⭐BMI·성장곡선 연동 3대영양소 매크로 트랙(저체중·성장더딤 → 탄·단·지 식재료 우선) + weekly_plans 3대영양소 카테고리(격주·2주 연속 금지 — 측정 빈도 낮아 과잉 잔소리 방지).
왜: P1 선호·P2 경계가 선 뒤라야 푸드체이닝이 깨끗한 입력을 받음. 배치 2단계로 1만 명 확장.
Phase 4 · 정합·정리 후속
도먼트 코드 제거 · 질의 대상 jsonb→정형 컬럼(필요한 것만) · 도메인 값 enum/CHECK · date-util 공용 분리 · 추천코드 도메인 이중모델 통합.
왜: 급하지 않은 정리. 분석 쿼리가 실제로 들어올 때만 jsonb 분리(과잉정규화 경고).
🗄️

JSON → SQL 전환 결정표 (무엇을 SQL/캐시/코드로)

"전부 SQL"이 아니라 SSOT=DB · 핫패스/클라=파생 캐시 · 로직=코드. 정적 JSON 스냅샷을 쓰던 이유(빌드 판정·클라 사용·속도)는 인정하되, 자라는 데이터는 DB를 SSOT로 옮겨 푸시 없이 갱신되게 한다.

데이터/자산지금→ 가야 할 곳이유 · 단계
food-graph (사촌·궁합)정적 JSONSQL(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 + 생성 캐시/캐시 APIDB가 진실, 클라·핫패스는 캐시. gen-grade가 JSON 재생성 대신 컬럼 업데이트 → P4
meal_logs 식재료배열 text[]정규화(meal_log_items)1NF·per-식재료 집계(선호/노출) → P1
cooking-amounts·cuisine-guide·curated 콘텐츠정적 JSONJSON 유지거의 안 변함·ROI 낮음·관계질의 불요
매퍼 규칙(menuMapCore boostFromName)·시나리오·유닛코드코드 유지절차 로직(테이블화 부적합)·푸시 필요는 당연. MENU_MAP 사전 부분만 테이블화 검토
왜 정적 JSON이 '나쁜 게 아니라' 단계적 전환인가 — 정적 스냅샷은 ①빌드타임 사람 판정 게이트 ②클라(브라우저) 즉시 매핑 ③서버리스 콜드스타트 속도 ④git diff/롤백에 유리. 그래서 참조·핫패스용 캐시는 유지하고, SSOT만 DB로 올린다. "100% 매요청 SQL"은 편지/메뉴마다 수십 쿼리라 느림 → DB SSOT + 캐시가 정답.
단계 매핑: P0(제약·무결성) → P1(meal_log_items·learned_menus 정규화) → P3(COACH_GRAPH_SQL ON·ingredient_edges 재집계·정합검증) → P4(vocab/등급 DB SSOT·질의대상 jsonb 정형화). ⚠️ graphSource ON 조건 = SQL 테이블 적재 + 엣지 재집계 + JSON과 정합 확인(지금 SQL==JSON이라 켜도 무효).
3

모듈별 리팩터 카드

모듈합치는 것 · 작업리스크테스트
① 신호선택기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)는 모듈로 흡수하지 않음 — 별도 계층/유틸 유지(통합맵 '합치면 안 되는 것').
4

리스크 · 롤백

착수 전 결정 필요 — ① 빌드 순서(P0→P1 먼저) 동의 ② 휴면 3종 승격/삭제 ③ meal_log_items 정규화 + 수용 5단계 포착 UX 범위 ④ ⑥ coach.ts 분해를 언제(가장 큰 작업).