구내식당 식단표 사진 대신 API로 — 매일 메뉴를 AI가 분석해 알림까지 보내는 n8n 워크플로우
이 글과 연결된 글 보기
드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.표시된 글 7편 목록
구내식당 메뉴는 대개 게시판이나 앱에 사진 한 장으로 올라오고, 오늘 식단이 건강한 편인지는 각자 눈으로 훑어 판단합니다. 이 워크플로우는 그 자리를 API 호출과 AI 분석으로 대신합니다. 매일 점심·저녁 시간에 맞춰 식단 API를 불러오고, 사진이 있는 메뉴는 AI가 탄수화물·단백질·지방 비율과 건강 점수, 개선 제안까지 만들어 디스코드로 보내고 데이터베이스에 쌓습니다.
이 워크플로우 하나로 보여줄 수 있는 것
"n8n으로 AI 자동화 만들어봤어요"라는 말에 설득력을 더하려면 "AI를 어디에, 왜 두 번 나눠 썼는지"까지 설명할 수 있어야 합니다. 이 워크플로우는 그 구조를 갖고 있습니다.
- 외부 API 스케줄 호출 — 정해진 시각마다 외부 공개 API를 불러 최신 데이터를 받아오는 경험
- AI Vision 분석 — 사진을 모델에 넘겨 정량값(비율)을 뽑아내는 경험
- 2단 AI 설계 — Vision 결과를 텍스트 컨텍스트와 함께 다른 AI에게 넘겨 최종 판단을 맡기는 경험
- 하나의 결과를 여러 목적지로 — 같은 분석 결과를 알림과 저장으로 동시에 내보내는 경험
세 번째가 특히 눈에 띕니다. "사진 분석"과 "텍스트 기반 판단"을 하나의 AI 호출에 몰아넣지 않고 역할을 나눴다는 점은, 프롬프트 하나에 모든 걸 맡기지 않고 단계를 쪼갤 줄 안다는 신호이기 때문입니다.
배경
이 워크플로우는 데이터팝콘 n8n 인스턴스에 만들어 둔 것입니다. 캔버스 상단 스티키 노트에 만든 목적이 그대로 적혀 있습니다. "TRIGGER: 매일 점심, 저녁 실행. TASK: 식단 내용을 이미지와 함께 분석해서 탄단지. TARGET: 디스코드에 발송, CSV로 추출(노션, 구글시트 등에 적재)." 설계 의도와 실제 구현이 얼마나 맞아떨어지는지는 아래 "원본 캔버스에 남아 있는 흔적"에서 짚습니다.
식단 데이터는 한 구내식당 식단 API 서비스에서 가져옵니다. 로그인이나 API 키 없이 매장 코드와 날짜만 넣으면 그날의 조·중·석식 메뉴를 JSON으로 돌려주는 공개 엔드포인트입니다. 급식·공공데이터 API는 회사마다 형식이 다르지만 "스케줄로 외부 API를 불러 가공한다"는 뼈대는 같습니다. 설치와 기본 노드 설명은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있습니다.
구조 — Trigger / Task / Target
- Trigger — 매일 오전 11시 30분(점심), 오후 5시 30분(저녁) 각각 스케줄 실행
- Task — 식단 조회 → 중식·석식 분리 → 사진 있는 메뉴 최대 10개 → 탄단지 비율 추정 → 분석·점수·제안 생성 → 원본과 다시 합치기
- Target — 디스코드 채널로 알림 발송, Supabase 테이블에 행 저장
본 흐름과 별개로 작은 확장이 하나 더 붙어 있습니다. MCP Server Trigger가 Supabase 조회 툴을 연결해, 쌓인 분석 결과를 에이전트가 직접 질의하게 열어주는 확장입니다. 같은 데이터를 에이전트가 질의하게 하는 이 확장은 MCP 편에서 따로 다룹니다.
만들기
0. 준비 — 식단 API와 저장할 테이블
먼저 자신이 쓸 수 있는 식단(또는 다른 일간 데이터) API의 요청 형식을 확인합니다. 이 워크플로우가 부르는 API는 매장 코드와 날짜를 쿼리 파라미터로 받아 그날의 메뉴를 코너별로 돌려줍니다. 분석 결과를 쌓을 테이블도 하나 준비합니다. 원본은 Supabase의 meal 테이블에 메뉴 정보와 AI 분석 결과를 함께 저장합니다.
1. Schedule Trigger 두 개 — 점심 조회, 저녁 조회
Schedule Trigger를 두 개 둡니다. 하나는 매일 11시 30분, 다른 하나는 매일 17시 30분에 실행되도록 시각을 지정합니다. 점심 메뉴와 저녁 메뉴는 같은 API 응답 안에서도 서로 다른 자리에 들어 있기 때문에, 트리거를 나눠두면 뒤에서 어느 시간대 메뉴를 꺼낼지도 자연스럽게 갈립니다.
2. HTTP Request — 당일 식단 조회
각 트리거 뒤에 HTTP Request 노드를 두고 같은 API를 호출합니다.
GET https://<식단 서비스 도메인>/meal/v1/today-all-meal?storeIdx=<매장 코드>&mealDt={{ $json.timestamp.toDateTime().format('yyyyMMdd') }}&reqType=total
toDateTime().format('yyyyMMdd')는 트리거 실행 시각을 20261210 같은 8자리 날짜 문자열로 바꿔줍니다. 응답은 data 배열 안에 그날의 끼니별 메뉴 목록을 순서대로 담아 돌려줍니다.
3. Split Out + Set — 중식·석식 분리와 라벨
Split Out 노드로 data 배열에서 필요한 자리만 꺼냅니다. 점심 흐름은 data[2](중식), 저녁 흐름은 data[3](석식)을 꺼내 메뉴 항목마다 아이템 하나로 만듭니다. 바로 뒤 Set 노드가 각 아이템에 time 필드를 추가해 2중식, 3석식처럼 표시해둡니다. 나중에 하나로 합쳤을 때 어느 끼니인지 구분하기 위해서입니다.
4. Merge → 컬럼 정리 → Sort → Filter → Limit
점심 쪽과 저녁 쪽 흐름은 Merge 노드에서 하나로 합쳐집니다. 두 트리거는 같은 실행에서 동시에 도는 게 아니라 각자 다른 시각에 따로 실행되므로, Merge는 그때그때 들어온 쪽의 데이터만 흘려보냅니다. 이어서 Set 노드가 mealDt, time, corner, mealIdx, name, side, thumbnailUrl, kcal 등 이후 단계에서 쓰는 컬럼만 남기고, Sort 노드가 mealDt → time → corner 순으로 정렬합니다.
Filter 노드는 사진(thumbnailUrl)이 있는 메뉴만 남깁니다(연산자: String → notEmpty). 사진이 없으면 뒤에서 Vision 분석을 할 대상이 없으니 자연스러운 필터입니다. Limit 노드는 최대 10개까지만 통과시켜 한 번 실행의 AI 호출 횟수를 묶어둡니다.
5. Loop 안에서 — 사진으로 탄단지 추정하기
Limit을 통과한 메뉴는 Loop Over Items(splitInBatches) 노드로 들어가 하나씩 처리됩니다. 아이템 하나가 사진 다운로드 → 리사이즈 → Vision 분석까지 마쳐야 다음 아이템으로 넘어갑니다.
HTTP Request 노드가 thumbnailUrl의 사진을 내려받고, Resize Img 노드가 가로 폭을 50%로 줄입니다. 이 노드 옆 스티키 노트엔 "이미지 다운사이징 — OpenAI 노드 버퍼"라는 메모가 붙어 있어, 원본 해상도 그대로 넘기면 버퍼 부담이 커진다는 걸 고려해 리사이즈를 끼워 넣은 것으로 보입니다.
리사이즈된 이미지는 OpenAI 노드(Resource: Image, Operation: Analyze, 모델 gpt-4o-mini)로 들어갑니다. 실제 프롬프트를 간추리면 다음과 같습니다.
너는 전문 영양사이자 음식 이미지 분석가야.
사용자가 제공한 식판 사진을 분석하여, 해당 식사의 탄수화물(carb), 단백질(protein), 지방(fat) 비율을 대략적으로 추정해줘.
Main: {{ $('Limit').item.json.name }}
Side: {{ $('Limit').item.json.side }}
이미지 분석 기준:
- 탄수화물(carb): 밥, 떡, 빵, 국수류 등
- 단백질(protein): 고기, 생선, 달걀, 콩류 등
- 지방(fat): 튀김류, 마요네즈 소스, 햄, 기름진 음식
- 채소류·국물은 비중 계산에서 제외해도 좋아.
출력 포맷:
{
"mealIdx": {{ $('Limit').item.json.mealIdx }},
"carb": 탄수화물 비중(%),
"protein": 단백질 비중(%),
"fat": 지방 비중(%)
}
원본 프롬프트 끝에는 "설명이나 코드블록 없이, 순수 텍스트(JSON 포맷)로만 출력해줘"라는 문장이 네 번 반복돼 있습니다. 모델이 JSON을 마크다운 코드블록으로 감싸 돌려주는 습관을 막으려는 다짐으로 보입니다.
6. 영양분석 Agent — 사진 결과와 텍스트를 합쳐 점수 매기기
Vision 분석 결과(carb/protein/fat)는 그대로 다음 AI Agent 노드로 넘어갑니다. 이 Agent는 gpt-4o-mini를 연결한 Chat Model과 Structured Output Parser를 붙여, 아래 정보를 입력으로 받습니다.
mealIdx : {{ $('Limit').item.json.mealIdx }}
main : {{ $('Limit').item.json.name }}
side : {{ $('Limit').item.json.side }}
kcal : {{ $('Limit').item.json.kcal }}
nutrition_ratio : {{ $json.content }}
시스템 프롬프트는 이 입력을 근거로 200자 이내 분석(analysis), 0100 건강 점수(900kcal가 이상적이고 1000kcal 이상이면 감점, 튀김류·가공육이 많으면 감점, 콩·채소·생선 반찬엔 가점입니다.score), 150자 이내 개선 제안(suggestion)을 만들라고 지시합니다. 점수 기준도 구체적입니다. 탄수화물 65% 이상·단백질 20% 미만·지방 30% 이상이면 감점, 칼로리는 700
원본 프롬프트에 실제로 들어 있는 예시 출력은 이렇습니다("main: 육개장, side: 크리미어니언생선커틀렛 외 4종, kcal: 1273"을 입력했을 때).
{
"mealIdx": 1526273,
"carb": 55,
"protein": 25,
"fat": 20,
"analysis": "단백질과 지방 비율은 적절하나, 칼로리가 높고 튀김 반찬 비중이 많아 주의가 필요합니다.",
"score": 80,
"suggestion": "튀김류 대신 구이나 찜 요리를 선택하고, 밥 양을 조금 줄이면 좋습니다."
}
Agent 결과는 Edit Fields 노드에서 $json.output.carb처럼 output 아래 값을 꺼내 평평한 필드로 정리한 뒤, 다시 Loop Over Items로 돌아가 다음 메뉴를 처리합니다. 최대 10개 메뉴가 모두 끝나면 결과 전체가 한 번에 다음 단계로 넘어갑니다.
7. Merge — 원본 메뉴 정보와 다시 합치기
루프가 끝난 AI 분석 결과에는 mealIdx, carb, protein, fat, analysis, score, suggestion만 있고 메뉴 이름·칼로리는 없습니다. Merge 노드를 하나 더 두어(Mode: Combine, 매칭 필드: mealIdx) Limit이 애초에 넘겨준 원본 메뉴 정보(코너·메뉴명·칼로리·사진 URL)와 mealIdx 기준으로 다시 합칩니다.
8. Discord 알림 / Supabase 저장 — 세 갈래로 나가기
합쳐진 결과는 세 노드로 동시에 흘러갑니다. Discord는 코너·메뉴·칼로리·탄단지 비율·점수·분석·제안을 한 메시지로 정리해 보내고, Supabase는 같은 값을 meal 테이블 행 하나로 저장하며, Convert to File은 결과를 CSV 파일로 만듭니다.
원본 캔버스에 남아 있는 흔적
이 워크플로우는 운영 실행 이력이 남아 있지 않아, "실제로 돌려보니" 대신 캔버스에 남은 흔적을 근거로 씁니다.
Convert to File 노드는 CSV 파일을 만들어내는 것까지만 하고, 그 뒤로는 아무 노드와도 연결돼 있지 않습니다. 스티키 노트의 TARGET 메모는 "CSV로 추출(노션, 구글시트 등에 적재)"이라고 적어뒀지만, 실제로 밀어 넣는 노드는 붙어 있지 않습니다. 설계 의도와 구현 사이에 한 단계가 비어 있는 셈이라, 따라 만든다면 이 자리에 Drive 업로드나 Notion 페이지 생성 노드를 이어 붙이는 게 자연스러운 다음 작업입니다.
Discord 메시지 템플릿의 맨 앞에는 "(TEST)"라는 문구가 그대로 남아 있습니다. 파일 이름은 "구내식당 식단 분석기(배포용)"이지만, 실제로 채널에 보내는 문구는 아직 테스트 표시를 지우지 않은 상태입니다.
한계
- 사진 없는 메뉴는 분석 대상에서 빠집니다. Filter가
thumbnailUrl이 빈 메뉴를 걸러내, 사진 없는 코너는 알림·저장 대상에서도 제외됩니다. - 한 번 실행에 최대 10개까지만 처리합니다. Limit 노드의 상한값이라, 사진 있는 메뉴가 10개를 넘는 날엔 일부가 잘려나갑니다.
- 탄단지 비율은 사진만 본 추정치입니다. 프롬프트 자체가 "대략적으로 추정해줘"라고 지시하고, 국물·채소류는 계산에서 제외합니다.
- 리사이즈 비율이 고정돼 있습니다. 원본 해상도와 상관없이 항상 가로 폭 50%라, 사진에 따라 여전히 크거나 지나치게 작을 수 있습니다.
포트폴리오·면접 포인트
이 워크플로우의 핵심은 "사진 → 정량값 추정"과 "정량값 + 텍스트 → 최종 판단"을 서로 다른 AI 호출로 나눈 설계입니다. 영수증 사진으로 항목을 인식하고 지출 내역과 합쳐 예산 초과 여부를 판단하는 흐름도 같은 뼈대를 씁니다.
면접에서 "왜 이미지 분석과 최종 판단을 한 번의 AI 호출로 합치지 않았나요?"라는 질문이 오면, Vision 모델의 역할은 사진에서 탄단지 비율을 뽑아내는 데 한정하고 그 값을 텍스트 정보와 함께 다시 판단하는 일은 별도 Agent에게 맡겨 단계별 책임을 나눴다고 답할 수 있습니다. 여기에 "Convert to File 노드가 아무 데도 연결돼 있지 않다는 걸 발견했다"는 한 문장을 더하면 구조를 읽고 빈틈을 찾아낸 인상을 줍니다.
다음 액션
구글시트 매출 데이터를 AI가 매일 리포트로 요약하는 편에서 스케줄 → 데이터 조회 → AI 분석 → 알림이라는 기본 뼈대를 다뤘습니다. 이 워크플로우는 그 뼈대에 이미지 분석 단계를 더 얹은 버전입니다. 다음 편은 입력이 다시 바뀝니다. 유튜브 영상 자막을 서브워크플로우로 뽑아 AI가 요약하는 흐름인데, "외부 콘텐츠를 가져와 AI로 정제한다"는 패턴이 텍스트 쪽에서 반복됩니다.
