실습 핵심 학습 개념 기본 노드 사용
식당메뉴알림 • OCR(Optical Character Recognition) • Form Trigger

[n8n workflow]

image.png

1. 폼 업로드

Form Elements에 이미지 파일 확장자를 올릴 수 있도록 제한하여 이미지 업로드 URL 생성

2. HTTP Request: Upstage OCR API

image.png

위의 양식과 같이 업로드된 주간 식단표에서 텍스트 추출

표에 어디에 해당하는지 모르기 때문에 boundingBox 영역과 text 정보 둘 다 필요하므로, Edit Fields에서 {{ $json.pages[0].words }} 사용

{
	"boundingBox": {
		"vertices": [
			{
				"x": 111,
				"y": 126
			},
			{
				"x": 166,
				"y": 126
			},
			{
				"x": 166,
				"y": 138
			},
			{
				"x": 111,
				"y": 138
			}
		]
	},
	"confidence": 0.9704,
	"id": 43,
	"text": "고추장불고기"
}

3. Basic LLM Chain

3-1. System Message

당신은 식단표 OCR 구조 복원 전문가이다.

입력 데이터는 OCR 엔진이 추출한 JSON 배열이다.

각 항목은 다음 정보를 가진다.

* text
* boundingBox.vertices[].x
* boundingBox.vertices[].y

중요:
OCR 출력 순서는 신뢰하지 마라.
반드시 좌표(x,y)를 사용하여 표 구조를 복원해야 한다.

작업 절차

1. 요일 헤더 찾기

상단 영역에서 다음 텍스트를 찾는다.

월
화
수
목
금
토
일

각 요일의 x좌표 중심값을 구한다.

2. 컬럼 생성

각 OCR 항목은 x좌표를 기준으로 가장 가까운 요일 컬럼에 배정한다.

3. 행 구분 찾기

다음 텍스트를 행 구분자로 사용한다.

MODU
WAA
간편식
추가코너
차코너

각 행의 y좌표를 기준으로 영역을 생성한다.

4. 메뉴 배치

각 OCR 항목을

* 요일(x좌표)
* 메뉴구역(y좌표)

으로 분류한다.

5. 제외 대상

다음은 출력하지 않는다.

CAFETERIA
MENU
롯데물산
직원식당
요일
날짜
원산지
문의사항
주요안내
이메일
워터마크
해시태그
이벤트문구
광고문구
NEW
중식
석식
배너문구

6. 메뉴 정리

동일 메뉴 중복 제거

y좌표 순으로 정렬

의미 없는 문자 제거

예)

■

#

"
,
.

등

7. OCR 오류 보정

오이피를 → 오이피클
깍투기 → 깍두기

처럼 명확한 오타는 수정한다.

확신이 없으면 원문 유지

8. 출력 형식

출력은 반드시 JSON만 출력한다.

각 요일 객체에는 반드시 date 필드를 포함한다.
date는 상단 날짜 영역에서 추출한다.
예: "06월 15일"

3-2. Model

Google Gemini Chat Model 사용

3-3. Output Parser: Structured Output Parser

{
  "월": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "화": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "수": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "목": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "금": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "토": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  },
  "일": {
    "date": "",
    "MODU": [],
    "WAA": [],
    "간편식": [],
    "추가코너": [],
    "차코너": []
  }
}

4. Code in JavaScript

오늘 날짜에 해당하는 식단 정보만 markdown으로 꾸며서 전송하도록 코드 추가

const raw = $input.first().json.output;
const menu = typeof raw === 'string' ? JSON.parse(raw) : raw;

const now = new Date();
const koreaNow = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Seoul' }));

const dayMap = ['일', '월', '화', '수', '목', '금', '토'];
const today = dayMap[koreaNow.getDay()];

const todayMenu = menu[today];

if (!todayMenu) {
  return [{
    json: {
      markdown: `🍽️ **오늘의 식단 (${today}요일)**\n\n해당 요일의 식단 정보를 찾지 못했습니다.`
    }
  }];
}

const sections = [
  { key: 'MODU', icon: '🍱' },
  { key: 'WAA', icon: '🍜' },
  { key: '간편식', icon: '🥗' },
  { key: '추가코너', icon: '🍚' },
  { key: '차코너', icon: '☕' },
];

let markdown = `🍽️ **오늘의 식단 (${today}요일`;

if (todayMenu.date) {
  markdown += ` / ${todayMenu.date}`;
}

markdown += `)**\n\n`;

for (const section of sections) {
  const items = todayMenu[section.key];

  if (!Array.isArray(items) || items.length === 0) continue;

  markdown += `**${section.icon} ${section.key}**\n`;
  markdown += items.map(v => `- ${v}`).join('\n');
  markdown += `\n\n`;
}

return [{
  json: {
    today,
    date: todayMenu.date || '',
    markdown
  }
}];