소피아의 자동차 모임을 위한 앱, RevSofia를 제작하다
요약
소피아의 자동차 애호가들을 위한 모임 게시판 'RevSofia'를 개발한 사례입니다. 이 앱은 그룹 채팅 내용을 분석하여 날짜, 시간, 장소, 참석 차량 등 구조화된 모임 초안을 자동으로 생성하는 플래너 기능을 핵심으로 합니다. 사용자는 이를 검토 후 Discord에 공지할 수 있습니다.
핵심 포인트
- 그룹 채팅의 비효율성을 해결한 커뮤니티 앱입니다.
- 오픈 웨이트 모델(Gemma 4)이 자연어 채팅을 구조화된 모임 초안으로 변환합니다.
- 최종 게시 전 사람이 검토하는 단계를 거쳐 신뢰도를 높였습니다.
_이 글은 Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass에 제출하는 내용입니다.
제가 만든 것
소피아에서 열리는 자동차 모임은 항상 같은 방식으로 시작됩니다. 바로 그룹 채팅이죠.
'일요일?' '오전 10시 전엔 안 돼.' 'E92 가져갈게.' '지난번처럼 Ring Mall 주차장 괜찮아?' '커피 마시고 갈까, 아니면 그냥 주차하고 이야기만 할까?' 이렇게 40개의 메시지가 오가고 나면 모임은 기술적으로 결정된 것이지만, 누가 언제, 어디에, 실제로 올지 알려면 처음부터 끝까지 스크롤을 올려 읽어야 합니다.
저는 소피아를 기반으로 하고 있으며, 지난 DEV 챌린지 출품작은 친구의 BMW E90 수리 보조 앱이었고, 이 작품이 DigitalOcean에서 최고의 활용도(Best Use of DigitalOcean)를 수상했습니다. 이번 프로젝트는 차가 수리된 다음 단계, 즉 실제로 다른 사람들과 함께 운전하며 나서는 것에 관한 것입니다.
그래서 저는 소피아의 자동차 애호가들을 위한 모임 게시판인 **RevSofia**를 만들었습니다:
- 운전하는 차량 종류에 상관없이 모두를 위한 일반 모임(General meets).
- 특정 브랜드 커뮤니티를 위한 클럽 모임(Club meets): BMW, Mercedes-Benz, 그리고 Audi/VW Group (VW, Skoda, Seat, Cupra, Porsche도 포함).
- 모든 모임에는 계획을 휴대폰에 저장하고 실제로 도로로 나설 수 있도록 달력에 추가(Add to calendar) 버튼이 있습니다.
- 상단 탐색 메뉴의 버튼 뒤에 한 달짜리 달력이 있으며, 클럽별 색상으로 각 모임을 점으로 표시합니다.
- 채팅 기능은 내장되어 있지 않습니다. 이 기능은 저희 Discord에서 이루어지며, 모든 클럽 페이지는 이를 대체하는 척하기보다 그곳으로 연결됩니다.
- 모임이 승인되면 Discord에 자동으로 게시되며, 각 사용자의 시간대에 맞춰 시간이 표시됩니다.
작동하게 만드는 핵심 부분은 바로 **플래너(planner)**입니다. 그룹 채팅을 영어 또는 불가리아어로 붙여넣으면, 오픈 웨이트 모델(Gemma 4)이 이를 초안 모임으로 변환합니다:
- 날짜와 시간 (소피아를 기준으로 오늘과 비교하여 '일요일'을 해결함)
- 만남의 장소와 목적지
- 누가 오는지, 그리고 어떤 차로 오는지
- 어떤 클럽 소속인지, 그리고 그 이유
- 채팅에서 실제로 결론 나지 않은 목록
- Discord에 붙여넣을 준비가 된 공지사항
당신이 잘못된 부분을 고치고 검토를 위해 전송(Send for review) 버튼을 누르면 GitHub 이슈로 등록됩니다. 사람이 승인하기 전까지는 아무것도 게시되지 않습니다.
그것이 바로 '현실 감각을 되찾는' 부분입니다. 화면 시간은 약 30초이고, 나머지는 비토샤(Vitosha)입니다.
데모
라이브 사이트: https://georgievalex.github.io/revsofia/
플래너: https://georgievalex.github.io/revsofia/plan.html

채팅 내용을 붙여넣고,
• 시작: 10월 11일 일요일, 09:30 (소피아 시간)
• 만남 장소: 시메오노프스코 거리에 있는 주유소
• 목적지: 즐라트니에 모스토베
• 참석 차량/인원: 니키, 마리아(골프), 이보(BMW 335i). 스토얀은 불참했으므로 목록에 올리지 않았습니다.
• 클럽: 일반 (General) - 다양한 브랜드가 참여하기 때문
• 아직 미정: 종료 시간
• 공지사항: 불가리아어, 붙여넣기 준비 완료
현재 보드에 표시된 네 개의 만남은 **예시 만남(Example meet)**으로 표시되어 있습니다. 이는 사이트가 비어있지 않도록 넣은 샘플 데이터입니다. 실제 만남이 존재하는 것처럼 꾸미고 싶지는 않았습니다.
코드
GitHub logo GeorgievAlex / revsofia
소피아 자동차 모임: 일반 및 브랜드 클럽 만남, 복잡한 그룹 채팅을 만남으로 변환하는 오픈 웨이트 AI와 함께
RevSofia
불가리아 소피아의 자동차 모임을 다룹니다. 일반 만남은 모든 자동차 팬에게 열려 있으며, 클럽 만남은 브랜드별 커뮤니티(BMW, 메르세데스-벤츠, 아우디/VW 그룹)를 위한 것입니다. 모든 만남 정보는 휴대폰 캘린더에 바로 추가할 수 있으며, 월간 달력(Calendar 버튼은 상단 탐색 메뉴에 있음)을 통해 다가오는 일정을 확인할 수 있습니다. 이 사이트 자체에는 채팅 기능이 없으며, 그 활동은 Discord에서 이루어집니다.
사이트: https://georgievalex.github.io/revsofia/ Discord: https://discord.gg/epUkNA99a
AI 기능: 그룹 채팅을 붙여넣기만 하면 만남 정보 획득
만남은 채팅 스레드에서 조직됩니다. 예:
지난 프로젝트와 마찬가지로 '의도적으로 지루한' 규칙을 적용했습니다: 프레임워크(framework) 없음, 데이터베이스(database) 없음, npm 의존성(dependencies) 없음.
browser ── GitHub Pages (정적 사이트) + Cloudflare Turnstile
│ 붙여넣은 채팅 내용 + 일회용 "저는 사람입니다" 토큰 POST
▼
...
- 모임 정보는 리포지토리(repo) 내의 JSON 파일로 관리됩니다. 작은 Node 빌드 스크립트가 이 파일들을 검증하고, 사이트 데이터와 모임별
.ics캘린더 파일을 출력합니다. 모임을 승인하는 것은 말 그대로 파일을 병합(merge)하는 행위입니다. - AI 부분은 DigitalOcean Function 하나로 처리됩니다. 이를 통해 모델 키가 브라우저에 절대 노출되지 않도록 합니다. 이 함수는 채팅 내용을 8,000자로 제한하고, 상위 시스템 오류(upstream errors)를 클라이언트에게 절대 전달하지 않으며, 모델의 JSON 출력을 빌드에서 사용하는 것과 동일한 검증 모듈을 통해 거칩니다. 따라서 모델이 "porsche"라는 클럽을 환각적으로 생성하더라도 아무것도 고장 나지 않도록 General로 제한됩니다.
- Gemma 4는 DigitalOcean Serverless Inference 위에서 실행됩니다. 이는 OpenAI와 호환되는 엔드포인트(endpoint)이기 때문에 관리해야 할 GPU가 없습니다. 모델은 단 한 줄의 설정으로 구성됩니다.
- 로고는 손으로 그린 SVG 파일입니다. 소피아의 모든 모임이 결국 그 산을 향해 운전하게 되기 때문에, Vitosha 능선을 아래에 두고 빨간색 영역까지 휩쓰는 회전계기판(rev gauge) 모양입니다. 이 로고는 삼각함수(trig) 몇 줄로 생성되어 눈금과 호가 정확한 각도에 위치하도록 했습니다.
- 승인된 모임 정보는 스스로 공지됩니다. GitHub Action이
data/events/폴더를 감시합니다. 새로운 모임 파일이main브랜치에 도착하면, Actions secrets에 보관된 웹훅(webhook)을 통해 클럽 디스코드 채널에 임베드(embed) 게시물을 올립니다. 언급(@mention) 기능은 꺼져 있어, 누군가의 채팅 텍스트 속에 숨어있는@everyone이 전체 서버를 호출하는 것을 방지합니다. 예시 모임이나 이미 종료된 모임 정보는 절대 게시되지 않습니다. - 시간대(Time zone)에는 별도의 테스트가 적용됩니다. 불가리아가 서머타임에서 표준시로 변경되는 날은 10월 25일입니다. 이날 오전 10시에 열리는 모임은 UTC+3이 아닌 UTC+2이며, 캘린더 파일에는 반드시 08:00Z라고 명시되어야 합니다. "모임 시간이 한 시간 틀렸다"는 종류의 버그가 사람들이 빈 주차장에 서 있게 만드는 문제이기 때문에, 이 경우를 위한 테스트가 존재합니다.
총 57개의 테스트 케이스가 있으며, 모두 node:test를 사용하고 별도의 테스트 프레임워크도 사용하지 않았습니다.
내가 잘못했던 점들
실제 라이브 모델을 대상으로 처음 테스트를 진행했을 때, 단위 테스트가 가르쳐준 것보다 더 많은 것을 배웠습니다.
'토요일 아침(Saturday morning)'이 이미 지나간 토요일이 되어버렸습니다. 제가 테스트를 토요일 저녁에 실행했거든요. 모델은 날짜는 알았지만 시간을 몰랐기 때문에, 'foliage drive saturday morning'이라는 문구는 그날의 같은 아침을 의미하게 되었고, 이미 시간상으로는 끝난 상태였습니다. 자체 공지에는 오늘 날짜 옆에 '내일(tomorrow)'이라고 적혀 있었습니다. 이 문제를 해결하기 위해 두 가지 수정이 이루어졌습니다. 첫째, 프롬프트가 현재 Sofia 시간을 받도록 했고, 둘째, 함수는 과거에 시작하는 모든 항목을 미해결(unresolved)로 표시합니다. 이렇게 하면 모델이 또다시 잘못 처리하더라도, 주최자는 이미 발생한 모임 대신 빨간색 필드를 보게 됩니다.
영어 채팅에서 불가리아어 공지가 나왔습니다. 저는 모델에게 '채팅의 언어가 가장 많이 사용된 언어를 사용하도록' 지시했었습니다. 하지만 그것은 신뢰할 수 있게 작동하지 않았습니다. 이제는 함수가 스스로 언어를 결정하고, 모델에게 어떤 언어로 작성해야 할지 알려줍니다. 제 초기 버전은 키릴 문자(Cyrillic)와 라틴 문자를 단순히 계산하는 방식이었는데, 코드 리뷰를 통해 일반적인 불가리아어 Discord 붙여넣기 내용이 이 방식을 통과하지 못한다는 것을 발견했습니다. 예를 들어 ivo_bmw_e46 같은 사용자 이름이나 지도 링크, 자동차 모델명은 모두 라틴 문자였습니다. 그래서 먼저 사용자 이름과 링크를 제거한 다음, 남아있는 내용 중 약 3분의 1 정도가 키릴 문자인 경우 불가리아어라고 판단합니다. 이것은 지루하고 결정론적인 규칙이지만, 저는 이 방식이 '느낌'보다 더 신뢰가 갑니다.
배포 과정에서 CORS 문제가 발생했는데, 로컬 테스트로는 절대 잡을 수 없는 종류였습니다. 브라우저가 모든 요청을 거부했습니다. 그 이유는 응답에 Access-Control-Allow-Origin: *, https://georgievalex.github.io라고 적혀 있었기 때문입니다. 이것은 두 개의 값이 포함되어 있어 허용되지 않았습니다. DigitalOcean의 게이트웨이가 자체적으로 *를 추가했고, 제 함수가 실제 원본 주소(origin)를 덧붙인 것입니다. project.yml 파일에 있는 하나의 어노테이션(web-custom-options: true)이 CORS 처리를 완전히 함수에 위임하며, 이 과정에서 원본 주소가 제 사이트로 고정되게 만들었습니다.
거의 누구나 청구서를 발행할 수 있는 엔드포인트를 배포할 뻔했습니다. CORS는 브라우저만 막습니다. curl을 가진 사람은 함수를 루프(loop)로 호출할 수 있고, 모든 호출은 Gemma 토큰과 함수 시간을 비용으로 발생시킵니다. AI 부분은 DigitalOcean에서 선불 처리되어 있어, 자동 재로드(auto-reload)가 꺼져 있다면 최악의 경우 잔액이 바닥나는 것뿐입니다. 하지만 함수는 사후에 청구되므로, 루프를 돌리면 실제 사용자들을 위한 플래너(planner) 자체가 다운될 수도 있습니다. 그래서 이제 플래너에는 Cloudflare Turnstile 확인 절차가 추가되었습니다. 이 함수는 요청에 신선하고 일회용 토큰이 포함되어 있고, 이 토큰은 방문자의 IP와 제 호스트 이름과 대조하여 검사할 때만 모델을 호출합니다. 위조된 토큰은 단 하나의 토큰이 소모되기 전에 약 0.3초 만에 403 오류를 받습니다.
여전히 약간의 상상력을 발휘합니다. 한 테스트 채팅에서 어떤 사람이 자신의 차 이름을 언급하지 않고
- DigitalOcean을 가장 잘 활용하는 방법: DigitalOcean Function이 백엔드를 호스팅하고, DigitalOcean Serverless Inference가 오픈 웨이트 모델에 서비스를 제공합니다. 지출 상한선은 자동 재충전이 꺼진 선불 추론(Inference) 잔액입니다.
- Gemma를 가장 잘 활용하는 방법:
gemma-4-31B-it가 복잡하게 얽힌 다자 채팅을 읽어 두 가지 언어로 구조화되고, 날짜와 위치가 지정된 만남으로 변환하는 실제 작업을 수행합니다.
다음 단계
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기