에이아이구루
경력근거: 온프레미스 환경을 직접 구축하고 운영해본 경험
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고는 필수로 Java 백엔드와 PostgreSQL을 요구하고, 우대에 온프레미스 직접 구축·운영, 오픈소스 LLM 서비스 구축, Kafka 운영, 데이터 모델링을 함께 둡니다. 이 포트폴리오는 그 다섯을 장식이 아니라 골조로 씁니다. Kafka로 들어온 알람을 PostgreSQL 모델로 정규화하고, 그 위에서 오픈소스 모델이 SQL을 쓰며, 폐쇄망 가정으로 외부 호출 없이 도는 구성입니다. 다만 이 포트폴리오가 보여주는 것은 백엔드 설계와 질의 변환 기술이며 반도체 공정 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
반도체 기업과 연간 AX 과제 계약을 맺고 자사 AX 제품을 납품한다고 공고가 밝힙니다. 만드는 대상이 수탁 프로젝트가 아니라 반복 납품되는 제품이라, 고객마다 갈아끼우는 부분과 그대로 두는 코어를 나누는 설계가 그대로 업무가 됩니다.
기획서를 쓰고 코드를 짜고 배포 후 숫자를 확인하는 것이 하나의 흐름이라고 느끼는 사람을 찾습니다. 기능 하나를 프론트부터 백엔드까지 끝까지 책임진 경험을 요구하므로, 조각 기능이 아니라 입력부터 결과 확인까지 닫힌 제품이 어울립니다.
필수는 Java 백엔드와 PostgreSQL이고 우대에 온프레미스 직접 구축·운영, 오픈소스 LLM 서비스 구축, Kafka 같은 오픈소스 플랫폼 운영, 데이터 모델링이 함께 놓입니다. 폐쇄망을 전제로 한 데이터 파이프라인과 모델 연동이 한 제품 안에서 만나는 구성입니다.
설비 담당자가 궁금한 것을 한국어로 물으면 알람 이력에서 답을 찾아 주고, 그 숫자를 만든 알람 행을 같이 보여 줘 담당자가 스스로 확인하게 합니다. 질문을 해석하는 일만 모델이 맡고 답이 무엇인지는 실제 조회 결과가 정합니다.
저는 반도체 라인 설비를 맡고 있습니다. 어젯밤 3호기가 몇 번 멈췄는지, 그게 지난주보다 늘었는지 알고 싶은데 그때마다 데이터 담당자에게 요청하고 반나절을 기다립니다. 숫자만 받아 오면 그게 어느 알람에서 나온 건지 확인할 방법이 없어 결국 다시 묻습니다.
현장 담당자가 조회를 스스로 못 하는 이유는 SQL을 몰라서만이 아니라, 받은 숫자의 출처를 확인할 수 없어 신뢰하지 못하기 때문입니다. 질문을 SQL로 바꿔 주되 실행된 SQL과 근거 행을 함께 보이면, 담당자가 답을 검증하고 다음 질문으로 넘어갑니다.
설비 담당자가 직접 쓰는 화면 하나가 돕니다. 질문을 입력하면 답과 근거가 된 알람 행이 표로 나오고 조회 조건도 함께 보입니다. Kafka 컨슈머로 알람을 적재하는 경로까지 1주차에 넣고, 질문 해석은 상용 모델 API를 가정해 시작합니다. 온프레미스 오픈웨이트 호스팅은 3주차로 미룹니다.
질문 60건에 정답 SQL을 붙인 세트로 결과 집합이 일치하는지 자동 대조하고, 쓰기·전체 스캔 같은 위험 질의를 차단한 기록을 리포트로 냅니다.
오픈웨이트 모델을 로컬에 올려 외부 호출 없이 같은 질문 세트를 통과시키고, 적재부터 조회까지 전 구간이 사내망 가정에서 돕니다.
"3호기가 어젯밤 몇 번 멈췄어" 같은 한국어 질문을 넣으면 읽기 전용 SQL과 답, 근거가 된 알람 행이 함께 나옵니다.
쓰기·DDL·전체 스캔을 유도하는 질의 12종을 넣어도 실행 전에 전부 막히고 거부 사유가 남습니다.
결측 필드와 미등록 설비 코드, 표기가 흔들리는 별칭을 섞은 입력 30종에서도 응답 형식이 깨지지 않고 미매핑으로 표시됩니다.
질문 60건과 정답 SQL을 대조해 결과 집합 일치율을 냅니다. 단, 질문과 정답을 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며, 실제 질의 정확도는 에이아이구루의 실 알람 데이터와 현장 담당자의 실제 질문이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 결측·경계값·미등록 키·표기 흔들림을 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
알람 횟수가 늘었다는 사실이 설비 이상을 뜻하지는 않습니다. 점검 주기나 계측기 교체로도 늘어나므로 조회 결과를 원인 판단으로 읽지 않게 근거 행을 항상 함께 보입니다.
실제 도입에서는 테이블이 두 개가 아니라 수십 개이고 컬럼 이름도 사내 약어입니다. 스키마가 커지면 컨텍스트에 다 넣을 수 없어 테이블 선택 단계가 따로 필요하며, 그것은 도입 단계 과제로 분리합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
설비에서 올라온 알람 이벤트입니다. 질문의 대상이 되는 본 테이블이며 결측과 미등록 설비 코드를 의도적으로 포함합니다.
| event_id | occurred_at | equipment_raw | equipment_id | alarm_type | severity | downtime_min | operator_note |
|---|---|---|---|---|---|---|---|
| ev_10001 | 2026-08-06T22:14:00 | 3호기 | eq_003 | 챔버압력 | 4 | 18.5 | 압력 스파이크 후 자동 복구 |
| ev_10002 | 2026-08-06T23:02:00 | 3 호기 | eq_003 | 파티클 | 3 | 6 | — |
| ev_10003 | 2026-08-07T01:40:00 | CVD-3 | eq_003 | 온도이탈 | 5 | 42 | 히터 교체 후 재가동 |
| ev_10004 | 2026-08-07T02:11:00 | 7호기 | — | 통신두절 | 2 | — | 신규 반입 설비, 마스터 미등록 |
| ev_10005 | 2026-08-07T03:25:00 | 1호기 | eq_001 | 모터과부하 | 0 | 0 | 경보만 발생, 정지 없음 |
아래 JSON Schema에 맞는 설비 알람 이벤트 행을 40건 더 만들어라. {"type":"object","properties":{"event_id":{"type":"string"},"occurred_at":{"type":"string"},"equipment_raw":{"type":"string"},"equipment_id":{"type":["string","null"]},"alarm_type":{"type":"string","enum":["챔버압력","파티클","온도이탈","모터과부하","통신두절"]},"severity":{"type":"integer","minimum":0,"maximum":5},"downtime_min":{"type":["number","null"]},"operator_note":{"type":["string","null"]}},"required":["event_id","occurred_at","equipment_raw","alarm_type","severity"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) downtime_min과 operator_note가 null인 결측 행 8건, (2) severity 0과 5, downtime_min 0과 240 같은 경계값 6건, (3) equipment_id가 null인 미등록 설비 행 6건, (4) 같은 설비를 "3호기"·"3 호기"·"CVD-3"로 다르게 적은 표기 흔들림 8건. 형식을 깨뜨리는 시도 3건은 데이터가 아니라 질의 입력으로 따로 준비하라.{"type":"object","properties":{"event_id":{"type":"string"},"occurred_at":{"type":"string"},"equipment_raw":{"type":"string"},"equipment_id":{"type":["string","null"]},"alarm_type":{"type":"string","enum":["챔버압력","파티클","온도이탈","모터과부하","통신두절"]},"severity":{"type":"integer","minimum":0,"maximum":5},"downtime_min":{"type":["number","null"]},"operator_note":{"type":["string","null"]}},"required":["event_id","occurred_at","equipment_raw","alarm_type","severity"]}설비 코드와 표기 별칭을 담은 마스터입니다. 알람 이벤트의 원문 표기를 정규화 키로 잇는 기준이며, 여기 없는 코드는 미매핑으로 드러납니다.
| equipment_id | display_name | aliases | line | process_step | installed_on |
|---|---|---|---|---|---|
| eq_001 | 1호기 | ["1 호기","DEP-1"] | A라인 | 증착 | 2023-04-01 |
| eq_002 | 2호기 | ["2 호기","ETCH-2"] | A라인 | 식각 | 2023-04-01 |
| eq_003 | 3호기 | ["3 호기","CVD-3"] | A라인 | 증착 | 2024-01-15 |
| eq_004 | 4호기 | ["4 호기"] | B라인 | — | 2024-06-20 |
| eq_005 | 5호기 | ["5 호기","CMP-5"] | B라인 | 연마 | — |
아래 JSON Schema에 맞는 설비 마스터 행을 20건 더 만들어라. {"type":"object","properties":{"equipment_id":{"type":"string"},"display_name":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}},"line":{"type":"string"},"process_step":{"type":["string","null"]},"installed_on":{"type":["string","null"]}},"required":["equipment_id","display_name","aliases","line"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) process_step과 installed_on이 null인 행 5건, (2) aliases가 빈 배열이라 원문 표기로는 절대 안 잡히는 행 3건, (3) 알람 이벤트에 등장하지 않는 설비 행 4건, (4) 같은 공정에 설비가 여럿 묶인 라인 1개. equipment_id는 eq_007처럼 중간 번호를 일부러 비워, 마스터에 없는 코드가 미매핑으로 드러나게 하라.{"type":"object","properties":{"equipment_id":{"type":"string"},"display_name":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}},"line":{"type":"string"},"process_step":{"type":["string","null"]},"installed_on":{"type":["string","null"]}},"required":["equipment_id","display_name","aliases","line"]}| Java | 질의 API와 SQL 실행 게이트를 맡는 백엔드 본체입니다. 생성된 SQL을 파싱해 읽기 전용인지 판정하는 가드도 여기 있습니다. |
| PostgreSQL | 알람 이벤트와 설비 마스터를 담는 저장소입니다. 원문 표기와 정규화 키를 분리한 스키마가 질의 정확도를 좌우합니다. |
| Kafka | 설비에서 올라오는 알람 이벤트를 받는 입구입니다. 1주차부터 컨슈머가 돌아 적재를 맡습니다. |
| AWS | 2주차 대조 리포트와 질의 로그를 S3에 남기고 RDS로 PostgreSQL을 띄워 검증 환경을 재현합니다. |
| LLM | 한국어 질문을 SQL 초안으로 바꾸는 변환만 맡습니다. 정답 판정은 실행 결과가 하므로 모델이 틀려도 근거 행에서 드러납니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 만들어진 SQL이 실제로 맞았는지 정답 세트와 대조하는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 모델을 폐쇄망 안으로 들여 전 구간이 사내망에서 도는 것을 보이는 단계로 확장합니다.
한국어 질문 한 건에 대해 만들어진 SQL과 답, 그리고 그 답의 근거가 된 행을 함께 담습니다. 근거 행을 필수로 두어 숫자만 떠다니지 않게 합니다.
{"type":"object","properties":{"question":{"type":"string"},"generated_sql":{"type":"string"},"read_only":{"type":"boolean"},"answer":{"type":"string"},"evidence_rows":{"type":"array","items":{"type":"object","properties":{"event_id":{"type":"string"},"field":{"type":"string"},"value":{"type":["string","number","null"]}},"required":["event_id","field","value"]}},"unmapped":{"type":"array","items":{"type":"string"}},"hedge":{"type":["string","null"]}},"required":["question","generated_sql","read_only","answer","evidence_rows","unmapped"]}[
{
"question": "3호기가 어젯밤 몇 번 멈췄어?",
"generated_sql": "SELECT count(*) FROM equipment_alarm_events WHERE equipment_id = 'eq_003' AND occurred_at >= '2026-08-06T18:00:00' AND occurred_at < '2026-08-07T06:00:00' AND downtime_min > 0",
"read_only": true,
"answer": "3호기는 어젯밤 3번 멈췄고 정지 시간 합계는 66.5분입니다.",
"evidence_rows": [
{
"event_id": "ev_10001",
"field": "downtime_min",
"value": 18.5
},
{
"event_id": "ev_10002",
"field": "downtime_min",
"value": 6
},
{
"event_id": "ev_10003",
"field": "downtime_min",
"value": 42
}
],
"unmapped": [],
"hedge": null
},
{
"question": "7호기 상태 알려줘",
"generated_sql": "SELECT * FROM equipment_alarm_events WHERE equipment_raw = '7호기' ORDER BY occurred_at DESC LIMIT 20",
"read_only": true,
"answer": "7호기로 올라온 알람은 1건 있으나 설비 마스터에 등록되지 않아 라인과 공정을 붙이지 못했습니다. 통신두절 알람이며 정지 시간은 기록되지 않았습니다.",
"evidence_rows": [
{
"event_id": "ev_10004",
"field": "alarm_type",
"value": "통신두절"
},
{
"event_id": "ev_10004",
"field": "downtime_min",
"value": null
}
],
"unmapped": [
"7호기"
],
"hedge": "마스터 미등록 설비라 집계 대상에서 빠질 수 있습니다. 합성 기준 추정이며 실제 운영에서는 반입 대장과 대조가 필요합니다."
},
{
"question": "알람 테이블 비워줘",
"generated_sql": "DELETE FROM equipment_alarm_events",
"read_only": false,
"answer": "쓰기 질의라 실행하지 않았습니다. 이 조회 경로는 읽기만 허용합니다.",
"evidence_rows": [],
"unmapped": [],
"hedge": "실행 전 게이트에서 차단된 요청입니다."
}
][자동 검증 — 사람 없이 매번 돌립니다] 1. generated_sql이 SELECT로 시작하고 세미콜론 뒤 추가 문장이 없는지 파싱해 확인합니다. 아니면 read_only를 false로 두고 실행하지 않습니다. 2. answer에 등장하는 모든 숫자가 evidence_rows의 value로 설명되는지 대조합니다. 근거 없는 숫자가 있으면 반려합니다. 3. equipment_id가 null인 행이 결과에 포함되면 unmapped 배열에 그 원문 표기가 들어 있는지 확인합니다. 4. 정답 SQL이 있는 질문은 결과 집합(행 집합)이 일치하는지 비교합니다. 문자열이 아니라 집합으로 비교해 컬럼 순서 차이를 오답으로 세지 않습니다. [수동 검증 — 사람이 봐야 합니다] 1. answer 문장이 현장 담당자에게 그대로 읽히는지, 숫자만 나열하고 끝나지 않는지 봅니다. 2. hedge가 붙은 답이 실제로 불확실한 경우인지, 안전하게 보이려고 습관적으로 붙인 것은 아닌지 봅니다.
근거: 온프레미스 환경을 직접 구축하고 운영해본 경험