그룹 v2 상호작용 — 보는 화이트보드에서 함께 쓰는 화이트보드로 (2026-08-26)
- 기간: 2026-08-26 (1세션, 오너 지시 원문: "이거 새로 도착한거 반영해서 배선하고 PR올려서 머지까지 해주라")
- 랜딩: PR #848(Phase 0,
e788544b) · #849(Phase 1,eee03501) · #850(Phase 2,15d9a72d) — 마이그레이션20260826120000_groups_v2_interactions.sqlProduction 적용 완료(db push, 마이그레이션 내 postcheck 통과), Vercel 배포 success 실측(e788544b·15d9a72d모두 success) - 설계서: 없음(디자인 배송 트랙 — 계획서 = 이슈 #847 Phase 계획, 예상 효과·개선사항 표 포함)
- 정본:
docs/contracts/group-props.md§3·§4(오버레이 스택·doneSessions·exerciseRecords·likes·공지 1개) ·docs/contracts/session-screen-props.md(ownerName) ·supabase/migrations/20260826120000_groups_v2_interactions.sql·src/react/controllers/groupStore.ts - 도구: 없음(레포 안 게이트·테스트로만 검증)
- 게이트: pgTAP
groups_v2_interactions.test.sql(plan 38 — 공동 멤버십 가시성·차단·비공허 구성) +groups_v1.test.sqlv2 전환 ·groupInteractionsStore.test.mjs(5) ·groupScreens.test.mjsv79 렌더·앵커 - 버그리포트: 없음(신규 기능 트랙)
- 계약: group-props.md §3·§4 개정(배송분) · get_group_board_v1 payload contract_version 1→2(likes 추가)
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 0 | v79 배송(delivery-20260826c) 반입 + 렌더 계약 테스트 v79 재고정 | ✅ PR #848 (e788544b) |
| Phase 1 | 서버 — 전원 개방·공지 교체·좋아요·완료 세션·그룹 스코프 상세·사후 초대 | ✅ PR #849 (eee03501) + db push |
| Phase 2 | 컨테이너 배선 ①~⑧ 완주 | ✅ PR #850 (15d9a72d) |
| Phase 3 | 작업 기록(이 문서) + 등록 2곳 | ✅ |
1. 배경
그룹 v1(#822)은 "보는" 화이트보드였다 — 보드 작성·공지는 owner/writer 전용, 멤버는 열람·댓글·대화만. v78(#831)이 작성기를 계획 편집기로 통일한 위에, 오너가 그룹을 상호작용 표면으로 재편하는 v79 배송(delivery-20260826c)을 보냈다: 라운지·보드가 오버레이 스택으로 쌓이고, 보드 아래에 완료한 멤버가 줄지어 서고, 행을 누르면 그 멤버의 세션 상세가 열리고, 종목을 누르면 그날 그 종목을 한 멤버들의 기록이 보드 안에서 펼쳐진다. 첫 패키지는 base가 v77 랜딩 시점이라 상류 v78 19커밋과 4파일이 겹쳤고, 오너가 재싱크판(base = 47011936)을 다시 보내 교집합 0으로 반입했다.
2. 문제 제기
서버가 배송 계약을 하나도 감당하지 못했다
좋아요(테이블·RPC 없음) · 완료 멤버 세션(세션 RPC는 전부 팔로우 게이트 — 그룹 멤버십과 완전 분리) · 사후 초대(create_group_v1의 생성 시 일괄 초대뿐) · 공지 교체(append-only, 최신 10개 반환) · 보드 쓰기(owner/writer 42501).
그룹 멤버십은 세션 가시성을 주지 않았다
get_session_detail = 본인 또는 팔로워(profile_feed_can_view_v1)만. 팔로우하지 않은 그룹 동료의 완료 세션은 P0002 — "완료 행 탭 = 세션 상세"가 성립할 경로 자체가 없었다.
컨테이너는 목록을 라운지로 교체 렌더하고 있었다
뒤로가기마다 그룹 목록이 재마운트 — 배송 ①(오버레이 스택, 목록 상시 마운트)의 전제가 없었다.
3. 해결 방안
원칙 (오너 결정, 2026-08-26)
- D1 "화이트보드 운동 일단 누구나 다 수정할 수 있게" — 보드 쓰기·공지 = 멤버 전원 개방(채택). 이력·잠금은 범위 밖.
- D2 배송 명세 = 계약(공지 1개 교체·YY/MM/DD·M/D 문구·초대 전원 노출) — 그대로 집행.
접근
| 대안 | 판단 |
|---|---|
| 세션 상세: profile_feed_can_view_v1에 공동 멤버십 OR | 기각 — 피드·친구 스코프 전 표면에 가시성이 번진다(BUG-029 계열 위험) |
| 세션 상세: 상세 payload를 그룹 RPC가 재구현 | 기각 — get_session_detail(계약 v6)의 사본 유지 비용 |
| 세션 상세: 전용 래퍼 + 트랜잭션 로컬 sub 스왑(친구 리포트 v1 기법) | 채택 — 게이트만 새로 쓰고 상세 체인은 불변 재사용, 존재 누출은 균일 P0002 |
| 클라 수용: 스크린 RPC 레지스트리 정식 등록 | 기각 — 계약문서·런타임 검증기·테스트 연쇄가 트랙 범위를 넘음. 그룹 RPC 11종과 같은 소셜 레인 + 기존 strict 어댑터(adaptSessionDetailRpcRows) 직결로 동일 검증 확보 |
| 완료 세션·종목 기록: 단일 RPC(get_group_board_day_sessions_v1) | 채택 — 완료 행·종목 기록 뷰가 한 payload를 나눠 쓴다(집계·매칭 = 컨테이너, 계약대로) |
4. 적용한 내용
Phase 0 — v79 반입 (#848)
배송 10파일 전량(교집합 0 확인) + 쿠리어 손질 3건: groupScreens.test.mjs 렌더 계약 v79 재고정, group-props.md 오타 1건("나눔스쿠어라운드") 교정, 장부 selfCheck 문구의 구 브랜드 단어 마스킹(당시 브랜드 검사 게이트가 장부 자체를 잡았기 때문. 이 게이트와 원문 표기 금지 지침은 2026-09-08 폐지).
Phase 1 — 서버 (#849, 20260826120000)
- 개방:
save_group_board_v1·add_group_notice_v1writer 게이트 소거(멤버십 게이트만), can_write 에코 4곳 = true, 공지 상한 120→200자 + 교체 의미론(등록 시 기존 삭제). - 신설:
group_board_likes+set_group_board_like_v1(멱등 토글) + 보드 payloadlikes{count, liked_by_me}·get_group_board_day_sessions_v1(완료만·차단 존중·테이크다운 제외·100 상한, 서버 해석 종목명 + 세트 kg 투영) ·get_group_member_session_v1(공동 멤버십 게이트 + sub 스왑 → get_session_detail 위임, 미존재·비완료·비멤버 소유·차단 = 균일 P0002) ·invite_to_group_v1(멤버 전원, invited_count = 실생성 수). - pgTAP: 신규 38 — 팔로우 없음을 먼저 실증(get_session_detail 직접 = P0002)한 뒤 래퍼 성립을 단언, 같은 날짜에 유출 후보(비멤버 완료 세션·계획 세션)를 실존시킨 비공허 구성, 차단 시점 분리(차단자 1건 ↔ 무차단자 2건).
Phase 2 — 배선 (#850)
- ① 목록 상시 마운트 + 라운지·보드·세션 오버레이 형제 렌더 ② daySessions 병렬 로드(실패 = 빈 목록, 보드 비볼모) + 완료 행 탭 → 하이드레이션 →
UiGroupSessionOverlay안 UiSessionScreen(조회 전용 관례: readOnly·owner·ownerName·복귀 바) ③ exerciseRecords = id 우선·정규화 이름 폴백 매칭 ④UiGroupInviteSheet신설(gp-create 문법 재사용, CSS 0) + sendInvites ⑤ 공지 YY/MM/DD·전원 ⑧ 시작 바 "M/D" ⑦ 마커 = 배송 사용분 전부 기등록(신규 0). - 보드 좋아요 = 낙관 토글 → 서버 집계 화해 → 실패 원복. 스토어 슬라이스는 likes를 분리 보관하고 화면엔 board.likes로 병합.
주요 결정과 그 근거
- 오버레이 열림 중 시작 바 격납은 배선 0: 시작 바가 탭바 안이라 기존 전역
:has([data-lg-view="dashboard.session.detail"])·fd-cmsheet 규칙이 탭바째 격납한다 — BUG-030(홈→리포트)과 달리 이쪽은 탭바가 함께 사라지는 표면이었다. - 세션 상세 하이드레이션 결과는 스토어가 원본(source)으로 보관, lgSessionToFull 변환·readOnly 부착은 컨테이너 렌더에서 — sessionDetailStore의 원점 계약(home·feed·calendar)을 건드리지 않는 별도 표면.
작업 중 드러난 것
- 배송 base 검증이 일을 살렸다: 첫 zip은 base가 19커밋 뒤(v78 전체와 교집합 4파일) — baseCommit..origin/main ∩ changedFiles 검사로 잡고 재싱크판을 받았다. 반입 전 이 교집합 검사가 최우선 게이트.
- 과거 브랜드 검사 대응 기록(2026-09-08 폐지): 당시에는 장부의 검사 대상 단어를 마스킹(L*fted)했다. 현재는 브랜드 표기 검사 게이트와 이 마스킹 지침을 적용하지 않는다. 제품명은
Barbelic으로 쓴다. - 스크린 RPC 레지스트리는 이름 하나에 5파일(name union·args·payload·adapter·contract + 계약문서·검증기) — 같은 payload를 다른 게이트로 받을 땐 정식 등록 대신 어댑터 직결이 맞는 규모였다.
- 프라이머리 체크아웃이 세션 내내 낡아 있었다(백엔드 조사 에이전트가 "마이그레이션 없음"을 오판) — 조사·감사는 항상 origin/main 워크트리 기준.
5. 적용 결과
| 항목 | 전 | 후 |
|---|---|---|
| 보드 쓰기·공지 권한 | owner/writer(42501) | 멤버 전원 (pgTAP·Production postcheck) |
| 공지 | 누적(최신 10 반환) | 그룹당 1개(교체), 200자 |
| 보드 좋아요 | 없음 | 멱등 토글 + count·likedByMe (pgTAP) |
| 완료 멤버 세션 행 | 없음 | 그날 완료 세션(차단 존중) — 행 탭 = 상세 오버레이 |
| 그룹 동료 세션 상세 | 팔로우 없으면 P0002 | 공동 멤버십으로 열람(sub 스왑), 비공허 pgTAP 실증 |
| 사후 초대 | 불가(생성 시만) | 멤버 전원, 실생성 수 반환 |
| 뒤로가기 시 그룹 목록 | 재마운트 | 상시 마운트 유지(형제 렌더) |
| 테스트 | 전체 스위트 통과 유지 | 2026 테스트 0 실패 + pgTAP 38 신설 |
| 실기기 확인 | — | 미검증 — 오너 실기기 확인 대기 |
6. 이번 개선으로 향상된 것
그룹이 기록을 나누는 표면이 됐다
완료 행·종목 기록 뷰·좋아요·댓글 드로어로, 같은 보드로 운동한 멤버들의 결과가 그룹 안에서 순환한다.
구조적으로 남는 것
- 그룹 스코프 세션 가시성 계약: 공동 멤버십 게이트 + sub 스왑 래퍼 한 경로 — 피드·친구 스코프의 팔로우 게이트는 불변(pgTAP가 대조 고정).
- 스크린 RPC payload를 소셜 레인에서 재사용할 때의 관례: 기존 strict 어댑터 직결(
adaptSessionDetailRpcRows). - 배송 반입 최우선 게이트로서의 base 교집합 검사(재싱크 요청 판단 기준).
남은 것
- 오너 실기기 확인(전 동선: 목록→라운지→보드→종목 기록 뷰→완료 행→세션 상세→복귀).
- 시간·거리 전용 종목의 보드 저장 구멍(#831 미결 승계) — 완료 세션 payload도 load/reps만 투영(카디오 행 표기 열화).
- 보드 전원 개방의 상호 덮어쓰기(이력·잠금 없음) — 오너 후속 판단 여지.
- 좋아요·댓글·초대 마커(data-lg-action) 미부착(배송대로) — 게임화 계측이 필요해지면 designContract 등록.