스켈터랩스
경력근거: 추상적인 비즈니스 요구사항을 명확한 기술 스펙으로 구체화하고
스켈터랩스는 고객사에 파견되어 자연어 검색과 Text2SQL 파이프라인을 짜는 일을 이 포지션에 맡깁니다. 그 파이프라인은 질문이 데이터에 닿을 때만 의미가 있어서, 어떤 질문이 지금 스키마로 닿지 못하는지를 먼저 나눠야 합니다. 이 도구는 그 앞단을 다룹니다. 이 포트폴리오는 질문을 컬럼 요구로 분해하고 매핑 여부를 규칙으로 판정하는 기술을 보여줍니다. 온톨로지 체계나 지식 그래프 설계 전문성은 주장하지 않습니다. 그 체계는 여러 사람이 합의해야 서는 것이라 한 사람이 1주에 만들 수 있는 층이 아니고, 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고가 이 포지션의 첫 일로 고객의 추상적인 비즈니스 요구사항을 명확한 기술 스펙으로 바꾸는 것을 듭니다. 질문 하나하나가 어떤 테이블과 컬럼을 필요로 하는지 적어 내려가는 것이 그 작업의 실물입니다.
주요 업무에 자연어 검색과 Text2SQL을 활용한 데이터 파이프라인 설계가 들어 있습니다. 질문을 컬럼 요구로 분해하는 일은 그 파이프라인을 짜기 전에 반드시 하는 앞단입니다.
자격 요건에 고객사 파견 업무 수행이 명시돼 있습니다. 현장에서 받는 자료는 정리된 API가 아니라 현업이 쓴 질문 목록과 테이블 명세라, 그 형태를 그대로 입력으로 받습니다.
현업이 모아 준 질문 목록과 고객사 테이블 명세를 대조해, 지금 데이터로 답이 되는 질문과 막힌 질문을 사유별로 나눠 보여주는 관리자 페이지입니다. 판정은 컬럼 매핑 규칙으로 정합니다.
고객사에 처음 들어간 담당자입니다. 현업이 '이런 것도 물어볼 수 있나요' 하고 서른 개쯤 던지면, 그중 무엇이 지금 테이블로 답이 되고 무엇이 안 되는지를 명세서를 넘겨 가며 하나씩 맞춰 보느라 첫 주를 씁니다.
질문에 쓰인 용어를 컬럼으로 매핑할 수 있으면, 어느 질문이 답이 되고 어느 질문이 왜 막히는지는 사람이 명세서를 넘기지 않아도 규칙으로 갈라집니다.
질문 목록과 테이블 명세를 올리면 사유별로 나눠 보여주는 화면입니다. 벡터 검색이나 실제 SQL 실행은 두지 않고, 용어와 컬럼의 매핑 사전으로만 판정합니다.
현업이 직접 표시한 정답표와 판정을 맞춰 사유별로 어긋난 건을 세는 평가 러너입니다.
막힌 질문을 모아 필요한 컬럼과 확인 질문을 정리한 요구사항 문서 생성기입니다.
질문 30건과 테이블 명세를 올려 사유별로 갈라진 화면이 나오는 것을 시연합니다.
질문에 결측·중복·용어 흔들림·명세에 없는 용어가 섞여 있어도 화면 형식이 깨지지 않고, 판정하지 못한 질문은 사유가 붙어 남습니다.
용어가 명세에 없으면 답이 된다고 단정하지 않고 미매핑으로 남깁니다. 없는 컬럼을 지어내지 않습니다.
현업 정답표와 맞춰 사유별로 어긋난 건을 셉니다. 단, 질문과 정답을 모두 작성자가 만든 합성 기준이라 성능 측정이 아니라 데모 조건 동작입니다.
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 더미데이터 자가채점이며, 실제 판정 정확도는 스켈터랩스가 파견 나간 고객사의 실제 질문과 실제 스키마가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 결측·중복 질문·용어 흔들림·명세에 없는 용어를 더미에 일부러 섞어 쉬운 문제만 푸는 것을 피합니다.
컬럼이 있다고 해서 질문에 답이 된다고 단정할 수 없습니다. 값의 세밀도가 질문과 어긋나는 경우가 있어 그 갈래를 따로 두고, 최종 판단은 현업이 합니다.
실제 도입하면 고객사마다 용어 사전이 없는 상태에서 시작합니다. 사전을 처음 채우는 일이 도입 단계의 실제 과제이고, 그 작업 자체는 이 도구가 대신하지 못합니다.
온톨로지 체계 설계는 이 도구의 범위 밖입니다. 여러 부서가 합의해야 서는 결과물이라 한 사람이 1주에 만들 수 있는 층이 아닙니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사 현업이 파견 첫 주에 모아 준 질문 목록입니다. 같은 질문을 두 부서가 따로 올린 중복과, 용어를 다르게 적은 흔들림을 일부러 섞었습니다.
| question_no | asked_by_team | question_raw | term_raw | expected_output | granularity_asked |
|---|---|---|---|---|---|
| Q-001 | 영업 | 이번 달 지점별 매출액이 얼마인가요 | 매출액 | 지점별 합계 금액 | monthly |
| Q-002 | 영업 | 어제 주문 건수가 몇 건인가요 | 주문 건수 | 건수 | daily |
| Q-003 | 물류 | 지연된 배송이 몇 건인가요 | 배송 지연 | 건수 | daily |
| Q-004 | 물류 | 반품 사유별로 나눠 볼 수 있나요 | 반품 사유 | 사유별 건수 | monthly |
| Q-005 | 마케팅 | 고객 이탈률이 어떻게 되나요 | 이탈률 | — | monthly |
아래 JSON Schema에 맞는 현업 질문 행을 더 만들어 주세요. {"type":"object","properties":{"question_no":{"type":"string"},"asked_by_team":{"type":"string"},"question_raw":{"type":"string"},"term_raw":{"type":"string"},"expected_output":{"type":["string","null"]},"granularity_asked":{"type":["string","null"],"enum":["daily","monthly","per_order",null]}},"required":["question_no","asked_by_team","question_raw","term_raw","expected_output","granularity_asked"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라. 첫째 결측으로 expected_output이 null인 질문(현업이 원하는 형태를 안 적음), 둘째 표기흔들림으로 같은 대상을 매출액과 매출처럼 다르게 적은 질문 쌍, 셋째 중복으로 같은 question_no가 두 행에 있는 재제출 건, 넷째 미매핑으로 명세 어디에도 없는 용어를 쓴 질문. 또한 형식 깨뜨리기 시도 3건을 별도 파일로 만들어 granularity_asked에 enum 밖 값을 넣거나 required 필드를 빼서 스키마 검증이 걸러내는지 확인하게 해 주세요.{"type":"object","properties":{"question_no":{"type":"string"},"asked_by_team":{"type":"string"},"question_raw":{"type":"string"},"term_raw":{"type":"string"},"expected_output":{"type":["string","null"]},"granularity_asked":{"type":["string","null"],"enum":["daily","monthly","per_order",null]}},"required":["question_no","asked_by_team","question_raw","term_raw","expected_output","granularity_asked"]}고객사가 넘겨준 테이블 명세입니다. 질문의 용어를 이 목록과 대조해 답이 되는지 판정합니다. 세밀도가 질문과 어긋나는 컬럼을 일부러 넣었습니다.
| table_name | column_name | term_alias | data_type | granularity | is_nullable |
|---|---|---|---|---|---|
| sales_daily | amount | 매출액 | number | daily | false |
| sales_daily | branch_id | 지점 | string | daily | false |
| orders | order_no | 주문 건수 | string | per_order | false |
| orders | ordered_at | 주문 일시 | datetime | per_order | false |
| delivery | delay_flag | 배송 지연 | boolean | per_order | false |
아래 JSON Schema에 맞는 고객사 테이블 명세 행을 더 만들어 주세요. {"type":"object","properties":{"table_name":{"type":"string"},"column_name":{"type":"string"},"term_alias":{"type":["string","null"]},"data_type":{"type":"string"},"granularity":{"type":"string","enum":["daily","monthly","per_order"]},"is_nullable":{"type":"boolean"}},"required":["table_name","column_name","term_alias","data_type","granularity","is_nullable"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라. 첫째 결측으로 term_alias가 null인 컬럼(현업 용어가 붙지 않은 것), 둘째 세밀도 어긋남으로 질문은 일자별을 묻는데 월별로만 있는 컬럼, 셋째 같은 뜻인데 표기가 다른 별칭. 또한 형식 깨뜨리기 시도 3건을 별도 파일로 만들어 granularity에 enum 밖 값을 넣어 스키마 검증이 걸러내는지 확인하게 해 주세요.{"type":"object","properties":{"table_name":{"type":"string"},"column_name":{"type":"string"},"term_alias":{"type":["string","null"]},"data_type":{"type":"string"},"granularity":{"type":"string","enum":["daily","monthly","per_order"]},"is_nullable":{"type":"boolean"}},"required":["table_name","column_name","term_alias","data_type","granularity","is_nullable"]}| Python | 질문과 명세를 읽어 매핑 규칙을 돌리는 판정 본체입니다. |
| TypeScript | 판정 결과를 사유별로 세워 보여주는 관리자 화면입니다. |
| Text2SQL | 질문이 어떤 테이블과 컬럼을 필요로 하는지 분해하는 기준으로 삼습니다. 실제 SQL 실행은 1주차 범위 밖입니다. |
| Knowledge Graph / Ontology | 용어와 컬럼을 잇는 매핑 사전의 형태로만 씁니다. 체계 설계는 다루지 않습니다. |
| Elasticsearch | 3주차에 용어 사전이 커졌을 때 유사 표기를 찾는 후보 검색기로 둡니다. |
| Langgraph | 질문 분해를 여러 단계로 나눌 때 각 단계의 출력 형식을 강제하는 오케스트레이션에 씁니다. |
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 판정이 실제로 맞았는지 현업 답과 맞춥니다.
✓ pass → 대조까지가 한 덩어리입니다. 더 키우려면 막힌 질문을 요구사항 문서로 정리해 냅니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · question_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
answerable답 가능term_unmapped용어 미매핑granularity_mismatch세밀도 불일치output_unclear결과 형태 불명duplicate_question중복 질문질문 한 건의 판정 결과입니다. 지금 데이터로 답이 되는지, 안 되면 무엇이 없어서 막혔는지를 근거와 함께 담습니다.
{"type":"object","properties":{"question_no":{"type":"string"},"verdict":{"type":"string","enum":["answerable","term_unmapped","granularity_mismatch","output_unclear","duplicate_question"]},"matched_columns":{"type":"array","items":{"type":"string"}},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{},"quote":{"type":["string","null"]}},"required":["field","value"]}},"next_action":{"type":"string"}},"required":["question_no","verdict","matched_columns","evidence","next_action"]}[
{
"question_no": "Q-001",
"verdict": "answerable",
"matched_columns": [
"sales_daily.amount",
"sales_daily.branch_id"
],
"evidence": [
{
"field": "term_raw",
"value": "매출액",
"quote": "매출액"
},
{
"field": "term_alias",
"value": "매출액"
},
{
"field": "column_name",
"value": "amount"
}
],
"next_action": "지점별 합계로 바로 답이 됩니다. 이 질문을 1차 구현 대상에 넣습니다."
},
{
"question_no": "Q-004",
"verdict": "answerable",
"matched_columns": [
"returns.reason_code"
],
"evidence": [
{
"field": "term_raw",
"value": "반품 사유",
"quote": "반품 사유"
},
{
"field": "term_alias",
"value": "반품 사유"
},
{
"field": "column_name",
"value": "reason_code"
}
],
"next_action": "사유별 건수로 답이 됩니다. 이 질문을 1차 구현 대상에 넣습니다."
},
{
"question_no": "Q-006",
"verdict": "answerable",
"matched_columns": [
"sales_daily.amount",
"sales_daily.branch_id"
],
"evidence": [
{
"field": "term_raw",
"value": "매출",
"quote": "매출"
},
{
"field": "term_alias",
"value": "매출액"
}
],
"next_action": "매출이라고 적혔지만 매출액과 같은 컬럼으로 이어집니다. Q-001과 묶어 한 번만 구현합니다."
},
{
"question_no": "Q-005",
"verdict": "term_unmapped",
"matched_columns": [],
"evidence": [
{
"field": "term_raw",
"value": "이탈률",
"quote": "이탈률"
}
],
"next_action": "이탈률에 해당하는 컬럼이 명세에 없습니다. 어떤 기준으로 이탈을 보는지 현업에 먼저 묻습니다."
},
{
"question_no": "Q-015",
"verdict": "term_unmapped",
"matched_columns": [],
"evidence": [
{
"field": "term_raw",
"value": "기사 처리 건수",
"quote": "기사 처리 건수"
},
{
"field": "term_alias",
"value": null
}
],
"next_action": "driver_id에 현업 용어가 붙어 있지 않습니다. 이 컬럼이 기사와 같은 것인지 확인하고 사전에 추가합니다."
},
{
"question_no": "Q-014",
"verdict": "granularity_mismatch",
"matched_columns": [
"settlement_monthly.fee_amount"
],
"evidence": [
{
"field": "granularity_asked",
"value": "daily"
},
{
"field": "granularity",
"value": "monthly"
},
{
"field": "column_name",
"value": "fee_amount"
}
],
"next_action": "수수료가 월별로만 있어 일자별로는 답이 되지 않습니다. 일자별 원천이 있는지 확인합니다."
},
{
"question_no": "Q-010",
"verdict": "output_unclear",
"matched_columns": [],
"evidence": [
{
"field": "term_raw",
"value": "재구매",
"quote": "재구매"
},
{
"field": "expected_output",
"value": null
}
],
"next_action": "원하는 결과 형태가 비어 있습니다. 비중을 무엇으로 나눈 값인지 현업에 묻습니다."
},
{
"question_no": "Q-003",
"verdict": "duplicate_question",
"matched_columns": [
"delivery.delay_flag"
],
"evidence": [
{
"field": "question_no",
"agg": "count",
"value": 2
}
],
"next_action": "물류와 CS가 같은 질문을 따로 올렸습니다. 한 건으로 합쳐 구현하고 두 부서에 같은 화면을 엽니다."
}
][자동 검증 · 1주차] 1. verdict가 enum 다섯 값 안에 있는지 확인합니다. 2. question_no·verdict·matched_columns·evidence·next_action이 모두 있는지 확인합니다. 3. verdict가 answerable이면 matched_columns가 비어 있지 않은지 확인합니다. 4. verdict가 term_unmapped이면 matched_columns가 비어 있는지 확인합니다. 5. evidence의 field가 두 입력 표의 컬럼에 실재하는지 확인합니다. 6. evidence의 quote가 그 질문 term_raw의 일부인지 확인합니다. 7. next_action이 존댓말로 끝나는지 확인합니다. [수동 검증] 1. 막힌 사유가 실제로 그 질문이 막힌 이유인지 현업이 읽어 확인합니다. 2. Q-002·Q-013처럼 용어가 명세와 그대로 맞아떨어진 질문은 사유를 달지 않는 것이 맞는지 확인합니다. [평가 러너 · 2주차] 1. 현업이 표시한 정답표와 verdict를 맞춰 사유별로 어긋난 건을 셉니다. 2. 질문과 정답을 작성자가 만든 합성 기준이라 이 수치는 성능 측정이 아니라 데모 조건 동작입니다.