🧩 코칭 엔진 모듈 명세 · ④/⑦

🚦 ④ 영양평가 레이어 (신호등·간식)

식재료 빈도로 결핍 신호등을 켜고, BMI·성장곡선으로 탄단지(macro) 트랙을 판정하며, 간식을 끼니와 분리해 따로 감시한다

← 문서 허브 · 통합 아키텍처 지도 · 코칭 엔진 모듈 명세
🗺️ 이 문서는 통합 아키텍처 지도의 모듈 ④를 코드 단위로 펼친 명세입니다 — 결핍 식품군·BMI·성장곡선·macro 트랙·간식. 코드 실측(lib/nutrition.ts·lib/coach.ts·lib/coachWeekly.ts·lib/growth-reference.ts·lib/snack.ts·app/api/cron/coach/route.ts) 기준 정본.
🧭

① 한눈에 — 이 모듈이 무엇을 왜 하나

이 모듈은 아이가 며칠간 먹은 식재료 목록을 받아 '영양 신호등'을 켭니다. 부엌 저울로 그램(g)을 재는 게 아니라 "이 영양소를 채워주는 음식을 며칠에 한 번꼴로 먹었나"라는 빈도로 판단합니다. 단백질은 매일(주5일+) 먹어야 초록, 철분처럼 몸에 저장이 잘 되는 건 주2일만 닿아도 초록이 됩니다. 정량 측정이 불가능한 가정·기관 식단 기록의 한계를 '빈도'라는 현실적 잣대로 바꾼 것입니다. 색은 초록(충분)·노랑(조금 부족)·빨강(부족) 세 단계입니다.

이 모듈에는 세 갈래가 있습니다. (가) 식사 신호등(nutrition.ts): 36종 영양소(보건복지부 KDRI 2025)와 8개 식품군(곡물·콩류·유제품·고기계란·생선해산물·비타민A채소·기타채소·과일)을 각각 빈도로 평가하고, 가공식품·반복 메뉴 감점을 붙여 '다양성 점수'를 냅니다. (나) 간식 엔진(snack.ts): 끼니와 완전히 분리해 간식만 봅니다. 과자·사탕·단 음료 같은 초가공을 얼마나 자주 주는지, 좋은 통식품(과일·삶은 계란·치즈)으로 채우는지, 그리고 간식이 다음 끼니 입맛을 망쳤는지를 봅니다. (다) 체격·성장 트랙(growth-reference.ts + coachWeekly.ts): 키·몸무게를 질병관리청 성장도표에 빗대 또래 곡선을 잘 따라가는지 보고, 저체중·성장 더딤이면 결핍 다양성 대신 탄단지(고기·계란·콩·유제품) 보강으로 추천 방향을 틀고(macro 트랙), 과체중이면 간식 절제로 틉니다.

핵심 비유는 '신호등 + 안전장치'입니다. 데이터가 적으면(기록 3일 미만) 섣불리 빨강을 켜지 않고 노랑으로 보류하고, 비타민D처럼 음식보다 햇빛·보충제로 채우는 건 빨강을 끕니다. 짠 음식 많은 한국 식단 특성상 나트륨은 아예 집계에서 빼서 '괜한 빨강(가짜 경고)'을 막습니다. 결과물은 별도 화면이 아니라 매일 가는 코칭 편지 안에 자연스럽게 녹습니다.

🥗 영양·편식 정보는 이렇게 처리합니다
결핍 판정은 '빈도 밴드'로 합니다. 영양소를 몸의 저장성·식품 분포·결핍 위험에 따라 매일군(green=주5일+)·자주군(주4일+)·가끔군(주2일+)으로 나눕니다. 한 번도 안 닿으면(0일) 빨강이지만, 비타민D는 빨강을 끄고(보충제 영양소), 기록 5일 미만이면 빨강을 노랑으로 보류합니다. 데이터가 3일 미만이면 모두 '참고(reference)'로 보류해 가짜 경고를 안 냅니다. 8식품군은 매일군(곡물·채소·과일·유제품 주5회)과 로테이션군(고기계란 주5회, 생선해산물 주2회, 콩류 주2회)으로 목표가 다르고, 로테이션군은 0회일 때만 빨강입니다.

편식 대응의 핵심은 '강요 금지 + 잘 먹는 것에 섞기'입니다. 부족 영양소가 나오면 그걸 채우는 음식을 추천하되(NUTRIENT_FOODS), 이 모듈은 신호만 만들고 실제 편지는 잘 먹는 음식에 결핍을 곁들이는 방향으로 씁니다. 집과 기관(점심 급식)을 분리해서, 부모가 못 바꾸는 기관 급식엔 가공·반복 감점을 안 매기고(applyMealPenalty=false) 과일·유제품 결핍도 빼줍니다(기관은 점심만이라 부당 감점 방지). 가공식품도 단백질은 주니 과잉 처벌하지 않게 감점 상한(가공 22·반복 12)을 둡니다.

체격은 '방향'으로만 말합니다 — 체중·살·다이어트 단어를 절대 안 쓰고, BMI 밴드·성장곡선은 '든든하게/유지/넉넉히 챙기기'로만 표현합니다. 간식의 작고 단단한 식품(방울토마토·생당근·포도·견과)은 추천에 항상 조리 표시('반으로 잘라'·'익혀·강판'·'잘게')를 붙여 질식을 막고, 같은 멘트가 같은 예시로만 보이지 않게 매일 회전(rotate)시킵니다.
🧒 실제 부모 시나리오 (아린·5세·정상 체격) — 아린의 최근 7일 식단엔 미역국·소고기·계란은 자주 나오는데 콩류(두부·콩)와 채소가 거의 안 보입니다. 입력: 날짜별 식재료 목록, 집 끼니/점심 기관급식 구분, 아린의 키·몸무게(있으면). 처리: 신호등이 '단백질=초록(소고기·계란 매일), 칼슘=초록, 철=초록, 식이섬유=노랑/빨강(채소 적음)'을 빈도로 매기고, 8식품군에서 콩류가 0회라 '부족(빨강)'이 됩니다(콩류는 가끔군이라 0회만 빨강). 아린은 정상 체격이라 macro 트랙은 잠자고 간식 잔소리도 침묵합니다. 결과: 코칭 편지엔 '소고기·계란은 잘 챙기고 있어요. 이번 주는 콩류(두부조림 같은 걸로)를 한 번 곁들여 볼까요' 같은 결핍 보완 신호가 넘어갑니다. 만약 아린이 저체중이었다면 같은 모듈이 결핍 다양성 대신 '끼니 양과 단백질·열량(고기·계란·콩·유제품)을 조금 더 챙겨주세요'라는 macro 방향으로 틀었을 것입니다.

구현된 내용 — 함수·타입·데이터흐름 (실측)

라이브🧠 뇌 + ✍️ 손 양쪽nutrition.ts · snack.ts · growth-reference.ts · coachWeekly.ts

이 모듈은 세 파일에 걸쳐 산다 — 식품군·미량 신호등(nutrition.ts), BMI·성장 추종도(growth-reference.ts), macro 트랙 + 거울 스케줄(coachWeekly.ts 주간계획 모듈). cron(app/api/cron/coach/route.ts)이 셋을 조립해 손(coach.ts)에 넘긴다.

A. 식품군·미량 신호등 — lib/nutrition.ts

함수 · 타입 (file:line)역할 · 실측 동작
nutrientsOf(ing, catOf) :147식재료 1개 → 커버 영양소 배열. ① 농진청 정밀맵 GEN_NUTRI(getNutrientMap) ∪ NUTRI_MAP(손큐레이션 200여종) 우선, ② 둘 다 없으면 CATEGORY_NUTRI 18종 카테고리 빗대기 폴백. '모름'으로 안 비움.
computeSignals(byDay, catOf) :266NutrientSignal[]KEY 15종 신호등. KEY_BAND(daily/frequent/occasional) × KDRI_BAND_NEED{5,4,2} 문턱. 기록<3일=yellow 보류, d===0이면 비타민D·기록<5일은 yellow 클램프 아니면 red. cron이 red만 뽑아 reds로 사용.
computeKdriSignals(byDay, catOf, ageBand) :48536종 KDRI 신호등(연령대 KDRI_VAL_BY_AGE 1-2/3-5/6-8세 기준치). mapped 없으면 'reference', 기록<3일 보류. 분모는 달력 7일 고정(누수 교정). 홈 모달용 데이터.
computeGroupSignals(byDay, catOf) :2008식품군 신호등(GROUP_TARGET: daily 5+ / rotation). daily는 weeklyEst≥5 green·≥2 yellow·else red. rotation은 0회만 red·≥green green·else yellow. proteinOk(고기계란·생선·콩 중 매일≥1군) 동반 반환.
computeFoodGroups(allIng, catOf) :3078군 covered/missing 분해(FOOD_GROUP 직접 → CATEGORY_GROUP 빗대기). cron이 missing/homeMissing으로 분리.
groupOf(ing, catOf) :197식재료→식품군 정본(FOOD_GROUPCATEGORY_GROUP[catOf]). K-01 — catOf(카테고리 18종)를 식품군(8종)과 직접 비교하던 네임스페이스 버그 수정용으로 export. refExposable(거부 결핍군 필터)이 공유.
computeTimeseries(byDate, menuFreq, catOf, today, {assertNoVeg}) :335추세 사실 문장('채소 N일째 없음'·반복 메뉴 3회+·흰쌀밥→잡곡 제안 전환). LLM 날짜 환각 차단(분석이 계산한 사실만). assertNoVeg는 catOf 신뢰 시만 '채소 없음' 단정(P4).
computeDiversityScore(args) :551점수 단일 진입점: 다양성 base(green100/yellow65/red0 평균) − 게이트캡(red 1개=90, 추가군당 −8, 하한66) − 가공패널티(상한22) − 반복패널티(상한12). applyMealPenalty=false 또는 끼니<3건이면 패널티 미적용. daycareMode면 과일·유제품 군 제외.
processedPenalty :525 · repeatPenalty :539 · isProcessed :518ULTRA_RE(초가공 가중1.0)·CURED_RE(가공육 0.7) 정규식으로 메뉴명 매칭. 흰쌀밥·김치·국 등 REPEAT_SKIP 제외. snack 엔진도 isProcessed 재사용.
상수: KDRI_NUTRIENTS:381 · KDRI_VAL_BY_AGE:451 · KDRI_EXCLUDED:374 · GROUP_TARGET:190 · NUTRIENT_FOODS:164KDRI 2025 연령대 기준치·제외 5종(나트륨·에너지·수분·불소·크롬)·8군 목표·부족 영양소별 추천 식재료.

B. BMI · 성장 추종도 — lib/growth-reference.ts 라이브

함수 (file:line)역할 · 실측 동작
bmiOf:210 · bmiPercentile:158 · bmiBand(pct):172 → BmiBand('저체중'|'정상'|'과체중'|'비만')키·몸무게·성별·월령 → KDCA 2017 LMS로 BMI 백분위·밴드. cron bmiBandFor(route:240)가 최신 growth_logs 체위 우선, 없으면 children 스냅샷.
growthTracking(baseline, current, sex, metric) :263 → GrowthTrack | null성장곡선 추종도: 첫 측정(baseline)으로 그 아이 백분위 채널을 잡고, 현재 측정의 z(SDS) 하향 이탈로 status 판정 — zDrift≤−1.34='경고', ≤−0.67='주의', else='양호'. 측정간격<1개월='정보부족'(노이즈 보류). z 둘 중 하나라도 null이면 전체 null. score=100+min(0,zDrift)×45.
growthTrackToPhrase({band, height, weight}) :290 → string | nullBMI 밴드+추종도 → 비수치 P10 한 구절. 우선순위: 곡선 경고>주의 > BMI 비만·과체중·저체중. 정상·정보부족이면 null(거울 생략). 숫자·퍼센타일·BMI·체중·살 단어 0(P10 톤). 경고 키 구절: '키가 또래 흐름을 천천히…단백질·열량(고기·계란·콩·유제품)을 넉넉히…소아과 상의'.

C. macro 트랙 + 거울 스케줄 — lib/coachWeekly.ts (주간계획 모듈) 라이브(코드)

함수 · 타입 (file:line)역할 · 실측 동작
MacroTrack 타입 :71 · buildMacroTrack(ctx) :485#7 macro 성장 nudge. EnrichContext.band/heightTrack/weightTrack 입력. lowWeight(band==='저체중') 또는 growthLag(height/weight track status가 '주의'|'경고')이면 active:true·boostGroups=['고기·계란','콩류','유제품']·boostDishes(각 군 대표의 popularDishesFor) 산출. overweight(과체중·비만)이면 snackRestraint:true. phrase=growthTrackToPhrase(...). cadenceWeek=(arcWeek%2===0)격주(2주 연속 금지·측정 저빈도 존중).
enrichWeeklyPlan(synth, ctx) :520 → PlanDetail일요일 종합 위 7일치 결정론 후처리(LLM 0콜). macro를 먼저 계산해 저체중/성장더딤이면 boostGroups 식재료를 supplyPool 앞에 주입('고기류 타깃'화)(:526-529). macroTrack을 PlanDetail에 박제.
buildMirrorSchedule(deficits, covered, macro, daycare, freqMap) :500 → MirrorSlot[7]7일 거울 스케줄. macro.active && cadenceWeek면 슬롯 i===3에 macro.phrase를 kind:'macro'로 박제(:504). 결핍 2군+면 라운드로빈, 단일 결핍은 격일 쿨다운(#5·K-04b — 짝수 슬롯만 결핍줄·홀수 칭찬/생략). 거울줄은 mirrorLineFor 3변형 회전 + deficitDishFor 구체 dish.
pickPlanSlot(detail, {daySeed,cidHash,deficitNow,recentIngredients}) :576 → PlanSlotPick일간 소비(유연성 가드). macroPhrase = (macroTrack.active && cadenceWeek && mirrorSchedule[idx].kind==='macro') ? phrase : null(:582). 즉 그날 슬롯이 macro 슬롯일 때만 macroPhrase가 살아 나온다.

D. 간식 엔진 — lib/snack.ts

함수 (file:line)역할
evaluateSnacks(args) :149 → SnackEval|nullSNACK_SLOTS만 추려 worst-of로 ultra/sugary/good 분류, 간식 직후 0~2.5h 내 덜 먹은 끼니(ate_well=false)로 간섭성, snackStance(band)→stance(encourage/maintain/reduce), reds로 결핍보완 간식.
snackStance(band) :74 · goodSnackSuggestions(stance, reds, rotate) :106 · snackEvalToPrompt(ev, rotate) :229BMI 밴드→방향(체중 단어 금지). 좋은 간식 4종(질식 조리표시 보존·중복제거·예시 rotate). 편지용 한 블록.

E. cron 조립 + 손(편지) 배선 — app/api/cron/coach/route.ts · lib/coach.ts

⚠️ 정정 — 이전 문서(2026-06-18)는 'BMI·성장이 편지에 아예 안 들어감(배선 끊김)'으로 적었으나, 코드 실측상 배선은 그 후 완성됐습니다. growthTracking은 firstGrowth+latestGrowth 2점으로 호출되고, growthMirror/growthBlock/macro 슬롯이 손까지 닿습니다. 다만 ⑤에서 보듯 — growthTrackToPhrase가 '정상·정보부족'이면 null을 반환하므로, 측정이 1점뿐이거나(정보부족) 밴드가 정상이면 거울이 비어 실제 발행 편지에는 한 번도 안 들어갑니다(랄프위검 24통 0토큰). 배선은 살았으나 데이터 다리(2점 측정)가 비어 트랙이 실전에서 잠자는 상태입니다.

설계 아키텍처 시각화 — 입력테이블 → 가공함수 → 출력컨텍스트키

데이터 파이프라인 전체 그림. 초록=손까지 닿음, 빨강 점선=코드는 있으나 실전에서 잠자는(DEAD) 경로.

📥 meal_logs
slot·log_date·ingredients·menus·place·ate_well·meal_time
📥 children
age_band·sex·birth_year/month·height·weight·excluded
📥 growth_logs
measured_on·height_cm·weight_kg
(≥2점·≥1개월 간격 필요)
computeSignals / computeGroupSignals / computeFoodGroups
빈도 밴드 신호등 · 8식품군 covered/missing · proteinOk
computeDiversityScore
base − 게이트캡 − 가공 − 반복 (집/기관 분리)
bmiBandFor → bmiBand
저체중/정상/과체중/비만
growthTracking(first,latest)
키·체중 zDrift → 양호/주의/경고/정보부족
buildMacroTrack(ctx) [#7]
저체중/성장더딤 → boostGroups=고기·계란/콩류/유제품 · 격주 cadenceWeek
growthTrackToPhrase
비수치 P10 한 구절
(정상·정보부족 → null)
evaluateSnacks → snackStance
encourage/maintain/reduce · 간섭성 · 좋은간식
enrichWeeklyPlan → buildMirrorSchedule
7일 거울 슬롯[i] · macro phrase는 i===3(격주)에 박제
pickPlanSlot(detail)
오늘 slot · macroPhrase(슬롯이 macro일 때만)
cron route.ts:916 손 배선
🧠 CoachSignals.reds / missing / homeMissing / timeseries
두뇌 시나리오 앵커 · 추천엔진 결핍 타깃
✍️ LetterInput.snackEval
간식 한 블록(snackEvalToPrompt)
✍️ LetterInput.growthMirror
= planSlotCtx.macroPhrase ?? growthMirrorCtx
✍️ coach.ts growthBlock 실전 DEAD
growthMirror==null이면 빈 문자열 → 본문 0구절(랄프위검 24/24)
┌──────────────────────────── 두 진단 축 (이 모듈의 골격) ────────────────────────────┐
│                                                                                      │
│   축1: 식품군 8분류 다양성  ──▶  reds·missing  ──▶  [라이브] 거울줄·추천 타깃·점수    │
│        (computeGroupSignals)      매일 손에 닿음                                       │
│                                                                                      │
│   축2: 체격·성장 연료(macro) ──▶  band·track   ──▶  [코드 라이브 / 실전 잠] phrase     │
│        (bmiBand·growthTracking)   growthTrack    null 다발 → growthBlock 빈 채 발행    │
│        ↑                                                                              │
│        └── 데이터 다리 부재: growth_logs 2점(≥1개월) 자녀 거의 없음 → 정보부족/정상   │
│            → growthTrackToPhrase=null → macro 트랙·성장 nudge 실전 미점화              │
└──────────────────────────────────────────────────────────────────────────────────────┘
   결론: 칼로리·단백·지방 절대량(성장 연료) 차원이 진단에서 사실상 빠짐 — 축1만 굴러감

작동 예시 — 아린 실데이터 before / after

아린(5세) 최근 5일 집/기관 식재료 → 이 모듈 통과 → 손에 닿는 출력. 두 케이스로 비교한다.

📥 INPUT
// computeSignals — 아린(5세) 최근 5일 식재료(날짜별)
computeSignals([
  ["쌀","소고기","계란","감자","미역"],
  ["쌀","계란","김치","두부"],
  ["쌀","소고기","감자","양파"],
  ["쌀","계란","미역","당근"],
  ["쌀","닭고기","감자","김치"],
], catOf)

// evaluateSnacks — 같은 아이 meal_logs + 밴드 + reds
evaluateSnacks({
  rows: [
   {slot:"pm_snack",log_date:"2026-06-15",
    menus:["초코우유","과자"],ingredients:[],
    ate_well:true,meal_time:15.5},
   {slot:"dinner",log_date:"2026-06-15",
    menus:["밥","미역국"],ingredients:["쌀","미역"],
    ate_well:false,meal_time:18.5},
  ],
  band:"정상",
  reds:["오메가3","비타민D","철"],
})

// macro 트랙 입력(EnrichContext 발췌)
band: "정상",  heightTrack: null,  weightTrack: null
// ↑ growth_logs 2점 없음 → growthTracking=null
📤 OUTPUT
// computeSignals → NutrientSignal[] (발췌)
[{nutrient:"단백질",daysCovered:5,level:"green"},
 {nutrient:"철",    daysCovered:4,level:"green"},
 {nutrient:"비타민A",daysCovered:1,level:"yellow"},
 {nutrient:"오메가3",daysCovered:0,level:"yellow"}]
// computeGroupSignals: 콩류 weeklyEst≈1.4
//   → rotation·0회 아님 → yellow (0회였다면 red)

// evaluateSnacks → SnackEval
{ultraCount:1, interference:[{date:"2026-06-15",
  mealSlot:"dinner",gapH:3,kind:"ultra"}],
 stance:"maintain", band:"정상",
 goodSuggestions:["치즈","삶은 계란","두유","과일"]}

// buildMacroTrack(band:정상,tracks:null)
{active:false, reason:null, boostGroups:[],
 phrase:null, cadenceWeek:false}   // ← macro 잠

// → LetterInput.growthMirror = null  (macroPhrase·격주거울 둘 다 null)
// → coach.ts growthBlock = ''  (성장 구절 0)
CASE 1 — 정상 체격 아린 (현 라이브 실측)
거울줄(손에 닿음·라이브): buildMirrorSchedule이 콩류 결핍을 잡아 슬롯에 박고, 편지는 →
"소고기·계란은 잘 챙기고 있어요. 집 끼니에 콩류(두부조림 같은 걸로)를 한 번씩 곁들이면 다양성이 좋아져요."
성장줄(DEAD): growthMirror=null → growthBlock 빈 채 발행. 성장·BMI 토큰 0개. 이게 랄프위검 24/24의 실태다.
CASE 2 — 만약 아린이 저체중이었다면 (코드상 의도된 동작)
입력에 band:"저체중"(또는 weightTrack.status:"주의")만 바뀌면:
buildMacroTrackactive:true·reason:"lowWeight"·boostGroups:["고기·계란","콩류","유제품"]·phrase:"또래보다 가벼운 편이라 끼니 양과 단백질·열량(고기·계란·콩·유제품)을 평소보다 조금 더…"
enrichWeeklyPlan이 supplyPool 앞에 고기·계란/콩류/유제품 식재료 주입 → 추천 타깃이 결핍 다양성에서 탄단지 보강으로 전환
• 격주(cadenceWeek)인 주에 한해 mirrorSchedule[3]에 macro 구절 박제 → 그날 편지 본문에 성장 nudge 한 구절
그러나 이 분기는 실데이터에서 거의 점화되지 않는다 — band 산출에 필요한 growth_logs 측정이 비어 대부분 band=null/정상으로 떨어지기 때문(⑤ 천장).

남은 문제점 · 해야할 일 — 랄프위검 실측 천장 (연속성 47점, 2026-06-20)

연속성 47점천장3 = 이 모듈 소관EARNED: 식품군 축·간식 축은 실전 라이브

EARNED(사실) — 식품군 8분류 신호등·결핍 거울줄·간식 stance·다양성 점수·집/기관 분리·질식 가드는 실측상 매일 손에 닿아 작동한다. macro/성장 코드(buildMacroTrack·growthTracking·growthBlock 배선)도 깔려 있다. 아래는 미화 없이 정직한 천장.

증상근본원인해야할일owner
[천장3] macro/성장 트랙 100% DEAD — 발행 24통에 BMI/키/몸무게/percentile 토큰 0. '단백질'은 안심 멘트로만 소비, 성장신호→고기류 타깃 전환 0회. #7 코드는 깔렸으나 본문 점화 0. growthTrackToPhrase(growth-reference:290)가 정상·정보부족이면 null을 반환 → 측정 1점뿐이거나 정상 밴드면 growthBlock이 빈 문자열로 발행. ② 더 근본은 데이터 다리 부재: growthTracking은 firstGrowth+latestGrowth 2점(≥1개월 간격)을 요구하는데, 실제 growth_logs에 2점 쌓인 자녀가 거의 없어 track=null/정보부족으로 떨어짐. P0-D(child_daily_state)는 환경신호 전용이라 성장신호 다리가 아님. 성장지표 데이터 다리 신설 — care 앱에서 키·몸무게 입력 유도(온보딩+주기 리마인드)로 growth_logs 2점 확보 경로를 명시 배선. 1점만 있는 자녀는 절대량(BMI 밴드)만이라도 살리도록 growthTrackToPhrase의 band 분기를 측정 1점에서도 쓰게 게이팅 분리(정보부족은 track만 보류·band는 살림). P0 · 코칭엔진(route+growth-reference)
결핍 모델이 식품군 다양성 축에만 편향 — 칼로리·단백질·지방 절대량(성장 연료) 차원이 진단에서 통째로 빠짐. computeGroupSignals/computeDiversityScore가 전부 '며칠 닿았나'(빈도·다양성) 축. 단백질도 proteinOk(매일≥1군 닿음)로만 보고 총량·열량은 안 봄. BMI/percentile이 진단(reds·missing) 산출로 흐르는 다리가 없음 — macro는 enrichWeeklyPlan 부가 트랙에만 있고 핵심 신호 산출에 안 섞임. 저체중/성장더딤 분기 강화 — band='저체중' 또는 track.status∈{주의,경고}이면 reds/supplyPool 산출 단계에서 결핍 다양성군 대신 고기류(단백·지방) 타깃을 1차 후보로 승격(현재 enrichWeeklyPlan:526에 supplyPool 주입은 있으나, 두뇌 시나리오·reds까지는 안 닿음 → 시나리오 게이트에도 macro 분기 추가). P1 · 코칭엔진(coachScenarios+route)
'끼니 30분 전 간식·우유·주스 멈춤' 무브가 BMI 무관 blind 4회 — 저체중 아이엔 총칼로리를 깎아 macro 목표와 정반대. 간식 절제·식사 간섭 무브 셀렉터가 snackStance/macro의 snackRestraint를 안 봄. 저체중(encourage)인데도 칼로리-차단 무브가 발동. evaluateSnacks의 stance는 산출되나 무브 선택 게이트에 stance/band 가드가 없음. 무브 셀렉터 BMI 가드 — 칼로리-차단 무브(끼니 전 간식 멈춤·우유 줄이기)는 저체중/성장더딤(macro.snackRestraint=false)이면 억제, 과체중/비만(snackRestraint=true)이면 강화. snackStance를 무브 셀렉터 입력으로 연결. P1 · 코칭엔진(coach 무브 게이트)
거울 앵무새 잔여(비대칭 해소됨·완전 종결 아님) 단일 결핍 격일 쿨다운(#5·K-04b)·문장틀 3변형 회전·macro 슬롯 격주는 라이브. 다만 추천풀이 협소(콜드스타트 Tier2·두부5/달걀6)해 같은 식재료가 회전 풀로 재등장하는 잔여 반복. 추천풀 확장(Tier3 youa 큐레이션·앵커 다양화)으로 거울 dish 다양성 확보. care 5단계 실데이터 누적 시 자연 완화. P2 · 추천엔진(②)·food-mapping
🔑 이 모듈이 근본적으로 무엇을 바꿔야 하는가 — ④의 진짜 숙제는 두 진단 축을 대등하게 굴리는 것이다. 축1(식품군 다양성)은 산다. 축2(체격·성장 연료=macro)는 코드는 깔렸으나 데이터 다리가 비어 실전에서 잠잔다. 우선순위 한 줄: ① growth_logs 2점 확보 경로(데이터 다리)를 만들고, ② 1점·정상에서도 band 절대량만이라도 살려 성장 nudge가 점화되게 하며, ③ 저체중/성장더딤이면 reds·시나리오·무브 게이트까지 macro로 분기시켜 '단백질 안심 멘트'가 아니라 '고기류 타깃 전환'이 실제로 일어나게 한다.
🧒 쉬운말 요약 — 지금 편지는 '오늘 먹은 음식 종류가 골고루였나(식품군)'는 매일 잘 말하는데, '키·몸무게가 또래 곡선을 잘 따라가는지(성장)'는 코드는 다 만들어 놓고도 한 번도 못 말합니다. 이유는 단순합니다 — 부모가 앱에 키·몸무게를 두 번(한 달 간격) 안 넣으면 엔진이 성장을 판단할 재료가 없어 입을 닫아버립니다. 그래서 ① 키·몸무게를 모으는 길을 터주고, ② 한 번만 재도 '또래보다 가벼운 편'은 말해주고, ③ 마른 아이한텐 '간식 줄이기' 같은 칼로리 깎는 잔소리를 막고 고기·계란·콩을 권하게 바꾸는 게 이 모듈의 숙제입니다.
📖

문헌 · 이론 근거 (실구현 적용)

이론 · 문헌어떻게 적용하나
보건복지부 「2025 한국인 영양소 섭취기준(KDRI)」36종 영양소의 연령대별(만1-2/3-5/6-8세) 권장 기준치를 KDRI_VAL_BY_AGE로 상수화해 모달 표시·신호등 밴드 근거로 사용
농촌진흥청 국가표준식품성분표 10.4식재료별 100g당 1일 권장 15%↑를 '공급원'으로 보는 정밀 영양맵(GEN_NUTRI)을 빗대기 대신 1차 출처로 사용
식약처 영유아 식생활지침 / WHO 8식품군'매끼 곡류·단백질·채소, 매일 우유·과일'을 GROUP_TARGET(매일군 주5·로테이션군)으로 코드화
질병관리청 「2017 소아청소년 성장도표」신장·체중·BMI for-age LMS를 kdca-growth-lms.json으로 박제(P3/P50/P97 골든·vitest 384). WHO 아님(만3세+ 국내값 상이·24mo 미만 WHO 폴백)
성장곡선 추종 모델(z/SDS 채널)첫 측정으로 백분위 채널을 잡고 z 하향 이탈로 주의(−0.67)/경고(−1.34) — 절대 위치 아니라 '자기 곡선 유지'를 봄. 가정 측정 노이즈는 측정간격<1개월='정보부족'으로 보류
Satter 식사 구조화(division of responsibility)식간 군것질·단 음료가 끼니 식욕을 떨어뜨린다는 원리로 간식 직후 끼니를 덜 먹은 '식사 간섭성' 판정
영양 신호등 자체 근거 보고서체내 저장·식품 분포·결핍 위험 3축으로 매일/자주/가끔 밴드(green 문턱 5/4/2일)를 정함
🛡️

가드 — 괴식·환각·안전 차단 (실구현)

🔗

CoachContext 전달 — 🧠 뇌 + ✍️ 손 양쪽

이 모듈이 채우는 컨텍스트 키:

signals.reds (computeSignals red)signals.homeRedssignals.missing / homeMissing (computeFoodGroups)signals.timeseries (computeTimeseries)snackEval (snackEvalToPrompt 블록)growthMirror (macroPhrase ?? growthMirrorCtx → growthBlock)plan_detail.macroTrack / mirrorSchedule (enrichWeeklyPlan)홈 UI: kdri/groupSig/diversityScore (data)