
Strike Log 제작기 네 번째 글입니다. 앞선 글에서는 Flutter에서 10프레임 볼링 점수를 입력하고 계산하는 흐름을 다뤘습니다. 하지만 앱이 실제 기록 앱이 되려면 계산 결과가 화면에만 머물러서는 안 됩니다. 사용자가 친 게임을 저장하고, 다시 불러오고, 평균과 최고점 같은 통계를 보여줄 수 있어야 합니다.
그래서 이번 단계에서는 NestJS와 TypeORM으로 볼링 기록 API를 설계했습니다. 핵심은 단순히 점수 하나를 저장하는 것이 아니라, 나중에 통계, 시리즈, 클럽 경기까지 확장할 수 있는 구조를 만드는 것이었습니다.
Flutter 화면 뒤에는 기록 API가 필요했다
점수 입력 화면에서 총점이 계산되면 사용자는 자연스럽게 그 기록이 남기를 기대합니다. Strike Log의 백엔드는 이 흐름을 받아 게임 저장, 프레임 저장, 통계 조회, 시리즈 관리까지 담당합니다.
백엔드는 NestJS 11과 TypeORM, MySQL 기반으로 구성했습니다. 앱에서는 Dio를 통해 API를 호출하고, 서버에서는 JWT 인증을 기준으로 사용자의 기록을 분리합니다. 모든 API를 공개하는 대신 전역 인증 가드를 두고, 필요한 라우트만 예외로 여는 방식으로 잡았습니다.

games와 frames를 분리한 이유
가장 먼저 나눈 것은 Game과 Frame입니다. Game은 볼링 한 게임의 대표 기록을 담습니다. 사용자 ID, 총점, 경기 날짜, 장소, 개인/클럽 게임 여부, 방 코드, 순위, 시작·종료 시각 같은 값이 여기에 들어갑니다.
반면 Frame은 1프레임부터 10프레임까지의 상세 투구 기록을 담당합니다. 각 프레임에는 프레임 번호, 첫 번째 투구, 두 번째 투구, 10프레임 전용 세 번째 투구, 계산된 점수가 저장됩니다.
처음에는 총점만 저장해도 충분해 보일 수 있습니다. 하지만 Strike Log는 단순 점수장이 아니라 기록 분석 앱에 가깝습니다. 스트라이크 개수, 스페어 개수, 오픈 프레임, 올커버 여부, 최장 연속 스트라이크 같은 통계는 프레임 데이터가 있어야 계산할 수 있습니다. 그래서 총점은 games에, 세부 투구 기록은 frames에 분리했습니다.
시리즈 기록을 위한 game_series
볼링은 한 게임만 치고 끝나는 경우보다 3게임, 6게임처럼 묶어서 보는 경우가 많습니다. 이 흐름을 위해 game_series 엔티티를 따로 만들었습니다.
GameSeries는 사용자 ID, 목표 게임 수, 시작 시각, 종료 시각을 가집니다. 각 게임은 series_id와 series_index를 통해 어떤 시리즈의 몇 번째 게임인지 연결됩니다.
여기서 중요한 점은 목표 게임 수만큼 저장됐다고 서버가 자동 완료 처리하지 않는다는 것입니다. 시리즈 종료는 클라이언트가 /game-series/:id/complete를 호출해 명시적으로 처리합니다. 사용자가 중간에 멈추거나 예외 상황이 생길 수 있기 때문에, 자동 판단보다 앱의 흐름에 맞춰 완료 시점을 정하는 방식을 선택했습니다.
API는 저장과 조회를 분리했다
게임 저장은 POST /games에서 처리합니다. 이때 총점, 프레임 목록, 날짜, 장소, 클럽 경기 여부, 시리즈 정보가 함께 들어옵니다. 저장 이후 새로 획득한 배지가 있으면 응답에 포함해 앱에서 바로 모달로 보여줄 수 있게 했습니다.
조회 API는 목적별로 나눴습니다. 내 기록 목록은 /games/me, 특정 게임 상세는 /games/:id/detail, 사용자 통계는 /games/users/:user_id/statistics, 이번 달 프레임 통계는 /games/users/:user_id/monthly-frame-stats에서 가져옵니다.
시리즈 쪽은 /game-series로 시작하고, /game-series/:id/complete로 종료합니다. 최근 시리즈와 베스트 시리즈도 별도 API로 분리해 홈 화면이나 프로필 화면에서 필요한 데이터를 바로 가져올 수 있게 했습니다.
권한 정책은 기능마다 다르게 봤다
기록 앱에서 중요한 것은 내 기록을 안전하게 보호하는 것입니다. 그래서 기본적으로 다른 사용자의 상세 기록은 볼 수 없게 했습니다. 게임 상세 조회는 본인만 가능하게 두었습니다.
다만 클럽 기능에서는 예외가 필요했습니다. 같은 클럽 멤버끼리는 평균이나 월별 프레임 통계를 비교할 수 있어야 합니다. 그래서 통계 API에 한해 본인, 관리자, 같은 클럽 멤버까지 접근을 허용했습니다. 상세 기록은 닫고, 비교에 필요한 통계만 열어둔 셈입니다.
마이그레이션으로 운영 환경을 맞추기
개발 중 엔티티가 바뀌면 로컬에서는 바로 확인할 수 있지만, 운영 환경에서는 데이터베이스 스키마를 안전하게 따라가야 합니다. Strike Log API는 TypeORM 마이그레이션을 두고, Railway 환경에서도 필요한 테이블과 컬럼이 적용되도록 구성했습니다.
특히 game_series 테이블과 games.series_id, games.series_index 컬럼은 기존 게임 기록 기능 위에 시리즈 기능을 얹기 위한 변화였습니다. 기능을 추가하더라도 기존 기록이 깨지지 않도록 관계는 신중하게 잡았습니다.
마무리
이번 4편의 핵심은 점수 저장 API를 넘어서 나중에 확장 가능한 기록 구조를 만드는 것이었습니다. games는 한 게임의 대표 정보, frames는 투구 단위의 상세 기록, game_series는 여러 게임을 묶는 흐름을 담당합니다.
이 구조 덕분에 평균, 최고점, 프레임 통계, 시리즈 기록, 클럽 비교 같은 기능을 자연스럽게 이어갈 수 있었습니다. 다음 편에서는 이 기록 구조 위에 Socket.IO를 붙여 클럽 멤버들이 함께 참여하는 실시간 볼링 경기방을 만든 과정을 정리해보겠습니다.
'APP' 카테고리의 다른 글
| 클럽 운영 기능 만들기: 가입 승인제와 1인 1클럽 정책 (0) | 2026.07.03 |
|---|---|
| Socket.IO로 클럽 실시간 볼링 경기방 만들기 (0) | 2026.07.01 |
| Dio와 JWT로 게임 기록 저장하기: 실패해도 UX가 무너지지 않게 (0) | 2026.06.28 |
| Flutter 볼링 점수 입력 화면: 10프레임 계산 로직 (0) | 2026.06.25 |
| 볼링 점수 기록 앱 Strike Log 제작기: Flutter와 NestJS로 시작하기 (0) | 2026.06.24 |
