66일차 교안/실습 추가

This commit is contained in:
mcchae committed 2026-10-15 22:45:16 +09:00
1 parent 787d7d8514
commit d7dc9dd68a
52 files changed
+6144

No files matched your search

+305
View File
@@ -0,0 +1,305 @@
# 66일차 개요 · 단일 업무 자동화 워크플로우 구현
> 노코드 멀티 에이전트 워크플로우 설계 및 운영 · 2026-10-16
> 15장 · 각 장은 가로줄(`---`)로 나뉩니다. 1교시에는 1~3장과 14장만 보고, 나머지 장은 각 단계를 설명할 때 함께 띄웁니다.
| 장 | 범위 |
| --- | --- |
| 1~3 | 표지 · 오늘의 목표 · 시간표 |
| 4~7 | 1단계 · 업무 설계와 데이터 흐름 |
| 8~10 | 2단계 · 파싱과 변환 노드 |
| 11~13 | 3단계 · 반복·병렬과 완성 |
| 14~15 | 실습 · 퀴즈와 예고 |
---
## 1 / 15 · 표지
```mermaid
flowchart LR
M["회의록<br/>1건"] --> A1["할 일"] & A2["할 일"] & A3["할 일"] & A4["할 일"]
A1 & A2 & A3 & A4 --> R["결과<br/>1건"]
classDef d fill:#0F3D3E,stroke:#0F3D3E,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
class M d
class A1,A2,A3,A4 o
class R t
```
**66일차**
# 단일 업무 자동화 워크플로우 구현
쉬운 예제 2개 → 고쳐 보는 과제 · 데이터 파싱·변환·집계 · 반복과 병렬
<sub>노코드 멀티 에이전트 워크플로우 설계 및 운영 · 2026-10-16</sub>
---
## 2 / 15 · 오늘 할 수 있게 될 세 가지
| | 목표 | 내용 |
| :-: | --- | --- |
| **①** | **업무 하나를 노드와 데이터로 설계한다** | 목적·입출력·성공 기준 · TCA → 노드 목록 · 연결선마다 항목 수와 필드 · 제어 구조 고르기 |
| **②** | **LLM의 답을 데이터로 바꾼다** | JSON 스키마 출력 · 문자열 파싱 · 검증과 기본값 · 실패 이유 남기기 · Edit Fields 변환 |
| **③** | **반복과 병렬로 워크플로우를 완성한다** | Split Out → Aggregate · Fork-Join(Merge) · 같은 결과 형식 · Python 호출 |
---
## 3 / 15 · 오늘의 시간표
| ① 1~2교시 | ② 3~4교시 | ③ 5~6교시 | ④ 7~8교시 |
| --- | --- | --- | --- |
| **1단계** | **2단계** | **3단계** | **정리** |
| 업무 설계와 데이터 흐름 | 파싱과 변환 노드 | 반복·병렬과 완성 | 퀴즈 A·B·C · 오늘의 정리·예고 |
| Lab 01 예제 2 + 과제 | Lab 02 예제 2 + 과제 | Lab 03 예제 2 + **과제 40분** | 퀴즈 20분 · Python 호출 |
> 09:00 시작 · 50분 수업 10분 휴식 · 12:00~13:00 점심
---
## 4 / 15 · 오늘의 업무: 회의록 → 할 일 정리
| 지금 방식 | 자동화 후 |
| --- | --- |
| 회의록을 읽고 할 일·담당자·기한을 직접 찾음 | 회의록을 보내면 요약·할 일 목록·정리문 |
| 빠진 담당자·기한을 놓치기 쉬움 | 빠진 값은 `미정`으로 바로 보임 |
| 급한 일이 묻힘 | 오늘·내일 마감은 우선순위 `높음` |
| 범위를 좁히는 세 줄 | 오늘의 답 |
| --- | --- |
| **목적 한 문장** | 회의록을 받으면 할 일 목록과 공유용 정리문을 돌려준다 |
| **입력과 출력** | `{title, notes}` → `{title, route, count, summary, actions, report_text}` |
| **성공 기준** | `weekly`에서 할 일 4건, 빠진 값은 `미정`, 오늘 마감은 `높음` |
> **단일 업무로 좁히기** 오늘은 회의록 → 할 일 정리 → 응답까지. 알림·일정 등록은 나중에 확장
---
## 5 / 15 · 예시 데이터로 결과를 먼저 그려 보기
| # | 할 일 | 담당자 | 기한 | 우선순위 | 누가 정하나 |
| --- | --- | --- | --- | --- | --- |
| 1 | 신규 고객 문의 응답 템플릿 정리 | 이준호 | 금요일 | 보통 | 찾기는 **LLM** |
| 2 | VPN 장애 보고서 작성 | 박서연 | 다음 주 월요일 | 보통 | |
| 3 | 서버 점검 일정 공지 | 김민지 | 오늘 | **높음** | 우선순위는 **규칙** |
| 4 | 회의실 예약 규칙 개선 담당자 선정 | **미정** | 10월 23일 | 보통 | 빈 값 채우기는 **규칙** (기한은 LLM이 "다음 회의"에서 추론) |
> **LLM은 찾기만, 채우기·우선순위·정리문은 규칙** · 결과 형식(JSON)은 노드를 만들기 전에 약속한다 · 할 일이 없어도 같은 필드(`route: 할 일 없음`, `count: 0`, `actions: []`)
---
## 6 / 15 · 상자(항목)가 몇 개 지나가나 · 1 → 4 → 1
```mermaid
flowchart LR
A["회의록<br/>1개"] -->|"LLM + 파싱"| B["1개<br/>안에 actions [4]"] -->|"Split Out<br/>1 → 4"| C["할 일<br/>4개"] -->|"항목마다 정리"| D["정리된 할 일<br/>4개"] -->|"Aggregate<br/>4 → 1"| E["결과<br/>1개"]
classDef d fill:#0F3D3E,stroke:#0F3D3E,color:#fff
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
classDef m fill:#5B8E7D,stroke:#5B8E7D,color:#fff
class A d
class B,E t
class C o
class D m
```
| 노드 | 상자 수 |
| --- | --- |
| Edit Fields · **If** · Switch | **그대로** (If는 한쪽 길로 보낼 뿐) |
| **Split Out** | **1 → N** (목록을 펼침) |
| **Aggregate** | **N → 1** (다시 모음) |
| Merge | 두 길 → 하나 |
> **Lab 01 예제 1** 어제 흐름은 모두 1 · **예제 2** 파싱 직후는 아직 1 (목록이 상자 하나에 들어 있음) → Split Out 뒤에 4
---
## 7 / 15 · 제어 구조 고르기 · 요청이 바뀌면
| 조건 분기 | 반복 | 병렬 (Fork-Join) |
| :-: | :-: | :-: |
| **입력에 따라 다른 결과** | **목록의 항목마다 같은 처리** | **서로 결과가 필요 없는 일** |
| 내용 없음 → 거절<br/>할 일 없음 → 다른 결과 | 할 일마다 우선순위 | 할 일 찾기와 요약 |
| If · Switch | Split Out → … → Aggregate | 가지 나누기 + Merge |
| 요청이 바뀌면 묻는 세 질문 | 고칠 곳 |
| --- | --- |
| 할 일 **하나마다** 정하는 값? | Split Out과 Aggregate **사이** (예: `priority`) |
| **모두 모은 뒤**에야 아는 값? | Aggregate **뒤** (예: `count`) |
| 결과 필드가 늘었나? | **모든 길**의 끝 |
> **Lab 01 과제** "담당자·기한이 없으면 확인 필요, 몇 건인지도" → 오후 Lab 03 과제에서 실제로 만든다
---
## 8 / 15 · LLM의 답은 글자다 → 파싱
```text
Fake LLM answer content: "{\"actions\": [{\"task\": \"...\", \"owner\": \"\", ...}]}" ← 글자 한 줄
Parse actions { actions: [ {task, owner: "미정", due: "미정"}, ... ], count: 2 } ← 목록
```
| ① 꺼내기 | ② 글자 → 데이터 | ③ 빈 할 일 버리기 | ④ 빈 값은 `미정` |
| :-: | :-: | :-: | :-: |
| `content` (진짜 LLM은 `choices[0].message.content`) | 실패하면 빈 목록 + `error` | | |
> **Lab 02 예제 1** LLM 없이 가짜 답으로 파싱 연습 · 끝 `}`를 지우면 `error: answer is not valid JSON` → 실패해도 **같은 모양**, 이유는 남긴다
---
## 9 / 15 · 답의 모양 알려 주기 · 하나 더 뽑기
| LLM 노드 JSON 칸 | 하는 일 | 비유 |
| --- | --- | --- |
| ① 스키마 `response_format` | 답의 **모양** | 빈칸이 정해진 답안지 |
| ② 지시문 `system` | 무엇을 찾을지, 없으면 `미정` | 문제 설명 |
| ③ 회의록 `user` | 읽을 글 | 지문 |
```mermaid
flowchart LR
A["① 스키마에 칸 추가"] --> B["② 지시문에 한 문장"] --> L{{"LLM 답<br/>(글자)"}} --> C["③ 파싱 코드 return"] --> R["결과"]
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
class A,B,C t
class L o
```
> **스키마는 모양만, 내용은 보장 안 함** · **Lab 02 과제** 참석자도 뽑기 = 세 곳 고치기 (③을 빠뜨리면 LLM 답에는 있어도 결과에서 사라짐) · `}}`가 붙으면 `invalid syntax` → `} }`
---
## 10 / 15 · Edit Fields와 만들고 확인하는 순서
| 하고 싶은 일 | Edit Fields 표현식 |
| --- | --- |
| 웹훅 입력 꺼내기 | `{{ $json.body.notes }}` |
| 비었으면 기본값 | `{{ $json.body.title \|\| '제목 없음' }}` |
| 여러 단어 중 하나라도 포함 | `{{ ['오늘','내일','긴급'].some(w => $json.due.includes(w)) }}` |
```mermaid
flowchart LR
A["노드 하나 추가"] --> B["이름 짓기"] --> C["설정"] --> D["실행"] --> E["OUTPUT 확인"]
E -->|"다음 노드"| A
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
class A,B,C t
class D,E o
```
> `{{ }}` 값은 **Expression** 모드 · 같은 요청 파일(`weekly`·`no-action`·`empty`)로 매번 확인 · Test URL은 요청마다 Listen 다시
---
## 11 / 15 · 반복과 병렬 · Lab 03 예제 1·2
```mermaid
flowchart LR
T(["Start"]) --> S["Sample actions<br/>[3]"] -->|"1 → 3"| SO["Split Out"] --> N["Normalize<br/>3번"] -->|"3 → 1"| AG["Aggregate"] -->|"Input 1"| J["Merge<br/>Position"]
T --> SS["Sample summary"] -->|"Input 2"| J
J --> O["{actions, summary}<br/>1개"]
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
classDef m fill:#5B8E7D,stroke:#5B8E7D,color:#fff
classDef p fill:#7A6FB0,stroke:#7A6FB0,color:#fff
class S,SS o
class SO,N,AG m
class J p
```
| 펼쳤으면 반드시 모은다 (수업 전 실측) | 결과 |
| --- | --- |
| 모으지 않고 바로 응답 | 200, **첫 할 일 하나만** (조용히 틀림) |
| 모으지 않고 `Build report` | 500 |
> Merge는 **두 입력이 모두 와야** 합침 · 한 길이 실패하면 응답 없음 · 항목마다 LLM을 부르면 Loop Over Items, 시간·비용은 항목 수에 비례
---
## 12 / 15 · 가짜를 진짜로 · Lab 03 과제 40분
| 예제의 가짜 | 과제의 진짜 |
| --- | --- |
| `Sample actions` | `LLM extract actions` → `Parse actions` (Lab 02) |
| `Sample summary` | `LLM summarize` → `Keep summary` |
| Merge · Split Out · Normalize · Aggregate | **복사해 붙이기** |
| (없음) | `Any actions?` · `Build report` · `No actions` · **확인 필요** |
```mermaid
flowchart LR
P["할 일 목록"] --> J["Merge"]
K["요약"] --> J
J --> A{"할 일<br/>있음?"} -->|"예"| L["펼치고 · 정리 · 모으기"] --> B["Build report"]
A -->|"아니오"| NA["No actions"]
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
classDef p fill:#7A6FB0,stroke:#7A6FB0,color:#fff
class A o
class P,K,L,B,NA t
class J p
```
> Merge를 **먼저** → 할 일 없음 길에도 요약이 있다 · 두 길 모두 `{title, route, count, summary, actions, need_check}`
---
## 13 / 15 · 기대 결과로 검증하고, 밖에서 부르기
| 요청 | 기대 결과 (수업 전 측정) |
| --- | --- |
| `weekly` | **할 일 정리** · `count: 4` · 오늘 마감 1건 `높음` · `need_check: 1` · 약 3~4초 |
| `no-action` | **할 일 없음** · `count: 0` · `need_check: 0` · 약 2초 |
| `empty` | **HTTP 400** · 1초 미만 |
| 오류 없이 틀리는 모양 (실측) | 원인 |
| --- | --- |
| 할 일 하나만 | Aggregate 빠짐 |
| `no-action`이 빈 응답 | false 길 끊김 |
| `need_check: 4` | `check`를 String으로 |
```mermaid
flowchart LR
APP["call('meeting')"] --> CFG["platforms.json<br/>웹훅 경로 한 줄"]
CFG --> N8["n8n · 내 워크플로우"]
CFG --> MF["miniflow · 08 flow"]
classDef d fill:#0F3D3E,stroke:#0F3D3E,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
classDef t fill:#0F7C80,stroke:#0F7C80,color:#fff
classDef m fill:#5B8E7D,stroke:#5B8E7D,color:#fff
class APP d
class CFG o
class N8 t
class MF m
```
> 실행 기록 항목 수 1 → 4 → 4 → 1 → 1 · 같은 입력·같은 결과 형식 → **설정 한 줄**로 대상 변경 (8교시 함께)
---
## 14 / 15 · 오늘의 실습
| | 실습 | 예제 1 | 예제 2 | 과제 (생각하기) |
| --- | --- | --- | --- | --- |
| **1** | **Lab 01** · 설계 | 어제 흐름의 항목 수 | 오늘 흐름 빈칸 (1 → 4 → 1) | "확인 필요" 요청 → 어디를 고치나 |
| **2** | **Lab 02** · 파싱 | LLM 없이 글자 → 목록 | LLM 붙은 1부 가져와 실행 | 참석자도 뽑기 (세 곳) |
| **3** | **Lab 03** · 완성 | 펼치고 모으기 (1 → 3 → 1) | 두 길을 Merge로 | **40분** · 내 워크플로우 완성 + 확인 필요 |
<sub>예제는 따라 하기, 과제는 생각하기 · 환경: 65일차 n8n · Credential `OpenAI (.env)` 그대로 · 막히면 `n8n/workflows`의 파일과 비교</sub>
---
## 15 / 15 · 7교시 퀴즈 · 그리고 내일
| A · 기본 | B · 응용 | C · 심화 |
| :-: | :-: | :-: |
| 데이터 노드 · 제어 구조 용어 | 항목 수 · 표현식 · 노드 고르기 | 흐름 오류 찾기 · 설계 판단 |
- 단계별 10문항 · 20분 · 원하는 단계 선택
- 마지막 주관식 '더 알고 싶은 것'은 선택 · 점수 없음
> **67일차 예고**
> AI Agent가 Tool을 골라 쓰는 **Agent·Tool·데이터 연계**
> 실행 결과 검증
### *오늘의 한 문장: 연결선마다 무엇이 몇 개 흐르는지 알면, 워크플로우는 틀리지 않는다*
+80
View File
@@ -0,0 +1,80 @@
# 66일차 실습 준비 · 수업 전에 미리 해 두면 좋은 것
- 수업일: 2026-10-16 (66일차) · 단일 업무 자동화 워크플로우 직접 만들기
- 준비 시간: 약 20분 (새로 받을 이미지·모델 없음)
- 먼저 WSL에서 `cd ~/aio-01d4-stu && git pull`로 오늘 자료를 받습니다 (수강생 저장소: https://gl.isitsoft.kr/publicproj/aio-01d4-stu).
- **수업 전날까지** 1~2절을 끝내 주세요. 3~4절은 시간이 될 때 읽어 오면 좋습니다.
```mermaid
flowchart LR
A["1. 어제 환경 켜기<br/>n8n · .env 키"] --> B["2. 확인<br/>pytest · OpenAI · n8n 로그인"]
B --> C["3. 미리 읽기<br/>JSON 객체·목록"]
C --> D["4. 생각해 오기<br/>항목 수가 바뀌는 곳"]
```
---
## 1. 어제 환경 다시 켜기 (전날 필수, 5분)
오늘은 65일차에 만든 n8n(Credential `OpenAI (.env)`)과 `Last3Mon/.env`를 그대로 씁니다. Ollama는 대체용으로 함께 켜 둡니다.
```bash
sudo systemctl stop ollama # WSL의 Ollama가 켜져 있다면 멈춤
cd ~/aio-01d4-stu/Last3Mon/65/n8n
docker compose start
docker compose ps # n8n, ollama 모두 running
```
브라우저에서 http://localhost:5678 에 로그인합니다. 65일차 준비를 하지 못했다면 [n8n 준비 안내](../../Last3Mon/65/SETUP-N8N.md)를 먼저 끝내 주세요.
## 2. 실습 엔진 확인 (전날 필수, 5분)
```bash
cd ~/aio-01d4-stu/Last3Mon/66/workflow-lab
uv sync
uv run pytest -q # 43 passed, 1 skipped
```
---
## 3. 미리 읽어 오면 좋은 내용 (선택, 약 10분)
### 3.1 JSON의 객체와 목록
| 모양 | 이름 | 예 |
| --- | --- | --- |
| `{ }` | 객체 (이름: 값의 묶음) | `{"task": "보고서 작성", "owner": "박서연", "due": "월요일"}` |
| `[ ]` | 목록 (값을 순서대로) | `[{...}, {...}, {...}]` |
| `"..."` | 문자열 (글자) | `"{\"task\": \"보고서\"}"` ← JSON처럼 보이지만 **글자** |
오늘 LLM은 할 일 목록을 **글자**로 돌려줍니다. 이 글자를 실제 목록으로 바꾸는 작업을 **파싱**이라고 합니다. 64일차 준비 안내의 JSON 기초도 함께 보세요.
### 3.2 오늘 처음 쓰는 n8n 노드
| 노드 | 한 줄 설명 |
| --- | --- |
| Edit Fields (Set) | 필드를 만들고 값을 계산 |
| Code | JavaScript로 데이터 가공 |
| Split Out | 목록 하나를 항목 여러 개로 펼침 |
| Aggregate | 항목 여러 개를 목록 하나로 모음 |
| Merge | 두 갈래의 결과를 하나로 합침 |
---
## 4. 생각해 오기 (선택, 5분)
회의록 1건에서 할 일 4건이 나왔습니다. 할 일마다 우선순위를 정한 뒤 다시 하나의 결과로 돌려주려면, 데이터의 개수가 어떻게 바뀌어야 할까요?
```text
회의록 1건 → ? → ? → 결과 1건
```
## 5. 당일 준비물
- 충전된 노트북과 충전기, WSL의 Docker, Windows Terminal(WSL), 웹 브라우저
## 확인
- [ ] `docker compose ps` → n8n, ollama running
- [ ] `uv run pytest -q` → `43 passed, 1 skipped`
- [ ] JSON 객체·목록·문자열의 차이를 읽었다
@@ -0,0 +1,260 @@
# 1단계 · 단일 업무 자동화 설계와 데이터 흐름
- 시간: 1~2교시 (1교시 앞부분은 [오늘의 개요](00-DAY66-OVERVIEW.md))
- 학습 목표
- 하나의 업무를 목적 하나로 좁히고, 결과를 먼저 그려 볼 수 있다.
- 노드 사이를 지나가는 **항목(item)** 의 개수를 읽고 쓸 수 있다 (1 → 4 → 1).
- LLM이 할 일과 규칙으로 할 일을 나눌 수 있다.
- 조건 분기·반복·병렬 중 어떤 제어 구조가 필요한지 고를 수 있다.
- 요청이 바뀌었을 때 **어느 노드를 고치면 되는지** 찾을 수 있다.
- 실습: [Lab 01 · 회의록 자동 정리 워크플로우 설계하기 (예제 2개 + 과제)](../../Last3Mon/66/LAB-01-DESIGN-DATA-FLOW.md)
```mermaid
flowchart LR
A["1. 오늘의 업무"] --> B["2. 결과 먼저"] --> C["3. 항목 읽기<br/>(Lab 01 예제 1)"] --> D["4. 1 → 4 → 1<br/>(Lab 01 예제 2)"] --> E["5. 제어 구조"] --> F["6. 요청이 바뀌면<br/>(Lab 01 과제)"]
```
오늘 수업은 모두 같은 순서로 갑니다. **설명 → 쉬운 예제 2개를 함께 → 그것을 고치는 과제를 혼자**. 이 교안의 절도 Lab의 예제·과제 순서에 맞춰 놓았습니다.
---
## 1. 오늘의 업무: 회의록 → 할 일 정리
64일차 시나리오 S1 "회의록 요약 후 할 일 정리"를 n8n으로 만듭니다.
| 지금 (AS-IS) | 자동화 후 (TO-BE) |
| --- | --- |
| 누군가 회의록을 읽고 할 일·담당자·기한을 찾아 정리 | 회의록을 보내면 요약·할 일 목록·정리문이 바로 나옴 |
| 담당자나 기한이 빠진 일을 놓침 | 빠진 값은 `미정`으로 보임 |
| 급한 일이 묻힘 | 오늘·내일 마감은 우선순위 `높음` |
### 1.1 목적 하나로 좁히기
처음 만드는 자동화는 **목적 하나**만 해결합니다. 메일 발송·일정 등록까지 한 번에 넣으면 어디가 틀렸는지 찾기 어렵습니다.
| 질문 | 오늘의 답 (한 문장씩) |
| --- | --- |
| 목적 | 회의록을 받으면 할 일 목록과 정리문을 돌려준다 |
| 입력 → 출력 | `{title, notes}` → `{title, route, count, summary, actions, report_text}` |
| 성공 기준 | 예시 회의록(`weekly`)에서 할 일 4건, 빠진 값은 `미정`, 오늘 마감은 `높음` |
세 줄이 한 문장씩으로 끝나지 않으면 아직 범위가 넓은 것입니다. 회의록이 **비어 있으면** LLM을 부르기 전에 맨 앞에서 거절(HTTP 400)합니다. 쓸데없는 호출과 비용을 막는 가장 싼 방법입니다.
---
## 2. 결과를 먼저 그려 보기
노드를 생각하기 전에 **예시 입력 한 건과 기대하는 결과**를 손으로 적습니다.
> 참석: 김민지, 이준호, 박서연. 1) 신규 고객 문의 응답 템플릿을 이준호가 금요일까지 정리한다. 2) 박서연은 다음 주 월요일까지 VPN 장애 보고서를 작성한다. 3) 서버 점검 일정은 김민지가 오늘 중 공지한다. 4) 회의실 예약 규칙 개선은 담당자를 다음 회의에서 정한다. 다음 회의는 10월 23일.
| # | 할 일 | 담당자 | 기한 | 우선순위 |
| --- | --- | --- | --- | --- |
| 1 | 신규 고객 문의 응답 템플릿 정리 | 이준호 | 금요일 | 보통 |
| 2 | VPN 장애 보고서 작성 | 박서연 | 다음 주 월요일 | 보통 |
| 3 | 서버 점검 일정 공지 | 김민지 | 오늘 | **높음** |
| 4 | 회의실 예약 규칙 개선 담당자 선정 | **미정** | 10월 23일 | 보통 |
이 표를 채우다 보면 일이 두 종류로 나뉩니다.
| 누가 | 하는 일 | 왜 |
| --- | --- | --- |
| **LLM** | 문장 속에서 할 일·담당자·기한 **찾기**, 요약 | 사람 말을 읽어야 함 |
| **규칙** | 빈 값을 `미정`으로 **채우기**, 우선순위 **정하기**, 정리문 **만들기** | 항상 같은 답이어야 함 |
> 4번의 기한은 회의록에 직접 적혀 있지 않지만, 수업 전 측정(`gpt-4.1-mini`)에서 LLM은 매번 "다음 회의는 10월 23일"을 읽고 `10월 23일`로 채웠습니다. 담당자는 적혀 있지 않아 `미정`입니다. LLM이 **추론한 값**을 받아들일지도 설계에서 정할 일입니다.
### 2.1 결과 형식을 먼저 약속하기
결과를 받는 쪽(사람, 다른 시스템, 65일차의 플랫폼 전환 코드)이 이 모양에 맞춰 만들어지므로, 노드를 만들기 **전에** 정합니다.
```json
{"title": "10월 16일 주간 회의", "route": "할 일 정리", "count": 4, "summary": "요약 2문장",
"actions": [{"task": "...", "owner": "...", "due": "...", "priority": "높음 | 보통"}],
"report_text": "공유용 정리문"}
```
할 일이 없어도 **같은 필드**를 쓰고 `route: 할 일 없음`, `count: 0`, `actions: []`로 둡니다 (65일차 "모든 길이 같은 모양으로 끝나게").
---
## 3. 항목(item) 읽기 · Lab 01 예제 1
n8n에서 노드와 노드 사이를 지나가는 데이터 한 덩어리를 **항목(item)** 이라고 합니다. 택배 상자 하나라고 생각하면 됩니다. 상자 안에는 필드(`user`, `text` ...)가 들어 있습니다.
어제 Lab 02A에서 만든 흐름을 보면서 상자가 몇 개 지나가는지 셉니다.
```mermaid
flowchart LR
T(["Start"]) -->|"1"| S["Sample request<br/>Edit Fields"] -->|"1"| I{"Is urgent?<br/>If"}
I -->|"true · 1"| U["Urgent route"]
I -.->|"false · 0"| N["Normal route"]
```
- 모든 연결선이 **1개**입니다. If는 상자를 **한쪽 길로 보낼 뿐** 개수를 바꾸지 않습니다.
- 개수를 바꾸는 노드는 따로 있습니다. 오늘 새로 쓰는 노드입니다.
| 노드 | 하는 일 | 항목 수 |
| --- | --- | --- |
| Edit Fields (Set) | 필드 만들기·값 계산 | 그대로 |
| If · Switch | 길 나누기 | 그대로 (한쪽으로) |
| Code | 무엇이든 (파싱·검증) | 코드가 정함 |
| **Split Out** | 상자 속 목록을 **상자 여러 개로 펼침** | **1 → N** |
| **Aggregate** | 상자 여러 개를 **목록 하나로 모음** | **N → 1** |
| **Merge** | 두 길에서 온 상자를 **합침** | 둘 → 하나 |
| Filter | 조건에 맞는 상자만 남김 | 줄어듦 |
---
## 4. 오늘 흐름의 항목 수 1 → 4 → 1 · Lab 01 예제 2
회의록 1건이 할 일 4건이 되었다가 다시 결과 1건이 됩니다.
```mermaid
flowchart LR
A["회의록<br/>상자 1개"] -->|"LLM + 파싱"| B["상자 1개<br/>안에 actions 목록 [4]"] -->|"Split Out<br/>1 → 4"| C["할 일<br/>상자 4개"] -->|"항목마다 정리"| D["정리된 할 일<br/>상자 4개"] -->|"Aggregate<br/>4 → 1"| E["결과<br/>상자 1개"]
```
가장 자주 헷갈리는 곳은 **B**입니다. 할 일이 4건이라도 파싱 직후에는 **상자 1개 안에 목록으로** 들어 있습니다. Split Out을 지나야 비로소 상자 4개가 됩니다.
오늘 완성할 전체 그림과 각 노드의 역할입니다. (오후 Lab 03 과제의 완성 그림과 같습니다.)
```mermaid
flowchart LR
W(["Meeting webhook"]) --> S["Settings"] --> H{"Has notes?"}
H -->|false| R0["Reject empty<br/>400"]
H -->|true| L1{{"LLM extract actions"}} --> P["Parse actions"] --> J["Join results<br/>Merge"]
H -->|true| L2{{"LLM summarize"}} --> K["Keep summary"] --> J
J --> A{"Any actions?"}
A -->|true| SO["One item per action<br/>Split Out"] --> N["Normalize action"] --> AG["Collect actions<br/>Aggregate"] --> B["Build report"] --> R["Send result"]
A -->|false| NA["No actions"] --> R
```
| 설계에서 | n8n 노드 | 언제 만드나 |
| --- | --- | --- |
| 회의록 받기, 설정 모으기, 비었으면 거절 | Webhook, Edit Fields, If, Respond | Lab 02 예제 2 (가져옴) |
| 할 일 찾기 (LLM) → 글자를 목록으로 | HTTP Request, Code | Lab 02 예제 1·2 |
| 요약 (LLM)과 합치기 | HTTP Request, Edit Fields, Merge | Lab 03 예제 2 → 과제 |
| 할 일마다 우선순위 → 모으기 → 정리문 | Split Out, Edit Fields, Aggregate, Code | Lab 03 예제 1 → 과제 |
| 할 일이 없을 때 | If, Edit Fields | Lab 03 과제 |
### 4.1 연결선 표에 적는 세 가지
| 적는 것 | 예 | 빠뜨리면 |
| --- | --- | --- |
| **항목 수** | Split Out 뒤 4 | 응답이 할 일 하나만 나가는 것을 늦게 알아챔 |
| **필드 이름** | `actions`, `count`, `summary` | 표현식(`{{ $json.actions }}`)에 없는 이름 → 빈 값 |
| **형식** | LLM 바로 뒤는 **글자** | LLM 답을 목록으로 착각 → 2단계의 파싱이 필요 |
---
## 5. 제어 구조 고르기
질문 세 개로 고릅니다.
| 질문 | 예이면 | n8n | 오늘의 예 |
| --- | --- | --- | --- |
| 입력에 따라 **다른 결과**를 내야 하나? | 조건 분기 | If, Switch | 내용 없음 → 거절, 할 일 없음 → 다른 결과 |
| 목록의 **항목마다** 같은 일을 하나? | 반복 | Split Out → … → Aggregate | 할 일마다 우선순위 |
| 서로의 **결과가 필요 없는** 일이 둘 이상인가? | 병렬 (Fork-Join) | 가지 나누기 + Merge | 할 일 찾기와 요약 |
### 5.1 연습
| 업무 | 제어 구조 |
| --- | --- |
| 문의 메일 50통을 각각 분류 | 반복 |
| 리뷰의 감정 분석과 키워드 추출 | 병렬 |
| 감정이 부정이면 담당자에게, 아니면 자동 답글 | 조건 분기 |
| 회의록 요약을 만든 뒤 그 요약으로 제목 짓기 | **순서** (제목에 요약 결과가 필요 → 병렬 불가) |
| 출장자마다 날씨 조회 후 우산 필요 여부 판단 | 반복 + 조건 분기 |
실제 업무는 여러 구조가 섞이고, **앞 결과가 필요하면 병렬로 나눌 수 없습니다.**
---
## 6. 요청이 바뀌면 어디를 고치나 · Lab 01 과제
설계를 해 두면 요청이 바뀌었을 때 고칠 곳을 금방 찾습니다. 찾는 순서는 세 질문입니다.
```mermaid
flowchart LR
Q1{"LLM이 할 일?<br/>규칙이 할 일?"} --> Q2{"할 일 하나마다?<br/>모두 모은 뒤?"} --> Q3{"모든 길이<br/>같은 필드로 끝나나?"}
```
| 질문 | 고칠 곳 |
| --- | --- |
| 할 일 **하나마다** 정하는 값인가? | 상자가 하나씩 흐르는 곳 = Split Out과 Aggregate **사이** |
| **모두 모은 뒤**에야 알 수 있는 값인가? (개수, 합계) | Aggregate **뒤** |
| 결과에 필드가 늘었나? | **모든 길**의 끝 (할 일 없음 길 포함) |
연습 삼아 이미 있는 값으로 확인해 봅니다.
| 값 | 하나마다? 모은 뒤? | 그래서 만드는 곳 |
| --- | --- | --- |
| `priority` (오늘·내일이면 높음) | 하나마다 | `Normalize action` |
| `count` (할 일 개수) | 모은 뒤 | `Build report` |
| `route` | 길마다 | `Build report` / `No actions` |
Lab 01 과제는 이 방법으로 "확인 필요" 요청을 설계하고, 오후 Lab 03 과제에서 실제로 만듭니다.
---
## 7. Lab 01 안내
- 실습 안내: [Last3Mon/66/LAB-01-DESIGN-DATA-FLOW.md](../../Last3Mon/66/LAB-01-DESIGN-DATA-FLOW.md) (22분, 활동지 `templates/LAB-01-WORKSHEET.md`)
| 순서 | 시간 | 할 일 | 교안 |
| --- | --- | --- | --- |
| 예제 1 | 5분 | 어제 흐름의 연결선 항목 수 읽기 | 3절 |
| 예제 2 | 7분 | 오늘 흐름의 빈칸 다섯 개 (1 → 4 → 1, 병렬 이유, LLM이 하는 일) | 2·4·5절 |
| 과제 | 10분 | "담당자·기한이 없는 할 일은 **확인 필요**로, 몇 건인지도" → 어디를 고치나 | 6절 |
- 전체 설계서(`templates/DATA-FLOW-DESIGN-TEMPLATE.md`)는 오늘 복습 과제에서 **내 업무**로 써 봅니다.
---
## 8. 자주 하는 설계 실수
| 실수 | 증상 | 고치는 법 |
| --- | --- | --- |
| 한 번에 알림·일정까지 넣음 | 어디가 틀렸는지 찾기 어려움 | 목적 하나로, 나머지는 "나중에" |
| LLM에게 우선순위·기본값까지 맡김 | 같은 회의록인데 결과가 매번 다름 | 찾기는 LLM, 채우기·계산은 규칙 |
| 항목 수를 안 적음 | 응답에 할 일이 하나만 | 1 → N → 1 표시 |
| 결과가 필요한 일을 병렬로 | 요약 전에 제목을 지으려 함 | "앞 결과가 필요한가?" |
---
## 9. 스스로 확인하기
<details><summary>Q1. If 노드를 지나면 항목 수는 어떻게 되나요?</summary>
그대로입니다. If는 항목을 true나 false 한쪽 길로 보낼 뿐 개수를 바꾸지 않습니다. 개수를 바꾸는 노드는 Split Out(1 → N), Aggregate(N → 1) 등입니다.
</details>
<details><summary>Q2. 할 일이 4건인데 `Parse actions` 바로 뒤의 항목 수가 1인 이유는?</summary>
4건이 `actions`라는 **목록 하나**에 담겨 상자 1개로 지나가기 때문입니다. Split Out을 지나야 상자 4개가 됩니다.
</details>
<details><summary>Q3. 우선순위를 LLM이 아니라 규칙으로 정하는 이유는?</summary>
"기한에 오늘·내일·긴급이 있으면 높음"은 규칙으로 항상 같게 정할 수 있습니다. LLM에 맡기면 결과가 흔들리고 시간·비용이 듭니다.
</details>
<details><summary>Q4. "요약을 만든 뒤 그 요약으로 제목 짓기"를 병렬로 나눌 수 없는 이유는?</summary>
제목 짓기가 요약 결과를 필요로 하기 때문입니다. 병렬은 서로의 결과가 필요 없을 때만 가능합니다.
</details>
<details><summary>Q5. 할 일 개수(`count`)를 `Normalize action`이 아니라 `Build report`에서 세는 이유는?</summary>
`Normalize action`은 할 일 하나마다 따로 실행되어 전체 개수를 모릅니다. 모두 모은(Aggregate) 뒤에야 셀 수 있습니다.
</details>
## 정리
- 처음 만드는 자동화는 목적 하나로 좁히고, 예시 입력의 기대 결과를 먼저 그립니다.
- LLM은 **찾기**, 규칙은 **채우기·계산·정리문**을 맡습니다. 결과 형식은 노드보다 먼저 약속합니다.
- 노드 사이를 지나가는 상자(항목)의 개수를 셉니다. 오늘 흐름은 **1 → 4 → 1**입니다.
- 결과가 필요 없는 일은 병렬, 항목마다 하는 일은 반복, 입력에 따라 다른 일은 조건 분기입니다.
- 요청이 바뀌면 "하나마다? 모은 뒤? 모든 길?"로 고칠 곳을 찾습니다.
+272
View File
@@ -0,0 +1,272 @@
# 2단계 · 데이터 파싱과 변환 노드
- 시간: 3~4교시 (3교시 끝에 Lab 02 예제 1, 4교시에 예제 2와 과제)
- 학습 목표
- LLM의 답은 JSON처럼 **보이는 글자**라는 것을 OUTPUT에서 구별할 수 있다.
- Code 노드로 글자를 목록으로 파싱하고, 빈 값을 채우고, 실패 이유를 남길 수 있다.
- 지시문과 JSON 스키마로 LLM에게 답의 모양을 알려 줄 수 있다.
- 뽑을 정보를 하나 늘릴 때 고칠 곳(알려 주기 · 받기 · 꺼내기)을 찾을 수 있다.
- Edit Fields와 표현식으로 간단한 변환을 할 수 있다.
- 실습: [Lab 02 · LLM 답을 데이터로 바꾸기 (예제 2개 + 과제)](../../Last3Mon/66/LAB-02-BUILD-PARSE-TRANSFORM.md)
```mermaid
flowchart LR
A["1. LLM 답은 글자<br/>(예제 1)"] --> B["2. 파싱<br/>(예제 1)"] --> C["3. 모양 알려 주기<br/>(예제 2)"] --> D["4. 하나 더 뽑기<br/>(과제)"] --> E["5. Edit Fields"] --> F["6. 만들고 확인하는 순서"]
```
---
## 1. LLM의 답은 "글자"다 · Lab 02 예제 1
LLM에게 "JSON으로 답해"라고 하면 이런 답이 옵니다.
```text
{"actions": [{"task": "신규 고객 문의 응답 템플릿 정리", "owner": "이준호", "due": "금요일"}, {"task": "회의실 예약 규칙 개선", "owner": "", "due": ""}]}
```
사람 눈에는 목록처럼 보이지만, n8n에게는 **따옴표로 둘러싸인 긴 글자 한 줄**입니다. 글자 상태로는 `actions`를 꺼낼 수도, 할 일 개수를 셀 수도 없습니다.
| 상태 | n8n OUTPUT에서 보이는 모습 | 할 수 있는 것 |
| --- | --- | --- |
| **글자** (문자열) | `content` 한 칸에 긴 글자 | 읽기만 |
| **데이터** (객체·목록) | Table 보기에서 `actions`가 줄줄이 펼쳐짐 | `$json.actions`, `count`, Split Out |
글자를 데이터로 바꾸는 일을 **파싱(parsing)** 이라고 합니다. 27일차 Structured Output에서 Pydantic으로 하던 일을 오늘은 Code 노드가 합니다.
예제 1에서는 LLM을 부르지 않고 위 글자를 `Fake LLM answer` 노드에 **직접 적어 넣고** 파싱부터 연습합니다. 돈도 시간도 들지 않고 매번 같은 결과가 나와서, 파싱만 따로 이해하기 좋습니다.
```mermaid
flowchart LR
T(["Start"]) --> F["Fake LLM answer<br/>content = 글자"] --> P["Parse actions<br/>글자 → 목록"]
```
---
## 2. 파싱: 글자를 목록으로 · Lab 02 예제 1
`Parse actions`의 코드는 네 단계입니다. 한 줄씩 외울 필요는 없고 **번호의 뜻**만 알면 됩니다.
```javascript
const raw = String($input.first().json.content ?? '').trim().replace(/^```(json)?\s*|```$/g, ''); // ① 글자 꺼내기
let data;
try {
data = JSON.parse(raw); // ② 글자 → 데이터
} catch (error) {
return [{ json: { actions: [], count: 0, error: 'answer is not valid JSON' } }]; // 실패해도 멈추지 않고 이유를 남김
}
const items = Array.isArray(data) ? data : (data.actions ?? []);
const actions = items
.filter(item => item && String(item.task ?? '').trim()) // ③ 빈 할 일 버리기
.map(item => ({
task: String(item.task).trim(),
owner: String(item.owner || '미정').trim() || '미정', // ④ 빈 값은 '미정'
due: String(item.due || '미정').trim() || '미정',
}));
return [{ json: { actions, count: actions.length } }]; // 항상 같은 모양으로 돌려줌
```
| 번호 | 하는 일 | 예제 1에서 확인 |
| --- | --- | --- |
| ① | 앞 노드에서 글자를 꺼냄 (모델이 ```json 으로 감싸면 벗김) | `content` |
| ② | 글자 → 데이터. 실패하면 빈 목록 + `error` | 끝 `}`를 지우면 `error: answer is not valid JSON` |
| ③ | 할 일이 빈 칸인 것은 버림 | |
| ④ | 담당자·기한이 비면 `미정` | 두 번째 할 일의 `owner`·`due`가 `미정` |
예제 2에서는 ①만 바뀝니다. 가짜 답 대신 진짜 LLM 답이 있는 곳 `choices[0].message.content`를 읽습니다.
### 2.1 어떤 답이 와도 같은 모양으로
| LLM 답 | 그대로 `JSON.parse` | 오늘 코드의 결과 |
| --- | --- | --- |
| `{"actions": [...]}` | 성공 | 그대로 |
| ```` ```json {"actions": [...]} ``` ```` | 실패 | ①에서 감싼 표시를 벗기고 성공 |
| `[{"task": ...}]` (목록만) | 성공 | 목록 그대로 사용 |
| `할 일은 다음과 같습니다 ...` (문장) | 실패 | `{actions: [], count: 0, error: ...}` |
| `{"actions": [{"task": "", ...}]}` | 성공 | 빈 할 일 버림 |
| `{"owner": null}` | 성공 | `owner: "미정"` |
마지막 열이 모두 `{actions, count}` 모양입니다. 다음 노드는 어떤 답이 와도 같은 필드를 받습니다.
### 2.2 실패를 숨기지 않기
| 결과 | `count` | `error` | 뜻 |
| --- | --- | --- | --- |
| 할 일 없는 회의 (`no-action`) | 0 | 없음 | 정상 |
| LLM 답이 깨짐 | 0 | `answer is not valid JSON` | 문제 → 실행 기록 확인 |
`error` 한 칸이 "정말 할 일이 없는 회의"와 "답이 깨진 경우"를 구별합니다. 76일차(예외 대응)에서 이런 경우를 모아 알림을 보내는 Error Workflow를 만듭니다.
---
## 3. LLM에게 답의 모양 알려 주기 · Lab 02 예제 2
예제 2는 예제 1의 앞에 **Webhook과 진짜 LLM**이 붙은 워크플로우를 가져와 실행합니다.
```mermaid
flowchart LR
W(["Meeting webhook"]) --> S["Settings"] --> H{"Has notes?"}
H -->|false| R0["Reject empty<br/>400"]
H -->|true| L{{"LLM extract actions"}} --> P["Parse actions"] --> R["Send result"]
```
`LLM extract actions` 노드의 JSON 칸은 세 부분입니다. (실습 파일은 읽기 쉽게 여러 줄로 적혀 있습니다.)
```text
{{ JSON.stringify({
model: $('Settings').first().json.model,
temperature: 0,
response_format: { type: 'json_schema', json_schema: { name: 'reply', schema: { ← ① 스키마: 답의 모양
type: 'object',
properties: {
actions: { type: 'array', items: { type: 'object',
properties: { task: { type: 'string' }, owner: { type: 'string' }, due: { type: 'string' } },
required: ['task', 'owner', 'due'] } },
},
required: ['actions'],
} } },
messages: [
{ role: 'system', content: "Extract action items ... Use \"미정\" when ..." }, ← ② 지시문: 무엇을 찾을지
{ role: 'user', content: $('Settings').first().json.notes }, ← ③ 회의록 원문
],
}) }}
```
| 부분 | 하는 일 | 비유 |
| --- | --- | --- |
| ① 스키마 (`response_format`) | "이 칸들로만 답해라" — 답의 **모양**을 정함 | 빈칸이 정해진 답안지 |
| ② 지시문 (`system`) | 무엇을 찾고, 없으면 무엇을 쓸지 | 문제 설명 |
| ③ 회의록 (`user`) | 읽을 글 | 지문 |
### 3.1 스키마는 모양만 지킨다
| | 지시문만 | 지시문 + 스키마 |
| --- | --- | --- |
| 모양 | 가끔 어긋남 (설명 문장, ```json) | 정한 모양으로만 |
| 필드 이름 | 가끔 `assignee`, `deadline` | 스키마 이름 그대로 |
| **내용이 맞나** | 보장 안 함 | **보장 안 함** |
스키마를 써도 담당자를 잘못 찾거나 할 일을 빠뜨리는 것은 막지 못합니다. 4번 할 일의 기한 `10월 23일`(회의록에 직접 적히지 않은 추론)이 그 예입니다. 그래서 스키마를 써도 2절의 파싱·검증은 그대로 둡니다. 그리고 답은 여전히 **글자**(`choices[0].message.content`)로 옵니다.
Anthropic·Gemini에도 비슷한 구조화 출력 기능이 있고, 대체용 Ollama도 같은 `response_format`을 받습니다.
> **표현식 주의**: `{{ }}` 안에서 `}`와 `}`가 **붙으면** 표현식이 거기서 끝나 `invalid syntax`가 납니다. 실습 파일에서 `} }`처럼 띄어 쓴 곳은 일부러 띄운 것입니다.
---
## 4. 뽑을 것을 하나 늘리려면 · Lab 02 과제
새 정보(예: 참석자)가 결과까지 오려면 데이터가 지나가는 길을 따라 **세 곳**이 모두 알아야 합니다.
```mermaid
flowchart LR
A["① 알려 주기<br/>스키마에 칸 추가"] --> B["② 부탁하기<br/>지시문에 한 문장"] --> L{{"LLM 답<br/>(글자)"}} --> C["③ 꺼내기<br/>파싱 코드의 return"] --> R["결과"]
```
| 곳 | 빠뜨리면 |
| --- | --- |
| ① 스키마 | LLM이 그 칸을 만들지 않을 수 있음 |
| ② 지시문 | 무엇을 넣을지 몰라 비우거나 엉뚱한 값 |
| ③ 파싱 코드 | LLM 답에는 있어도 **새 상자를 만들며 버려짐** → 결과에 없음 |
③이 가장 잘 빠집니다. 파싱 코드는 `return [{ json: { actions, count } }]`처럼 **필요한 필드만 골라 새 상자를 만들기** 때문입니다. 어디서 사라졌는지 모를 때는 노드를 하나씩 열어 OUTPUT에 그 필드가 있는지 봅니다.
뽑을 것을 늘린 뒤에는 **원래 기대 결과(할 일 4건)도 다시** 확인합니다. 요청이 바뀌면 원래 뽑던 결과도 흔들릴 수 있습니다.
---
## 5. 변환: Edit Fields와 표현식
코드 없이 필드를 만들거나 값을 계산할 때는 Edit Fields(Set)를 씁니다. 예제 2의 `Settings`가 그 예입니다.
| 하고 싶은 일 | 설정 예 |
| --- | --- |
| 입력값 꺼내기 | `notes` = `{{ $json.body.notes }}` (웹훅 입력은 `body` 안에) |
| 비었으면 기본값 | `title` = `{{ $json.body.title \|\| '제목 없음' }}` |
| 공백 다듬기 | `summary` = `{{ $json.choices[0].message.content.trim() }}` |
| 조건으로 값 정하기 | `priority` = `{{ ['오늘', '내일', '긴급'].some(w => $json.due.includes(w)) ? '높음' : '보통' }}` |
| 빈 목록 | `actions` (Array) = `{{ [] }}` |
- `{{ }}`가 들어간 값은 입력칸이 **Expression** 모드(초록색)여야 합니다. Fixed 모드면 글자 그대로 들어갑니다.
- 필드 형식(String·Number·Array·Boolean)을 맞춥니다. 예를 들어 `count`가 글자 `"0"`이면 숫자 비교가 원하는 대로 되지 않을 수 있습니다.
| Edit Fields | Code |
| --- | --- |
| 필드 몇 개를 만들거나 바꿀 때 | 파싱, 여러 줄 검증, 실패 처리 (`try / catch`) |
| 누구나 읽기 쉬움 | 자유롭지만 실수하기 쉬움 |
---
## 6. 만들고 확인하는 순서
```mermaid
flowchart LR
A["노드 하나 추가"] --> B["이름 짓기<br/>(표현식이 이름을 씀)"] --> C["설정"] --> D["실행"] --> E["OUTPUT 확인"]
E -->|"다음 노드"| A
```
- **한 번에 다 만들지 않습니다.** 노드 하나를 붙일 때마다 실행해 OUTPUT을 봅니다.
- 노드 이름을 안내와 똑같이 짓습니다. `$('Settings')`처럼 표현식이 이름을 씁니다.
- 같은 요청 파일(`weekly`·`no-action`·`empty`)로 매번 확인해야 고친 것의 효과를 비교할 수 있습니다.
- Test URL은 **한 번에 한 요청**만 받습니다. 요청마다 `Listen for test event`를 다시 누릅니다.
---
## 7. 막혔을 때
| 증상 | 원인 | 해결 |
| --- | --- | --- |
| `invalid syntax` | 표현식 안에서 `}}`가 붙음 | `} }`로 띄어 쓰기 |
| `401` | Credential이 `OpenAI (.env)`가 아님 | Authentication 확인 |
| 값에 `{{ $json.body.notes }}` 글자가 그대로 | Fixed 모드 | Expression 모드 |
| `actions`가 비고 `error`가 있음 | LLM 답이 JSON이 아님 | 앞 노드 OUTPUT의 `content` 확인 |
| 새 필드가 결과에 없음 | 파싱 코드의 `return`에 안 넣음 | 4절 ③ |
| 고치다 망가짐 | | 예제 2 파일을 다시 가져오기 |
---
## 8. Lab 02 안내
- 실습 안내: [Last3Mon/66/LAB-02-BUILD-PARSE-TRANSFORM.md](../../Last3Mon/66/LAB-02-BUILD-PARSE-TRANSFORM.md)
| 순서 | 시간 | 할 일 | 교안 |
| --- | --- | --- | --- |
| 예제 1 | 3교시 끝 15분 (함께) | `Fake LLM answer`(글자) → `Parse actions`(목록), 끝 `}`를 지워 `error` 보기 | 1~2절 |
| 예제 2 | 4교시 15분 | `lab02-example2-meeting-part1.json` 가져오기 → `weekly`·`empty`·`no-action`, JSON 칸의 ①②③ 찾기 | 3절 |
| 과제 | 4교시 25분 | 결과에 **참석자(`attendees`)** 추가 | 4절 |
---
## 9. 스스로 확인하기
<details><summary>Q1. `Fake LLM answer`의 `content`와 `Parse actions`의 `actions`는 무엇이 다른가요?</summary>
`content`는 글자 한 줄이고, `actions`는 파싱해서 만든 목록입니다. 목록이어야 개수를 세고 Split Out으로 펼칠 수 있습니다.
</details>
<details><summary>Q2. 스키마(`response_format`)를 지정했는데도 파싱·검증이 필요한 이유는?</summary>
스키마는 모양만 지켜 줍니다. 답은 여전히 글자로 오므로 파싱이 필요하고, 빈 값이나 잘못 찾은 내용은 검증으로 걸러야 합니다.
</details>
<details><summary>Q3. 파싱에 실패하면 빈 목록을 돌려주면서도 `error`를 남기는 이유는?</summary>
흐름이 멈추지 않게 하면서도, "정말 할 일이 없는 회의"와 "LLM 답이 깨진 경우"를 구별하기 위해서입니다.
</details>
<details><summary>Q4. 스키마와 지시문에 `attendees`를 넣었는데 결과에 없습니다. 어디를 보나요?</summary>
`LLM extract actions`의 OUTPUT에는 있는데 `Parse actions`의 OUTPUT에 없다면, 파싱 코드의 `return`이 `attendees`를 넣지 않은 것입니다. 노드를 앞에서부터 하나씩 열어 어디서 사라졌는지 찾습니다.
</details>
<details><summary>Q5. 기한을 스키마에서 날짜 형식이 아니라 글자(`string`)로 둔 이유는?</summary>
회의록에는 "금요일", "다음 주 월요일"처럼 날짜가 아닌 표현이 많습니다. 날짜로 바꾸려면 회의 날짜를 아는 계산이 필요하므로, 필요하면 규칙으로 따로 합니다.
</details>
## 정리
- LLM의 JSON 답은 **글자**입니다. Code 노드로 파싱해야 목록이 됩니다.
- 파싱은 꺼내기 → 바꾸기(실패하면 `error`) → 빈 할 일 버리기 → `미정` 채우기, 그리고 항상 같은 모양으로 돌려줍니다.
- 스키마는 답의 **모양**, 지시문은 **무엇을 찾을지**를 알려 줍니다. 스키마도 내용은 보장하지 않습니다.
- 뽑을 것을 늘리면 **알려 주기(스키마) · 부탁하기(지시문) · 꺼내기(파싱)** 세 곳을 고칩니다.
- 간단한 변환은 Edit Fields, 파싱·검증은 Code. 노드 하나마다 실행해 OUTPUT을 확인합니다.
@@ -0,0 +1,244 @@
# 3단계 · 반복·병렬 제어 구조와 완성
- 시간: 5~6교시 (5교시 후반에 Lab 03 예제 1·2, 6교시에 과제 40분), 4절 밖에서 부르기는 8교시
- 학습 목표
- 펼치기(Split Out) → 항목마다 처리 → 모으기(Aggregate)로 반복을 만들 수 있다.
- 두 길로 나눈 일을 Merge로 합치는 Fork-Join을 만들 수 있다.
- 연습용 가짜 데이터 자리에 진짜 LLM 결과를 넣어 워크플로우를 완성할 수 있다.
- 기대 결과 표와 실행 기록의 항목 수로 결과를 검증할 수 있다.
- 실습: [Lab 03 · 반복·병렬로 회의록 워크플로우 완성하기 (예제 2개 + 40분 과제)](../../Last3Mon/66/LAB-03-BUILD-LOOP-PARALLEL.md)
```mermaid
flowchart LR
A["1. 반복<br/>(예제 1)"] --> B["2. 병렬<br/>(예제 2)"] --> C["3. 가짜를 진짜로<br/>(과제)"] --> D["4. 기대 결과로 검증<br/>(과제)"] --> E["5. 밖에서 부르기<br/>(8교시)"]
```
---
## 1. 반복: 펼치고 · 항목마다 · 모으기 · Lab 03 예제 1
n8n의 노드는 **들어온 상자(항목)마다 한 번씩** 실행됩니다. 그래서 목록을 상자 여러 개로 펼치기만 하면 반복이 됩니다. 별도의 "for 문" 노드가 필요 없습니다.
예제 1은 할 일 3건이 든 가짜 목록으로 연습합니다.
```mermaid
flowchart LR
S["Sample actions<br/>상자 1개<br/>actions [3]"] -->|"1"| SO["One item per action<br/>Split Out"]
SO -->|"3"| N["Normalize action<br/>Edit Fields<br/>(3번 실행)"]
N -->|"3"| AG["Collect actions<br/>Aggregate"]
AG -->|"1"| OUT["상자 1개<br/>actions [3] + priority"]
```
| 노드 | 설정 | 하는 일 |
| --- | --- | --- |
| Split Out | Fields To Split Out: `actions` | 목록 → 할 일 하나씩 상자로 (**1 → 3**) |
| Edit Fields | `task`, `owner`, `due`, `priority` | 상자마다 실행 (3번) |
| Aggregate | All Item Data, 결과 필드 `actions` | 상자들 → 다시 목록 하나 (**3 → 1**) |
**펼쳤으면 반드시 모은다.** 이것이 반복의 짝입니다. 오후 과제에서는 진짜 회의록이라 같은 모양이 **1 → 4 → 1**이 됩니다.
### 1.1 모으지 않으면? (5교시 시연, 수업 전 실측)
| 틀린 곳 | `weekly` 결과 |
| --- | --- |
| `Normalize action` → 바로 응답 | HTTP 200인데 **첫 할 일 하나만** — 응답 노드는 첫 상자로 한 번만 답함 |
| Aggregate 없이 `Build report` | HTTP 500 `Error in workflow` — 목록을 기대했는데 할 일 하나짜리 상자가 옴 |
첫 번째가 더 위험합니다. 오류 없이 **조용히 틀린 답**이 나갑니다. 그래서 실행 기록에서 노드마다 **항목 수**를 읽는 습관을 들입니다.
### 1.2 상자마다 실행될 때 `$json`이 가리키는 것
| 위치 | `$json` | 다른 값이 필요하면 |
| --- | --- | --- |
| Split Out 뒤 (상자마다) | 할 일 **하나** (`task`, `owner`, `due`) | 회의 제목: `$('Settings').first().json.title` |
| Aggregate 뒤 (한 번) | 모은 결과 `{actions: [...]}` | 요약: `$('Join results').first().json.summary` |
`$('노드 이름')`은 "그 노드의 OUTPUT을 가져와"라는 뜻입니다. 노드 이름을 안내와 똑같이 지어야 하는 이유입니다.
### 1.3 Loop Over Items는 언제?
| 상황 | 선택 |
| --- | --- |
| 상자마다 같은 변환만 (오늘) | Split Out + 일반 노드 |
| 상자마다 외부 API·LLM을 부르는데 한꺼번에 보내면 호출 제한에 걸림 | **Loop Over Items**로 몇 개씩 나눠 차례로 |
반복 안에 LLM을 두면 **시간과 비용이 항목 수에 비례**합니다 (75일차 비용 관리).
---
## 2. 병렬: 나누고 · 합치기 · Lab 03 예제 2
할 일 찾기와 요약은 서로의 결과가 필요 없습니다. 한 노드에서 **두 길로 나누고(Fork)**, 둘 다 끝나면 **합칩니다(Join)**. 합치는 노드가 Merge입니다.
예제 2는 예제 1에 가짜 요약 길을 하나 더 붙입니다.
```mermaid
flowchart LR
T(["Start"]) --> S["Sample actions → … → Collect actions<br/>{actions}"] -->|"Input 1"| J["Join results<br/>Merge · Combine · Position"]
T --> SS["Sample summary<br/>{summary}"] -->|"Input 2"| J
J --> O["상자 1개<br/>{actions, summary}"]
```
| Merge 설정 | 뜻 |
| --- | --- |
| Mode `Combine` | 두 입력의 상자를 하나로 합침 |
| Combine By `Position` | 1번 입력의 첫 상자 + 2번 입력의 첫 상자 → 상자 하나 |
- Merge는 **두 입력이 모두 올 때까지** 기다립니다.
- n8n은 두 길을 동시에가 아니라 **차례로** 실행합니다. 그래도 그림에서 두 일이 서로 독립이라는 것이 드러나고, 한쪽을 고쳐도 다른 쪽에 영향이 없습니다.
### 2.1 Merge의 다른 모드
| 모드 | 하는 일 | 쓰는 때 |
| --- | --- | --- |
| Append | 두 입력을 이어 붙임 (2 + 3 = 5개) | 같은 모양 목록 두 개 |
| **Combine · By Position** | 같은 순서끼리 합침 (1 + 1 = 1개) | **오늘** |
| Combine · By Matching Fields | 같은 값(`customer_id` 등)끼리 짝 | 주문 목록 + 고객 정보 |
| Choose Branch | 한쪽만 내보냄 | 한쪽 결과만 필요할 때 |
### 2.2 한 길이 실패하면
요약 길의 LLM 호출이 실패하면, 할 일 찾기가 성공해도 Merge가 기다리다 **결과가 나오지 않습니다.** 길이 늘수록 실패할 곳도 늘어납니다. 대처는 72일차(재시도)와 76일차(Error Workflow)에서 배웁니다.
---
## 3. 가짜를 진짜로 바꾸기 · Lab 03 과제
예제 1·2는 가짜 데이터였습니다. 과제는 그 자리에 Lab 02의 **진짜 LLM 결과**를 넣어 `66 Meeting notes (my)`를 완성합니다.
| 예제의 가짜 | 과제의 진짜 | 하는 일 |
| --- | --- | --- |
| `Sample actions` | `LLM extract actions` → `Parse actions` (Lab 02) | 할 일 목록 |
| `Sample summary` | `LLM summarize` → `Keep summary` (새로) | 요약 |
| `Join results` | 그대로 복사 | 두 결과 합치기 |
| Split Out → Normalize → Aggregate | 그대로 복사 | 할 일마다 우선순위, 다시 모으기 |
| (없음) | `Any actions?`, `Build report`, `No actions` (새로) | 할 일이 있을 때 / 없을 때 |
```mermaid
flowchart LR
H{"Has notes?"} -->|true| L1{{"LLM extract actions"}} --> P["Parse actions"] --> J["Join results"]
H -->|true| L2{{"LLM summarize"}} --> K["Keep summary"] --> J
J --> A{"Any actions?"}
A -->|true| SO["Split Out → Normalize → Aggregate"] --> B["Build report"] --> R["Send result"]
A -->|false| NA["No actions"] --> R
classDef new stroke-width:3px
class L2,K,J,A,SO,B,NA new
```
### 3.1 순서에 담긴 이유
| 질문 | 답 |
| --- | --- |
| 왜 Merge를 `Any actions?` **앞**에 두나? | 할 일이 없는 회의에도 **요약**은 있어야 하니까. 합친 뒤에 나누면 두 길 모두 `summary`를 가짐 |
| 왜 `Any actions?`가 Split Out **앞**에 있나? | 할 일이 0건이면 펼칠 것도, 정리문에 쓸 것도 없음 → 펼치기 전에 길을 나눠 `할 일 없음` 결과를 따로 만듦 |
| 정리문(`report_text`)은 왜 코드로? | 매번 같은 모양이어야 메신저·메일에 붙이기 좋고, 시간·비용이 들지 않음 |
### 3.2 두 길을 같은 필드로 끝내기
| 필드 | 할 일 정리 (`Build report`) | 할 일 없음 (`No actions`) |
| --- | --- | --- |
| `title` | 회의 제목 | 회의 제목 |
| `route` | `할 일 정리` | `할 일 없음` |
| `count` | 4 | 0 |
| `summary` | 요약 | 요약 |
| `actions` | 할 일 4개 (+ `priority`) | `[]` |
| (과제) `need_check` | 확인 필요 건수 | 0 |
과제의 마지막 단계 **확인 필요**는 Lab 01 과제에서 설계한 것입니다. 1단계 6절의 질문 "하나마다? 모은 뒤? 모든 길?"을 다시 꺼내 봅니다.
---
## 4. 기대 결과로 검증하기 · Lab 03 과제
"오류가 안 났다"는 "맞았다"가 아닙니다. 두 가지로 확인합니다.
**① 기대 결과 표와 비교** (수업 전 측정)
| 요청 | 기대 결과 | 처리 시간 |
| --- | --- | --- |
| `weekly` (할 일 4건) | `할 일 정리`, `count: 4`, 오늘 마감 1건 `높음`, `need_check: 1` | 약 3~4초 |
| `no-action` (결정 사항 없음) | `할 일 없음`, `count: 0`, `need_check: 0` | 약 2초 |
| `empty` (빈 회의록) | HTTP 400 | 1초 미만 |
**② 실행 기록의 항목 수 읽기**: `Join results` 1 → `One item per action` 4 → `Normalize action` 4 → `Collect actions` 1 → `Build report` 1
수업 준비 때 실제로 본 **오류 없이 틀리는** 모양입니다.
| 증상 | 원인 |
| --- | --- |
| 할 일 하나만 옴 | Aggregate를 거치지 않음 (1.1절) |
| `no-action`에서 HTTP 200인데 빈 응답 | `Any actions?`의 false 길이 끊김 |
| `need_check: 4` | `check`의 형식이 Boolean이 아니라 String → 글자 `"false"`도 참으로 셈 |
---
## 5. 밖에서 부르기: 설정 한 줄로 대상 바꾸기 · 8교시
65일차의 플랫폼 전환 실행기로 오늘의 업무 `meeting`도 부를 수 있습니다.
```mermaid
flowchart LR
APP["call('meeting', {title, notes})"] --> CFG["platforms.json<br/>meeting → n8n webhook · miniflow flow"]
CFG --> N8["n8n<br/>/webhook/my-meeting-actions"]
CFG --> MF["miniflow<br/>flows/08-meeting-actions.json"]
```
- 내가 만든 워크플로우를 부르려면 `platforms.json`의 웹훅 경로 **한 줄만** `my-meeting-actions`로 바꿉니다. 호출 코드는 그대로입니다.
- n8n이 멈추면 같은 업무를 `miniflow`의 `08` 워크플로우가 이어받습니다. 이것이 되려면 두 플랫폼이 **같은 입력(`title`, `notes`)과 같은 결과 필드**를 지켜야 합니다.
| 하는 일 | n8n (오늘 만든 것) | miniflow `08-meeting-actions.json` |
| --- | --- | --- |
| 입력 | Webhook `body.title`, `body.notes` | `-i title=... -i notes=...` |
| 할 일 찾기 | HTTP Request → OpenAI (스키마) | LLM 노드 |
| 파싱·검증 | Code `Parse actions` | `parse_actions` Tool |
| 결과 | `{title, route, count, summary, actions, ...}` | 같은 필드 |
---
## 6. Lab 03 안내
- 실습 안내: [Last3Mon/66/LAB-03-BUILD-LOOP-PARALLEL.md](../../Last3Mon/66/LAB-03-BUILD-LOOP-PARALLEL.md)
| 순서 | 시간 | 할 일 | 교안 |
| --- | --- | --- | --- |
| 예제 1 | 5교시 끝 12분 | 가짜 할 일 3건 펼치고 모으기, 항목 수 1 → 3 → 3 → 1 | 1절 |
| 예제 2 | 5교시 끝 10분 | `Sample summary`를 Merge(Position)로 합치기 | 2절 |
| **과제** | 6교시 40분 | 예제 노드를 복사해 내 워크플로우 완성 + **확인 필요** | 3~4절 |
| 마무리 | 8교시 함께 | `platforms.json` 한 줄 바꾸고 `miniflow call meeting` | 5절 |
---
## 7. 스스로 확인하기
<details><summary>Q1. `One item per action`이 4개, `Collect actions`가 1개입니다. 그 사이의 `Normalize action`은 몇 번 실행되었나요?</summary>
4번입니다. 대부분의 n8n 노드는 들어온 항목마다 한 번씩 실행됩니다.
</details>
<details><summary>Q2. `Build report`에서 회의 제목을 `$json.title`이 아니라 `$('Settings').first().json.title`로 꺼내는 이유는?</summary>
`Build report` 바로 앞은 Aggregate이고, 그 결과에는 `actions`만 있습니다. 제목은 앞쪽 `Settings` 노드에 있으므로 이름으로 꺼냅니다.
</details>
<details><summary>Q3. Merge를 `Any actions?` 앞에 두는 이유는?</summary>
할 일이 없는 회의에도 요약이 필요하기 때문입니다. 먼저 합치면 할 일 정리 길과 할 일 없음 길 모두 `summary`를 가집니다.
</details>
<details><summary>Q4. 주문 목록과 고객 정보를 `customer_id`로 합치려면 Merge를 어떻게 설정하나요?</summary>
Combine · By Matching Fields로 `customer_id`를 기준으로 합칩니다. By Position은 순서가 같을 때만 맞습니다.
</details>
<details><summary>Q5. 요약 길의 LLM 호출이 실패하면 할 일 찾기가 성공해도 응답이 나오지 않는 이유는?</summary>
Merge는 두 입력이 모두 와야 합치기 때문입니다. 병렬 길이 늘면 실패할 곳도 늘어납니다.
</details>
## 정리
- 반복은 **펼치고(Split Out) · 항목마다 · 모으기(Aggregate)**. 펼쳤으면 반드시 모읍니다.
- 병렬은 **나누고 · Merge로 합치기**. Merge는 두 입력을 모두 기다립니다.
- 예제의 가짜 데이터 자리에 진짜 LLM 결과를 넣으면 워크플로우가 완성됩니다.
- 모든 길이 같은 필드로 끝나게 하고, **기대 결과 표와 항목 수**로 검증합니다. 오류가 없어도 틀릴 수 있습니다.
- 같은 입력·같은 결과 형식을 지키면 설정 한 줄로 호출 대상을 바꾸고, 플랫폼 전환도 그대로 됩니다.
+40
View File
@@ -0,0 +1,40 @@
# 66일차 · 단일 업무 자동화 워크플로우 구현
- 일자: 2026-10-16 (66일차)
- 교과목: 노코드 멀티 에이전트 워크플로우 설계 및 운영
- 단원: AI 워크플로우 설계 및 구현
- 학습 내용: 노코드 Workflow 도구 활용, 단일 업무 자동화 Workflow 구현
- 실습 폴더: [Last3Mon/66](../../Last3Mon/66/README.md)
- 실습 환경: 65일차 n8n 2.42.6 + OpenAI API(`gpt-4.1-mini`, Credential `OpenAI (.env)`) 그대로 사용, Ollama는 대체용
- **실습 준비: [00-DAY66-PREP.md](00-DAY66-PREP.md) (수업 전날까지 1~2절 완료)**
## 오늘의 시간표
| 교시 | 시간 | 내용 | 자료 |
| --- | --- | --- | --- |
| 1 | 09:00~09:50 | 오늘의 개요 · 1단계 시작 | [00-DAY66-OVERVIEW.md](00-DAY66-OVERVIEW.md), [1단계](01-STAGE1-DESIGN-AND-DATA-FLOW.md) |
| 2 | 10:00~10:50 | 1단계 · 단일 업무 설계와 데이터 흐름, Lab 01 | [1단계](01-STAGE1-DESIGN-AND-DATA-FLOW.md), [Lab 01](../../Last3Mon/66/LAB-01-DESIGN-DATA-FLOW.md) |
| 3 | 11:00~11:50 | 2단계 · LLM JSON 출력, 파싱과 변환, Lab 02 예제 1 | [2단계](02-STAGE2-PARSE-AND-TRANSFORM.md), [Lab 02](../../Last3Mon/66/LAB-02-BUILD-PARSE-TRANSFORM.md) |
| - | 12:00~13:00 | 점심 | |
| 4 | 13:00~13:50 | 2단계 · Lab 02 예제 2와 과제 (참석자 뽑기) | [Lab 02](../../Last3Mon/66/LAB-02-BUILD-PARSE-TRANSFORM.md) |
| 5 | 14:00~14:50 | 3단계 · 반복·병렬 제어 구조와 완성, Lab 03 예제 1·2 | [3단계](03-STAGE3-LOOP-PARALLEL-AND-FINISH.md), [Lab 03](../../Last3Mon/66/LAB-03-BUILD-LOOP-PARALLEL.md) |
| 6 | 15:00~15:50 | 3단계 · Lab 03 과제 40분 (내 워크플로우 완성 + 확인 필요) · 풀이 | [Lab 03](../../Last3Mon/66/LAB-03-BUILD-LOOP-PARALLEL.md) |
| 7 | 16:00~16:50 | 퀴즈 (A·B·C 중 선택, 20분) · 풀이 | [퀴즈와 정리](04-QUIZ-AND-WRAPUP.md) |
| 8 | 17:00~17:50 | 오늘의 정리 · Python에서 부르기(함께) · 다음 시간 예고 · 복습 과제 | [퀴즈와 정리](04-QUIZ-AND-WRAPUP.md) |
```mermaid
flowchart LR
P["개요"] --> S1["1단계<br/>설계·데이터 흐름"] --> S2["2단계<br/>파싱·변환"] --> S3["3단계<br/>반복·병렬·완성"] --> Q["퀴즈·정리"]
```
## 자료 목록
| 파일 | 내용 |
| --- | --- |
| `00-DAY66-PREP.md` | 실습 준비: 어제 환경 켜기, JSON 객체·목록, 새 노드 미리 보기 (수업 전) |
| `00-DAY66-OVERVIEW.md` | 개요 15장 (PPT 대신 Markdown, 가로줄로 장 구분). 1교시에 앞부분, 각 단계 설명 때 해당 장을 함께 띄움 |
| `01-STAGE1-DESIGN-AND-DATA-FLOW.md` | 목적 하나로 좁히기, 결과 먼저, 항목(상자) 읽기 1 → 4 → 1, 제어 구조, 요청이 바뀌면 (Lab 01 예제·과제 순서) |
| `02-STAGE2-PARSE-AND-TRANSFORM.md` | LLM 답은 글자, 파싱, 답의 모양 알려 주기(스키마), 하나 더 뽑기, Edit Fields (Lab 02 예제·과제 순서) |
| `03-STAGE3-LOOP-PARALLEL-AND-FINISH.md` | 펼치고·모으기, Merge로 합치기, 가짜를 진짜로, 기대 결과로 검증, 밖에서 부르기 (Lab 03 예제·과제 순서) |
| `04-QUIZ-AND-WRAPUP.md` | 퀴즈 안내, 오늘의 정리, 자주 막히는 부분, 67일차 예고 |
| `quiz/` | 퀴즈 문항(A·B·C), Google Form 생성 스크립트, 응답 분석 엑셀 |