메타넷글로벌
경력근거: 고객사 환경 현장 배포, QA, 안정화 지원
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고가 주요업무로 '자연어 검색, Knowledge Graph / Ontology, Text2SQL 기술을 활용한 지식 기반 데이터 파이프라인 설계 및 구축' 과 '고객사 환경 현장 배포, QA, 안정화 지원' 을 나란히 적었습니다. Text2SQL 을 세우는 사람과 그것을 고객사 인수 검수에 넘기는 사람이 같은 FDE 라는 뜻입니다. 메타넷글로벌은 공공/SOC·금융·제조·교육 고객사의 대규모 구축을 맡는 IT 서비스 회사라, 고객사마다 테이블 카탈로그와 부서별 열람 권한이 다르고 현업이 쓰는 QA 시트 양식도 다릅니다. 우대사항의 'LLM 평가(eval) 체계 설계' 와 '권한 모델 경험' 이 이 도구의 두 판정 축과 맞닿습니다. 이 포트폴리오는 LLM 출력을 현업 정답과 규칙으로 대조하고 테이블 권한을 판정하는 기술을 보여줍니다. 고객사 ERP 업무 규칙이나 제조 원가 회계 전문성은 주장하지 않습니다.
주요업무에 '자연어 검색, Knowledge Graph / Ontology, Text2SQL 기술을 활용한 지식 기반 데이터 파이프라인 설계 및 구축' 이 있습니다. 질문을 SQL 로 바꾸는 것까지가 구축이라면, 그 답이 현업이 적은 정답과 맞는지 문항마다 판정하는 도구는 같은 파이프라인을 인수 검수에 넘기는 물건입니다.
'고객사 환경 현장 배포, QA, 안정화 지원' 과 '배포 이후 성능 모니터링 및 지속적인 이슈 대응' 을 함께 적었고, 자격요건에 'Field Work: 고객사 파견 업무를 수행하여 현장의 문제를 직접 해결할 수 있는 분' 이 있습니다. 배포할 때마다 QA 시트 전체를 다시 돌리는 CLI 는 파견된 엔지니어가 자기 터미널에서 쓰는 도구입니다.
우대사항에 'LLM 평가(eval) 체계 설계' 와 'Confluence/Jira Data Center API 연동 및 권한 모델 경험' 이 있습니다. 질문자 역할과 테이블 허용 역할을 견주는 권한 판정, 문항별 정답 비교가 그 두 항목의 실물이고, 걸린 문항은 3주차에 Jira 이슈로 올립니다.
고객사 현업이 엑셀로 적어 준 QA 시트와, 자연어 질문을 SQL 로 바꿔 답하는 Text2SQL 의 실행 기록, 테이블 카탈로그 세 파일을 받아 질문마다 다섯 판정 중 하나를 터미널에 찍는 CLI 입니다. 현업은 정답 숫자를 '3,880개'·'12.4억'·'3억 2,500만 원' 처럼 단위와 한글 수 단위를 섞어 적으므로, 이를 숫자로 풀어 Text2SQL 결과와 허용오차 0.5퍼센트 안에서 견줍니다. '약 3천' 처럼 숫자로 풀리지 않는 정답은 판정하지 않고 따로 셉니다. 생성된 SQL 이 쓴 테이블은 스키마 접두어를 떼고 대문자로 맞춰 카탈로그에 있는지, 질문한 사람의 역할이 그 테이블을 볼 수 있는지 확인합니다. 메타넷글로벌 FDE 엔지니어는 배포 전마다 이 명령 한 줄로 QA 시트 전체를 다시 돌리고, 걸린 질문만 판정별 구획으로 읽습니다.
고객사에 파견돼 Text2SQL 을 배포하고 인수 검수(QA)와 안정화를 맡은 메타넷글로벌 FDE 엔지니어 — 배포 전마다 자기 터미널에서 QA 시트 전체를 다시 돌린다
인수 검수에서 재검수가 반복되는 까닭은 Text2SQL 이 자주 틀려서라기보다, 프롬프트나 스키마 설명을 고칠 때마다 전에 맞던 질문이 조용히 틀어지는데 120문항을 손으로 다시 대조할 수 없기 때문입니다. 현업이 적은 정답 문자열을 숫자로 풀고 테이블 권한까지 한 번에 판정하면, 배포 전에 어긋난 질문만 추려 고칠 수 있습니다.
FDE 엔지니어가 QA 시트와 실행 기록, 테이블 카탈로그를 넣고 명령 한 줄을 치면 질문마다 다섯 판정 중 하나와 정답 숫자, 결과 숫자, 차이 비율, 행 수 차이를 판정별 구획으로 찍고 같은 내용을 마크다운 리포트로 남기는 CLI 입니다. 정답 문자열의 한글 수 단위를 숫자로 푸는 파서와 허용오차 비교가 이 주의 계산입니다. '약 3천' 처럼 숫자로 풀리지 않는 정답은 판정하지 않고 따로 세어 리포트 끝에 원문 그대로 둡니다. 실행 기록은 고객사 Text2SQL 이 남긴 JSONL 파일을 받는다고 두고, 예시 DB 에서 SQL 을 다시 실행하는 일은 2주차로 미룹니다.
직접 세운 제조 예시 DB 에서 generated_sql 을 다시 실행해 실행 기록의 결과 숫자와 행 수가 재현되는지 보고, SQL 구문을 파싱해 실행 기록의 참조 테이블 목록이 빠지지 않았는지 대조합니다. 120문항에 손으로 단 정답 판정과 CLI 판정을 견주는 평가 러너를 만듭니다.
프롬프트나 스키마 설명을 바꾼 두 배포의 판정을 견줘 맞음에서 어긋남으로 뒤집힌 질문을 따로 냅니다. 걸린 질문마다 클로드가 생성 SQL 의 어느 조건이 차이를 만들었을지 추정을 한 줄로 달고, Jira Data Center API 로 고객사 프로젝트에 이슈를 올립니다.
FDE 엔지니어가 `t2s-qa check --sheet qa_sheet_2026-09-26.xlsx --runs runs/2026-09-26.jsonl` 을 치면 Q-031 이 숫자가 다름 구획에 4,120개와 정답 3,880개를 나란히 찍고, Q-066 은 권한 밖 테이블을 씀 구획에 질문자 생산관리와 재무 전용 COST_MASTER 를 함께 찍습니다. 이 터미널 화면과 같은 실행이 남긴 마크다운 리포트를 시연합니다.
QA 시트 120문항에 손으로 정답 판정을 달아 CLI 판정과 일치하는 문항 수를 셉니다. 이 수가 알려 주는 것은 허용오차 0.5퍼센트 안팎의 숫자 판정과 역할 권한 판정이 적어 둔 규칙대로 갈리는지 하나뿐입니다. QA 시트와 실행 기록, 정답 판정을 모두 작성자가 지은 합성 데이터이기 때문입니다.
정답 칸이 비어 행 수만 적힌 문항, '3억 2,500만 원' 처럼 수 단위가 둘 섞인 정답, 소문자와 스키마 접두어가 붙은 테이블 이름, 허용오차 0.5퍼센트 바로 안쪽의 결과, 숫자로 풀 수 없는 정답만 모은 회귀 세트를 규칙을 고칠 때마다 돌려 판정이 바뀌지 않는지 봅니다.
여기 나온 숫자는 작성자가 꾸민 QA 시트와 실행 기록 위에서 작성자가 채점한 결과입니다. 인수 검수의 재검수가 실제로 줄었는지는 고객사 현업이 쓴 실제 QA 시트와 운영 DB 로 몇 차례 배포를 거쳐 봐야 알 수 있고, 이 포트폴리오는 그 배포 때마다 바로 돌릴 CLI 와 평가 러너까지 만듭니다.
일치 문항 수는 작성자가 만든 QA 시트를 작성자가 채점한 값이라 성능 증명이 아닙니다. 면접에서는 데모 조건에서 판정이 어떻게 갈리는지와, 실제 QA 시트가 들어오면 파서가 못 푸는 정답부터 셀 계획으로 설명합니다.
정답 칸이 비어 있는 문항, 수 단위가 섞인 정답, 소문자와 접두어가 붙은 테이블 이름, 허용오차 바로 안쪽의 결과, 없는 테이블을 써서 실행이 실패한 기록, 합계 대상이 없어 결과 칸이 빈 기록, '약 3천' 처럼 숫자로 풀 수 없는 정답을 샘플에 일부러 넣었습니다. 이 가운데 Q-083 과 Q-095 는 처음 적은 규칙으로는 정답과 맞음으로 흘러가던 문항이라, 규칙을 고칠 때마다 이 둘이 제 자리로 떨어지는지부터 봅니다.
이 포트폴리오는 LLM 출력을 현업 정답과 규칙으로 대조하고 테이블 권한을 판정하는 기술을 보여줍니다. 고객사 ERP 업무 규칙이나 제조 원가 회계 전문성은 주장하지 않습니다.
숫자가 다르다는 판정이 SQL 이 틀렸다는 뜻만은 아닙니다. 현업이 정답을 적은 시점과 데이터가 달라졌거나 '납기 지난' 의 뜻을 부서마다 다르게 쓰는 일도 있어, 정답을 고칠지 SQL 을 고칠지는 고객사 현업과 엔지니어가 함께 정합니다.
행 단위 결과를 합계 하나로 견주므로, 공장별 세 줄의 합이 맞아도 줄마다 값이 틀린 경우는 이 판정이 잡지 못합니다. 실제 도입에서는 행 단위 정답을 받는 문항을 따로 두어야 합니다.
실행 기록의 결과 숫자에는 고객사 운영 데이터가 담깁니다. 원인 추정에 넘기는 값은 질문과 SQL, 두 숫자로 줄이고, 고객사가 외부 모델 호출을 허락하지 않으면 3주차 추정 기능은 끄고 판정만 씁니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사 현업이 엑셀로 적어 준 인수 검수용 질문과 정답입니다. 질문 번호와 질문 원문, 질문한 사람의 부서 역할, 정답 값, 정답 행 수를 한 줄에 둡니다. 정답 값은 현업이 쓰던 대로 '3,880개'·'12.4억'·'3억 2,500만 원' 처럼 적혀 있고, 목록을 묻는 질문은 정답 값을 비우고 행 수만 적습니다.
| question_id | question | asker_role | expected_value | expected_rows |
|---|---|---|---|---|
| Q-012 | 이번 달 2공장 생산 수량 | 생산관리 | 52,300개 | — |
| Q-019 | 지난 분기 매출 총액 | 재무 | 12.4억 | — |
| Q-024 | 이번 주 불량 건수 | 품질 | 37건 | — |
| Q-031 | 3분기 공장별 불량 수량 | 품질 | 3,880개 | 3 |
| Q-036 | 거래 중인 협력사 수 | 구매 | 142 | — |
아래 JSON Schema 를 그대로 만족하는 QA 시트 줄을 110개 더 만들어 주세요. 제조 고객사의 생산·품질·구매·재무·영업 현업이 ERP 와 MES 데이터에 대해 물을 법한 질문을 한국어로 씁니다. 질문 번호는 'Q-' 뒤에 세 자리로 씁니다. 합계·건수를 묻는 질문은 정답 값을 채우고, 목록을 묻는 질문은 정답 값을 null 로 두고 행 수를 적습니다. 쉬운 데이터만 만들지 말고 다음을 섞으세요: 정답 값을 '52,300개'·'12.4억'·'3억 2,500만 원'·'1.2만 건'·'142' 처럼 단위와 수 단위를 섞어 적고, 열 줄에 한 번쯤 질문자 역할이 평소 보지 않는 데이터(생산관리가 원가, 구매가 인건비)를 묻게 합니다. 판정 결과는 넣지 마세요. 형식 깨뜨리기 시도로 정답 값에 '약 3천' 이나 '확인 필요' 처럼 숫자로 풀 수 없는 문자열을 적은 줄을 따로 5건 만들어 파서가 걸러내는지 보게 합니다.
{"type":"object","properties":{"question_id":{"type":"string"},"question":{"type":"string"},"asker_role":{"type":"string","enum":["생산관리","품질","구매","재무","영업"]},"expected_value":{"type":["string","null"]},"expected_rows":{"type":["integer","null"],"minimum":0}},"required":["question_id","question","asker_role","expected_value","expected_rows"]}{"type":"object","properties":{"question_id":{"type":"string"},"question":{"type":"string"},"asker_role":{"type":"string","enum":["생산관리","품질","구매","재무","영업"]},"expected_value":{"type":["string","null"]},"expected_rows":{"type":["integer","null"],"minimum":0}},"required":["question_id","question","asker_role","expected_value","expected_rows"]}Text2SQL 이 QA 시트 질문마다 남긴 실행 기록입니다. 실행 번호와 질문 번호, 생성된 SQL, 그 SQL 이 쓴 테이블 목록, 결과의 수치 합계, 결과 행 수를 한 줄에 둡니다. 테이블 이름은 생성된 SQL 에 적힌 그대로라 소문자나 'erp.' 같은 스키마 접두어가 섞이고, 없는 테이블을 써서 실행이 실패한 기록은 결과 칸이 비어 있습니다.
| run_no | question_id | generated_sql | referenced_tables | result_value | result_rows |
|---|---|---|---|---|---|
| RUN-0926-012 | Q-012 | SELECT SUM(qty) FROM PROD_RESULT WHERE… | ["PROD_RESULT"] | 52,300 | 1 |
| RUN-0926-019 | Q-019 | SELECT SUM(sales_amt) FROM SALES_LEDGE… | ["SALES_LEDGER"] | 1,238,000,000 | 1 |
| RUN-0926-024 | Q-024 | SELECT COUNT(*) FROM DEFECT_LOG WHERE … | ["DEFECT_LOG"] | 37 | 1 |
| RUN-0926-031 | Q-031 | SELECT p.plant_nm, SUM(d.defect_qty) F… | ["DEFECT_LOG","PLANT_MASTER"] | 4,120 | 3 |
| RUN-0926-036 | Q-036 | SELECT COUNT(*) FROM VENDOR_MASTER WHE… | ["VENDOR_MASTER"] | 142 | 1 |
아래 JSON Schema 를 그대로 만족하는 실행 기록 줄을 110개 더 만들어 주세요. 질문 번호는 QA 시트와 같은 체계로 쓰고, 생성된 SQL 은 PROD_RESULT·DEFECT_LOG·PLANT_MASTER·ITEM_MASTER·VENDOR_MASTER·PO_HEADER·PO_LINE·PO_RECV·COST_MASTER·SALES_LEDGER·SHIP_ORDER·PAYROLL 테이블을 쓰는 실행 가능한 SELECT 문으로 씁니다. referenced_tables 에는 SQL 에 나온 테이블 이름을 적힌 그대로 넣습니다. 쉬운 데이터만 만들지 말고 다음을 섞으세요: 열 줄에 한 번쯤 SQL 을 소문자로 쓰고 'erp.' 접두어를 붙이며, 스무 줄에 한 번꼴로 카탈로그에 없는 테이블(예: PO_RECV_TMP)을 써서 result_value 와 result_rows 를 null 로 두고, 합계 질문의 결과를 정답에서 0.3~8퍼센트 벗어나게 둔 줄과, 조건에 걸린 행이 없어 SUM 이 빈 값을 돌려준 줄(result_value 는 null, result_rows 는 1)을 섞습니다. 판정 결과는 넣지 마세요.
{"type":"object","properties":{"run_no":{"type":"string"},"question_id":{"type":"string"},"generated_sql":{"type":"string"},"referenced_tables":{"type":"array","items":{"type":"string"}},"result_value":{"type":["number","null"]},"result_rows":{"type":["integer","null"],"minimum":0}},"required":["run_no","question_id","generated_sql","referenced_tables","result_value","result_rows"]}{"type":"object","properties":{"run_no":{"type":"string"},"question_id":{"type":"string"},"generated_sql":{"type":"string"},"referenced_tables":{"type":"array","items":{"type":"string"}},"result_value":{"type":["number","null"]},"result_rows":{"type":["integer","null"],"minimum":0}},"required":["run_no","question_id","generated_sql","referenced_tables","result_value","result_rows"]}지원자가 직접 세운 제조 예시 DB 의 테이블 카탈로그입니다. 테이블 이름과 설명, 그 테이블을 볼 수 있는 부서 역할 목록을 한 줄에 둡니다. 원가·인건비처럼 재무나 인사만 보는 테이블이 섞여 있어, 질문자 역할에 따라 같은 SQL 도 권한 밖이 됩니다. 이 표는 기준으로 받는 설정이고 이 도구가 만들지 않습니다.
| table_name | description | allowed_roles |
|---|---|---|
| PROD_RESULT | 라인별 일일 생산 실적 | ["생산관리","품질","재무"] |
| DEFECT_LOG | 공정 불량 발생 기록 | ["품질","생산관리"] |
| PLANT_MASTER | 공장 코드와 공장 이름 | ["생산관리","품질","구매","재무","영업"] |
| ITEM_MASTER | 품목 코드와 품목 속성 | ["생산관리","품질","구매","재무"] |
| VENDOR_MASTER | 협력사 기본 정보 | ["구매","재무"] |
아래 JSON Schema 를 그대로 만족하는 테이블 카탈로그 줄을 30개 더 만들어 주세요. 제조 고객사의 ERP·MES 테이블로 보고 테이블 이름은 영문 대문자와 밑줄로, 설명은 한국어 한 구절로 씁니다. 허용 역할은 그 테이블을 실제로 볼 부서만 한두 개에서 다섯 개까지 두고, 원가·급여·인사 평가처럼 민감한 테이블은 재무나 인사 하나로 좁힙니다. 판정 결과는 넣지 마세요.
{"type":"object","properties":{"table_name":{"type":"string"},"description":{"type":"string"},"allowed_roles":{"type":"array","items":{"type":"string","enum":["생산관리","품질","구매","재무","영업","인사"]}}},"required":["table_name","description","allowed_roles"]}{"type":"object","properties":{"table_name":{"type":"string"},"description":{"type":"string"},"allowed_roles":{"type":"array","items":{"type":"string","enum":["생산관리","품질","구매","재무","영업","인사"]}}},"required":["table_name","description","allowed_roles"]}| Python | 정답 문자열 파서, 허용오차 비교, 테이블 이름 대조와 권한 판정, CLI 와 마크다운 리포트를 맡습니다. |
| TypeScript | 3주차에 Jira 이슈 본문을 만드는 작은 연동 모듈을 씁니다. |
| LangGraph | 2주차 예시 DB 에서 쓰는 Text2SQL 을 질문 해석, 스키마 검색, SQL 생성, 실행 네 단계 그래프로 세워 실행 기록을 스스로 만듭니다. |
| Prompting | 3주차 원인 추정 프롬프트에 질문과 생성 SQL, 정답·결과 숫자만 넣고 추정 한 줄로 출력을 묶습니다. |
| Elasticsearch | 2주차 Text2SQL 이 질문에 맞는 테이블 설명을 찾는 스키마 검색 색인으로 씁니다. |
| RAG | 스키마 설명 문서를 검색해 SQL 생성 프롬프트에 넣는 구조로, 판정기가 대조할 실행 기록을 만듭니다. |
| NLP | 현업이 적은 '3억 2,500만 원' 같은 한국어 수량 표현을 숫자로 푸는 파서에 씁니다. |
| Jira Data Center API | 3주차에 걸린 질문을 고객사 프로젝트의 이슈로 올리고, 다음 실행에서 풀린 이슈를 닫습니다. |
✗ fail → 풀지 못한 정답이 열에 하나를 넘으면 판정보다 QA 시트 양식이 먼저라, 현업과 정답 칸 적는 방식을 맞추고 다시 돌립니다.
✓ pass → 2주차 SQL 재실행과 평가 러너로 넘어갑니다.
✗ fail → 재현이 안 되는 문항이 많으면 예시 DB 와 실행 기록의 데이터 시점이 다른 것이라, 시점을 맞춘 스냅샷부터 뜹니다.
✓ pass → 3주차 배포 간 회귀 비교와 Jira 연동으로 넘어갑니다.
질문별 검수 판정입니다. QA 시트 질문 하나마다 Text2SQL 답이 정답과 맞는지 판정을 적습니다. 판정과 함께 정답 문자열을 푼 숫자, 결과 숫자, 차이 비율, 두 행 수와 그 차이, 걸린 테이블 이름을 한 줄에 실어 터미널의 판정별 구획과 마크다운 리포트가 그대로 읽습니다.
{"type":"object","properties":{"verdict_no":{"type":"string"},"question_id":{"type":"string"},"run_no":{"type":"string"},"verdict":{"type":"string","enum":["정답과 맞음","숫자가 다름","행 수가 다름","없는 테이블을 씀","권한 밖 테이블을 씀"]},"expected_number":{"type":["number","null"],"description":"qa_sheet.expected_value 에서 천 단위 구분과 '개'·'건'·'원' 같은 단위 접미를 지우고 '억' 은 100000000, '만' 은 10000 을 곱해 더한 수입니다. expected_value 가 null 이거나 숫자로 풀리지 않으면 null 입니다"},"result_value":{"type":["number","null"]},"value_diff_pct":{"type":["number","null"],"description":"result_value 에서 expected_number 를 빼고 expected_number 로 나눠 100 을 곱한 뒤 소수 둘째 자리로 반올림한 비율입니다. 어느 한쪽이 null 이면 null 입니다"},"expected_rows":{"type":["integer","null"]},"result_rows":{"type":["integer","null"]},"rows_gap":{"type":["integer","null"],"description":"result_rows 에서 expected_rows 를 뺀 값입니다. 어느 한쪽이 null 이면 null 입니다"},"offending_table":{"type":["string","null"],"description":"없는 테이블을 씀·권한 밖 테이블을 씀일 때 걸린 테이블 이름을 referenced_tables 에 적힌 그대로 옮깁니다. 나머지 판정이면 null 입니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"table":{"type":"string","enum":["qa_sheet","text2sql_runs","schema_catalog"]},"field":{"type":"string"},"agg":{"type":["string","null"],"enum":["count","sum","avg","min","max","parsed",null],"description":"parsed 는 그 칸의 원문을 규칙 3으로 푼 수라는 표시입니다"},"value":{}},"required":["table","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["verdict_no","question_id","run_no","verdict","evidence","summary"]}[
{
"verdict_no": "QV-001",
"question_id": "Q-031",
"run_no": "RUN-0926-031",
"verdict": "숫자가 다름",
"expected_number": 3880,
"result_value": 4120,
"value_diff_pct": 6.19,
"expected_rows": 3,
"result_rows": 3,
"rows_gap": 0,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": "3,880개"
},
{
"table": "qa_sheet",
"field": "expected_value",
"agg": "parsed",
"value": 3880
},
{
"table": "text2sql_runs",
"field": "result_value",
"agg": null,
"value": 4120
},
{
"table": "qa_sheet",
"field": "expected_rows",
"agg": null,
"value": 3
},
{
"table": "text2sql_runs",
"field": "result_rows",
"agg": null,
"value": 3
}
],
"summary": "공장 세 곳의 불량 수량 합계가 정답 3,880개보다 6.19퍼센트 많은 4,120개로 나왔습니다.",
"hedge": "현업이 정답을 적은 뒤 3분기 불량 기록이 추가됐을 수 있어, 정답을 고칠지 SQL 을 고칠지는 품질 현업과 함께 정합니다."
},
{
"verdict_no": "QV-002",
"question_id": "Q-047",
"run_no": "RUN-0926-047",
"verdict": "행 수가 다름",
"expected_number": null,
"result_value": null,
"value_diff_pct": null,
"expected_rows": 11,
"result_rows": 18,
"rows_gap": 7,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": null
},
{
"table": "qa_sheet",
"field": "expected_rows",
"agg": null,
"value": 11
},
{
"table": "text2sql_runs",
"field": "result_rows",
"agg": null,
"value": 18
}
],
"summary": "납기 지난 발주가 정답 11건보다 7건 많은 18건으로 나왔습니다.",
"hedge": "부분 입고된 발주를 미결로 셀지 부서마다 다를 수 있어 구매 현업에게 기준을 되묻습니다."
},
{
"verdict_no": "QV-003",
"question_id": "Q-058",
"run_no": "RUN-0926-058",
"verdict": "없는 테이블을 씀",
"expected_number": null,
"result_value": null,
"value_diff_pct": null,
"expected_rows": 27,
"result_rows": null,
"rows_gap": null,
"offending_table": "PO_RECV_TMP",
"evidence": [
{
"table": "text2sql_runs",
"field": "referenced_tables",
"agg": null,
"value": [
"PO_RECV_TMP"
]
},
{
"table": "schema_catalog",
"field": "table_name",
"agg": "count",
"value": 12
}
],
"summary": "생성된 SQL 이 카탈로그 12개 테이블에 없는 PO_RECV_TMP 를 써서 실행이 실패했습니다.",
"hedge": null
},
{
"verdict_no": "QV-004",
"question_id": "Q-066",
"run_no": "RUN-0926-066",
"verdict": "권한 밖 테이블을 씀",
"expected_number": null,
"result_value": null,
"value_diff_pct": null,
"expected_rows": 10,
"result_rows": 10,
"rows_gap": 0,
"offending_table": "COST_MASTER",
"evidence": [
{
"table": "qa_sheet",
"field": "asker_role",
"agg": null,
"value": "생산관리"
},
{
"table": "text2sql_runs",
"field": "referenced_tables",
"agg": null,
"value": [
"COST_MASTER",
"ITEM_MASTER"
]
},
{
"table": "schema_catalog",
"field": "allowed_roles",
"agg": null,
"value": [
"재무"
]
},
{
"table": "qa_sheet",
"field": "expected_rows",
"agg": null,
"value": 10
},
{
"table": "text2sql_runs",
"field": "result_rows",
"agg": null,
"value": 10
}
],
"summary": "생산관리가 물은 질문에 재무만 볼 수 있는 COST_MASTER 를 써서 답했습니다.",
"hedge": "행 수는 정답과 같지만 권한 밖 데이터라, 이 질문을 생산관리에게 허용할지는 고객사 권한 담당이 정합니다."
},
{
"verdict_no": "QV-005",
"question_id": "Q-019",
"run_no": "RUN-0926-019",
"verdict": "정답과 맞음",
"expected_number": 1240000000,
"result_value": 1238000000,
"value_diff_pct": -0.16,
"expected_rows": null,
"result_rows": 1,
"rows_gap": null,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": "12.4억"
},
{
"table": "qa_sheet",
"field": "expected_value",
"agg": "parsed",
"value": 1240000000
},
{
"table": "text2sql_runs",
"field": "result_value",
"agg": null,
"value": 1238000000
}
],
"summary": "정답 12.4억을 숫자로 풀면 12억 4천만 원이고, 결과와 0.16퍼센트 차이라 허용오차 안입니다.",
"hedge": null
},
{
"verdict_no": "QV-006",
"question_id": "Q-071",
"run_no": "RUN-0926-071",
"verdict": "정답과 맞음",
"expected_number": 8450,
"result_value": 8412,
"value_diff_pct": -0.45,
"expected_rows": null,
"result_rows": 1,
"rows_gap": null,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": "8,450개"
},
{
"table": "qa_sheet",
"field": "expected_value",
"agg": "parsed",
"value": 8450
},
{
"table": "text2sql_runs",
"field": "result_value",
"agg": null,
"value": 8412
}
],
"summary": "등록 품목 수가 정답보다 38개 적지만 0.45퍼센트 차이라 허용오차 0.5퍼센트 안에 듭니다.",
"hedge": "허용오차 바로 안쪽이라, 고객사 인수 기준이 더 엄하면 이 질문부터 숫자가 다름으로 넘어갑니다."
},
{
"verdict_no": "QV-007",
"question_id": "Q-078",
"run_no": "RUN-0926-078",
"verdict": "정답과 맞음",
"expected_number": 325000000,
"result_value": 325000000,
"value_diff_pct": 0,
"expected_rows": null,
"result_rows": 1,
"rows_gap": null,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": "3억 2,500만 원"
},
{
"table": "qa_sheet",
"field": "expected_value",
"agg": "parsed",
"value": 325000000
},
{
"table": "text2sql_runs",
"field": "result_value",
"agg": null,
"value": 325000000
}
],
"summary": "정답 '3억 2,500만 원' 을 풀면 325,000,000이고 결과와 같습니다.",
"hedge": null
},
{
"verdict_no": "QV-008",
"question_id": "Q-090",
"run_no": "RUN-0926-090",
"verdict": "정답과 맞음",
"expected_number": null,
"result_value": null,
"value_diff_pct": null,
"expected_rows": 6,
"result_rows": 6,
"rows_gap": 0,
"offending_table": null,
"evidence": [
{
"table": "text2sql_runs",
"field": "referenced_tables",
"agg": null,
"value": [
"erp.po_header"
]
},
{
"table": "qa_sheet",
"field": "expected_rows",
"agg": null,
"value": 6
},
{
"table": "text2sql_runs",
"field": "result_rows",
"agg": null,
"value": 6
}
],
"summary": "SQL 에 erp.po_header 로 적혔지만 접두어를 떼면 카탈로그의 PO_HEADER 이고, 구매가 볼 수 있는 테이블이며 행 수도 6건으로 같습니다.",
"hedge": null
},
{
"verdict_no": "QV-009",
"question_id": "Q-095",
"run_no": "RUN-0926-095",
"verdict": "숫자가 다름",
"expected_number": 42000000,
"result_value": null,
"value_diff_pct": null,
"expected_rows": null,
"result_rows": 1,
"rows_gap": null,
"offending_table": null,
"evidence": [
{
"table": "qa_sheet",
"field": "expected_value",
"agg": null,
"value": "4,200만 원"
},
{
"table": "qa_sheet",
"field": "expected_value",
"agg": "parsed",
"value": 42000000
},
{
"table": "text2sql_runs",
"field": "result_value",
"agg": null,
"value": null
},
{
"table": "text2sql_runs",
"field": "result_rows",
"agg": null,
"value": 1
}
],
"summary": "정답은 4,200만 원인데 SQL 이 한 줄을 돌려주면서 합계 칸이 비어 있어, 견줄 결과 숫자가 없습니다.",
"hedge": "반품 전표를 가르는 조건 값 'RETURN' 이 실제 전표 표기와 달라 걸린 행이 없었을 수 있어, 엔지니어가 SQL 의 조건부터 봅니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 다섯 값 중 하나인지 확인합니다. 2. verdict_no·question_id·run_no·verdict·evidence·summary 가 모두 채워졌는지 확인합니다. 3. expected_number 가 expected_value 에서 천 단위 구분과 단위 접미를 지우고 '억' 은 100000000, '만' 은 10000 을 곱해 더한 수와 같은지 다시 계산합니다. Q-078 의 '3억 2,500만 원' 은 300000000 과 25000000 을 더해 325000000 입니다. evidence 에 agg 가 parsed 인 expected_value 가 있으면 그 값이 expected_number 와 같은지도 봅니다. 3-2. expected_value 가 적혀 있는데 규칙 3으로 숫자가 되지 않는 문항(Q-083 의 '약 3천')과 expected_value·expected_rows 가 둘 다 비어 있는 문항은 판정 줄을 만들지 않습니다. 다섯 판정 어디에도 넣지 않고 '정답을 풀지 못한 문항' 목록에 원문 그대로 따로 세우며, 1주차 끝 결정 관문이 이 목록의 비율을 셉니다. 견줄 정답이 없는 문항을 정답과 맞음으로 세지 않기 위해서입니다. 4. value_diff_pct 가 result_value 에서 expected_number 를 빼고 expected_number 로 나눠 100 을 곱한 값과 같은지 다시 계산합니다. 어느 한쪽이 null 이면 null 인지 봅니다. 5. rows_gap 이 result_rows 에서 expected_rows 를 뺀 값과 같은지 다시 계산합니다. 어느 한쪽이 null 이면 null 인지 봅니다. 6. expected_rows·result_rows 가 0 이상인지, value_diff_pct 가 -100 이상인지 범위를 봅니다. 7. referenced_tables 의 이름을 스키마 접두어를 떼고 대문자로 바꿔도 schema_catalog.table_name 에 없으면 verdict 는 없는 테이블을 씀입니다. Q-090 의 erp.po_header 는 PO_HEADER 가 되어 이 판정에 걸리지 않습니다. 8. referenced_tables 의 어느 테이블이든 그 allowed_roles 에 qa_sheet.asker_role 이 없으면 verdict 는 권한 밖 테이블을 씀입니다. 9. value_diff_pct 의 절댓값이 0.5 보다 크면 verdict 는 숫자가 다름입니다. expected_number 가 있는데 result_value 가 null 이어도 숫자가 다름이고 value_diff_pct 는 null 로 둡니다. Q-095 가 그런 질문으로, 조건에 걸린 행이 없어 SUM 이 빈 값을 돌려줬습니다. 절댓값이 0.5 이하면 이 판정에 걸리지 않습니다. Q-071 이 그런 질문입니다. 10. rows_gap 이 0 이 아니면 verdict 는 행 수가 다름입니다. expected_rows 가 있는데 result_rows 가 null 이어도 행 수가 다름입니다. 11. 판정 우선순위는 없는 테이블을 씀, 권한 밖 테이블을 씀, 숫자가 다름, 행 수가 다름 순이고, 넷에 모두 걸리지 않으면 verdict 는 정답과 맞음입니다. 12. expected_value 가 null 인 질문은 숫자 비교를 건너뛰고 행 수만 견줍니다. Q-047 과 Q-090 이 그런 질문입니다. 13. offending_table 이 없는 테이블을 씀·권한 밖 테이블을 씀에만 채워졌는지, 채워진 값이 그 실행의 referenced_tables 에 실재하는지 대조합니다. 14. evidence 의 field 가 세 데이터셋에 실재하는 컬럼인지 대조합니다. 15. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 줄의 값과 같은지 확인합니다. agg 가 parsed 인 evidence 는 규칙 3의 재계산으로 대신 봅니다. 16. summary 와 hedge 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 1. QA 시트 120문항에 손으로 정답 판정을 달아 CLI 판정과 일치하는 문항 수를 셉니다. 2. 예시 DB 에서 generated_sql 을 다시 실행해 result_value·result_rows 가 재현되는지 봅니다. 이 재실행 결과는 런타임 측정값이라 위 재계산 항목에 넣지 않습니다. [수동 검증 · 사람이 봅니다] 1. 숫자가 다른 질문의 정답을 고칠지 SQL 을 고칠지 고객사 현업과 엔지니어가 함께 정합니다. 2. 권한 밖 판정이 난 질문을 그 역할에 허용할지 고객사 권한 담당이 정합니다.