친구·그룹 데이터 접근 계약 (이슈 #1028)
화면 픽스처(앱 저장소):
src/react/ui/mobile/fixtures/FeedScreen.fixture.ts
이 문서는 명시적 신원 전달 계약(이슈 #1033, 앱 전체 원칙)의 1단(친구/그룹 읽기)이다.
원칙 (오너 결정 2026-08-31): 다른 계정의 데이터를 응답하는 지점은 전용 접근점으로 분리한다. 서버가 요청 처리 중에 "대상 계정인 것처럼" 신원을 바꾸는 방식(JWT sub 클레임 전환,
set_config('request.jwt.claim.sub', …))은 친구·그룹 노출 함수에서 금지한다. 계산이 비싸면 내부 엔진(대상 유저를 명시 인자로 받는 함수)을 본인 경로와 공유한다.
1. 구조
데이터 종류마다 세 조각으로 나눈다.
| 조각 | 형태 | 역할 |
|---|---|---|
| 내부 엔진 | …_engine(p_user_id, …) — 실행 권한 전면 회수(클라 호출 불가) | 계산 본문. auth.uid()를 읽지 않고 대상 유저를 인자로 받는다 |
| 본인 접근점 | 기존 RPC, 시그니처·payload 불변 | engine(auth.uid(), …) 위임 |
| 친구/그룹 접근점 | get_following_* / get_group_member_* | 가시성 게이트 통과 후 engine(p_user_id, …) 직접 호출 |
- 열람자(viewer)와 대상(target)은 항상 별개 인자로 다룬다. 응답에 열람자 종속 값(예:
liked_by_me)이 있으면 엔진이p_viewer를 따로 받는다. 두 역할을 한 값으로 뭉개는 것이 2026-08-26 제3자 노출 사고(20260821790000 교정)의 뿌리였다. - 접근점을 분리해 두는 이유: ① 사고 시 친구 접근점의 실행 권한만 회수하는 긴급 차단(08-26 실사용) ② 표면별 노출 축소(친구 일지=제목·개수, 친구 피드=본인 글만) ③ 본인 경로 무접촉 ④ 접속 로그에서 친구 조회 식별.
2. 게이트 정본
- 친구(팔로우) 가시성:
profile_feed_can_view_v1(호출자, 대상)— 본인 또는 팔로우. 실패 42501. - 공개 동의 게이트(앱 #1625 Phase 4, 2026-09-15):
profile_feed_can_view_v1은 본인이 아니면owner_shares_profile_v1(대상)도 요구한다 — 서버 요구 목록(onboarding_consent_requirements)에profile_sharing이 없으면(전환 전) 항상 통과, 있으면 대상이 그 버전의 유효한 동의(철회 안 됨)를 가진 경우만 통과. 검색(search_user_profiles_v1)·팔로우(follow_user_v1, 동의 없음 = 차단과 같은 P0002)·팔로잉 목록(list_following_v1)·프로필 사진 정책(profile_image_readable_v1)도 같은 판정을 쓴다. 동의 철회 = 즉시 비공개, 재동의 = 팔로우 행이 남아 있어 즉시 복구. - 민감정보 제공 동의(
owner_shares_sensitive_v1(대상)): 열람자가 본인이 아니고 대상에게sensitive_sharing동의가 없으면 팔로워 응답에서 건강 관련 필드를 뺀다 —get_session_detail은strip_session_detail_sensitive_v1로 session.condition/note/pre_note/post_condition/body_weight_kg, session_exercises[].note/entry_review, exercise_sets[].note를 null(키 유지), 피드 엔진은 review·notes, 종목 카드 엔진은 review, 팔로위 달력은 계획 note를 null. 두 판정 함수는 엔진 등급(service_role 전용)이라 문에서만 호출한다. - 그룹 동료 가시성:
group_require_member_v1+group_member_role_v1+group_content_visible_v1+ 테이크다운 검사 — 실패는 존재 누출 방지 위해 일괄 P0002. - 게이트는 접근점(문)에서만 검사한다. 엔진은 게이트를 갖지 않는다(순수 payload 빌더).
3. 친구·그룹 접근점 장부
| 접근점 | 화면 | 상태 |
|---|---|---|
get_following_calendar_month_v1 | 친구 일지 달력 | 전용 조회 (원래부터 전환 없음) |
get_following_exercise_favorites_v1 | 친구 즐겨찾기 | 전용 호출 (전환 없음) |
get_following_volume_overview_v1 | 친구 리포트 | 전환 → 엔진 직결로 전환 예정 (Phase 2-3) |
get_following_pr_overview_v1 | 친구 랜딩 종합기록 | 전환 → 엔진 직결 예정 (Phase 2-1) |
get_following_home_dashboard_v1 | 친구 랜딩 레벨·누적 | 전환 → 엔진 직결 예정 (Phase 2-2) |
get_following_profile_feed_v1 | 친구 피드 | 전환+자기글 필터 → target/viewer 분리 엔진 예정 (Phase 2-4) |
get_group_member_session_v1 | 그룹 멤버 세션 상세 | 전환 → 엔진 직결 예정 (Phase 2-5) |
새 친구/그룹 노출 함수를 추가할 때는 이 표에 등재하고, 본문에 신원 전환 코드를 넣지 않는다. 팔로워에게 새 필드를 여는 payload 변경은 그 필드가 건강 관련(체중·컨디션·메모·리뷰)인지 판단해 위 민감정보 제공 동의 판정을 붙인다(#1625) (게이트 테스트가 prosrc에 set_config('request.jwt.claim.sub' 부재를 상시 단언한다 — Phase 3).
4. 신원 전환의 잔존 허용 범위
set_config('request.jwt.claim.sub', …) 전환은 schema 전체 52곳 중 친구/그룹 접근점 5곳만 이번 트랙에서 제거한다. 나머지(통계 갱신 잡 처리 process_user_exercise_stats_refresh_jobs*, 잡 적재 트리거 enqueue_*, WodUp/Motra 인입·백필, 카탈로그 병합 정비)는 서버 자신이 배치·트리거·관리 작업을 각 유저 명의로 실행하는 내부 기계로, "다른 계정의 호출에 응답"이 아니므로 이 계약의 대상이 아니다. 단 이 함수들도 클라이언트 실행 권한이 없어야 한다.
5. 노출면 규칙
전환 제거 후에도 남는 규칙: 본인용 읽기모델(payload)에 필드를 추가하면 그 함수를 감싼 친구 접근점의 노출도 함께 넓어진다. 위 장부의 접근점이 위임하는 엔진의 payload를 바꾸는 PR은 친구 화면 노출이 적절한지 함께 판단하고, 부적절하면 친구 접근점에서 깎아낸다 (예: 친구 피드의 자기 글 한정, 친구 일지의 제목·개수 한정).