TT Lab
블로그

한국어

시작하기
블로그

블로그

경기 기록과 개인정보: 끝난 경기는 어디에 남고, 무엇이 남지 않는가

상태: 초안 (2026-10-26), 코드 근거. 동작은 2026-10-25 기준 저장소 코드와 설계 문서에서 읽었습니다. 운영 DB 의 「고친 기록」 건수는 2026-10-18 에 읽은 값입니다. 사용자 수 같은 운영 통계는 싣지 않았습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 근거를 찾지 못한 것은 「확인 못 함」으로 표시합니다.

목차

1. 경기가 끝나면

5게임제 경기가 3:1 로 끝났습니다. 카메라 점수판에는 「경기 끝」 화면이 뜨고, 게임 점수(11:7, 9:11, 11:5, 13:11)가 나란히 보입니다. 이 경기는 지금 어디에 있을까요?

휴대폰에만 있습니다. 2편에서 봤듯 카메라 점수판은 경기 이력 전체를 브라우저 localStorage(ttplay:cam)에 저장합니다. 이력의 칸마다 출처도 붙어 있습니다. 화면을 누른 점수(tap), 리모컨 키(key), 점수 조정(adj), 자동 판정을 받아들인 점수(auto)입니다. 서버는 이 경기를 모릅니다.

이 상태에서 서버에 무언가를 남기는 길은 사용자가 고르는 셋뿐입니다.

길 서버로 가는 것 남는 곳
이 경기 기록하기 게임 점수, 경기 길이, 상대 경기 기록, 레이팅
라이브로 공유(경기 중) 득점·되돌리기·끝 이벤트 라이브 경기와 이벤트 열
자동 점수(경기 중) 프레임(저장 안 함), 사람이 칩에 답한 기록 고친 기록(영상 없음)
경기 끝 이력 전체는 휴대폰 localStorage 에만 「이 경기 기록하기」 게임 점수만 넘김 · 손으로 올린 게임은 뺌 Google 로그인 · 표시 이름 구글 실명은 어디에도 안 나감 상대 고르고 「기록하기」 하루 30경기 · 같은 상대 10경기 계정 있는 상대: 확인 대기 상대가 「확인」을 눌러야 함 계정 없는 상대 개인 기록만 7일 안 확인 이의 7일 지남 확인됨 Elo 반영 K 40 · 24 · 16 이의 레이팅 안 움직임 개인 기록 레이팅에 안 들어감 상대 이름은 적은 사람만 순위표: 공개를 켠 사람 중 확인된 경기 3개 이상 프로필은 비공개가 기본 · 레이팅은 1500 에서 시작
그림: 끝난 경기가 기록·레이팅이 되기까지

2. 「이 경기 기록하기」

무엇이 넘어가나

「경기 끝」 시트의 「이 경기 기록하기」를 누르면(backend/static/tt/play/lib/cam-sheets.mjs) 화면은 경기 요약 {best_of, games, source:'scorekeeper'} 를 sessionStorage 에 잠시 두고 「내 기록」 화면(/tt/me#match)으로 넘어갑니다. 로그인 전이면 로그인을 거칩니다. 기록 화면에서 상대를 고르고 「기록하기」를 누르면 그때 서버로 갑니다(POST /api/tt/matches).

넘어가는 것은 게임 점수뿐입니다. 랠리별 이력, 누가 언제 어떻게 득점했는지, 자동 판정의 흔적은 넘어가지 않습니다. 몇 가지 경우는 기록할 수 없습니다.

로그인과 표시 이름

기록을 쓰려면 Google 로그인이 필요합니다. 게스트는 읽기만 합니다. 쓰기 자격은 사이트 커뮤니티와 같은 관문입니다.

여기서 중요한 설계가 하나 있습니다. 구글 계정의 이름은 어디에도 내보내지 않습니다. 구글 이름은 대개 실명이고, 경기 상대나 순위표에 그대로 나가면 탁구장에서 만난 사람이 내 실명을 알게 됩니다. 그래서 탁구 구역의 어떤 응답·화면도 구글 이름을 쓰지 않습니다(backend/app/tt_records.py 머리말 「구글 이름은 내보내지 않는다」). 표시 이름을 정하기 전의 선수는 「탁구인 #번호」이고, 경기를 기록하려면 먼저 표시 이름(2–20자)을 정해야 합니다. 상대 화면에 번호로만 나오는 경기는 누가 적었는지 알아볼 수 없기 때문입니다.

카메라 점수판의 별명 칸(12자까지)에도 「실명 대신 별명」 안내가 붙어 있습니다.

상대가 확인해야 레이팅이 움직인다

혼자 적은 승리로 레이팅을 올릴 수 있으면, 순위표는 누가 더 많이 적었는지의 표가 됩니다. 그래서 계정이 있는 상대와의 경기는 확인 대기로 들어가고, 상대가 「확인」을 눌러야 확인됨이 되며 그때 한 번 레이팅이 움직입니다. 상대가 「이의」를 걸 수도 있습니다. 7일 안에 확인이 없으면 레이팅에는 들어가지 않고 개인 기록으로만 남습니다. 만료는 배치 작업으로 행을 고치지 않고, 읽을 때 만든 시각으로 판정합니다(DB 시계 하나로 잼).

계정 없는 상대(동네 친구)와의 경기도 적을 수 있습니다. 이때 상대 이름은 그 경기를 적은 사람에게만 보이고, 레이팅은 움직이지 않습니다.

Elo 레이팅

레이팅은 체스에서 온 Elo 방식입니다.

# backend/app/tt_records.py 에서 줄임
def k_factor(rating, rated_games):
    if rated_games < 10:   return 40      # 처음 10경기: 제자리를 빨리 찾게
    if rating >= 2000:     return 16
    return 24

def expected(ra, rb):                     # A 가 이길 기대값
    return 1 / (1 + 10 ** ((rb - ra) / 400))

# 변화량 = K × (실제 결과 − 기대값). 두 사람이 각자 자기 K 를 쓴다.

예를 들어 둘 다 1500 이고 처음 10경기 안이면 기대값은 0.5, 이긴 사람은 40 × 0.5 = +20, 진 사람은 −20 입니다. 같은 K 40 에서 1600 이 1500 을 이기면 기대값이 약 0.64 라 +14.4 정도만 오르고, 지면 −25.6 정도 내려갑니다.

하루 한도와 동시성

한도 값 왜
하루 경기 기록 30 몰아 적기 막기
같은 상대와 하루 10 짜고 주고받으며 레이팅 올리기를 늦춤
하루 연습 기록 20 —
경기 날짜 1년 안쪽 옛 기록을 몰아 넣어 레이팅을 만드는 길을 좁힘
한 게임 점수 상한 50 오타(110:9) 거르기

한도는 DB 에서 셉니다. 같은 사람의 요청 둘이 동시에 「29번째」로 세어져 함께 통과하지 못하게, 사람마다 DB 잠금(advisory lock)으로 줄을 세웁니다. 확인할 때는 경기 행을 먼저 잠그고 두 선수 행을 번호가 작은 쪽부터 잠급니다. 앨리스와 밥이 서로의 다른 경기를 동시에 확인할 때 「내 행부터」 잠그면 서로 상대 행을 기다리며 교착에 빠지기 때문입니다.

3. 기록 화면들

화면 보이는 것 누가
내 기록 /tt/me 내 경기, 확인 대기·이의, 레이팅, 연습 일지 본인
순위표 /tt/rank 공개 선수 순위, 동호회·이번 달 필터, 이번 주 급상승 누구나
히스토리 /tt/me/history 신청·경기·점수의 변화를 한데 모음, CSV 내려받기 본인
경기 공유 /tt/m/{코드} 공유를 켠 경기 하나 링크를 받은 사람

히스토리는 새 기록을 쌓지 않고 각 기능이 이미 남긴 것을 읽어 모읍니다(backend/app/tt_history.py). 경기 기록, 라이브 점수판 이벤트(서버가 다시 셈), 녹화본 분석 결과, 자동 점수를 고친 기록, 레이팅 변화가 한 줄에 섞여 나옵니다. 라이브 점수판과 경기 기록은 서로 열쇠로 이어져 있지 않아서, 「끝 점수가 같고 시간이 가까운」 경기에 추정으로 잇습니다. 틀리게 이을 수 있으므로 화면은 흐름의 출처(라이브·영상)를 늘 함께 보입니다. 주소에 사람 번호가 없고 모든 질의가 로그인한 사람으로만 묶여, 다른 사람의 히스토리를 볼 길이 없습니다. CSV 는 엑셀 수식 주입을 막아서 냅니다.

경기 공유는 경기 번호가 아니라 따로 만든 11자 코드를 씁니다. 경기 번호는 순번이라 주소에 쓰면 남의 경기를 하나씩 훑을 수 있기 때문입니다. 확인된 경기는 두 선수 누구나, 개인 기록은 적은 사람만 공유를 켭니다. 확인 대기나 이의가 걸린 경기는 「있었던 경기」인지부터 정해지지 않아 공유하지 않습니다. 공유 화면에서도 공개하지 않은 선수는 「탁구인」, 계정 없는 상대의 이름은 끝까지 나가지 않습니다.

4. 라이브 공유: 서버가 점수를 다시 센다

경기 중에 「라이브로 공유」를 켜면, 관람 링크를 받은 사람이 실시간으로 점수를 봅니다(backend/app/tt_watch.py).

이름 규칙도 같습니다. 링크는 기록자가 직접 나눠 준 것이라 그 경기에 정한 별명을 보이지만, 아무나 보는 홈 피드에서는 공개를 켜지 않은 주인은 「탁구인」, 상대는 「상대 선수」로 가립니다.

5. 고친 기록: 동의한 것만, 그리고 아직 0건

3편에서 사람이 자동 판정의 칩에 답하면(「맞음」, 다른 선수로 고침, 「무효」) 그 답이 고친 기록이 된다고 했습니다. 이것이 어디로 가는지 따라가 봅니다.

무엇을 담나

자동 점수 화면은 사람이 답한 랠리마다 한 행을 만들어 POST /api/tt/corrections 로 보냅니다. 행에는 다음이 들어갑니다(backend/app/tt_corrections.py).

영상은 담지 않습니다. 서버는 브라우저가 보낸 것을 그대로 넣지 않고 아는 모양만 새로 만듭니다. 글이 들어갈 자리가 거의 없어 욕설·주소·스크립트가 쌓일 곳이 없습니다. 행의 열쇠에는 로그인한 사람의 번호가 들어가므로 남의 행을 덮을 수 없습니다. 한도는 사람당 분당 30요청, 하루 2,000행입니다.

시간이 지나 그대로 들어간 판정은 고친 기록을 남기지 않습니다. 사람이 확인한 것이 아니기 때문입니다.

누가 무엇을 가져가나

지금까지 학습에 쓴 적이 없다

2026-10-18 에 운영 DB 를 읽었을 때 고친 기록은 0건이었습니다. 따라서 고친 기록을 학습이나 평가에 쓴 적은 아직 한 번도 없습니다. 5편의 학습 모델들은 모두 공개 데이터와 합성 그림으로 배웠습니다.

계획은 있습니다. 5편에서 본 대로 학습형 모듈을 상업 사용 가능한 자료로 다시 학습할 때, 학습 이용에 동의한 사람의 고친 판정은 「확인 필요」 보정 같은 모듈의 재료가 될 수 있습니다. 「사람이 고친 판정」은 곧 「엔진이 틀린 자리」의 정답이기 때문입니다. 다만 그러려면 고친 기록이 충분히 쌓여야 하고, 학습 재료로 쓸 때의 보관 기간과 동의 안내를 다시 점검하는 일이 먼저입니다.

6. 영상은 기기 밖으로 나가지 않는다, 그리고 예외

카메라 점수판의 첫 원칙은 카메라 영상은 기기 밖으로 나가지 않는다입니다(backend/static/tt/play/cam.js 머리말). 녹화도 휴대폰 안에서 합성하고 휴대폰에 저장합니다. 점수 읽어 주기도 브라우저의 음성 합성을 씁니다.

예외는 사용자가 켜는 기능 둘입니다.

자동 점수. 공을 찾는 GPU 가 서버에 있으므로 프레임을 보냅니다. 대신 범위를 좁혔습니다(1편).

녹화본 올려 분석. 사용자가 영상 파일을 직접 골라 올리는 별도 기능입니다. 브라우저가 먼저 360p60 500kbps 로 줄여 올리고, 원본은 30일 뒤 지웁니다. 분석 작업자는 DB 와 저장소 열쇠 없이 웹의 내부 API 로만 일합니다.

7. 실명 대신 별명

개인정보 설계를 한 줄로 줄이면 「탁구장에서 만난 사람이 내 실명을 알게 되지 않게」 입니다.

자리 보이는 이름
카메라 점수판 화면 그 경기에 정한 별명(12자, 「실명 대신 별명」 안내)
경기 기록·상대 화면 표시 이름(2–20자). 정하기 전에는 「탁구인 #번호」
순위표·공개 프로필 공개를 켠 사람만. 기본은 비공개
계정 없는 상대 그 경기를 적은 사람에게만
라이브 홈 피드 공개 안 한 주인은 「탁구인」, 상대는 「상대 선수」
고친 기록 내보내기 이름 없음, 내보내기마다 바뀌는 가림 값
구글 계정 이름 어디에도 나가지 않음

데모 영상에서도 같은 원칙입니다. 원본 화면에 박힌 이름(중계 자막)은 원본 그대로 두지만, 우리가 덧그린 글자는 「선수 A·B」로만 씁니다.

8. 서버에는 어떤 표가 있나

기록과 관련된 서버 쪽 표를 한곳에 모으면 이렇습니다. 표마다 DB 이전(migration) 번호가 붙어 있고, 이전 하나가 기능 하나를 들여왔습니다.

이전 표 담는 것 담지 않는 것
0036 tt_players 표시 이름(2–20자, 비어 있으면 「탁구인 #번호」), 레이팅, 레이팅에 든 경기 수, 공개 여부(기본 비공개) 구글 이름
0036 tt_matches 경기 길이, 게임 점수, 출처(직접 입력·점수 도우미·녹화본 분석), 상태(확인 대기·확인됨·이의·기한 지남·개인 기록), 두 사람의 레이팅 변화량 랠리별 이력
0036 tt_practice 연습 일지(본인만 봄) —
0039 tt_live_matches·tt_live_events 라이브 경기와 이벤트 열(번호 붙은 득점·되돌리기·끝). 같은 이전에서 경기 공유용 11자 코드를 tt_matches 에 더함 점수 자체(서버가 이벤트에서 다시 셈), 영상
0040 tt_corrections 원래 판정·고친 판정·이벤트 요약·근거·동의 여부 영상
0048 tt_videos 사용자가 올린 영상의 분석 대기열과 결과 — (원본은 30일 뒤 지움)

이 표들에서 눈여겨볼 점은 「무엇을 담지 않는가」가 설계의 절반이라는 것입니다. 경기 기록은 랠리를 모르고, 라이브는 점수를 저장하지 않고 이벤트만 저장하며, 고친 기록은 영상을 모릅니다. 각 표는 자기 기능에 꼭 필요한 것만 담고, 히스토리 화면은 새 표를 만들지 않고 이 표들을 읽어 모읍니다(3절). 담지 않은 것은 새어 나갈 수도 없습니다.

9. 정리

티티 한마디. 오늘 경기는 폰이 먼저 기억해요. 서버한테는 「몇 대 몇이었는지」만 알려 주고, 이름도 별명으로만요. 상대가 「맞아요」 해 줘야 점수가 올라가니까, 이긴 날은 꼭 확인 부탁해 주세요.

참고문헌

  1. A. E. Elo. The Rating of Chessplayers, Past and Present. Arco, 1978. (Elo 레이팅의 원 서지. 원문은 이번에 열지 않았고, 우리 식은 코드 backend/app/tt_records.py 에서 확인)
  2. International Table Tennis Federation. ITTF Statutes 2026, Chapter 2: The Laws of Table Tennis, §2.12(경기와 게임 수). https://db.ittf.com/document
  3. 코드와 설계: 저장소 backend/app/tt_records.py·tt_history.py·tt_watch.py·tt_corrections.py·tt_live.py 머리말, backend/static/tt/play/cam.js·lib/cam-sheets.mjs·lib/camauto.mjs.

함께 읽기

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다