🔀 HANDOFF · 코칭엔진 ↔ 데이터 세션

추천 데이터 SQL 전환
코칭엔진 핸드오프

추천 네트워크(사촌·궁합·음식×식재료)가 정적 JSON → SQL로 전환된다. 추천 로직은 그대로 두고, "데이터를 어디서 읽는가"만 graphSource 한 겹으로 추상화해 무중단 대비한다. 캐시 3층 + 7테이블 모델 + 불변 계약 + 분담을 양 세션이 참조한다.

⚠️ 임시 참조용 — 전환 완료 후 정본 통합
← 문서 허브 · 엔진·데이터 명세 · 2026-06-14 · 연관 파이프라인·ERD
🎯 한 줄 요지

코칭엔진은 graphSource.ts 한 모듈로 JSON import을 격리한다 → 소비 함수(strongPairsOf·scoreCombo·rankIngredients)는 byte 무변경. 데이터 세션이 그 뒤를 JSON→SQL로 바꿔도 엔진은 그대로 돌고, 추천이 매일 누적된 사촌 그래프로 실시간 복리된다.

1

왜 — 빌드 lag 제거 + 복리

지금 추천 네트워크는 빌드 JSON이라 "빌드 돌려야만" 갱신된다. SQL로 옮겨 부모 입력이 매일 동시출현으로 누적되게(복리) + 비도감 식재료(아귀 등)도 사촌 다리가 되게 한다.

추천 '알고리즘'은 안 바뀐다 — '데이터 출처'만 바뀐다. 식재료↔식재료 사촌/궁합(ingredient_edges)·음식×식재료(dish_ingredient_stats)가 SQL에서 매일 누적되면, 푸드체이닝(잘 먹는 것 → 닮은 새 식재료)이 빌드 없이 저절로 촘촘해진다. 예: 감자·돈까스 좋아하는 아이 → 단호박(감자 사촌·비타민A) 크로켓 + 생선까스(튀김 전이·생선 갭) 도출이 매일 정밀해짐.

2

7테이블 모델 (데이터 세션이 DDL·이관·스냅샷 담당)

ingredients가 허브(원천 registry) — 나머지가 전부 이걸 참조. 영양은 ingredients.nong_code → nong_foods JOIN(중복 X).

meal_logs 기존 SQL
부모 끼니·ate_well·refused·menus[]·ingredients[]. 매일↑ · 아이별 선호 강도 원천
learned_menus 기존 SQL
menu PK → ingredients[]·hits. 분해 사전·복리
nong_foods 기존 SQL
식재료 영양 19종·3,366. 영양 원천
ingredients 신규·허브
nm PK · FK nong_code→nong_foods · freq. 분해에서 나온 식재료 유니버스(아귀 등 비도감 포함)
dogam 신규
nm PK FK→ingredients · grade·💎·safety·season·cuisine·age_meta·status. 편식 치료 타깃 큐레이션
ingredient_edges 신규 ← food-graph.json
a·b FK→ingredients · kind(pair/bridge/tray)·lift·grade·strength·count·src. 사촌/궁합
dish_ingredient_stats 신규 ← kit-dish-matrix.json
dish·ingredient FK→ingredients · count·score. 음식×식재료

관계: ingredients = 허브. dogam·ingredient_edges·dish_ingredient_stats·learned_menus 전부 ingredients 참조. 추천 노드 자격 = ingredients(전체) · 치료 타깃 = dogam(부분집합) — 푸드체이닝은 ingredients 위에서.

3

⭐ 캐시 아키텍처 — 3층

엔진엔 동기 경로(클라/SSG: 도감 페이지 PersonalBridge)와 비동기 경로(크론 편지)가 둘 다 있다. 순수 라이브 SQL은 동기 경로를 깨므로 3층으로 간다.

L1
SQL

원천 (매일 누적)

ingredient_edges·dish_ingredient_stats·ingredients·dogam. 부모 입력 → 동시출현 누적 = 복리.

▼ 야간 export
L2
스냅샷

야간 스냅샷 JSON (현 JSON과 '동일 shape')

SQL → public/food-graph.json·kit-dish-matrix.json export. 파일이 'gen 스크립트 산출'에서 'SQL export'로 바뀔 뿐 경로·shape 동일 → 클라/SSG/동기 import는 무변경.

▼ 런타임 로드
L3
인메모리

서버 인메모리 캐시 + 폴백

크론/API는 모듈 로드 시 1회 적재(현 foodGraph ADJ Map 패턴 그대로) + 선택적 SQL 직접 read(라이브). + affinity_cache(이미 SQL) = 스냅샷에 없는 조합 런타임 폴백.

4

graphSource 계약 (코칭엔진 세션의 핵심 작업)

JSON import을 직접 하지 말고 데이터 액세스 모듈 한 겹 뒤로 숨겨라. 그러면 SQL 전환 시 이 모듈 내부만 바뀌고 엔진 전체 무변경(1파일 격리).

// 신규 lib/graphSource.ts — 반환 shape = 현 JSON shape (이게 두 세션의 접점 계약)
export function getEdges(): RawEdge[]        // food-graph.json 동일 shape (스냅샷 or SQL)
export function getDishMatrix(): KitMatrix   // kit-dish-matrix.json 동일 shape
export function getIngredient(nm): { nong, nutri, grade, ... }

// 그러면 소비 모듈은 'import json' → 'graphSource.getX()' 한 줄만 교체:
//   foodGraph.ts / comboMatrix.ts / kitGuide.ts / nutrition.ts
// 소비 함수(strongPairsOf·scoreCombo·rankIngredients)는 byte 무변경. 추천 로직 안 건드림.
// graphSource 내부(스냅샷 JSON ↔ SQL ↔ 캐시 TTL)는 데이터 세션과 함께 확정.

너가 '지금' 준비할 것 (SQL 생기기 전 선제)

  • (a) 아래 5개 JSON import을 graphSource.ts 한 모듈로 모아 추상화(지금은 그 안에서 기존 JSON 그대로 import) → 전환 시 1파일만 바뀜.
  • (b) selectDailyMaterialsmeal_logs아이별 선호 강도(ate_well 빈도·거부)를 랭킹에 직접 쓰도록 입력 계약 정리 — 전환 후 ingredient_edges(전역) × meal_logs(개인선호) × nong_foods(갭) 결합이 핵심.
  • (c) freqMap(ingredient-recipes.json)도 graphSource 경유로 통일(크론 fetch도 여기로).
5

현재 읽기 경로 (실측 — 정확히 이걸 graphSource로 격리)

파일:라인읽는 것 / 방식
lib/foodGraph.ts:9import './food-graph.json' — edges(a,b,kind,lift,grade,strength,count,src,verified,tray) · 동기
lib/comboMatrix.ts:13import './kit-dish-matrix.json' — dishes,cells,scores · 동기
lib/kitGuide.ts:7import './kit-dish-matrix.json' · 동기
lib/nutrition.ts:139import './nutrient-map.generated.json' · 동기
lib/affinity.ts:8import '@/public/ingredients-light.json' · 동기
cron/coach/route.ts:100fetch('/ingredients-light.json') · 런타임
cron/coach/route.ts:106fetch('/ingredient-recipes.json') (freqMap) · 런타임

소비 함수 계약(★시그니처·반환 shape 불변): neighborsOf·strongPairsOf·verifiedCousinsOf·scoreCombo·isComboOk·dishesForIngredient·popularDishesFor·pickFoodReco·buildRecoFacts·selectDailyMaterials.

6

분담 — 접점은 graphSource 반환 shape

🟠 데이터/SQL 세션 (나)

  • DDL 4종(ingredients·dogam·ingredient_edges·dish_ingredient_stats)
  • 기존 JSON → SQL 이관 스크립트
  • 야간 스냅샷 export job(SQL → 동일 shape JSON)
  • 분해 검증을 CANON_VOCAB(도감) → nong_foods(유니버스)로 확장
  • 크론 신규 식재료 자동 INSERT into ingredients

🟣 코칭엔진 세션 (너)

  • graphSource.ts 추상화 신설
  • foodGraph/comboMatrix/kitGuide/nutrition의 import → graphSource 교체
  • selectDailyMaterials의 meal_logs 개인선호 결합
  • 소비측 테스트 green 유지(835)

접점 = graphSource.ts의 반환 shape(= 현 JSON shape). 이걸 계약으로 고정하고 각자 양쪽을 맞춘다.

7

불변 계약 · 하지 말 것

✅ 절대 불변

소비 함수 시그니처·반환 shape · RawEdge 필드명(a,b,kind,strength,lift,grade,count,src,verified,tray) · PAIR_MIN_STRENGTH=2 · CELLS_MIN=8 · lift 임계(strong 1.2/med 1.0)·grade 의미 · 괴식/환각 가드(strongPairsOf만 추천·validatedCombos≥1·미역국+당근 차단·떡+달걀 weak) · 835 테스트 green · '재료=결정론·문장=LLM 자유'.

  • 추천 알고리즘/임계/가드를 'SQL 전환 김에' 바꾸지 마라 — 데이터 출처만 바꾼다. 로직 변경은 별건.
  • JSON import을 여기저기서 직접 하지 마라 — 반드시 graphSource 경유(전환 시 누락 방지).
  • dogam과 ingredients를 혼동 마라 — 노드 자격=ingredients(전체), 치료 타깃=dogam(부분집합). 푸드체이닝은 ingredients 위.
  • 스냅샷 shape을 현 JSON과 다르게 만들지 마라 — 테스트·동기경로 다 깨짐. 필드 추가는 OK, 기존 필드명/구조 변경 금지.
  • 영양을 dogam에 중복 저장 마라 — ingredients.nong_code → nong_foods JOIN.
8

작업 순서

  1. graphSource.ts 추상화(현 JSON 그대로 감싸기) 코칭세션 — 즉시 가능, SQL 무관
  2. DDL 4종 작성 → 이사님이 Supabase에서 실행 데이터세션이사님 실행
  3. JSON → SQL 이관 스크립트(ingredients 먼저 → dogam FK → edges/stats) 데이터세션
  4. 야간 스냅샷 export job(SQL → 동일 shape JSON) + graphSource를 스냅샷/SQL로 스위치 데이터세션코칭세션
  5. 분해 검증 nong_foods로 확장 + 크론 자동 INSERT into ingredients 데이터세션
  6. selectDailyMaterials 개인선호 결합 + 835 테스트 green 확인 → 컷오버 코칭세션
📌 컷오버 안전

스냅샷이 현 JSON과 동일 shape이면 — 컷오버는 "JSON 생성 주체가 gen 스크립트 → SQL export로 바뀌는 것"뿐이라 엔진·테스트 무영향. 동기 경로는 스냅샷, 비동기(크론)는 SQL 직접, 누락은 affinity_cache 폴백. 단계별로 롤백 가능(스냅샷을 옛 gen 산출로 되돌리면 즉시 원복).