스프레틱스
경력근거: 회사 내부에서 반복되거나 해결되지 않은 문제를 발견하고 소프트웨어로 해결합니다
공고는 주요 역할 첫 줄에 회사 내부에서 반복되거나 해결되지 않은 문제를 발견하고 소프트웨어로 해결한다고 적고, 웹과 백엔드, API 연동, 업무 자동화를 넘나든다고 이어 씁니다. 스프레틱스는 코딩온으로 K-디지털 트레이닝과 청년취업사관학교 과정을 운영하므로, 사내에서 매주 반복되는 일이 바로 훈련생 출결을 규정에 맞춰 세는 일입니다. 이 포트폴리오는 그 일을 규칙으로 옮기고, 우대사항에 적힌 Docker 와 GitHub Actions, AWS 를 배포 골조로 씁니다. 다만 이 포트폴리오는 풀스택 구현과 규칙 판정 설계를 보여줍니다. 국비 훈련 규정 실무나 개인정보 처리 절차에 대한 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
주요 역할 첫 줄이 회사 내부에서 반복되거나 해결되지 않은 문제를 발견하고 소프트웨어로 해결하는 일입니다. 무엇을 만들지 정하는 것부터 업무에 들어가므로, 요구사항을 받아 구현하는 자리가 아닙니다.
공고는 기획과 개발을 완전히 분리하지 않는다고 못박고, 모호한 아이디어를 사용자 흐름과 기능, 데이터, 정책과 예외 상황으로 구체화하라고 적습니다. 예외 처리를 어디까지 볼지 정하는 판단이 그대로 업무입니다.
AI 가 개발자의 책임을 대신하지 않는다고 적고, 생성한 결과를 그대로 받아들이지 않고 정확성과 품질을 판단하는 능력을 자격요건에 둡니다. 모델을 어디에 쓰고 어디에 쓰지 않을지 가르는 설계가 필요합니다.
과정 운영 담당자가 출결 기록과 과정 규정을 함께 올리면, 규정에 적힌 인정 시간 기준으로 훈련생별 누적 시간을 세어 미달 위험을 사유별로 모아 줍니다. 미달인지 아닌지는 규정 시간과 인정 시간을 비교하는 규칙이 정하고, 모델은 그 사유를 담당자가 읽을 한 줄로 옮기는 일만 맡습니다.
저는 국비 교육 과정 운영을 맡고 있습니다. 매주 출결 파일을 내려받아 엑셀에서 훈련생별 누적 시간을 더해 보는데, 지각과 조퇴가 섞이면 인정 시간을 손으로 깎아야 하고 외출 복귀 시각이 빠진 날은 그냥 결석으로 잡힙니다. 수료가 위태로운 사람을 과정이 거의 끝날 무렵에야 압니다.
수료 미달을 늦게 아는 이유는 계산이 어려워서가 아니라 규정 적용이 사람 손에 남아 있기 때문입니다. 과정 규정을 값으로 두고 출결 기록에 규칙으로 적용해 모자란 시간을 근거와 같이 보여주면, 담당자가 남은 회차 안에 손을 씁니다.
과정 운영 담당자가 쓰는 화면 하나가 돕니다. 출결 기록과 과정 규정을 올리면 과정 코드를 정규화해 붙이고, 규정 시간과 인정 시간을 비교해 판정을 사유별로 근거와 같이 보여줍니다. 파일은 직접 올리는 것으로 시작하고, 새 파일이 올라올 때 자동으로 다시 계산하는 부분은 3주차로 미룹니다.
손으로 매긴 판정 60건과 자동으로 맞춰 보고 어긋난 유형을 리포트로 냅니다. Docker 로 묶어 GitHub Actions 가 규칙 테스트를 돌리고, 2주차부터 AWS 로 배포해 담당자가 사내 주소로 들어와 씁니다.
출결 파일이 새로 올라오면 판정을 다시 계산해 상태가 바뀐 훈련생만 추리고, 미달 위험 명단과 상담 문안 초안을 문서로 내보냅니다. 문자 발송은 발신번호 사전등록이 있어야 해서 범위 밖에 두고, 담당자가 그 문서를 들고 상담합니다.
과정 운영 담당자가 출결 파일과 과정 규정을 올리면 표기가 흔들린 과정 코드도 붙고, 훈련생별 판정이 사유별로 근거와 같이 나옵니다.
열 이름이 빠지거나 시간 칸이 비거나 인코딩이 깨진 파일 12종을 올려도 화면이 죽지 않고, 어느 줄이 왜 걸렸는지 담당자가 그 자리에서 읽습니다.
인정 시간 미기재와 0분 결석, 규정 표에 없는 과정 코드, 같은 회차 중복 업로드, 띄어쓰기가 흔들린 출결 표기를 섞은 입력 30종을 넣어도 응답 형식이 깨지지 않고, 판정하지 못한 건은 사유를 보여줍니다.
손으로 매긴 판정 60건으로 판정 일치율을 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 판정 정확도는 스프레틱스의 실 출결 기록과 실제 과정 규정이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 인정 시간 미기재와 0분 결석, 미등록 과정 코드, 중복 업로드, 표기 흔들림을 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
인정 시간이 모자란다는 사실이 곧 수료 불가를 뜻하지는 않습니다. 공가나 보충 회차로 채워지는 경우가 있어 판정을 결론으로 읽지 않게 근거를 항상 같이 보여줍니다.
실제 도입에서는 과정마다 지각과 조퇴를 인정 시간에 반영하는 방식과 유예 시간이 다릅니다. 과정별 예외가 늘면 규칙을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
출결 기록은 개인을 식별하는 자료라 실제 운영에서는 접근 권한과 보관 기간을 먼저 정해야 합니다. 이 포트폴리오는 가명으로 만든 더미로만 다루고, 실제 훈련생 데이터를 쓰지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
훈련생의 회차별 출결 기록입니다. 판정의 한쪽 축이며 인정 시간 미기재, 0분 결석, 규정 표에 없는 과정 코드, 같은 회차 중복 업로드, 출결 표기 흔들림을 의도적으로 포함합니다.
| log_no | trainee_id | course_code_raw | session_date | scheduled_minutes | attended_minutes | status_raw | status_code | note |
|---|---|---|---|---|---|---|---|---|
| ATT-20260803-101 | TR-101 | KDT-2026-FS | 2026-08-03 | 480 | 480 | 출석 | present | — |
| ATT-20260804-101 | TR-101 | KDT-2026-FS | 2026-08-04 | 480 | 480 | 출석 | present | — |
| ATT-20260807-101 | TR-101 | KDT-2026-FS | 2026-08-07 | 480 | 480 | 출석 | present | — |
| ATT-20260803-102 | TR-102 | KDT-2026-FS | 2026-08-03 | 480 | 420 | 지각 | late | 1시간 지각 |
| ATT-20260804-102 | TR-102 | KDT-2026-FS | 2026-08-04 | 480 | 480 | 출석 | present | — |
아래 JSON Schema 에 맞는 출결 기록 행을 더 만들어 주세요. {"type":"object","properties":{"log_no":{"type":"string"},"trainee_id":{"type":"string"},"course_code_raw":{"type":"string"},"session_date":{"type":"string"},"scheduled_minutes":{"type":"integer"},"attended_minutes":{"type":["integer","null"]},"status_raw":{"type":"string"},"status_code":{"type":["string","null"],"enum":["present","late","early_leave","outing","absent","excused",null]},"note":{"type":["string","null"]}},"required":["log_no","trainee_id","course_code_raw","session_date","scheduled_minutes","status_raw"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) attended_minutes 가 null 인 복귀 시각 미기재 건, (2) attended_minutes 가 0 인 전일 결석 건과 예정 시간을 꽉 채운 최대 건, (3) course_code_raw 가 과정 규정 표에 없는 신규 과정, (4) 같은 log_no 가 두 행에 있는 재업로드 건, (5) status_raw 에 띄어쓰기나 영문 표기가 섞여 status_code 가 null 인 건. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"log_no":{"type":"string"},"trainee_id":{"type":"string"},"course_code_raw":{"type":"string"},"session_date":{"type":"string"},"scheduled_minutes":{"type":"integer"},"attended_minutes":{"type":["integer","null"]},"status_raw":{"type":"string"},"status_code":{"type":["string","null"],"enum":["present","late","early_leave","outing","absent","excused",null]},"note":{"type":["string","null"]}},"required":["log_no","trainee_id","course_code_raw","session_date","scheduled_minutes","status_raw"]}과정별 수료 인정 기준입니다. 판정의 다른 쪽 축이며 지각 유예 기준이 비어 있는 과정과 전 회차 출석을 요구하는 단기 과정을 포함합니다.
| course_code | course_name | total_minutes | required_minutes | min_attendance_pct | late_grace_minutes | unit_minutes | funding_type | note |
|---|---|---|---|---|---|---|---|---|
| KDT-2026-FS | K-디지털 트레이닝 풀스택 웹 개발 6기 | 4,800 | 3,840 | 80 | 30 | 480 | kdt | — |
| SESAC-2026-AI | 청년취업사관학교 AI 서비스 개발 3기 | 3,600 | 2,880 | 80 | 30 | 360 | sesac | — |
| KDT-2026-BE | K-디지털 트레이닝 백엔드 심화 2기 | 4,320 | 3,456 | 80 | 30 | 480 | kdt | — |
| SESAC-2026-DATA | 청년취업사관학교 데이터 분석 5기 | 3,240 | 2,592 | 80 | 20 | 360 | sesac | — |
| KDT-2026-CLOUD | K-디지털 트레이닝 클라우드 인프라 1기 | 4,800 | 3,840 | 80 | — | 480 | kdt | 지각 유예 기준 미기재 |
아래 JSON Schema 에 맞는 과정 규정 행을 더 만들어 주세요. {"type":"object","properties":{"course_code":{"type":"string"},"course_name":{"type":"string"},"total_minutes":{"type":"integer"},"required_minutes":{"type":"integer"},"min_attendance_pct":{"type":"number"},"late_grace_minutes":{"type":["integer","null"]},"unit_minutes":{"type":"integer"},"funding_type":{"type":"string","enum":["kdt","sesac","corporate"]},"note":{"type":["string","null"]}},"required":["course_code","course_name","total_minutes","required_minutes","min_attendance_pct","funding_type"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) late_grace_minutes 가 null 인 지각 유예 기준 미기재 과정, (2) required_minutes 가 total_minutes 와 같아 전 회차 출석을 요구하는 단기 과정. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"course_code":{"type":"string"},"course_name":{"type":"string"},"total_minutes":{"type":"integer"},"required_minutes":{"type":"integer"},"min_attendance_pct":{"type":"number"},"late_grace_minutes":{"type":["integer","null"]},"unit_minutes":{"type":"integer"},"funding_type":{"type":"string","enum":["kdt","sesac","corporate"]},"note":{"type":["string","null"]}},"required":["course_code","course_name","total_minutes","required_minutes","min_attendance_pct","funding_type"]}| TypeScript | 판정 규칙과 화면을 같은 언어로 씁니다. 규정 값과 출결 상태 코드를 타입으로 묶어 잘못된 조합이 컴파일에서 걸립니다. |
| React | 과정 운영 담당자가 실제로 보는 판정 화면입니다. 사유별 묶음과 근거를 한 화면에 둡니다. |
| Node.js | 파일 업로드와 판정 실행, 결과 조회를 맡는 API 서버입니다. 규칙 실행은 이쪽 한 곳에만 둡니다. |
| PostgreSQL | 출결 기록과 과정 규정을 담는 저장소입니다. 원문 과정 코드와 정규화 키를 나눈 스키마가 미매핑 판정을 좌우합니다. |
| Git | 과정별 규정 값을 코드가 아니라 설정 파일로 두고, 규정이 바뀐 이력을 커밋으로 남깁니다. |
| Docker | 개발과 배포 환경을 같은 이미지로 맞춥니다. 판정 엔진과 API 서버, 데이터베이스를 한 컴포즈로 띄웁니다. |
| GitHub Actions | 규칙 테스트를 커밋마다 돌리고 통과한 것만 배포합니다. 규정 값을 고쳤을 때 기존 판정이 깨지는지 여기서 걸립니다. |
| AWS | 2주차부터 배포와 운영을 맡습니다. 업로드한 원본을 S3 에 남기고 RDS 로 PostgreSQL 을 띄웁니다. |
| Claude Code | 규칙 구현과 테스트 작성을 맡깁니다. 생성된 판정 코드는 손으로 매긴 정답 세트로 되돌려 확인하고, 어긋나면 규칙을 직접 고칩니다. |
| LLM | 판정 코드가 정해진 뒤 그 사유를 담당자가 읽을 한 문장으로 옮기는 일만 맡습니다. 미달인지 아닌지는 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 판정이 실제로 맞았는지 손으로 매긴 정답 세트와 맞춰 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 새 파일이 올라올 때 다시 계산하고 상담할 명단을 문서로 내는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · trainee_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
on_track여유 있음at_risk미달 위험already_failed미달 확정hours_missing시간 미기재unmapped_course과정 규정 없음duplicate_log중복 기록status_unmapped상태 표기 미매핑훈련생 한 명의 수료 요건 충족 여부를 판정 코드와 근거로 남기는 형식입니다. 판정은 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"check_id":{"type":"string"},"trainee_id":{"type":"string"},"course_code":{"type":["string","null"]},"verdict":{"type":"string","enum":["on_track","at_risk","already_failed","hours_missing","unmapped_course","duplicate_log","status_unmapped"]},"shortfall_minutes":{"type":["integer","null"]},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{"type":["string","integer","number","null"]}},"required":["field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["check_id","trainee_id","verdict","evidence","summary"]}[
{
"check_id": "chk_0001",
"trainee_id": "TR-101",
"course_code": "KDT-2026-FS",
"verdict": "on_track",
"shortfall_minutes": null,
"evidence": [
{
"field": "required_minutes",
"value": 3840
},
{
"field": "attended_minutes",
"agg": "sum",
"value": 4080
}
],
"summary": "인정 시간이 수료 기준을 240분 넘겨 여유가 있습니다.",
"hedge": null
},
{
"check_id": "chk_0002",
"trainee_id": "TR-102",
"course_code": "KDT-2026-FS",
"verdict": "on_track",
"shortfall_minutes": null,
"evidence": [
{
"field": "required_minutes",
"value": 3840
},
{
"field": "attended_minutes",
"agg": "sum",
"value": 3840
},
{
"field": "late_grace_minutes",
"value": 30
}
],
"summary": "인정 시간이 수료 기준과 정확히 같아 지금은 통과이지만 남은 회차에 여유가 없습니다.",
"hedge": null
},
{
"check_id": "chk_0003",
"trainee_id": "TR-103",
"course_code": "KDT-2026-FS",
"verdict": "at_risk",
"shortfall_minutes": 780,
"evidence": [
{
"field": "required_minutes",
"value": 3840
},
{
"field": "attended_minutes",
"agg": "sum",
"value": 3060
},
{
"field": "log_no",
"value": "ATT-20260803-103"
}
],
"summary": "인정 시간이 수료 기준보다 780분 모자라 남은 회차를 모두 출석해야 채워집니다.",
"hedge": null
},
{
"check_id": "chk_0004",
"trainee_id": "TR-104",
"course_code": "KDT-2026-FS",
"verdict": "already_failed",
"shortfall_minutes": 1440,
"evidence": [
{
"field": "required_minutes",
"value": 3840
},
{
"field": "attended_minutes",
"agg": "sum",
"value": 2400
},
{
"field": "log_no",
"value": "ATT-20260805-104"
}
],
"summary": "남은 회차를 모두 출석해도 1,440분이 모자라 수료 기준에 닿지 않습니다.",
"hedge": "합성 기준 판정이라 실제로는 공가나 보충 회차로 채워질 수 있습니다."
},
{
"check_id": "chk_0005",
"trainee_id": "TR-105",
"course_code": "KDT-2026-FS",
"verdict": "hours_missing",
"shortfall_minutes": null,
"evidence": [
{
"field": "required_minutes",
"value": 3840
},
{
"field": "attended_minutes",
"value": null
},
{
"field": "log_no",
"value": "ATT-20260805-105"
}
],
"summary": "외출 복귀 시각이 비어 있어 인정 시간을 계산하지 못했습니다.",
"hedge": "출결 담당자가 복귀 시각을 채워야 판정이 확정됩니다."
},
{
"check_id": "chk_0006",
"trainee_id": "TR-107",
"course_code": null,
"verdict": "unmapped_course",
"shortfall_minutes": null,
"evidence": [
{
"field": "course_code_raw",
"value": "KDT-2026-DA"
},
{
"field": "log_no",
"value": "ATT-20260807-107"
}
],
"summary": "출결 기록의 과정 코드가 규정 표에 없어 수료 기준을 적용하지 못했습니다.",
"hedge": null
},
{
"check_id": "chk_0007",
"trainee_id": "TR-106",
"course_code": "SESAC-2026-AI",
"verdict": "duplicate_log",
"shortfall_minutes": null,
"evidence": [
{
"field": "log_no",
"value": "ATT-20260806-106"
},
{
"field": "log_no",
"agg": "count",
"value": 2
}
],
"summary": "같은 회차 기록이 두 번 올라와 인정 시간이 두 배로 잡힙니다.",
"hedge": null
},
{
"check_id": "chk_0008",
"trainee_id": "TR-106",
"course_code": "SESAC-2026-AI",
"verdict": "status_unmapped",
"shortfall_minutes": null,
"evidence": [
{
"field": "status_raw",
"value": "지 각"
},
{
"field": "status_code",
"value": null
}
],
"summary": "출결 상태 표기가 규정 코드에 붙지 않아 인정 시간을 깎을 기준을 정하지 못했습니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 enum 일곱 값 중 하나인지 확인합니다. 규칙이 정한 코드라 모델이 새 코드를 만들면 그 응답을 버립니다. 2. 필수 필드 check_id·trainee_id·verdict·evidence·summary 가 모두 있는지 확인합니다. 3. shortfall_minutes 가 0 이상인지 범위를 확인합니다. 4. shortfall_minutes 가 required_minutes 와 인정 시간 합의 차이와 일치하는지 다시 계산합니다. 5. evidence 의 field 이름이 출결 기록과 과정 규정의 컬럼에 실재하는지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 만든 기본 문장으로 되돌립니다. 6. evidence 의 value 가 수록된 행의 원문과 같은지 확인합니다. 여러 행에서 계산한 수치는 agg 를 적었는지만 봅니다. 7. summary 가 존댓말 한 문장으로 끝나는지 확인합니다. 8. attended_minutes 가 null 이면 verdict 는 hours_missing 이고 shortfall_minutes 는 null 입니다. 조건이 성립하는 chk_0005 와 성립하지 않는 chk_0003 을 samples 에 함께 넣었습니다. [평가 러너 · 2주차] 1. 손으로 매긴 판정 60건과 비교해 verdict 일치율을 냅니다. 2. 어긋난 건을 판정 코드별로 묶어 어느 코드에서 규칙이 흔들리는지 리포트로 냅니다. [수동 검증 · 사람이 봅니다] 1. 판정 코드가 사람이 보기에도 그 코드가 맞는지 표본 20건을 눈으로 확인합니다. 2. 같은 회차가 중복이면서 상태 표기도 흔들린 건처럼 한 건이 두 코드에 걸릴 때 어느 쪽으로 묶는 것이 담당자에게 편한지 판단합니다.