친구 리포트 — 피드 친구 아이디 탭 → 친구의 리포트 페이지 (2026-08-23)
오너 요청
"피드에서 친구 아이디를 누르면 친구의 리포트 페이지를 조회할 수 있는 기능" — 친구 검색·팔로우 v1 · 팔로잉 포스트의 후속.
조사
- 피드 친구 카드의 아바타/닉네임 탭은 이미
onOpenFriend(author.id)를 쏘고 있었다(FdPost, 2026-07-22 계약). 죽어 있던 이유는 컨테이너(mobileApp)가 그 콜백을 넘기지 않아서다. - 진짜 공백은 서버였다. 리포트 화면의 유일한 데이터원
get_volume_overview(p_as_of, 4)는 체인 다섯 층(get_volume_overview → _annual_v3 → _v3_core → _v2_core → _v1_core)이 전부auth.uid()로 대상을 고른다 — 친구 데이터를 읽을 경로가 없었다.
설계 결정
- 체인 재발행 대신 클레임 스왑: 체인(약 900줄)을
p_user_id엔진으로 다시 발행하지 않는다. 새 래퍼get_following_volume_overview_v1(p_user_id, p_as_of)가 팔로우 게이트를 통과한 뒤 같은 트랜잭션 로컬로(set_config(..., true)) JWTsub클레임을 대상 유저로 바꿔 기존 체인을 그대로 호출하고, 돌아오면 원복한다.auth.uid()는request.jwt.claim.sub를request.jwt.claims보다 먼저 읽으므로 체인 안의 모든auth.uid()가 대상 유저를 돌려준다. 체인 정의는 한 줄도 바뀌지 않는다(본인 리포트 경로 무접촉).set_config를 품으므로volatile(쓰기는 없다). - 게이트 =
profile_feed_can_view_v1(본인 또는 팔로우한 사람) — 피드 팔로잉 포스트·친구 달력과 같은 기준. 프라이버시(공개 범위)는 이번 범위 밖. - 응답 =
get_volume_overviewv4 페이로드 그대로 → 클라이언트는 같은 strict adapter(adaptScreenRpcPayload("get_volume_overview"))와 같은 리포트 투영(buildMobileReportProjection)을 재사용한다. - 프로필 시트를 거치지 않는다: 탭 = 바로 리포트 페이지(오너 요청 그대로). 친구 프로필 시트·친구 홈 페이지(
friendProfile/onOpenFriendPage)는 계약만 유지. - 조회 전용: 친구 리포트엔
onOpenExercise/onOpenDay/onOpenWeek를 주지 않는다(종목 점프·날짜/주차 드로어는 내 데이터로 열리므로).UiReportScreen은onOpenWeek미공급 시 분기 주차 드로어도 열지 않도록 게이트. - 범위: 모바일 피드 탭. 데스크톱 홈 피드 카드(
DkH3SessionPost)는 작성자 탭 계약 자체가 없어 이번 범위 밖.
변경 — 마이그레이션 20260821520000_following_volume_overview_v1.sql
| 층 | 내용 |
|---|---|
| DB RPC | get_following_volume_overview_v1(p_user_id uuid, p_as_of date) → jsonb: 미인증 42501 · p_user_id null 22023 · 비팔로우 42501 · 게이트 통과 시 sub 클레임 스왑 → get_volume_overview(p_as_of, 4) → 원복. authenticated/service_role만 실행. 자가 검증(게이트·위임 호출·권한) |
pgTAP following_volume_overview_v1.test.sql | 14 assert — 권한 2, 본인 조회 동일, 팔로워 조회 = 대상 본인 조회 페이로드와 정확히 동일(+ contract_version 4 + 대상의 월 통계 행), 호출 뒤 auth.uid()·sub 클레임 원복, 호출자 본인 리포트 무오염(팔로워·비팔로워), 비팔로우 42501·null 22023·미인증 42501 |
| 클라이언트 데이터층 | types/supabase.ts RPC 등록 · barbelicRepository.loadFollowingVolumeOverview(같은 strict adapter) · socialDomain/BarbelicApi 노출 · controllers/friendReportStore.ts(프레임워크 무의존: open(friend)→loading→ready/error, 늦은 응답·닫힌 뒤·오너 전환 뒤 응답 폐기, 42501 문구 "팔로우한 사람의 리포트만 볼 수 있어요.", retry, close=초기 상태) + friendReportController.ts |
| 컨테이너 | appController: 렌더 컨텍스트 friendReport · openFriendReport · closeFriendReport · retryFriendReport, 히스토리 레이어 friendReport(push/consume, lgCloseLayerStateOnly 분기, 스크롤 복원 대상, 탭 이동 시 닫힘). mobileApp: UiFeedScreen.onOpenFriend 배선(피드 카드 author → 친구 lite, 팔로잉 목록 폴백) + 친구가 열려 있는 동안 피드 탭 자리에 UiReportScreen(친구 볼륨 개요 투영, profile=친구, backLabel="피드로 돌아가기", 기간·종목 컨트롤 친구별 초기화, 로딩 스켈레톤/오류 게이트+재시도) |
| UI | ReportScreen.tsx backLabel/onBack 돌아가기 행(.rp-back, 홈 hm2-back 문법) + onOpenWeek 게이트 · report.css 3줄 · barbelicMappers.buildVolumePropsFromOverview export |
| 테스트 | tests/react/friendReportStore.test.mjs 6건 |
| 문서 | rpc-catalog.md(§1 행 + "user id 파라미터 없음" 예외 명시), app-screen-rpc-contract.md(+ko) 버전표, feed-props.md, report-props.md, PRD §5.7·§8, 이 기록 |
검증
- 샌드박스 스택
db reset(520000 적용) +supabase test db전체 PASS(신규 14 포함). npm run check체인 PASS(check:unused·build포함),npm testPASS.- 브라우저 스모크(로컬 스택·온보딩 완료 계정 2개·팔로우): 피드 친구 카드 닉네임 탭 → 친구 리포트(친구 이름 타이틀·친구 월 통계) → "피드로 돌아가기"/브라우저 뒤로가기 → 피드 복귀, 비팔로우 직접 호출 42501.