비개발자용 — 편지 한 통이 만들어지는 과정
왜 8개로 나누나매일 새벽 2시(0 17 * * * UTC), 활성 자녀 한 명마다 아래 흐름이 한 바퀴 돕니다. 한 명의 코치가 하는 일을 8개 부품이 나눠 맡습니다 — 코드를 몰라도 읽히게 정리했습니다.
try/catch degrade).구현된 내용 — 코드에 실제로 있는 파이프라인
route.ts 실측전부 web/app/api/cron/coach/route.ts(1,097줄)의 단일 GET 핸들러 안에서 자녀 루프(for (const cid of activeIds)·route.ts:264)로 순차 실행됩니다. 모듈은 @/lib/* 순수 함수로 import되어 결정론 재료를 채우고, LLM은 두뇌(pickActionByBrain)·작문(composeLetter)·질문(generateQuestion) 3곳뿐입니다.
8개 모듈 · 입력 → 함수 → 출력(컨텍스트 키)
| # | 모듈 (lib) | 핵심 함수 · file:line | 📥 입력 | 📤 출력 → context | 상태 |
|---|---|---|---|---|---|
| 0 | 야간 백필 remapMenus | backfillUnmappedMenus()route.ts:95 | menus 있고 ingredients 빈 meal_logs(60일·LLM≤8콜) | 식재료 백필(영양계산 선행) → backfill 지표 | live |
| 1 | 신호 수집 + 선호 계량화 nutrition · preferenceQuantification | computeSignals/FoodGroups/Timeseries/GroupSignals·quantifyPreferences()·confidentLiked()·dislikedFoods()route.ts:368-381 | 7일 meal_logs(집/기관 분해·place!=='daycare')·acceptance_level(0~4)·catMap | CoachSignals(reds/missing/refusedExposable)·confidentLiked[]·dislikedIng·coldStart | live⭐신규 |
| 2 | 사실·재노출 컴파일러 coachFacts · reexposure | compileFacts()·reexposurePick()route.ts:423,457 | rows·거부→수용 전환(28일)·노출횟수/경과일 | fc.cards/noteCards/forbidParts·재노출 적기 사실(ts에 push) | live |
| 3 | 주간 닻(작전층) coachWeekly · curriculumUnits · curriculum · coachDaily | runWeeklyPlanning()·enrichWeeklyPlan()·planFromWeekly()·advanceProgress()·pickPlanSlot()route.ts:596,627,740,680,852 | reds·missing·refExposable·stalledTarget(3주)·progByChild·focusHistory·candSignals | weekly_plans 행(mission_target·target_pool·budget.lever·ledger·plan_detail)·curriculumDecision·planSlotCtx | live⭐신규 |
| 4 | 추천엔진 + 네트워크 조회 coachRecos · graphSource | buildRecoFacts()·buildIngredientPool()·coldStartSeed()·getRecipeFreq/IngredientsLight()route.ts:760,842,867 | likedSeed·target(결핍군)·freqMap·recentRecoIng(회전) | recoPoolArr(5)·recoIng(회전)·bridgeFacts·slotFood/slotDish | live⭐신규 |
| 5 | 영양 거울 + 간식 + 성장 snack · growth-reference · (mirror) | evaluateSnacks()·growthTracking()·bmiBand()·mirrorCooldownDue()route.ts:385,401,894 | byChild rows·snackBand(BMI)·firstGrowth/latestGrowth·recentMirrorShown | snackText·growthMirror·mirrorLine(쿨다운)·heightTrack/weightTrack | live⭐신규 |
| 6 | 일간 두뇌(판단) coachBrain · coachScenarios | buildBrainContext()·pickActionByBrain()·anchorOverrideAllowed()route.ts:761,771,782 | signals·nutritionMirror·recoCandidates·weeklyEchoes·anchorLever·anchorTargetPool·recentUseFood·curriculumSummary | brainPick:BrainAction{scenarioId,useFood} → 닻 게이트 통과 시 precomputed 교체 | live·LLM |
| 7 | 작문기(손) coach · coachChronic | composeLetter()·generateLetter/OnboardingLetter()·chronicGuidanceText()route.ts:925 | base(①~⑤ 재료 전부)+precomputed(plan/scenario)+detForbid+slotFood/slotDish | {letter, oneliner}+verifyCtx(검증자)+simToPrev/repeatAlert | live·LLM |
generateQuestion()·pickUnitProbe()·icfqForDate()·route.ts:983-997)을 합쳐 오케스트레이션 단위 8종. 모든 모듈 호출이 try/catch로 감싸져 한 모듈이 throw해도 precomputed = planFor(...)(route.ts:557) 폴백으로 편지를 끝까지 발행합니다(안전 제1원칙).⭐ 이번 세션(2026-06-19~20) 신규 — 이 모듈에 반영된 것
| 신규 | 무엇 · file:line | 오케스트레이션 영향 |
|---|---|---|
| 선호 계량화 신호포착 | quantifyPreferences(rs, today)·confidentLikedFrom·dislikedFoods (route.ts:368-370). acceptance_level 컬럼 select 추가(route.ts:112). 미상(null)을 liked로 오판 차단 — 확신=완식 반복+Wilson만. | coldStart = confidentLiked.length < 2(372)로 콜드스타트 게이트·likedSeed(373)·dislikedIng 추천 제외(374). 아린: ate_well true 4건이 전부 다른 식재료 → confidentLiked=0 = 콜드스타트(구 누적기는 4로 오판). |
| targetAccepts 정량화 | acceptanceLevel(r) != null && lv >= 1 (route.ts:711). 구버전 ate_well !== false가 미상도 진척으로 세던 것을 만짐(≥1) 이상으로 제한. | ledger.targetAccepts(743)→ 일요일 synth 스톨 판정 입력. 미상 80% 아동이 영영 '진전 중'으로 보여 두부 반복하던 근원 차단. |
| hardStall 피벗 | 주간: stalledTarget(STALL_PIVOT_WEEKS=3)(route.ts:49,592)로 타깃 전환. 일간: advanceProgress({...coachedDays, pivotsThisWeek})(680)가 유닛 18일 정체 시 강제 피벗 → adv.goalsAfter 영속화(688-692). | 한 유닛 18일 진전0이면 주1회 캡 무시 강제전환. table↔exposure 교대. |
| F-18 슬롯본문봉합 rank1 | slotDish/slotFood를 작문기에 must-weave(route.ts:864-865). suppressCousins:!!slotFood(867)·거울음식=항상 슬롯음식(870-889·deficitDishFor 디폴트 제거). _foodScen(859)으로 음식 시나리오엔 _noFood 끔. | 본문 두부 도돌이표 종결 — 메타=메추리알·본문=두부 이중장부 봉합. 실측 비-콩류 슬롯 12일 두부누수 0. |
| #5 거울 쿨다운 | mirrorCooldownDue(recentMirrorShown, {cooldownDays:2, severe}) (route.ts:894). 최근 2일 거울 나왔으면 오늘 생략·심한 결핍(≥2)이면 면제. | '어린이집 덕에 영양 채워진다' 줄이 24통 중 17통이던 것 격일화. 무슬롯(plateau) 경로도 동일 적용(mirrorCooldown·920). |
| #6 도입/추천 회전 | 식재료: recentRecoIng dedup(route.ts:848). 슬롯: pickPlanSlot({recentIngredients})(852). 거울 문장: daySeed 변주 3종(_posV·877-883). | 같은 식재료 연속 추천·축자 반복(템플릿 마모) 차단. (※도입 새 수렴은 §④ 천장 참조 — 풀 협소로 여전히 잔존.) |
| #7 macro 트랙 | growthTracking()·heightTrack/weightTrack(route.ts:398)·planSlotCtx.macroPhrase 우선(916). 성장 권유 매3일 강화(231-232) — 데이터 유입 유도. | 저체중→고기·계란 macro 슬롯. 단 성장데이터 없으면 트랙 전체 dead(input-starvation·§④ 참조). |
| A 추천 근거(어드민) | context에 planSlot{group,track,via,mirror,macro}·recoIng·recoPool·recoMode 적재(route.ts:956,960). slot.track=supply/challenge·pairLiked(918). | 어드민이 '결핍군 콩류 보급(주0.8회·red)'·'잘먹는 두부의 검증사촌' 노출. |
| P0-D 프로브 다리 절반 | pickUnitProbe(curriculumDecision?.unit, dayNum)(route.ts:990)·unitProbe를 질문 context에 적재(999). ICFQ 위험일 아닌 날 focus 유닛 1차 신호 프로브를 결정론 질문으로. | '답해도 신호로 안 흐르던' 다리의 출력측은 이어짐(질문 발행). 입력측(답→envTablePct7d 등 양성신호 적립)이 미완 = §④ 단일 천장. |
발행 게이트 · 안전 불변식 (실측)
| 가드 | 코드 | 지키는 약속 |
|---|---|---|
| 당일 입력 차단 | .lte('log_date', dAgo(1)) (route.ts:113) | 편지는 '어제까지' 확정 데이터로 — 자기참조·미래 차단 |
| 닻 종속 override | anchorOverrideAllowed({anchorLever,sid,fov,triggerOk}) (route.ts:782) | 비-food 주·트리거 미충족·food override 캡 소진 시 두뇌 시나리오 무시 → 닻 잠금 보존 |
| 연속 food 캡 | recentBrainUseFood.slice(0,_need).filter(Boolean).length >= _need (route.ts:774-775) | food주 3일·비food주 2일 연속 음식이면 useFood=false 강제(잔소리 쿨다운) |
| 재사용 동결 방지 | srcHash에 today 포함(472)·재사용 시 context 전체 보존(498-509) | 날짜 바뀌면 식단 같아도 새 편지·firstOfWeek 오판 방지(S6) |
| 검증자 + 반복경보 | verifyCtx(927)·simToPrev>=0.6 ‖ sigRun>=2 → repeatAlert(931) | 발행 전 의미 검증·자가 유사도 측정 → issues에 노출(사람 제보 의존 탈피) |
| 비용 계측 | resetUsage()(269)·aggregateUsage→llm_usage upsert(finally·1020) | 자녀별 LLM 비용 실측 — ₩331/자녀·월 |
설계 아키텍처 시각화 — 입력 테이블 → 가공 함수 → 출력 키
파이프라인한 자녀 루프(route.ts:264~1029) 안의 데이터 파이프라인. 초록=입력 테이블/데이터 → 보라=가공 모듈 → 회색=두뇌(판단) → 주황=출력(편지).
⤷ anchorOverrideAllowed 게이트 통과 시에만 weeklyReplan(sid)로 precomputed 교체(닻 잠금 보존)
route.ts:174-188에서 어제 context를 읽어 복원). 즉 편지가 곧 상태 원장 — child_daily_state 테이블은 설계상 미구현이고, 실제로는 coach_letters.context가 그 역할을 대신합니다(§④ 참조).작동 예시 — 아린 케이스 한 바퀴 (before / after)
실측아린(table-stage·콩류 결핍·미상 80%) 한 명이 이번 세션 신규 기능을 통과하며 무엇이 바뀌었는지, F-18 슬롯본문봉합 + 선호계량화 두 케이스로 봅니다.
케이스 1 — 선호 계량화: 콜드스타트 오판 차단
ate_well===true가 4건 — 그런데 전부 다른 식재료(각 1회). 구 누적기는 likedIng.length=4로 보고 콜드스타트 미발동. 추천 앵커가 빈약한데도 '잘 먹는 것 많음'으로 오판 → 결핍군 대표 디폴트(콩류→두부)로 무너짐.quantifyPreferences(rs, today) → confidentLiked()는 완식 반복+Wilson 통과만 liked. 아린은 단발 4건이라 confidentLiked = 0 → coldStart = true(<2). likedSeed = coldStartSeed(servedTop, 6)로 전환 — 자주 차려진 음식을 앵커로 안전 추천. 미상(null)은 더 이상 liked로 새지 않음.케이스 2 — F-18: 메타·본문 이중장부 봉합
slotDish='단호박찜'를 작문기에 must-weave로 강제 + linter 재생성. suppressCousins:!!slotFood로 감자→두부 사촌 끄기. 거울음식=항상 슬롯음식(deficitDishFor 콩류 디폴트 제거) — 슬롯≠결핍군 날엔 거울이 콩류 단어를 빼고 generic-positive로.advanceProgress의 졸업 조건(passWhen)이 영영 안 풀립니다. 피벗(table↔exposure 교대)만 돌고 진짜 졸업은 0 — 이것이 §⑤ 단일 천장입니다.남은 문제점 · 해야할 일 — 랄프위검 47점 천장
정직 감사연속성 47점(2026-06-20). 추이 52→48→44→47. 6개 랭크천장이 전부 단일 뿌리 = P0-D 양성신호 미입력으로 수렴합니다. 이번 세션이 본문 품질(F-18·#5·#6)은 EARNED했으나, 시스템이 학습으로 진전하는 능력은 입력 기근에 막혀 있습니다. 미화하지 않습니다.
| 랭크천장 증상 | 근본원인 | 해야할 일 (owner) |
|---|---|---|
| ① step 1/2단 19일 고착 + table↔exposure 핑퐁 — 피벗만 돌고 졸업 0 | ⭐P0-D 단일 뿌리. advanceProgress 졸업(passWhen)은 envTablePct7d 등 양성신호 표본을 요구하는데, 프로브 답이 신호로 적립되는 입력측 다리가 미완. 출력측(질문 발행·route.ts:990)만 이어짐. |
P0-D 환경신호 입력 완성(코칭엔진) — daily_questions.context.unitProbe 답변을 읽어 child_daily_state(또는 progress.evidence)에 envTablePct7d 적립 → advanceProgress가 졸업 판정. 다음 P0=여기부터. |
| ② F-16 environment 레버 슬롯음식 본문 증발 — 환경 코칭 날 음식 추천이 사라짐 | environment 날 _noFood=true면 본문에 음식 액션 없음(route.ts:860). 거울에 슬롯음식 소프트 노출(886-887)로 완화했으나, 환경 양성신호가 안 들어와 environment 유닛 자체가 졸업 못 함 → 음식 트랙 복귀 지연. |
①과 동근. 환경신호 입력되면 environment 졸업 → food 레버 자연 복귀. 그 전까지 거울 소프트 노출(현행) 유지로 음식 다양성 최소 보장. |
| ③ macro/성장 트랙 dead — 저체중 탄단지 슬롯이 영영 비활성 | input-starvation — growthTracking은 firstGrowth/latestGrowth 2점이 필요한데(route.ts:394-398) 성장데이터 없으면 heightTrack/weightTrack=null. macro 슬롯(macroPhrase)이 안 켜짐. |
이번 세션 #7로 성장 권유 매3일 강화(route.ts:231) 착수 — 데이터 유입 유도. owner=코칭엔진(현행 유지) + 앱 care 화면 키/몸무게 입력 UX(앱). 데이터 들어오면 자동 활성. |
| ④ 추천풀·앵커 협소 — 콜드스타트 Tier2→Tier3, 앵커 단일(돼지불고기) | youa 큐레이션 풀이 두부5·달걀6로 협소. coldStartSeed·buildIngredientPool이 뽑을 후보가 적어 회전(#6)을 해도 금세 수렴. |
P0-B/C 추천풀 확장(코칭엔진+콘텐츠) — Tier2→Tier3 youa 큐레이션 확대 · 앵커 다양화(돼지불고기 단일 탈피). /mealfred-food-mapping 코퍼스 보강과 연계. |
| ⑤ 도입(온보딩) 새 수렴 — 새 음식 도입이 좁은 집합으로 수렴 | ④와 동근(풀 협소) + 회전 dedup(recentRecoIng.slice(0,5)·route.ts:848)가 풀이 작으면 곧 소진. CORE_ORDER 확장트랙 4종 편입(C 온보딩3주차)은 됐으나 풀 자체가 얕음. |
④ 풀 확장과 함께 해소. lever 태그 정합(디버그) 점검 — 슬롯 track(supply/challenge) 라벨이 어드민과 어긋나는지 확인(코칭엔진). |
| ⑥ 본문 템플릿 회전 부재 — plateau/무브 카피 반복 | 거울줄은 daySeed 변주(#6·route.ts:877-883)했으나 무브·plateau 카피풀이 얕아 반복 인상 잔존. plateau는 trigger:()=>true 안전망이라 자주 도달. |
무브/plateau 카피풀 확충(콘텐츠+코칭엔진) — daySeed 변주 슬롯을 거울 외 무브 문장으로 확대. |
unitProbe 답 → progress.evidence → advanceProgress 졸업)이 단 하나의 최우선입니다. 이게 풀리기 전엔 추천풀을 넓혀도 '졸업 없는 회전'이 계속됩니다.부록 — 네이밍 규칙 · 연계 문서
참고세 가지로만 부릅니다 — 산출물과 루프를 가지면 엔진, 데이터를 돌려주면 모듈, 읽고 쓰는 공용 데이터면 레이어.
child_daily_state 단일 스냅샷 테이블은 아직 미구현이며, 실제 코드에선 coach_letters.context가 그 역할을 대신합니다(다음날 루프가 어제 context를 읽어 회전·중복회피 이력 복원). 1만 명 배치로 갈 때 머티리얼라이즈 테이블로 승격하는 것이 후속 과제입니다.