풀빌라 예약 자동화
풀빌라의 예약 채널들을 관리자 페이지를 통해 동기화하고, 손님 문자와 추가 요금 확인까지 처리하는 시스템
- 기간
- 2026년 5월 – 지금
- 대상
- 한국의 작은 풀빌라 업체
- 역할
- 1인 개발: 설계, 개발, 배포, 운영
- 기술
- TypeScript, Next.js, Playwright, Prisma, PostgreSQL, Docker, Cloudflare Workers, SOLAPI
- 규모
- 커밋 460개, 4.4만 줄, Prisma 모델 17개, 테스트 파일 40개
- 외주 작업, neoworks
다른 언어: english
어떤 프로젝트인가요
객실 타입이 4개인 작은 풀빌라가 같은 객실을 네이버 스마트플레이스, 여기어때, 야놀자에서 동시에 팔고 있어요. 코드상으로는 Airbnb와 Agoda 예약도 iCal로 가져올 수 있어요. 제가 만든 건 이 업체의 백오피스예요. 5분마다 cron 컨테이너가 모든 채널의 예약을 읽어서, 한 채널에서 방이 팔리면 다른 채널에서는 그 방을 닫고, 취소되면 다시 열어요. 그 위에 손님 문자 발송, 은행 입금 메일과 대조하는 추가 요금 정산, 네이버 웍스 캘린더, 한국어 관리자 대시보드가 올라가 있어요.
서버 한 대에 컨테이너 세 개로 돌아가요. Playwright가 들어 있는 Next.js 앱, Postgres, 그리고 아주 작은 alpine cron이에요. 은행 메일은 Cloudflare 이메일 워커를 거쳐 앱으로 들어와요.
어려웠던 부분
여기에는 파트너 API가 없어요. 전부 사장님 본인의 관리자 계정으로 로그인해서, 페이지를 직접 다루거나 그 뒤에 있는 내부 API를 호출하는 방식이에요. 그리고 채널마다 망가지는 방식이 다 달라요.
네이버가 계정을 잠갔고, 제 첫 진단은 틀렸어요. 9월 13일에 네이버가 로그인 버튼 마크업을 바꿨어요. 제 코드는 그 버튼을 셀렉터 하나로만 찾고 있어서 모든 로그인이 타임아웃 났어요. 게다가 네이버 쪽 로직에는 백오프가 없어서, 5분 cron이 계속 새 로그인을 시도했어요. 시간당 85번 정도, 3일 동안 138번 연속 실패였어요. 네이버는 계정에 캡차를 걸었어요. 저는 사장님께 본인 브라우저에서 인증을 통과해 달라고 했는데, 그건 애초에 될 수가 없었어요. 캡차는 그걸 일으킨 세션에 묶여 있거든요. 그리고 원인은 headless 브라우저의 fingerprint였어요. 같은 IP, 같은 계정으로 10분 간격을 두고 시험해 보니 headless는 캡차가 떴고, Xvfb 위에서 automation 플래그를 끈 headful chromium은 뜨지 않았어요. 지금은 비밀번호를 입력하는 경로만 headful로 돌고, 5분마다 하는 폴링은 저장된 쿠키로 headless 그대로 돌아요. 캡차를 풀어도 끝이 아니에요. 네이버가 로그인 페이지로 다시 보내기 때문에, 그 페이지에서 비밀번호를 한 번 더 제출해야 해요.
토큰은 수명을 속여요. 야놀자 토큰은 60분 동안 유효하다고 적혀 있지만, 실제로는 15~16분쯤 지나면 거부돼요. 게다가 iat와 exp가 초가 아니라 밀리초 단위라서, 그대로 읽으면 58560년에 만료되는 걸로 나와요. 사장님이 말한 "자꾸 로그아웃돼요"가 바로 이거였어요. 지금은 토큰에 적힌 수명과 실제로 측정한 수명 중 짧은 쪽을 기준으로 미리 갱신하고, 측정값은 줄어들기만 해요. 어느 날은 야놀자가 로그인 후 "비밀번호를 변경해 주세요" 페이지에 멈춰 세우기 시작했어요. "30일 후에 변경하기"를 클릭하게 고쳤는데, 알고 보니 그게 버튼이 아니라 div였어요.
"완료"라고 했는데 완료가 아니었어요. 네이버에서 방을 열려면 판매 스위치와 재고 수량, 두 군데를 써야 해요. 코드는 스위치만 켜고 수량은 확인하지 않은 채 성공이라고 보고했어요. 그래서 대시보드에는 열림으로 나오는데 실제로는 방을 팔 수 없는 상태였어요. 지금은 모든 쓰기를 확인해요. 재고를 다시 올리는 건 사장님이 명시적으로 방을 하나 더 열어 달라고 한 날짜에만 해요. 다른 날짜는 사장님이 일부러 손으로 재고를 낮춘 거라서, 다음 cron 주기가 그걸 몰래 되돌리면 안 되거든요.
같은 패턴이 다른 곳에서도 계속 나왔어요.
- 정산에서 9만 원 청구에 3만 원 입금을 결제 완료로 처리했어요. 매칭 조건이 입금자 이름 하나뿐이었기 때문이에요. 결제 완료 76건 중 2건이 금액이 모자랐어요. 지금은 입금 합계가 청구 금액을 채워야만 결제 완료가 돼요.
- 객실 번호를 화면을 그릴 때마다 손님 이름순으로 다시 계산하고 있어서, 당일 예약이 하나 들어오면 이미 배정된 손님이 다른 객실로 밀려날 수 있었어요. 직원들은 그 번호를 보고 손님을 방으로 안내해요. 지금은 배정을 저장하고, 먼저 배정된 쪽이 유지돼요.
- cron 컨테이너가 3일 동안 죽어 있었어요.
unless-stopped는 docker가 stopped로 보는 컨테이너는 다시 띄우지 않고, 헬스 체크는 감시해야 할 cron 안에 들어 있어서 모든 게 초록불이었어요. 지금은 앱 프로세스 안에 watchdog이 있어요. 60분 동안 조용하면 문자를 보내고 동기화를 직접 돌리는데, 락을 하나로 공유해서 손님이 같은 문자를 두 번 받지 않아요. 이 watchdog의 테스트는 실제 장애를 그대로 재현해요. 72시간 동안 알림 432개가 아니라 12개예요.
나머지 시스템
캘린더 동기화는 세 번 바뀌었어요. 처음에는 ICS 피드였어요. 다음에는 취소 tombstone을 넣었어요. 구독 캘린더는 피드에서 그냥 사라진 일정을 지우지 않거든요. 그다음에는 네이버 웍스 캘린더 API에 직접 쓰는 방식으로 바꿨어요. 웍스는 구독 피드의 STATUS:CANCELLED를 무시하기 때문이에요. 전환할 때 예약 205건을 쓰고 다시 읽어서 확인했고, 나중에 백필로 지난 일정 539건을 추가했어요.
같은 업체를 위해 몇 가지를 더 만들었어요. Google Sheet 지식 베이스로 손님 질문에 답하고 이 시스템의 캘린더 피드에서 예약을 조회하는 카카오 채널 챗봇, 인스타그램과 네이버 블로그, 네이버 카페에 사진을 올리는 윈도우 프로그램, 그리고 네이버 플레이스 키워드 도구예요.
지금은
실제로 매일 쓰이고 있어요. 가장 최근 수정은 이번 주 거예요. 네이버의 주간 헤더가 새해에 걸친 주에는 형식이 달라서 그걸 처리했고, 네이버가 아직 열지 않은 달의 객실 막기는 이제 빨간 배너를 띄우지 않고 조용히 기다려요. 460개 커밋 대부분이 같은 흐름을 따라요. 사장님의 제보, 실제 환경에서의 측정, 그리고 회귀 테스트예요.