Shamino in Lycaeum

Lycaeum

나의 AI 친구 Naaru, Wisp들과의 Lore, 그리고 나의 삶도 가끔?

아들 공부를 위한 AI 과외 앱, SeekerStone

과외 선생님을 구하는 대신, 직접 만들기로 했다.


발단

아들이 중학교에 올라갔다. 요즘은 우리 때보다 더 열심히 공부해야 했고, 그래서 학원도 다니지만 나와 Naaru가 도울 일이 없을까 고민하게 되었다. 과외 선생님을 알아볼 수도 있었지만, 문득 다른 생각이 들었다.

어차피 Naaru.code와 밤마다 코딩을 하고 있었다. 그 연장선에서 아들이 언제든 꺼내 쓸 수 있는 AI 학습 도우미를 만들어보기로 했다. 이름은 SeekerStone — 젤다 스카이워드 소드에 등장하는 시커 스톤(Sheikah Stone)에서 따왔다. 링크가 던전에서 막혔을 때 힌트를 주는 돌이다. 공부가 막혔을 때 방향을 알려주는 앱이라면, 이름이 딱 맞다. 나중에 알고 보니 Sheikah를 영어로 쓰면 Seeker처럼 읽히기도 한다 — 의도하지 않았는데 두 겹으로 맞아떨어졌다.

Sheikah Stone in Zelda: Skyward Sword


구조

스택은 간단하게 잡았다.

  • 백엔드: Spring Boot 3 + H2 (파일 기반 DB)
  • 프론트엔드: React PWA — 아이패드에서 앱처럼 쓸 수 있게
  • AI: Claude API (Sonnet 4.6)

기능은 네 가지다.

  1. 퀴즈 — 과목/학년/시험 범위에 맞춰 AI가 문제를 낸다. 틀린 문제를 올리면 비슷한 유형의 문제를 골라 다시 낼 수도 있다
  2. AI 과외 — 모르는 내용을 대화로 물어보고, 자료를 올리면 그걸 바탕으로 설명해준다
  3. 자료 관리 — 학교 프린트물을 사진으로 찍어 올리면 Claude가 과목/단원/개념을 분석한다
  4. 리포트 — 풀이 이력을 쌓아두다가 AI에게 실력 분석을 요청한다

SeekerStone 앱 화면


가장 오래 붙잡혔던 문제 — 손글씨 제거

자료 관리에 기능 하나를 욕심 냈다. 아내 Pong이 매 시험 때마다 수작업으로 열심히 챙겨주는 일이 있다 — 아이가 필기해둔 프린트물을 깔끔하게 정리하는 것. AI의 도움을 받을 수 있는 부분이 있지 않을까 싶었다. 그래서 만들어보기로 했다.

원리는 단순해 보였다.

  1. Claude Vision으로 손글씨 영역 감지 → 바운딩 박스 좌표 반환
  2. Python 스크립트(OpenCV)로 해당 영역 인페인팅

실제로 구현하면서 버그가 폭포처럼 쏟아졌다.

좌표 불일치 문제. 핸드폰 카메라로 찍은 사진은 4000px짜리다. Claude는 이미지를 내부적으로 리사이즈해서 처리하기 때문에 반환된 좌표는 리사이즈된 공간 기준이었다. 그 좌표를 원본 이미지에 그대로 적용하면 엉뚱한 위치가 지워진다.

해결책은 의외로 단순했다. 업로드 시점에 이미지 자체를 1568px로 정규화해버린다. 저장된 이미지 = Claude가 처리하는 이미지 = Python 스크립트가 보는 이미지. 세 공간을 하나로 맞추면 좌표 불일치는 원천 차단된다. 절대 픽셀 좌표 대신 0~1 상대 좌표를 요청하도록 프롬프트도 바꿨다.

브라우저 캐시 문제. 손글씨 제거 후 이미지를 다시 불러와도 브라우저는 이전 이미지를 캐시에서 꺼내온다. URL이 같으니까. Cache-Control: no-store를 응답 헤더에 박아도 효과가 없었다. 결국 두 겹으로 막았다 — 출력 파일명에 타임스탬프를 붙여 URL 자체를 매번 다르게 만들고, React에서 <img key={viewerKey} />로 DOM을 강제 재생성했다.


자동화의 한계, 그리고 사람

자동화에는 한계가 있다.

색상 기반 감지(HSV 필터링), 엣지 기반 감지 — 둘 다 실제 학생 필기 사진 앞에서는 믿음직스럽지 않았다. 인쇄 잉크와 손글씨의 경계가 사진 조건에 따라 너무 달라진다.

Naaru가 짚어준 방향은 간단했다. “자동화가 틀릴 바에는, 사람이 확인하게 하자.”

그렇게 만든 게 Human-in-the-loop 크롭 검증 UI다.

  1. “손글씨 감지” 버튼 → Claude가 의심 영역을 감지하고, 각 영역의 크롭 이미지를 카드로 보여준다
  2. 아이나 부모가 카드를 확인하며 맞는 것만 선택한다
  3. 선택한 영역만 인페인팅한다

오탐이 있어도 사람이 걸러낸다. 카드 회전 버튼과 박스 크기 조절도 붙였다. 완전 자동화보다 훨씬 실용적인 흐름이다.


퀴즈와 리포트

퀴즈는 처음엔 과목만 고를 수 있었다. 실제로 쓰다 보니 아이가 시험 준비를 하는 구체적인 맥락이 있다는 걸 알았다. 선택 범위를 넓혔다.

  • 학년: 중1 / 중2 / 중3 / 전체
  • 시험 기간: 1학기 중간 / 1학기 기말 / 2학기 중간 / 2학기 기말
  • 단원 직접 입력: 자유 텍스트 (예: 일차방정식)

Claude는 한국 중학교 교육과정을 알고 있어서 “중2 수학 1학기 중간고사 범위”라고 하면 그 범위에서 문제를 낸다. 학교마다 진도가 조금씩 다르니 단원 직접 입력이 보완재 역할을 한다.

틀린 문제를 사진으로 올리는 것도 된다. Claude가 문제를 분석하고, 같은 유형·비슷한 난이도의 문제를 바로 생성한다. 학원 숙제에서 틀린 것들을 모아 올리면 그 자리에서 복습 세트가 만들어진다.

리포트 탭에는 풀이 이력이 쌓인다. 과목별, 난이도별 정답률이 테이블로 보이고, 버튼 하나로 Claude에게 분석을 요청한다. 약점 과목, 난이도별 패턴, 추천 학습 방향이 마크다운 리포트로 돌아온다.

온디맨드로 설계한 이유는 단순하다 — 자동 분석은 조용히 API 비용이 나간다.


macOS에서 Linux로

로컬 맥에서 잘 돌아가던 앱을 Wisp(Ubuntu)에 올리려니 하나가 막혔다. 이미지 정규화에 sips를 쓰고 있었는데, macOS 전용 도구였다.

Python Pillow로 교체했다. ImageOps.exif_transpose()로 EXIF 회전을 픽셀에 구워서, Image.resize()로 1568px로 맞춘다. pillow-heif를 추가하면 HEIC도 처리된다.

하드코딩되어 있던 Python 경로도 application.ymlseekerstone.python-path로 뺐다. 로컬 맥은 application-local.yml에서 오버라이드, Wisp는 기본값 python3 그대로.


배포

Wisp에는 이미 다른 서비스들이 돌고 있어서 nginx를 앞에 세웠다. seekerstone.shamino.devlocalhost:8080. Spring Boot가 React 빌드 결과물을 정적 파일로 함께 서빙한다.

deploy.sh 한 줄이면 코드 반영부터 서비스 재시작까지 끝난다.


마치며

이제 첫 MVP를 만든 셈이다. 퀴즈를 좋아하는 아들에게 먼저 보여주고, Pong과 함께 셋이서 AI가 실제로 쓸모 있는 부분이 어디인지 계속 찾아볼 예정이다. 그래도 멋진 일은, 하루 만에 이걸 뚝딱 만들고 진짜 필요한 것들을 하나씩 발견해갈 수 있는 시대를 살고 있다는 점이다.

이 서비스는 이번에 도입한 도메인 기반 seekerstone.shamino.dev(w/Wisp 서버)로 안정적 서비스 예정이다.

코드는 GitHub에 있다.