Skip to content

Report screen props (mobile)

화면 픽스처(앱 저장소): src/react/ui/mobile/fixtures/ReportScreen.fixture.ts, src/react/ui/mobile/fixtures/HomeScreen.fixture.ts

UiReportScreen — 리포트 탭. 데스크톱 훈련 리포트(A~F)의 모바일 세로 스크롤 대응 (코드 독립, rp-*). 화면 fixture는 위 앱 저장소 경로에 두고 화면 props와 같은 PR에서 갱신한다. 표시 의미의 규범은 이 계약이며, 반입 방식은 전달 계약을 따른다.

심사 계정 fixture의 점수·강도·RPE는 실제 로컬 시드의 세션별 관측 snapshot(src/react/ui/shared/fixtures/reviewReportObservations.ts)을 기간별로 합산한다. 원본 세트 payload가 바뀌면 기존 관측 재사용을 거부하므로 시드 관측도 함께 갱신한다. 기준 날짜 변경으로 같은 출석 템플릿의 날짜가 추가될 때는 동일 인물·동일 세트 payload인 가장 가까운 관측을 재생하고 replayedTemplates에 남긴다. 이 추가 날짜의 값은 새 DB에서 다시 계산한 수치가 아니라 프리뷰용 관측 재사용이다. 제품 런타임에는 이 snapshot을 반입하지 않는다.

Props

propshapenotes
profile{ name, photo?, short? }헤드라인 "OO님의 훈련 리포트" + 아바타
report.periodNamestring예: "월간" — 헤드라인 rangeText 뒤 "· 월간"
report.rangeLabel / report.rangeTextstring"2026년 6월" (헤드라인·A/C 제목·기간 시트) / "2026-06-01 ~ 2026-06-30"
report.totalVolume / report.activeDaysnumberA 헤드라인 · 하루 평균 = totalVolume/activeDays
report.stats{ sets, reps, sessions, hours, mainReps? }히어로 스탯 밴드 — 운동일/주간 출석 목표/세션/세트수/메인세트 반복수 (운동일 = activeDays, mainReps 미공급 시 해당 칸 비노출)
report.topSets[{ name, load, reps, pct?, date }]B TOP 5 — date는 YYYY-MM-DD, pct = "세트 강도 N%"
report.days[{ iso, volume }]C 잔디밭 그리드 + F1 스트립 공용. B 셀 단위 = periodUnit별: 주간/월간 일 · 분기 주 합산(13셀) · 연간 월 합산(12셀 4열, 월 라벨) — 제목에 "(주/월 단위)" 소표기
report.trend[{ label, volume }]현재 미사용(예약 — C가 잔디로 전환됨)
report.dStats{ avgVolume, avgExercises, avgSets }D 세션당 평균 스탯 밴드
report.byExercise[{ id, name, volume, sets, share }]D 랭킹 (볼륨 내림차순, share = %). 화면은 1~10위 + "기타"(11위 이하 합산) — 2026-09-08: "기타" 탭 → 11~100위 스크롤 모달(action report.rank.etc, 미등록). Codex: 최대 100행 공급(구 etc 합산 행은 불필요, 있으면 합산에만 반영)
favorites?string[]2026-09-08 즐겨찾기(주요 종목) exerciseId 순서 — 기록 성장 섹션은 이 목록의 종목만 이 순서로 표시(비었거나 미공급 = growth 전체). 컨테이너: UiPrFavScreen과 같은 favorites 배열 전달. Codex: 서버 report_year_growth가 즐겨찾기 종목을 모두 포함하도록 집계 기준을 즐겨찾기로
report.growth[{ id, name, prev, now, unit: "kg"|"회"|"초"|"km", delta, isNew }]주요 종목 기록 성장(전년 말 최고 대비). unit: "kg" = 측정 1RM, "회" = 실측 최고 반복수(#1100), "초" = 최고 버티기(#1102), "km" = 최장 거리(#1103, 서버 m를 1/1000 환산·소수 1자리). 선택 기간 상세 v1은 즐겨찾기 순서대로 실측 기록이 있는 모든 행(변화 0 포함)을 공급하며 구 6행 제한을 적용하지 않는다. 기존 volume v4의 top-N 성장 목록은 호환 유지
report.byPart[{ id, name, volume, sets, share, exercises? }]부위별 6각 레이더. exercises?: [{ id, name, volume, sets, share }](2026-09-08 신설) = 이 카테고리에 속하는 종목의 기간 훈련 중량(볼륨 내림차순, share = 카테고리 내 %) — 레전드 탭 모달(hook mobileReportPartModal) 내용. 미공급 = 모달에 빈 안내. 모달 행 탭 = onOpenExercise(id)(pr.open)
report.byFunc[{ name, volume, sets, share, exercises? }]기능별 6각 레이더 — exercises? 동일 규약, 모달 hook mobileReportFuncModal
report.eStats{ avgInt, maxInt, avgReps, avgScore, avgRpe? }E 스탯 밴드 (평균 강도/최고 일평균/평균 렙). avgScore = 평균 세트 스코어(이슈 #1180, 서버 average_set_score 소수 1자리, 없으면 null) — 세트 스코어 히스토그램의 평균 점선
report.intensity{ str:{zones,pcts}, score:{zones,pcts}, goal:{...}, rpe:{...} }E1~E3 — zones = 세트 수 배열(연한→진한 밴드 순). E1 도넛은 score(세트 스코어 4구간: 7.0 미만 여유 / 7.0–8.5 볼륨 / 8.5–10.0 최대 근접 / 10.0 이상 최대 갱신, 메인 세트만·1RM판+반복수판 — 이슈 #1180 오너 D1·D5). str(무게 ÷ 직전세션 추정 1RM 4구간)은 데스크톱·구 소비자용으로 계속 공급
report.scoreHist{ start: 1, step: 1, counts: number[10] }E0 세트 스코어 분포 — 2026-09-08: 1~10 10칸 막대(첫 칸 2 미만 · 2–3 · … · 9–10 · 10+). 원래 관측값으로 서버가 직접 집계한다. 구 8칸(set_score_histogram_counts, 5 미만…11+)은 원래 경계로만 표시하고 임의로 10칸에 재배치하지 않는다. 평균 점선 = eStats.avgScore
report.loadHist{ start: 0, step: 10, counts: number[10] }E0 세트 무게 분포 — 2026-09-08: 기존 세트 강도 정본(직전 세션 기준 추정 1RM 대비) 10% 단위 10칸(0–10 · … · 80–90 · 90% 이상). 관측에 기록된 set_intensity_percent를 직접 센다. 기준 미확정 세트는 기존 평균과 같은 정책으로 제외한다. scoreHist와 각각 독립 표시. 평균 점선 = eStats.avgInt
report.repsHist{ counts: number[10] }2026-09-08 신설 반복수 분포 — 1 · 2 · … · 9 · 10+ 10칸, 값 = 세트 수. 색 = 세트 목적 3구간(1–5 스트렝스 / 6–9 근비대 / 10+ 근지구력). 평균 점선 = eStats.avgReps. 미공급 = 섹션 미표시
report.rpeHist{ counts: number[10] }2026-09-08 신설 RPE 분포 — <6 · 6 · 6.5 · 7 · 7.5 · 8 · 8.5 · 9 · 9.5 · 10 10칸(0.5 간격), 값 = 세트 수. 색 = RPE 4구간. 평균 점선 = eStats.avgRpe(신설). 미공급 = 구 intensity.rpe 4구간 스택바 폴백
report.fStats{ totalDays, attendanceWeeks, attendanceWeekPct, wkTotal, maxRun, wk4, wk4Pct }F 스탯 밴드. attendanceWeeks/attendanceWeekPct가 정식 필드이며 기준은 공용 attendance policy를 따른다. wk4/wk4Pct는 구 소비자 호환 별칭이다.
report.durZones{ zones: number[3] }F2 세션 시간 비율 — 2026-09-08: 3그룹(30분 미만 · 30분 이상 2시간 미만 · 2시간 이상), 값 = 세션 수. 기존 5구간 저장값을 정확히 합산(0 | 1+2+3 | 4)해 경계 정책을 유지한다. 종목 필터를 걸어도 기간 전체의 세션 습관으로 표시
report.slotsnumber[36]F3 운동 시간대(구 "시간대 리듬", 2026-09-08) — 6시~24시 30분 슬롯 레벨 0~3
periodUnit"week" | "month" | "quarter" | "year"기간 시트 단위 세그 선택값. 기본 "month"
canPrev / canNextboolean기간 스테퍼 활성 (기본 true — false일 때만 비활성)
onDownloadMonthCard?() => void2026-09-14 신설 월간 카드 이미지 저장. 월간 화면 카드 아래 액션 행의 "이미지로 다운받기" 버튼 — 미공급 = 버튼 미노출(세션 상세 onDownloadImage와 같은 관례). 세션용 onDownloadImage(session)은 인자 계약이 달라 재사용하지 않는다
filterOptions[{ id, name }]종목 시트 목록. 미공급 시 byExercise에서 파생(etc 제외)
filterIdstring활성 종목 필터 id, ""/미공급 = 전체. 활성 시 헤드라인 칩 + 필터 아이콘 dot
backLabel / onBackstring / () => void(2026-08-23 친구 리포트) onBack이 있으면 워드마크 아래 돌아가기 행(.rp-back, 홈 친구 페이지 hm2-back 문법). 기본 라벨 "돌아가기"

밴드 라벨·컬러는 화면 내 상수(RP_BANDS) — 구간 정의는 데스크톱 동일, 컬러는 네이비 계열(2026-07-21 분기). 레이아웃: 풀블리드 흰 지면, 섹션 콘텐츠는 제목보다 좌우 10px 인셋. E~F2 레전드 = 1행 n열 스탯박스.

월간 화면 = 월간 카드 한 장 (2026-09-14 지시)

periodUnit === "month"면 화면은 카드(rp-mcard) + 카드 바깥 액션 행(rp-mo-act)만 그린다. A 헤드(오버라인·타이틀·총 중량 대자·잔디)와 그 뒤 모든 섹션(기록 성장 / 가장 많이 훈련한 종목 / 부위·기능 레이더 / 상대적 세트 강도·세트 무게·반복수·RPE 분포 / 근자극 타입 / 세션 길이 / 운동 시간대)은 월간에서 비노출이다 — 연간·분기 화면은 종전 그대로이며 같은 마크업·CSS를 공유한다.

카드 내용(위→아래, 전부 기존 필드): 우상단 워드마크 · 월 라벨(rangeLabel) · 든 무게 대자(totalVolume) · 아바타+이름(profile.photo/name, 60px 라운드 사각, 폴백 "나") · 3칸(운동일 activeDays/fStats.totalDays · 총 세트 stats.sets · 운동 시간 stats.hours{h}h {m}m, 0분이면 h만) · 일별 막대(days[].volume, 달의 전 일수로 합성 — 없는 날 0, 최고 막대만 값 라벨) · 종목 Top 5(byExercise 볼륨 상위 5, n세트 + 총 중량 + 최상위 100% 기준 바) · 푸터 MONTHLY CARD · MM / YYYY · barbelic.com. 가로선·눈금선·평균선은 쓰지 않는다(블록 분리는 간격).

액션 행: ‹ › = onStepPeriod(∓1)·canPrev/canNext(같은 단위 내 월 이동) · "이미지로 다운받기" = onDownloadMonthCard(). 카드 안에는 조작 요소를 두지 않는다 — 카드 영역이 그대로 공유 이미지가 된다.

Codex 후속: ① onDownloadMonthCard 컨테이너 구현(카드 DOM 캡처 또는 네이티브 브리지) ② 종목 평균 중량 byExercise[].avgLoad(시안의 "42세트 · 118kg" 복원용, 현재 세트 수만) ③ stats.hours 분 단위 정밀도 ④ Lv 칭호(profile.level/title)를 카드에 쓸지 판단 — 지금은 미노출.

Callbacks (모두 선택)

  • onOpenExercise(id) — D 랭킹 행 탭 → 종목 상세(나의 기록) 점프. 미공급 시 행은 탭해도 아무 일도 없다.
  • onOpenDay(iso) / onOpenWeek(iso) — 잔디 셀 탭 → 연 탭의 daySummary 스택 항목을 열고 컨테이너가 하루 상세 슬라이드 오버레이를 렌더한다(2026-09-08, lib/DayOverlay — 일지 탭 하루 상세와 동일 컴포넌트, 뒤로 = 공용 스택 한 겹 pop → 리포트 지면 복귀, 하단 돌아가기 바 없음). 세션 탭은 같은 탭에 session을 쌓고 공용 UiStackSessionOverlay를 표시한다(hook sessionDetailSheet). 주차는 컨테이너가 selectedWeek*를 공급하는 드로어다. 리포트 화면 내부에 하루·세션 오버레이를 중복 렌더하지 않는다. onOpenWeek 미공급 시 분기 주차 드로어도 열지 않는다(2026-08-23) — 친구 리포트는 셋 다 미공급 = 조회 전용.
  • onChangePeriodUnit(unit) — 기간 시트 단위 세그 탭 ("week"|"month"|"quarter"|"year"). 컨테이너가 report 재집계·periodUnit 갱신.
  • onStepPeriod(dir) — 기간 시트 ◀▶ (dir = -1|1), 같은 단위 내 이동. 시트는 열린 채 라벨만 갱신됨.
  • onChangeFilter(id) — 종목 시트 행 탭(빈 문자열 = 전체) + 헤드라인 칩 × 해제. 컨테이너가 filterId·report 재집계.
  • onClose() — (2026-09-14) 있으면 워드마크 행 좌측에 뒤로 버튼(.mb-brand > .sj-back, 하루/월간 오버레이와 동일 마커업, 미등재 마커 없음 — 마커가 필요하면 등록표 요청)을 그린다 — 리포트 나가기 intent. 용도 = 드로어 연간/월간 리포트 탭(복귀 = 드로어를 열었던 이전 탭) · 홈→리포트 homeReport 스택 항목(복귀 = pop; onClose={startBack}로 같은 행을 사용). 미공급 = 버튼 없음(탭 바 직접 진입이 기본). 친구 리포트(onBack/fband 소유자 행)에는 공급하지 않는다 — 이탈은 FriendScopeBar 도크. 버튼은 pending 스켈레톤과 오류 화면의 워드마크 행에도 같이 그려 로딩·오류 중에도 나갈 수 있다. 타입 정본은 ReportScreenProps(types/reportPresentation.ts)의 onClose?: () => void이며 화면의 별도 확장 타입을 두지 않는다. 복귀 위치는 navigationStorereportReturnTab이 관리한다. 직접 탭을 선택하면 해제하고 계정 전환 시 초기화하며, 리포트에서 종목 상세를 다녀오는 경로에서는 유지한다.

기간/종목 시트는 화면 내부 상태로 열리고(트리거 = 헤드라인 달력/필터 아이콘), 적용은 위 콜백으로 즉시 — 확인 버튼 없음. 종목 시트 상단 = 공용 검색창 UiSearchField(ui/mobile/lib/SearchField.tsx — PR 검색 시트·종목 피커와 공유, name/nameEn 매칭). 검색 중엔 "전체 종목" 행 숨김.

Markers

  • data-lg-hook="mobileReportView" data-lg-view="dashboard.report" (식별용, 레지스트리 등록 완료)
  • hook mobileReportPeriod/mobileReportFilter(헤드라인 아이콘), mobileReportPeriodSheet/mobileReportFilterSheet(시트 스크림), mobileReportLens(D 렌즈) — 전부 식별용, 레지스트리 등록 완료.
  • hook mobileReportMonthCard(월간 카드), mobileReportMonthCardDownload(이미지로 다운받기) — 2026-09-14 신설, 등록표 등재 요청. 월 스텝은 기존 mobileReportPeriodStep을 그대로 쓴다.
  • 액션은 기존 등록명만: pr.open(+option-value=종목 id). 신규 액션 마커 0.
  • .rp-punit.twodata-lg-* 계약이 아닌 순수 CSS 변형 클래스이므로 마커 레지스트리에 등록하지 않는다.

pending (2026-08-12 추가 — 로딩 체계)

  • pending?: boolean — true면 화면이 "크롬 실물 + 데이터 자리 셔머" 스켈레톤을 즉시 렌더한다(2026-09-10 #1554: 첫 프레임부터 표시하며 재방문 시 지연·투명화 없음). 컨테이너: 캐시가 없을 때만 true, 데이터 도착 시 false. 게이트/스피너로 화면 마운트를 막지 말 것.

최신 통계 숫자 공개 (#1563, 2026-09-11)

  • 대상은 본인 리포트의 모바일·PC 화면이다. 홈 UI는 변경하지 않는다. release/v0.17.11앱 PR #1570, merge 782248231b59f1005d44aff68bd5a84044badd7c로 반영됐으며 Production 미출시다. 이슈 #1563.
  • 캐시 객체가 있다는 사실이나 캐시에 저장된 stale:false만으로 숫자를 공개하지 않는다. 첫 렌더부터 live 볼륨 개요의 최신 여부를 확인하며, applied_version이 관측된 요청 세대 이상이고 재계산 중이 아님을 확인해야 한다. 처음에는 스켈레톤, 확인 완료 후에는 최신 숫자로 표시해 기존 숫자 → 스켈레톤 → 새 숫자 순서의 노출을 막는다.
  • 완성된 리포트를 갱신할 때는 제목·카드·단위·선택 상태·스크롤을 보존하고 통계 숫자 자리를 스켈레톤으로 바꾼다. 숫자와 함께 확정되어야 하는 비율·증감·그래프 값에도 같은 공개 조건을 적용한다. 고정 지연이나 강제 대기 시간으로 완료를 추정하지 않는다.
  • 모바일 정밀 리포트는 같은 소유자·선택 기간·종목 필터·asOf에 맞는 ReportDesignData까지 도착해야 완료다. 통계 요청 세대가 증가하면 정밀 조회 캐시의 revision도 바꾼다. 개요만 최신이고 정밀 결과는 이전 세대인 조합을 게시하지 않는다. PC도 live 개요가 확인되기 전에 캐시 숫자를 확정하지 않는다.
  • 모바일은 dataStatus·pending·updating·refreshError로, PC는 statsStatus: "pending" | "ready" | "error"로 표시 상태를 전달한다. 최신 조회 실패나 기존 회복 시도 소진은 error다. 숫자 자리에 와 다시 불러오기를 제공하며 이전 수치를 최신 값처럼 확정하거나 스켈레톤을 무한 유지하지 않는다. 모바일 onRetry, PC onRetryStats는 필요한 기존 읽기 경로를 다시 실행한다.
  • 이미 확인된 같은 소유자·기간·종목·기준일·통계 세대의 탭/상세 왕복은 완성 응답을 재사용한다. 사용하지 않는 탭의 보존된 오래된 개요 때문에 현재 화면의 회복을 반복하지 않는다.

6각 레이더 = 세트 수 전용 · 최대값 상대 스케일 (2026-08-27 · 이슈 #890 랜딩 08-28)

  • byPart/byFunc 각 행에 sets(정수) 를 공급한다 — 폴리곤·라벨 %·서브라인("N세트")이 전부 세트 수 기준. 정밀 경로는 서버 taxonomy_set_counts(20260828100000, 부위 축 = target_muscles 지분 가중)를 축별 반올림해 공급하고, 그룹핑 폴백 경로는 종목 행 sets 합산으로 기공급.
  • 중량 폴백 없음(오너 지시 08-28): sets가 전부 0이거나 미공급이면 kg/share로 그리지 않고 빈 그리드를 유지한다. 행의 volume/share 필드는 계약상 남아 있으나 이 컴포넌트는 읽지 않는다.
  • 스케일: 축 최대값 = 외곽선(상대 스케일). 라벨 % = 세트 합 대비 비율, 데이터 0이면 빈 그리드.

선택 기간 상세 읽기 v1 (2026-09-08)

  • 호출: loadReportDesignData({ periodType, periodStart, asOf, exerciseId?, ownerId?, signal? })get_report_design_v1(p_period_type, p_period_start, p_as_of, p_exercise_id, p_owner_id). ownerId 생략은 본인, 친구는 기존 profile_feed_can_view_v1 팔로우·차단 정책을 그대로 검사한다. 익명과 미허용 친구는 42501이며 RLS 우회용 임의 전체공개 경로는 없다.
  • 읽기 범위는 선택한 주/월/분기/년 하나다. 기존 volume v4의 334기간과 8칸 히스토그램은 변경하지 않는다. user_exercise_period_stats의 일별 요약을 periodStart..min(기간 말일,asOf)로 합산하며 화면에 전체 raw sessions를 내려주지 않는다. 응답 version은 schema_version:1, 1MB 상한이다.
  • 분포는 새 nullable report_distributions_v1 열에 원 관측값으로 저장한다. 기존 refresh_user_strength_estimation_projection 말미에서 종목 ID 범위로 갱신한다. 점수는 기존 set_score_observations_v1의 최신 main의 세션 시작 시점 nRM/최대 반복수 기준·복합 세트 평균 정책, 무게와 RPE는 기존 strength period counters의 관측 스트림, 반복수는 그중 main 세트의 양수 raw_reps를 사용한다. 0회 실패는 반복수 막대에 세지 않는다. 직접 입력한 RPE 소수는 0.5 간격 하한 구간에 들어가며 원래 값으로 평균을 낸다.
  • migration은 기존 strength/max-rep 관측을 이용해 새 표시 집계만 backfill한다. 열 추가 트랜잭션이 commit된 다음 migration에서 기존 global-worker→사용자 projection 잠금 순서를 지켜 실행하므로 열 추가의 ACCESS EXCLUSIVE 잠금을 backfill 동안 유지하지 않는다. 기존 8칸, 추정 정책, 사용자 원본 기록을 바꾸지 않는다. 미갱신 행이 섞여 있으면 distributions_ready:falsedistributions:null로 응답해 부분 집계를 완료 데이터처럼 표시하지 않는다.
  • 순위는 전체 기간에서 볼륨/세트수/반복수/id 순으로 최대 100종목이다. 부위·기능 총합은 전체 종목을 포함하고, 축별 드릴다운만 최대 100종목(세트수 내림차순)으로 제한한다. target_muscles 지분에 따른 세트·중량은 소수를 유지하며 화면의 축 합계만 기존 규약대로 반올림한다. 드릴다운 share는 축 세트수 대비 비율이다.
  • 성장 데이터는 즐겨찾기 최대128종목의 기존 실측 정본(PR events, max-rep observations, record_metric_measured_sets_v1)을 사용한다. 직전 연도 말 최고와 선택 연도/asOf까지 최고를 비교하며 전체 즐겨찾기 순서를 보존한다. 단위별 최대512행이고 추정치를 실측 기록으로 바꾸지 않는다.
  • 컨테이너는 기간·필터·친구 소유자가 달라지면 해당 요청만 취소/갱신하고, 그 scope와 일치하는 ReportDesignDatabuildMobileReportProjection({ ..., reportDesign })에 전달한다. 매퍼는 기간/필터가 다르면 해당 정밀 응답을 사용하지 않는다. 기존 하루 오버레이와 세션 상세 전환 계약은 그대로다.
  • 같은 기간의 정밀 조회는 실제 볼륨 개요 갱신 또는 즐겨찾기 변경 때 다시 읽는다. 카탈로그·PR 상세 도착으로 표시용 volumeProps 객체가 재생성되거나 하루/세션 스택을 왕복할 때는 같은 요청을 반복하지 않는다.

2026-09-10 기간 전환의 본문 보존 (#1554)

  • 최초 자료가 없을 때만 전체 스켈레톤을 표시한다. 완성된 리포트의 다음 기간을 요청할 때는 현재 DOM·스크롤·순위 선택을 유지하고 요청 중인 기간과 aria-busy를 표시한다. 숫자는 위 #1563 공개 조건을 적용해 확인 중에는 스켈레톤, 실패하면 와 다시 불러오기를 표시한다.
  • 기간 제목·필터·통계는 같은 완성 응답 단위로 함께 교체한다. 이전 기간 수치를 새 기간 제목 아래 최신 자료처럼 표시하지 않는다. 빠른 연속 이동은 마지막 선택에 맞는 응답만 게시하고 이전 요청을 취소한다.
  • 완료된 응답은 사용자 store·개요/즐겨찾기 revision별 캐시에서 기간·기준일·종목·소유자 키로 재사용한다. 연도·탭 왕복의 같은 요청은 반복하지 않으며 실제 개요/즐겨찾기 갱신 때 무효화한다. 사용자 store 또는 보고서 소유자 변경 시 이전 본문을 즉시 감춘다.