YBIGTA SQL 3주차 과제용 셀프체크 도구. 학회원이 쿼리 · 실행 결과 · 인사이트를 넣으면 GPT-5.6 Terra가 루브릭에 따라 점수와 구체적 피드백을 준다.
설계 전제: 이 점수는 성적이 아니고 통과 기준도 아니다. 제출 전 스스로 점검하는 도구이고, UI 문구도 전부 그렇게 잡혀 있다. 점수를 게이트로 쓰면 학회원이 데이터가 아니라 채점기를 최적화하게 된다.
| 파일 | 역할 |
|---|---|
app.py |
Streamlit UI, API 호출, 에러 처리 |
rubric.py |
채점 기준·프롬프트·출력 스키마. 루브릭 수정은 여기만 고치면 된다. |
.streamlit/secrets.toml.example |
시크릿 템플릿 |
cd "인사이트_셀프체크"
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .streamlit/secrets.toml.example .streamlit/secrets.toml
# secrets.toml 을 열어 OPENAI_API_KEY 와 ACCESS_CODE 를 채운다
streamlit run app.pyAPI 키는 https://platform.openai.com/api-keys 에서 발급하고, Billing 에서 크레딧을 먼저
충전해야 호출이 된다 (충전 전에는 429 insufficient_quota 가 뜬다).
- 이 폴더를 GitHub 리포로 올린다.
secrets.toml은 올라가면 안 된다 —.gitignore에 이미 들어 있지만git status로 한 번 확인할 것. - https://share.streamlit.io → New app → 리포와
app.py선택 - Advanced settings → Secrets 에 아래를 붙여넣는다:
로컬
OPENAI_API_KEY = "sk-..." ACCESS_CODE = "학회에_공유할_코드"
secrets.toml은 배포되지 않는다. 여기에 따로 넣어야 한다. - Deploy. URL이 나오면 학회 공지에 코드와 함께 올린다.
무료 티어는 트래픽이 12시간 없으면 앱을 잠재운다. 다음 방문자는 "Yes, get this app back up!" 화면을 보고 30초쯤 기다려야 한다. 코드 문제가 아니라 플랫폼 정책이다.
.github/workflows/keep-awake.yml 이 6시간마다 실제 브라우저로 접속해 이를 막는다.
쓰려면 리포 Settings → Secrets and variables → Actions 에
APP_URL 시크릿(배포된 앱 주소)을 넣으면 된다. Actions 탭에서 수동 실행도 된다.
UptimeRobot 같은 단순 GET 핑거는 통하지 않는다. 잠든 앱도 GET 에는 200 을 돌려주므로 트래픽으로 집계되지 않는다. 실제 브라우저 세션(웹소켓)이 붙어야 해서 Playwright 를 쓴다.
워크플로를 안 돌릴 거면 공지에 "잠들어 있으면 버튼 눌러 30초만 기다려주세요" 한 줄이면 충분하다. 과제 기간에는 12시간 안에 누군가 쓸 가능성이 높다.
Community Cloud 앱은 URL만 알면 누구나 들어온다. API 키가 발제자 것이므로
최소한의 문지기가 필요하다. ACCESS_CODE 를 비우면 문지기 없이 동작한다.
추가 안전장치로 세션당 채점 20회 제한과 입력 길이 제한이 걸려 있다.
제출마다 이름 · 시각 · 총점 · 항목별 점수 · 질문 · 쿼리 · 실행 결과 · 인사이트 · 쿼리 번역문 · 호출 비용이 한 줄씩 기록된다.
앱 안에는 기록에 대한 고지를 두지 않았다. "기록됩니다"라는 문구가 감시처럼 읽혀 제출을 위축시키기 때문이다 — 이 도구는 여러 번 돌려야 값을 하는데 그 문구가 정확히 그걸 막는다. 제출물을 발제자가 본다는 건 과제의 전제이기도 하다.
그래도 한 줄은 남기는 편이 깔끔하므로, 공지 문구에 "제출 내용은 발제자가 확인합니다" 정도를 넣는 것을 권한다 (아래 공지 예시에 포함해 두었다).
기록되는 곳은 두 군데다.
| 대상 | 언제 쓰나 | 지속성 |
|---|---|---|
submissions.jsonl |
항상 | 컨테이너가 새로 만들어지면 사라진다 (아래 참고) |
LOG_WEBHOOK_URL |
시크릿이 있을 때만 | 영구 |
코드만 바꿔 푸시하는 경우에는 사라지지 않는다. Streamlit Cloud 는 그때 컨테이너를 다시 만들지 않고 스크립트만 다시 실행한다(핫 리로드). 파일이 살아남는다.
컨테이너가 새로 만들어질 때만 사라진다.
| 상황 | 로그 |
|---|---|
| 코드만 푸시 (핫 리로드) | 남음 |
requirements.txt 변경 |
사라짐 (의존성 재설치 = 재빌드) |
| 수동 Reboot | 사라짐 |
| 잠들었다 깨어남 | 사라짐 — keep-awake 워크플로가 이걸 막는다 |
| 플랫폼 자체 재시작 | 사라짐 |
즉 실질적인 위험은 의존성 변경 · 수동 Reboot · 플랫폼 재시작 셋이다. 앞의 둘은 예측 가능하니 그 전에 CSV 를 받으면 되고, 마지막 하나 때문에 가끔 받아두는 습관이 필요하다.
ADMIN_CODE 를 시크릿에 넣으면 사이드바에 발제자 코드 입력칸이 생긴다. 맞게 넣으면
본문 하단에 예산 소진율 · 제출 표 · JSONL 내려받기가 나온다. ADMIN_CODE 를 비워두면
패널 자체가 뜨지 않으므로, 학회원에게는 ACCESS_CODE 만 알려주면 서로의 제출물을 볼 수 없다.
(두 코드를 다른 값으로 정할 것)
의도적으로 감췄다. 금액이 보이면 학회원이 재시도를 아낀다. 이 도구의 가치는 고쳐서 다시 돌리는 반복에 있고 UI 문구도 "몇 번이든 다시 돌려도 됩니다"라고 말하는데, 옆에 돈이 떠 있으면 그 말이 무력해진다. 퍼센트로 바꾸면 더 나쁘다 ("내가 8% 썼네" → 눈치). 게다가 학회원이 보는 값은 자기 세션분뿐이라 전체 예산과 무관한, 죄책감만 주는 숫자다.
학회원에게는 세션당 20회 카운터만 보인다. 횟수 제한은 금액과 달리 공정하고 눈치볼 게 없다.
금액은 발제자 패널에서 로그 전체를 합산해 보여준다 (BUDGET_USD 기준 소진율 · 회당 평균 ·
남은 예산으로 몇 회 더 가능한지). 단 로그가 초기화되면 합계도 초기화되므로 정확한 잔액은
OpenAI 대시보드가 기준이다.
Community Cloud 무료 티어는 트래픽이 없으면 앱을 잠재우고, 깨어날 때 프로세스가 재시작되면서 파일이 초기화된다. 제출물을 남기려면 외부로 흘려보내야 한다.
-
새 Google Sheets 를 만든다
-
확장 프로그램 → Apps Script 에 아래를 붙여넣는다
function doPost(e) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; const row = JSON.parse(e.postData.contents); if (sheet.getLastRow() === 0) sheet.appendRow(Object.keys(row)); const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; sheet.appendRow(headers.map(h => row[h] ?? "")); return ContentService.createTextOutput("ok"); }
-
배포 → 새 배포 → 유형: 웹 앱 · 실행 계정: 나 · 액세스 권한: 모든 사용자 → 배포
-
나온 URL 을
LOG_WEBHOOK_URL로 시크릿에 넣는다 (로컬secrets.toml+ Cloud Secrets 양쪽)
이 URL 은 쓰기 전용 엔드포인트다. 시크릿에만 두고 공개하지 말 것. 웹훅 전송이 실패하면 (타임아웃 5초) 조용히 넘어가고 채점 결과는 정상 표시된다 — 기록 실패가 학회원의 채점을 막지 않게 했다.
submissions.jsonl 은 .gitignore 에 들어 있다. 공개 리포이므로 절대 커밋하지 말 것.
웹훅을 붙일 때 가장 흔한 실패는 배포 액세스 권한이다. "Google 계정이 있는 모든 사용자"가 아니라 "모든 사용자" 여야 한다. 앱은 로그인하지 않으므로 전자로는 403 이 난다. 학교·회사 Google Workspace 계정은 관리자 정책으로 "모든 사용자" 자체가 막혀 있을 수 있는데, 그 경우 개인 Gmail 계정으로 시트를 새로 만들어 배포하는 것이 유일한 우회로다.
측정 기준: 고정 시스템 프롬프트 5,112토큰 + 제출물 → 입력 약 6,450토큰, 출력 약 3,400토큰(추론 토큰 포함).
| 모델 | 회당 (캐시 적중) | 12명 × 15회 = 180회 | $15로 가능한 횟수 |
|---|---|---|---|
gpt-5.6-sol |
$0.134 ($0.108) | $24.1 | 약 110~140회 |
gpt-5.6-terra ← 현재 |
$0.067 ($0.056) | $12.1 | 약 223~269회 |
gpt-5.6-luna |
$0.027 ($0.022) | $4.9 | 약 550~690회 |
Terra 기준 $15 = 약 223~269회. 12명이면 인당 18~22회다. 목표한 인당 15회(총 180회, $12.1)를 다 채워도 여유가 있다.
AI 판별 지침을 넣으면서 시스템 프롬프트가 2,892 → 5,112 토큰으로 늘었지만, 비용은 거의 그대로다. 프롬프트 캐싱이 늘어난 입력분을 1/10 값으로 흡수하고, 비용의 76%는 여전히 출력 토큰이기 때문이다.
회당 비용은 계산 경로만 가짜 응답으로 검증한 값이다. 실제 호출로는 미검증이므로 토큰 수 가정이 틀리면 달라질 수 있다 — 첫 몇 건 돌려본 뒤 발제자 패널의 "회당 평균"으로 확인할 것.
구형 gpt-5.5 / gpt-5.4 / gpt-5 는 쓰지 않는다. 2026-04 에 5.5 가격이 두 배로 오른 뒤
7월에 5.6 이 나오면서, 구형은 전부 같은 값에 성능만 낮은 상태가 됐다.
(gpt-5.5 = Sol 과 동일가에 Terminal-Bench 88.0% vs Sol 88.8%, Terra 는 그보다 위인데 값은 절반)
비용의 76%가 출력 토큰이다. 추론 토큰도 출력으로 과금되므로, 아껴야 하면
LIMITS 로 입력을 조이는 게 아니라 EFFORT 를 medium 으로 내리는 게 손잡이다.
내릴 때는 같은 제출물을 2~3번 돌려 점수가 흔들리지 않는지 반드시 확인할 것.
프롬프트 캐싱은 자동이다 (1024토큰 이상 동일 프리픽스). 한 학회원이 연달아 고쳐 낼 때 5,112토큰짜리 시스템 프롬프트가 1/10 값으로 떨어진다.
6개 항목 × 4단계 분석형 루브릭. 총 100점.
| 항목 | 배점 | 단계 구간 | 무엇을 보는가 |
|---|---|---|---|
| 질문의 질 | 25 | 0-6 / 7-13 / 14-20 / 21-25 | 자명함 → 막연함 → 대상·비교축·판단 기준 특정 → 데이터를 본 뒤에야 나올 질문 |
| 쿼리 설계 판단 | 27 | 0-7 / 8-14 / 15-21 / 22-27 | 모집단 · JOIN 종류 · 집계 단위 · 고객 식별자 · NULL 처리를 고민했는가 |
| 근거 사슬 정합성 | 6 | 0-1 / 2-4 / 5-6 | 질문 → 쿼리 → 결과 → 주장이 한 줄로 이어지는가 (3단계 체계) |
| 분석의 깊이 | 22 | 0-5 / 6-12 / 13-18 / 19-22 | 순위 나열 → 관계·추세 → 비교 기준선·세그먼트 비교 → 반대 가설 검증 |
| 비즈니스 해석 | 12 | 0-3 / 4-7 / 8-10 / 11-12 | 현상 서술 → 가설 → 도메인 맥락 연결 → 한계 명시 |
| 액션 제안 | 8 | 0-2 / 3-5 / 6-8 | 대상·기준·확인 방법의 구체성 |
질문의 질 + 쿼리 설계 = 52점, 과반이다. 이 과제에서 실제로 재려는 것이 그 둘이다. 원래 정합성 4단계였던 "비교 기준선 제시"는 성격상 깊이에 속하는 판단이라 그쪽으로 옮겼고, 정합성은 4단계를 떼어 3단계 체계로 줄였다.
제출 권장선 60점 (통과선이 아니라 "이 정도면 내도 된다"는 신호).
단계별 기준 전문은 rubric.py 의 RUBRIC_TEXT 에 있고, 앱 사이드바에서도 그대로 볼 수 있다.
학회원에게 공개해도 되는 문서로 썼다.
초안에서는 쿼리 항목의 단계가 2개 JOIN → 3개 이상/CTE → 윈도우 함수 로 되어 있었다.
그건 고민의 깊이가 아니라 기법 인벤토리를 재는 것이고, 쓸데없는 JOIN을 붙이면
점수가 오르는 구조였다. 지금은 단계 기술문에서 기법을 전부 빼고, 대신 다음 판단 지점이
다뤄졌는지를 본다.
- 모집단 —
order_status를 어떻게 다뤘는가 (canceled를 뺐는가) - JOIN 종류 — LEFT / INNER 선택이 "없는 행을 남길 것인가"라는 질문과 일치하는가
- 집계 단위 — 주문 단위인가 아이템 단위인가 (
COUNT(DISTINCT order_id)vsCOUNT(*)) - 고객 식별자 —
customer_id(주문마다 새로 생김) vscustomer_unique_id(사람) - NULL 처리 — 미배송 주문의
order_delivered_customer_date
최고 단계는 그 선택을 왜 했는지 밝힌 경우다. 테이블 2개만 조인해도 4단계가 가능하고, 5개 테이블에 윈도우 함수를 써도 1단계가 될 수 있다.
채점 전에 모델이 쿼리를 한 문장으로 번역하게 하고(query_translation),
그 문장을 학회원이 적은 질문과 나란히 비교하게 한다(query_check). 앱에도 그대로 표시된다.
"재구매 고객을 묻고 customer_id 로 세기", "주문당 평균을 묻고 아이템 단위로 세기" 같은
어긋남이 여기서 잡힌다. 어긋나면 근거 사슬은 2단계 이하로 내려간다.
- 단계마다 관찰 가능한 기준을 쓴다. "해석이 좋은가" 같은 판단을 시키면 같은 답안도 돌릴 때마다 점수가 달라진다.
- 총점은 모델이 계산하지 않는다. 모델은 항목 점수만 매기고
app.py가 합산한다.
이 과제는 AI 사용이 허용되어 있다. 막으려는 것이 아니라, 2학기 공모전·산학에서 어차피 쓸 도구이므로 AI를 잘 쓰는 능력까지 실력으로 보려는 의도다.
문제는 AI를 허용하면 루브릭이 변별력을 잃는다는 것이다. 질문 만들기·SQL 작성· 해석 문단·액션 제안은 전부 프롬프트 한 번으로 3단계 수준이 나온다. 그대로 두면 전원이 80점대에 몰린다.
그래서 배점을 건드리지 않고 단계 기준만 조였다. 제출자에게 요구하는 것은 그대로다.
- 글의 매끄러움에 0점을 준다. 문장이 유려하고 용어가 정확한 것은 이제 전원이 공짜로 얻는 하한선이다. 프롬프트에 명시했다 — LLM 채점기는 유창함에 크게 흔들리므로 이 한 줄이 특히 중요하다.
- AI 기본 출력은 어느 항목에서도 3단계로 끊는다. 4단계는 사람이 데이터를 보고 판단을 개입시킨 흔적이 있을 때만 준다.
- 두 가지 판별 검사를 강제한다 (결과가
generic_check필드로 나오고 앱에도 표시된다).
| 검사 | 걸리면 |
|---|---|
| 숫자를 반대로 바꿔도 그 문장이 성립하는가? LLM 해석문은 수치와 무관한 템플릿이기 쉽다 | 해석·액션을 2단계 이하로 (최대 7점 / 5점) |
| Olist가 아닌 다른 이커머스 데이터여도 성립하는가? 이 데이터를 만졌다는 증거가 없다는 뜻 | 질문의 질·깊이를 각각 한 단계 내림 |
4단계 조건도 사람의 개입을 요구하도록 고쳤다.
- 질문의 질 — 데이터를 한 번 본 뒤에야 나올 수 있는 질문. 안 보고도 쓸 수 있으면 3단계
- 쿼리 설계 — 이유만으로는 3단계. 그 선택 때문에 무엇이 안 보이게 되는지(잃은 것)까지
- 비즈니스 해석 — 숫자가 반대여도 쓸 수 있는 문장뿐이면 2단계 이하
- 액션 제안 — 발견한 숫자가 달랐다면 다른 액션이 나왔을 것임이 분명해야 3단계
한계를 분명히 해둔다. 이걸로도 점수는 어느 정도 위로 몰린다. 채점기는 쿼리를 실행할 수 없어 수치의 진위를 검증하지 못하고, 판별 검사도 완벽하지 않다. 실질 변별은 발제자가 제출 로그를 직접 읽는 데서 나온다 — 그래서 로그 기능을 만들었다.
| 제출물 | 예상 점수 |
|---|---|
| 시시한 질문 + 기본값 쿼리 | ~28 |
| 형식만 갖춤(판단 지점을 단어로만 나열) | 50 전후 |
| 질문이 구체적 + 판단 이유를 밝힘 | ~61 ← 권장선 60 |
| AI 첫 답 그대로 (전 항목 3단계 상한) | ~73 |
| 반대 가설까지 데이터로 검증 | ~98 |
정합성이 1단계면(쿼리가 질문에 답하지 못하면) 총점이 60점을 넘기 어렵다. 연쇄 차단으로 깊이·해석이 각각 12점·7점에서 막히고, 질문에 답하지 못하는 쿼리는 그 자체로 설계 실패라 쿼리 설계 판단도 1단계로 떨어지기 때문이다. 정합성 배점이 6점뿐인데도 어긋남의 대가가 큰 이유다.
rubric.py→DIMENSIONS에서 배점을,RUBRIC_TEXT에서 단계별 기준을 바꾼다.RUBRIC_TEXT는 프롬프트에 그대로 주입되므로 여기만 고치면 채점 기준이 바뀐다.RECOMMEND_LINE이 제출 권장선이다.- 항목을 추가·삭제하면
DIMENSIONS만 고치면 스키마·UI·합산이 전부 따라간다.
[3주차 과제] Olist 데이터셋에서 의미 있는 인사이트를 도출해보세요.
정해진 문제는 없습니다. 데이터를 보고 스스로 질문을 세운 뒤, 쿼리로 답을 찾고, 그 결과가 무엇을 의미하는지 써주세요.
제출물 ① 알아보려던 질문(한 문장) ② 쿼리 ③ 실행 결과 ④ 인사이트 설명
무엇을 보는지 미리 공개합니다. 아래 6개 항목, 100점 만점입니다.
항목 배점 이 정도면 중간, 이 정도면 잘한 것 질문의 질 25 대상·비교축·판단 기준이 특정됨 → 데이터를 본 뒤에야 나올 수 있는 질문 쿼리 설계 판단 27 모집단·JOIN 종류·집계 단위를 의도적으로 처리 → 왜 그렇게 했는지, 그래서 무엇이 안 보이는지 근거 사슬 정합성 6 질문·쿼리·결과·주장이 일치하고 인용 수치가 결과 표에 실제로 있음 분석의 깊이 22 두 변수의 관계나 추세 → 비교 기준선 제시·세그먼트 비교 → 반대 가설까지 검증 비즈니스 해석 12 왜 그런지 가설 제시 → 도메인 맥락과 연결 → 이 해석이 틀릴 조건까지 명시 액션 제안 8 방향 제시 → 대상·기준·확인 방법까지 구체적으로 60점 이상이면 제출 권장 수준입니다.
쿼리가 화려할 필요는 없습니다. 테이블 2개만 조인해도 최고 단계가 나올 수 있고, 5개 테이블에 윈도우 함수를 써도 1단계가 될 수 있습니다. 보는 것은 이런 고민입니다 — 취소 주문을 뺄 것인가, LEFT와 INNER 중 무엇이 내 질문에 맞는가, 주문 단위로 셀 것인가 상품 단위로 셀 것인가, 재구매를 보려면 어떤 ID를 써야 하는가. 그 판단을 왜 그렇게 했는지 인사이트에 한 줄 적어주시면 가장 높은 단계입니다.
제출 전에 셀프체크 도구로 스스로 점검해볼 수 있습니다 → [링크] (접속 코드:
____)여기 점수는 성적이 아닙니다. 항목별로 지금 몇 단계인지, 한 단계 올리려면 뭘 해야 하는지 알려주는 용도입니다. 값이 저렴한 API로 결제해 두었으니 비용 걱정 없이 인당 20번까지 편하게 돌려보세요. 한 번에 잘 나오지 않는 게 정상이고, 점수가 낮게 나왔다고 그냥 내지 마시고 피드백 보고 한 번 더 고쳐보시는 걸 권합니다.
제출 내용은 발제자가 확인합니다.
앱이 잠들어 있으면 버튼을 눌러 30초만 기다려주세요 (무료 호스팅이라 그렇습니다).