RUNNING — 백엔드·웹 출시 준비 중 — 모바일 앱

힐링민턴파크

배드민턴 체육관의 코트 배정·경기 진행·대기·출석을 서버 권위(server-authoritative) 상태로 관리하고, 관리자·현장 모니터·모바일까지 Socket.IO 실시간 브로드캐스트로 동기화하는 운영 관리 시스템. Flutter 모바일 앱은 QR·NFC 현장 출석과 실시간 조회를 담당하며 출시 준비 중이다.

Node.js Express Socket.IO PostgreSQL Flutter Dart Riverpod Docker JWT
PROBLEM

해결한 문제

운영 상태가 재시작과 함께 사라진다

진행 중인 경기 이력이 메모리와 JSON 파일에만 존재해, 서버를 재시작하면 코트 현황이 모두 초기화됐다.

BEFORE 재시작 시 진행 중 경기·대기 목록 소실. 수동으로 다시 입력.
AFTER PostgreSQL 영속화 + 대기조 스냅샷 테이블로 재시작 후 상태 자동 복원.

화면마다 다른 상태를 보여준다

운영자 화면·TV 모니터·모바일이 각각 다른 상태를 표시해, 코트 배정 결과가 즉시 반영되지 않았다.

BEFORE 화면을 새로 고침해야 최신 상태 확인 가능. 혼선 빈발.
AFTER 모든 쓰기 작업이 DB 트랜잭션 → EventBus → Socket.IO 순으로 전 클라이언트에 동시 브로드캐스트.

출석 중복 처리와 수기 기록

네트워크 재시도나 중복 클릭으로 출석이 이중 저장되고, 운영자가 수기로 출석을 처리해 누락이 잦았다.

BEFORE 동일 회원 출석이 여러 건 기록되는 이중 저장 사고 발생.
AFTER Idempotency-Key 기반 중복 차단 + QR/NFC 현장 스캔으로 출석 자동화.
ARCHITECTURE

데이터 흐름

모든 상태 변경은 단방향 선형 파이프라인을 따른다. 클라이언트는 상태를 계산하지 않고 수신만 한다.

관리자 웹
Flutter 앱
REST

REST API

Idempotency-Key

FOR UPDATE

PostgreSQL

partial unique index

emit

EventBus

in-process

io.emit()

Socket.IO

12토픽 브로드캐스트

수신
TV 모니터
모바일 앱
관리자 웹

쓰기 요청은 Idempotency-Key 래퍼를 통과한 뒤 FOR UPDATE row-locking과 partial unique index가 걸린 트랜잭션으로 커밋된다. 커밋 성공 후 EventBus가 이벤트를 emit하고, Socket.IO가 연결된 전 클라이언트에 동일한 상태를 브로드캐스트한다. 이벤트 토픽 12종은 모두 JSON Schema(ajv)로 계약 테스트된다.

관리자 토큰

JWT_SECRET

웹 운영 콘솔 전용 · 앱에 사용 불가

앱 사용자 토큰

APP_JWT_SECRET

소셜 로그인 · 관리자 API 접근 불가

현장 출석 토큰

VENUE_TOKEN_SECRET

QR/NFC 고정 인쇄용 · 별도 시크릿

ENGINEERING

기술적으로 내세울 점

개발자 관점에서 이 시스템이 어떻게 동작하는지를 코드 수준에서 설명한다.

01

동시성 제어는 DB가 강제한다

코트 배정 시 FOR UPDATE row-locking으로 동시 쓰기를 직렬화하고, 코트당 동시 1경기를 partial unique index (WHERE status='active')로 DB 레벨에서 강제한다. 애플리케이션 코드에 버그가 있더라도 DB가 무결성을 보장한다.

CREATE UNIQUE INDEX uniq_active_match_per_court ON court_match_sessions(court_id) WHERE status = 'active' AND deleted_at IS NULL;
02

소켓 12토픽 전부 계약 테스트

live:state, court:updated, match:started/finished 등 12개 소켓 토픽의 페이로드를 JSON Schema(ajv)로 정의하고 전수 검증한다. 백엔드와 앱·웹 사이의 인터페이스 회귀를 CI에서 자동으로 잡는다.

// 소켓 12토픽 전수 계약 테스트 const { valid, errors } = validateTopic(topic, VALID[topic]);
소켓 계약 테스트 12토픽
03

Idempotency-Key로 중복 저장 차단

네트워크 재시도나 중복 클릭이 발생해도 idempotency_keys 테이블이 동일 요청을 감지해 경기 기록의 이중 저장을 막는다. 출석 요청도 동일한 원칙이 적용되어 동일 토큰의 중복 출석은 에러가 아닌 멱등 응답(200)을 반환한다.

멱등성(Idempotency) 내장
04

앱 테스트 119건 — Flutter analyze 0 issues

Flutter 앱은 flutter_test로 119건의 테스트가 green이며, flutter analyze도 0 issues 상태다. Riverpod 코드 생성(@riverpod)과 JSON 직렬화 코드 생성을 통해 보일러플레이트를 선언적으로 관리한다.

앱 테스트 119건 green
05

서버 시간 오프셋 보정 — 기기 시계를 믿지 않는다

앱은 매 응답의 serverTime으로 ServerClock의 오프셋을 갱신한다. 경기 진행 타이머는 이 보정 시각으로 계산하므로, 기기 시계가 틀려도 모든 화면에서 동일한 진행 시간을 표시한다. WebSocket 전용(transports=['websocket'] 고정)으로 폴링 폴백을 제거해 배터리·트래픽 비용도 줄였다.

WebSocket 전용 — 폴링 제거 서버시간 오프셋 보정