Skip to content

운동 기록 데이터 모델 — 다섯 층

  • 작성일: 2026-09-04 · 이슈 #1215 운동 기록 계층 리모델링(Phase 1~8)
  • 정본 범위: 운동 기록(완료·계획·그룹 보드)의 테이블 이름·층 구조·세션 종류·세는 규칙·저장 계약 이름. 화면 RPC 응답 모양은 화면 RPC 계약, 쓰기 경로의 클라이언트 규칙은 완료 기록 쓰기 파이프라인가 정본이다.

1. 한 문장 요약

운동 기록은 세션 → 종목 → 세부 종목 → 세트 → 세부 세트 다섯 층이고, 완료 기록·계획·그룹 보드는 모두 같은 session 테이블의 행이다. 저장은 save_session_v5 하나, 삭제는 delete_session_v5 하나로 한다.

2. 용어

용어예 (클린 + 저크 복합 종목, 3세트)
세션(session)하루에 한 번 하는 운동 한 판. 완료 기록·계획·그룹 보드가 모두 세션이다.9월 2일 저녁 운동
종목(exercise)세션 안에서 사용자가 "한 종목"으로 보는 단위. 복합 종목이면 동작 여럿을 묶는다.클린 + 저크 1개
세부 종목(exercise part)종목을 이루는 동작 하나. 단일 종목은 세부 종목이 하나다. 카탈로그 종목(exercise_id)은 여기에 붙는다.클린, 저크 2개
세트(set)종목의 n번째 세트. 복합 종목이면 "클린 1회 + 저크 1회"가 세트 하나다.3개
세부 세트(set part)세트 안에서 세부 종목 하나의 값(무게·횟수·시간 등).6개 (3세트 × 2동작)

"세부 종목"과 "세부 세트"는 오너 결정 D5(2026-09-03)로 정한 우리말 이름이다. 코드·SQL에서는 part로 쓴다.

3. 테이블과 층

테이블 이름은 단수다(오너 결정 D5: 행 하나가 곧 개체 하나라는 뜻). 옛 복수형 이름은 2026-09-04 이슈 #1215 Phase 1 마이그레이션에서 이름이 바뀌었고, 옛 이름의 테이블은 더 이상 없다.

테이블부모 열옛 이름(폐기)주요 열
1 세션sessionsessions, planned_sessions, group_boardsuser_id, date, status, group_id, title, note, scheduled_time, source, source_ref, origin_kind, origin_ref, server_revision
2 종목session_exercisesession_id(없었음 — 복합 여부는 composite_meta JSON 메모에 있었다)position, name, import_group_key
3 세부 종목session_exercise_partsession_exercise_id (NOT NULL), session_idsession_exercises, planned_sets(종목 부분)exercise_id, synonym_id, position, movement_position, entry_kind, entry_title, recording_fields, bodyweight_factor, load_multiplier, raw_payload
4 세트exercise_setsession_exercise_id(없었음)position, set_type(warmup·main·top)
5 세부 세트exercise_set_partexercise_set_id (NOT NULL), session_exercise_part_id(= 세부 종목 id, #1245에서 이름 정정)exercise_sets, planned_sets(세트 부분), group_boards.exercises(JSON)position, reps, load, load_lb, load_percent, distance_meters, duration_seconds, calories, assist_kg, perceived_rpe, stats_* 투영 열(stats_load_kg·stats_effective_load_kg·stats_reps — 기록 유형에 없는 값은 통계에서 숨김, #1236·#1256)

같이 사라진 테이블: planned_session_adoptions(계획 반영 기능 자체 폐기), group_board_likes·group_board_comments(세션 좋아요·댓글 session_likes·session_comments로 합침). composite_meta 열도 삭제되었다. 복합 종목의 묶음은 별도 메모 없이 층 필드로 읽는다(이슈 #1244): 읽기 함수(세션 상세·달력 하루 요약·피드 카드·계획 상세)가 세부 종목 행마다 session_exercise_id(소속 종목 id)·session_exercise_name(종목 이름, 복합이 아니면 ''session_exercise_position(종목 순서)·movement_position(종목 안 동작 순서)·details(수행 상세)를 싣고, 계획 상세는 exercise_set_position(세트 순서)도 싣는다. 같은 소속 종목에 동작이 2개 이상이면 앱이 한 종목(복합)으로 묶는다. 옛 composite_meta 합성 함수(session_part_composite_meta_v1·bounded_home_composite_meta)는 삭제됐다.

주의: exercise_set_part의 소속 열은 session_exercise_part_id(세부 종목 id)다. #1215 랜딩 때는 옛 이름 session_exercise_id로 남겼다가 #1245에서 담긴 값대로 이름을 바꿨다. session_exercise_part.session_exercise_id(종목 층 id)와 혼동하지 말 것.

4. 세션 종류

한 테이블 안에서 세션 종류는 statusgroup_id로 구분한다. 종류마다 테이블을 두지 않는다(오너 결정 D4·D9).

종류statusgroup_id누가 만드나화면
완료 기록completednull사용자(라이브 기록·수정·백필), 인입(WodUp·Motra)일지·상세·통계·피드
계획plannednull사용자(계획 편집기)일지 회색 카드·홈 오늘 계획 카드
놓친 계획missednull서버가 날짜가 지난 planned를 표시일지 회색 카드
그룹 보드planned그룹 id그룹장(화이트보드 작성)그룹 화이트보드
보드로 시작한 완료 기록completednull회원(보드 → "운동으로 시작" → 종료)회원 일지 + 그룹 "그날 완료 멤버"
  • 계획 완료 = 같은 행 전이(D13): 계획으로 운동을 시작해 종료하면 그 session 행의 statuscompleted로 바뀐다. 새 행을 만들지 않고, 계획↔기록 연결 테이블도 없다. 하지 않은 세트 행은 지운다. 세션 정체(source_ref)는 계획 것이 그대로 남는다(앱이 전이 저장을 계획의 source_ref로 보낸다).
  • 보드 계보: 보드로 시작한 완료 기록은 origin_kind = 'group-board', origin_ref = '<group_id>:<date>'를 단다. 그룹장의 보드 행은 그대로 planned로 남는다(회원이 완료해도 바뀌지 않는다).
  • **개정번호 server_revision**은 모든 종류에 있다. 계획도 완료 기록과 같은 낙관적 잠금(수정·삭제 시 기대 개정번호 불일치 → 40001)을 쓴다. 옛 계획의 expected_updated_at 방식은 폐기.
  • 통계·달력·피드 함수는 session을 직접 읽지 않고 뷰 completed_session_v1(완료만) / planned_session_v1(계획·놓친 계획, 그룹 보드 제외)을 읽는다 — pgTAP 가드가 강제한다. 그래서 계획·보드 행은 통계에 섞이지 않는다.

5. 세는 규칙

무엇규칙근거
세션(또는 하루·주·월)의 세트 수exercise_set 행 수D2 — 클린+저크 3세트는 3세트다(세부 세트 6이 아니다)
종목별 세트 수(세트 스코어·종목 통계)그 세부 종목을 가리키는 exercise_set_part 행 수D1 — 클린 3세트, 저크 3세트
횟수·볼륨·1RM·PR세부 종목(동작)마다 따로D3 — 클린 1회 + 저크 1회는 "2회"로 더하지 않는다
세트 스코어 관측exercise_set_id로 묶은 세트 하나에 관측 하나; 종목 필터는 그 세트의 세부 종목 어느 것이든 맞으면 포함D6

정본 SQL 도우미: session_set_totals_v1(session_id)(세트 수·종목 수), set_score_observations_v1. 하루·주·월 합계는 user_calendar_day_summaries가 같은 규칙으로 물질화한다. 규칙을 잠그는 테스트: pgTAP session_set_count_rule_v1.test.sql, e2e CASE-033.

6. 저장·삭제 계약(이름만)

함수역할비고
save_session_v5(p_payload, p_client_mutation_id, p_request_hash)완료 기록·계획·그룹 보드의 신규 저장과 수정payload contract_version 5, status, group_id, exercises[] → parts[] → sets[] → set parts. 수정은 id + expected_revision
delete_session_v5(p_session_id, p_expected_revision, p_client_mutation_id, p_request_hash)모든 종류의 삭제영수증 v2(mutation_kind delete_session)
save_workout_v4 · update_completed_session_v4 · delete_completed_session_v4 · save_plan_v3 · delete_planned_session_v2 · save_group_board_v1 · adopt_planned_session_v1 · unadopt_planned_session_v1폐기 스텁(D10)아무것도 쓰지 않고 SQLSTATE LG426("App update required")만 돌려준다. 앱은 "앱 업데이트가 필요해요…"로 안내하고 재시도 큐에 넣지 않는다

영수증 v2(contract_version 2)는 session_id·status·server_revision과 다섯 층의 id 목록 exercises[]를 돌려주고, 계획 저장은 통계 갱신을 요청하지 않으므로 stats_requested_version 0이다. 자세한 필드는 화면 RPC 계약 — 운동 기록 쓰기 계약 v5.

D14 자식 저장

같은 v5 요청과 영수증을 유지하면서 정규화한 실제 값이 달라진 자식만 쓴다. 메모·동일 내용 새 저장의 자식 I/U/D는 0이며, 새 완료 저장의 revision·영수증·통계 요청 세대 규칙은 유지한다. 실제 재정렬·파생값·원본 이력 규칙은 세션 자식의 변경 행 저장을 따른다.

7. 인입(WodUp·Motra)과 자동 층

인입 엔진은 세부 종목·세부 세트만 넣는다. 그때 BEFORE INSERT 트리거 두 개가 위 층을 자동으로 세운다.

  • zz_attach_session_exercise_layer_v1 — 세부 종목에 소속 종목이 없으면 종목 행을 만든다. WodUp 복합 해석(raw_payload.normalized.complex_interpretation.group_key)이 있으면 같은 키의 세부 종목을 한 종목(session_exercise.import_group_key)에 묶는다.
  • zz_attach_exercise_set_layer_v1 — 세부 세트에 소속 세트가 없으면 (종목, 세트 번호)로 세트 행을 찾거나 만든다.

앱 저장(v5)은 소속을 직접 적으므로 이 트리거를 거치지 않는다.

8. 식별자

다섯 층의 id는 모두 서버가 만드는 무작위 uuid다. BRID 문자열은 붙이지 않는다(D5). 클라이언트는 영수증 v2의 id 목록으로 저장 직후 사본에 번호표를 붙인다(writeSource: 'receipt').

9. 관련 문서