비개발자용 — 이 모듈이 무엇을, 왜 하나
이 모듈은 코칭 엔진이 "이 식재료, 어디에 어떻게 넣으면 아이가 잘 먹을까?"를 물었을 때 대답해 주는 '음식 인맥 검색대'입니다. 직접 추천을 결정하지는 않습니다. 다른 코칭 모듈이 편지를 쓸 때 필요한 '사실' 네 가지를 결정론(매번 같은 답)으로 찾아 줍니다. ① 궁합(같이 차리면 자연스러운 짝, 예: 두부+다시마) ② 검증된 사촌(맛·식감이 닮아 한 걸음 갈아탈 만한 대체 음식, 예: 감자→고구마) ③ 음식 적합도(이 식재료를 넣기 좋은 요리, 예: 두부→순두부·계란찜) ④ 급식 흔함도(이 식재료가 또래 급식 표준식단에 얼마나 자주 오르나). 이 넷은 우리가 모은 실제 레시피·급식 데이터에서 미리 계산해 둔 표를 그냥 '읽어 오는' 것뿐입니다.
왜 필요하냐면, 코칭의 핵심 전략이 "잘 먹는 음식에 결핍 식재료를 슬쩍 섞기" 또는 "잘 먹는 음식의 닮은 사촌으로 한 걸음 갈아타기(푸드체이닝)"인데, 아무 조합이나 섞으면 '미역국에 당근'처럼 아이가 질색하는 괴식이 나옵니다. 그래서 추천하기 전에 반드시 이 검색대에서 "이 짝이 실제 레시피에 충분히 같이 나왔나? 닮은 사촌이 사람이 검증한 것인가? 이 요리에 넣어도 자연스럽나?"를 확인합니다. 즉 코치가 상상으로 조합을 지어내지 못하게 막는 '현실 검문소'입니다.
① 약한 궁합 차단: 궁합(pair)은 등급 'strong'(없으면 강도 ≥ 2)만 추천에 씁니다. '떡+달걀'(lift 0.72, 강도 1) 같은 약한 짝이 괴식 사고를 낸 적이 있어,
strongPairsOf가 약신호를 걸러냅니다. 이 임계값(2)은 추천을 쓰는 모든 모듈이 같은 문을 통과하도록 통일돼 있습니다. ② 검증된 사촌만 갈아타기: 푸드체이닝에 쓰는 사촌(bridge)은 verified 표시가 붙은 것만 씁니다(verifiedCousinsOf). ③ 곁들임 2차 필터: 부모에게 "잘 먹는 X 곁들이면 좋아요"로 나가는 짝은 식판 동시출현(tray)을 뺀 garnishPairsOf 위에, 매운/김치류·날곡물·교차괴식(단 과일·우유 ↔ 짠 생선·콩)을 한 번 더 거릅니다(safeGarnishOf). ④ 급식 흔함은 정직하게: 급식 표준식단(youa-freq)에 충분히 자주 오르는 재료만 '익숙하다'고 안심시키고, 미수록은 빈 문자열로 둡니다(0회를 '흔함'으로 위장 금지). 이 모듈 자체는 결핍 판정·강요 금지 같은 '판단'은 하지 않습니다 — 그건 상위 모듈의 몫이고, ⑦은 그 판단에 필요한 '사실'만 안전하게 공급합니다.구현된 내용 — 실제 함수·데이터흐름·입출력 (코드 실측)
이 모듈의 코드 본체는 lib/coachRecos.ts(추천 사실 조립)와 그 하위의 그래프 질의 헬퍼(lib/foodGraph.ts·lib/kitGuide.ts), 그리고 휴면 중인 괴식 게이트(lib/comboGuard.ts·lib/comboMatrix.ts)입니다.
그래프 질의 헬퍼 (lib/foodGraph.ts — 결정론 조회의 뿌리)
| 함수 (file:line) | 역할 · 반환 (실측) |
|---|---|
neighborsOf(nm) foodGraph.ts:36 | 한 식재료의 모든 이웃을 Neighbor[]로 반환(foodGraph.ts:16 {nm,kind,strength,basis,count?,lift?,grade?,verified?,tray?,src?}). 정렬 = bridge(사촌) 먼저 → grade(strong>medium>weak) → strength → count. |
strongPairsOf(nm) foodGraph.ts:49 | 곁들임용 강한 궁합만 — kind==='pair' && (grade ? grade==='strong' : strength>=PAIR_MIN_STRENGTH). PAIR_MIN_STRENGTH=2(line 19). 모든 소비자(coachRecos·coachMaterials·comboMatrix·comboGuard) 일원화. |
verifiedCousinsOf(nm) foodGraph.ts:54 | 푸드체이닝 chain용 검증 사촌만 — kind==='bridge' && (verified===undefined || verified===true). 이 모듈의 challenge-다리 핵심. |
garnishPairsOf(nm) foodGraph.ts:65 | strongPairsOf에서 식판 동시출현(src!=='tray')만 — tray(김치+요구르트류)는 맛 궁합이 아니라 '식판 공출현'이라 곁들임 권유에서 제외. strongPairsOf(TR-02 골든)는 byte 무변경 보존. |
곁들임 안전 필터 + 조리 표기 (lib/coachRecos.ts)
| 함수 / 상수 (file:line) | 역할 (실측) |
|---|---|
safeGarnishOf(nm) coachRecos.ts:165 | 곁들임 추천 전용 — garnishPairsOf 위에 ① 매운/김치류(isSpicyIngredient·KIMCHI_EXTRA) ② 날곡물(STAPLE_FORMS) ③ 교차괴식(garnishConflict: 단 과일·우유류 ↔ 짠 생선·콩/두부)을 추가 차단. 부모 "곁들이면 좋아요" 경로 전용. |
garnishConflict(a,b) coachRecos.ts:160 | 무방향 교차괴식 판정 — isSweetBland(과일·유제품) ↔ isSavoryProtein(생선·해산물·콩). 짠↔짠(두부+멸치)은 허용. |
cookedName(nm) · stapleDisplay(nm) coachRecos.ts:60·38 | 조리법+식재료 표기. COOK_ADJ(coachRecos.ts:42)로 익혀야 하는 재료에 접두('찐 단호박'·'삶은 검은콩'), 주식 곡물은 먹는 형태(밥·빵·면·떡), 그 외는 원래 이름. 생물 오인 방지. |
cleanDishName(raw) coachRecos.ts:64 | NEIS 원본명 정제 — 접두 분류태그'(간식)'·접미 부재료'&쌈장'·용량 괄호'(200ml)' 제거. inappropriateDish(line 76)로 매운·짠지·견과·튀김 차단. |
popularDishesFor(ing, freqMap?) coachRecos.ts:80 | 식재료의 '가장 많이 쓰이는 실존 음식' 최대 2개. freqMap(또래 급식 빈도, freq≥4) 우선 → dishesForIngredient(ing,2)(count>0) 폴백. 주식=먹는 형태, 과일=빈 배열(간식채널). |
급식 흔함도 — youa-freq (이번 세션 ⑤거울/A어드민근거의 안심 톤 소스)
이 모듈의 '급식 빈도' 소스는 youa-freq.json(dietary4u 영유아 표준식단 등장률%)입니다. coachRecos.ts:176~216에 인덱싱·조회가 박혀 있습니다.
| 함수 / 상수 (file:line) | 역할 (실측) |
|---|---|
YOUA_RANK (IIFE) coachRecos.ts:194 | youa 전 항목으로 동률 안전 순위 산출 — rank = (등장률 더 큰 항목 수)+1, topPct = max(1, round(rank/total*100)). 모집단=전체(분모 인위 축소 금지). |
youaRankOf(ing) coachRecos.ts:207 | YOUA_RANK[ing] ?? YOUA_RANK[ING_YOUA_ALIAS[ing]] ?? null. ING_YOUA_ALIAS(line 184)로 표기 봉합(요거트→요구르트·검은콩→콩(대두)·현미→멥쌀). 미수록=null(0을 꼴등으로 위장 금지). 연어는 의도적 미매핑. |
youaReassuranceFor(ing) coachRecos.ts:213 | 부모 안심 문구 — pct >= YOUA_COMMON_PCT(50)이면 '또래 급식에도 자주 오르는 익숙한 재료', 아니면 빈 문자열(서열·등수·% 노출 금지). buildRecoFacts (a) 줄의 evid 절에 직조됨. |
coldStartSeed(servedIngredients, max=6) coachRecos.ts:218 | 콜드스타트 앵커 사다리 — Tier2(자주 차려진=servedIngredients) → Tier3(YOUA_TOP 급식 고빈도). 식품군 인식 + 과일(간식채널) 제외. 확신 liked가 적을 때 challenge-다리의 출발점(앵커)을 보강(cron이 confidentLiked와 합쳐 likedSeed로 주입). |
추천 사실 조립 — buildRecoFacts / buildIngredientPool (진입점)
| 함수 (file:line) · 시그니처 | 역할 (실측) |
|---|---|
buildRecoFacts({likedIngredients, target?, targetIngredient?, freqMap?, suppressCousins?}) coachRecos.ts:276 → RecoFacts{target,cousins[],lines[],text} | 네트워크 헬퍼를 묶어 호출하는 진입점. (a) 타깃 줄(line 285~298): targetIngredient(주간 풀에서 일일 회전) 우선, 그 인기 음식 + 잘 먹는 것과의 safeGarnishOf 곁들임 + youaReassuranceFor 안심 절. (b) 사촌 줄(line 303~318): 잘 먹는 식재료의 verifiedCousinsOf(+그 사촌 인기 음식)·궁합. ⭐ F-18(line 301~303): suppressCousins=true면 (b) 전체 생략 — 슬롯이 이미 음식 타깃을 정한 날 part(b)의 사촌(감자→두부)이 슬롯 음식과 경쟁해 본문이 두부로 회귀하던 근원(랄프위검 rank1) 봉합. |
buildIngredientPool({signals, likedIngredients, freqMap?, max=5}) coachRecos.ts:237 → IngredientPool{pool[],mode,reason} | 주간 추천 식재료 풀 5개. 결핍 상위 3군만(line 244~246 심각도순·채소 가점). mode: red≥2 → supply(결핍군 대표 라운드로빈) / red0·결핍≤1 → challenge(cousinsOfLiked() 푸드체이닝 + 안 먹는 도전) / 그 사이 → mixed. cousinsOfLiked(line 250)이 verifiedCousinsOf로 사촌 풀을 만듦. |
pickFoodReco({target, likedIngredients, freqMap?, seed?}) coachRecos.ts:103 → FoodReco{group,food,via,...} | ③거울용 구체 추천. 대표 식재료를 seed로 회전 → liked → pair(safeGarnishOf) → chain(verifiedCousinsOf) → dish → plain 우선순위. via로 프레이밍 결정. |
weeklyExposureTarget(signals, liked, seed) coachRecos.ts:128 | 주간 노출 1개 고정(푸드체이닝 일관성). 끼니 채널·채소 우선·도전(안 먹는 것) 우선. |
괴식 게이트 (lib/comboGuard.ts · comboMatrix.ts — ⚠️ 라이브 미배선·휴면)
| 함수 (file:line) | 역할 · 상태 (실측) |
|---|---|
dishIngredientFit(dish,ing) · validCombos(dishes,ings) comboGuard.ts:27·51 | 음식×식재료 게이트 — dishesForIngredient(ing,0) 조회 후 score>=COMBO_THRESHOLD(2)만 ok. 미수록=금지(보수적). validCombos=LLM 후보 화이트리스트. ⚠️ 외부 호출 0건(휴면). |
ingredientPairFit(a,b) comboGuard.ts:41 | 식재료×식재료 — strongPairsOf(a).find(b). dish는 노드 아니라 ok=false(시그니처로 경계 강제). ⚠️ 휴면. |
scoreCombo(dish,ing) · isComboOk(dish,ing,2) comboMatrix.ts:31·47 | matrix(정성채점 0~3) → pair → cells(약신호) → none. borderline 게이트: m===2 && cells<CELLS_MIN(8)이면 score 1로 강등(떡+달걀 차단). score 3·실증된 2·국+당근(cells49)은 유지. ⚠️ 라이브 추천 경로에 미배선(휴면). |
• 선호계량화 → ⑦ 직접 함수 아님(
lib/preferenceQuantification.ts·route 소관). 단 cron이 confidentLiked를 산출해 ⑦의 buildRecoFacts/buildIngredientPool 앵커(likedIngredients)로 주입 — ⑦의 출력 다양성이 이 앵커 폭에 종속(천장4의 뿌리).• F-18 슬롯본문봉합 →
buildRecoFacts suppressCousins 인자(coachRecos.ts:276·303)로 ⑦에 직접 구현됨. cron(route)이 slotFood면 suppressCousins:!!slotFood 전달(cron/coach/route.ts:867).• 콜드스타트 사다리 →
coldStartSeed(coachRecos.ts:218) ⑦ 본체. cron이 likedSeed=[...confidentLiked, ...coldStartSeed(servedTop,6)]로 합성(route.ts:373).• A 어드민 근거 →
youaReassuranceFor의 '익숙한 재료' 안심 절이 (a) 줄 근거 텍스트로 흐름. buildIngredientPool.reason('supply(red 2·결핍군 3)')이 어드민 노출.• hardStall 피벗 · #5거울쿨다운 · #6도입회전 · #7macro · P0-D 프로브 → ⑦에 함수 없음(⑤커리큘럼·③거울·⑧주간계획·route 소관). 이 문서에서 ⑦ 기능으로 기술하지 않음.
설계 아키텍처 — 데이터 파이프라인 시각화
입력 테이블(JSON 스냅샷) → 그래프 질의 헬퍼 → 추천 사실 조립 → CoachContext 키. 결정론(매번 같은 답)이고 순수 함수(fs/HTTP 불사용).
(edges: pair·bridge·tray)
kit-dish-matrix.json
(scores 0~3·cells)
youa-freq.json
(급식 등장률%)
ingredient-recipes
(freqMap·호출자 주입)
neighborsOf↳
strongPairsOf (s≥2)↳
verifiedCousinsOf ✔↳
garnishPairsOf (¬tray)dishesForIngredientyouaRankOf / youaReassuranceForsafeGarnishOf매운·날곡물·교차괴식 차단
cookedName찐 단호박·삶은 검은콩
cleanDishNameNEIS 원본명 정제
buildRecoFacts(a)타깃줄 (b)사촌줄
buildIngredientPoolsupply/challenge/mixed
coldStartSeedbridgeFactsrecoIng · recoPoolrecoModerecoCandidatesbuildRecoFacts({ likedIngredients, target, targetIngredient, freqMap, suppressCousins })
│
├─(a) 타깃 줄 ──────────────────────────────────────────────────────────────────
│ targetIngredient (주간 풀 일일 회전) ──▶ cookedName() "삶은 검은콩"
│ popularDishesFor(ing, freqMap) ──▶ "또래 인기: 콩자반·검은콩밥"
│ safeGarnishOf(ing) ∩ likedSet ──▶ "잘 먹는 두부 곁들이면 좋아요"
│ youaReassuranceFor(ing) (pct≥50) ──▶ "또래 급식에도 자주 오르는 익숙한 재료"
│ └─ break (오늘 추천 식재료 1개만)
│
└─(b) 사촌 줄 ── if (suppressCousins) → [] ◀── ⭐F-18: 슬롯이 음식 타깃 정한 날 생략
for ing of liked (김치류 제외):
verifiedCousinsOf(ing) \ likedSet ──▶ "사촌 찐 단호박(또래 인기: 단호박죽)" ◀── challenge 다리
safeGarnishOf(ing) \ likedSet ──▶ "궁합 계란·당근"
└─ lines ≥ 4 면 중단
│
▼
RecoFacts{ target, cousins[], lines[], text }
│
cron: bridgeFacts = _noFood ? '' : buildRecoFacts(...).text (두뇌 useFood=false면 '')
▼
CoachContext.bridgeFacts → ⑥ 작문기(손) 그라운딩comboGuard.validCombos·comboMatrix.isComboOk(dish×ingredient 임계 2)는 존재하지만 위 라이브 파이프라인에 연결돼 있지 않습니다(외부 호출 0건). 현재 괴식 방어는 safeGarnishOf의 식품군 휴리스틱 + strongPairsOf 임계뿐이고, dish×ingredient 정합 게이트는 휴면입니다(⑤ 참고).작동 예시 — 아린(5세) 실데이터 before/after
아린: 콩류·채소 결핍, 확신 liked = 감자·계란(2개), 콜드스타트 보강 후 likedSeed에 미역·두부·쌀 등 합류. 오늘 슬롯이 콩류 회전 식재료 '검은콩'을 잡은 날.
buildRecoFacts({
likedIngredients: ["계란","감자","미역","두부","쌀"],
target: "콩류",
targetIngredient: "검은콩", // 주간 풀에서 일일 회전
freqMap: { "검은콩":[{name:"콩자반",freq:31}],
"단호박":[{name:"단호박죽",freq:18}] },
suppressCousins: true // ⭐ F-18: 오늘 슬롯이 음식 타깃을 이미 정함
})
// 내부 단위 헬퍼 호출:
// verifiedCousinsOf("감자") → [{nm:"고구마",kind:"bridge",verified:true},
// {nm:"단호박",kind:"bridge",verified:true}]
// safeGarnishOf("검은콩") ∩ liked → [{nm:"두부"}]
// youaReassuranceFor("검은콩") → "또래 급식에도 자주 오르는 익숙한 재료"{
"target": "콩류",
"cousins": [], // suppressCousins로 (b) 생략
"lines": [
"[오늘 타깃 콩류] 삶은 검은콩 (또래 인기 음식: 콩자반 · 잘 먹는 두부 곁들이면 좋아요 · 또래 급식에도 자주 오르는 익숙한 재료)"
],
"text": "[오늘 타깃 콩류] 삶은 검은콩 (또래 인기 음식: 콩자반 · 잘 먹는 두부 곁들이면 좋아요 · 또래 급식에도 자주 오르는 익숙한 재료)"
}
// suppressCousins=false 였다면 (b)에 추가됐을 줄(천장4 단조성):
// "감자 → 사촌 찐 단호박(또래 인기: 단호박죽) / 궁합 계란"
// "계란 → 사촌 삶은 메추리알 / 궁합 ..."감자 → 사촌 두부 줄이 본문에서 검은콩과 경쟁해, 작문기가 결국 두부로 회귀(랄프위검 rank1 '두부 도돌이표'). after: suppressCousins:true로 (b) 생략 → 본문이 슬롯 음식(검은콩) 단일 푸드체이닝으로 봉합. 실측 비-콩류 슬롯 12일 두부 누수 0. 단 (b)가 살아 있는 날엔 사촌 줄이 항상 감자→단호박·계란→메추리알처럼 감자·계란 단일 앵커에서만 갈려 나옴(천장4 — ⑤ 참고).남은 문제점 · 해야 할 일 (랄프위검 47점 실측 천장 · 미화 없음)
연속성 47점(2026-06-20) 실측 천장을 이 모듈 관점에서 정직하게. 각 항목 증상 → 근본원인 → 해야할일(owner).
verifiedCousinsOf·cousinsOfLiked(coachRecos.ts:250) 그래프 조회 자체는 결정론·정상(EARNED). 문제는 입력 앵커(잘 먹는 음식 = likedIngredients/confidentLiked)가 감자·계란 2개로 협소해 출력 다양성에 상한이 걸리는 것. 이 협소함의 뿌리는 천장1(아린 확신 liked 0개에 가까움 — P0-D child_daily_state 환경신호 미입력)이라, ⑦ 단독으로는 못 푼다.buildRecoFacts (b)가 liked.slice(0,8) 순서대로만 쓰지 말고, 최근 회전 이력(recentRecoIng)을 받아 같은 앵커 연속 사용을 강등. (b) 사촌 다리 카피 변주: '사촌 X' 단일 패턴 → '닮은 맛', '식감이 비슷한', '한 걸음 옆' 등 변주 풀(⑥ 작문기와 합의). (c) 의존관계 명시: confidentLiked 확장이 P0-D 신호 입력에 종속(천장1)임을 막아두기 전엔 앵커가 2개로 묶임을 코드 주석·어드민에 박는다. owner: ⑦(앵커 다양화 정렬) + route/P0-D(앵커 폭 확장 — 선행) + ⑥(카피 변주).coldStartSeed Tier2→Tier3로 올라가도 YOUA_TOP 큐레이션 풀이 협소해, challenge 후보가 두부·달걀 등 소수로 수렴. 앵커 다양화(1)의 공급원도 같이 좁아짐.YOUA_TOP(coachRecos.ts:177)이 groupOfIngredient 인식되는 표준 식재료로 한 번 더 걸러지고, verifiedCousinsOf 진입 가능한 노드만 남아 실효 풀이 좁다. food-graph bridge 자체도 두부·계란 주변에 집중.YOUA_TOP 큐레이션 확장(/mealfred-food-mapping Part B 매트릭스 재생성 시 bridge 다양화) · ING_YOUA_ALIAS 봉합 누락 보강. owner: 데이터(food-mapping 스킬) + ⑦(풀 소비).validCombos·isComboOk, 임계 2)가 구현돼 있으나 라이브 추천 경로에서 호출 0건. 현재 괴식 방어는 safeGarnishOf 식품군 휴리스틱 + 궁합 임계뿐.buildRecoFacts의 곁들임 산출('잘 먹는 음식 × 추천 식재료')이 safeGarnishOf까지만 통과하고, comboGuard.validCombos로 dish×ing 정합 재검을 안 거침. 두 게이트(comboGuard·comboMatrix)가 동일 임계·동일 사상인데 병존·휴면.comboGuard.validCombos 권장 — dish×ing/ing×ing 경계가 시그니처로 강제). 통과분만 본문 노출(미통과=제외, 빈 결과 허용=조합 강요 금지). 중복 게이트 정리. ⚠️ 호출 트리거 위치(cron buildRecoFacts 호출부)는 통합/cron과 1줄 합의 필요. owner: ⑦(게이트 함수) + cron(호출부).cousinsOfLiked(coachRecos.ts:250)이 verifiedCousinsOf 결과를 'MEAL_GROUPS 소속'으로만 게이트하고 '결핍군 + 앵커와 다른 군' 게이트가 없음. buildRecoFacts (b)는 사촌을 '사촌 ~' 교체 프레이밍으로만 쓰므로 현재 곁들임 누수는 제한적이나, 풀(buildIngredientPool)에는 같은 군 사촌이 들어갈 수 있음.cousinsOfLiked에 '결핍군 + 앵커와 다른 군' 정렬·필터 추가, bridge 엣지에 곁들임 부적합 표식 명문화(장기). 단 — 사촌을 교체 축으로만 쓰는 한 P0보다 후순위. owner: ⑦ + 데이터(food-graph.json).strongPairsOf 임계 통일, verifiedCousinsOf verified-only, popularDishesFor 테이블 근거(목록 밖 음식 못 지어냄).• F-18 봉합 실측 EARNED:
suppressCousins로 비-콩류 슬롯 12일 두부 누수 0(랄프위검 rank1 해소).• youa 정직 null: 미수록·연어 의도적 미매핑으로 '흔함 위장' 0.
• 곁들임 교차괴식 가드: 단 과일·우유 ↔ 짠 생선·콩 차단(
garnishConflict)이 라이브 동작.child_daily_state 미입력(천장1)이다. ⑦이 단독으로 할 수 있는 건 앵커 다양화 정렬·카피 변주·휴면 괴식 게이트 배선이고, 앵커 폭 자체의 확장은 route/P0-D 선행에 종속된다 — 이 의존관계를 미화 없이 명시하는 것이 ⑦의 정직한 현 위치다.