볼링 점수 기록 앱을 만들다 보면 처음에는 “내 점수만 잘 저장하면 되지 않을까?”라고 생각하기 쉽습니다. 하지만 실제 볼링은 혼자보다 클럽, 지인, 정기전처럼 함께 치는 경우가 많습니다.
Strike Log에서도 그래서 클럽 기능을 넣었습니다. 다만 단순히 “클럽 만들기” 버튼 하나로 끝내면 운영 문제가 바로 생깁니다. 아무나 클럽을 만들 수 있으면 중복 클럽이 늘어나고, 아무나 가입할 수 있으면 실제 클럽 운영자가 멤버를 관리하기 어렵습니다.
이번 글에서는 Strike Log에서 클럽 운영 기능을 어떻게 설계했는지 정리해보겠습니다.

클럽 기능의 핵심 정책
Strike Log의 클럽 기능은 크게 세 가지 정책을 기준으로 만들었습니다.
첫 번째는 클럽 생성 승인제입니다. 일반 사용자가 바로 클럽을 생성하는 대신, 클럽 생성 신청을 먼저 보냅니다. 플랫폼 관리자가 신청 내용을 확인한 뒤 승인하면 실제 클럽이 생성됩니다.
두 번째는 가입 승인제입니다. 사용자가 클럽에 가입 신청을 하면 클럽장 또는 운영진이 승인해야 멤버가 됩니다.
세 번째는 1인 1클럽 정책입니다. 한 사용자가 여러 클럽에 동시에 가입하면 평균 점수, 클럽 랭킹, 실시간 경기 권한이 복잡해집니다. 그래서 Strike Log에서는 이미 가입한 클럽이 있으면 다른 클럽 가입 신청을 막았습니다.
역할은 OWNER, STAFF, MEMBER로 나누기
클럽 운영 권한은 세 단계로 나눴습니다.
- OWNER: 클럽장
- STAFF: 운영진
- MEMBER: 일반 멤버
일반 멤버는 클럽 조회, 랭킹 확인, 경기 참여 같은 기본 기능을 사용할 수 있습니다. 운영진은 가입 신청 승인, 공지 작성, 정기전 관리, 일반 멤버 추방 같은 운영 업무를 맡습니다. 클럽장은 여기에 더해 운영진 임명, 운영진 해제, 클럽장 이양, 클럽 삭제 같은 소유권에 가까운 권한을 가집니다.
이렇게 나눈 이유는 “운영 업무”와 “소유권 변경”을 분리하기 위해서입니다. 운영진에게 모든 권한을 주면 편하긴 하지만, 운영진이 다른 운영진을 내보내거나 클럽장을 바꾸는 상황은 위험합니다.
그래서 서버에서는 assertOwner, assertStaffOrAbove, getMembership 같은 권한 헬퍼를 두고, 기능마다 필요한 권한을 명확히 검증하도록 했습니다.

클럽 생성은 신청과 승인으로 분리
클럽 생성 흐름은 바로 생성이 아니라 신청 기반입니다.
사용자가 클럽 이름, 설명, 활동 지역 등을 입력해 생성 신청을 보냅니다. 서버는 기존 클럽 이름뿐 아니라 아직 승인 대기 중인 신청 이름까지 확인해 중복을 막습니다.
관리자는 생성 신청 목록을 보고 승인하거나 반려할 수 있습니다. 승인되면 실제 groups 데이터가 만들어지고, 신청자는 클럽장으로 등록됩니다. 반려되면 반려 사유를 남길 수 있게 했습니다.
이 구조는 조금 번거로워 보이지만, 실제 서비스 운영 관점에서는 장점이 큽니다. 의미 없는 클럽 생성을 줄일 수 있고, 같은 이름의 클럽이 여러 개 생기는 문제도 미리 막을 수 있습니다.
가입도 바로 가입이 아니라 신청으로 처리
클럽 가입 역시 바로 멤버가 되는 방식으로 만들지 않았습니다.
사용자가 클럽 탐색 화면에서 가입 신청을 보내면, 클럽장과 운영진에게 알림이 갑니다. 운영진 이상 권한을 가진 사용자는 가입 신청 목록에서 승인 또는 거절을 처리합니다.
승인되면 GroupMember가 생성되고, 사용자의 역할은 기본적으로 MEMBER가 됩니다. 이후 클럽장이 필요하면 해당 멤버를 STAFF로 승격할 수 있습니다.
여기서 중요한 예외 처리는 세 가지였습니다.
- 이미 같은 클럽에 가입된 사용자는 다시 신청할 수 없음
- 이미 다른 클럽에 가입된 사용자는 신청할 수 없음
- 같은 클럽에 대기 중인 신청이 있으면 중복 신청할 수 없음
이런 조건을 서버에서 막아두면 프론트 UI가 조금 실수하더라도 데이터가 꼬이지 않습니다.

1인 1클럽 정책을 둔 이유
처음에는 여러 클럽 가입도 가능하게 만들 수 있었습니다. 하지만 Strike Log의 클럽 기능은 단순 게시판이 아니라 경기 기록, 평균 점수, 클럽 랭킹, 실시간 경기방과 연결됩니다.
한 사용자가 여러 클럽에 속하면 “이 기록은 어느 클럽 기록인가?”, “클럽 평균에 어떤 점수를 반영해야 하나?”, “실시간 클럽 경기 권한은 어디까지 허용해야 하나?” 같은 문제가 생깁니다.
그래서 초기 버전에서는 명확하게 1인 1클럽으로 제한했습니다. 사용자가 다른 클럽에 들어가고 싶다면 먼저 기존 클럽에서 탈퇴해야 합니다.
제약이 있는 설계지만, 서비스 초반에는 기능을 단순하게 유지하고 운영 정책을 명확히 하는 쪽이 더 낫다고 판단했습니다.
체험판 상태와 클럽 접근 권한
클럽 기능에는 체험판 개념도 들어갔습니다.
클럽은 trial, active, expired, cancelled 같은 구독 상태를 가집니다. 체험판 클럽은 시작 시각과 만료 시각을 저장하고, 만료가 가까워지면 알림을 보냅니다.
서버에서는 클럽 기능 접근이 필요한 곳에서 “사용자가 활성 클럽 접근 권한을 가지고 있는지” 확인합니다. 가입한 클럽 중 정식 활성 상태이거나, 아직 만료되지 않은 체험판 클럽이 있으면 접근을 허용합니다.
반대로 체험판이 만료되면 클럽 게임이나 실시간 경기방 같은 기능에서 club_trial_expired 코드를 내려보내고, 앱에서는 만료 안내 다이얼로그를 띄우도록 연결했습니다.
클럽 랭킹과 운영 화면
클럽 기능이 운영 정책만 있으면 딱딱합니다. 실제 사용자가 계속 들어오게 하려면 볼 이유가 있어야 합니다.
그래서 클럽 멤버의 평균 점수를 기반으로 리더보드를 만들었습니다. 클럽 상세 화면에서는 멤버 목록과 평균 점수를 확인할 수 있고, 같은 클럽 멤버끼리는 상세 통계도 볼 수 있게 했습니다.
운영진 이상에게는 가입 신청 관리 진입점과 뱃지를 보여줍니다. 새 가입 신청이 오면 알림을 보내고, 앱 하단 클럽 탭에도 표시해 놓치지 않도록 했습니다.
구현하면서 느낀 점
클럽 기능은 화면보다 정책이 더 중요했습니다.
버튼을 만들고 API를 붙이는 일보다 어려웠던 건 “누가 무엇을 할 수 있는가”, “어떤 상태에서는 막아야 하는가”, “나중에 데이터가 꼬이지 않으려면 어디서 검증해야 하는가”를 정하는 일이었습니다.
특히 운영 권한은 프론트에서만 숨기면 안 됩니다. 앱 화면에서 버튼을 숨기더라도 API를 직접 호출할 수 있기 때문에, 최종 검증은 반드시 서버에서 해야 합니다.
Strike Log에서는 Flutter 앱에서 사용자에게 자연스러운 화면을 보여주고, NestJS 서버에서 권한과 정책을 한 번 더 강하게 막는 식으로 역할을 나눴습니다.
마무리
이번 편에서는 Strike Log의 클럽 운영 기능을 정리했습니다.
- 클럽 생성은 관리자 승인제로 관리한다.
- 가입은 클럽장/운영진 승인제로 처리한다.
- 사용자는 하나의 클럽에만 속하게 해 정책을 단순화한다.
- OWNER, STAFF, MEMBER 역할을 나눠 권한을 명확히 한다.
- 체험판 만료와 알림까지 서버 정책으로 연결한다.
다음 편에서는 사용자가 앱을 다시 열고 싶게 만드는 요소인 배지와 출석 streak 기능을 다뤄보겠습니다.
'APP' 카테고리의 다른 글
| FCM 푸시와 인앱 알림을 함께 연결하기 (0) | 2026.07.07 |
|---|---|
| 배지와 출석 streak로 다시 켜고 싶은 앱 만들기 (0) | 2026.07.06 |
| Socket.IO로 클럽 실시간 볼링 경기방 만들기 (0) | 2026.07.01 |
| NestJS와 TypeORM으로 볼링 기록 API 설계하기 (0) | 2026.06.29 |
| Dio와 JWT로 게임 기록 저장하기: 실패해도 UX가 무너지지 않게 (0) | 2026.06.28 |
