1. 한눈에 보기
SoccerGPS는 기기에서 1차 계산 → 서버에서 검증·재계산하는 2단 구조입니다. 워치(Wear OS)가 GPS 위치와 심박 등 컨디션 지표를 수집하고, 폰이 이를 중계해 서버로 업로드합니다. 네트워크나 블루투스가 끊겨도 각 기기가 로컬(Room DB)에 계속 기록하고 지연 재전송 큐에 보관하므로 경기 데이터가 유실되지 않습니다.
팀(KRTM…)의 감독·코치·사무장이 경기일에 매치(KRMC…)를 만들고, 매치 안에 쿼터(KRQT…, 실제 뛴 구간)를 20/25/30/45분 등 커스텀 시간으로 생성합니다. GPS 트랙과 컨디션 지표는 쿼터 단위로 업로드됩니다. 선수는 고유 ID KRPL…를 갖습니다.
2. 경기 기록 데이터 흐름 (워치 → 폰 → 서버)
- 쿼터 시작 — 폰 블루투스 근처에서 쿼터를 시작하면 워치의 Foreground Service(위치+헬스)가 기동합니다. GPS는 배터리 모드에 따라 1~5초 간격으로 수집하고, 심박 등 컨디션 지표를 1분당 1건 Room DB에 저장합니다.
- 블루투스 중계 — 워치는 수집한 트랙·컨디션을 Wear Data Layer(블루투스)로 폰에 전달합니다. 폰은 이를 수신 큐에 담아 서버로 중계합니다.
- 업로드 — 트랙과 컨디션 지표(
metrics[])를 쿼터 단위로POST /matches/{id}/track?quarterId=…로 전송합니다. - 서버 분석 — Lambda가 원본 트랙을 S3(
tracks/{matchId}/{quarterId}.json)에 보관하고, 정확도 필터로 지표를 재계산하며 심박 평균·최대·최소 컨디션 요약을 DynamoDB에 저장합니다. - 지연 재전송 — 경기 중 폰과 멀어져 워치에서 쿼터를 종료하면, 전송 못 한 데이터를 워치의 영속 재전송 큐에 남깁니다. 나중에 폰 블루투스 범위에 다시 들어오면(또는 워치가 직접 인터넷에 연결되면) 큐를 자동으로 비워 서버에 전송합니다.
- 리포트 조회 —
GET /matches/{id}/track이 1분 간격 마커(minuteMarkers, 동그라미 안 분 숫자 + 심박)를 반환하고,analytics가 컨디션 요약을 제공합니다. 지도에서 마커를 누르면 그 시점 컨디션 팝업이 열려 교체 판단에 활용됩니다. - 구간 속도 색상 — 웹/앱 모두 원본 좌표(
track.points)를 1분 구간으로 나눠 구간 평균 속도를 계산하고, 이동경로 선을 느림(연한 노랑)→중간(주황)→빠름(레드)으로 색칠합니다. 임계값(러닝 12km/h·스프린트 25km/h)은LiveMetrics.kt를 단일 출처로 웹 app.js와 동일하게 유지합니다. - 지도 표시 성능 — 히트맵·경로·1분마커 오버레이는 데이터 도착 시 한 번만 생성하고, 토글 체크박스는
setMap(null/지도)로 보이기/숨기기만 전환합니다. (이전에는 토글마다 오버레이 전체를 파괴·재생성해 클릭 반응이 몇 초씩 늦어졌습니다) 웹은applyOverlayVisibility(), 앱은ResultActivity.applyOverlayVisibility()가 동일한 방식으로 동작합니다. 모바일 앱 경기 결과 화면에도 웹과 동일한 이동경로/히트맵/1분마커 토글이 추가되었습니다. - 1분 마커 표시 통일 — 웹·앱 모두 동그라미 안에 경과 분 숫자를 표시하고, 원 색상은 그 시점 심박수에 따라 초록(안정)→주황→빨강(높음)으로 바뀝니다. 웹은 HTML 마커, 앱은 같은 모양을
Canvas로 그린 아이콘(MapVisuals.minuteMarkerBitmap)을 씁니다. 색 기준은 웹heartColor()와 앱MapVisuals.heartColor()가 동일합니다. - 경기 목록의 일시 — 트랙 업로드 시 요약에 실제 GPS 기록 구간(
trackStartAt/trackEndAt)과durationSec을 함께 저장합니다. 목록은GET /matches?limit=로 최신순 N건만 받아 26년 9월 5일 06:00~13:30 형식으로 표시합니다. 이 필드가 없는 옛 기록은 서버가ANALYTICS에서durationSec을 보강해 시간 범위를 복원하고, 그래도 없으면 날짜만 표시합니다.
3. 구간별 통신 · 보안
| 구간 | 프로토콜 | 인증 · 보안 | 비고 |
|---|---|---|---|
| 워치 ↔ 폰 | Wear Data Layer (블루투스) | 기기 페어링 · 앱 서명 | 같은 서명키의 짝 앱끼리만 통신 |
| App ↔ API Gateway | HTTPS (REST/JSON) | TLS 1.2+, JWT Bearer | 모든 보호 라우트에 토큰 필수 |
| API Gateway ↔ Lambda | AWS 내부 호출 | IAM 리소스 정책 | 퍼블릭 노출 없음 |
| Lambda ↔ DynamoDB · S3 | AWS SDK (HTTPS) | IAM Role (최소권한) | 테이블·버킷 ARN 단위 제한 |
| Lambda ↔ Cognito | AWS SDK (HTTPS) | IAM Role | 회원가입·로그인 처리 |
| Browser ↔ 웹사이트 | HTTPS | CloudFront + ACM 인증서 | S3 직접 접근 차단(OAC) |
서버에서 기기로 직접 푸시할 채널(FCM 등)이 없어, 지시 레코드 + 폴링 방식을 쓴다.
감독이 POST /quarters/{id}/start·stop 를 호출하면 서버가 매치 lineup 의
선수마다 USER#{userId} / ACTIVEQUARTER 레코드를 남기고(동시성 5로 병렬 기록),
워치는 GET /me/active-quarter 를 주기적으로 조회한다
(대기 중 15초 · 기록 중 30초, 앱 화면이 보이는 동안만 — 배터리 절약).
RECORDING 을 받으면 워치의 시작 버튼이 "전반 시작"처럼 활성화되고, 누르면 감독이 만든
matchId·quarterId 로 기록이 붙는다. DONE 을 받으면 기록을 자동 종료·업로드하고 분석 요약을 표시한다.
중복 처리를 막기 위해 commandSeq 로 같은 지시를 한 번만 반영한다.
폴링이라 통신이 끊겼다 복구되면 다음 주기에 자동으로 따라잡는다.
업로드는 쿼터 종료 시점에 배치로 수행됩니다. 네트워크가 없거나 폰과 블루투스가 끊긴 상태여도 각 기기의 Room DB에 데이터가 남고, 전송 실패 건은 영속 재전송 큐에 적재됩니다. 워치가 다시 폰 블루투스 범위에 들어오거나 인터넷에 연결되면 큐를 자동으로 비워 서버에 올리므로, 먼 거리에서 쿼터를 종료해도 데이터가 사라지지 않습니다.
4. 주요 API 엔드포인트
| 메서드 | 경로 | 인증 | 설명 |
|---|---|---|---|
| GET | /health | 공개 | 헬스체크 |
| POST | /auth/signup | 공개 | 회원가입 |
| POST | /auth/login | 공개 | 로그인 (JWT 발급) |
| POST | /auth/refresh | 공개 | Refresh Token으로 재발급(자동 로그인) |
| POST | /matches | JWT | 매치 생성(경기장·라인업 지정, 팀 매치는 운영진만) |
| GET | /matches | JWT | 내 경기 이력 — 최신순, ?limit= 로 개수 제한 (?teamId= 시 팀 매치 목록) |
| POST | /matches/{id}/lineup | JWT | 매치 출전 선수(라인업) 입력·수정 |
| POST | /matches/{id}/quarters | JWT | 쿼터 생성(운영진만, 커스텀 시간) |
| GET | /matches/{id}/quarters | JWT | 쿼터 목록 |
| POST | /quarters/{id}/start | JWT | 쿼터 시작(운영진만) |
| POST | /quarters/{id}/stop | JWT | 쿼터 종료(운영진만) — 출전 선수 기기에 종료 전파 |
| GET | /me/active-quarter | JWT | 내게 내려온 쿼터 시작/종료 지시(워치·폰이 폴링) |
| POST | /matches/{id}/track | JWT | 트랙+컨디션 업로드(쿼터 단위) |
| GET | /matches/{id}/track | JWT | 경로 + 1분 마커(minuteMarkers) |
| GET | /matches/{id}/analytics | JWT | 분석 · 히트맵 · 컨디션 요약 |
| POST | /teams | JWT | 팀 생성 |
| POST | /teams/{id}/members | JWT | 선수 등록(등급·역할, 운영진만) |
| POST | /teams/{id}/roster/bulk | JWT | 엑셀 명단 일괄 등록(계정 자동 생성, 운영진만) |
| GET | /teams/{id}/player-matches | JWT | 선수별 개인 경기 조회(팀 분석 라인업 자동 연결, 운영진만) |
| GET | /teams/{id}/stats | JWT | 팀 통계 · 랭킹 |
5. 데이터 저장 구조
DynamoDB 단일 테이블 설계로 조회 패턴을 커버하고, 용량이 큰 원본 트랙은 S3로 분리해 저장 비용을 낮춥니다.
| PK | SK | 엔티티 | 주요 속성 |
|---|---|---|---|
USER#<userId> | PROFILE | 사용자 | email, name(한글), nameEn(영문), playerId, position/positionGroup, grade |
USER#<userId> | MATCH#<matchId> | 경기 요약 | distance, maxSpeed, sprints |
MATCH#<matchId> | META | 매치 | teamId, title, status, venueId/venueName, lineup[] |
MATCH#<matchId> | ANALYTICS#<quarterId> | 쿼터 분석 | avgSpeed, heatmap, conditionSummary |
MATCH#<matchId> | QUARTER#<quarterId> | 쿼터(목록) | label, durationMin, status |
QUARTER#<quarterId> | META | 쿼터(단건) | matchId, teamId, durationMin |
USER#<userId> | ACTIVEQUARTER | 쿼터 지시 | status(RECORDING/DONE), quarterId, matchId, label, durationMin, commandSeq |
TEAM#<teamId> | PROFILE | 팀 | name, ownerId, inviteCode |
TEAM#<teamId> | MEMBER#<userId> | 팀원 | playerId(KRPL), name(한글), nameEn(영문), position(상세), positionGroup(GK/DF/MF/FW), role, grade |
- ID 체계:
KR(한국) + 타입 2자 + 10자 = 14자. 매치KRMC…, 선수KRPL…, 쿼터KRQT…, 팀KRTM…(혼동 문자 I·O·0·1 제외). 화면에는 Cognito 내부 식별자 대신 항상 이 ID를 노출한다. - 포지션은 2단 구조: 상세 포지션(GK, CB/LB/RB/LWB/RWB/SW, DM/CM/ACM/LM/RM, LW/RW/CF/ST/SS)을 저장하고, 분석 벤치마크·전술 조언은 라인 그룹(GK/DF/MF/FW)으로 환산해 계산한다.
- 원본 트랙 경로:
s3://<bucket>/tracks/<matchId>/<quarterId>.json— 30일 후 STANDARD_IA로 자동 전환 - 컨디션 요약(
conditionSummary)에는 심박 평균·최대·최소가 쿼터 단위로 저장됩니다. GSI1(GSI1PK=TEAM#<teamId>)로 팀 단위 랭킹·통계를 한 번의 Query로 조회- DynamoDB는 온디맨드 과금 → 유휴 시 비용이 발생하지 않음