Skip to content

친구 일지 하루 상세 수리 — 통계 0·운동 내용 공백·세션 클릭 무반응 (2026-09-01)

  • 기간: 2026-09-01 (세션 1개, 오너 보고: "지금 친구 일지탭에 들어가서 하루 상세 클릭하면 1. 하루 통계도 안나오고 2. 세션 세부 데이터 하나도 안나오고, 세션 클릭해도 상세페이지 진입이 안됨")
  • 랜딩: PR #1096(Phase 1~5, c30cd7ad) — 마이그레이션 없음(서버 무변경, Production DB 적용 없음), Vercel 배포로 반영
  • 설계서: 없음(수리 건) — 분석·Phase 계획은 이슈 #1095 본문
  • 정본: 조회 문맥의 하루 상세 공급 의무 = docs/contracts/session-screen-props.md "조회 문맥(친구·그룹)의 하루 상세 공급 의무" 절 · 친구 스코프 데이터 상태 = src/react/controllers/friendScopeStore.tsday·session 슬라이스
  • 도구: 없음
  • 게이트: tests/react/friendScopeStore.test.mjs(하루·세션 슬라이스 행동 6건) · tests/react/friendScope.test.mjs(하루 상세 렌더 1건 + 컨테이너·레이어 배선 단언 2건)
  • 버그리포트: bug-report/bug-055-20260901.md
  • 계약: session-screen-props.md에 절 1개 신설(조회 문맥의 하루 상세 공급 의무 4항)

Phase 현황

Phase내용상태
Phase 1친구 스코프 스토어 day 슬라이스 — 날짜 선택 시 그날 완료 세션 상세 적재✅ PR #1096
Phase 2session 슬라이스 — 하루 슬라이스 재사용(요청 0회), 미보유 시 1건 적재✅ PR #1096
Phase 3화면 배선 — 하루 세션·존 분포·세션 상세·소유자 표기✅ PR #1096
Phase 4friendSession 히스토리 레이어 push/consume✅ PR #1096
Phase 5계약 문서 + 버그리포트 + 작업 기록✅ PR #1096(계약) · 문서 PR(기록)

1. 배경

친구 스코프 v1(2026-08-25, 이슈 #792)이 친구 프로필을 랜딩·일지·리포트·피드 4화면으로 열어 준 뒤, 일지 탭은 달력까지만 완성돼 있었다. 달력에는 친구가 운동한 날에 점이 찍히고 세션 제목도 보였다. 그 아래 층 — 날짜를 눌렀을 때 열리는 하루 상세 — 은 화면 컴포넌트만 붙어 있고 데이터와 콜백이 연결되지 않은 채 6일이 지났다.

오너가 2026-09-01에 그 층을 열어 보고 세 가지를 한꺼번에 보고했다.

2. 문제 제기

하루 합계가 전부 0으로 나온다

친구 일지 하루 상세 맨 위 3항이 총 무게 0kg · 운동 시간 0분 · 세트 수 0세트 (0랩). 친구가 실제로 그날 100kg × 5회 × 2세트를 했어도 0이었다.

원인은 데이터가 화면까지 오지 않는 것이다. 친구 일지 달력은 한 달치를 get_following_calendar_month_v1 한 번으로 받는데, 이 함수가 세션마다 싣는 값은 id · date · status · title · created_at 5개뿐이다. 한 달 최대 64세션이라 종목·세트까지 실으면 600KB 응답 상한에 걸려서, 처음부터 "달력용"으로 설계된 함수다.

하루 상세 본문 UiDayDetailBody는 서버가 준 정확한 하루 요약이 없으면 세션의 exercises[].sets[]를 훑어 합계를 직접 계산한다. 세트가 없으니 전부 0이 됐다. 그리고 이것은 오류가 아니라 "기록이 0인 날"과 똑같이 보이는 정상 경로여서 콘솔에도 계측에도 흔적이 없었다.

세션 카드에 운동 내용이 한 줄도 없다

같은 이유다. 종목 줄을 만드는 부분(DsvExSum)이 exercises를 훑는데 배열이 비어 있어 null을 반환했다. 카드에는 "완료" 배지와 제목만 남았다.

세션 카드를 눌러도 아무 일도 안 일어난다

하루 상세의 세션 카드 클릭 핸들러는 onClick={() => onOpenSession && onOpenSession(s)}이다. onOpenSession이 없으면 오류도 로그도 없이 아무 일도 하지 않는다. mobileApp.tsx의 친구 일지 인스턴스는 onOpenSession · selectedSession · onCloseSession 셋 다 넘기지 않았다. 본인 일지 탭도 친구 피드 탭도 셋 다 넘긴다 — 친구 일지 탭만 빠져 있었다.

왜 6일 동안 안 잡혔나

친구 스코프 렌더 테스트는 달력 화면(월 통계·셀 제목·조회 전용 FAB 부재)만 잠갔고 하루 상세를 열어 본 테스트가 없었다. 세션 화면 계약 문서에도 "조회 문맥이 selectedDateSessions에 무엇을 넘겨야 하는가"가 없어서, 세션 lite만 넘겨도 계약 위반으로 잡히지 않았다.

3. 해결 방안

원칙

오너 지시(2026-09-01): "원인파악해서 이슈 만들고, 일단 오너 확인없이 바로 픽스 진행해줘" — 분석·이슈 등록 후 오너 결정을 기다리지 않고 착수했다.

기존 원칙 두 개가 설계를 결정했다.

  • 친구·그룹 데이터 접근 계약 — 다른 계정의 데이터를 응답하는 지점은 전용 접근점으로 분리한다. 이번에는 새 접근점이 필요 없었다: get_session_detail이 원래부터 "본인 것이거나, 완료 세션이면서 팔로우 중"이면 열려 있고(20260831850000) 친구 피드 탭이 이미 이 경로를 쓴다.
  • 조회 전용 관례(친구 스코프 v1) — 수정 콜백을 넘기지 않는 것으로 편집 표면을 봉인한다. 세션의 readOnly 플래그는 화면이 읽지 않고, 편집 버튼 노출은 콜백 유무로 결정된다.

접근

대안내용판단
(a) 한 달 조회 함수에 세트까지 싣기get_following_calendar_month_v1 payload 확장기각 — 한 달 최대 64세션 × 종목 × 세트라 600KB 상한에 걸린다. 달력만 보는 사용자에게도 매번 전송
(b) 친구 전용 하루 조회 함수 신설get_calendar_day_summary 본문을 대상 유저 인자를 받는 내부 엔진으로 분리하고, 팔로우 검사를 거치는 get_following_calendar_day_v1 신설기각 — 계약상 가장 정석이지만 get_calendar_day_summary본인 일지의 핵심 읽기 경로(약 520줄)다. 본문을 옮기면 그 함수 본문을 텍스트로 검사하는 SQL 계약 테스트 5종(screenRpcContracts · mainOnlySetCountSqlContract · plannedSessionHardCutoverSqlContract · statsCentralizationPhase3SqlContract · completedWorkoutWriteBoundary)이 함께 흔들리고 마이그레이션·Production 적용까지 필요하다. (c)로 같은 숫자가 나오므로 이 위험을 질 이유가 없다
(c) 날짜를 누를 때 그날 세션 상세를 기존 함수로 받아온다달력이 이미 아는 그날 세션 id로 get_session_detail 호출(≤6건). 본인 일지와 같은 변환기(lgSessionToFull)를 태워 넘긴다채택 — 서버 변경 0. 세션 상세를 열 때도 받아둔 것을 재사용해 추가 요청 0회
(d) 세션 상세 진입만 배선증상 3만 수리기각 — 보고된 증상 3개 중 2개가 남는다

세션 상세를 여는 방식은 그룹 화이트보드 선례(전용 슬롯 + 판 안에서 렌더)를 따랐다. 이유는 아래 "작업 중 드러난 것"에 적었다.

4. 적용한 내용

Phase 1~2 — 친구 스코프 스토어의 두 슬라이스 (#1096)

src/react/controllers/friendScopeStore.ts

  • day 슬라이스 — 날짜를 누르면 state.calendar.sessions에서 그날 완료 세션 id를 뽑아(최대 6건 = 본인 일지 하루 응답이 싣는 세션 수와 같은 자리) get_session_detail을 병렬 호출해 담는다. 같은 날 재선택은 재요청하지 않고, 세션이 없는 날은 요청 없이 빈 결과로 끝낸다. 슬라이스 토큰으로 늦은 응답을 버린다.
  • session 슬라이스 — 세션 상세는 day 슬라이스가 이미 받아둔 것을 찾아 쓰므로 추가 요청 0회. 하루 상한 밖의 세션 등 미보유일 때만 그 1건을 받아온다.
  • 두 슬라이스 모두 실패는 토스트로 알린다 — 하루 상세에도 세션 오버레이에도 재시도 표면이 없어서다. showToast는 스토어 env의 선택 항목으로 주입한다.
  • 친구 판 탭을 떠나면 세션 슬라이스를 비운다. 닫기·오너 전환은 기존 리셋 리터럴이 두 슬라이스까지 함께 초기화한다.

src/react/controllers/friendScopeController.ts — 새 명령 3종(setCalendarDate · openSession · closeSession) 노출, showToast 주입.

Phase 3 — 화면 배선 (#1096)

src/react/mobileApp.tsx 친구 일지 인스턴스에 6개 prop을 공급한다.

prop
selectedDateSessions하루 슬라이스가 도착했으면 lgSessionToFull로 옮긴 완전한 세션, 아직이면 lite 행 + _calendarDetailPending: true(본인 일지와 같은 스켈레톤 문법, 이슈 #905)
selectedDaySetPurposeZones본인 일지와 같은 buildMobileDaySetPurposeZones
selectedSession친구 스코프 세션 슬롯 + readOnly: true
onOpenSession / onCloseSession친구 스코프 전용 열기·닫기
ownerName세션 상세 헤더의 "○○○ 님의 바벨릭 페이지" 줄

수정·삭제·시작 콜백은 계속 넘기지 않는다(조회 전용 관례).

Phase 4 — 뒤로가기 레이어 (#1096)

src/react/appController.tsxfriendSession 히스토리 레이어를 추가하고, 친구 판 탭 전환·판 닫기·탭 직접 이동에서 함께 소비한다.

Phase 5 — 계약 (#1096)

docs/contracts/session-screen-props.md에 "조회 문맥(친구·그룹)의 하루 상세 공급 의무" 절을 신설했다(4항: 하이드레이션 의무 · 3종 세트 콜백 · 수정 콜백 미공급으로 봉인 · 전역 세션 상태 대신 전용 슬롯).

주요 결정과 그 근거

서버를 건드리지 않기로 한 것 — 계약상 정석은 친구 전용 하루 조회 함수를 새로 만드는 것이지만, 그러려면 본인 일지의 핵심 읽기 경로 520줄을 엔진으로 옮겨야 하고 그 본문을 텍스트로 검사하는 SQL 계약 테스트 5종이 함께 흔들린다. 기존 get_session_detail로 같은 숫자가 나오는 것을 확인한 뒤 그 위험을 지지 않기로 했다.

하루 합계를 화면이 계산해도 되는 이유 — 계산에 쓰는 무게는 서버가 세트마다 실어 보낸 stats_effective_load_kg를 그대로 쓴다(clientStrengthFallback.tsclientStrengthLoadKgstatsEffectiveLoad를 우선한다). 세트 수도 서버와 같은 규칙(메인 세트만). 본인 일지도 서버 하루 요약이 도착하기 전에는 같은 계산을 쓴다. 즉 친구 본인 화면과 같은 숫자가 나온다.

작업 중 드러난 것

전역 세션 상태를 쓰면 내 일지가 오염된다. 친구 판에서 앱 전역 openSessionDetail을 부르면 origin과 무관하게 setSessionDate·setVisibleMonth가 실행돼 내 일지의 선택 날짜·표시 달이 친구 기록 날짜로 바뀌고, origin이 home/feed가 아니면 setTab("calendar")까지 실행돼 오버레이 뒤 탭이 몰래 일지로 전환되며 내 달력 읽기 모델 적재까지 발화한다. 결정적으로 mobileApp.tsxcalendarSelectedSession 조건 때문에 내 일지 판에도 같은 친구 세션이 수정·삭제 버튼이 달린 채 동시에 렌더된다 — 친구 스코프를 닫으면 그 사본이 화면에 드러난다. 그룹 화이트보드가 쓰는 전용 슬롯 방식이 이 전부를 피한다.

친구 스코프 판을 닫을 때 그 위의 레이어를 먼저 소비해야 한다. 히스토리 레이어 매니저의 consume은 스택 맨 위가 아닌 레이어를 만나면 배열에서만 빼고 history.back()을 부르지 않는다. 세션 상세가 열린 채 판을 접으면 히스토리 엔트리 1개가 주인 없이 남아 뒤로가기 한 번이 먹통이 되고 다음 뒤로가기에서 앱을 빠져나간다. 같은 어긋남이 친구 피드 경로에도 이미 있다(범위 밖 — 아래 "남은 것").

달을 넘는 날짜 이동은 목록이 늦게 온다. 하루 상세의 전날/다음날 스테퍼가 달을 넘으면 화면이 달 이동과 날짜 선택을 연달아 부르는데, 그 시점의 state.calendar.sessions는 아직 이전 달 것이다. 그대로 두면 "세션 0건"으로 굳고 날짜 dedupe에 걸려 영영 다시 안 채워진다. 달 적재가 끝날 때 열려 있는 날짜가 그 달에 속하면 하루를 다시 맞추도록 했다.

세션 lite를 넘겨도 화면은 오류를 내지 않는다. 이번 결함이 6일 동안 조용했던 구조적 이유다 — 하루 상세는 "기록이 0인 날"과 구분하지 않는다. 그래서 계약 문서에 명문화하고 컨테이너 배선 자체를 테스트로 잠갔다.

5. 적용 결과

항목결과
친구 하루 합계0kg / 0분 / 0세트 (0랩) → 실제 값(친구 본인 화면과 같은 숫자 — 무게 정본이 서버의 stats_effective_load_kg)
친구 세션 카드 종목 줄0개 → 그날 종목 수만큼(종목명 + 세트 수 + 무게 반복수x세트수 축약)
세트 강도·목적·RPE 도넛, 탑세트 TOP3미표시 → 표시(본인 화면과 같은 기준으로 화면이 계산)
세션 카드 클릭무반응 → 세션 상세 진입, 뒤로가기 1회로 하루 상세 복귀
세션 상세 진입 요청 수(열리지 않음) → 0회(하루 조회분 재사용)
날짜 선택 시 요청 수0회 → 그날 완료 세션 수만큼(대개 1, 최대 6). 같은 날 재선택은 0회
친구 일지 조회 전용 관례유지 — 수정·삭제·시작 버튼 미노출(렌더 테스트가 부재를 단언)
서버무변경 — 마이그레이션 없음, Production DB 적용 없음
로컬 게이트npm run check 2298 tests 0 fail · check:unused · build 통과
신규 테스트9건(스토어 6 + 렌더 1 + 컨테이너·레이어 배선 2)
미검증실제 두 계정 사이의 Production 실측 — 친구 스코프를 다루는 e2e 케이스가 아직 0건이다(아래 "남은 것"). 스켈레톤이 실기기에서 얼마나 자연스러운지 같은 시각적 완성도는 자동 검증 대상이 아니다

6. 이번 개선으로 향상된 것

친구 일지가 "볼 수 있는 화면"이 됐다

종전에는 친구 일지 탭이 달력까지만 쓸모가 있었다. 날짜를 누르면 빈 화면이 나왔고, 세션을 눌러도 반응이 없어 사용자 입장에서는 고장으로 보였다. 이제 친구가 그날 무슨 종목을 몇 세트 얼마로 했는지, 총 무게·시간·세트 수가 얼마인지, 세트 강도·목적 분포가 어땠는지를 본인 화면과 같은 밀도로 볼 수 있다.

남의 기록을 여는 화면이 내 화면을 오염시키지 않는다 (구조적)

친구 스코프에 전용 세션 슬롯과 전용 히스토리 레이어가 생겼다. 그룹 화이트보드에만 있던 이 패턴이 친구 쪽에도 갖춰져, 앞으로 "판 위에 겹치는 조회 화면"을 추가할 때 따라갈 선례가 두 개가 됐다.

조용한 실패를 계약이 잡는다 (구조적)

session-screen-props.md에 조회 문맥의 공급 의무가 명문화됐다 — 세션 lite만 넘기면 화면이 오류 없이 0kg·빈 카드를 그린다는 것, 세션 열기 콜백은 3종 세트라는 것, 조회 전용은 수정 콜백 미공급으로 봉인한다는 것, 판 위에 겹치는 문맥은 전용 슬롯을 쓴다는 것. 컨테이너 배선 자체도 소스 단언으로 잠겼다.

실패가 흔적을 남긴다

하루 상세 적재 실패는 reportedUserError 관문을 지나 client_error_eventssurface: "friend-scope" / operation: "get_session_detail"로 기록되고 사용자에게 토스트로 알려진다. 종전에는 실패해도 화면도 로그도 조용했다.

남은 것

  • 친구 스코프 e2e 케이스 — 친구 스코프(.fscope · friendScopeBar · friend.scope.*)를 다루는 케이스가 0건이다. 친구를 쓰는 유일한 케이스 CASE-018은 피드 신고/차단만 다룬다. 친구 일지 하루 상세를 잠그려면 일회용 계정 2개가 필요해 local-auth-admin-full-stack 프로파일이 되고 tests/react/externalAuthE2eConfig.test.mjs의 프로파일 목록 등재가 함께 필요하다. 선택자는 .fscope 하위로 좁혀야 한다 — 친구 판은 내 탭 판과 동시에 DOM에 존재해서(방문한 탭은 .off로 숨겨질 뿐 언마운트되지 않는다) journalDaySheet 같은 훅이 2개 매칭된다.
  • 친구 피드 경로의 뒤로가기 어긋남(선행) — 친구 피드에서 세션 상세를 연 채 판의 ‹(닫기)를 누르면 히스토리 엔트리가 주인 없이 남아 뒤로가기 1회가 먹통이 된다. 이번 트랙이 새로 만든 일지 경로에는 같은 문제가 없도록 배선했지만, 기존 피드 경로는 전역 세션 상태를 쓰고 있어 그대로다. 별도 수리 대상.
  • 친구 일지의 계획 세션 — 서버는 친구의 계획(planned_sessions)도 내려주지만 스토어가 저장하지 않아 친구 일지에는 완료 세션만 보인다. 종전과 동일(이번 트랙 범위 밖).