← 에이전트

ASHD - 영수증 사진을 환불·보증 마감 알림으로 바꾸는 웹 서비스

사진 한 장에서 구매 정보를 읽어 마감 전에 알려 주는 서비스를 두 번 만들고 조용히 지나가던 실패를 고친 뒤 전시물로 남긴 기록

목차
  1. 무엇인가
  2. 왜 만들었나
  3. 어떻게 동작하나
  4. 써 보니
  5. 실패와 고친 것
  6. 지금 상태

무엇인가

영수증이나 보증서 사진을 올리면 상호·구매일·금액을 읽어
환불·보증 마감을 계산해 두고 마감이 다가오면
이메일이나 텔레그램으로 알려 주는 웹 서비스를 만들었다.
7월 하루 동안 실제 키가 든 파일을 걷어내고
실패하고도 성공으로 끝나던 저장 경로를 실패로 드러나게 고쳤다.
그리고 일주일 남짓 뒤 제품을 접고 전시물로 전환했다.

이름 ASHD는 Almost Service Handling Disorder의 줄임말이다.
ADHD에 빗대어 AS(사후 서비스)·환불·보증 같은 뒤처리를
잘 챙기지 못하는 사람을 돕는다는 뜻을 담았다.
지금은 라이브 데모와
공개 스냅샷 리포만 남아 있다.

왜 만들었나

가계부 앱은 언제 얼마를 썼는지를, 택배 앱은 상자가 어디쯤인지를 보여 준다.
그러나 구매 뒤 1~2년 동안 유효한 보증·AS·환불권을 챙기는 도구는 거의 없었다.
관련 정보는 이메일, 카카오톡, 종이 영수증, 상자 스티커로 흩어진다.
그러다 보증이 끝난 뒤에야 무상 AS를 떠올리고
환불 기간을 넘겨 초기 불량도 환불받지 못한다.

첫 버전은 12월에 FastAPI와 SQLite로 만들었다.
2월에 다시 보니 지금 내 워크플로우에서는 쓰지 않는 방식이었고
"현재 내 스택에 맞춰 supabase+vercel 조합으로 바꿀거야."라고 정했다.
Python 코드를 지우는 커밋이 7868줄을 걷어내고 Next.js 골격을 넣었고
36초 뒤 다음 커밋이 v2 기능을 넣었다.
범위는 업로드, OCR(광학 문자 인식) 추출, 알림 세 가지로 묶었다.

어떻게 동작하나

사진은 presigned URL로 Cloudflare R2에 올라가고
서버는 작업 번호를 먼저 돌려준 뒤 응답을 보낸 다음에 처리를 이어 간다.
처리 직전에는 R2 객체를 다시 조회해 크기(10MB까지)와 형식을 확인하고
Google Vision OCR 결과에서 룰 추출기가 필드를 추출한다.
부족한 필드는 LLM(대규모 언어 모델, OpenAI·Anthropic 듀얼)이 채운다.

LLM이 낸 값은 그대로 저장하지 않는다.
날짜는 네 가지 형식만 받아 YYYY-MM-DD로 맞추고
금액은 정수로 잘라 1 이상 10억 이하만 남긴다.
범위를 벗어난 필드는 오류를 내지 않고 버린다.

알림은 매일 오전 9시에 한 번 실행되는 크론이 계산한다.
제품·종류·임계일·채널 조합마다 발송 이력을 한 번만 남길 수 있게 막아서
같은 알림이 두 번 가지 않고 놓친 알림은 다음 실행에서 따라잡는다.
처리는 사용자당 하루 30건까지이고, 재시도도 그 30건에 포함된다.
스택은 Next.js 15, Supabase(인증·DB), Vercel Hobby 서울 리전이다.

내 역할은 기획·방향 설정·검수였다.
병합 커밋을 뺀 내 계정 커밋 48건 중 29건에 Claude 공동 작성 표기가 붙어 있다.
3월에 UI를 맡긴 러버블(Lovable, AI 웹앱 빌더)이
핵심인 OCR+LLM 로직을 없애고 수동 등록으로 바꾸는 계획을 제안했을 때,
기존 로직을 그대로 두자고 한 것은 나였다.
그 커밋은 "OCR+LLM 백엔드 파이프라인은 일체 변경 없이 프론트엔드만 교체"했다.

7월에는 다른 코딩 에이전트 런타임(GJC)에 프로젝트를 넘기며
"모든 로직을 매우 정밀하게 분석해."라고 지시했고
딥다이브가 내놓은 추천안에는 "전부 추천내용으로 진행해"라고 답했다.

써 보니

실사용 규모는 작았다.
가입 계정은 몇 개 수준이었고
결제는 Polar.sh(결제 서비스) 웹훅 로그만 남긴 채 과금하지 않았다.
공개 스냅샷 리포의 스타·포크·이슈는 모두 0이다.

2월 말에는 "누르면 1~2초후 페이지이동."이라는 증상을 전하며 병목 분석을 맡겼다.
링크를 전부 Next.js Link로 바꿔 클라이언트 측 이동을 켜고
요청마다 중복되던 getUser() 호출을 React cache()로 묶었다.
9월에는 "저번에 ASHD 랜딩페이지만들었는데 지금들어가려고보니 안들어가진다."
라는 말로 시작해 구글 로그인을 OurFolio와 같은 방식으로 맞추려 했다.

추출 정확도는 7월에 필드별 F1(정밀도와 재현율을 함께 본 점수)로 측정했다.
시드 골드 10건을 처음 측정한 결과는 상호 94%, 구매일 100%, 금액 95%였다.
공개 한국어 영수증 데이터 KORIE의 OCR 테스트 스플릿 314예제로 측정하면서 룰을 고치자
상호는 9%에서 100%, 구매일은 89%에서 99%, 금액은 98%에서 100%가 됐다.

상호 점수가 오른 것은 브랜드 목록 방식을 첫 줄 휴리스틱으로 바꾼 결과다.
시드 골드 세 필드 100%를 기준점으로 고정하고
풀 리퀘스트마다 eval을 실행하는 CI(지속 통합) 회귀 게이트를 걸었다.

실패와 고친 것

고친 것의 상당수는 오류를 내지 않고 지나가던 경로였다.
v2를 만든 바로 다음 날, 처리를 기다리지 않고 던져 두는 방식이
응답을 돌려주면 함수가 끝나는 Vercel에서 실패했다.
백그라운드 처리를 Next.js의 after()로 옮겼고
지금도 작업 번호를 먼저 돌려준 뒤 그 안에서 OCR·LLM 처리를 이어 간다.

7월 하드닝의 첫 커밋은 git에 추적되던 환경변수 파일을 지웠다.
그 파일에는 OCR·LLM API 키, 메일 SMTP 비밀번호,
텔레그램 봇 토큰이 실제 값으로 들어 있었다.
두 원격이 모두 비공개라 이력은 다시 쓰지 않았고 키 교체는 내 몫으로 남겼다.

작업 트리에는 실제 키가 든 로컬 환경변수 파일과
연구용 라이선스 데이터가 여전히 있다.
그래서 공개 스냅샷은 허용 목록에 있는 경로만 개발 이력 없이 복사해 만들었다.

같은 날 가장 많이 손댄 곳은 제품 저장이었다.
LLM 출력은 문자열인지 숫자인지만 검사받았기 때문에
"2026년 3월" 같은 날짜나 터무니없는 금액 때문에 Postgres 삽입이 실패했고,
그래도 작업은 제품 없이 completed로 끝났다.

이제 저장이 실패하면 오류 메시지를 남기고 실패로 끝나며
같은 파일로 다시 돌리는 재시도 버튼도 생겼다.
범위 검사를 통과하던 2026-02-31이나 평년의 2월 29일은
Date.UTC 왕복 검증으로 거른다.

9월에는 로그아웃이 405를 냈다.
NextResponse.redirect의 기본값인 307은 메서드를 보존하기 때문에,
브라우저가 POST 핸들러가 없는 로그인 페이지에 POST를 다시 보냈다.
로그인 페이지에 POST 핸들러를 다는 안은
리다이렉트의 의미를 라우트에 떠넘긴다는 이유로 버리고 303을 명시했다.
세션이 실제로 끊기는지는 배포 뒤 브라우저로만 확인할 수 있다고 커밋에 남겼다.

나머지는 한 줄씩 적는다.

시기증상고친 것
3월환경변수 끝 줄바꿈이 R2 presigned URL에 %0A로 섞여 CORS(교차 출처 요청) 사전 요청이 깨짐R2 환경변수를 읽을 때 전부 trim
3월문서 처리 요청의 파일 경로가 요청한 사용자 것인지 확인하지 않음사용자 id 접두사 검증으로 IDOR(남의 객체 직접 접근) 차단
6월Vercel Hobby가 조직 소유 비공개 리포를 배포하지 못함main을 개인 계정 미러 리포로 보내 배포
7월presigned PUT이 선언한 크기를 강제하지 않음처리 직전 객체를 다시 조회해 범위 밖이면 400
7월로그인 콜백의 next 파라미터로 바깥 주소로 보낼 수 있음같은 출처의 상대 경로만 허용
9월react만 19.2.8로 올라 빌드 실패, dependabot 풀 리퀘스트는 CI가 돌지 않은 채 자동 병합react-dom도 19.2.8로 맞춤
8~9월v1 리포의 일일 알림 크론이 필요한 환경변수가 없어 26번 모두 실패워크플로우 수동 비활성화

지금 상태

ASHD는 보관 상태다.
내 마지막 커밋은 9월 로그아웃 수정이고
그 뒤로는 dependabot(의존성 자동 갱신 봇)의 커밋 5건만 들어왔다.
라이브 데모는 지금도 응답하고 첫 화면에
"결제 없음. 사용자당 하루 30건까지 처리합니다."라고 적혀 있다.

접은 날은 7월 중순 새벽이다.
"내 ASHD 냉정하게 평가해줘."로 시작해 수익을 낼 수 있는지 묻자, 에이전트는
"결론부터: 수익화 가능성 낮다. 접고 포트폴리오로 남기는 게 맞다."라고 답했다.

이유는 네 가지였다. 직접 경쟁하는 앱이 이미 모든 기능을 무료로 제공하고,
지불 의사가 구조적으로 낮고, 대형 플랫폼이 번들링할 수 있고,
업로드마다 OCR·LLM 비용이 나가서 유닛 이코노믹스가 역방향이라는 것이다.
나는 "그럼 포트폴리오로 전환해서 마무리해보자."라고 답했다.
물어본 지 5분 만에 접었다.

그 평가가 근거로 삼은 시장 조사는 웹 검색 한 번이었다.
검색 결과에는 영수증·보증서를 찍으면 인식해서 만료 전에 알려 주는
무료 서비스 하나가 있었다.
지불 의사·번들링·유닛 이코노믹스는 결과에 없었고
나머지 세 이유는 에이전트의 추론이었다.
그 평가가 공개 README의 "왜 접었나" 단락으로 옮겨졌다.

10월에는 "ASHD는 너가 적절히 조치하자."라고 맡겼고 에이전트는
README의 "실제 상용 운영(결제 연동 실험까지 진행, 미과금)을 거친 뒤"를
"라이브로 배포해 직접 써 본 서비스입니다."로 바꾸면서
가입 계정은 몇 개 수준이었고 과금하지 않았다고 적었다.

"왜 접었나"에 적힌 시장 판단은 이때 손대지 않았다.
닫으면서 검증되지 않은 주장을 지운 커밋들은
검사기 셋을 지우고 테스트 73건을 71건으로 줄여서 닫았다에 따로 모았다.

남은 일은 두 가지다.
v1에서 넘어온 OCR 타임아웃 설정값(15초)은 v2의 OCR 호출에 연결돼 있지 않다.
dependabot 풀 리퀘스트의 CI 승인 게이트도 그대로여서
9월 같은 빌드 깨짐은 다시 생길 수 있다.