[연재] 기술사 스터디 (2) 진도·복습·채점 붙이기
여섯 조각이 다 통과했는데, 이어 붙이니 틀렸습니다
시험 공부 도구에 채점을 붙였습니다. 답안을 쓰면 점수가 나오고, 점수가 60점 아래면 그 주제가 복습 큐에 들어가요. "이건 아직 약하니까 며칠 뒤에 다시 봐라"는 목록입니다.
일을 여섯 덩어리로 잘라서 만들었어요. 기출 빈출도 계산, 채점, 명령어 확장, 대시보드 칸, 문서… 하나 끝날 때마다 검토를 붙였고, 여섯 개가 전부 "깨끗함"으로 통과했습니다. 기록에도 여섯 줄이 나란히 그렇게 적혀 있어요.
그런데 마지막에 전체를 한 번 더 훑었더니 세 개가 나왔습니다. 셋 다 조각 하나 안이 아니라 조각과 조각이 만나는 자리에 있었어요. 그중 첫 번째가 이겁니다.
done이 복습 큐 미동기화 (숙달인데 약함으로 남음)
화면으로 옮기면 이런 뜻이에요. 진도판에서 그 주제는 숙달입니다. 초록색이에요. 그런데 바로 아래 복습 큐 칸에는 같은 주제가 "약함, 복습 예정"으로 앉아 있습니다. 같은 프로그램 안에서 한 주제의 상태가 두 개인 거죠.
무서운 건 이겁니다. 이건 아무도 겪은 적이 없는 사고예요. 이 도구는 아직 한 번도 제대로 안 돌려봤거든요(그 얘기는 글 끝에 다시 하겠습니다). 한 번이라도 써봤으면 첫날 눈에 걸렸을 겁니다. 안 써봤으니, 마지막에 전체를 훑지 않았으면 영영 몰랐을 거고요.
원인은 허무합니다. 큐에 넣는 길과 큐에서 빼는 길이 따로 나 있었어요.
- 채점(
grade)은 큐를 건드립니다. 점수가 낮으면 넣고, 높으면 뺍니다. - 진도 기록(
done)은 큐를 안 건드렸습니다. 진도만 바꾸고 끝이었어요.
그래서 채점으로 들어간 주제를 손으로 숙달 처리하면, 진도만 바뀌고 큐는 옛날 얘기를 계속하는 겁니다.
고친 방법은 큐를 만지는 코드를 한 군데로 모으는 거였어요. reconcile_queue 라는 함수 하나를 만들고, 채점이든 진도 기록이든 둘 다 그걸 부르게 했습니다. 지금 진도 기록 쪽 코드는 이렇게 생겼어요.
prog = progress.record(prog, args.topic_id, args.state, today=today)
store.save_json("progress.json", prog)
tutor.reconcile_queue(args.topic_id, prog["topics"][args.topic_id]["state"], today=today)
세 줄째가 그날 추가된 줄입니다. 그리고 같은 일이 또 안 생기게 시험 두 개를 박아뒀어요. 실제 시험 파일에 이렇게 적혀 있습니다.
def test_done_clears_review_queue_when_mastered(tmp_path, monkeypatch):
_seed(tmp_path, monkeypatch)
assert cli.main(["grade", "earth-01", "--score", "50", "--keywords", "x"], ...) == 0
assert store.load_json("review_queue.json", None)["weak"][0]["topic_id"] == "earth-01"
assert cli.main(["done", "earth-01", "--state", "숙달"], ...) == 0
assert store.load_json("review_queue.json", None)["weak"] == []
50점 받아서 큐에 들어간 주제를, 숙달로 바꾸면, 큐가 비어야 한다. 딱 그 얘기예요.

지난 편이 무엇을 공부할지의 목록이었다면, 이번 편은 얼마나 했고 뭘 잊었나입니다. 그러니까 통째로 기록에 관한 얘기예요. 그래서 이 사고를 맨 앞에 꺼냈습니다. 기록이 두 벌 있으면 둘 중 하나는 반드시 거짓말을 합니다. 그리고 어느 쪽이 거짓말인지는 화면이 안 알려줘요.
점수 하나로는 답안을 못 고칩니다
먼저 채점 얘기부터 할게요. 이 편에서 제일 손이 많이 간 부분이고, 제일 쓸모 있는 부분이기도 합니다.
기술사 필기는 논술입니다. 2·3·4교시는 25점짜리 문제를 한 문항당 1~1.5쪽씩 씁니다. 그리고 이런 답안은 통째로 틀리는 일이 거의 없어요. 대부분은 어느 한 덩어리가 비어서 점수가 깎입니다.
서술형 답안 뼈대를 이렇게 잡아뒀거든요.
Ⅰ. 개요 → Ⅱ. 원리 → Ⅲ. 원인 → Ⅳ. 대책 → Ⅴ. 맺음말
여기서 흔한 실패가 뭐냐면, 원인은 다섯 개 잘 써놓고 대책이 세 줄로 끝나는 거예요. 아니면 개요에서 정의를 안 하고 바로 원인으로 뛰어드는 것. 이런 건 "72점입니다"라는 말로는 절대 못 고칩니다. 72점을 받으면 어디가 부족한지 모르니까 다음에도 똑같이 씁니다.
그래서 채점을 문단 단위로 받게 만들었어요. 작업 문서에 이렇게 적혀 있습니다.
LLM이 문단별 정밀 첨삭(점수 0~100, 문단별 좋은 점/빠진 점,
부족 키워드, 모범답안 요지)을 HTML로 작성하고 …
점수는 그냥 딸려오는 값이고, 진짜는 "Ⅳ 대책이 얇다", "계측관리가 빠졌다" 같은 문장들입니다. 그리고 부족했던 낱말들은 따로 모아서 저장해요. 나중에 복습 큐에 뜰 때 "이 주제 · 정착길이, 겹침이음" 이런 식으로 뭘 몰랐는지가 같이 뜹니다.

결제를 안 하려고 복사·붙여넣기를 골랐습니다
여기서 좀 촌스러운 얘기를 해야 합니다.
이 도구에는 API 키가 한 개도 안 들어갑니다. 채점도, 튜터도, 문제 풀이도요. 설계 문서에 아예 못을 박아뒀어요.
실행: python3(키 없음). LLM은 출제·채점·정리에만 사용.
보통 "AI 붙인다"고 하면 결제부터 하잖아요. 키 발급받고, 카드 등록하고, 호출할 때마다 돈이 나가고요. 저는 그 길을 안 갔습니다. 이유는 단순해요. 이건 제가 혼자 쓰는 공부 도구입니다. 매일 몇십 번 두드릴 건데 그때마다 요금이 붙으면, 아껴 쓰게 되고, 아껴 쓰면 안 쓰게 됩니다.
대신 이렇게 굴러갑니다.
1. 화면(콕핏)에서 답안을 씁니다. 2. 「답안 복사 (채점 요청)」 버튼을 누릅니다. 채점 요청 덩어리가 클립보드에 담겨요. 3. 그걸 클로드 창에 붙여넣습니다. 4. 문단별 첨삭이 나오면 그걸 파일로 저장하고, 명령 한 줄로 도구에 집어넣습니다.
python3 -m sigong_coach grade <주제> --score 72 --keywords "정착길이,겹침이음" --feedback-file /tmp/fb.html
클립보드에 담기는 덩어리는 이렇게 생겼어요. 코드에 그대로 있습니다.
const block = "[채점 요청] 주제 "+t.id+" ("+t.name+")\n문제: "+q+"\n\n내 답안:\n"+(ans||"(답안 미작성)");
네, 손으로 복사해서 손으로 붙여넣습니다. 자동이 아니에요. 그런데 이 촌스러움 덕에 연 비용이 0원입니다. 그리고 첨삭 결과는 그 주제 밑에 저장돼서, 다음에 그 주제를 열면 지난번에 뭘 틀렸는지가 바로 같이 뜹니다.
역할도 이 선에서 갈립니다. 설계 문서 표현을 그대로 옮기면 이래요.
파이썬 = 저장·멱등병합·freq재계산·서브노트 아카이브(테스트 가능)
LLM(대화) = 수집·추출·매핑·채점·정리
"셀 수 있는 건 파이썬이, 판단하는 건 대화가." 이렇게 갈라놓으니까 시험 코드를 쓸 수 있는 부분과 못 쓰는 부분이 깔끔하게 나뉘더라고요. 실제로 시험 코드가 578줄 붙어 있는데, 전부 파이썬 쪽에만 있습니다.

튜터를 붙인 지 8분 만에 손발을 묶었습니다
복사·붙여넣기가 귀찮아서, 화면 오른쪽에 튜터 채팅창을 하나 더 붙였습니다. 지금 보고 있는 주제의 정리 내용을 같이 넘겨서, 모르는 걸 바로 물어보는 창이에요.
이것도 키 없이 굴립니다. 로컬에 작은 서버를 하나 띄우고, 거기서 이미 로그인돼 있는 클로드를 그대로 부르는 식이에요. 서버 파일 맨 위 설명에 이렇게 적혀 있습니다.
- Anthropic API 키/과금 없이, 이미 로그인된 Claude Code 구독 인증을 그대로 사용.
- localhost 전용(외부 노출 안 함).
붙인 게 7월 18일 오전 10시 1분이었어요. 그리고 8분 뒤인 10시 9분에 커밋이 하나 더 올라갑니다.
hardening: 튜터 Q&A에서 도구 전부 비활성(--disallowedTools)
뭘 껐냐면, 이겁니다.
out = subprocess.run(
[CLAUDE, "-p", prompt,
"--disallowedTools", "Bash", "Read", "Edit", "Write", "WebFetch",
"WebSearch", "Task", "Glob", "Grep", "NotebookEdit"],
capture_output=True, text=True, timeout=180)
파일 읽기, 파일 쓰기, 명령 실행, 웹 접속. 전부 껐습니다.
왜 8분 만에 껐냐면, 붙이고 나서 그 창이 어디에 앉아 있는지를 생각했거든요. 바로 옆 폴더에 노트 5.3MB, 기출 1.4MB, 진도 기록이 통째로 들어 있습니다. 공부를 도와주는 창이 그 폴더를 만질 이유는 하나도 없어요. 그런데 막아두지 않으면 만질 수 있는 상태입니다.
재밌는 건, 프롬프트 안에도 이미 이렇게 적혀 있었다는 거예요.
도구를 쓰거나 파일을 열지 말고 바로 답만 작성하세요.
말로도 막아놨는데 설정으로 또 막은 겁니다. 말로 하는 부탁은 규칙이 아니거든요. 대체로 지켜지지만 항상은 아니에요. 항상이어야 하는 건 설정으로 막아야 합니다.
하나 더 있어요. 이 채팅창은 주소가 로컬일 때만 켜집니다.
const QA_ENDPOINT = window.QA_ENDPOINT || (/^(localhost|127\.0\.0\.1)$/.test(location.hostname) ? "/ask" : null);
공개 사이트에 올라간 화면에서는 채팅 버튼이 아예 안 뜹니다. 없는 기능인 것처럼요. 이것도 같은 생각이에요. 켜질 수 있는데 안 켜진 것보다, 켜질 수 없는 게 낫습니다.

"공부했다"는 한 칸으로는 부족했습니다
이제 진도 쪽입니다.
처음엔 체크박스 하나면 될 줄 알았어요. 봤으면 체크, 안 봤으면 빈칸. 그런데 이게 왜 안 되냐면, 한 번 읽은 것과 시험장에서 쓸 수 있는 것이 완전히 다른 상태이기 때문입니다.
논술 시험에서 "아, 그거 봤는데"는 0점입니다. 백지에 스스로 다섯 문단을 뽑아낼 수 있어야 점수가 붙어요. 그런데 체크박스는 그 둘을 구분 못 합니다. 다 체크해놓고 시험장에서 백지를 받는 일이 벌어져요.
그래서 상태를 넷으로 갈랐습니다.
| 상태 | 뜻 | 진도 가중치 |
|---|---|---|
| 미학습 | 아직 안 봄 | 0 |
| 학습 | 1회독 | 0.5 |
| 약함 | 봤는데 안 나옴 | 0.5 |
| 숙달 | 됐다 | 1.0 |
코드로는 이 한 줄이 전부예요.
STATE_WEIGHT = {"미학습": 0.0, "학습": 0.5, "약함": 0.5, "숙달": 1.0}
여기서 제가 제일 오래 고민한 건 약함을 0.5로 둘지, 더 낮출지였습니다.
지난 편 마지막에 "점수를 잘 주는 것보다 어려운 게 점수를 나쁘게 주는 것"이라고 썼는데, 그게 이 얘기예요. 55점을 받아서 약함이 된 주제와, 65점 받아서 학습이 된 주제. 진도율에서는 똑같이 0.5입니다. 나쁜 점수를 받았다고 진도가 깎이지 않아요.
처음엔 이게 이상해 보였습니다. 못했으면 뭔가 손해가 있어야 하는 거 아닌가 싶었죠.
그런데 진도율은 성적표가 아니라 지도입니다. "내가 전체의 어디쯤 왔나"를 재는 자예요. 여기에 벌점을 섞으면, 열심히 채점받은 사람의 지도가 더 어두워집니다. 그러면 어떻게 되냐면 — 채점을 안 받게 돼요. 안 받으면 그냥 학습으로 남고, 화면은 더 예뻐집니다.
그래서 나쁜 점수의 대가를 진도율에서 빼고, 다른 데로 옮겼습니다. 다시 돌아오게 하는 것으로요.

약함을 누르면 사흘 뒤에 돌아옵니다
약함을 찍으면 딱 한 가지 일이 더 일어납니다. 사흘 뒤 날짜가 하나 적혀요.
REVIEW_DAYS = 3
...
entry["next_review"] = (today + timedelta(days=REVIEW_DAYS)).isoformat() if state == "약함" else None
숙달이나 학습을 찍으면 이 칸은 그냥 비워집니다(None). 약함일 때만 날짜가 생겨요.
그리고 오늘 뭘 공부할지 고르는 코드가 이 날짜를 봅니다.
candidates.sort(key=lambda t: (not _is_due_review(prog, t["id"], today),
-curriculum.priority(t)))
읽기 어려운 줄인데, 뜻은 간단해요. 복습 날짜가 오늘 이하인 것들이 먼저, 그다음이 우선순위 순. 사흘 전에 "이거 약하다"고 찍은 주제는, 사흘 뒤 아침에 오늘 할 일 맨 위에 다시 올라옵니다. 제가 다시 안 불러도요.
이게 이 도구에서 제가 제일 마음에 들어하는 부분입니다. 잊는다는 걸 전제로 만든 장치거든요. 공부 계획을 세울 때 우리는 보통 "한 번 보면 아는 걸로" 치고 일정을 짭니다. 그리고 안 그렇다는 걸 시험 한 달 전에 알게 되죠.
솔직하게 덧붙일 게 있어요. 이건 제대로 된 간격 반복이 아닙니다. 진짜 간격 반복은 맞힐 때마다 간격이 벌어져요. 1일 → 3일 → 7일 → 2주 이런 식으로요. 이건 그냥 무조건 3일입니다. 설계 문서에도 "간격반복 단순버전"이라고 적어놨어요.
안 늘린 이유는 하나입니다. 간격이 벌어지는 규칙을 만들면, 그 규칙이 맞는지 확인하려면 몇 달을 써봐야 합니다. 지금은 그럴 상황이 아니에요. 3일은 틀릴 수도 있지만 적어도 지킬 수는 있는 숫자입니다.

오늘 할 일은 가용 시간으로 자릅니다
오늘 뭘 할지 고르는 규칙은 세 줄입니다.
for t in candidates:
if picks and used >= cap:
break
picks.append(t)
used += t["est_hours"]
cap 은 그날 쓸 수 있는 시간이에요. 설정 파일에 이렇게 적혀 있습니다.
{ "exam_date": "2027-01-30", "weekday_hours": 1.0, "weekend_hours": 4.0 }
평일 1시간, 주말 4시간. 주당 13시간이에요. 욕심을 좀 뺀 숫자입니다.
여기서 한 군데만 짚을게요. if picks and used >= cap 에서 앞의 picks and 가 있고 없고가 꽤 큽니다. 이게 없으면 어떻게 되냐면, 가용 시간이 1시간인 평일에 예상 2시간짜리 주제만 남았을 때 오늘 할 일이 0개가 나와요.
0개는 답이 아닙니다. 오늘 할 일이 없다는 뜻이 아니라, 오늘 뭘 할지 못 정했다는 뜻이거든요. 그래서 하나는 무조건 준다로 뒀습니다. 시간을 넘겨도요.
그리고 시험 날짜 얘기. 2027-01-30 인데, 이거 확정 아닙니다. 커밋 메시지에 그렇게 적혀 있어요.
chore: 임시 시험일 2027-01-30(2027년 1회차 추정) 설정
추정입니다. 화면 맨 위에 D-day가 큼직하게 떠 있는데, 그 숫자가 추정값 위에서 돌고 있어요. 실제 시험 일정이 공고되면 설정 파일 한 줄만 고치면 되긴 합니다. 그래도 확정처럼 보이는 숫자가 화면 제일 큰 자리에 있는 건 좀 걸리는 부분이에요. 이건 아직 안 고쳤습니다.

빈출도는 20회를 모아놓고 10회만 셉니다
우선순위를 정하는 값은 딱 하나예요.
def priority(topic: dict) -> float:
return topic["importance"] * (topic["freq"] + 1)
중요도 곱하기 (빈출도 + 1). freq 는 최근 10회 중에 그 주제가 나온 회차 수입니다. 0에서 10 사이 값이에요.
여기서 두 가지가 사람이 정한 규칙입니다.
첫째, 같은 회차에 두 번 나와도 1로 셉니다.
rounds = {e["round"] for e in exams.get("exams", [])
if e.get("topic_id") == t["id"] and e["round"] in window}
out[t["id"]] = len(rounds)
문항 수가 아니라 서로 다른 회차 수를 세요. 한 회차에 같은 주제가 두 번 나온 건 "자주 나오는 주제"라는 뜻이 아니라 "그 회차가 그랬다"는 뜻에 가깝거든요. 회차 수로 세면 그 착시가 사라집니다.
둘째, 마지막의 + 1 이 91개 주제를 살립니다.
지난 편에 썼듯이 목록 325개 중 91개는 최근 20회 동안 한 번도 안 나온 주제예요. 이것들은 freq 가 0입니다. 만약 +1 이 없었으면 우선순위가 전부 0이 되고, 정렬하면 맨 밑으로 가라앉아서 영원히 오늘의 목록에 안 올라옵니다. 지우지는 않았는데 사실상 지워진 상태가 되는 거죠.
괄호 안 글자 두 개가 91개 주제의 운명을 가릅니다. 이런 건 화면만 봐서는 절대 안 보여요.
그리고 창을 두 개로 나눠 쓴 것도 사람 판단이었습니다.
- 빈출도(우선순위 계산) = 최근 10회
- 출제 경향 히트맵(눈으로 보는 그림) = 20회 전부
지난 편에서 기출을 20회차 620문항까지 모았거든요. 그런데 순서를 정하는 값에는 10회만 씁니다. 10년 치를 평균 내면 지금 흐름이 묻히고, 5년 치만 보면 그림이 안 보여요. 그래서 판단은 짧게, 관찰은 길게로 갈랐습니다.

"이번에 나올 것 같은 주제"는 근거가 감입니다
화면에 「이번 차수 예상 주제 Top 15」라는 칸이 있어요. 이걸 뽑는 식이 이겁니다.
due = latest - tlast.get(tid, 0)
score = imp.get(tid, 1) * f + (3 if due >= 1 else 0) + (2 if due >= 2 else 0)
중요도 × 빈출도에다가, 최근에 안 나왔으면 가산점을 줍니다. 한 회차 걸렀으면 +3, 두 회차 이상 걸렀으면 +2 더요.
이 3점과 2점에 근거가 있냐고요. 없습니다. 제가 정한 감이에요. 기술사 시험에 "한동안 안 나왔으니 이제 나올 때가 됐다" 같은 주기가 진짜로 있는지, 저는 증명 못 합니다.
그래서 점수를 화면에 안 띄우고, 대신 이유를 글로 띄웁니다.
reason = ("주기 도래(최근 미출제)" if due >= 2 else "최근 미출제·빈출" if due == 1 else "초빈출·상시대비")
「빈출 6/10 · 최종 137회 · 주기 도래(최근 미출제)」 이런 식으로요. 점수 숫자를 크게 띄우면 그게 예언처럼 보입니다. 이유를 적어두면 "아 이 규칙 때문에 여기 있구나" 하고 제가 판단할 수 있어요. 화면 위쪽에도 이렇게 적어뒀습니다.
빈출도·중요도가 높은데 최근 회차에 안 나온 주제(출제 주기 도래)를 우선 배치.
근거가 약한 기능을 없애는 대신, 근거가 약하다는 걸 화면에 적는 쪽을 골랐어요.

폴더 하나를 접었더니 아홉 개가 다 접혔습니다
화면 쪽 버그도 하나 얘기할게요.
왼쪽 메뉴가 폴더 트리입니다. 대분류 아홉 개가 폴더고, 그 안에 세부 주제들이 들어 있어요. 325개가 한 줄로 쭉 늘어서면 못 보니까 접었다 폈다 하게 만든 겁니다.
철근콘크리트만 보고 싶어서 나머지를 접으려고 마감 폴더를 눌렀어요.
아홉 개가 전부 접혔습니다.
다시 하나를 눌렀더니 이번엔 아홉 개가 전부 펴졌어요. 개별로 안 되고 전체만 됩니다.
원인은 화면에 넘기는 데이터에 폴더 이름표를 안 실은 것이었어요. 열린 폴더를 기억하는 코드가 이렇게 생겼거든요.
hd.onclick = () => { if(openCats.has(cat.id)) openCats.delete(cat.id); else openCats.add(cat.id); ... }
cat.id 로 "이 폴더는 열려 있음"을 기억합니다. 그런데 데이터를 만드는 쪽에서 id 를 안 넣어줬어요. 그러니까 아홉 개 폴더가 전부 같은 열쇠를 쓴 겁니다. 하나 열면 다 열리고, 하나 닫으면 다 닫히는 게 당연했죠.
고친 건 한 줄입니다. 지금 코드는 이래요.
cats.append({"id": cat["id"], "name": cat["name"], "topics": [...]})
"id": cat["id"] 이 한 조각이 빠져 있었던 겁니다.
같은 날 저녁에 비슷한 걸 하나 더 잡았어요. 주제 위쪽에 「← 이전 / 다음 →」 버튼 줄을 고정으로 붙였는데, 그게 제목을 가리고 있었습니다. 커밋 메시지에 그대로 남아 있어요.
fix: 주제 상단 네비바 sticky 최상단 고정 — 제목 가림/내용 비침 해결
두 개 다 공통점이 있습니다. 시험 코드는 전부 통과했어요. 화면은 정상적으로 만들어졌고, 에러도 없고, 글자도 다 들어 있습니다. 그냥 쓸 수가 없었을 뿐이에요.
이런 건 코드를 읽어서는 안 나옵니다. 손으로 눌러봐야 나와요. 그리고 손으로 눌러보는 건 아직 사람 몫입니다.

진도판을 다 만들어놓고, 아직 한 칸도 안 채웠습니다
여기까지 쓰고 나니 자랑처럼 보여서, 실제 상태를 세어봤습니다.
진도 기록 파일을 열어보니 325개 주제의 상태가 이렇습니다.
325 "state": "미학습"
전부 미학습입니다. 하나도 없어요.
학습 기록을 쌓는 파일(history.jsonl)은 아예 없습니다. 복습 큐 파일(review_queue.json)도 없어요. 첨삭을 저장하는 파일(feedback.json)도 없습니다. 만들어진 적이 없거든요. 한 번도 안 돌렸다는 뜻입니다.
그러니까 이번 편에서 설명한 것 중에, 실제로 제 기록이 들어간 건 하나도 없어요. 채점기도, 복습 큐도, 진도율도 전부 빈 채로 잘 돌아가고 있습니다.
숫자를 하나 더 해볼게요. 지난 편에서 세어본 예상 학습 시간이 441시간이었습니다. 시험(추정) 날은 2027년 1월 30일이니 오늘부터 168일, 어림잡아 24주예요. 설정에 적어둔 평일 1시간·주말 4시간이면 주당 13시간이고, 24주면 약 312시간입니다. 129시간이 모자랍니다.
이 계산을 도구는 안 해줍니다. 진도율 0%와 D-168을 나란히 보여줄 뿐이에요.
그래서 이걸 어떻게 봐야 하나 한참 생각했는데, 두 가지가 남더라고요.
하나. 도구가 이 사실을 숨기지 않는다는 것은 잘한 겁니다. 화면에는 이렇게 뜹니다.
hist = "".join(...) or "<li>아직 학습 기록 없음 — 콕핏에서 공부한 주제를 기록하세요</li>"
빈 목록을 안 보여주고 "아직 없다"고 말합니다. 0%도 0%라고 씁니다. 여기서 만약 "잘하고 있어요!" 같은 걸 띄웠으면 그게 제일 나쁜 도구예요.
둘. 그리고 이게 이 연재를 통틀어서 제일 정직한 문장일 것 같은데 — 도구를 만드는 건 공부가 아닙니다.
325개 목록을 만들고, 620문항을 붙이고, 문단별 채점기를 붙이고, 복습이 사흘 뒤 돌아오게 만드는 동안, 저는 건축시공기술사 공부를 한 시간도 안 했어요. 도구를 만드는 게 훨씬 재밌었거든요. 진도가 눈에 보이고, 고치면 바로 결과가 나오고, 커밋이 쌓이니까요. 공부는 안 그렇잖아요.
AI가 있으면 이 함정이 훨씬 깊어집니다. 예전 같으면 이런 도구를 만들다가 지쳐서 포기했을 거예요. 그러면 그 시점에 어쩔 수 없이 책을 폈겠죠. 그런데 이제는 끝까지 만들어집니다. 잘 만들어져요. 그래서 책을 안 펴게 됩니다.

그래서 사람이 어디에 남았나
기계가 한 것. 상태 전이, 가중 진도율, 복습 큐 병합, 빈출도 재계산, 화면 렌더링. 시험 코드까지 578줄. 문단별 첨삭 자체도 기계가 합니다 — 답안을 읽고 "Ⅳ가 얇다"를 짚는 건 사람보다 빠르고 꼼꼼해요.
사람이 한 것은 다섯 가지입니다.
첫째, 기록을 한 곳으로 모은 것. 큐에 넣는 길과 빼는 길이 따로 나 있는 걸 알아본 것. 두 장부가 어긋나 있다는 건 화면이 안 알려줍니다. 사람이 진도판과 복습 큐를 동시에 보고 "어, 얘 왜 두 군데 있지" 해야 나와요.
둘째, 나쁜 점수의 대가를 진도율에서 빼고 복습으로 옮긴 것. 못한 걸 진도에서 깎으면 채점을 안 받게 됩니다. 이건 코드 문제가 아니라 사람이 어떻게 행동하는지의 문제예요. 도구는 그걸 모릅니다.
셋째, 튜터에 결제 대신 복사·붙여넣기를 고르고, 8분 만에 손발을 묶은 것. 붙일 수 있는 걸 다 붙이는 게 아니라, 그게 어디에 앉아 있는지를 보고 권한을 좁힌 것. 말로 부탁한 걸 설정으로 다시 막은 것.
넷째, 세는 창을 두 개로 나눈 것. 순서는 최근 10회로, 그림은 20회 전부로. 그리고 괄호 안의 +1 로 한 번도 안 나온 91개 주제를 살려둔 것. 기계는 시키는 대로 셉니다. 무엇을 셀지, 어디까지 셀지는 그 시험을 볼 사람이 정해요.
다섯째, 손으로 눌러본 것. 폴더가 전부 접히고 제목이 가려지는 건 시험 코드를 아무리 늘려도 안 나옵니다.
그리고 사람이 안 한 것도 하나 적어둡니다. 공부요. 도구는 다 됐고 진도판은 0%입니다. 이 연재가 계속 "사람은 어디에 남는가"를 묻고 있는데, 이번 편의 답은 좀 김이 빠집니다. 책상에 앉는 자리에 남아요. 그건 아직 아무도 대신 못 해주더라고요.

다음 편
여기까지가 기술사 스터디 두 편입니다. 목록을 만들고, 진도를 세고, 잊는 걸 전제로 복습을 돌리고, 답안을 문단별로 채점받는 데까지 왔어요.
다음 편은 결이 좀 다릅니다. 이 연재에서 유일하게 사이트에 안 올라가는 도구 얘기예요. 사진 정리 봇입니다.
지금까지 나온 도구들은 전부 "만들었으니 올려서 남도 쓰게 하자"는 쪽이었는데, 이건 반대예요. 올리면 안 되는 도구입니다. 왜 이건 제 컴퓨터 안에서 끝나야 했는지, 그리고 처음 실제 사진을 넣었더니 전부 「기타」로 분류됐던 이야기를 하겠습니다.
#건축시공기술사 #기술사시험 #기술사공부 #건축시공 #시공기술사 #기술사필기 #논술답안 #답안첨삭 #진도관리 #복습 #간격반복 #학습관리 #공부법 #독학 #자격증공부 #기출분석 #빈출도 #출제경향 #스터디플래너 #대시보드 #파이썬 #CLI #로컬실행 #API키없이 #개인정보보호 #권한최소화 #AI활용 #클로드 #휴먼인더루프 #사이드프로젝트