78일차 정답: LAB-A · VIBE-A-TCHECK · answers(정답 워크플로우 · 실행 기록) · 강사가 만든 tcheck
This commit is contained in:
1 parent
acf3a93041
commit
c7df2e5344
85 files changed
+52637
No files matched your search
@@ -0,0 +1,113 @@
|
|||||||
|
# Lab 01 · 품질 규칙 · 눈으로 검사하고, 규칙으로 바꾸기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-01-Q-QUALITY-RULES.md](LAB-01-Q-QUALITY-RULES.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 실습을 5개로 늘린 뒤 정답 워크플로우를 학생 순서대로 2026-11-01에 1회씩 실행한 값입니다 (강사 Mac). 판정은 규칙대로라 누가 돌려도 같습니다. 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 결과 (에이전트 없이, `answers/01`)
|
||||||
|
|
||||||
|
| 줄 | id | 내 눈 | 기준 검사 | 규칙 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 3 · 5 · 8 | FAQ-103 · 105 · 108 | 나쁨 | ✔ 잡음 | `title_missing` · `answer_missing` · `keywords_missing` |
|
||||||
|
| 4 | FAQ-10 | 나쁨 | **놓침** | `id_format` |
|
||||||
|
| 6 | FAQ-106 | 나쁨 | **놓침** | `answer_too_long` |
|
||||||
|
| 7 | FAQ-102 (둘째) | 나쁨 | **놓침** | `duplicate_in_batch` |
|
||||||
|
| 9 | FAQ-109 | 나쁨 | **놓침** | `pii` (`010-1234-5678`) |
|
||||||
|
| 10 | FAQ-110 | 나쁨 | **놓침** | `external_link` (`bit.ly`) |
|
||||||
|
| 11 · 12 | FAQ-003 · 112 | 이미 있는 FAQ를 봐야 | 통과 | 과제 1 |
|
||||||
|
| 13 | FAQ-113 | 뜻이 다름 | 통과 | Lab 02 예제 2 (LLM) |
|
||||||
|
|
||||||
|
- 기준: 13줄 중 **10줄 통과**(빈 값 셋만). 눈: 분명히 나쁜 8줄 + 판단 보류 3줄 → 좋은 줄 2줄.
|
||||||
|
- 풀이 때: "기준이 틀렸나?" → 아니오, **규칙이 셋뿐**. 가장 위험한 놓침은 FAQ-109(개인 전화번호)와 FAQ-110(단축 링크) — 73일차 보안과 이어서.
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-check.json 을 새로 만들어 줘. 웹훅 POST my-faq-check, 이름 "78 FAQ check (my)".
|
||||||
|
Is a list?(IF): 본문 items 가 목록이 아니면 Reject body 로 HTTP 400 {"error": "items must be a list"}.
|
||||||
|
Check rows(Code) 하나에서 줄마다 AGENTS.md 3절 FAQ 품질 규칙대로 errors 를 붙인다. 공백만 있는 값은 빈 값, 글자 수는 앞뒤 공백을 뺀 뒤 센다.
|
||||||
|
pii 는 하이픈 없는 휴대전화도, external_link 는 호스트가 정확히 intra.example.com 이 아니면.
|
||||||
|
결과 {batch, total, valid, invalid, rows: [{id, ok, errors, warnings}], codes}, rows 는 들어온 순서 그대로. LLM 은 쓰지 않는다. 기대 결과 cases/lab01.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답** (Q 문서 ▶ 빈칸 채우기 `____①`~`____④`)
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `my-faq-check` | AGENTS.md 3절 계약 · 테스트 파일의 `webhook` |
|
||||||
|
| ② | `400` | 목록이 아닌 본문은 줄을 셀 수 없음 — 기준 검사는 여기서 500으로 멈춤 |
|
||||||
|
| ③ | 앞뒤 공백을 뺀 뒤 | 공백 세 칸짜리 제목이 '있는 제목'으로 통과하지 않게 (예제 3의 경계) |
|
||||||
|
| ④ | 정확히 intra.example.com | `intra.example.com.evil.io`처럼 앞만 같은 바깥 주소를 막음 |
|
||||||
|
|
||||||
|
정답: [reference/lab01-example2-check.json](reference/lab01-example2-check.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/02`, 기준에 같은 테스트) | **1/5** · 빈 값만 ✔, 형식 · 중복 · 개인정보 · 링크 없음, B-2 `valid` 10, 목록이 아닌 본문 **500** |
|
||||||
|
| 고친 뒤 (`answers/03`) | **5/5** · B-1 13줄 중 5줄 통과 · 8줄 나쁨 (눈과 같은 8줄), 목록이 아닌 본문 400, 0.1초 |
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/04`)
|
||||||
|
|
||||||
|
| 경계 | 내 검사 | 기준 | 기준이 틀린 까닭 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 40자 · 41자 | ✔ 통과 · `title_too_long` | ✘ 둘 다 통과 | 길이 규칙 없음 |
|
||||||
|
| 공백만 | ✔ `title_missing` | ✘ 통과 | `!r.title` — 공백은 "있음" |
|
||||||
|
| 10자 · 9자 | ✔ 통과 · `answer_too_short` | ✘ 둘 다 통과 | 길이 규칙 없음 |
|
||||||
|
| `01012345678` · `02-123-4567` | ✔ `pii` · 통과 | ✘ 둘 다 통과 | 전화 규칙 없음 |
|
||||||
|
| `intra.example.com/help` · `intra.example.com.evil.io` | ✔ 통과 · `external_link` | ✘ 둘 다 통과 | 링크 규칙 없음 |
|
||||||
|
| `faq-410` | ✔ `id_format` | ✘ 통과 | 형식 규칙 없음 |
|
||||||
|
|
||||||
|
- 내 검사 **6/6**, 기준 **0/6**. 강사의 첫 판(10/18 판 reference)은 `evil.io`를 통과시켰음 — `(?!intra\.example\.com)`만 보고 호스트가 끝나는지는 안 봄. 지금 판은 `(?!intra\.example\.com(?![\w.-]))`. 학생 판이 FAQ-409에서 ✘라면 좋은 토론거리.
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-check.json 에 이미 있는 데이터와 맞춰 보기를 더해 줘.
|
||||||
|
Is a list? 와 Check rows 사이에 Get FAQ(HTTP GET http://data-api:8800/faq-all) 를 둔다.
|
||||||
|
이미 있는 id 면 errors 에 exists_id, 이미 있는 제목이면 same_title.
|
||||||
|
이미 있는 FAQ 가 쓰는 키워드는 errors 가 아니라 warnings 에 keyword_used:<낱말> (대소문자 무시) — 통과는 시킨다.
|
||||||
|
앞의 규칙은 그대로. 기대 결과 cases/lab01-challenge.json, cases/lab01.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `faq-all` | 처음 6개 + 오늘 더한 FAQ 전체 |
|
||||||
|
| ② | `same_title` | 제목이 같으면 검색에서 두 답이 겹침 |
|
||||||
|
| ③ | `warnings` | 키워드가 겹치는 것은 흔하고 나쁘지 않음 — 알려만 줌 |
|
||||||
|
| ④ | 그대로 | 같은 Code에 더함. `lab01` 5/5가 그대로여야 함 |
|
||||||
|
|
||||||
|
정답: [answers/78-lab01-challenge-existing.json](answers/78-lab01-challenge-existing.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/05`) | **0/4** · `exists_id` · `same_title` 없음, 통과 5 |
|
||||||
|
| 고친 뒤 (`answers/06`) | **4/4** · FAQ-003 `exists_id`, FAQ-112 `same_title` + 경고 `keyword_used:vpn`, 통과 3, 막 더한 FAQ-201도 `exists_id`. `lab01` 5/5 |
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-check.json 의 결과에 보낸 사람에게 돌려줄 고침 목록을 더해 줘.
|
||||||
|
fix_list: 통과하지 못한 줄만 줄 순서대로 {row (1부터), id, codes (그 줄의 errors), fix}.
|
||||||
|
fix 는 코드마다 AGENTS.md 3절 "고침 목록" 표의 말을 그대로 쓴다 (LLM 으로 만들지 않는다).
|
||||||
|
message: "<batch>: <total>줄 중 <valid>줄 통과, <invalid>줄 고칠 것".
|
||||||
|
나쁜 줄을 고쳐 주지는 않는다 — 고치는 것은 사람. 앞의 칸은 그대로. 기대 결과 cases/lab01-challenge2.json, lab01 · lab01-challenge 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | 줄 순서대로 | 보낸 사람이 자기 파일의 몇째 줄인지 바로 찾게 (`row`는 1부터) |
|
||||||
|
| ② | 그대로 | 같은 코드에 늘 같은 말. LLM으로 만들면 매번 말이 달라짐 |
|
||||||
|
| ③ | 고칠 것 | 테스트가 `message`를 글자 그대로 봄 |
|
||||||
|
| ④ | 사람 | 워크플로우가 고쳐 넣으면 무엇이 틀렸는지 아무도 모름 (README 원칙) |
|
||||||
|
|
||||||
|
정답: [answers/78-lab01-challenge2-fix-list.json](answers/78-lab01-challenge2-fix-list.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/07`) | **1/5** · `fix_list` · `message` 없음 ("앞의 칸은 그대로"만 ✔) |
|
||||||
|
| 고친 뒤 (`answers/08`) | **5/5** · B-1 고칠 줄 10 (3째 FAQ-103 ~ 12째 FAQ-112), `B-1: 13줄 중 3줄 통과, 10줄 고칠 것`, B-2 FAQ-209 하나. `lab01` 5/5 · `lab01-challenge` 4/4 · `lab01-edges` 6/6 |
|
||||||
|
|
||||||
|
- 풀이 때: "고침 말을 LLM으로 만들면?" — 같은 코드에 날마다 다른 말, 돈, 그리고 지어낸 조언. 표 하나면 충분.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Lab 02 · 품질 게이트 · 막고, 격리하고, 알리기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-02-Q-QUALITY-GATE.md](LAB-02-Q-QUALITY-GATE.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 2026-11-01 정답 워크플로우 1회 실행 값입니다 (LLM `gpt-4.1-mini` · temperature 0, 시간은 그때그때 다름). 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-import.json 을 새로 만들어 줘. 웹훅 POST my-faq-import, 이름 "78 FAQ import (my)".
|
||||||
|
Run check: 규칙을 다시 쓰지 말고 HTTP POST http://localhost:5678/webhook/my-faq-check 로 부른다 (본문 그대로, neverError, timeout 30000).
|
||||||
|
Batch ok?(IF): invalid / total 이 기준 이하일 때만 받는다. 기준은 본문 max_invalid_ratio, 없으면 0.3. 넘으면 HTTP 422 {error: "batch rejected", batch, invalid_ratio (4자리), max_invalid_ratio, codes}.
|
||||||
|
Split rows → Store row: 줄마다 ok 면 POST /faq/add, 아니면 POST /quarantine {items: [{batch, row, errors}]} — IF 로 가르지 말고 HTTP 노드 하나에서 주소를 표현식으로.
|
||||||
|
Summarize: {batch, total, imported, quarantined, rejected: false, score (imported / total, 4자리), max_invalid_ratio, codes, imported_ids, quarantined_ids}. 기대 결과 cases/lab02.json 앞 네 케이스.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `my-faq-check` | 규칙을 다시 쓰지 않고 Lab 01의 검사를 부름 — 규칙이 한 벌 |
|
||||||
|
| ② | `0.3` | AGENTS.md 3절 기본 기준. 예제 3에서 바꿔 봄 |
|
||||||
|
| ③ | `422` | 받았지만 처리할 수 없는 내용 (400은 모양이 틀린 것) |
|
||||||
|
| ④ | 하나 | IF로 갈랐다가 Merge로 합치면 LLM을 더한 뒤 격리 수가 틀림 (AGENTS.md 5절) |
|
||||||
|
|
||||||
|
정답: [reference/lab02-example1-import.json](reference/lab02-example1-import.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/09`, 기준에 같은 테스트) | **0/6** · B-1 **12줄 넣음**(FAQ-105만 API가 막음, FAQ-109 전화 · FAQ-110 링크 · FAQ-003 두 벌), B-2 10줄 (메일 주소 포함), 격리 0 |
|
||||||
|
| 고친 뒤 (`answers/10`) | **5/6** · B-1 422 (`invalid_ratio` 0.7692), B-2 9줄 · FAQ-209 격리 · `score` 0.9, B-3 FAQ-305 격리. FAQ-210만 ✘ (예제 2) |
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-import.json 에 LLM 검사를 더해 줘.
|
||||||
|
Split rows 뒤에 Only passed(Code) 로 규칙을 통과한 줄만 Judge(AI Agent + OpenAI gpt-4.1-mini, temperature 0) 에게 보낸다.
|
||||||
|
지시문: 제목과 답만 주고 "답이 제목의 질문에 답하나?" 를 {"match": true|false, "reason"} JSON 으로만.
|
||||||
|
Decide(Code): Judge 의 i번째 결과를 Only passed 의 i번째 줄과 맞추고, 읽을 수 없으면 match false. match 가 false 면 errors 에 llm_mismatch.
|
||||||
|
Judge 의 onError 는 continueRegularOutput. 규칙에 걸린 줄은 LLM 에 보내지 않고 그대로 격리. 앞의 칸은 그대로. 기대 결과 cases/lab02.json 전체.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | 통과한 | 규칙에 걸린 줄은 이미 격리 — LLM 비용을 쓸 까닭이 없음 (75일차) |
|
||||||
|
| ② | `false` | 판정을 읽을 수 없으면 넣지 않음 (fail-closed, 73일차) |
|
||||||
|
| ③ | `llm_mismatch` | AGENTS.md 3절 코드. 격리함에서 이유가 보임 |
|
||||||
|
| ④ | `continueRegularOutput` | LLM 오류여도 멈추지 않고 그 줄을 격리로 |
|
||||||
|
|
||||||
|
정답: [reference/lab02-example2-import.json](reference/lab02-example2-import.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 | **5/6** (예제 1 그대로) |
|
||||||
|
| 고친 뒤 (`answers/11`) | **6/6** · B-2 8줄 · FAQ-209(`pii`) · FAQ-210(`llm_mismatch`) 격리 · `score` 0.8, B-3 5줄. B-2 1.4초 → **9.3초** |
|
||||||
|
|
||||||
|
- LLM이 막은 줄은 FAQ-210 **하나**. 풀이 때: "드문 한 줄을 위해 줄마다 1초" — 가져오기라 괜찮지만 헬프데스크 답이라면?
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/12`)
|
||||||
|
|
||||||
|
| 실험 | 기준 | 결과 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| B-1 | 0.3 | 422 · 0.7692 |
|
||||||
|
| B-1 | 0.8 | 받음 · 넣음 **2** · 격리 **11** · `score` 0.1538 (FAQ-113은 LLM이 막음) |
|
||||||
|
| FAQ만 비움 | — | `cleared` 2 |
|
||||||
|
| B-2 | 0.05 | 422 · 0.1 |
|
||||||
|
| B-2 | 0.1 | 받음 (딱 같음) · 넣음 8 · 격리 2 |
|
||||||
|
| 격리함 | — | 13 |
|
||||||
|
|
||||||
|
- **6/6.** 수업 준비 때 실험 순서 때문에 두 번 틀렸음: ① B-1을 받은 뒤 B-2의 FAQ-201이 `same_title` → FAQ 비우기를 넣음, ② B-2를 넣은 뒤 0.05를 보내니 9줄 `exists_id`로 0.9 → 0.05를 먼저. 학생에게도 "앞 배치가 다음 판정을 바꾼다"로.
|
||||||
|
- 강사 결론: 0.3 유지. 토론 질문: "B-2를 한 줄 때문에 돌려보내는 팀과, B-1을 받아 11줄을 격리하는 팀 중 누가 더 일이 많은가?"
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-import.json 에 품질 기록과 알림을 더해 줘.
|
||||||
|
배치마다 POST http://data-api:8800/quality-log 한 줄 {batch, dataset: "faq", total, valid: imported, quarantined, rejected, score, codes} — 거절된 배치도 rejected: true · score 0 으로.
|
||||||
|
거절이면 POST /alerts {workflow: "faq-import", severity: "high", reason: "batch_rejected", batch, message}.
|
||||||
|
받은 배치의 score 가 0.9 미만이면 reason "low_quality" 알림.
|
||||||
|
알림을 보내도 응답은 같은 결과 (Summarize 의 결과를 다시). 기대 결과 cases/lab02-challenge.json, cases/lab02.json 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `rejected: true · score 0` | 거절도 기록해야 보고서에 '나쁜 배치'가 보임 |
|
||||||
|
| ② | `batch_rejected` | AGENTS.md 3절 알림 이유. 테스트가 이 글자를 찾음 |
|
||||||
|
| ③ | `0.9` | 열 줄 중 한 줄 넘게 격리되면 사람이 볼 일 |
|
||||||
|
| ④ | 같은 결과 | 알림은 곁가지. 부른 쪽이 받는 결과는 바뀌지 않아야 함 |
|
||||||
|
|
||||||
|
정답: [answers/78-lab02-challenge-log-alert.json](answers/78-lab02-challenge-log-alert.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/13`) | **2/5** · 가져오기 둘만 ✔, 기록 · 알림 0 |
|
||||||
|
| 고친 뒤 (`answers/14`) | **5/5** · 기록 B-1(거절 · 0) · B-2(0.8, 8 · 2), 알림 `batch_rejected` · `low_quality`. `lab02` 6/6 |
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-import.json 의 결과에 llm_calls 를 더해 줘.
|
||||||
|
llm_calls = LLM 검사에 보낸 줄 수 (Only passed 의 줄 수). 거절된 배치(422)의 본문에도 llm_calls 0.
|
||||||
|
LLM 은 규칙을 통과한 줄에만 부른다는 것을 숫자로 보이게 하는 칸이다. 다른 칸은 그대로.
|
||||||
|
기대 결과 cases/lab02-challenge2.json 을 세 번 돌려 늘 같은지, lab02 · lab02-challenge 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | 수 | `$('Only passed').all().length` |
|
||||||
|
| ② | `0` | 거절된 배치는 LLM 앞에서 끝남 — 비용 0을 숫자로 |
|
||||||
|
| ③ | 통과한 | B-2는 10줄 중 9줄, B-3은 6줄 중 5줄만 |
|
||||||
|
| ④ | 세 번 | LLM 판정이 늘 같은지 (`--repeat`는 같은 배치를 다시 보내 `exists_id`로 막힘 — 실행 결과 참고) |
|
||||||
|
|
||||||
|
정답: [answers/78-lab02-challenge2-llm-calls.json](answers/78-lab02-challenge2-llm-calls.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/15`) | **0/3** · `llm_calls` 없음 |
|
||||||
|
| 고친 뒤 (`answers/16`) | **3/3 × 세 번** · B-1 0 · B-2 9 · B-3 5, 세 번 모두 FAQ-210만 막음 (B-2 9.4 · 8.1 · 7.6초). `--no-before --repeat 2`는 **2/6** (B-2 · B-3 거절 — `exists_id`). `lab02` 6/6 · `lab02-challenge` 5/5 |
|
||||||
|
|
||||||
|
- 풀이 때: `--repeat`의 ✘는 판정이 흔들린 것이 아니라 데이터가 바뀐 것. "같은 배치를 두 번 보내도 FAQ가 두 벌 생기지 않는다"는 덤.
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Lab 03 · 품질 관리 · 스키마로 넓히고, 고쳐서 다시, 추세로 보기
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업이 끝난 뒤 올립니다. 학생용 문제는 [LAB-03-Q-QUALITY-MANAGEMENT.md](LAB-03-Q-QUALITY-MANAGEMENT.md).
|
||||||
|
> 요청문은 예시이며 CLI 에이전트로 보내 재지는 않았습니다. 결과 숫자는 2026-11-01 정답 워크플로우 1회 실행 값입니다. 명령 · 출력 전부: [answers/LAB-TEST-RESULT.md](answers/LAB-TEST-RESULT.md).
|
||||||
|
|
||||||
|
## 예제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/check.json 을 새로 만들어 줘. 웹훅 POST my-check, 이름 "78 Schema check (my)".
|
||||||
|
Get schema: HTTP GET http://data-api:8800/schemas/<본문 schema> (neverError). 스키마가 없으면(fields 가 없음) HTTP 400 {error: "unknown schema", schema}.
|
||||||
|
Check rows(Code): 본문 csv 가 있으면 첫 줄을 열 이름으로 쉼표로 나눠 줄로 바꾸고, items 가 있으면 그대로.
|
||||||
|
필드 규칙은 코드에 쓰지 말고 스키마의 fields 를 읽어 AGENTS.md 3절 타입 표대로 (required · string · enum · email · phone · date · pattern · list), 키는 배치 안 중복.
|
||||||
|
결과 {schema, batch, total, valid, invalid, rows: [{row (1부터), key, ok, errors}], codes}. 기대 결과 cases/lab03.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `400` | 모르는 스키마는 검사할 수 없음 |
|
||||||
|
| ② | 열 이름 | `name,dept,email,phone,joined` |
|
||||||
|
| ③ | `fields` | 규칙을 데이터(스키마)로 두면 새 데이터셋에 워크플로우를 고치지 않음 (예제 3) |
|
||||||
|
| ④ | 1부터 | CSV의 열 이름 줄을 빼고 사람이 세는 번호 |
|
||||||
|
|
||||||
|
정답: [reference/lab03-example1-schema-check.json](reference/lab03-example1-schema-check.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/17`) | FAQ 검사에 직원 CSV → **400** (`items must be a list`), `lab03` **0/4** (404) |
|
||||||
|
| 고친 뒤 (`answers/18`) | **4/4** · 직원 9줄 중 4줄 (메일 · 팀 · 13월 · 이름 없음 · 중복), FAQ 스키마로 B-3 6줄 중 5줄, 모르는 스키마 400 |
|
||||||
|
|
||||||
|
**스키마로 하는 것 · 못 하는 것 (`QUALITY.md` 8절 예)**
|
||||||
|
|
||||||
|
| 규칙 | 스키마로 | 따로 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 필수 · 길이 · 형식 · 목록 · 중복 | ✔ | |
|
||||||
|
| 개인정보 (답 안의 전화 · 메일) | | Code (Lab 01) |
|
||||||
|
| 이미 있는 데이터와 겹침 | | Code + 데이터 API (Lab 01 과제 1) |
|
||||||
|
| 답이 질문에 맞나 | | LLM (Lab 02 예제 2) |
|
||||||
|
|
||||||
|
## 예제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/faq-import.json 에서 고쳐서 다시 넣은 줄을 격리함에서 빼 줘.
|
||||||
|
Summarize 뒤에 Release fixed: POST http://data-api:8800/quarantine/remove {ids: imported_ids}.
|
||||||
|
넣은 줄만 뺀다 — 이번에도 걸린 줄은 격리함에 남는다. 품질 기록 · 알림은 그대로.
|
||||||
|
응답은 Summarize 의 결과를 다시 (Release fixed 의 응답이 아님). 기대 결과 cases/lab03-fix.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `imported_ids` | 이번에 넣은 id |
|
||||||
|
| ② | 남는다 | 고쳤는데 또 틀린 줄은 다시 사람에게 |
|
||||||
|
| ③ | 그대로 | Lab 02 과제 1의 기록 · 알림 |
|
||||||
|
| ④ | 다시 | HTTP 노드의 출력은 API 응답 — 결과는 Summarize에서 다시 꺼냄 |
|
||||||
|
|
||||||
|
정답: [reference/lab03-example2-import.json](reference/lab03-example2-import.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/19`) | **3/4** · 넣기는 됨(`score` 1 · FAQ 16), 격리함 **2** 그대로 |
|
||||||
|
| 고친 뒤 (`answers/20`) | **4/4** · 격리함 0, 기록 0.8 → 1 |
|
||||||
|
|
||||||
|
## 예제 3 · 결과 (에이전트 없이, `answers/21`)
|
||||||
|
|
||||||
|
| | FAQ 검사 | 스키마 검사 (고치지 않음) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 접수 건 9줄 | 0줄 통과 (`id_format` · `keywords_missing` · `answer_missing` 9, 빈 id 중복 8) | **2줄 통과**, 줄마다 다른 이유 7가지 |
|
||||||
|
| 테스트 | — | **5/5** (직원 CSV 4/9 그대로) |
|
||||||
|
|
||||||
|
- 풀이 때: "스키마 파일 한 장으로 새 데이터셋" — 그래서 스키마 변경은 **규칙 변경**이라 리뷰가 필요 (74일차 관문).
|
||||||
|
|
||||||
|
## 과제 1 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-work/quality-report.json 을 새로 만들어 줘. 웹훅 POST my-quality-report, 이름 "78 Quality report (my)".
|
||||||
|
Get log: HTTP GET http://data-api:8800/quality-log. 본문에 dataset 이 있으면 그 데이터셋만.
|
||||||
|
Make report(Code) 하나에서 AGENTS.md 3절 "품질 보고서" 정의대로 count · batches · avg_score (소수 4자리, 0건이면 0) · worst_batch (같으면 먼저 나온 것) · rejected_batches.
|
||||||
|
top_codes: 모든 기록의 codes 를 더해 많은 순, 같으면 코드 글자순. trend: 마지막 score − 첫 score 가 0.05 넘으면 좋아짐, −0.05 미만이면 나빠짐, 아니면 그대로.
|
||||||
|
LLM 은 쓰지 않는다. 기대 결과 cases/lab03-challenge.json.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `4` | 0.65833…을 0.6583으로 |
|
||||||
|
| ② | 먼저 나온 것 | 같은 점수면 순서로 하나를 정함 — 늘 같은 답 |
|
||||||
|
| ③ | 글자순 | 같은 수면 코드 이름으로 — 늘 같은 순서 |
|
||||||
|
| ④ | 그대로 | 1건뿐이거나 차이가 작으면 |
|
||||||
|
|
||||||
|
정답: [answers/78-lab03-challenge-report.json](answers/78-lab03-challenge-report.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/22`) | **0/4** · 404 (기록 4줄은 생김, B-1 `!` allow_fail) |
|
||||||
|
| 고친 뒤 (`answers/23`) | **4/4** · 4 · 0.6583 · B-1 · 거절 1, `pii` 2, `좋아짐` |
|
||||||
|
|
||||||
|
- 풀이 때: 거절을 빼면 0.8778. `trend`가 "13줄 거절 → 고친 2줄"을 비교한 것이라는 함정.
|
||||||
|
|
||||||
|
## 과제 2 · 요청문 예
|
||||||
|
|
||||||
|
```text
|
||||||
|
두 워크플로우를 고쳐 줘.
|
||||||
|
① my-work/check.json: 검사가 끝나면 POST http://data-api:8800/quality-log 한 줄 {batch, dataset: schema, total, valid, quarantined: invalid, rejected: false, score: valid / total (4자리), codes}. 응답은 검사 결과 그대로.
|
||||||
|
② my-work/quality-report.json: by_dataset — 모든 기록을 dataset 마다 {count, avg_score, worst_batch, top_code} (처음 나온 순서), worst_dataset — avg_score 가 가장 낮은 데이터셋.
|
||||||
|
본문의 dataset 거르기와 앞의 칸은 그대로. 기대 결과 cases/lab03-challenge2.json, lab03 · lab03-challenge 도.
|
||||||
|
```
|
||||||
|
|
||||||
|
**빈칸 답**
|
||||||
|
|
||||||
|
| 빈칸 | 답 | 왜 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ① | `schema` | 직원 · 티켓 · FAQ가 데이터셋 이름 |
|
||||||
|
| ② | 처음 나온 | 보고서가 늘 같은 순서 |
|
||||||
|
| ③ | 낮은 | 가장 손볼 데이터셋 |
|
||||||
|
| ④ | 그대로 | `lab03-challenge` 4/4가 그대로여야 함 |
|
||||||
|
|
||||||
|
정답: [answers/78-lab03-challenge2-check-log.json](answers/78-lab03-challenge2-check-log.json) · [answers/78-lab03-challenge2-report.json](answers/78-lab03-challenge2-report.json)
|
||||||
|
|
||||||
|
| 11/01 결과 | |
|
||||||
|
| --- | --- |
|
||||||
|
| 고치기 전 (`answers/24`) | **0/4** · 기록 1줄 (FAQ만), `by_dataset` 없음 |
|
||||||
|
| 고친 뒤 (`answers/25`) | **4/4** · employee 0.4444 · ticket **0.2222** · faq 0.8333, `worst_dataset` ticket, 전체 0.5. `lab03` · `lab03-challenge` 4/4 |
|
||||||
|
|
||||||
|
- 풀이 때: 섞은 `trend`(좋아짐)는 직원 → FAQ를 비교한 것 — 추세는 데이터셋 안에서.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 7교시 바이브 코딩 · `tcheck` 정답본
|
||||||
|
|
||||||
|
> **강사용 정답본** · 학생 저장소에는 수업 뒤에 올립니다. 학생용: [VIBE-REQ-TCHECK.md](VIBE-REQ-TCHECK.md) → [VIBE-Q-TCHECK.md](VIBE-Q-TCHECK.md).
|
||||||
|
> 강사가 직접 만든 본: [tcheck/](tcheck/README.md) (테스트 22개, `uv tool install --editable .`). 아래 판정은 2026-11-01 강사 Mac에서 78일차 데이터 API에 실제로 돌린 값이고, n8n `my-check`의 응답(정답 실행 기록 `answers/18` · `answers/21`)과 비교했습니다. 68일차부터 바이브 코딩은 CLI 에이전트로 다시 만들어 재지 않습니다 (요청문은 예시).
|
||||||
|
|
||||||
|
## 1. TODO 예 (요청의 말 → 할 일)
|
||||||
|
|
||||||
|
| # | 요청의 말 | 할 일 (내 말) | 꼭 · 나중 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| ① | CSV · JSON · 요청 파일 | `.csv` · JSON 목록 · `{"items"}` · `{"schema", "batch", "csv" \| "items"}` | 꼭 |
|
||||||
|
| ② | 스키마는 API에서, 파일로도 | `GET /schemas/<name>` · `--schema-file`, 필드 이름을 코드에 쓰지 않음 | 꼭 |
|
||||||
|
| ③ | 몇째 줄 · 왜 · 같은 코드 | 줄 번호 1부터, AGENTS.md 3절 코드 | 꼭 |
|
||||||
|
| ④ | n8n과 똑같은 판정 | 공백 = 빈 값, 선택 칸, 실제 날짜, 정규식 ASCII, 키 중복 | 꼭 |
|
||||||
|
| ⑤ | JSON, 같은 칸 이름 | `--json` = `my-check` 모양 | 꼭 |
|
||||||
|
| ⑥ | 깨끗한 줄 · 나쁜 줄 파일 | `--out` · `--rejects` (+ `errors` 칸, 고치지 않음) | 꼭 |
|
||||||
|
| ⑦ | 거절될까? · 종료 코드 | `gate --max-invalid 0.3`, 넘으면 4 | 꼭 |
|
||||||
|
| ⑧ | 개인정보 · 바깥 링크 | `--content answer` (Lab 01 규칙) | 나중 |
|
||||||
|
| ⑨ | 사람이 읽는 말 · 한 줄 | `--fix` (`fix_list` · `message`) | 나중 |
|
||||||
|
| ⑩ | 품질 기록에도 | `--log` (`/quality-log`) | 나중 |
|
||||||
|
| ⑪ | 화면 · 탭 · 거르기 · 내보내기 | `ui` | 꼭 |
|
||||||
|
| ⑫ | 종료 코드 | 0 · 1 · 2 · 4 | 꼭 |
|
||||||
|
| 끝 | n8n 없이 시험 · 같은 판정 확인 | 스키마 · 배치 · n8n 결과 사본, 가짜 데이터 API | 꼭 |
|
||||||
|
|
||||||
|
## 2. 요청문 (처음 한 번 + 다음 단계 한 줄)
|
||||||
|
|
||||||
|
단계마다 긴 요청문을 쓰지 않습니다. 할 일은 이미 `Doc/SPEC.md` · `Doc/IMP-PLAN.md`에 있으니, 에이전트가 그 파일을 읽게 하면 됩니다.
|
||||||
|
|
||||||
|
### 처음 한 번
|
||||||
|
|
||||||
|
```text
|
||||||
|
Doc/SPEC.md 와 Doc/IMP-PLAN.md 를 읽고 체크 안 된 첫 단계만 만들어 줘. uv 프로젝트, 명령 이름 tcheck, 코드와 화면 글자는 영어.
|
||||||
|
끝나면 IMP-PLAN 체크를 표시하고 확인 명령을 알려 줘. 다음 단계는 내가 말할 때까지 하지 마.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 그다음 단계마다 (같은 한 줄)
|
||||||
|
|
||||||
|
```text
|
||||||
|
확인했고 commit 했어. 다음 단계.
|
||||||
|
```
|
||||||
|
|
||||||
|
- 그냥 "계속"만 치면 단계 두세 개를 한꺼번에 하거나 체크 · 확인 명령을 빼먹기 쉽습니다. 이 한 줄은 처음 요청의 약속(한 단계만 · 체크 · 확인 명령 · 멈춤)을 이어 가게 합니다.
|
||||||
|
- 대화가 길어지면 새 세션에서 **처음 요청**을 다시 보냅니다. "체크 안 된 첫 단계"부터 이어집니다.
|
||||||
|
- 에이전트가 단계 내용을 잘못 알았다면, 요청문을 길게 쓰기보다 **SPEC · IMP-PLAN을 고치고** 다시 시킵니다.
|
||||||
|
|
||||||
|
### 틀렸을 때만 · 고쳐 달라는 요청문 예
|
||||||
|
|
||||||
|
무엇이 틀렸나(보인 것) · SPEC의 어느 요구인가 · 어디만 고치나 · 무엇으로 확인하나, 넷을 적습니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
tcheck check tickets-new.json 이 4째 줄(opened 2026-02-30)을 통과시켜. n8n my-check 는 bad_format:opened 야. SPEC R4 대로 실제 있는 날짜인지까지 봐야 해.
|
||||||
|
checks.py 의 date 판정만 고치고, 2026-02-30 · 2026-13-01 은 bad_format, 2026-10-28 은 통과인 테스트를 넣어 줘. 다른 파일은 건드리지 마.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 확인 기준과 강사 실측 (11/01, 78일차 데이터 API)
|
||||||
|
|
||||||
|
| 요구 | 기준 | 실측 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| R1 | 요청 파일(CSV 든) · CSV · 목록 | `employees-new.json` · `employees.csv` · `tickets-new.json` 모두 9줄 |
|
||||||
|
| R2 | 스키마 셋, 필드 이름 없이 | `employee` · `ticket` · `faq` 같은 `field_errors`로, 없는 스키마 → 1 |
|
||||||
|
| R3 | 직원 | 3 `bad_format:email` · 4 `not_allowed:dept` · 5 `bad_format:joined` · 6 `missing:name` · 8 `duplicate_in_batch`, 9줄 중 4줄 |
|
||||||
|
| R4 | `my-check`와 같은 판정 | 직원 HR-1 · 접수 건 TK-1 · FAQ B-3의 `rows` · `codes` · `valid`가 **모두 같음** (`tests/data/n8n-my-check.json`) |
|
||||||
|
| R5 | `--json` | `{schema, batch, total, valid, invalid, rows, codes}` — 위와 같은 비교 |
|
||||||
|
| R6 | 파일 둘 | 직원 → 깨끗한 4줄 · 나쁜 5줄 (첫 줄 `errors` = `bad_format:email`) |
|
||||||
|
| R7 | `tcheck gate` | 직원 0.5556 → `✘ rejected` · **종료 코드 4**, `--max-invalid 0.6` → 0, B-2 `--content answer` 0.1 · 기준 0.1 → `✔ accepted` (같으면 통과) |
|
||||||
|
| R8 | `--content answer` | B-2 → FAQ-209 `pii`, `faq-edges.json` → 10줄 중 4줄 (FAQ-406 `pii` · FAQ-409 `external_link`) — Lab 01 예제 3의 내 검사와 같은 4줄, B-1 → 13줄 중 8줄 나쁨 (0.6154) |
|
||||||
|
| R9 | `--fix` 접수 건 | `TK-1: 9줄 중 2줄 통과, 7줄 고칠 것`, 3째 "priority은(는) low · normal · high 중 하나로" |
|
||||||
|
| R10 | `--log` | `flow.py quality` → `HR-1 employee total 9 valid 4 score 0.4444` |
|
||||||
|
| R11 | `run_test` | 문제 줄 5 · 코드 5 · 깨끗한 줄 4, `email`로 거르면 1줄, `e` → 깨끗한 4줄 파일 |
|
||||||
|
| R12 | `uv run pytest -q` | 22 passed in 3.0s |
|
||||||
|
| R13 | 종료 코드 | 없는 파일 1 · 없는 스키마 1 · 스키마를 안 줌 2 · 모르는 명령 2 · 거절 4 |
|
||||||
|
| R14 | `/tmp`에서 `tcheck --help` | `usage: tcheck [-h] {check,gate,ui} ...`, `tcheck check …` 0.06초 |
|
||||||
|
|
||||||
|
- 수업 준비 때 찾은 실수 (학생도 겪기 쉬움): ① Python `re`의 `\w`는 한글도 맞아 메일 판정이 JavaScript와 다를 수 있음 → `re.ASCII`. ② TUI 거르기 상자를 연 `/`가 상자 글자에 들어가고, 상자를 닫아도 포커스가 남아 `e`가 글자로 들어감 → `/`를 지우고 닫을 때 표로 포커스.
|
||||||
|
- 같은 FAQ B-1을 스키마 + 내용 규칙으로 보면 Lab 01 예제 2의 8줄과 **같은 줄**이 나쁩니다. 코드 이름만 다릅니다 (`title_missing` ↔ `missing:title`, `id_format` ↔ `bad_format:id`). 발표 때 "같은 판정 · 다른 이름"을 짚을 거리.
|
||||||
|
|
||||||
|
## 4. 7교시에 볼 것
|
||||||
|
|
||||||
|
| 볼 것 | 막혔을 때 한마디 |
|
||||||
|
| --- | --- |
|
||||||
|
| 판정이 n8n과 다름 | "어느 줄 · 어느 코드가 다른지 붙여 주고, AGENTS.md 3절 스키마 표 그대로" |
|
||||||
|
| 필드 이름이 코드에 | "스키마의 fields를 돌며 type마다" |
|
||||||
|
| 2월 30일 · 13월이 통과 | "실제 있는 날짜인지까지" |
|
||||||
|
| 빈 선택 칸이 `bad_format` | "비었으면 required만 보고 다음 칸으로" |
|
||||||
|
| 테스트가 진짜 API를 부름 | "가짜 서버로, 진짜는 확인 단계에서만" |
|
||||||
|
| 규칙이 여기저기 | "checks.py 한 곳" |
|
||||||
|
| `gate`가 늘 0 | "넘으면 거절, 거절이면 4" |
|
||||||
|
| 나쁜 줄 파일에 고친 값 | "원래 줄 그대로 + errors 칸만" |
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
$ uv run tools/flow.py ops --reset
|
||||||
|
✔ live run logs, added FAQ, quarantine, quality log, created tickets, DLQ and alerts emptied, fault switch all ok
|
||||||
|
|
||||||
|
$ uv run tools/flow.py off my-faq-check my-faq-import my-check my-quality-report
|
||||||
|
✔ unpublished my-faq-check (id weecbf2cc21d6fd2)
|
||||||
|
✔ unpublished my-faq-import (id we9ca88e4f314222)
|
||||||
|
✔ unpublished my-check (id wf4c2f04db13849f)
|
||||||
|
✔ unpublished my-quality-report (id we40b9a92abbf7fd)
|
||||||
|
✔ n8n restarted. Turn it back on with: uv run tools/flow.py push <the workflow file>
|
||||||
|
|
||||||
|
$ uv run tools/flow.py push reference/baseline-faq-check.json reference/baseline-faq-import.json
|
||||||
|
✔ pushed '78 FAQ check (team baseline)' (id w2d09b3af4461197)
|
||||||
|
POST http://localhost:5678/webhook/base-faq-check
|
||||||
|
✔ pushed '78 FAQ import (team baseline)' (id w45c6472ffd87501)
|
||||||
|
POST http://localhost:5678/webhook/base-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 0
|
||||||
|
quality log: 0
|
||||||
|
alerts: 0
|
||||||
|
|
||||||
|
$ uv run tools/flow.py doctor
|
||||||
|
! n8n install folder /Users/mcchae/day65-n8n
|
||||||
|
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
|
||||||
|
✔ n8n container day65-n8n-n8n-1
|
||||||
|
✔ n8n http://localhost:5678
|
||||||
|
✔ data API http://localhost:8800
|
||||||
|
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||||
|
✔ run logs /logs (666 seeded runs) · FAQ additions /faq-extra (day 77 data API)
|
||||||
|
✔ schemas faq · employee · ticket, quarantine, quality log (day 78 data API)
|
||||||
|
✔ n8n → http://data-api:8800
|
||||||
|
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||||
|
✔ node catalog (for check)
|
||||||
|
✔ CLI coding agent: codex, claude, agy
|
||||||
|
ready
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
$ uv run tools/flow.py call base-faq-check requests/faq-batch-1.json
|
||||||
|
HTTP 200 · 0.9s
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,865 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01.json",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"passed": 1,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "빈 값 (제목 · 답 · 키워드)",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "형식 (id · 답 길이)",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"rows.3.errors has none of ['id_format']",
|
||||||
|
"rows.5.errors has none of ['answer_too_long']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 안 중복 · 개인정보 · 바깥 링크",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"rows.6.errors has none of ['duplicate_in_batch']",
|
||||||
|
"rows.8.errors has none of ['pii']",
|
||||||
|
"rows.9.errors has none of ['external_link']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 · 메일 주소만 걸림, 사내 링크는 통과",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"valid = 10 (expected 9)",
|
||||||
|
"codes.pii = null (expected 1)",
|
||||||
|
"rows.8.ok = true (expected false)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "목록이 아닌 본문 → 400",
|
||||||
|
"webhook": "base-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 500 (expected 400)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 500,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-x",
|
||||||
|
"items": "FAQ-101"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"message": "Error in workflow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,818 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01.json --webhook base-faq-check -v
|
||||||
|
POST http://localhost:5678/webhook/base-faq-check · 5 case(s) × 1
|
||||||
|
✔ 빈 값 (제목 · 답 · 키워드) · 0.0s
|
||||||
|
→ request POST /webhook/base-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 형식 (id · 답 길이) · 0.1s · rows.3.errors has none of ['id_format']; rows.5.errors has none of ['answer_too_long']
|
||||||
|
→ request POST /webhook/base-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 배치 안 중복 · 개인정보 · 바깥 링크 · 0.0s · rows.6.errors has none of ['duplicate_in_batch']; rows.8.errors has none of ['pii']; rows.9.errors has none of ['external_link']
|
||||||
|
→ request POST /webhook/base-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 3,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"keywords_missing": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 배치 2 · 메일 주소만 걸림, 사내 링크는 통과 · 0.1s · valid = 10 (expected 9); codes.pii = null (expected 1); rows.8.ok = true (expected false)
|
||||||
|
→ request POST /webhook/base-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 10,
|
||||||
|
"invalid": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {}
|
||||||
|
}
|
||||||
|
✘ 목록이 아닌 본문 → 400 · 0.1s · status 500 (expected 400)
|
||||||
|
→ request POST /webhook/base-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-x",
|
||||||
|
"items": "FAQ-101"
|
||||||
|
}
|
||||||
|
← response HTTP 500
|
||||||
|
{
|
||||||
|
"message": "Error in workflow"
|
||||||
|
}
|
||||||
|
Passed 1/5
|
||||||
|
report: runs/lab01-20261101-180505.json
|
||||||
@@ -0,0 +1,905 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01.json",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "빈 값 (제목 · 답 · 키워드)",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "형식 (id · 답 길이)",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 안 중복 · 개인정보 · 바깥 링크",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 · 메일 주소만 걸림, 사내 링크는 통과",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 9,
|
||||||
|
"invalid": 1,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "목록이 아닌 본문 → 400",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 400,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-x",
|
||||||
|
"items": "FAQ-101"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "items must be a list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,991 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab01-example2-check.json
|
||||||
|
✔ pushed '78 FAQ check (reference)' (id w318880a1bd61aa2)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check · 5 case(s) × 1
|
||||||
|
✔ 빈 값 (제목 · 답 · 키워드) · 0.9s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 형식 (id · 답 길이) · 0.0s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 안 중복 · 개인정보 · 바깥 링크 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 2 · 메일 주소만 걸림, 사내 링크는 통과 · 0.0s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 9,
|
||||||
|
"invalid": 1,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 목록이 아닌 본문 → 400 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-x",
|
||||||
|
"items": "FAQ-101"
|
||||||
|
}
|
||||||
|
← response HTTP 400
|
||||||
|
{
|
||||||
|
"error": "items must be a list"
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-20261101-180518.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-faq-check requests/faq-batch-1.json
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,899 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge.json",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "이미 있는 id (FAQ-003)",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"rows.10.errors has none of ['exists_id']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "이미 있는 제목 · 쓰인 키워드는 경고",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"rows.11.errors has none of ['same_title']",
|
||||||
|
"rows.11.warnings has none of ['keyword_used:vpn']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 1 · 통과 3건",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"valid = 5 (expected 3)",
|
||||||
|
"invalid = 8 (expected 10)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "막 더한 FAQ도 기존 데이터",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"rows.0.errors has none of ['exists_id']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 9,
|
||||||
|
"invalid": 1,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,890 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check · 4 case(s) × 1
|
||||||
|
✘ 이미 있는 id (FAQ-003) · 0.1s · rows.10.errors has none of ['exists_id']
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 이미 있는 제목 · 쓰인 키워드는 경고 · 0.1s · rows.11.errors has none of ['same_title']; rows.11.warnings has none of ['keyword_used:vpn']
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ 배치 1 · 통과 3건 · 0.0s · valid = 5 (expected 3); invalid = 8 (expected 10)
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 8,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq/add · HTTP 200 · 0.0s · {"added": "FAQ-201", "size": 7}
|
||||||
|
→ request POST http://localhost:8800/faq/add
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 예약합니다."
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"added": "FAQ-201",
|
||||||
|
"size": 7
|
||||||
|
}
|
||||||
|
✘ 막 더한 FAQ도 기존 데이터 · 0.1s · rows.0.errors has none of ['exists_id']
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 9,
|
||||||
|
"invalid": 1,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 0/4
|
||||||
|
report: runs/lab01-challenge-20261101-180519.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,920 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab01-challenge.json",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "이미 있는 id (FAQ-003)",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "이미 있는 제목 · 쓰인 키워드는 경고",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 1 · 통과 3건",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "막 더한 FAQ도 기존 데이터",
|
||||||
|
"webhook": "my-faq-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"invalid": 2,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id",
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:회의실"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1,
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,936 @@
|
|||||||
|
$ uv run tools/flow.py push answers/78-lab01-challenge-existing.json
|
||||||
|
! unpublished '78 FAQ check (reference)' (w318880a1bd61aa2): it used the same webhook path
|
||||||
|
✔ pushed '78 FAQ check + existing data (answer)' (id w99a77a91b9217c9)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check · 4 case(s) × 1
|
||||||
|
✔ 이미 있는 id (FAQ-003) · 0.9s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 이미 있는 제목 · 쓰인 키워드는 경고 · 0.0s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 1 · 통과 3건 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"invalid": 10,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:vpn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq/add · HTTP 200 · 0.0s · {"added": "FAQ-201", "size": 7}
|
||||||
|
→ request POST http://localhost:8800/faq/add
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 예약합니다."
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"added": "FAQ-201",
|
||||||
|
"size": 7
|
||||||
|
}
|
||||||
|
✔ 막 더한 FAQ도 기존 데이터 · 0.0s
|
||||||
|
→ request POST /webhook/my-faq-check
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"invalid": 2,
|
||||||
|
"with_warnings": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"exists_id",
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"warnings": [
|
||||||
|
"keyword_used:회의실"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"ok": true,
|
||||||
|
"errors": [],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1,
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab01-challenge-20261101-180533.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab01.json
|
||||||
|
POST http://localhost:5678/webhook/my-faq-check · 5 case(s) × 1
|
||||||
|
✔ 빈 값 (제목 · 답 · 키워드) · 0.1s
|
||||||
|
✔ 형식 (id · 답 길이) · 0.2s
|
||||||
|
✔ 배치 안 중복 · 개인정보 · 바깥 링크 · 0.1s
|
||||||
|
✔ 배치 2 · 메일 주소만 걸림, 사내 링크는 통과 · 0.0s
|
||||||
|
✔ 목록이 아닌 본문 → 400 · 0.1s
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab01-20261101-180533.json
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,773 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "base-faq-import",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "나쁜 배치는 통째로 거절 (422)",
|
||||||
|
"webhook": "base-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 200 (expected 422)",
|
||||||
|
"invalid_ratio = null (expected 0.3..1)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"imported": 12,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9231,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-101",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-103",
|
||||||
|
"FAQ-10",
|
||||||
|
"FAQ-106",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-108",
|
||||||
|
"FAQ-109",
|
||||||
|
"FAQ-110",
|
||||||
|
"FAQ-003",
|
||||||
|
"FAQ-112",
|
||||||
|
"FAQ-113"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리",
|
||||||
|
"webhook": "base-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"quarantined_ids has none of ['FAQ-209']"
|
||||||
|
],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 10,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ에 들어갔고 메일 주소는 안 들어감",
|
||||||
|
"webhook": "http://localhost:8800/faq/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"faqs must not have ['security.desk']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 27,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함에 이유와 함께",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"items has none of ['\"pii\"']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 제목과 답이 다른 줄 (FAQ-210)",
|
||||||
|
"webhook": "base-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"imported = 10 (expected 8)",
|
||||||
|
"quarantined = 0 (expected 2)",
|
||||||
|
"codes.llm_mismatch = null (expected 1)",
|
||||||
|
"quarantined_ids has none of ['FAQ-210']"
|
||||||
|
],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 10,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 규칙에 걸린 줄은 그대로 (FAQ-305)",
|
||||||
|
"webhook": "base-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"imported = 6 (expected 5)",
|
||||||
|
"quarantined_ids has none of ['FAQ-305']"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 6,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-305",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,771 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02.json --webhook base-faq-import -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/base-faq-import · 6 case(s) × 1
|
||||||
|
✘ 나쁜 배치는 통째로 거절 (422) · 0.1s · status 200 (expected 422); invalid_ratio = null (expected 0.3..1)
|
||||||
|
→ request POST /webhook/base-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"imported": 12,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9231,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-101",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-103",
|
||||||
|
"FAQ-10",
|
||||||
|
"FAQ-106",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-108",
|
||||||
|
"FAQ-109",
|
||||||
|
"FAQ-110",
|
||||||
|
"FAQ-003",
|
||||||
|
"FAQ-112",
|
||||||
|
"FAQ-113"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
✘ 배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리 · 1.1s · quarantined_ids has none of ['FAQ-209']
|
||||||
|
→ request POST /webhook/base-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 10,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
✘ FAQ에 들어갔고 메일 주소는 안 들어감 · 0.0s · faqs must not have ['security.desk']
|
||||||
|
→ request POST http://localhost:8800/faq/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 27,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 격리함에 이유와 함께 · 0.0s · items has none of ['"pii"']
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 21}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 21
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✘ LLM · 제목과 답이 다른 줄 (FAQ-210) · 1.1s · imported = 10 (expected 8); quarantined = 0 (expected 2); codes.llm_mismatch = null (expected 1); quarantined_ids has none of ['FAQ-210']
|
||||||
|
→ request POST /webhook/base-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 10,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
✘ LLM · 규칙에 걸린 줄은 그대로 (FAQ-305) · 0.1s · imported = 6 (expected 5); quarantined_ids has none of ['FAQ-305']
|
||||||
|
→ request POST /webhook/base-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 6,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-305",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": []
|
||||||
|
}
|
||||||
|
Passed 0/6
|
||||||
|
report: runs/lab02-20261101-180552.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 16}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 16
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 0
|
||||||
|
quality log: 0
|
||||||
|
alerts: 0
|
||||||
@@ -0,0 +1,689 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "나쁜 배치는 통째로 거절 (422)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 9,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ에 들어갔고 메일 주소는 안 들어감",
|
||||||
|
"webhook": "http://localhost:8800/faq/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 15,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함에 이유와 함께",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:06:06"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 제목과 답이 다른 줄 (FAQ-210)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"imported = 9 (expected 8)",
|
||||||
|
"quarantined = 1 (expected 2)",
|
||||||
|
"codes.llm_mismatch = null (expected 1)",
|
||||||
|
"quarantined_ids has none of ['FAQ-210']"
|
||||||
|
],
|
||||||
|
"seconds": 1.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 9,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 규칙에 걸린 줄은 그대로 (FAQ-305)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab02-example1-import.json
|
||||||
|
✔ pushed '78 FAQ import + gate (reference)' (id we19b3911975e55e)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 6 case(s) × 1
|
||||||
|
✔ 나쁜 배치는 통째로 거절 (422) · 0.9s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리 · 1.4s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 9,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ FAQ에 들어갔고 메일 주소는 안 들어감 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/faq/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 15,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 격리함에 이유와 함께 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:06:06"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 9}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 9
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✘ LLM · 제목과 답이 다른 줄 (FAQ-210) · 1.2s · imported = 9 (expected 8); quarantined = 1 (expected 2); codes.llm_mismatch = null (expected 1); quarantined_ids has none of ['FAQ-210']
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 9,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.9,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ LLM · 규칙에 걸린 줄은 그대로 (FAQ-305) · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 5/6
|
||||||
|
report: runs/lab02-20261101-180607.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 14}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 14
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 2
|
||||||
|
B-2 FAQ-209 pii
|
||||||
|
B-3 FAQ-305 answer_too_short
|
||||||
|
quality log: 0
|
||||||
|
alerts: 0
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 6,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "나쁜 배치는 통째로 거절 (422)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.0,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 9.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ에 들어갔고 메일 주소는 안 들어감",
|
||||||
|
"webhook": "http://localhost:8800/faq/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 14,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함에 이유와 함께",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:18"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 제목과 답이 다른 줄 (FAQ-210)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 8.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "LLM · 규칙에 걸린 줄은 그대로 (FAQ-305)",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 4.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,708 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab02-example2-import.json
|
||||||
|
! unpublished '78 FAQ import + release fixed rows (reference)' (we9ca88e4f314222): it used the same webhook path
|
||||||
|
✔ pushed '78 FAQ import + LLM check (reference)' (id wee5dfb169c284f5)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 6 case(s) × 1
|
||||||
|
✔ 나쁜 배치는 통째로 거절 (422) · 1.0s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리 · 9.3s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ FAQ에 들어갔고 메일 주소는 안 들어감 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/faq/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 14,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 격리함에 이유와 함께 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:18"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 8
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ LLM · 제목과 답이 다른 줄 (FAQ-210) · 8.4s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ LLM · 규칙에 걸린 줄은 그대로 (FAQ-305) · 4.4s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-20261101-181231.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 13
|
||||||
|
}
|
||||||
@@ -0,0 +1,790 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-gate.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 6,
|
||||||
|
"total": 6,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "B-1 · 기본 기준 0.3 → 거절",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-1 · 기준을 0.8로 풀면 → 받음, 넣는 것은 2줄",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 9.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"max_invalid_ratio": 0.8,
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 11,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.1538,
|
||||||
|
"max_invalid_ratio": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-101",
|
||||||
|
"FAQ-102"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-103",
|
||||||
|
"FAQ-10",
|
||||||
|
"FAQ-105",
|
||||||
|
"FAQ-106",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-108",
|
||||||
|
"FAQ-109",
|
||||||
|
"FAQ-110",
|
||||||
|
"FAQ-003",
|
||||||
|
"FAQ-112",
|
||||||
|
"FAQ-113"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ만 처음으로 (B-1이 넣은 2줄을 비움, 격리함은 그대로)",
|
||||||
|
"webhook": "http://localhost:8800/faq-reset",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-2 · 기준 0.05 → 거절",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"max_invalid_ratio": 0.05,
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-2",
|
||||||
|
"invalid_ratio": 0.1,
|
||||||
|
"max_invalid_ratio": 0.05,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-2 · 기준 0.1 · 나쁜 줄이 딱 10% → 받음",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 8.7,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"max_invalid_ratio": 0.1,
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.1,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함이 13줄 (B-1 11 + B-2 2)",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 13,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-103",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-10",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-105",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-106",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-113",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-108",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-003",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-109",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-110",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-112",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-102",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,792 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-gate.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 6 case(s) × 1
|
||||||
|
✔ B-1 · 기본 기준 0.3 → 거절 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ B-1 · 기준을 0.8로 풀면 → 받음, 넣는 것은 2줄 · 9.0s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"max_invalid_ratio": 0.8,
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"total": 13,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 11,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.1538,
|
||||||
|
"max_invalid_ratio": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-101",
|
||||||
|
"FAQ-102"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-103",
|
||||||
|
"FAQ-10",
|
||||||
|
"FAQ-105",
|
||||||
|
"FAQ-106",
|
||||||
|
"FAQ-102",
|
||||||
|
"FAQ-108",
|
||||||
|
"FAQ-109",
|
||||||
|
"FAQ-110",
|
||||||
|
"FAQ-003",
|
||||||
|
"FAQ-112",
|
||||||
|
"FAQ-113"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ FAQ만 처음으로 (B-1이 넣은 2줄을 비움, 격리함은 그대로) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ B-2 · 기준 0.05 → 거절 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"max_invalid_ratio": 0.05,
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-2",
|
||||||
|
"invalid_ratio": 0.1,
|
||||||
|
"max_invalid_ratio": 0.05,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ B-2 · 기준 0.1 · 나쁜 줄이 딱 10% → 받음 · 8.7s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"max_invalid_ratio": 0.1,
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.1,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 격리함이 13줄 (B-1 11 + B-2 2) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 13,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-103",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"title_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-10",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"id_format"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-105",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-106",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"answer_too_long"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-113",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-108",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"keywords_missing"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-003",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"exists_id"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-109",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-110",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"external_link"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-112",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"same_title"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-102",
|
||||||
|
"batch": "B-1",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:12:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-gate-20261101-181249.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 14 (FAQ-202, FAQ-201, FAQ-203, FAQ-204, FAQ-205, FAQ-207, FAQ-208, FAQ-206)
|
||||||
|
quarantine: 13
|
||||||
|
B-1 FAQ-108 keywords_missing
|
||||||
|
B-1 FAQ-003 exists_id
|
||||||
|
B-1 FAQ-109 pii
|
||||||
|
B-1 FAQ-110 external_link
|
||||||
|
B-1 FAQ-112 same_title
|
||||||
|
B-1 FAQ-102 duplicate_in_batch
|
||||||
|
B-2 FAQ-210 llm_mismatch
|
||||||
|
B-2 FAQ-209 pii
|
||||||
|
quality log: 0
|
||||||
|
alerts: 0
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 2,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "거절도 품질 기록",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 가져오기",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 8.7,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "품질 기록 두 줄 (거절 · 0.8)",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 0 (expected 2)",
|
||||||
|
"items.0.rejected = null (expected true)",
|
||||||
|
"items.1.score = null (expected 0.8)",
|
||||||
|
"items.1.valid = null (expected 8)",
|
||||||
|
"items.1.quarantined = null (expected 2)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림 두 건 (거절 · 낮은 점수)",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 0 (expected 2)",
|
||||||
|
"alerts has none of ['batch_rejected']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림에 낮은 점수",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"alerts has none of ['low_quality']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 8
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 13
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 5 case(s) × 1
|
||||||
|
✔ 거절도 품질 기록 · 0.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 2 가져오기 · 8.7s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 품질 기록 두 줄 (거절 · 0.8) · 0.0s · count = 0 (expected 2); items.0.rejected = null (expected true); items.1.score = null (expected 0.8); items.1.valid = null (expected 8); items.1.quarantined = null (expected 2)
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
✘ 알림 두 건 (거절 · 낮은 점수) · 0.0s · count = 0 (expected 2); alerts has none of ['batch_rejected']
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
✘ 알림에 낮은 점수 · 0.0s · alerts has none of ['low_quality']
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"alerts": []
|
||||||
|
}
|
||||||
|
Passed 2/5
|
||||||
|
report: runs/lab02-challenge-20261101-180707.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 8
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "거절도 품질 기록",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.3,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치 2 가져오기",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 9.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "품질 기록 두 줄 (거절 · 0.8)",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"score": 0,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:07:21"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:07:31"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림 두 건 (거절 · 낮은 점수)",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 2,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "batch_rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"message": "B-1 rejected",
|
||||||
|
"at": 1793524041.7771661
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Low score?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "low_quality",
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"message": "B-2 quality 0.8",
|
||||||
|
"at": 1793524051.234785
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "알림에 낮은 점수",
|
||||||
|
"webhook": "http://localhost:8800/alerts/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 2,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "batch_rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"message": "B-1 rejected",
|
||||||
|
"at": 1793524041.7771661
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Low score?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "low_quality",
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"message": "B-2 quality 0.8",
|
||||||
|
"at": 1793524051.234785
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
$ uv run tools/flow.py push answers/78-lab02-challenge-log-alert.json
|
||||||
|
! unpublished '78 FAQ import + LLM check (reference)' (wee5dfb169c284f5): it used the same webhook path
|
||||||
|
✔ pushed '78 FAQ import + quality log and alerts (answer)' (id w53039b5a517bf0d)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.1s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 5 case(s) × 1
|
||||||
|
✔ 거절도 품질 기록 · 1.3s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 배치 2 가져오기 · 9.4s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 품질 기록 두 줄 (거절 · 0.8) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 13,
|
||||||
|
"valid": 3,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"score": 0,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:07:21"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:07:31"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 알림 두 건 (거절 · 낮은 점수) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "batch_rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"message": "B-1 rejected",
|
||||||
|
"at": 1793524041.7771661
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Low score?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "low_quality",
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"message": "B-2 quality 0.8",
|
||||||
|
"at": 1793524051.234785
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 알림에 낮은 점수 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/alerts/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "batch_rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"message": "B-1 rejected",
|
||||||
|
"at": 1793524041.7771661
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"workflow": "faq-import",
|
||||||
|
"node": "Low score?",
|
||||||
|
"severity": "high",
|
||||||
|
"reason": "low_quality",
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"message": "B-2 quality 0.8",
|
||||||
|
"at": 1793524051.234785
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab02-challenge-20261101-180731.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 8
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 6 case(s) × 1
|
||||||
|
✔ 나쁜 배치는 통째로 거절 (422) · 0.1s
|
||||||
|
✔ 배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리 · 8.8s
|
||||||
|
✔ FAQ에 들어갔고 메일 주소는 안 들어감 · 0.0s
|
||||||
|
✔ 격리함에 이유와 함께 · 0.0s
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
✔ LLM · 제목과 답이 다른 줄 (FAQ-210) · 8.6s
|
||||||
|
✔ LLM · 규칙에 걸린 줄은 그대로 (FAQ-305) · 4.5s
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-20261101-180753.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 3
|
||||||
|
B-2 FAQ-210 llm_mismatch
|
||||||
|
B-2 FAQ-209 pii
|
||||||
|
B-3 FAQ-305 answer_too_short
|
||||||
|
quality log: 4
|
||||||
|
B-1 faq total 13 valid 3 score 0 rejected True
|
||||||
|
B-2 faq total 10 valid 8 score 0.8 rejected False
|
||||||
|
B-2 faq total 10 valid 8 score 0.8 rejected False
|
||||||
|
B-3 faq total 6 valid 5 score 0.8333 rejected False
|
||||||
|
alerts: 4
|
||||||
|
[high] faq-import · low_quality · B-2 quality 0.8
|
||||||
|
[high] faq-import · low_quality · B-2 quality 0.8
|
||||||
|
[high] faq-import · low_quality · B-3 quality 0.8333
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge2.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 3,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "거절된 배치는 LLM을 부르지 않음",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"llm_calls = null (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-2 · 규칙을 통과한 9줄만 LLM에",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"llm_calls = null (expected 9)"
|
||||||
|
],
|
||||||
|
"seconds": 8.6,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-3 · 5줄만 LLM에",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"llm_calls = null (expected 5)"
|
||||||
|
],
|
||||||
|
"seconds": 4.3,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 4
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 4
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 3 case(s) × 1
|
||||||
|
✘ 거절된 배치는 LLM을 부르지 않음 · 0.1s · llm_calls = null (expected 0)
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✘ B-2 · 규칙을 통과한 9줄만 LLM에 · 8.6s · llm_calls = null (expected 9)
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ B-3 · 5줄만 LLM에 · 4.3s · llm_calls = null (expected 5)
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 0/3
|
||||||
|
report: runs/lab02-challenge2-20261101-180806.json
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab02-challenge2.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 3,
|
||||||
|
"total": 3,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "거절된 배치는 LLM을 부르지 않음",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 1.1,
|
||||||
|
"status": 422,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-2 · 규칙을 통과한 9줄만 LLM에",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 9.4,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "B-3 · 5줄만 LLM에",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 4.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
$ uv run tools/flow.py push answers/78-lab02-challenge2-llm-calls.json
|
||||||
|
! unpublished '78 FAQ import + quality log and alerts (answer)' (w53039b5a517bf0d): it used the same webhook path
|
||||||
|
✔ pushed '78 FAQ import + LLM calls (answer)' (id w4b0730166dd93f5)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 13
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 3 case(s) × 1
|
||||||
|
✔ 거절된 배치는 LLM을 부르지 않음 · 1.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
✔ B-2 · 규칙을 통과한 9줄만 LLM에 · 9.4s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
✔ B-3 · 5줄만 LLM에 · 4.1s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
Passed 3/3
|
||||||
|
report: runs/lab02-challenge2-20261101-180834.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 3 case(s) × 1
|
||||||
|
✔ 거절된 배치는 LLM을 부르지 않음 · 0.1s
|
||||||
|
✔ B-2 · 규칙을 통과한 9줄만 LLM에 · 8.1s
|
||||||
|
✔ B-3 · 5줄만 LLM에 · 4.4s
|
||||||
|
Passed 3/3
|
||||||
|
report: runs/lab02-challenge2-20261101-180846.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 3 case(s) × 1
|
||||||
|
✔ 거절된 배치는 LLM을 부르지 않음 · 0.1s
|
||||||
|
✔ B-2 · 규칙을 통과한 9줄만 LLM에 · 7.6s
|
||||||
|
✔ B-3 · 5줄만 LLM에 · 4.0s
|
||||||
|
Passed 3/3
|
||||||
|
report: runs/lab02-challenge2-20261101-180858.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge2.json --no-before --repeat 2
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 3 case(s) × 2
|
||||||
|
✔ 거절된 배치는 LLM을 부르지 않음 #1 · 0.1s
|
||||||
|
✔ 거절된 배치는 LLM을 부르지 않음 #2 · 0.1s
|
||||||
|
✘ B-2 · 규칙을 통과한 9줄만 LLM에 #1 · 0.1s · status 422 (expected 200); llm_calls = 0 (expected 9); imported = null (expected 8); quarantined_ids has none of ['FAQ-210']
|
||||||
|
✘ B-2 · 규칙을 통과한 9줄만 LLM에 #2 · 0.1s · status 422 (expected 200); llm_calls = 0 (expected 9); imported = null (expected 8); quarantined_ids has none of ['FAQ-210']
|
||||||
|
✘ B-3 · 5줄만 LLM에 #1 · 0.1s · status 422 (expected 200); llm_calls = 0 (expected 5); imported = null (expected 5)
|
||||||
|
✘ B-3 · 5줄만 LLM에 #2 · 0.1s · status 422 (expected 200); llm_calls = 0 (expected 5); imported = null (expected 5)
|
||||||
|
Passed 2/6
|
||||||
|
report: runs/lab02-challenge2-20261101-180859.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 9}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 9}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 6 case(s) × 1
|
||||||
|
✔ 나쁜 배치는 통째로 거절 (422) · 0.1s
|
||||||
|
✔ 배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리 · 8.2s
|
||||||
|
✔ FAQ에 들어갔고 메일 주소는 안 들어감 · 0.0s
|
||||||
|
✔ 격리함에 이유와 함께 · 0.0s
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
✔ LLM · 제목과 답이 다른 줄 (FAQ-210) · 8.1s
|
||||||
|
✔ LLM · 규칙에 걸린 줄은 그대로 (FAQ-305) · 5.0s
|
||||||
|
Passed 6/6
|
||||||
|
report: runs/lab02-20261101-180921.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 13}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab02-challenge.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 5 case(s) × 1
|
||||||
|
✔ 거절도 품질 기록 · 0.1s
|
||||||
|
✔ 배치 2 가져오기 · 8.5s
|
||||||
|
✔ 품질 기록 두 줄 (거절 · 0.8) · 0.0s
|
||||||
|
✔ 알림 두 건 (거절 · 낮은 점수) · 0.0s
|
||||||
|
✔ 알림에 낮은 점수 · 0.0s
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab02-challenge-20261101-180930.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03.json",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "직원 CSV · 9줄 중 4줄 통과",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"schema = null (expected employee)",
|
||||||
|
"total = null (expected 9)",
|
||||||
|
"valid = null (expected 4)",
|
||||||
|
"rows.6.ok = null (expected true)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "직원 CSV · 줄마다 이유",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"rows.2.errors has none of ['bad_format:email']",
|
||||||
|
"rows.3.errors has none of ['not_allowed:dept']",
|
||||||
|
"rows.4.errors has none of ['bad_format:joined']",
|
||||||
|
"rows.5.errors has none of ['missing:name']",
|
||||||
|
"rows.7.errors has none of ['duplicate_in_batch']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "같은 워크플로우로 FAQ 스키마",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"schema = null (expected faq)",
|
||||||
|
"total = null (expected 6)",
|
||||||
|
"valid = null (expected 5)",
|
||||||
|
"rows.4.errors has none of ['too_short:answer']"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "모르는 스키마 → 400",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 400)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"schema": "invoice",
|
||||||
|
"items": []
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
$ uv run tools/flow.py call my-faq-check requests/employees-new.json
|
||||||
|
HTTP 400 · 0.0s
|
||||||
|
{
|
||||||
|
"error": "items must be a list"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-check · 4 case(s) × 1
|
||||||
|
✘ 직원 CSV · 9줄 중 4줄 통과 · 0.0s · status 404 (expected 200); schema = null (expected employee); total = null (expected 9); valid = null (expected 4); rows.6.ok = null (expected true)
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 직원 CSV · 줄마다 이유 · 0.0s · status 404 (expected 200); rows.2.errors has none of ['bad_format:email']; rows.3.errors has none of ['not_allowed:dept']; rows.4.errors has none of ['bad_format:joined']; rows.5.errors has none of ['missing:name']; rows.7.errors has none of ['duplicate_in_batch']
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 같은 워크플로우로 FAQ 스키마 · 0.0s · status 404 (expected 200); schema = null (expected faq); total = null (expected 6); valid = null (expected 5); rows.4.errors has none of ['too_short:answer']
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 모르는 스키마 → 400 · 0.0s · status 404 (expected 400)
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "invoice",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-check\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
Passed 0/4
|
||||||
|
report: runs/lab03-20261101-180930.json
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03.json",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "직원 CSV · 9줄 중 4줄 통과",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.9,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "직원 CSV · 줄마다 이유",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "같은 워크플로우로 FAQ 스키마",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "FAQ-301",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "FAQ-302",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "FAQ-303",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "FAQ-304",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "FAQ-305",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:answer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "FAQ-306",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"too_short:answer": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "모르는 스키마 → 400",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 400,
|
||||||
|
"request": {
|
||||||
|
"schema": "invoice",
|
||||||
|
"items": []
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"error": "unknown schema",
|
||||||
|
"schema": "invoice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab03-example1-schema-check.json
|
||||||
|
✔ pushed '78 Schema check (reference)' (id w6b17d7c76908a6a)
|
||||||
|
POST http://localhost:5678/webhook/my-check
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-check · 4 case(s) × 1
|
||||||
|
✔ 직원 CSV · 9줄 중 4줄 통과 · 0.9s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 직원 CSV · 줄마다 이유 · 0.0s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 같은 워크플로우로 FAQ 스키마 · 0.1s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "FAQ-301",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "FAQ-302",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "FAQ-303",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "FAQ-304",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "FAQ-305",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:answer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "FAQ-306",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"too_short:answer": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 모르는 스키마 → 400 · 0.0s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "invoice",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
← response HTTP 400
|
||||||
|
{
|
||||||
|
"error": "unknown schema",
|
||||||
|
"schema": "invoice"
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-20261101-180943.json
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-fix.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 3,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "고친 두 줄 다시 가져오기",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.5,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함이 비었나",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"size = 2 (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ에 FAQ-209 · FAQ-210",
|
||||||
|
"webhook": "http://localhost:8800/faq/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 16,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "품질 기록 · 점수 1",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 2,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"codes": {},
|
||||||
|
"at": "2026-11-01T18:09:54"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-fix.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 8.4s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 4 case(s) × 1
|
||||||
|
✔ 고친 두 줄 다시 가져오기 · 2.5s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
✘ 격리함이 비었나 · 0.0s · size = 2 (expected 0)
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-209",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"pii"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row_id": "FAQ-210",
|
||||||
|
"batch": "B-2",
|
||||||
|
"row": {
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"llm_mismatch"
|
||||||
|
],
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ FAQ에 FAQ-209 · FAQ-210 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/faq/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 16,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 품질 기록 · 점수 1 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:09:51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 2,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"codes": {},
|
||||||
|
"at": "2026-11-01T18:09:54"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 3/4
|
||||||
|
report: runs/lab03-fix-20261101-180954.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 10}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 10
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 2
|
||||||
|
B-2 FAQ-209 pii
|
||||||
|
B-2 FAQ-210 llm_mismatch
|
||||||
|
quality log: 2
|
||||||
|
B-2 faq total 10 valid 8 score 0.8 rejected False
|
||||||
|
B-2-fix faq total 2 valid 2 score 1 rejected False
|
||||||
|
alerts: 1
|
||||||
|
[high] faq-import · low_quality · B-2 quality 0.8
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-fix.json",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "고친 두 줄 다시 가져오기",
|
||||||
|
"webhook": "my-faq-import",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 2.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "격리함이 비었나",
|
||||||
|
"webhook": "http://localhost:8800/quarantine/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"size": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "FAQ에 FAQ-209 · FAQ-210",
|
||||||
|
"webhook": "http://localhost:8800/faq/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 16,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "품질 기록 · 점수 1",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:10:18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 2,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"codes": {},
|
||||||
|
"at": "2026-11-01T18:10:20"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
$ uv run tools/flow.py push reference/lab03-example2-import.json
|
||||||
|
! unpublished '78 FAQ import + LLM calls (answer)' (w4b0730166dd93f5): it used the same webhook path
|
||||||
|
✔ pushed '78 FAQ import + release fixed rows (reference)' (id we9ca88e4f314222)
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-fix.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 10.7s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-faq-import · 4 case(s) × 1
|
||||||
|
✔ 고친 두 줄 다시 가져오기 · 2.0s
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
✔ 격리함이 비었나 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quarantine/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"size": 0,
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
✔ FAQ에 FAQ-209 · FAQ-210 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/faq/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 16,
|
||||||
|
"faqs": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-001",
|
||||||
|
"title": "비밀번호 재설정",
|
||||||
|
"keywords": [
|
||||||
|
"비밀번호",
|
||||||
|
"패스워드",
|
||||||
|
"password",
|
||||||
|
"재설정",
|
||||||
|
"초기화"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-002",
|
||||||
|
"title": "계정 잠김 해제",
|
||||||
|
"keywords": [
|
||||||
|
"계정",
|
||||||
|
"잠김",
|
||||||
|
"잠금",
|
||||||
|
"로그인",
|
||||||
|
"실패"
|
||||||
|
],
|
||||||
|
"answer": "로그인 5회 실패 시 30분 동안 잠깁니다. 30분 후 다시 시도하거나 헬프데스크에 잠금 해제를 요청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"원격",
|
||||||
|
"재택",
|
||||||
|
"접속",
|
||||||
|
"연결"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-004",
|
||||||
|
"title": "프린터 출력 안 됨",
|
||||||
|
"keywords": [
|
||||||
|
"프린터",
|
||||||
|
"출력",
|
||||||
|
"인쇄",
|
||||||
|
"용지"
|
||||||
|
],
|
||||||
|
"answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-005",
|
||||||
|
"title": "메일함 용량 초과",
|
||||||
|
"keywords": [
|
||||||
|
"메일",
|
||||||
|
"용량",
|
||||||
|
"메일함",
|
||||||
|
"첨부"
|
||||||
|
],
|
||||||
|
"answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-006",
|
||||||
|
"title": "소프트웨어 설치 요청",
|
||||||
|
"keywords": [
|
||||||
|
"설치",
|
||||||
|
"소프트웨어",
|
||||||
|
"프로그램",
|
||||||
|
"라이선스"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 품질 기록 · 점수 1 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 10,
|
||||||
|
"valid": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:10:18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 2,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"codes": {},
|
||||||
|
"at": "2026-11-01T18:10:20"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-fix-20261101-181020.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 10}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 10
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 0
|
||||||
|
quality log: 2
|
||||||
|
B-2 faq total 10 valid 8 score 0.8 rejected False
|
||||||
|
B-2-fix faq total 2 valid 2 score 1 rejected False
|
||||||
|
alerts: 1
|
||||||
|
[high] faq-import · low_quality · B-2 quality 0.8
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-schema.json",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"passed": 5,
|
||||||
|
"total": 5,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "스키마 목록에 ticket이 있다",
|
||||||
|
"webhook": "http://localhost:8800/schemas/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"schemas": [
|
||||||
|
"employee",
|
||||||
|
"faq",
|
||||||
|
"ticket"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "새 스키마 ticket · 워크플로우는 그대로 · 9줄 중 2줄 통과",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "형식 · 목록 · 날짜 · 길이",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "선택 칸 형식 · 중복 · 빈 칸, 선택 칸이 없어도 통과",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "직원 CSV는 그대로 9줄 중 4줄",
|
||||||
|
"webhook": "my-check",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,844 @@
|
|||||||
|
$ uv run tools/flow.py call my-faq-check requests/tickets-new.json
|
||||||
|
HTTP 200 · 0.1s
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 0,
|
||||||
|
"invalid": 9,
|
||||||
|
"with_warnings": 0,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"warnings": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"id_format": 9,
|
||||||
|
"keywords_missing": 9,
|
||||||
|
"answer_missing": 9,
|
||||||
|
"duplicate_in_batch": 8
|
||||||
|
},
|
||||||
|
"fix_list": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"id": "",
|
||||||
|
"codes": [
|
||||||
|
"id_format",
|
||||||
|
"keywords_missing",
|
||||||
|
"answer_missing",
|
||||||
|
"duplicate_in_batch"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"id를 FAQ-숫자 3자리로 (예: FAQ-104)",
|
||||||
|
"키워드를 하나 이상 넣어 주세요",
|
||||||
|
"답을 써 주세요",
|
||||||
|
"같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message": "TK-1: 9줄 중 0줄 통과, 9줄 고칠 것"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-schema.json -v
|
||||||
|
POST http://localhost:5678/webhook/my-check · 5 case(s) × 1
|
||||||
|
✔ 스키마 목록에 ticket이 있다 · 0.0s
|
||||||
|
→ request POST http://localhost:8800/schemas/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schemas": [
|
||||||
|
"employee",
|
||||||
|
"faq",
|
||||||
|
"ticket"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 새 스키마 ticket · 워크플로우는 그대로 · 9줄 중 2줄 통과 · 0.1s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 형식 · 목록 · 날짜 · 길이 · 0.1s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 선택 칸 형식 · 중복 · 빈 칸, 선택 칸이 없어도 통과 · 0.0s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ 직원 CSV는 그대로 9줄 중 4줄 · 0.1s
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Passed 5/5
|
||||||
|
report: runs/lab03-schema-20261101-181021.json
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge.json",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "배치 네 개 · 평균 · 가장 나쁜 배치",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"count = null (expected 4)",
|
||||||
|
"avg_score = null (expected 0.6583)",
|
||||||
|
"worst_batch = null (expected B-1)",
|
||||||
|
"rejected_batches = null (expected 1)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "가장 많은 이유 · 흐름",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"top_codes.0.code = null (expected pii)",
|
||||||
|
"top_codes.0.count = null (expected 2)",
|
||||||
|
"trend = null (expected 좋아짐)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치마다 점수 순서",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"batches.1.score = null (expected 0.8)",
|
||||||
|
"batches.2.score = null (expected 0.8333)",
|
||||||
|
"batches.3.batch = null (expected B-2-fix)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "다른 데이터셋으로 거르기",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"status 404 (expected 200)",
|
||||||
|
"count = null (expected 0)",
|
||||||
|
"avg_score = null (expected 0)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 404,
|
||||||
|
"request": {
|
||||||
|
"dataset": "employee"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,457 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 2
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
! before: POST /webhook/my-faq-import · HTTP 422 · 0.1s (allow_fail: rejected on purpose) · {"error": "batch rejected", "batch": "B-1", "invalid_ratio": 0.7692, "max_invalid_ratio": 0.3, "codes": {"title_missing"
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 8.4s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 4.6s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 1.9s · {"batch": "B-2-fix", "total": 2, "imported": 2, "quarantined": 0, "rejected": false, "score": 1, "max_invalid_ratio": 0.
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
|
||||||
|
✘ 배치 네 개 · 평균 · 가장 나쁜 배치 · 0.0s · status 404 (expected 200); count = null (expected 4); avg_score = null (expected 0.6583); worst_batch = null (expected B-1); rejected_batches = null (expected 1)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 가장 많은 이유 · 흐름 · 0.0s · status 404 (expected 200); top_codes.0.code = null (expected pii); top_codes.0.count = null (expected 2); trend = null (expected 좋아짐)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 배치마다 점수 순서 · 0.0s · status 404 (expected 200); batches.1.score = null (expected 0.8); batches.2.score = null (expected 0.8333); batches.3.batch = null (expected B-2-fix)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
✘ 다른 데이터셋으로 거르기 · 0.0s · status 404 (expected 200); count = null (expected 0); avg_score = null (expected 0)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{
|
||||||
|
"dataset": "employee"
|
||||||
|
}
|
||||||
|
← response HTTP 404
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested webhook \"POST my-quality-report\" is not registered.",
|
||||||
|
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||||
|
}
|
||||||
|
Passed 0/4
|
||||||
|
report: runs/lab03-challenge-20261101-181036.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 15}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 15
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge.json",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "배치 네 개 · 평균 · 가장 나쁜 배치",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "가장 많은 이유 · 흐름",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "배치마다 점수 순서",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "다른 데이터셋으로 거르기",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"dataset": "employee"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"batches": [],
|
||||||
|
"avg_score": 0,
|
||||||
|
"worst_batch": null,
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
$ uv run tools/flow.py push answers/78-lab03-challenge-report.json
|
||||||
|
✔ pushed '78 Quality report (answer)' (id wa6b87a0a6957e93)
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 4
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
! before: POST /webhook/my-faq-import · HTTP 422 · 1.2s (allow_fail: rejected on purpose) · {"error": "batch rejected", "batch": "B-1", "invalid_ratio": 0.7692, "max_invalid_ratio": 0.3, "codes": {"title_missing"
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 422
|
||||||
|
{
|
||||||
|
"error": "batch rejected",
|
||||||
|
"batch": "B-1",
|
||||||
|
"invalid_ratio": 0.7692,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"title_missing": 1,
|
||||||
|
"id_format": 1,
|
||||||
|
"answer_missing": 1,
|
||||||
|
"answer_too_long": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"keywords_missing": 1,
|
||||||
|
"pii": 1,
|
||||||
|
"external_link": 1,
|
||||||
|
"exists_id": 1,
|
||||||
|
"same_title": 1
|
||||||
|
},
|
||||||
|
"llm_calls": 0
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 9.5s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-201",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-202",
|
||||||
|
"title": "출입증 재발급",
|
||||||
|
"keywords": [
|
||||||
|
"출입증",
|
||||||
|
"재발급"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-203",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-204",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터",
|
||||||
|
"추가"
|
||||||
|
],
|
||||||
|
"answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-205",
|
||||||
|
"title": "윈도우 업데이트",
|
||||||
|
"keywords": [
|
||||||
|
"업데이트",
|
||||||
|
"윈도우"
|
||||||
|
],
|
||||||
|
"answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-206",
|
||||||
|
"title": "화상회의 장비 대여",
|
||||||
|
"keywords": [
|
||||||
|
"화상회의",
|
||||||
|
"카메라"
|
||||||
|
],
|
||||||
|
"answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-207",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-208",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"keywords": [
|
||||||
|
"공유 드라이브",
|
||||||
|
"권한"
|
||||||
|
],
|
||||||
|
"answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"total": 10,
|
||||||
|
"imported": 8,
|
||||||
|
"quarantined": 2,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"pii": 1,
|
||||||
|
"llm_mismatch": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-201",
|
||||||
|
"FAQ-202",
|
||||||
|
"FAQ-203",
|
||||||
|
"FAQ-204",
|
||||||
|
"FAQ-205",
|
||||||
|
"FAQ-206",
|
||||||
|
"FAQ-207",
|
||||||
|
"FAQ-208"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"llm_calls": 9
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 4.3s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 2.2s · {"batch": "B-2-fix", "total": 2, "imported": 2, "quarantined": 0, "rejected": false, "score": 1, "max_invalid_ratio": 0.
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-209",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT 포털의 '보안 신고' 메뉴로 바로 알려 주세요. 급하면 IT 문의 창구로 연락합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-210",
|
||||||
|
"title": "모니터 밝기 조절",
|
||||||
|
"keywords": [
|
||||||
|
"밝기"
|
||||||
|
],
|
||||||
|
"answer": "모니터 밝기는 모니터 아래 버튼이나 윈도우 '디스플레이 설정'에서 조절합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"total": 2,
|
||||||
|
"imported": 2,
|
||||||
|
"quarantined": 0,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 1,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-209",
|
||||||
|
"FAQ-210"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [],
|
||||||
|
"llm_calls": 2
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
|
||||||
|
✔ 배치 네 개 · 평균 · 가장 나쁜 배치 · 0.0s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
✔ 가장 많은 이유 · 흐름 · 0.1s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
✔ 배치마다 점수 순서 · 0.0s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
|
✔ 다른 데이터셋으로 거르기 · 0.1s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{
|
||||||
|
"dataset": "employee"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"batches": [],
|
||||||
|
"avg_score": 0,
|
||||||
|
"worst_batch": null,
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-challenge-20261101-181105.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 15}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 15
|
||||||
|
}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py call my-quality-report '{}'
|
||||||
|
HTTP 200 · 0.0s
|
||||||
|
{
|
||||||
|
"count": 4,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"score": 0,
|
||||||
|
"rejected": true,
|
||||||
|
"total": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2",
|
||||||
|
"score": 0.8,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-2-fix",
|
||||||
|
"score": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.6583,
|
||||||
|
"worst_batch": "B-1",
|
||||||
|
"rejected_batches": 1,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "pii",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_long",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "exists_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "external_link",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "id_format",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "keywords_missing",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "llm_mismatch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "same_title",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "title_missing",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐"
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge2.json",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"passed": 0,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "검사도 품질 기록 (직원 · 티켓 · FAQ)",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 1 (expected 3)",
|
||||||
|
"items.0.dataset = faq (expected employee)",
|
||||||
|
"items.0.score = 0.8333 (expected 0.4444)",
|
||||||
|
"items.1.dataset = null (expected ticket)",
|
||||||
|
"items.2.dataset = null (expected faq)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "데이터셋마다 나눠 보기",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 1 (expected 3)",
|
||||||
|
"by_dataset.employee.avg_score = null (expected 0.4444)",
|
||||||
|
"by_dataset.ticket.avg_score = null (expected 0.2222)",
|
||||||
|
"by_dataset.faq.avg_score = null (expected 0.8333)"
|
||||||
|
],
|
||||||
|
"seconds": 0.2,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "가장 나쁜 데이터셋 · 그 이유",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"worst_dataset = null (expected ticket)",
|
||||||
|
"by_dataset.employee.top_code = null (expected bad_format:email)"
|
||||||
|
],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "한 데이터셋만 (앞의 칸은 그대로)",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": false,
|
||||||
|
"problems": [
|
||||||
|
"count = 0 (expected 1)",
|
||||||
|
"worst_batch = null (expected TK-1)"
|
||||||
|
],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"dataset": "ticket"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 0,
|
||||||
|
"batches": [],
|
||||||
|
"avg_score": 0,
|
||||||
|
"worst_batch": null,
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 0
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 4}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 4
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 3
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-check · HTTP 200 · 0.1s · {"schema": "employee", "batch": "HR-1", "total": 9, "valid": 4, "invalid": 5, "rows": [{"row": 1, "key": "sehoon.oh@exam
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-check · HTTP 200 · 0.1s · {"schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [{"row": 1, "key": "T-2001", "ok": t
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 4.0s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
|
||||||
|
✘ 검사도 품질 기록 (직원 · 티켓 · FAQ) · 0.0s · count = 1 (expected 3); items.0.dataset = faq (expected employee); items.0.score = 0.8333 (expected 0.4444); items.1.dataset = null (expected ticket); items.2.dataset = null (expected faq)
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✘ 데이터셋마다 나눠 보기 · 0.2s · count = 1 (expected 3); by_dataset.employee.avg_score = null (expected 0.4444); by_dataset.ticket.avg_score = null (expected 0.2222); by_dataset.faq.avg_score = null (expected 0.8333)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
✘ 가장 나쁜 데이터셋 · 그 이유 · 0.1s · worst_dataset = null (expected ticket); by_dataset.employee.top_code = null (expected bad_format:email)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
✘ 한 데이터셋만 (앞의 칸은 그대로) · 0.0s · count = 0 (expected 1); worst_batch = null (expected TK-1)
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{
|
||||||
|
"dataset": "ticket"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"batches": [],
|
||||||
|
"avg_score": 0,
|
||||||
|
"worst_batch": null,
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [],
|
||||||
|
"trend": "그대로"
|
||||||
|
}
|
||||||
|
Passed 0/4
|
||||||
|
report: runs/lab03-challenge2-20261101-181110.json
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
{
|
||||||
|
"cases": "lab03-challenge2.json",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"passed": 4,
|
||||||
|
"total": 4,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"case": "검사도 품질 기록 (직원 · 티켓 · FAQ)",
|
||||||
|
"webhook": "http://localhost:8800/quality-log/list",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 3,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"dataset": "employee",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"quarantined": 5,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.4444,
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"dataset": "ticket",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 7,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.2222,
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:35"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "데이터셋마다 나눠 보기",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 3,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"score": 0.4444,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.5,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:email",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:joined",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:name",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:dept",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "가장 나쁜 데이터셋 · 그 이유",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.0,
|
||||||
|
"status": 200,
|
||||||
|
"request": {},
|
||||||
|
"response": {
|
||||||
|
"count": 3,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"score": 0.4444,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.5,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:email",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:joined",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:name",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:dept",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case": "한 데이터셋만 (앞의 칸은 그대로)",
|
||||||
|
"webhook": "my-quality-report",
|
||||||
|
"run": 1,
|
||||||
|
"passed": true,
|
||||||
|
"problems": [],
|
||||||
|
"seconds": 0.1,
|
||||||
|
"status": 200,
|
||||||
|
"request": {
|
||||||
|
"dataset": "ticket"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,761 @@
|
|||||||
|
$ uv run tools/flow.py push answers/78-lab03-challenge2-check-log.json answers/78-lab03-challenge2-report.json
|
||||||
|
! unpublished '78 Schema check (reference)' (w6b17d7c76908a6a): it used the same webhook path
|
||||||
|
! unpublished '78 Quality report (answer)' (wa6b87a0a6957e93): it used the same webhook path
|
||||||
|
✔ pushed '78 Schema check + quality log (answer)' (id wf4c2f04db13849f)
|
||||||
|
POST http://localhost:5678/webhook/my-check
|
||||||
|
✔ pushed '78 Quality report + by dataset (answer)' (id we40b9a92abbf7fd)
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||||
|
→ request POST http://localhost:8800/faq-reset
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 5
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/quarantine-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/quality-log-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
→ request POST http://localhost:8800/alerts-clear
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"cleared": 1
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-check · HTTP 200 · 0.9s · {"schema": "employee", "batch": "HR-1", "total": 9, "valid": 4, "invalid": 5, "rows": [{"row": 1, "key": "sehoon.oh@exam
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-check · HTTP 200 · 0.3s · {"schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [{"row": 1, "key": "T-2001", "ok": t
|
||||||
|
→ request POST /webhook/my-check
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 5.0s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
|
||||||
|
→ request POST /webhook/my-faq-import
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"imported": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"max_invalid_ratio": 0.3,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"imported_ids": [
|
||||||
|
"FAQ-301",
|
||||||
|
"FAQ-302",
|
||||||
|
"FAQ-303",
|
||||||
|
"FAQ-304",
|
||||||
|
"FAQ-306"
|
||||||
|
],
|
||||||
|
"quarantined_ids": [
|
||||||
|
"FAQ-305"
|
||||||
|
],
|
||||||
|
"llm_calls": 5
|
||||||
|
}
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
|
||||||
|
✔ 검사도 품질 기록 (직원 · 티켓 · FAQ) · 0.0s
|
||||||
|
→ request POST http://localhost:8800/quality-log/list
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"dataset": "employee",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"quarantined": 5,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.4444,
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"dataset": "ticket",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"quarantined": 7,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.2222,
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"dataset": "faq",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"quarantined": 1,
|
||||||
|
"rejected": false,
|
||||||
|
"score": 0.8333,
|
||||||
|
"codes": {
|
||||||
|
"answer_too_short": 1
|
||||||
|
},
|
||||||
|
"at": "2026-11-01T18:11:35"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
✔ 데이터셋마다 나눠 보기 · 0.1s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"score": 0.4444,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.5,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:email",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:joined",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:name",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:dept",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
✔ 가장 나쁜 데이터셋 · 그 이유 · 0.0s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 3,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "HR-1",
|
||||||
|
"score": 0.4444,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"score": 0.8333,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.5,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "answer_too_short",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:email",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:joined",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:name",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:dept",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "좋아짐",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
✔ 한 데이터셋만 (앞의 칸은 그대로) · 0.1s
|
||||||
|
→ request POST /webhook/my-quality-report
|
||||||
|
{
|
||||||
|
"dataset": "ticket"
|
||||||
|
}
|
||||||
|
← response HTTP 200
|
||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batch": "TK-1",
|
||||||
|
"score": 0.2222,
|
||||||
|
"rejected": false,
|
||||||
|
"total": 9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"rejected_batches": 0,
|
||||||
|
"top_codes": [
|
||||||
|
{
|
||||||
|
"code": "bad_format:contact",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:opened",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "bad_format:ticket_id",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "duplicate_in_batch",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "missing:requester",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "not_allowed:priority",
|
||||||
|
"count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "too_short:title",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trend": "그대로",
|
||||||
|
"by_dataset": {
|
||||||
|
"employee": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.4444,
|
||||||
|
"worst_batch": "HR-1",
|
||||||
|
"top_code": "bad_format:email"
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.2222,
|
||||||
|
"worst_batch": "TK-1",
|
||||||
|
"top_code": "bad_format:contact"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"count": 1,
|
||||||
|
"avg_score": 0.8333,
|
||||||
|
"worst_batch": "B-3",
|
||||||
|
"top_code": "answer_too_short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"worst_dataset": "ticket"
|
||||||
|
}
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-challenge2-20261101-181135.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03.json
|
||||||
|
POST http://localhost:5678/webhook/my-check · 4 case(s) × 1
|
||||||
|
✔ 직원 CSV · 9줄 중 4줄 통과 · 0.1s
|
||||||
|
✔ 직원 CSV · 줄마다 이유 · 0.1s
|
||||||
|
✔ 같은 워크플로우로 FAQ 스키마 · 0.1s
|
||||||
|
✔ 모르는 스키마 → 400 · 0.0s
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-20261101-181136.json
|
||||||
|
|
||||||
|
$ uv run tools/flow.py test cases/lab03-challenge.json
|
||||||
|
✔ before: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||||
|
✔ before: POST http://localhost:8800/quarantine-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
✔ before: POST http://localhost:8800/quality-log-clear · HTTP 200 · 0.0s · {"cleared": 6}
|
||||||
|
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||||
|
! before: POST /webhook/my-faq-import · HTTP 422 · 0.1s (allow_fail: rejected on purpose) · {"error": "batch rejected", "batch": "B-1", "invalid_ratio": 0.7692, "max_invalid_ratio": 0.3, "codes": {"title_missing"
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 8.8s · {"batch": "B-2", "total": 10, "imported": 8, "quarantined": 2, "rejected": false, "score": 0.8, "max_invalid_ratio": 0.3
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 4.6s · {"batch": "B-3", "total": 6, "imported": 5, "quarantined": 1, "rejected": false, "score": 0.8333, "max_invalid_ratio": 0
|
||||||
|
✔ before: POST /webhook/my-faq-import · HTTP 200 · 1.8s · {"batch": "B-2-fix", "total": 2, "imported": 2, "quarantined": 0, "rejected": false, "score": 1, "max_invalid_ratio": 0.
|
||||||
|
POST http://localhost:5678/webhook/my-quality-report · 4 case(s) × 1
|
||||||
|
✔ 배치 네 개 · 평균 · 가장 나쁜 배치 · 0.0s
|
||||||
|
✔ 가장 많은 이유 · 흐름 · 0.1s
|
||||||
|
✔ 배치마다 점수 순서 · 0.0s
|
||||||
|
✔ 다른 데이터셋으로 거르기 · 0.1s
|
||||||
|
Passed 4/4
|
||||||
|
report: runs/lab03-challenge-20261101-181151.json
|
||||||
|
✔ after: POST http://localhost:8800/faq-reset · HTTP 200 · 0.0s · {"cleared": 15}
|
||||||
|
|
||||||
|
$ uv run tools/flow.py quality
|
||||||
|
FAQ: 6 (nothing added)
|
||||||
|
quarantine: 1
|
||||||
|
B-3 FAQ-305 answer_too_short
|
||||||
|
quality log: 4
|
||||||
|
B-1 faq total 13 valid 3 score 0 rejected True
|
||||||
|
B-2 faq total 10 valid 8 score 0.8 rejected False
|
||||||
|
B-3 faq total 6 valid 5 score 0.8333 rejected False
|
||||||
|
B-2-fix faq total 2 valid 2 score 1 rejected False
|
||||||
|
alerts: 3
|
||||||
|
[high] faq-import · batch_rejected · B-1 rejected
|
||||||
|
[high] faq-import · low_quality · B-2 quality 0.8
|
||||||
|
[high] faq-import · low_quality · B-3 quality 0.8333
|
||||||
|
|
||||||
|
$ uv run tools/flow.py ops --reset
|
||||||
|
✔ live run logs, added FAQ, quarantine, quality log, created tickets, DLQ and alerts emptied, fault switch all ok
|
||||||
|
|
||||||
|
$ uv run tools/flow.py doctor
|
||||||
|
! n8n install folder /Users/mcchae/day65-n8n
|
||||||
|
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
|
||||||
|
✔ n8n container day65-n8n-n8n-1
|
||||||
|
✔ n8n http://localhost:5678
|
||||||
|
✔ data API http://localhost:8800
|
||||||
|
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||||
|
✔ run logs /logs (666 seeded runs) · FAQ additions /faq-extra (day 77 data API)
|
||||||
|
✔ schemas faq · employee · ticket, quarantine, quality log (day 78 data API)
|
||||||
|
✔ n8n → http://data-api:8800
|
||||||
|
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||||
|
✔ node catalog (for check)
|
||||||
|
✔ CLI coding agent: codex, claude, agy
|
||||||
|
ready
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"name": "78 FAQ check + existing data (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Check webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-faq-check",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "6751875f-37e8-51bf-9685-da994f9dec81",
|
||||||
|
"webhookId": "736fe44f-e179-58a3-9856-295b28a7073d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Is a list?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ Array.isArray($json.body.items) }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "56a85def-836a-56b6-981a-3327bee5c75e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject body",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"error\": \"items must be a list\" }",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "4b6d0650-0ddb-5d07-8ced-af0e3ee7c7cc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get FAQ",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/faq-all",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "9bf7a3b3-52ec-5976-803b-54cae1e2244b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check rows",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const body = $('Check webhook').first().json.body;\n\nconst existing = $('Get FAQ').first().json.faqs ?? [];\nconst existingIds = new Set(existing.map((f) => f.id));\nconst existingTitles = new Set(existing.map((f) => String(f.title).trim()));\nconst existingKeywords = new Set(existing.flatMap((f) => (f.keywords ?? []).map((k) => String(k).toLowerCase())));\nconst seen = new Set();\nconst rows = body.items.map((r) => {\n const errors = [], warnings = [];\n const id = String(r?.id ?? '');\n const title = String(r?.title ?? '').trim();\n const answer = String(r?.answer ?? '').trim();\n if (!/^FAQ-\\d{3}$/.test(id)) errors.push('id_format');\n if (!title) errors.push('title_missing'); else if (title.length > 40) errors.push('title_too_long');\n if (!Array.isArray(r?.keywords) || r.keywords.length === 0) errors.push('keywords_missing');\n if (!answer) errors.push('answer_missing'); else if (answer.length < 10) errors.push('answer_too_short'); else if (answer.length > 300) errors.push('answer_too_long');\n if (seen.has(id)) errors.push('duplicate_in_batch');\n seen.add(id);\n if (/01[016789][-. ]?\\d{3,4}[-. ]?\\d{4}/.test(answer) || /[\\w.+-]+@[\\w-]+\\.[\\w.]+/.test(answer)) errors.push('pii');\n if (/https?:\\/\\/(?!intra\\.example\\.com(?![\\w.-]))/i.test(answer)) errors.push('external_link');\n\n if (existingIds.has(id)) errors.push('exists_id');\n if (title && existingTitles.has(title)) errors.push('same_title');\n for (const k of (Array.isArray(r?.keywords) ? r.keywords : [])) if (existingKeywords.has(String(k).toLowerCase())) warnings.push(`keyword_used:${k}`);\n return { id, ok: errors.length === 0, errors, warnings };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nconst out = { batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid,\n with_warnings: rows.filter((r) => r.warnings.length).length, rows, codes };\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "a5d95489-c8a9-5734-ac13-39c87bc61620"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "0331ae2d-ec56-5470-856a-f92507037bab"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Is a list?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get FAQ",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject body",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get FAQ": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check rows",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check rows": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Is a list?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"name": "78 FAQ check + fix list (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Check webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-faq-check",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "7ded9091-d32b-514c-9bba-8cc9010cd8c5",
|
||||||
|
"webhookId": "cf6a2ab6-8e06-511c-b239-ef1a5207977f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Is a list?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ Array.isArray($json.body.items) }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "4485bb43-0e0f-5736-9855-cf88dc66cdef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject body",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"error\": \"items must be a list\" }",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "b2273143-7dc4-52c9-b1f3-36fd45b4a595"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get FAQ",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/faq-all",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "eac0cc92-5d69-5d2a-b999-7a071a4cd7e5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check rows",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const body = $('Check webhook').first().json.body;\n\nconst existing = $('Get FAQ').first().json.faqs ?? [];\nconst existingIds = new Set(existing.map((f) => f.id));\nconst existingTitles = new Set(existing.map((f) => String(f.title).trim()));\nconst existingKeywords = new Set(existing.flatMap((f) => (f.keywords ?? []).map((k) => String(k).toLowerCase())));\nconst seen = new Set();\nconst rows = body.items.map((r) => {\n const errors = [], warnings = [];\n const id = String(r?.id ?? '');\n const title = String(r?.title ?? '').trim();\n const answer = String(r?.answer ?? '').trim();\n if (!/^FAQ-\\d{3}$/.test(id)) errors.push('id_format');\n if (!title) errors.push('title_missing'); else if (title.length > 40) errors.push('title_too_long');\n if (!Array.isArray(r?.keywords) || r.keywords.length === 0) errors.push('keywords_missing');\n if (!answer) errors.push('answer_missing'); else if (answer.length < 10) errors.push('answer_too_short'); else if (answer.length > 300) errors.push('answer_too_long');\n if (seen.has(id)) errors.push('duplicate_in_batch');\n seen.add(id);\n if (/01[016789][-. ]?\\d{3,4}[-. ]?\\d{4}/.test(answer) || /[\\w.+-]+@[\\w-]+\\.[\\w.]+/.test(answer)) errors.push('pii');\n if (/https?:\\/\\/(?!intra\\.example\\.com(?![\\w.-]))/i.test(answer)) errors.push('external_link');\n\n if (existingIds.has(id)) errors.push('exists_id');\n if (title && existingTitles.has(title)) errors.push('same_title');\n for (const k of (Array.isArray(r?.keywords) ? r.keywords : [])) if (existingKeywords.has(String(k).toLowerCase())) warnings.push(`keyword_used:${k}`);\n return { id, ok: errors.length === 0, errors, warnings };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nconst out = { batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid,\n with_warnings: rows.filter((r) => r.warnings.length).length, rows, codes };\n\nconst FIX = {\n id_format: 'id를 FAQ-숫자 3자리로 (예: FAQ-104)',\n title_missing: '제목을 써 주세요',\n title_too_long: '제목을 40자 이내로 줄여 주세요',\n keywords_missing: '키워드를 하나 이상 넣어 주세요',\n answer_missing: '답을 써 주세요',\n answer_too_short: '답을 10자 이상으로 써 주세요',\n answer_too_long: '답을 300자 이내로 줄여 주세요',\n duplicate_in_batch: '같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요',\n pii: '개인 전화번호 · 메일 주소를 빼고 IT 포털 안내로 바꿔 주세요',\n external_link: '바깥 링크 대신 사내 주소(intra.example.com)를 써 주세요',\n exists_id: '이미 있는 id입니다. 새 id를 쓰거나, 고치려는 것이면 FAQ 담당자에게 알려 주세요',\n same_title: '같은 제목의 FAQ가 이미 있습니다. 제목을 바꾸거나 기존 FAQ와 합쳐 주세요',\n};\nout.fix_list = rows.map((r, i) => ({ row: i + 1, id: r.id, codes: r.errors, fix: r.errors.map((e) => FIX[e] ?? e) })).filter((r) => r.codes.length);\nout.message = `${out.batch}: ${out.total}줄 중 ${out.valid}줄 통과, ${out.invalid}줄 고칠 것`;\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "71deee57-d95f-500e-87cc-e352362e221a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "26ac83fd-b10f-56ae-8669-52a14deafc35"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Is a list?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get FAQ",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject body",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get FAQ": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check rows",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check rows": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Is a list?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
{
|
||||||
|
"name": "78 FAQ import + quality log and alerts (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Import webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-faq-import",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "e1896aee-de3a-5afe-89ab-af4a1e59a7fe",
|
||||||
|
"webhookId": "40c49459-ef29-5162-b270-9f02debc6803"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run check",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://localhost:5678/webhook/my-faq-check",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 30000
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify($json.body) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "3b92ccd8-144b-50af-8826-e9b2112763f0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Checked?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ Array.isArray($json.rows) && $json.rows.length > 0 }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "f56c9042-070b-5c0a-b2be-a818784927f9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject body",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ JSON.stringify({ error: $json.error ?? 'check failed' }) }}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
"id": "62c6cdf6-bc48-50e8-8255-af71ee09b8c8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Batch ok?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $json.invalid / $json.total <= ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3) }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "ee39faa6-b293-5cc0-aa10-f1f1afe4a23d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject batch",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ JSON.stringify({ error: 'batch rejected', batch: $('Run check').first().json.batch, invalid_ratio: Math.round($('Run check').first().json.invalid / $('Run check').first().json.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3), codes: $('Run check').first().json.codes }) }}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 422
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "c36ae045-13ee-5c26-8c55-596102c81464"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Log rejected",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ batch: $json.batch, dataset: 'faq', total: $json.total, valid: $json.valid, quarantined: 0, rejected: true, score: 0, codes: $json.codes }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "6a210860-265e-5675-b62c-9aa055215e27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alert rejected",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/alerts",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Batch ok?', severity: 'high', reason: 'batch_rejected', batch: $('Run check').first().json.batch, message: $('Run check').first().json.batch + ' rejected' }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "a27d519d-a8ba-5da8-8b20-91666e65f435"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Split rows",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const c = $('Run check').first().json;\nconst items = $('Import webhook').first().json.body.items;\nreturn c.rows.map((r, i) => ({ json: { ...r, row: items[i], batch: c.batch } }));"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "d21b12e6-d7c3-5693-ab62-1690f04fe10b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Only passed",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return $input.all().filter((i) => i.json.ok);"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "0a85d85b-5527-584e-8956-5a33eeb770a0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Judge",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||||
|
"typeVersion": 3.1,
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "=제목: {{ $json.row.title }}\n답: {{ $json.row.answer }}",
|
||||||
|
"options": {
|
||||||
|
"systemMessage": "You check a company IT FAQ entry before it is published. Does the answer actually answer the title's topic? Ignore style. Return only JSON, no code fence: {\"match\": true, \"reason\": \"short reason in Korean\"}",
|
||||||
|
"maxIterations": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onError": "continueRegularOutput",
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "7c0e4c8e-3e63-539b-9902-ea10d43acca2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Judge model",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"parameters": {
|
||||||
|
"model": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "id",
|
||||||
|
"value": "gpt-4.1-mini"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"openAiApi": {
|
||||||
|
"id": "openAiCourse0001",
|
||||||
|
"name": "OpenAI (.env)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
520
|
||||||
|
],
|
||||||
|
"id": "4d1aac5b-af62-54ea-8cf5-3fa807ed58db"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Decide",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const judged = $('Only passed').all().map((item, i) => {\n let v = { match: false, reason: 'judge unreadable' };\n try { v = JSON.parse(String($('Judge').all()[i].json.output ?? '').replace(/^```(json)?\\s*|```$/g, '')); } catch (e) {}\n return { ...item.json, ok: v.match === true, errors: v.match === true ? [] : ['llm_mismatch'], reason: String(v.reason ?? '') };\n});\nlet k = 0;\nreturn $('Split rows').all().map((i) => ({ json: i.json.ok ? judged[k++] : i.json }));"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1540,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "d775abc0-029e-5213-8d2f-63ed42224453"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Store row",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "={{ $json.ok ? 'http://data-api:8800/faq/add' : 'http://data-api:8800/quarantine' }}",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify($json.ok ? $json.row : { items: [{ batch: $json.batch, row: $json.row, errors: $json.errors }] }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1760,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "8abf3036-e347-514d-8140-8bc277ceb4a3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Summarize",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const c = $('Run check').first().json;\nconst rows = $('Decide').all().map((i) => i.json);\nconst imported = rows.filter((r) => r.ok);\nconst quarantined = rows.filter((r) => !r.ok);\nconst codes = { ...c.codes };\nfor (const r of quarantined) for (const e of r.errors) if (e === 'llm_mismatch') codes[e] = (codes[e] ?? 0) + 1;\nreturn [{ json: { batch: c.batch, total: c.total, imported: imported.length, quarantined: quarantined.length, rejected: false,\n score: Math.round(imported.length / c.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3),\n codes, imported_ids: imported.map((r) => r.id), quarantined_ids: quarantined.map((r) => r.id) } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1980,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "f904284d-6948-5dfc-85db-023fb7e7bbfa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Log quality",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ batch: $('Summarize').first().json.batch, dataset: 'faq', total: $('Summarize').first().json.total, valid: $('Summarize').first().json.imported, quarantined: $('Summarize').first().json.quarantined, rejected: false, score: $('Summarize').first().json.score, codes: $('Summarize').first().json.codes }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2200,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "0fb77808-55c4-54fc-b1ba-9bb98478a8dc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Low score?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $('Summarize').first().json.score < 0.9 }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2420,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "5c9f4cf0-8451-58f4-aa92-6d21adb1ef7c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alert low score",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/alerts",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Low score?', severity: 'high', reason: 'low_quality', batch: $('Summarize').first().json.batch, score: $('Summarize').first().json.score, message: $('Summarize').first().json.batch + ' quality ' + $('Summarize').first().json.score }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2640,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"id": "09f87701-f48a-51c1-bd8c-d15486860992"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Result again",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{ json: $('Summarize').first().json }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2860,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "77f7a5b7-4ad0-50ec-8b9b-5fed5597b549"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
3080,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "1a367765-da4c-5405-8c06-441779da8090"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Import webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Run check",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Run check": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Checked?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Checked?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject body",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Batch ok?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Split rows",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log rejected",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log rejected": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Alert rejected",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Alert rejected": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject batch",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Split rows": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Only passed",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Only passed": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Judge",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Judge": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Decide",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Judge model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Judge",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Decide": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Store row",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Store row": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Summarize",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Summarize": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log quality",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log quality": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Low score?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Low score?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Alert low score",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result again",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Alert low score": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result again",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Result again": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
{
|
||||||
|
"name": "78 FAQ import + LLM calls (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Import webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-faq-import",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "97923cda-6ae1-50d4-a0ad-df64c161c39c",
|
||||||
|
"webhookId": "23a35671-e3aa-5f09-a094-45b3da4c643c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run check",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://localhost:5678/webhook/my-faq-check",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout": 30000
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify($json.body) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "9ebb8863-ee15-58d6-9115-b6ff5a1bbfd0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Checked?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ Array.isArray($json.rows) && $json.rows.length > 0 }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "4a3efc44-a09f-5cf8-8654-8c976fc86008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject body",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ JSON.stringify({ error: $json.error ?? 'check failed' }) }}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
760
|
||||||
|
],
|
||||||
|
"id": "88c8455e-da32-5781-8c2d-40c1262fcd70"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Batch ok?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $json.invalid / $json.total <= ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3) }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "74cca906-ea01-5106-a9b7-e9166ccfa367"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reject batch",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ JSON.stringify({ error: 'batch rejected', batch: $('Run check').first().json.batch, invalid_ratio: Math.round($('Run check').first().json.invalid / $('Run check').first().json.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3), codes: $('Run check').first().json.codes, llm_calls: 0 }) }}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 422
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "50f5afd9-e4fb-5f03-b99c-b0bbbd02f637"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Log rejected",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ batch: $json.batch, dataset: 'faq', total: $json.total, valid: $json.valid, quarantined: 0, rejected: true, score: 0, codes: $json.codes }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "b1d03d7a-d131-59b4-b7c3-63c90c4bd3e2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alert rejected",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/alerts",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Batch ok?', severity: 'high', reason: 'batch_rejected', batch: $('Run check').first().json.batch, message: $('Run check').first().json.batch + ' rejected' }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "0f3fae58-f846-5ff3-99b0-ad519ff51546"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Split rows",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const c = $('Run check').first().json;\nconst items = $('Import webhook').first().json.body.items;\nreturn c.rows.map((r, i) => ({ json: { ...r, row: items[i], batch: c.batch } }));"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "aa264c2a-0b2c-5c68-ab00-48e193d952d8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Only passed",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return $input.all().filter((i) => i.json.ok);"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "57ba3d3a-7a75-5848-90f7-cc4072007dd2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Judge",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||||
|
"typeVersion": 3.1,
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "=제목: {{ $json.row.title }}\n답: {{ $json.row.answer }}",
|
||||||
|
"options": {
|
||||||
|
"systemMessage": "You check a company IT FAQ entry before it is published. Does the answer actually answer the title's topic? Ignore style. Return only JSON, no code fence: {\"match\": true, \"reason\": \"short reason in Korean\"}",
|
||||||
|
"maxIterations": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onError": "continueRegularOutput",
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "8936b8b1-19a9-57fe-aae5-d934a6c1e6c7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Judge model",
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"parameters": {
|
||||||
|
"model": {
|
||||||
|
"__rl": true,
|
||||||
|
"mode": "id",
|
||||||
|
"value": "gpt-4.1-mini"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"temperature": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"credentials": {
|
||||||
|
"openAiApi": {
|
||||||
|
"id": "openAiCourse0001",
|
||||||
|
"name": "OpenAI (.env)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
520
|
||||||
|
],
|
||||||
|
"id": "ada85f33-792e-562b-8802-463ced397599"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Decide",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const judged = $('Only passed').all().map((item, i) => {\n let v = { match: false, reason: 'judge unreadable' };\n try { v = JSON.parse(String($('Judge').all()[i].json.output ?? '').replace(/^```(json)?\\s*|```$/g, '')); } catch (e) {}\n return { ...item.json, ok: v.match === true, errors: v.match === true ? [] : ['llm_mismatch'], reason: String(v.reason ?? '') };\n});\nlet k = 0;\nreturn $('Split rows').all().map((i) => ({ json: i.json.ok ? judged[k++] : i.json }));"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1540,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "43e33f20-a354-548d-98fe-a22a8a295de5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Store row",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "={{ $json.ok ? 'http://data-api:8800/faq/add' : 'http://data-api:8800/quarantine' }}",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify($json.ok ? $json.row : { items: [{ batch: $json.batch, row: $json.row, errors: $json.errors }] }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1760,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "8df856a9-afff-5685-8095-51fceddbd4de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Summarize",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const c = $('Run check').first().json;\nconst rows = $('Decide').all().map((i) => i.json);\nconst imported = rows.filter((r) => r.ok);\nconst quarantined = rows.filter((r) => !r.ok);\nconst codes = { ...c.codes };\nfor (const r of quarantined) for (const e of r.errors) if (e === 'llm_mismatch') codes[e] = (codes[e] ?? 0) + 1;\nreturn [{ json: { batch: c.batch, total: c.total, imported: imported.length, quarantined: quarantined.length, rejected: false,\n score: Math.round(imported.length / c.total * 10000) / 10000, max_invalid_ratio: ($('Import webhook').first().json.body.max_invalid_ratio ?? 0.3),\n codes, imported_ids: imported.map((r) => r.id), quarantined_ids: quarantined.map((r) => r.id), llm_calls: $('Only passed').all().length } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1980,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "c287c65f-99fd-59ef-a44d-dc4b021d7a9c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Log quality",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ batch: $('Summarize').first().json.batch, dataset: 'faq', total: $('Summarize').first().json.total, valid: $('Summarize').first().json.imported, quarantined: $('Summarize').first().json.quarantined, rejected: false, score: $('Summarize').first().json.score, codes: $('Summarize').first().json.codes }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2200,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "9607be48-7b62-5f67-9aec-23db3fb0fbb8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Low score?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $('Summarize').first().json.score < 0.9 }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2420,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "4d32f16f-6554-580b-83eb-2c489df417d3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Alert low score",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/alerts",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ workflow: 'faq-import', node: 'Low score?', severity: 'high', reason: 'low_quality', batch: $('Summarize').first().json.batch, score: $('Summarize').first().json.score, message: $('Summarize').first().json.batch + ' quality ' + $('Summarize').first().json.score }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2640,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"id": "e7dcfdb3-8ed7-5783-93cb-4daf2a75cbb6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Result again",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{ json: $('Summarize').first().json }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
2860,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "90b42f0a-eaa4-55d7-b579-c2e6c67ea701"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
3080,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "700d4355-509a-546a-936e-d8e96c297925"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Import webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Run check",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Run check": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Checked?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Checked?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Batch ok?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject body",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Batch ok?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Split rows",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log rejected",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log rejected": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Alert rejected",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Alert rejected": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Reject batch",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Split rows": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Only passed",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Only passed": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Judge",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Judge": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Decide",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Judge model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Judge",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Decide": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Store row",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Store row": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Summarize",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Summarize": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log quality",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log quality": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Low score?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Low score?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Alert low score",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result again",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Alert low score": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result again",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Result again": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"name": "78 Quality report (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Report webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-quality-report",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "d39880e6-d916-5d87-a2d7-39a1f1201094",
|
||||||
|
"webhookId": "2b2b3919-d93a-579c-ae38-a51266677926"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get log",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "ddba6f7c-251f-5309-87dd-cdaf072aa5c8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Make report",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const want = $('Report webhook').first().json.body?.dataset;\nconst all = $json.items ?? [];\nconst r4 = (v) => Math.round(v * 10000) / 10000;\nconst summary = (items) => {\n const batches = items.map((i) => ({ batch: i.batch, score: i.score, rejected: i.rejected === true, total: i.total }));\n const codes = {};\n for (const i of items) for (const [k, v] of Object.entries(i.codes ?? {})) codes[k] = (codes[k] ?? 0) + v;\n const top_codes = Object.entries(codes).map(([code, count]) => ({ code, count })).sort((a, b) => b.count - a.count || (a.code < b.code ? -1 : a.code > b.code ? 1 : 0));\n const avg = batches.length ? r4(batches.reduce((s, b) => s + b.score, 0) / batches.length) : 0;\n const worst = batches.reduce((w, b) => (w === null || b.score < w.score ? b : w), null);\n let trend = '그대로';\n if (batches.length >= 2) { const d = batches.at(-1).score - batches[0].score; trend = d > 0.05 ? '좋아짐' : d < -0.05 ? '나빠짐' : '그대로'; }\n return { count: batches.length, batches, avg_score: avg, worst_batch: worst?.batch ?? null, rejected_batches: batches.filter((b) => b.rejected).length, top_codes, trend };\n};\nconst out = summary(all.filter((i) => !want || i.dataset === want));\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "e4e4147a-061c-5ef0-8ada-d588ef283c8c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "61076c2a-8679-511f-877a-f16b8e7983e4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Report webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get log",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get log": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Make report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Make report": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
{
|
||||||
|
"name": "78 Schema check + quality log (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Check webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-check",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "71f637f7-7dd3-5109-b1eb-e41f19084672",
|
||||||
|
"webhookId": "2e73b93d-487c-5d47-abf7-0367dc834812"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get schema",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "=http://data-api:8800/schemas/{{ encodeURIComponent($json.body.schema ?? '') }}",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "c2e0870e-6d3c-52e5-9eea-c2d612aa9c02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Known schema?",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"leftValue": "={{ $json.fields !== undefined }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "5cbdeaec-a7b4-5d52-b1dc-a9652df407f7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Unknown schema",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ JSON.stringify({ error: 'unknown schema', schema: $('Check webhook').first().json.body.schema ?? null }) }}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "0f5c53bb-1643-5cab-958d-183709fc78bd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check rows",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const body = $('Check webhook').first().json.body;\nconst schema = $json;\nlet items = body.items;\nif (typeof body.csv === 'string') {\n const [head, ...lines] = body.csv.trim().split(/\\r?\\n/);\n const cols = head.split(',').map((c) => c.trim());\n items = lines.map((line) => Object.fromEntries(line.split(',').map((v, i) => [cols[i], v.trim()])));\n}\nconst isDate = (v) => { if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(v)) return false; const d = new Date(v + 'T00:00:00Z'); return !isNaN(d) && d.toISOString().slice(0, 10) === v; };\nconst seen = new Set();\nconst rows = (Array.isArray(items) ? items : []).map((r, index) => {\n const errors = [];\n for (const [name, f] of Object.entries(schema.fields)) {\n const v = r?.[name];\n const empty = v === undefined || v === null || (typeof v === 'string' && v.trim() === '') || (Array.isArray(v) && v.length === 0);\n if (empty) { if (f.required) errors.push(`missing:${name}`); continue; }\n const s = String(v);\n if (f.type === 'string') { if (f.max && s.length > f.max) errors.push(`too_long:${name}`); if (f.min && s.length < f.min) errors.push(`too_short:${name}`); }\n if (f.type === 'enum' && !f.values.includes(s)) errors.push(`not_allowed:${name}`);\n if (f.type === 'email' && !/^[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+$/.test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'phone' && !/^0\\d{1,2}-\\d{3,4}-\\d{4}$/.test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'date' && !isDate(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'pattern' && !new RegExp(f.pattern).test(s)) errors.push(`bad_format:${name}`);\n if (f.type === 'list') { if (!Array.isArray(v)) errors.push(`bad_format:${name}`); else { if (f.min_items && v.length < f.min_items) errors.push(`too_few:${name}`); if (f.max_items && v.length > f.max_items) errors.push(`too_many:${name}`); } }\n }\n const key = String(r?.[schema.key] ?? '');\n if (key && seen.has(key)) errors.push('duplicate_in_batch');\n if (key) seen.add(key);\n return { row: index + 1, key, ok: errors.length === 0, errors };\n});\nconst codes = {};\nfor (const row of rows) for (const e of row.errors) codes[e] = (codes[e] ?? 0) + 1;\nconst valid = rows.filter((r) => r.ok).length;\nreturn [{ json: { schema: schema.name, batch: body.batch ?? '', total: rows.length, valid, invalid: rows.length - valid, rows, codes } }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "709a8af9-9764-5bc9-9ba6-832437e37d66"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Log quality",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"method": "POST",
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={{ JSON.stringify({ batch: $('Check rows').first().json.batch, dataset: $('Check rows').first().json.schema, total: $('Check rows').first().json.total, valid: $('Check rows').first().json.valid, quarantined: $('Check rows').first().json.invalid, rejected: false, score: $('Check rows').first().json.total ? Math.round($('Check rows').first().json.valid / $('Check rows').first().json.total * 10000) / 10000 : 0, codes: $('Check rows').first().json.codes }) }}"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
880,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "6b7a1222-281c-5a0b-8e5d-ce8340bace06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Result again",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{ json: $('Check rows').first().json }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "44b28b34-d252-5957-8be7-db1828cfe536"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
1320,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "bde5ad2b-cfad-55b6-893b-1110d25c6d0b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Check webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get schema",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get schema": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Known schema?",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Known schema?": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check rows",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Unknown schema",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check rows": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log quality",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Log quality": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Result again",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Result again": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"name": "78 Quality report + by dataset (answer)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Report webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "my-quality-report",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "2a4e58b4-7434-5181-9fca-13bc3ba808cf",
|
||||||
|
"webhookId": "e900dd73-cfae-5c26-9e5b-204c1e0867ec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get log",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"parameters": {
|
||||||
|
"url": "http://data-api:8800/quality-log",
|
||||||
|
"options": {
|
||||||
|
"response": {
|
||||||
|
"response": {
|
||||||
|
"neverError": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "52996b11-04b3-5567-b18f-061a3e1bc213"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Make report",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const want = $('Report webhook').first().json.body?.dataset;\nconst all = $json.items ?? [];\nconst r4 = (v) => Math.round(v * 10000) / 10000;\nconst summary = (items) => {\n const batches = items.map((i) => ({ batch: i.batch, score: i.score, rejected: i.rejected === true, total: i.total }));\n const codes = {};\n for (const i of items) for (const [k, v] of Object.entries(i.codes ?? {})) codes[k] = (codes[k] ?? 0) + v;\n const top_codes = Object.entries(codes).map(([code, count]) => ({ code, count })).sort((a, b) => b.count - a.count || (a.code < b.code ? -1 : a.code > b.code ? 1 : 0));\n const avg = batches.length ? r4(batches.reduce((s, b) => s + b.score, 0) / batches.length) : 0;\n const worst = batches.reduce((w, b) => (w === null || b.score < w.score ? b : w), null);\n let trend = '그대로';\n if (batches.length >= 2) { const d = batches.at(-1).score - batches[0].score; trend = d > 0.05 ? '좋아짐' : d < -0.05 ? '나빠짐' : '그대로'; }\n return { count: batches.length, batches, avg_score: avg, worst_batch: worst?.batch ?? null, rejected_batches: batches.filter((b) => b.rejected).length, top_codes, trend };\n};\nconst out = summary(all.filter((i) => !want || i.dataset === want));\n\nout.by_dataset = {};\nfor (const name of [...new Set(all.map((i) => i.dataset ?? ''))]) {\n const s = summary(all.filter((i) => (i.dataset ?? '') === name));\n out.by_dataset[name] = { count: s.count, avg_score: s.avg_score, worst_batch: s.worst_batch, top_code: s.top_codes[0]?.code ?? null };\n}\nconst names = Object.keys(out.by_dataset);\nout.worst_dataset = names.length ? names.reduce((w, n) => (out.by_dataset[n].avg_score < out.by_dataset[w].avg_score ? n : w)) : null;\nreturn [{ json: out }];"
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
440,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "c4fa7448-4a77-5856-91b6-37ae2d841a7c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send result",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={{ $json }}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"position": [
|
||||||
|
660,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"id": "79bb121c-6ef3-546d-8b1c-9ae6dda0a815"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Report webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get log",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get log": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Make report",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Make report": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send result",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,4 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# tcheck IMP-PLAN
|
||||||
|
|
||||||
|
## 엔진 1단계 · 읽기 · 스키마 · 코드 · n8n과 같은 결과 (R1 · R2 · R3 · R4 · R12 · R13)
|
||||||
|
- [x] load.py: `read_rows(path)` — `.csv`(첫 줄 열 이름) · JSON 목록 · `{"items"}` · 78일차 요청 파일(`{"schema", "batch", "csv" | "items"}`), `read_schema(name, url, file)` — 데이터 API `GET /schemas/<name>` 또는 `--schema-file`
|
||||||
|
- [x] checks.py: `check(rows, schema, batch)` — 스키마의 `fields`를 돌며 타입마다 (필드 이름을 코드에 쓰지 않음), 코드는 AGENTS.md 3절 그대로, 키 중복, `row`는 1부터
|
||||||
|
- [x] cli: `tcheck check FILE [--schema] [--url | $TCHECK_URL]`, 종료 코드 0 · 1(못 읽음) · 2(쓰는 법)
|
||||||
|
- [x] tests: 직원 9줄 중 4줄 · 접수 건 2줄 · FAQ B-3 5줄, n8n `my-check`가 낸 rows · codes와 **같음** (`tests/data/n8n-my-check.json`), 가짜 데이터 API
|
||||||
|
- 확인: `uv run pytest -q`, `uv run tcheck check ../requests/employees-new.json --url http://localhost:8800`
|
||||||
|
|
||||||
|
## 엔진 2단계 · JSON · 내보내기 · 게이트 (R5 · R6 · R7)
|
||||||
|
- [x] `--json`: my-check와 같은 모양
|
||||||
|
- [x] `--out` 깨끗한 줄 · `--rejects` 나쁜 줄 + `errors` 칸 (.csv · .json)
|
||||||
|
- [x] `tcheck gate --max-invalid 0.3`: 넘으면 종료 코드 4, 같으면 통과
|
||||||
|
- [x] tests: 파일 두 개 · 직원 0.5556 → 4 · 0.6 → 0 · 1/10 · 0.1 → 통과
|
||||||
|
- 확인: `uv run tcheck gate ../requests/employees-new.json --url http://localhost:8800; echo $?`
|
||||||
|
|
||||||
|
## TUI 1단계 · 탭 · 거르기 · 내보내기 (R11)
|
||||||
|
- [x] app.py `CheckApp`: 위 요약 한 줄, 탭(문제 줄 · 코드별 · 깨끗한 줄), `[/]` 거르기(키 · 코드), `[Esc]` 풀기, `[e]` 깨끗한 줄 내보내기
|
||||||
|
- [x] 엔진(`checks`)의 결과를 보여 주기만
|
||||||
|
- [x] tests: `run_test`로 5 · 5 · 4줄, `email`로 거르면 1줄, `e` → 파일
|
||||||
|
|
||||||
|
## 설치 (R14)
|
||||||
|
- [x] `[project.scripts] tcheck = "tcheck.cli:main"`, `uv tool install --editable .`, `cd /tmp && tcheck --help`
|
||||||
|
|
||||||
|
## 엔진 3단계 · 내용 규칙 · 고침 목록 · 품질 기록 (R8 · R9 · R10)
|
||||||
|
- [x] `--content answer`: Lab 01의 `pii`(하이픈 없는 휴대전화 · 메일) · `external_link`(호스트가 정확히 intra.example.com)
|
||||||
|
- [x] `--fix`: `fix_list`(줄 순서 · 코드마다 말) · `message`(`TK-1: 9줄 중 2줄 통과, 7줄 고칠 것`)
|
||||||
|
- [x] `--log`: 데이터 API `POST /quality-log`에 my-check(Lab 03 과제 2)와 같은 한 줄
|
||||||
|
- [x] tests: 내용 규칙 다섯 줄, 고침 말, 가짜 API에 기록 한 줄
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# tcheck · 강사가 수업 준비 때 직접 만든 본 (2026-11-01)
|
||||||
|
|
||||||
|
> **정답 자료** · 학생 저장소에는 수업 뒤 [VIBE-A-TCHECK.md](../VIBE-A-TCHECK.md)와 함께 올립니다.
|
||||||
|
> 이 본은 강사가 CLI 에이전트에 단계 요청문을 보내 만든 것이 아니라, Claude Code가 VIBE-Q의 요구 R1~R14와 [Doc/IMP-PLAN.md](Doc/IMP-PLAN.md)를 보고 직접 썼습니다. 학생이 만든 것과 파일 구성이 달라도 됩니다. 요구의 **확인 방법**이 같으면 됩니다.
|
||||||
|
|
||||||
|
## 설치 · 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/aio-01d4-stu/Last3Mon/78/tcheck
|
||||||
|
uv sync && uv run pytest -q # 테스트 22개 (n8n · 데이터 API 없이, 가짜 서버)
|
||||||
|
uv tool install --editable . # 어느 폴더에서나 tcheck (R14)
|
||||||
|
cd /tmp && tcheck --help
|
||||||
|
```
|
||||||
|
|
||||||
|
| 하고 싶은 것 | 명령 |
|
||||||
|
| --- | --- |
|
||||||
|
| 검사 (n8n `my-check`와 같은 판정) | `tcheck check ../requests/employees-new.json --url http://localhost:8800` (`--json`) |
|
||||||
|
| 스키마를 파일에서 | `tcheck check employees.csv --schema-file ../data-api/data/schemas/employee.json` |
|
||||||
|
| 깨끗한 줄 · 나쁜 줄 파일 | `tcheck check … --out clean.csv --rejects rejects.csv` |
|
||||||
|
| 배치 게이트 (넘으면 종료 코드 4) | `tcheck gate … --max-invalid 0.3` |
|
||||||
|
| FAQ 내용 규칙 (개인정보 · 바깥 링크) | `tcheck check ../requests/faq-batch-2.json --schema faq --content answer` |
|
||||||
|
| 보낸 사람에게 고침 목록 | `tcheck check … --fix` |
|
||||||
|
| 품질 기록 한 줄 | `tcheck check … --log` (데이터 API `/quality-log`) |
|
||||||
|
| 화면(TUI) | `tcheck ui …` (`[/]` 거르기, `[Esc]` 풀기, `[e]` 깨끗한 줄 내보내기, `[q]` 끝) |
|
||||||
|
|
||||||
|
- 종료 코드: 0 됨 · 1 파일이나 스키마를 못 읽음 · 2 쓰는 법이 틀림 · 4 게이트가 배치를 거절.
|
||||||
|
- 데이터 API 주소는 `--url` 또는 `$TCHECK_URL`. `--schema-file`을 주면 파일이 먼저입니다. 스키마 이름은 `--schema` 또는 요청 파일의 `"schema"`.
|
||||||
|
- 판정 규칙은 [AGENTS.md](../AGENTS.md) 3절 "스키마" 표와 같습니다 (공백만 있는 값은 빈 값, 선택 칸은 비면 통과, 실제 있는 날짜, 키 중복, `row`는 1부터, 소수 4자리 half up).
|
||||||
|
|
||||||
|
## 파일
|
||||||
|
|
||||||
|
| 파일 | 하는 일 |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/tcheck/load.py` | 읽는 층 (R1 · R2 · R10): CSV · JSON · 요청 파일, 스키마(API · 파일), 품질 기록 보내기 |
|
||||||
|
| `src/tcheck/checks.py` | 규칙 한 곳 (R3 · R7 · R8 · R9 · R13): 타입마다 판정 · 내용 규칙 · 게이트 · 고침 목록 · 기록 한 줄 · 나누기 |
|
||||||
|
| `src/tcheck/cli.py` | `check` · `gate` · `ui`, 종료 코드 (R5 · R6 · R12) |
|
||||||
|
| `src/tcheck/app.py` | Textual 화면 (R11) |
|
||||||
|
| `tests/` | 직원 CSV · 접수 건 · FAQ B-3 · 스키마 셋 사본, n8n `my-check`가 낸 결과(`n8n-my-check.json`), 가짜 데이터 API (R12) |
|
||||||
|
|
||||||
|
## 수업 준비 때 확인한 것 (11/01)
|
||||||
|
|
||||||
|
- 같은 세 배치(직원 HR-1 · 접수 건 TK-1 · FAQ B-3)의 `rows` · `codes` · `valid`가 n8n `my-check`의 응답(`answers/18` · `answers/21`)과 **모두 같음** (`test_same_rows_and_codes_as_n8n`).
|
||||||
|
- FAQ B-1에 `--schema faq --content answer`: 13줄 중 8줄 나쁨 (0.6154) — Lab 01 예제 2의 내 검사와 같은 8줄 (코드는 스키마 꼴 `missing:title` · `bad_format:id` …).
|
||||||
|
- 반올림은 JavaScript `Math.round`처럼 half up (`js_round`). 77일차 `tlog`와 같은 까닭.
|
||||||
|
- 처음 판에서 TUI의 거르기 상자를 연 `/` 글자가 상자에 들어가고, 상자를 닫은 뒤에도 포커스가 남아 `e`(내보내기)가 글자로 들어갔음 → 글자 앞의 `/`를 지우고, 닫을 때 표로 포커스를 옮김.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[project]
|
||||||
|
name = "tcheck"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Check CSV · JSON rows against a data schema (the day 78 data API), with the same codes and results as n8n my-check, a batch gate, and a TUI"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = ["textual>=0.80"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
tcheck = "tcheck.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/tcheck"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""tcheck · day 78 data quality checks in the terminal."""
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Textual screen (R11): the summary on top, three tabs (problem rows · by code · clean rows), [/] filter, [e] export.
|
||||||
|
The screen only shows what the engine (checks) already decided."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import DataTable, Footer, Header, Input, Static, TabbedContent, TabPane
|
||||||
|
|
||||||
|
from . import checks
|
||||||
|
|
||||||
|
|
||||||
|
class CheckApp(App):
|
||||||
|
TITLE = "tcheck"
|
||||||
|
BINDINGS = [("slash", "filter", "Filter"), ("e", "export", "Export clean rows"), ("escape", "clear", "Clear filter"),
|
||||||
|
("q", "quit", "Quit")]
|
||||||
|
CSS = "#top { height: auto; margin: 0 1; }"
|
||||||
|
|
||||||
|
def __init__(self, args, rows: list, schema: dict, result: dict):
|
||||||
|
super().__init__()
|
||||||
|
self.args, self.rows, self.schema, self.result = args, rows, schema, result
|
||||||
|
self.word = ""
|
||||||
|
self.exported = ""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
yield Static("", id="top", markup=False)
|
||||||
|
yield Input(placeholder="filter: key or code, Enter to apply, Esc to clear", id="filter")
|
||||||
|
with TabbedContent(id="tabs"):
|
||||||
|
with TabPane("Problem rows", id="problems"):
|
||||||
|
yield DataTable(id="problem-table", cursor_type="row")
|
||||||
|
with TabPane("By code", id="codes"):
|
||||||
|
yield DataTable(id="code-table", cursor_type="row")
|
||||||
|
with TabPane("Clean rows", id="clean"):
|
||||||
|
yield DataTable(id="clean-table", cursor_type="row")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self.query_one("#filter", Input).display = False
|
||||||
|
r = self.result
|
||||||
|
self.sub_title = f"{self.args.file} · schema {r['schema']}"
|
||||||
|
self.query_one("#top", Static).update(
|
||||||
|
f"{r['batch']} · {r['total']} rows · valid {r['valid']} · invalid {r['invalid']} · score {checks.score(r)}")
|
||||||
|
self.fill()
|
||||||
|
|
||||||
|
def keep(self, row: dict) -> bool:
|
||||||
|
return not self.word or self.word in row["key"] or any(self.word in e for e in row["errors"])
|
||||||
|
|
||||||
|
def fill(self):
|
||||||
|
problems = self.query_one("#problem-table", DataTable)
|
||||||
|
problems.clear(columns=True)
|
||||||
|
problems.add_columns("row", "key", "errors")
|
||||||
|
for row in self.result["rows"]:
|
||||||
|
if not row["ok"] and self.keep(row):
|
||||||
|
problems.add_row(str(row["row"]), row["key"] or "-", ", ".join(row["errors"]))
|
||||||
|
codes = self.query_one("#code-table", DataTable)
|
||||||
|
codes.clear(columns=True)
|
||||||
|
codes.add_columns("count", "code", "rows")
|
||||||
|
for code, count in sorted(self.result["codes"].items(), key=lambda kv: (-kv[1], kv[0])):
|
||||||
|
if not self.word or self.word in code:
|
||||||
|
where = [str(r["row"]) for r in self.result["rows"] if code in r["errors"]]
|
||||||
|
codes.add_row(str(count), code, " ".join(where))
|
||||||
|
clean = self.query_one("#clean-table", DataTable)
|
||||||
|
clean.clear(columns=True)
|
||||||
|
clean.add_columns("row", "key")
|
||||||
|
for row in self.result["rows"]:
|
||||||
|
if row["ok"] and self.keep(row):
|
||||||
|
clean.add_row(str(row["row"]), row["key"] or "-")
|
||||||
|
|
||||||
|
def action_filter(self):
|
||||||
|
box = self.query_one("#filter", Input)
|
||||||
|
box.value, box.display = "", True
|
||||||
|
self.call_after_refresh(box.focus)
|
||||||
|
|
||||||
|
def on_input_submitted(self, event: Input.Submitted):
|
||||||
|
self.word = event.value.strip().lstrip("/").strip() # the "/" that opened the box may land in it
|
||||||
|
event.input.display = False
|
||||||
|
self.fill()
|
||||||
|
self.query_one("#problem-table", DataTable).focus()
|
||||||
|
|
||||||
|
def action_clear(self):
|
||||||
|
self.word = ""
|
||||||
|
box = self.query_one("#filter", Input)
|
||||||
|
box.value, box.display = "", False
|
||||||
|
self.fill()
|
||||||
|
self.query_one("#problem-table", DataTable).focus()
|
||||||
|
|
||||||
|
def action_export(self):
|
||||||
|
from .cli import write_rows
|
||||||
|
|
||||||
|
source = Path(self.args.file)
|
||||||
|
target = self.args.out or str(source.with_suffix(".clean" + (".csv" if source.suffix.lower() == ".csv" else ".json")))
|
||||||
|
clean, _ = checks.split(self.rows, self.result)
|
||||||
|
write_rows(target, clean)
|
||||||
|
self.exported = target
|
||||||
|
self.notify(f"{len(clean)} clean rows → {target}")
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""The rules, in one place (R13): schema types (R2 · R3), content rules (R8), the gate (R7), the fix list (R9) and
|
||||||
|
the quality log line (R10). Same codes and the same results as the day 78 n8n my-check (AGENTS.md 3)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
|
EMAIL = re.compile(r"^[\w.+-]+@[\w-]+(\.[\w-]+)+$", re.ASCII)
|
||||||
|
PHONE = re.compile(r"^0\d{1,2}-\d{3,4}-\d{4}$", re.ASCII)
|
||||||
|
DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$", re.ASCII)
|
||||||
|
PII = (re.compile(r"01[016789][-. ]?\d{3,4}[-. ]?\d{4}", re.ASCII), re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+", re.ASCII))
|
||||||
|
EXTERNAL = re.compile(r"https?://(?!intra\.example\.com(?![\w.-]))", re.ASCII | re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def js_round(value: float, digits: int = 4) -> float:
|
||||||
|
"""Half up like JavaScript Math.round (n8n Code nodes), not Python's round-half-even."""
|
||||||
|
scale = 10 ** digits
|
||||||
|
out = math.floor(value * scale + 0.5) / scale
|
||||||
|
return int(out) if out == int(out) else out
|
||||||
|
|
||||||
|
|
||||||
|
def js_str(value) -> str:
|
||||||
|
if value is True or value is False:
|
||||||
|
return "true" if value else "false"
|
||||||
|
if isinstance(value, float) and value == int(value):
|
||||||
|
return str(int(value))
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(value) -> bool:
|
||||||
|
return value is None or (isinstance(value, str) and value.strip() == "") or (isinstance(value, list) and not value)
|
||||||
|
|
||||||
|
|
||||||
|
def is_date(text: str) -> bool:
|
||||||
|
if not DATE.match(text):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return datetime.date.fromisoformat(text).isoformat() == text
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def field_errors(name: str, rule: dict, value) -> list[str]:
|
||||||
|
"""One field against one schema rule. The field name never appears in this code (R2)."""
|
||||||
|
if is_empty(value):
|
||||||
|
return [f"missing:{name}"] if rule.get("required") else []
|
||||||
|
kind, text, out = rule.get("type"), js_str(value), []
|
||||||
|
if kind == "string":
|
||||||
|
if rule.get("max") and len(text) > rule["max"]:
|
||||||
|
out.append(f"too_long:{name}")
|
||||||
|
if rule.get("min") and len(text) < rule["min"]:
|
||||||
|
out.append(f"too_short:{name}")
|
||||||
|
elif kind == "enum" and text not in [js_str(v) for v in rule.get("values", [])]:
|
||||||
|
out.append(f"not_allowed:{name}")
|
||||||
|
elif kind == "email" and not EMAIL.match(text):
|
||||||
|
out.append(f"bad_format:{name}")
|
||||||
|
elif kind == "phone" and not PHONE.match(text):
|
||||||
|
out.append(f"bad_format:{name}")
|
||||||
|
elif kind == "date" and not is_date(text):
|
||||||
|
out.append(f"bad_format:{name}")
|
||||||
|
elif kind == "pattern" and not re.search(rule.get("pattern", ""), text, re.ASCII):
|
||||||
|
out.append(f"bad_format:{name}")
|
||||||
|
elif kind == "list":
|
||||||
|
if not isinstance(value, list):
|
||||||
|
out.append(f"bad_format:{name}")
|
||||||
|
else:
|
||||||
|
if rule.get("min_items") and len(value) < rule["min_items"]:
|
||||||
|
out.append(f"too_few:{name}")
|
||||||
|
if rule.get("max_items") and len(value) > rule["max_items"]:
|
||||||
|
out.append(f"too_many:{name}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def content_errors(value) -> list[str]:
|
||||||
|
"""R8: Lab 01 content rules on a text field (personal phone · e-mail, links outside intra.example.com)."""
|
||||||
|
text = "" if value is None else js_str(value).strip()
|
||||||
|
out = []
|
||||||
|
if any(p.search(text) for p in PII):
|
||||||
|
out.append("pii")
|
||||||
|
if EXTERNAL.search(text):
|
||||||
|
out.append("external_link")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def check(rows: list, schema: dict, batch: str = "", content: tuple[str, ...] = ()) -> dict:
|
||||||
|
"""{"schema", "batch", "total", "valid", "invalid", "rows": [{"row", "key", "ok", "errors"}], "codes"} like my-check."""
|
||||||
|
seen, out = set(), []
|
||||||
|
key_name = schema.get("key", "")
|
||||||
|
for index, row in enumerate(rows, 1):
|
||||||
|
row = row if isinstance(row, dict) else {}
|
||||||
|
errors: list[str] = []
|
||||||
|
for name, rule in schema["fields"].items():
|
||||||
|
errors += field_errors(name, rule, row.get(name))
|
||||||
|
for name in content:
|
||||||
|
errors += content_errors(row.get(name))
|
||||||
|
key = "" if row.get(key_name) is None else js_str(row.get(key_name))
|
||||||
|
if key and key in seen:
|
||||||
|
errors.append("duplicate_in_batch")
|
||||||
|
if key:
|
||||||
|
seen.add(key)
|
||||||
|
out.append({"row": index, "key": key, "ok": not errors, "errors": errors})
|
||||||
|
codes: dict[str, int] = {}
|
||||||
|
for r in out:
|
||||||
|
for e in r["errors"]:
|
||||||
|
codes[e] = codes.get(e, 0) + 1
|
||||||
|
valid = sum(r["ok"] for r in out)
|
||||||
|
return {"schema": schema.get("name", ""), "batch": batch, "total": len(out), "valid": valid,
|
||||||
|
"invalid": len(out) - valid, "rows": out, "codes": codes}
|
||||||
|
|
||||||
|
|
||||||
|
def score(result: dict) -> float:
|
||||||
|
return js_round(result["valid"] / result["total"]) if result["total"] else 0
|
||||||
|
|
||||||
|
|
||||||
|
def gate(result: dict, max_invalid: float = 0.3) -> dict:
|
||||||
|
"""R7: reject when invalid / total is above the limit (equal passes), like the Lab 02 batch gate."""
|
||||||
|
ratio = js_round(result["invalid"] / result["total"]) if result["total"] else 0
|
||||||
|
raw = result["invalid"] / result["total"] if result["total"] else 0
|
||||||
|
return {"accepted": raw <= max_invalid, "invalid_ratio": ratio, "max_invalid_ratio": max_invalid}
|
||||||
|
|
||||||
|
|
||||||
|
FIX = {
|
||||||
|
"missing": "{f} 칸을 채워 주세요",
|
||||||
|
"too_long": "{f}을(를) 더 짧게 ({max}자 이내)",
|
||||||
|
"too_short": "{f}을(를) 더 길게 ({min}자 이상)",
|
||||||
|
"not_allowed": "{f}은(는) {values} 중 하나로",
|
||||||
|
"bad_format": "{f}의 형식을 확인해 주세요",
|
||||||
|
"too_few": "{f}에 {min_items}개 이상",
|
||||||
|
"too_many": "{f}에 {max_items}개 이하",
|
||||||
|
"duplicate_in_batch": "같은 배치에 같은 {key}가 또 있습니다. 하나만 남겨 주세요",
|
||||||
|
"pii": "개인 전화번호 · 메일 주소를 빼고 IT 포털 안내로 바꿔 주세요",
|
||||||
|
"external_link": "바깥 링크 대신 사내 주소(intra.example.com)를 써 주세요",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fix_text(code: str, schema: dict) -> str:
|
||||||
|
kind, _, name = code.partition(":")
|
||||||
|
rule = schema["fields"].get(name, {})
|
||||||
|
values = " · ".join(map(str, rule.get("values", [])))
|
||||||
|
return FIX.get(kind, code).format(f=name, key=schema.get("key", "key"), values=values,
|
||||||
|
max=rule.get("max"), min=rule.get("min"),
|
||||||
|
min_items=rule.get("min_items"), max_items=rule.get("max_items"))
|
||||||
|
|
||||||
|
|
||||||
|
def fix_list(result: dict, schema: dict) -> dict:
|
||||||
|
"""R9: what the sender should fix, in row order, plus a one-line message (Lab 01 task 2)."""
|
||||||
|
items = [{"row": r["row"], "key": r["key"], "codes": r["errors"], "fix": [fix_text(c, schema) for c in r["errors"]]}
|
||||||
|
for r in result["rows"] if r["errors"]]
|
||||||
|
message = f"{result['batch']}: {result['total']}줄 중 {result['valid']}줄 통과, {result['invalid']}줄 고칠 것"
|
||||||
|
return {"fix_list": items, "message": message}
|
||||||
|
|
||||||
|
|
||||||
|
def quality_entry(result: dict) -> dict:
|
||||||
|
"""R10: the line my-check writes to /quality-log (Lab 03 task 2)."""
|
||||||
|
return {"batch": result["batch"], "dataset": result["schema"], "total": result["total"], "valid": result["valid"],
|
||||||
|
"quarantined": result["invalid"], "rejected": False, "score": score(result), "codes": result["codes"]}
|
||||||
|
|
||||||
|
|
||||||
|
def split(rows: list, result: dict) -> tuple[list[dict], list[dict]]:
|
||||||
|
"""R6: clean rows as they came, rejected rows with an "errors" column."""
|
||||||
|
clean = [rows[r["row"] - 1] for r in result["rows"] if r["ok"]]
|
||||||
|
rejects = [{**(rows[r["row"] - 1] if isinstance(rows[r["row"] - 1], dict) else {}), "errors": " ".join(r["errors"])}
|
||||||
|
for r in result["rows"] if not r["ok"]]
|
||||||
|
return clean, rejects
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""tcheck commands: check · gate · ui. Exit codes (R12): 0 ok · 1 cannot read · 2 usage · 4 batch rejected by the gate."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from . import checks, load
|
||||||
|
|
||||||
|
EXIT_OK, EXIT_READ, EXIT_USAGE, EXIT_REJECTED = 0, 1, 2, 4
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(argparse.ArgumentParser):
|
||||||
|
def error(self, message): # usage mistakes are exit code 2, with the message on stderr
|
||||||
|
self.print_usage(sys.stderr)
|
||||||
|
print(f"tcheck: {message}", file=sys.stderr)
|
||||||
|
sys.exit(EXIT_USAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def common(p: argparse.ArgumentParser) -> None:
|
||||||
|
p.add_argument("file", help="rows: .csv, a JSON list, {\"items\": [...]} or a day 78 request {\"schema\", \"batch\", \"csv\" | \"items\"}")
|
||||||
|
p.add_argument("--schema", default="", help="schema name in the data API (faq · employee · ticket); default: the file's \"schema\"")
|
||||||
|
p.add_argument("--schema-file", help="read the schema from a JSON file instead of the data API")
|
||||||
|
p.add_argument("--url", default=os.environ.get("TCHECK_URL"), help="data API base, e.g. http://localhost:8800 ($TCHECK_URL)")
|
||||||
|
p.add_argument("--batch", default="", help="batch name (default: the file's \"batch\" or its file name)")
|
||||||
|
p.add_argument("--content", action="append", default=[], metavar="FIELD",
|
||||||
|
help="also apply the content rules (pii · external_link) to this text field (R8), e.g. --content answer")
|
||||||
|
p.add_argument("--json", action="store_true", help="print JSON in the shape of n8n my-check (R5)")
|
||||||
|
|
||||||
|
|
||||||
|
def load_all(args) -> tuple[list, dict, dict]:
|
||||||
|
rows, meta = load.read_rows(args.file)
|
||||||
|
name = args.schema or meta.get("schema", "")
|
||||||
|
if not args.schema_file and not name:
|
||||||
|
raise SystemExit(usage("which schema? give --schema NAME (or a request file with \"schema\") or --schema-file"))
|
||||||
|
schema = load.read_schema(name, args.url, args.schema_file)
|
||||||
|
result = checks.check(rows, schema, args.batch or meta.get("batch", ""), tuple(args.content))
|
||||||
|
return rows, schema, result
|
||||||
|
|
||||||
|
|
||||||
|
def usage(message: str) -> int:
|
||||||
|
print(f"tcheck: {message}", file=sys.stderr)
|
||||||
|
return EXIT_USAGE
|
||||||
|
|
||||||
|
|
||||||
|
def write_rows(path: str, rows: list[dict]) -> None:
|
||||||
|
"""R6: .csv keeps columns in first-seen order, anything else is a JSON list."""
|
||||||
|
target = Path(path)
|
||||||
|
if target.suffix.lower() == ".csv":
|
||||||
|
cols: list[str] = []
|
||||||
|
for row in rows:
|
||||||
|
cols += [c for c in row if c not in cols]
|
||||||
|
with target.open("w", encoding="utf-8", newline="") as out:
|
||||||
|
writer = csv.DictWriter(out, fieldnames=cols)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows([{c: (json.dumps(v, ensure_ascii=False) if isinstance(v, (list, dict)) else v)
|
||||||
|
for c, v in row.items()} for row in rows])
|
||||||
|
else:
|
||||||
|
target.write_text(json.dumps(rows, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def print_result(r: dict, fixes: dict | None) -> None:
|
||||||
|
print(f"{r['schema']} · {r['batch']} · {r['total']} rows · valid {r['valid']} · invalid {r['invalid']} · score {checks.score(r)}")
|
||||||
|
for row in r["rows"]:
|
||||||
|
if not row["ok"]:
|
||||||
|
print(f" {row['row']:>3} {row['key'] or '-':<26} {', '.join(row['errors'])}")
|
||||||
|
if r["codes"]:
|
||||||
|
print("codes: " + " · ".join(f"{k} {v}" for k, v in sorted(r["codes"].items(), key=lambda kv: (-kv[1], kv[0]))))
|
||||||
|
if fixes:
|
||||||
|
print(fixes["message"])
|
||||||
|
for item in fixes["fix_list"]:
|
||||||
|
print(f" {item['row']:>3} " + " / ".join(item["fix"]))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = Parser(prog="tcheck", description="Check CSV · JSON rows against a day 78 data schema, like n8n my-check.")
|
||||||
|
commands = parser.add_subparsers(dest="command", required=True)
|
||||||
|
p = commands.add_parser("check", help="rows · codes · valid (R1 · R2 · R3 · R5), clean / rejected files (R6)")
|
||||||
|
common(p)
|
||||||
|
p.add_argument("--out", help="write the clean rows here (.csv or .json)")
|
||||||
|
p.add_argument("--rejects", help="write the rejected rows with an 'errors' column here")
|
||||||
|
p.add_argument("--fix", action="store_true", help="add the fix list and the one-line message (R9)")
|
||||||
|
p.add_argument("--log", action="store_true", help="write one line to the data API quality log (R10)")
|
||||||
|
p = commands.add_parser("gate", help="the batch gate: exit code 4 when invalid / total is above --max-invalid (R7)")
|
||||||
|
common(p)
|
||||||
|
p.add_argument("--max-invalid", type=float, default=0.3, help="limit for invalid / total, equal passes (default 0.3)")
|
||||||
|
p = commands.add_parser("ui", help="the TUI: problem rows · by code · clean rows (R11)")
|
||||||
|
common(p)
|
||||||
|
p.add_argument("--out", help="where [e] writes the clean rows (default: <file>.clean.csv or .json)")
|
||||||
|
try:
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
except SystemExit as stop: # --help is 0, usage mistakes are 2
|
||||||
|
return int(stop.code or 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rows, schema, r = load_all(args)
|
||||||
|
if args.command == "ui":
|
||||||
|
from .app import CheckApp
|
||||||
|
CheckApp(args, rows, schema, r).run()
|
||||||
|
return EXIT_OK
|
||||||
|
if args.command == "gate":
|
||||||
|
g = checks.gate(r, args.max_invalid)
|
||||||
|
out = {**{k: r[k] for k in ("schema", "batch", "total", "valid", "invalid", "codes")}, **g}
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
mark = "✔ accepted" if g["accepted"] else "✘ rejected"
|
||||||
|
print(f"{mark} · {r['batch']} · invalid {r['invalid']}/{r['total']} = {g['invalid_ratio']} (limit {args.max_invalid})")
|
||||||
|
return EXIT_OK if g["accepted"] else EXIT_REJECTED
|
||||||
|
fixes = checks.fix_list(r, schema) if args.fix else None
|
||||||
|
if args.out or args.rejects:
|
||||||
|
clean, rejects = checks.split(rows, r)
|
||||||
|
if args.out:
|
||||||
|
write_rows(args.out, clean)
|
||||||
|
if args.rejects:
|
||||||
|
write_rows(args.rejects, rejects)
|
||||||
|
if args.log:
|
||||||
|
if not args.url:
|
||||||
|
return usage("--log needs --url (or $TCHECK_URL)")
|
||||||
|
load.post_log(args.url, checks.quality_entry(r))
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({**r, **(fixes or {})}, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print_result(r, fixes)
|
||||||
|
if args.out:
|
||||||
|
print(f"clean rows → {args.out}")
|
||||||
|
if args.rejects:
|
||||||
|
print(f"rejected rows → {args.rejects}")
|
||||||
|
if args.log:
|
||||||
|
print(f"quality log ← {r['batch']} · {r['schema']} · score {checks.score(r)}")
|
||||||
|
return EXIT_OK
|
||||||
|
except load.LoadError as error:
|
||||||
|
print(f"tcheck: cannot read: {error}", file=sys.stderr)
|
||||||
|
return EXIT_READ
|
||||||
|
except SystemExit as stop:
|
||||||
|
return int(stop.code or 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""Read rows (R1) from CSV · JSON files, and the schema (R2) from the data API or a file."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class LoadError(Exception):
|
||||||
|
"""The rows or the schema could not be read (exit code 1)."""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_csv(text: str) -> list[dict]:
|
||||||
|
"""First line = column names, comma separated (AGENTS.md 3: no quotes needed). Values are trimmed."""
|
||||||
|
reader = csv.reader(io.StringIO(text.strip()))
|
||||||
|
lines = [line for line in reader if line]
|
||||||
|
if not lines:
|
||||||
|
return []
|
||||||
|
cols = [c.strip() for c in lines[0]]
|
||||||
|
return [{cols[i]: v.strip() for i, v in enumerate(line) if i < len(cols)} for line in lines[1:]]
|
||||||
|
|
||||||
|
|
||||||
|
def read_rows(path: str) -> tuple[list[dict], dict]:
|
||||||
|
"""A .csv file, a JSON list, {"items": [...]}, or a day 78 request {"schema", "batch", "csv" | "items"}.
|
||||||
|
Returns (rows, meta) where meta may hold "schema" and "batch"."""
|
||||||
|
file = Path(path)
|
||||||
|
if not file.exists():
|
||||||
|
raise LoadError(f"no such file: {path}")
|
||||||
|
text = file.read_text(encoding="utf-8")
|
||||||
|
if file.suffix.lower() == ".csv":
|
||||||
|
return parse_csv(text), {"batch": file.stem}
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except json.JSONDecodeError as error:
|
||||||
|
raise LoadError(f"not CSV or JSON: {path} ({error.msg})") from error
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data, {"batch": file.stem}
|
||||||
|
if isinstance(data, dict):
|
||||||
|
meta = {k: data[k] for k in ("schema", "batch") if k in data}
|
||||||
|
meta.setdefault("batch", file.stem)
|
||||||
|
if isinstance(data.get("csv"), str):
|
||||||
|
return parse_csv(data["csv"]), meta
|
||||||
|
if isinstance(data.get("items"), list):
|
||||||
|
return data["items"], meta
|
||||||
|
raise LoadError(f"expected a list of rows, {{\"items\": [...]}} or {{\"csv\": \"...\"}}: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def http_json(url: str, body: dict | None = None, timeout: float = 10) -> object:
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
request = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout) as answer:
|
||||||
|
return json.loads(answer.read())
|
||||||
|
|
||||||
|
|
||||||
|
def read_schema(name: str = "", url: str | None = None, file: str | None = None) -> dict:
|
||||||
|
"""--schema-file wins over the data API ($TCHECK_URL may be set for every call)."""
|
||||||
|
if file:
|
||||||
|
try:
|
||||||
|
schema = json.loads(Path(file).read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as error:
|
||||||
|
raise LoadError(f"cannot read schema file {file}: {error}") from error
|
||||||
|
elif url and name:
|
||||||
|
try:
|
||||||
|
schema = http_json(url.rstrip("/") + "/schemas/" + urllib.parse.quote(name))
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
raise LoadError(f"unknown schema '{name}' (data API {error.code})") from error
|
||||||
|
except (urllib.error.URLError, OSError, json.JSONDecodeError) as error:
|
||||||
|
raise LoadError(f"data API {url}: {error}") from error
|
||||||
|
else:
|
||||||
|
raise LoadError("no schema: give --schema NAME with --url (or $TCHECK_URL), or --schema-file")
|
||||||
|
if not isinstance(schema, dict) or not isinstance(schema.get("fields"), dict):
|
||||||
|
raise LoadError("the schema has no 'fields'")
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def post_log(url: str, entry: dict) -> dict:
|
||||||
|
"""R10: one line in the data API quality log (POST /quality-log)."""
|
||||||
|
try:
|
||||||
|
return http_json(url.rstrip("/") + "/quality-log", entry)
|
||||||
|
except (urllib.error.URLError, OSError, json.JSONDecodeError) as error:
|
||||||
|
raise LoadError(f"data API {url}: {error}") from error
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
DATA = Path(__file__).parent / "data"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def n8n():
|
||||||
|
"""What n8n my-check answered on 2026-11-01 for the same three batches (Last3Mon/78/answers/18 · 21)."""
|
||||||
|
return json.loads((DATA / "n8n-my-check.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def api():
|
||||||
|
"""A local stand-in for the day 78 data API: GET /schemas/<name> from tests/data/schemas, POST /quality-log kept in memory."""
|
||||||
|
log: list[dict] = []
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def send(self, status, body):
|
||||||
|
data = json.dumps(body, ensure_ascii=False).encode()
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
path = DATA / "schemas" / (self.path.removeprefix("/schemas/") + ".json")
|
||||||
|
if self.path.startswith("/schemas/") and path.exists():
|
||||||
|
return self.send(200, json.loads(path.read_text(encoding="utf-8")))
|
||||||
|
return self.send(404, {"error": "schema not found"})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}")
|
||||||
|
if self.path == "/quality-log":
|
||||||
|
log.append(body)
|
||||||
|
return self.send(200, {"stored": 1, "count": len(log)})
|
||||||
|
return self.send(404, {})
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||||
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||||
|
server.log = log
|
||||||
|
yield server
|
||||||
|
server.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def url(api):
|
||||||
|
return f"http://127.0.0.1:{api.server_address[1]}"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name,dept,email,phone,joined
|
||||||
|
오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01
|
||||||
|
윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05
|
||||||
|
강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06
|
||||||
|
서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07
|
||||||
|
임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01
|
||||||
|
,IT팀,noname@example.com,,2026-10-09
|
||||||
|
배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12
|
||||||
|
배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12
|
||||||
|
정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14
|
||||||
|
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"batch": "B-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-101",
|
||||||
|
"title": "사내 와이파이 연결",
|
||||||
|
"keywords": [
|
||||||
|
"와이파이",
|
||||||
|
"wifi",
|
||||||
|
"무선"
|
||||||
|
],
|
||||||
|
"answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약",
|
||||||
|
"keywords": [
|
||||||
|
"회의실",
|
||||||
|
"예약"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-103",
|
||||||
|
"title": "",
|
||||||
|
"keywords": [
|
||||||
|
"출입증"
|
||||||
|
],
|
||||||
|
"answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-10",
|
||||||
|
"title": "노트북 반납",
|
||||||
|
"keywords": [
|
||||||
|
"노트북",
|
||||||
|
"반납"
|
||||||
|
],
|
||||||
|
"answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-105",
|
||||||
|
"title": "모니터 추가 신청",
|
||||||
|
"keywords": [
|
||||||
|
"모니터"
|
||||||
|
],
|
||||||
|
"answer": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-106",
|
||||||
|
"title": "회의실 예약 규칙",
|
||||||
|
"keywords": [
|
||||||
|
"회의실 규칙"
|
||||||
|
],
|
||||||
|
"answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-102",
|
||||||
|
"title": "회의실 예약 (중복)",
|
||||||
|
"keywords": [
|
||||||
|
"회의실"
|
||||||
|
],
|
||||||
|
"answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-108",
|
||||||
|
"title": "화상회의 장비",
|
||||||
|
"keywords": [],
|
||||||
|
"answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-109",
|
||||||
|
"title": "보안 사고 신고",
|
||||||
|
"keywords": [
|
||||||
|
"보안",
|
||||||
|
"신고"
|
||||||
|
],
|
||||||
|
"answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-110",
|
||||||
|
"title": "사내 메신저 설치",
|
||||||
|
"keywords": [
|
||||||
|
"메신저"
|
||||||
|
],
|
||||||
|
"answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-003",
|
||||||
|
"title": "VPN 접속 오류 (새 판)",
|
||||||
|
"keywords": [
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-112",
|
||||||
|
"title": "VPN 접속 오류",
|
||||||
|
"keywords": [
|
||||||
|
"vpn",
|
||||||
|
"재택근무"
|
||||||
|
],
|
||||||
|
"answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-113",
|
||||||
|
"title": "프린터 토너 교체",
|
||||||
|
"keywords": [
|
||||||
|
"토너"
|
||||||
|
],
|
||||||
|
"answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"batch": "B-3",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "FAQ-301",
|
||||||
|
"title": "택배 수령",
|
||||||
|
"keywords": [
|
||||||
|
"택배"
|
||||||
|
],
|
||||||
|
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-302",
|
||||||
|
"title": "주차 등록",
|
||||||
|
"keywords": [
|
||||||
|
"주차"
|
||||||
|
],
|
||||||
|
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-303",
|
||||||
|
"title": "명함 신청",
|
||||||
|
"keywords": [
|
||||||
|
"명함"
|
||||||
|
],
|
||||||
|
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-304",
|
||||||
|
"title": "사내 식당 메뉴",
|
||||||
|
"keywords": [
|
||||||
|
"식당",
|
||||||
|
"메뉴"
|
||||||
|
],
|
||||||
|
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-305",
|
||||||
|
"title": "교육 신청",
|
||||||
|
"keywords": [
|
||||||
|
"교육"
|
||||||
|
],
|
||||||
|
"answer": "짧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "FAQ-306",
|
||||||
|
"title": "건강검진",
|
||||||
|
"keywords": [
|
||||||
|
"건강검진"
|
||||||
|
],
|
||||||
|
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"employee": {
|
||||||
|
"schema": "employee",
|
||||||
|
"batch": "HR-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 4,
|
||||||
|
"invalid": 5,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "sehoon.oh@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "haneul.yoon@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "minsu.kang@example",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:email"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "jia.seo@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:dept"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "doyun.lim@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:joined"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "noname@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "sua.bae@example.com",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "daeun.jung@example.com",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:email": 1,
|
||||||
|
"not_allowed:dept": 1,
|
||||||
|
"bad_format:joined": 1,
|
||||||
|
"missing:name": 1,
|
||||||
|
"duplicate_in_batch": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"schema": "faq",
|
||||||
|
"batch": "B-3",
|
||||||
|
"total": 6,
|
||||||
|
"valid": 5,
|
||||||
|
"invalid": 1,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "FAQ-301",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "FAQ-302",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "FAQ-303",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "FAQ-304",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "FAQ-305",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:answer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "FAQ-306",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"too_short:answer": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"total": 9,
|
||||||
|
"valid": 2,
|
||||||
|
"invalid": 7,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 2,
|
||||||
|
"key": "T-12",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:ticket_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 3,
|
||||||
|
"key": "T-2003",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"not_allowed:priority"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 4,
|
||||||
|
"key": "T-2004",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:opened"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 5,
|
||||||
|
"key": "T-2005",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"too_short:title"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 6,
|
||||||
|
"key": "T-2006",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"bad_format:contact"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 7,
|
||||||
|
"key": "T-2001",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"duplicate_in_batch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 8,
|
||||||
|
"key": "T-2008",
|
||||||
|
"ok": false,
|
||||||
|
"errors": [
|
||||||
|
"missing:requester"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 9,
|
||||||
|
"key": "T-2009",
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": {
|
||||||
|
"bad_format:ticket_id": 1,
|
||||||
|
"not_allowed:priority": 1,
|
||||||
|
"bad_format:opened": 1,
|
||||||
|
"too_short:title": 1,
|
||||||
|
"bad_format:contact": 1,
|
||||||
|
"duplicate_in_batch": 1,
|
||||||
|
"missing:requester": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "employee",
|
||||||
|
"key": "email",
|
||||||
|
"fields": {
|
||||||
|
"name": {"type": "string", "required": true, "max": 20},
|
||||||
|
"dept": {"type": "enum", "required": true, "values": ["영업팀", "재무팀", "인사팀", "개발팀", "IT팀", "총무팀"]},
|
||||||
|
"email": {"type": "email", "required": true},
|
||||||
|
"phone": {"type": "phone", "required": false},
|
||||||
|
"joined": {"type": "date", "required": true}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "faq",
|
||||||
|
"key": "id",
|
||||||
|
"fields": {
|
||||||
|
"id": {"type": "pattern", "required": true, "pattern": "^FAQ-\\d{3}$"},
|
||||||
|
"title": {"type": "string", "required": true, "max": 40},
|
||||||
|
"keywords": {"type": "list", "required": true, "min_items": 1, "max_items": 8},
|
||||||
|
"answer": {"type": "string", "required": true, "min": 10, "max": 300}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "ticket",
|
||||||
|
"key": "ticket_id",
|
||||||
|
"fields": {
|
||||||
|
"ticket_id": {"type": "pattern", "required": true, "pattern": "^T-\\d{4}$"},
|
||||||
|
"title": {"type": "string", "required": true, "min": 4, "max": 40},
|
||||||
|
"requester": {"type": "string", "required": true, "max": 20},
|
||||||
|
"priority": {"type": "enum", "required": true, "values": ["low", "normal", "high"]},
|
||||||
|
"opened": {"type": "date", "required": true},
|
||||||
|
"contact": {"type": "phone", "required": false}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"schema": "ticket",
|
||||||
|
"batch": "TK-1",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 켜지지 않음",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-28",
|
||||||
|
"contact": "010-2345-6789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-12",
|
||||||
|
"title": "메일 첨부가 안 열림",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2003",
|
||||||
|
"title": "VPN이 계속 끊김",
|
||||||
|
"requester": "최유진",
|
||||||
|
"priority": "urgent",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2004",
|
||||||
|
"title": "키보드 교체 요청",
|
||||||
|
"requester": "한지훈",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-02-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2005",
|
||||||
|
"title": "프린터",
|
||||||
|
"requester": "김민지",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2006",
|
||||||
|
"title": "공유 드라이브 권한",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "normal",
|
||||||
|
"opened": "2026-10-30",
|
||||||
|
"contact": "010-12-3456"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2001",
|
||||||
|
"title": "모니터가 또 꺼짐",
|
||||||
|
"requester": "박서연",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2008",
|
||||||
|
"title": "화상회의 카메라 대여",
|
||||||
|
"requester": "",
|
||||||
|
"priority": "low",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ticket_id": "T-2009",
|
||||||
|
"title": "새 직원 계정 만들기",
|
||||||
|
"requester": "이준호",
|
||||||
|
"priority": "high",
|
||||||
|
"opened": "2026-10-30"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from tcheck import checks, load
|
||||||
|
from tcheck.app import CheckApp
|
||||||
|
from conftest import DATA
|
||||||
|
|
||||||
|
|
||||||
|
def make(tmp_path):
|
||||||
|
rows, _ = load.read_rows(str(DATA / "employees.csv"))
|
||||||
|
schema = json.loads((DATA / "schemas" / "employee.json").read_text())
|
||||||
|
args = SimpleNamespace(file=str(DATA / "employees.csv"), out=str(tmp_path / "clean.csv"))
|
||||||
|
return CheckApp(args, rows, schema, checks.check(rows, schema, "HR-1"))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_tabs_filter_and_export(tmp_path):
|
||||||
|
app = make(tmp_path)
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
assert "9 rows · valid 4" in str(app.query_one("#top").render())
|
||||||
|
assert app.query_one("#problem-table").row_count == 5
|
||||||
|
assert app.query_one("#code-table").row_count == 5
|
||||||
|
assert app.query_one("#clean-table").row_count == 4
|
||||||
|
await pilot.press("slash")
|
||||||
|
await pilot.pause()
|
||||||
|
for key in "email":
|
||||||
|
await pilot.press(key)
|
||||||
|
await pilot.press("enter")
|
||||||
|
assert app.query_one("#problem-table").row_count == 1
|
||||||
|
await pilot.press("escape")
|
||||||
|
assert app.query_one("#problem-table").row_count == 5
|
||||||
|
await pilot.press("e")
|
||||||
|
assert app.exported.endswith("clean.csv") and (tmp_path / "clean.csv").read_text().count("\n") == 5
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tcheck import checks, load
|
||||||
|
from conftest import DATA
|
||||||
|
|
||||||
|
FILES = {"employee": "employees.csv", "ticket": "tickets-new.json", "faq": "faq-batch-3.json"}
|
||||||
|
|
||||||
|
|
||||||
|
def schema(name):
|
||||||
|
return json.loads((DATA / "schemas" / f"{name}.json").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, batch="X", content=()):
|
||||||
|
rows, _ = load.read_rows(str(DATA / FILES[name]))
|
||||||
|
return checks.check(rows, schema(name), batch, content)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["employee", "ticket", "faq"])
|
||||||
|
def test_same_rows_and_codes_as_n8n(name, n8n):
|
||||||
|
"""R4: the same file gives the same rows · codes · valid as n8n my-check."""
|
||||||
|
r = run(name, n8n[name]["batch"])
|
||||||
|
for key in ("schema", "batch", "total", "valid", "invalid", "rows", "codes"):
|
||||||
|
assert r[key] == n8n[name][key], key
|
||||||
|
|
||||||
|
|
||||||
|
def test_employee_reasons():
|
||||||
|
r = run("employee")
|
||||||
|
assert [row["errors"] for row in r["rows"]] == [[], [], ["bad_format:email"], ["not_allowed:dept"], ["bad_format:joined"],
|
||||||
|
["missing:name"], [], ["duplicate_in_batch"], []]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ticket_optional_field_and_impossible_date():
|
||||||
|
r = run("ticket")
|
||||||
|
assert r["rows"][3]["errors"] == ["bad_format:opened"] # 2026-02-30
|
||||||
|
assert r["rows"][5]["errors"] == ["bad_format:contact"] # present but wrong
|
||||||
|
assert r["rows"][8]["ok"] # contact missing is fine
|
||||||
|
|
||||||
|
|
||||||
|
def test_string_list_and_blank_edges():
|
||||||
|
s = {"name": "t", "key": "id", "fields": {"id": {"type": "string", "required": True, "min": 2, "max": 3},
|
||||||
|
"tags": {"type": "list", "required": False, "min_items": 1, "max_items": 2}}}
|
||||||
|
r = checks.check([{"id": "ab"}, {"id": "abcd"}, {"id": " "}, {"id": "abc", "tags": "x"}, {"id": "ab2", "tags": [1, 2, 3]}], s)
|
||||||
|
assert [row["errors"] for row in r["rows"]] == [[], ["too_long:id"], ["missing:id"], ["bad_format:tags"], ["too_many:tags"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_rules_on_a_field():
|
||||||
|
"""R8: Lab 01 rules — phone without dashes, exact intra host."""
|
||||||
|
s = {"name": "faq", "key": "id", "fields": {}}
|
||||||
|
rows = [{"id": "1", "answer": "당직 01012345678"}, {"id": "2", "answer": "02-123-4567"},
|
||||||
|
{"id": "3", "answer": "https://intra.example.com/help"}, {"id": "4", "answer": "http://intra.example.com.evil.io/x"},
|
||||||
|
{"id": "5", "answer": "a@b.com"}]
|
||||||
|
r = checks.check(rows, s, content=("answer",))
|
||||||
|
assert [row["errors"] for row in r["rows"]] == [["pii"], [], [], ["external_link"], ["pii"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_equal_passes():
|
||||||
|
r = run("employee") # 5 / 9 = 0.5556
|
||||||
|
assert checks.gate(r, 0.3) == {"accepted": False, "invalid_ratio": 0.5556, "max_invalid_ratio": 0.3}
|
||||||
|
assert checks.gate({"invalid": 1, "total": 10}, 0.1)["accepted"] is True
|
||||||
|
assert checks.gate({"invalid": 0, "total": 0}, 0.3)["accepted"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_fix_list_and_message():
|
||||||
|
r = run("ticket", "TK-1")
|
||||||
|
f = checks.fix_list(r, schema("ticket"))
|
||||||
|
assert f["message"] == "TK-1: 9줄 중 2줄 통과, 7줄 고칠 것"
|
||||||
|
assert [i["row"] for i in f["fix_list"]] == [2, 3, 4, 5, 6, 7, 8]
|
||||||
|
assert f["fix_list"][1]["fix"] == ["priority은(는) low · normal · high 중 하나로"]
|
||||||
|
assert "ticket_id" in f["fix_list"][5]["fix"][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_quality_entry_and_round():
|
||||||
|
e = checks.quality_entry(run("employee", "HR-1"))
|
||||||
|
assert e == {"batch": "HR-1", "dataset": "employee", "total": 9, "valid": 4, "quarantined": 5, "rejected": False,
|
||||||
|
"score": 0.4444, "codes": e["codes"]}
|
||||||
|
assert checks.js_round(0.00005) == 0.0001 and checks.js_round(2 / 3) == 0.6667
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import csv
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tcheck import cli
|
||||||
|
from conftest import DATA
|
||||||
|
|
||||||
|
EMP = str(DATA / "employees.csv")
|
||||||
|
EMP_SCHEMA = str(DATA / "schemas" / "employee.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_plain(capsys):
|
||||||
|
assert cli.main(["check", EMP, "--schema-file", EMP_SCHEMA]) == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "9 rows · valid 4 · invalid 5 · score 0.4444" in out and "bad_format:email" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_json_like_my_check(capsys, url, n8n):
|
||||||
|
assert cli.main(["check", str(DATA / "tickets-new.json"), "--url", url, "--json"]) == 0 # schema from the file
|
||||||
|
out = json.loads(capsys.readouterr().out)
|
||||||
|
assert {k: out[k] for k in ("schema", "batch", "valid", "rows", "codes")} == {k: n8n["ticket"][k] for k in ("schema", "batch", "valid", "rows", "codes")}
|
||||||
|
|
||||||
|
|
||||||
|
def test_out_and_rejects_files(tmp_path):
|
||||||
|
clean, rejects = tmp_path / "clean.csv", tmp_path / "rejects.csv"
|
||||||
|
assert cli.main(["check", EMP, "--schema-file", EMP_SCHEMA, "--out", str(clean), "--rejects", str(rejects)]) == 0
|
||||||
|
assert len(list(csv.DictReader(clean.open()))) == 4
|
||||||
|
bad = list(csv.DictReader(rejects.open()))
|
||||||
|
assert len(bad) == 5 and bad[0]["errors"] == "bad_format:email"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gate_exit_codes(capsys):
|
||||||
|
assert cli.main(["gate", EMP, "--schema-file", EMP_SCHEMA]) == 4
|
||||||
|
assert "rejected" in capsys.readouterr().out
|
||||||
|
assert cli.main(["gate", EMP, "--schema-file", EMP_SCHEMA, "--max-invalid", "0.6"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_fix_and_log(capsys, url, api):
|
||||||
|
assert cli.main(["check", EMP, "--schema", "employee", "--url", url, "--batch", "HR-1", "--fix", "--log"]) == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "HR-1: 9줄 중 4줄 통과, 5줄 고칠 것" in out and "email의 형식을 확인해 주세요" in out
|
||||||
|
assert api.log == [{"batch": "HR-1", "dataset": "employee", "total": 9, "valid": 4, "quarantined": 5, "rejected": False,
|
||||||
|
"score": 0.4444, "codes": api.log[0]["codes"]}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit_codes_read_and_usage(tmp_path, url, capsys):
|
||||||
|
assert cli.main(["check", str(tmp_path / "none.csv"), "--schema-file", EMP_SCHEMA]) == 1
|
||||||
|
assert cli.main(["check", EMP, "--schema", "nothing", "--url", url]) == 1
|
||||||
|
assert cli.main(["check", EMP]) == 2 # which schema?
|
||||||
|
assert cli.main(["nonsense"]) == 2
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tcheck import load
|
||||||
|
from conftest import DATA
|
||||||
|
|
||||||
|
|
||||||
|
def test_csv_first_line_is_columns():
|
||||||
|
rows, meta = load.read_rows(str(DATA / "employees.csv"))
|
||||||
|
assert len(rows) == 9 and rows[2]["email"] == "minsu.kang@example" and rows[1]["phone"] == ""
|
||||||
|
assert meta["batch"] == "employees"
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_file_with_csv_or_items(tmp_path):
|
||||||
|
rows, meta = load.read_rows(str(DATA / "tickets-new.json"))
|
||||||
|
assert len(rows) == 9 and meta == {"schema": "ticket", "batch": "TK-1"}
|
||||||
|
f = tmp_path / "hr.json"
|
||||||
|
f.write_text(json.dumps({"schema": "employee", "batch": "HR-1", "csv": (DATA / "employees.csv").read_text()}))
|
||||||
|
assert len(load.read_rows(str(f))[0]) == 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_list(tmp_path):
|
||||||
|
f = tmp_path / "rows.json"
|
||||||
|
f.write_text("[{\"a\": 1}]")
|
||||||
|
assert load.read_rows(str(f)) == ([{"a": 1}], {"batch": "rows"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_or_bad_files(tmp_path):
|
||||||
|
with pytest.raises(load.LoadError):
|
||||||
|
load.read_rows(str(tmp_path / "nope.csv"))
|
||||||
|
bad = tmp_path / "bad.json"
|
||||||
|
bad.write_text("{\"x\": 1}")
|
||||||
|
with pytest.raises(load.LoadError):
|
||||||
|
load.read_rows(str(bad))
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_from_api_or_file(url):
|
||||||
|
assert load.read_schema("employee", url)["key"] == "email"
|
||||||
|
assert load.read_schema("", None, str(DATA / "schemas" / "ticket.json"))["name"] == "ticket"
|
||||||
|
with pytest.raises(load.LoadError):
|
||||||
|
load.read_schema("nothing", url)
|
||||||
|
with pytest.raises(load.LoadError):
|
||||||
|
load.read_schema("employee", None)
|
||||||
Generated
+195
@@ -0,0 +1,195 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkify-it-py"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/45/98/7a1a5f31fd5c7ba93e963b168e244b8e3dd705b3d2a718e3c3307583bf57/linkify_it_py-2.2.0.tar.gz", hash = "sha256:907acd2d17ac1fbb9ddb62c8957ccbd6158cac602231a15c3b0cd1e215f03cee", size = 32939, upload-time = "2026-08-29T07:07:08.305Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl", hash = "sha256:3adc40eb5af300b2605fcfdb968c24e1d780a90f1f2221af7c15e5111e94d443", size = 21971, upload-time = "2026-08-29T07:07:07.164Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
linkify = [
|
||||||
|
{ name = "linkify-it-py" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdit-py-plugins"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.14.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6e/4e/2684728d9f2c8c4110738dcadca7e0bed4b60c8dc68c40c2a9e0cf017900/platformdirs-4.14.2.tar.gz", hash = "sha256:6d13b4c1d66b7548dcec9e3db13c1ba9d9adb65041b5d07869313afc07e6e41b", size = 70792, upload-time = "2026-10-31T15:10:34.029Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/2d/98af31fdcd7c95c7114e2eb7c2eb0fa55d2a012c37ba4eb3722b7272abcb/platformdirs-4.14.2-py3-none-any.whl", hash = "sha256:dc1f93390f2a9265e44e5afc8667ec7faf737739604e39bc538fec889720c36b", size = 35291, upload-time = "2026-10-31T15:10:32.615Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/9a/887535e477847f5159377ab35303923644840b13c0ca03c0a467f84d4680/pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b", size = 145237, upload-time = "2026-10-25T21:05:37.135Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/d7/cd9e2708afdbf059658adaf08bdb0792ca52489871051d6ce7d498b51277/pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc", size = 17061, upload-time = "2026-10-25T21:05:35.473Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "15.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tcheck"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "textual" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "textual", specifier = ">=0.80" }]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest", specifier = ">=8" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=0.23" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textual"
|
||||||
|
version = "8.2.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||||
|
{ name = "mdit-py-plugins" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
Reference in new issue
Block a user