본문으로 건너뛰기Hwanseok Sim

작성일

VisitCall 개발기 1 — SMS 링크 프로토타입에서 Flutter와 Express로

React Native 앱을 Flutter로 옮기고 SMS 링크로 방문 차량을 받던 첫 프로토타입에서, 앱에서 차량번호를 직접 등록해 주차관제 쪽으로 넘기는 구조로 바뀌기까지의 2025년 3~7월 기록.

VisitCall 개발기 시리즈 1/8편. 전체 목록은 visitcall 태그에서 볼 수 있다.

VisitCall은 입주민이 손님 차량번호를 앱에 미리 등록하면 단지 차단기가 방문 차량으로 인식하게 하는 서비스다. 지금의 모습은 처음 만든 프로토타입과 꽤 다르다. 처음 몇 달은 문자 메시지로 링크를 보내 손님이 직접 차량번호를 넣게 하는 방식이었고, 서버·앱·관리자 웹은 서로 다른 저장소에서 따로 자랐다.

React Native 앱을 Flutter로 옮긴 첫 커밋

가장 오래된 커밋은 2025-03-31이고 메시지는 "ReactNative앱 Flutter로 컨버팅"이다. 이미 있던 React Native 앱을 Flutter로 다시 쓰면서 저장소를 새로 열었다. 첫 커밋의 화면은 로그인과 회원가입, 홈, 메시지 보내기, 내 정보, 사용량 조회 정도였다. 프로젝트 소개에는 개발 시작이 2025년 5월로 적혀 있지만 앱 저장소는 3월 말에 열었다.

앱은 서버 주소를 코드에 상수로 박아 두고 http 패키지로 직접 호출했다. 서비스 클래스마다 URL과 헤더를 따로 조립했고, 이 구조는 5편의 출시 전 점검과 8편의 공통 ApiClient 도입까지 이어진다. 한 코드베이스로 iOS와 Android를 같이 내보낼 수 있다는 점 때문에 Flutter를 택했고, 이후 바꾼 적이 없다.

처음 설계는 SMS 링크였다

그때의 API 문서(ReadME_API.md)에 처음 흐름이 남아 있다.

POST /api/auth/login            요청 id, password → 응답 token, user
POST /api/sms/send              세대주가 손님 번호와 문구를 보냄 → 방문자 링크 생성
GET  /api/visitor/validate-link/:uniqueString   링크 유효성 검사
POST /api/visitor/register      손님이 링크에서 차량번호를 등록

세대주가 앱에서 손님 연락처를 고르면 서버가 일회용 링크가 담긴 문자를 보내고, 손님이 그 링크의 웹 페이지에서 차량번호를 입력한다. 서버에는 문자 발송용 모델, 라우트, 서비스가 있었고 환경 변수에도 문자 서비스 키 항목이 따로 있었다. 앱의 message_service.dart는 연락처 권한까지 요청했다.

같은 달 관리자 웹도 처음 만들었다. 2025-04-07 첫 커밋의 React 앱이었고 로그인, 대시보드, 세대 목록, 메시지 사용량 화면이 있었다. 할 일 메모의 첫 줄이 "메시지 사용량 메뉴 구체화, 전체 문자 로그 표시"였으니 이때 관리자 웹은 문자 발송 내역을 보는 도구였다.

서버: Express, Sequelize, MySQL, Docker

서버 저장소의 첫 커밋은 2025-05-05이다. Express, Sequelize ORM, MySQL로 시작했고 처음부터 docker-compose.yml과 Dockerfile을 두었다. 모델은 아파트, 세대, 계정, 방문자 정도의 단순한 관계였다.

Apartment (1) → (N) Host(세대) → (N) Account
                            → (N) Visitor

스택을 비교한 문서는 없다. 혼자 만들어서 한 단지에 빨리 내놓는 것이 목표였기 때문에 익숙하고 자료가 많은 조합을 골랐다. 인증은 JWT, 비밀번호는 bcrypt 해시였고 이 선택은 1.x가 끝날 때까지 바뀌지 않았다. 저장소를 셋으로 나누면서 API 문서도 저장소마다 사본이 생겼고, 곧 코드와 어긋나기 시작했다.

6월 21일에 문자 발송 코드를 걷어냈다

2025-06-21에 문자 발송 코드를 한 커밋에서 걷어냈다. 커밋 메시지에는 날짜만 적었다. 지운 양은 다음과 같다.

파일변화
문자 라우트281줄 삭제
문자 서비스154줄 삭제
문자 모델107줄 삭제
방문자 웹 페이지233줄 변경, 내용 단순화
README 환경 변수문자 서비스 키 4개 항목 삭제
입주민 라우트37줄 추가

이 뒤로 손님이 링크를 여는 단계가 없어지고, 입주민이 앱에서 바로 차량번호를 넣는 흐름이 됐다.

7월 4~5일, 주차장 시스템에 처음 붙인 날

7월 5일 커밋은 "목업 서버랑 붙임"이다. 실제 단지의 주차관제 DB에 붙기 전에, 전달 형식을 맞춘 목업 서버를 상대로 먼저 연동했다. 서버의 visitorService에 등록 정보를 주차장 시스템으로 보내는 함수가 이때 생겼다. 구조만 옮기면 아래와 같고, 필드 이름은 업체 쪽 이름 대신 일반 용어로 바꿨다.

services/visitorService.js
// 방문 등록을 주차장 시스템으로 전송 (필드 이름은 일반 용어로 바꿈)
const payload = {
  siteCode: visitor.apartment_id,       // 단지 코드
  plate: visitor.car_number,            // 차량번호
  driverName: host.resident_name,
  driverPhone: visitor.phone_number,
  driverNote: '방문객',
  validFrom: toLocalISOString(visitor.visit_date),
  validTo: toLocalISOString(visitor.leave_date),
  registerType: registrationType,       // 앱 등록과 시스템 자동 등록을 구분
};
await axios.post(`${parkingSystemUrl}/api/parking/register`, payload);

전송이 실패해도 로그만 남기고 넘어가게 짰다. 나중에 등록 재시도 상태 테이블이 생긴 이유다. 시각도 toLocalISOString으로 서버 로컬 시간 기준으로 만들었는데, 서버가 한국 시간대에 있다는 가정이 여기서 시작됐다.

다음 글

VisitCall 개발기 2 — 입차 푸시 알림과 중복 제거