diff --git a/CHANGELOG.md b/CHANGELOG.md index d35d3f96..533e3a17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ ReadMates는 Git tag와 GitHub Releases를 함께 사용합니다. 이 파일은 ### Highlights - 다음 릴리즈 후보 변경을 이 섹션에 기록합니다. +- **호스트 세션 기록 완성 UX 정리**: 호스트 세션 편집기에서 단독 피드백 문서 업로드 경로를 제거하고, AI 생성 기본 경로와 외부 JSON fallback을 하나의 `세션 기록 완성` 패널로 통합했습니다. 새 피드백 문서 저장은 세션 기록 패키지 commit을 통해서만 발생하며, 기존 `FEEDBACK_DOCUMENT_PUBLISHED` 알림 이벤트는 JSON import와 AI commit 경로에서 동일하게 기록됩니다. +- **platform-admin:** 플랫폼 운영자용 triage 콘솔(`/admin`) — 온보딩 큐, 클럽 디렉터리, 클럽 상세 + Support access grant 패널을 단일 워크벤치로 통합. OWNER 전용 support access, 라이프사이클 우선 정렬, 온보딩 결과의 즉시 선택 반영. + +### Engineering Proof Portfolio + +- Add reviewer-facing showcase index, guest-mode walkthrough, architecture evidence, engineering confidence, and operational proof docs under `docs/showcase/`. +- Add a "How to Review This Project" entry point to `README.md` pointing at the showcase set. +- Migrate `host/members` server state to TanStack Query (route loader factory seeds query cache; mutations invalidate on success; UI remains prop-driven). Documented in `docs/development/server-state-migration.md`. +- Plan host notifications query migration as a separate slice in `docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md`. +- Document the server transaction boundary policy (application-service-owned `@Transactional`; adapters stay non-transactional) in `docs/development/technical-decisions.md`. +- Refactor `JdbcHostSessionWriteAdapter` to drop redundant adapter-level `@Transactional` annotations, aligning with the documented policy. ## v1.10.2 - 2026-05-17 diff --git a/README.md b/README.md index c40dc1b5..c60e375c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,17 @@ ReadMates는 여러 정기 독서모임의 세션 준비, 참여 관리, 기록 이 저장소는 외부 공개를 전제로 정리되어 있습니다. 운영 secret, 실제 멤버 데이터, private deployment state, DB dump, 로컬 경로, OCI OCID는 문서와 예시에 포함하지 않습니다. +## How to Review This Project + +처음 보는 리뷰어라면 아래 순서가 가장 빠릅니다. + +1. **제품 표면 확인** — 게스트로 공개 클럽 소개, 공개 기록, 공개 세션 상세를 확인합니다. 시작점은 [Guest-mode walkthrough](docs/showcase/guest-mode-walkthrough.md)입니다. +2. **아키텍처 판단** — Cloudflare Pages Functions BFF, Spring API, MySQL/Flyway, Redis/Kafka, AI generation, release safety가 어떻게 연결되는지 [Architecture evidence](docs/showcase/architecture-evidence.md)에서 봅니다. +3. **유지보수 품질 확인** — frontend boundary, server ArchUnit, query budget, public release scan 같은 검증은 [Engineering confidence](docs/showcase/engineering-confidence.md)에 정리합니다. +4. **운영 증거 확인** — release readiness, deploy runbook, post-deploy watch, postmortem 흐름은 [Operational proof](docs/showcase/operational-proof.md)에서 봅니다. + +Showcase 문서는 현재 동작의 source of truth가 아니라 읽는 순서입니다. 실제 경계와 동작은 코드, 테스트, scripts, migrations, [아키텍처 문서](docs/development/architecture.md)를 우선합니다. + ## Engineering Highlights 운영 중인 서비스에서 풀어낸 비자명한 문제들입니다. 각 항목은 deep-dive로 연결됩니다. @@ -36,6 +47,8 @@ README는 제품과 아키텍처의 첫 진입점입니다. 실제 작업에서 ReadMates는 이 문제를 단순 게시판이나 CRUD 목록으로 풀지 않습니다. 공개 사이트, 멤버 앱, 호스트 운영 도구, 공개 기록, 참석자 전용 피드백 문서를 하나의 제품 흐름으로 연결해 세션 전후의 실제 운영을 줄이는 데 초점을 둡니다. +리뷰어가 로그인 없이 확인할 수 있는 공개 표면은 guest-mode walkthrough에 따로 묶었습니다. 공개 접근은 클럽 소개, 공개 기록, 공개 세션 상세로 제한되며 멤버, 호스트, platform admin, AI 생성, 알림 운영 흐름은 권한을 열지 않고 sanitized evidence로 설명합니다. + ## 역할별 기능 | 역할 | 할 수 있는 일 | @@ -43,7 +56,7 @@ ReadMates는 이 문제를 단순 게시판이나 CRUD 목록으로 풀지 않 | 게스트 | 로그인 없이 클럽별 공개 소개, 공개 기록, 공개 세션 상세를 볼 수 있습니다. | | 둘러보기 멤버 | 초대 없이 Google로 로그인한 계정입니다. 비공개 세션 기록, 현재 세션 현황, 멤버 공개 예정 세션을 읽을 수 있지만 RSVP, 체크인, 질문/서평 작성, 피드백 문서 열람, 호스트 도구는 제한됩니다. | | 정식 멤버 | 초대 링크를 수락했거나 호스트가 전환한 계정입니다. 현재 세션 참여, 예정 세션 확인, RSVP, 읽은 분량 제출, 질문, 한줄평, 장문 서평 작성, 본인 표시 이름과 이메일 알림 설정 변경, `/app/notifications` 알림함 확인이 가능하며 참석한 회차의 피드백 문서를 읽을 수 있습니다. | -| 호스트 | 정식 멤버 권한에 운영 권한이 추가됩니다. 초대 생성, 둘러보기 멤버 전환, 멤버 상태와 표시 이름 관리, 예정 세션 생성/수정, 공개 범위 설정, 현재 세션 시작, 참석 확정, 진행 세션 닫기, 닫힌 기록 발행, 세션 기록 JSON 가져오기, 피드백 문서 업로드, 세션별 수동 알림 발송과 발송 원장 운영을 수행합니다. | +| 호스트 | 정식 멤버 권한에 운영 권한이 추가됩니다. 초대 생성, 둘러보기 멤버 전환, 멤버 상태와 표시 이름 관리, 예정 세션 생성/수정, 공개 범위 설정, 현재 세션 시작, 참석 확정, 진행 세션 닫기, 닫힌 기록 발행, AI 생성 또는 JSON 가져오기를 통한 세션 기록 패키지 저장, 세션별 수동 알림 발송과 발송 원장 운영을 수행합니다. | | 플랫폼 관리자 | 클럽 생성, 첫 호스트 온보딩, 공개/비공개 상태, domain alias 생성, Cloudflare Pages marker 기반 상태 확인을 관리합니다. 클럽별 호스트/멤버 권한과 별도 권한입니다. | 로그인은 Google OAuth를 사용하며, 로컬 개발에서는 fixture 기반 dev-login을 사용할 수 있습니다. diff --git a/docs/README.md b/docs/README.md index c91320bd..7ed7f0aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ ReadMates 문서의 진입점입니다. 어떤 일을 할 때 어디 문서를 ## 디렉터리 의미 +- [Showcase](showcase/README.md): 처음 보는 리뷰어를 위한 guest-mode walkthrough, architecture evidence, engineering confidence, operational proof 진입점입니다. - [`development/`](development) — 현재 동작 기준의 정전 가이드 (architecture, local setup, test, technical decisions, versioning, release management). 코드와 충돌하면 코드와 함께 갱신합니다. - [`../design/`](../design) — 재사용 UI source package와 정적 디자인 catalog. 제품 코드가 공유하는 디자인 primitive와 pattern preview를 확인합니다. - [`deploy/`](deploy) — 운영 배포 runbook. Cloudflare Pages, OCI Compose stack, OCI MySQL HeatWave, multi-club domain, public repo safety. diff --git a/docs/development/architecture.md b/docs/development/architecture.md index 4bf2c225..7bfa70aa 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -13,7 +13,7 @@ ReadMates는 여러 정기 독서모임의 공개 소개, 멤버 세션 준비, | 공개 사이트 | `/clubs/:slug`, `/clubs/:slug/about`, `/clubs/:slug/records`, `/clubs/:slug/sessions/:sessionId`, `/`, `/about`, `/records`, `/sessions/:sessionId`, `/login`, `/clubs/:slug/invite/:token`, `/invite/:token`, `/reset-password/:token` | 게스트, 로그인 사용자 | 클럽 소개, 공개 기록, 공개 세션 상세, Google OAuth 시작, 클럽 context가 있는 초대 수락 진입, 종료된 비밀번호 경로 안내. Unscoped public route는 호환성을 위해 baseline club을 사용 | | 로그인 후 진입 | `/app`, `/clubs/:slug/app`, 등록된 club host의 `/app` | 로그인 사용자 | 가입 클럽이 하나면 해당 클럽 앱으로 이동하고, 여러 개면 클럽 선택 화면을 보여주며, 선택한 클럽 context로 앱에 진입 | | 멤버 앱 | `/clubs/:slug/app`, `/clubs/:slug/app/pending`, `/clubs/:slug/app/session/current`, `/clubs/:slug/app/notes`, `/clubs/:slug/app/archive`, `/clubs/:slug/app/sessions/:sessionId`, `/clubs/:slug/app/feedback/:sessionId`, `/clubs/:slug/app/feedback/:sessionId/print`, `/clubs/:slug/app/me`, `/clubs/:slug/app/notifications`, 등록된 club host의 `/app/**` | 둘러보기 멤버, 정식 멤버, 호스트 | 현재 세션 확인, 멤버 공개 예정 세션 확인, 둘러보기 멤버 안내, RSVP, 읽은 분량, 질문, 한줄평, 장문 서평, 아카이브, 참석 회차 피드백 문서, 본인 표시 이름과 알림 설정 변경, 클럽별 멤버 알림함 확인 | -| 호스트 앱 | `/clubs/:slug/app/host`, `/clubs/:slug/app/host/notifications`, `/clubs/:slug/app/host/members`, `/clubs/:slug/app/host/invitations`, `/clubs/:slug/app/host/sessions/new`, `/clubs/:slug/app/host/sessions/:sessionId/edit`, 등록된 club host의 `/app/host/**` | 현재 클럽의 호스트 | 예정 세션 생성/수정, 공개 범위 설정, 현재 세션 시작, 참석 확정, 진행 세션 닫기, 닫힌 기록 발행, 세션 기록 JSON 가져오기, 초대 관리, 멤버 상태와 표시 이름 관리, 피드백 문서 업로드, 알림 발송 운영 | +| 호스트 앱 | `/clubs/:slug/app/host`, `/clubs/:slug/app/host/notifications`, `/clubs/:slug/app/host/members`, `/clubs/:slug/app/host/invitations`, `/clubs/:slug/app/host/sessions/new`, `/clubs/:slug/app/host/sessions/:sessionId/edit`, 등록된 club host의 `/app/host/**` | 현재 클럽의 호스트 | 예정 세션 생성/수정, 공개 범위 설정, 현재 세션 시작, 참석 확정, 진행 세션 닫기, 닫힌 기록 발행, 세션 기록 JSON 가져오기, 초대 관리, 멤버 상태와 표시 이름 관리, 세션 기록 패키지 저장, 알림 발송 운영 | | 플랫폼 관리 | `/admin` | platform admin | 클럽 생성, 클럽 목록 확인, 공개/비공개 상태 관리, 공개 소개 정보 관리, 등록형 domain alias 요청과 상태 확인, 첫 호스트 온보딩 상태 확인. 세션/멤버/알림 같은 클럽 내부 운영은 호스트 앱 책임 | ## 프런트엔드 route-first 경계 @@ -330,17 +330,13 @@ Public route/API에는 명시적으로 공개된 데이터만 나갑니다. 피드백 문서는 모임 후 운영 산출물을 저장하고 읽기 좋게 제공하기 위한 기능입니다. ```text -External operating workflow +Session record package commit | - | Markdown or text feedback document + | AI generation result or readmates-session-import:v1 JSON v -Host upload +SessionImportService validation and replacement | - | POST /api/host/sessions/{sessionId}/feedback-document - v -Spring validation and parser - | - | UTF-8, .md/.txt, size, filename, structured sections + | UTF-8, structured feedback template, session metadata, attendee authors v MySQL session_feedback_documents | @@ -349,7 +345,7 @@ MySQL session_feedback_documents Readable response for host or attended full member ``` -호스트는 `.md` 또는 `.txt` 피드백 문서를 업로드합니다. 서버는 파일명, 크기, UTF-8 텍스트 여부를 검증하고 `FeedbackDocumentParser`로 문서를 typed response 형태로 파싱합니다. 저장은 원문 텍스트와 metadata를 versioned document로 남깁니다. +호스트는 더 이상 피드백 문서만 별도로 업로드하지 않습니다. 새 피드백 문서는 AI 생성 또는 `readmates-session-import:v1` JSON import commit이 세션 기록 패키지를 저장할 때 함께 교체됩니다. 프런트엔드에는 `/app/feedback/:sessionId/print` route와 browser print 기반 helper가 남아 있지만, 현재 `front/shared/config/readmates-feature-flags.ts`의 `feedbackDocumentPdfDownloadsEnabled`가 `false`라서 사용자는 `PDF로 저장` 또는 자동 print action을 보지 않습니다. 이 기능을 다시 켤 때는 archive, my page, feedback document route, E2E print smoke를 함께 검증합니다. diff --git a/docs/development/server-state-migration.md b/docs/development/server-state-migration.md index 70744c54..a69c4df2 100644 --- a/docs/development/server-state-migration.md +++ b/docs/development/server-state-migration.md @@ -2,8 +2,19 @@ 본 문서는 TanStack Query 마이그레이션 진행 상황을 추적합니다. +## 이번 분기 계획 + +Engineering proof portfolio 분기에서는 다음 순서로 server state migration을 진행합니다. + +1. `host/members` — 멤버 목록과 lifecycle/profile/viewer mutation을 Query invalidation 패턴으로 정리합니다. +2. `host/notifications` — 수동 알림 options/preview/confirm/dispatch ledger를 route-owned state와 Query cache로 분리합니다. +3. `host/sessions` — 세션 목록/read path부터 좁게 시작하고 editor mutation은 별도 pass로 나눕니다. + +각 migration은 UI 컴포넌트가 API를 직접 호출하지 않는다는 route-first 경계를 유지해야 합니다. + ## 완료 - `host/invitations` — list query + create/revoke mutation + loader hand-off +- `host/members` — list query + lifecycle/profile/viewer mutation refresh + loader hand-off ## 패턴 - query: `features//queries/-queries.ts` 에 `queryOptions` + `useXxxMutation` export @@ -12,8 +23,7 @@ - 컴포넌트는 actions props 인터페이스를 유지 — 테스트는 wrapper + mock actions 로 동일하게 작성 ## 후속 후보 (우선순위) -1. `host/members` +1. `host/notifications` — detailed migration plan: `docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md` 2. `host/sessions` -3. `host/notifications` -4. `current-session` (actions 4개) -5. `archive`, `feedback`, `public` — 읽기 중심, loader 와 결합도 높음 +3. `current-session` (actions 4개) +4. `archive`, `feedback`, `public` — 읽기 중심, loader 와 결합도 높음 diff --git a/docs/development/session-import-generator.md b/docs/development/session-import-generator.md index 8432f44e..2d206f3d 100644 --- a/docs/development/session-import-generator.md +++ b/docs/development/session-import-generator.md @@ -6,7 +6,7 @@ ## 모드 병존 안내 (in-app AI 생성과의 관계) -ReadMates 호스트 세션 편집기는 이 외부 JSON 업로드 흐름과 **in-app AI 생성** 두 모드를 함께 제공합니다. 편집기 상단의 `[ 외부 도구 JSON 업로드 ]` / `[ AI 결과 가져오기 ]` 토글로 모드를 전환하며, 모드 선택은 `?aigen=1` URL query로 보존됩니다. +호스트 세션 편집기는 `세션 기록 완성` 패널에서 AI 생성을 기본 경로로 보여주고, 외부 JSON 가져오기를 fallback으로 제공합니다. 단독 `.md` 또는 `.txt` 피드백 문서 업로드는 더 이상 제공하지 않습니다. | 모드 | 입력 | LLM 호출 위치 | 운영 게이트 | | --- | --- | --- | --- | diff --git a/docs/development/technical-decisions.md b/docs/development/technical-decisions.md index 05071cd5..96261365 100644 --- a/docs/development/technical-decisions.md +++ b/docs/development/technical-decisions.md @@ -129,3 +129,11 @@ boot 시 Kafka listener bean이 등록되지 않는지 log 확인. **Trade-off:** token bucket이 주 경계에서 reset되는 의도된 부작용이 있습니다. 율 제한은 단기(분~시간 단위) 정책이므로 실질적인 영향은 없습니다. 토큰·세션 ID 해시에는 여전히 `stableHash`(salt 없음)를 사용해 주 경계 영향을 받지 않습니다. **관련 문서와 검증:** `./server/gradlew -p server test --tests '*ClientIpHashing*'` + +## Transaction Boundary Policy + +Application services own business transaction boundaries. Controllers parse HTTP and call use cases; persistence adapters execute SQL and mapping. When an application service coordinates more than one write port, the service method owns the transaction so cache invalidation, notification event recording, and state mutation share one visible boundary. + +Adapter-level `@Transactional` is allowed only when the adapter is called by an inbound scheduler, Kafka listener, or other path that does not already pass through an application service transaction. If both service and adapter carry `@Transactional`, the service boundary is treated as the authoritative boundary and the adapter annotation should be removed in a narrow cleanup once tests pin the behavior. + +Isolation is specified only where the operation depends on claim/read-modify-write behavior that needs a non-default guarantee. Existing examples include session/login restoration and notification delivery claiming. New isolation choices must be explained in the service or adjacent decision record. diff --git a/docs/showcase/README.md b/docs/showcase/README.md new file mode 100644 index 00000000..a2a23490 --- /dev/null +++ b/docs/showcase/README.md @@ -0,0 +1,26 @@ +# ReadMates Showcase + +이 디렉터리는 ReadMates를 처음 보는 리뷰어가 제품, 아키텍처, 운영 증거, 유지보수 품질을 빠르게 따라갈 수 있도록 만든 reviewer-facing guide입니다. + +현재 동작의 source of truth는 코드, 테스트, scripts, migrations, `docs/development/architecture.md`입니다. Showcase 문서는 그 자료를 대체하지 않고 읽는 순서를 제공합니다. + +## 추천 리뷰 순서 + +1. `README.md`에서 제품 문제와 역할 모델을 확인합니다. +2. `docs/showcase/guest-mode-walkthrough.md`에서 로그인 없이 볼 수 있는 공개 제품 표면을 따라갑니다. +3. `docs/showcase/architecture-evidence.md`에서 BFF, Spring API, MySQL, Redis/Kafka, AI generation, release safety가 어떻게 연결되는지 봅니다. +4. `docs/showcase/engineering-confidence.md`에서 테스트와 경계 검증이 어떤 회귀를 막는지 확인합니다. +5. `docs/showcase/operational-proof.md`에서 release, deploy, observability, postmortem 흐름을 확인합니다. + +## 문서별 역할 + +| 문서 | 답하는 질문 | +| --- | --- | +| `guest-mode-walkthrough.md` | 로그인 없이 무엇을 볼 수 있고, private workflow는 어떤 evidence로 확인하는가? | +| `architecture-evidence.md` | 이 프로젝트가 단순 CRUD가 아니라 운영형 제품인 근거는 무엇인가? | +| `engineering-confidence.md` | 코드베이스가 커져도 무너지지 않게 하는 경계와 검증은 무엇인가? | +| `operational-proof.md` | 배포, 공개 릴리즈 안전, 장애 대응은 어떤 흐름으로 관리되는가? | + +## 공개 안전 기준 + +Showcase 문서는 실제 멤버 데이터, private domain, 운영 secret, deployment state, OCID, token-shaped example, local absolute path를 포함하지 않습니다. Private workflow는 접근 권한을 넓히지 않고 sanitized 설명, fixture, 테스트, runbook으로 설명합니다. diff --git a/docs/showcase/architecture-evidence.md b/docs/showcase/architecture-evidence.md new file mode 100644 index 00000000..5b7d9af4 --- /dev/null +++ b/docs/showcase/architecture-evidence.md @@ -0,0 +1,42 @@ +# Architecture Evidence + +이 문서는 ReadMates가 단순 CRUD 앱이 아니라 운영형 멀티클럽 제품인 이유를 한 장으로 보여줍니다. 상세 source of truth는 `docs/development/architecture.md`입니다. + +## One-Page Map + +```text +Browser + -> Cloudflare Pages SPA + -> Pages Functions BFF (/api/bff/**, OAuth proxy) + -> Spring Boot API + -> MySQL/Flyway source of truth + -> optional Redis cache/rate-limit/job state + -> optional Kafka/Redpanda notification and AI job pipeline + -> SMTP/in-app notification side effects +``` + +## Evidence Table + +| Product/engineering claim | Why it matters | Evidence | +| --- | --- | --- | +| Browser traffic goes through a same-origin BFF | Keeps browser-facing security policy, trusted headers, OAuth proxying, and cookie handling at the edge boundary. | `docs/development/adr/0001-cloudflare-pages-functions-bff.md`, `docs/case-studies/01-bff-security-and-secret-rotation.md` | +| Club context is scoped by slug or registered host | Multi-club operation needs role, cache, public URL, and OAuth return behavior to stay club-aware. | `docs/case-studies/03-multi-club-domain-platform.md`, `docs/deploy/multi-club-domains.md` | +| Server feature slices follow clean architecture | Controllers parse HTTP; application services own authorization/orchestration; persistence stays behind ports/adapters. | `docs/development/architecture.md`, `ServerArchitectureBoundaryTest` | +| Notifications use transactional outbox | Mutations do not block on SMTP/in-app delivery; retry and audit state are explicit. | `docs/case-studies/02-notification-pipeline-with-outbox.md` | +| AI generation is feature-gated and audited | Transcript handling, provider calls, cost guard, kill switch, and PII policy are operational boundaries. | `docs/case-studies/04-pii-safe-ai-session-generation.md`, `docs/operations/runbooks/ai-session-generation.md`, `scripts/aigen-pii-check.sh` | +| Public release safety is scripted | Public candidates are built and scanned before release assumptions are made. | `scripts/README.md`, `docs/deploy/security-public-repo.md` | + +## Request Flow + +1. Browser requests same-origin SPA or `/api/bff/**`. +2. Pages Functions strips untrusted internal headers and adds trusted BFF headers. +3. Spring validates BFF secret, session cookie, membership, role, visibility, and attendance rules. +4. MySQL/Flyway remains source of truth. +5. Redis and Kafka are optional supporting layers, never the durable source of private transcript or membership truth. + +## What This Document Does Not Replace + +- API and role details: `docs/development/architecture.md` +- Local setup and checks: `docs/development/README.md` +- Release safety details: `scripts/README.md` +- Deployment runbooks: `docs/deploy/README.md` diff --git a/docs/showcase/engineering-confidence.md b/docs/showcase/engineering-confidence.md new file mode 100644 index 00000000..a2460dd5 --- /dev/null +++ b/docs/showcase/engineering-confidence.md @@ -0,0 +1,59 @@ +# Engineering Confidence + +이 문서는 ReadMates가 커진 뒤에도 변경 가능한 코드베이스로 남기 위해 사용하는 경계, 테스트, 품질 게이트를 정리합니다. + +## Boundary Evidence + +| Boundary | Guardrail | What it prevents | +| --- | --- | --- | +| Frontend route-first architecture | `front/tests/unit/frontend-boundaries.test.ts` | shared가 app/page/feature를 거꾸로 import하거나 feature UI가 route/API를 직접 잡는 회귀 | +| Server clean architecture | `ServerArchitectureBoundaryTest` | web adapter가 persistence/JDBC를 직접 잡거나 application package가 Spring Web/adapter에 의존하는 회귀 | +| CQRS read/write convention | `@ReadOnlyApplicationService` + ArchUnit rules | read-only service가 mutation port나 write transaction을 갖는 회귀 | +| Flyway migration compatibility | `MySqlFlywayMigrationTest` | MySQL-specific migration, collation, FK compatibility 회귀 | +| Query budget | `ServerQueryBudgetTest` | 주요 화면의 accidental N+1 query 회귀 | +| Public release safety | `scripts/build-public-release-candidate.sh`, `scripts/public-release-check.sh` | public candidate에 private state, local path, secret-shaped data가 포함되는 회귀 | + +## Frontend Server-State Migration + +Current source: `docs/development/server-state-migration.md` + +Completed: + +- `host/invitations` — list query, create/revoke mutation, loader handoff + +Next candidates: + +1. `host/members` +2. `host/notifications` +3. `host/sessions` + +Migration rule: route modules own loader/action coordination, UI components stay prop/callback driven, and new Query helpers live under `front/features//queries/`. + +## Server Boundary Follow-Ups + +The session package already has separate draft, lifecycle, attendance, publication, and query services. The next useful server confidence work is transaction boundary documentation and a narrow cleanup of adapter-level transaction annotations where application services already own the transaction. + +## Validation Commands + +Frontend: + +```bash +pnpm --dir front lint +pnpm --dir front test +pnpm --dir front build +``` + +Server: + +```bash +./server/gradlew -p server unitTest +./server/gradlew -p server architectureTest +./server/gradlew -p server check +``` + +Public release: + +```bash +./scripts/build-public-release-candidate.sh +./scripts/public-release-check.sh .tmp/public-release-candidate +``` diff --git a/docs/showcase/guest-mode-walkthrough.md b/docs/showcase/guest-mode-walkthrough.md new file mode 100644 index 00000000..9db50268 --- /dev/null +++ b/docs/showcase/guest-mode-walkthrough.md @@ -0,0 +1,43 @@ +# Guest-Mode Walkthrough + +이 문서는 ReadMates를 처음 보는 리뷰어가 로그인 없이 확인할 수 있는 공개 제품 표면과, 로그인 없이 볼 수 없는 private workflow를 어떤 evidence로 확인할지 정리합니다. + +현재 동작의 source of truth는 public route code와 `docs/development/architecture.md`입니다. + +## 로그인 없이 볼 수 있는 것 + +Guest는 클럽이 `ACTIVE`이고 `PUBLIC`인 경우 아래 표면을 볼 수 있습니다. + +| 표면 | 경로 | 확인할 수 있는 것 | +| --- | --- | --- | +| 클럽 소개 | `/clubs/` 또는 `/clubs//about` | 클럽의 공개 소개와 공개 진입 경험 | +| 공개 기록 | `/clubs//records` | 공개된 회차 목록과 archive 흐름 | +| 공개 세션 상세 | `/clubs//sessions/` | 공개 요약, 하이라이트, 한줄평 등 공개 범위에 포함된 기록 | + +운영 fallback 경로는 `https://readmates.pages.dev/clubs/` 형태입니다. 등록된 custom domain은 운영 설정에 따라 달라지므로 이 문서에서는 placeholder만 사용합니다. + +## 추천 관람 순서 + +1. 클럽 소개에서 제품의 공개 첫인상을 확인합니다. +2. 공개 기록 목록에서 회차가 누적되는 방식을 확인합니다. +3. 공개 세션 상세에서 모임 후 기록이 어떻게 읽히는지 확인합니다. +4. README의 Engineering Highlights로 돌아가 공개 화면 뒤의 BFF, publication visibility, notification, AI generation 근거를 확인합니다. + +## 로그인 없이 볼 수 없는 것 + +아래 흐름은 제품 권한상 guest에게 공개하지 않습니다. + +| Private workflow | 공개하지 않는 이유 | 확인 evidence | +| --- | --- | --- | +| 멤버 현재 세션 참여, RSVP, 질문, 서평 작성 | 정식 멤버 권한과 club membership이 필요합니다. | `docs/development/architecture.md`, frontend route guard tests | +| 호스트 세션 생성/수정, 출석 확정, 기록 발행 | 클럽 host 권한이 필요합니다. | host route tests, session server tests, case studies | +| Platform admin onboarding/domain/support access | platform admin 권한이 필요합니다. | platform admin plan/spec, server authorization tests | +| In-app AI 세션 생성 | host 권한, feature flag, provider key, cost/PII guard가 필요합니다. | `docs/case-studies/04-pii-safe-ai-session-generation.md`, AI runbook, `scripts/aigen-pii-check.sh` | +| 수동 알림 발송 | host 권한과 notification outbox pipeline이 필요합니다. | `docs/case-studies/02-notification-pipeline-with-outbox.md`, notification tests | + +## Public-Safety Notes + +- 이 walkthrough는 guest 권한을 넓히지 않습니다. +- 실제 멤버 데이터, private domain, 운영 secret, provider key, deployment state는 사용하지 않습니다. +- Screenshot을 추가할 때는 synthetic 또는 sanitized fixture만 사용합니다. +- Private workflow를 보여줄 필요가 있으면 접근 권한을 열지 않고 테스트, runbook, sanitized 설명으로 연결합니다. diff --git a/docs/showcase/operational-proof.md b/docs/showcase/operational-proof.md new file mode 100644 index 00000000..e706a633 --- /dev/null +++ b/docs/showcase/operational-proof.md @@ -0,0 +1,44 @@ +# Operational Proof + +이 문서는 ReadMates가 기능 구현 뒤 release, deploy, observability, incident learning까지 어떻게 닫는지 보여주는 reviewer-facing guide입니다. + +## Release Evidence Flow + +```text +Change + -> targeted local checks + -> release readiness review + -> public release candidate build/check + -> changelog/release note update + -> deploy runbook + -> smoke/post-deploy watch + -> postmortem when an incident occurs +``` + +## Evidence Links + +| Stage | Evidence | +| --- | --- | +| Release readiness | `docs/development/release-readiness-review.md` | +| Public release candidate | `scripts/build-public-release-candidate.sh`, `scripts/public-release-check.sh`, `scripts/README.md` | +| Public repository safety | `docs/deploy/security-public-repo.md` | +| Deploy runbooks | `docs/deploy/README.md`, `docs/deploy/release-publish-runbook.md` | +| Observability | `docs/operations/observability/README.md` | +| Post-deploy watch | `docs/operations/runbooks/post-deploy-watch.md` | +| Incident learning | `docs/operations/postmortems/README.md` | + +## Operating Principle + +Passing tests is evidence, not proof that release risk is closed. Release readiness review also checks changelog coverage, operator-facing behavior changes, CI/deploy script risks, security-code hygiene, architecture-test baselines, and public-release safety. + +## Public-Safe Incident Learning + +Incident writeups should explain: + +- trigger and customer/operator impact +- detection path +- rollback or mitigation +- root cause +- prevention added to code, tests, scripts, or runbooks + +Incident writeups must not include real member data, private domains, secrets, raw provider payloads, local paths, or deployment identifiers. diff --git a/docs/superpowers/plans/2026-05-17-readmates-engineering-proof-portfolio-implementation-plan.md b/docs/superpowers/plans/2026-05-17-readmates-engineering-proof-portfolio-implementation-plan.md new file mode 100644 index 00000000..4c615fd0 --- /dev/null +++ b/docs/superpowers/plans/2026-05-17-readmates-engineering-proof-portfolio-implementation-plan.md @@ -0,0 +1,1348 @@ +# ReadMates Engineering Proof Portfolio Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn ReadMates into a reviewable engineering proof portfolio by connecting README, guest-mode showcase, architecture evidence, quality gates, operational proof, and selected frontend/server confidence work. + +**Architecture:** Keep documentation entrypoints separate from current source-of-truth docs. Add `docs/showcase/` as a reviewer-facing layer that links to `README.md`, `docs/development/architecture.md`, case studies, scripts, runbooks, and tests without replacing them. Keep code work scoped to existing frontend route-first and server clean-architecture boundaries. + +**Tech Stack:** Markdown documentation, React 19/Vite/TanStack Query v5 for frontend confidence work, Kotlin/Spring Boot/MySQL/Flyway for server confidence work, existing shell release-safety scripts. + +--- + +## Source Spec + +Design spec: `docs/superpowers/specs/2026-05-17-readmates-engineering-proof-portfolio-design.md` + +## Scope Check + +This is a quarter-level master plan with multiple independent tracks. Execute it as separate PRs in the task order below. Each task has its own changed files and validation command. Do not combine documentation showcase work, frontend Query migration, and server boundary work in one PR. + +The first execution pass should complete Tasks 1-6. Tasks 7-10 are code-confidence follow-ups that can run after the reviewer-facing documentation exists. Task 11 is the release-readiness closeout for the whole initiative. + +## Public Safety Rules + +Every task follows the same public-repository constraints: + +- Do not add real member data, private domains, deployment state, local absolute paths, OCIDs, secrets, API keys, token-shaped examples, DB dumps, or raw logs. +- Use repo-relative paths in docs. +- Use placeholders such as `https://api.example.com`, ``, and `host@example.com`. +- Treat `docs/development/architecture.md`, code, tests, scripts, and runbooks as current source of truth. Treat `docs/superpowers/` as historical planning context unless the task is explicitly editing this plan or the source spec. + +## File Structure + +Create: + +- `docs/showcase/README.md` — reviewer-facing index for the engineering proof portfolio. +- `docs/showcase/guest-mode-walkthrough.md` — login-free guest-mode review path and private-workflow evidence links. +- `docs/showcase/architecture-evidence.md` — one-page architecture/evidence map for external readers. +- `docs/showcase/engineering-confidence.md` — tests, quality gates, frontend/server boundary evidence, and improvement status. +- `docs/showcase/operational-proof.md` — release, deploy, observability, post-deploy watch, and postmortem evidence flow. +- `front/features/host/queries/host-members-queries.ts` — TanStack Query helpers for host members, mirroring `host-invitation-queries.ts`. + +Modify: + +- `README.md` — add a compact "How to review this project" entry path and link to showcase docs. +- `docs/README.md` — include `docs/showcase/` as reviewer-facing documentation. +- `docs/development/server-state-migration.md` — update host-members migration status when Task 7 lands. +- `docs/development/technical-decisions.md` — add transaction boundary decision note in Task 9. +- `front/src/app/routes/host.tsx` — pass `QueryClient` into the host members loader factory. +- `front/features/host/route/host-members-data.ts` — seed host-members query data and expose response parsers/actions for UI. +- `front/features/host/route/host-members-route.tsx` — keep route as UI composition only. +- `front/features/host/ui/host-members.tsx` — read list state through TanStack Query while preserving prop-driven actions. +- `front/tests/unit/host-members.test.tsx` — pin loader handoff, invalidation, and pagination behavior. +- `server/src/main/kotlin/com/readmates/session/adapter/out/persistence/JdbcHostSessionWriteAdapter.kt` — first transaction-boundary cleanup after Task 9 policy is documented. + +Do not modify: + +- `docs/private/**` +- real deploy state files +- `.env*` except existing placeholder-only `.env.example` if a future task explicitly requires it + +--- + +## Task 1: Create Showcase Index + +**Files:** + +- Create: `docs/showcase/README.md` +- Modify: `docs/README.md` + +- [ ] **Step 1: Read documentation source-of-truth guidance** + +Run: + +```bash +sed -n '1,240p' docs/agents/docs.md +sed -n '1,220p' docs/README.md +``` + +Expected: `docs/agents/docs.md` states that `README.md` is an entry point and `docs/development/architecture.md` is source of truth for technical boundaries. + +- [ ] **Step 2: Create the showcase directory index** + +Create `docs/showcase/README.md` with this exact structure: + +```markdown +# ReadMates Showcase + +이 디렉터리는 ReadMates를 처음 보는 리뷰어가 제품, 아키텍처, 운영 증거, 유지보수 품질을 빠르게 따라갈 수 있도록 만든 reviewer-facing guide입니다. + +현재 동작의 source of truth는 코드, 테스트, scripts, migrations, `docs/development/architecture.md`입니다. Showcase 문서는 그 자료를 대체하지 않고 읽는 순서를 제공합니다. + +## 추천 리뷰 순서 + +1. `README.md`에서 제품 문제와 역할 모델을 확인합니다. +2. `docs/showcase/guest-mode-walkthrough.md`에서 로그인 없이 볼 수 있는 공개 제품 표면을 따라갑니다. +3. `docs/showcase/architecture-evidence.md`에서 BFF, Spring API, MySQL, Redis/Kafka, AI generation, release safety가 어떻게 연결되는지 봅니다. +4. `docs/showcase/engineering-confidence.md`에서 테스트와 경계 검증이 어떤 회귀를 막는지 확인합니다. +5. `docs/showcase/operational-proof.md`에서 release, deploy, observability, postmortem 흐름을 확인합니다. + +## 문서별 역할 + +| 문서 | 답하는 질문 | +| --- | --- | +| `guest-mode-walkthrough.md` | 로그인 없이 무엇을 볼 수 있고, private workflow는 어떤 evidence로 확인하는가? | +| `architecture-evidence.md` | 이 프로젝트가 단순 CRUD가 아니라 운영형 제품인 근거는 무엇인가? | +| `engineering-confidence.md` | 코드베이스가 커져도 무너지지 않게 하는 경계와 검증은 무엇인가? | +| `operational-proof.md` | 배포, 공개 릴리즈 안전, 장애 대응은 어떤 흐름으로 관리되는가? | + +## 공개 안전 기준 + +Showcase 문서는 실제 멤버 데이터, private domain, 운영 secret, deployment state, OCID, token-shaped example, local absolute path를 포함하지 않습니다. Private workflow는 접근 권한을 넓히지 않고 sanitized 설명, fixture, 테스트, runbook으로 설명합니다. +``` + +- [ ] **Step 3: Link the showcase index from docs hub** + +Modify `docs/README.md` by adding this bullet near the documentation index: + +```markdown +- [Showcase](showcase/README.md): 처음 보는 리뷰어를 위한 guest-mode walkthrough, architecture evidence, engineering confidence, operational proof 진입점입니다. +``` + +Keep the Korean-first documentation tone and do not remove existing links. + +- [ ] **Step 4: Validate docs formatting** + +Run: + +```bash +git diff --check -- docs/showcase/README.md docs/README.md +``` + +Expected: no output. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add docs/showcase/README.md docs/README.md +git commit -m "docs: add showcase index" +``` + +Expected: one commit containing only the showcase index and docs hub link. + +--- + +## Task 2: Add README Review Entry Path + +**Files:** + +- Modify: `README.md` + +- [ ] **Step 1: Inspect current README entry flow** + +Run: + +```bash +sed -n '1,180p' README.md +``` + +Expected: README begins with product summary, stack, engineering highlights, and role/function overview. + +- [ ] **Step 2: Add a compact review path after the opening summary** + +Insert this section after the opening bullet list and before `## Engineering Highlights`: + +```markdown +## How to Review This Project + +처음 보는 리뷰어라면 아래 순서가 가장 빠릅니다. + +1. **제품 표면 확인** — 게스트로 공개 클럽 소개, 공개 기록, 공개 세션 상세를 확인합니다. 시작점은 [Guest-mode walkthrough](docs/showcase/guest-mode-walkthrough.md)입니다. +2. **아키텍처 판단** — Cloudflare Pages Functions BFF, Spring API, MySQL/Flyway, Redis/Kafka, AI generation, release safety가 어떻게 연결되는지 [Architecture evidence](docs/showcase/architecture-evidence.md)에서 봅니다. +3. **유지보수 품질 확인** — frontend boundary, server ArchUnit, query budget, public release scan 같은 검증은 [Engineering confidence](docs/showcase/engineering-confidence.md)에 정리합니다. +4. **운영 증거 확인** — release readiness, deploy runbook, post-deploy watch, postmortem 흐름은 [Operational proof](docs/showcase/operational-proof.md)에서 봅니다. + +Showcase 문서는 현재 동작의 source of truth가 아니라 읽는 순서입니다. 실제 경계와 동작은 코드, 테스트, scripts, migrations, [아키텍처 문서](docs/development/architecture.md)를 우선합니다. +``` + +- [ ] **Step 3: Add a short guest-mode pointer near the role table** + +Before `## 역할별 기능`, add: + +```markdown +리뷰어가 로그인 없이 확인할 수 있는 공개 표면은 guest-mode walkthrough에 따로 묶었습니다. 공개 접근은 클럽 소개, 공개 기록, 공개 세션 상세로 제한되며 멤버, 호스트, platform admin, AI 생성, 알림 운영 흐름은 권한을 열지 않고 sanitized evidence로 설명합니다. +``` + +- [ ] **Step 4: Validate README diff** + +Run: + +```bash +git diff --check -- README.md +rg -n "How to Review This Project|guest-mode walkthrough|private domain|OCID|token-shaped|local absolute path" README.md +``` + +Expected: `git diff --check` has no output. `rg` finds the new review section and does not reveal local absolute paths or private values. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add README.md +git commit -m "docs: add reviewer entry path" +``` + +Expected: one README-only commit. + +--- + +## Task 3: Write Guest-Mode Walkthrough + +**Files:** + +- Create: `docs/showcase/guest-mode-walkthrough.md` + +- [ ] **Step 1: Verify current public route names** + +Run: + +```bash +sed -n '1,180p' docs/development/architecture.md +sed -n '1,180p' front/src/app/routes/public.tsx +``` + +Expected: architecture lists public routes for `/clubs/:slug`, `/clubs/:slug/about`, `/clubs/:slug/records`, and `/clubs/:slug/sessions/:sessionId`. + +- [ ] **Step 2: Create walkthrough document** + +Create `docs/showcase/guest-mode-walkthrough.md`: + +```markdown +# Guest-Mode Walkthrough + +이 문서는 ReadMates를 처음 보는 리뷰어가 로그인 없이 확인할 수 있는 공개 제품 표면과, 로그인 없이 볼 수 없는 private workflow를 어떤 evidence로 확인할지 정리합니다. + +현재 동작의 source of truth는 public route code와 `docs/development/architecture.md`입니다. + +## 로그인 없이 볼 수 있는 것 + +Guest는 클럽이 `ACTIVE`이고 `PUBLIC`인 경우 아래 표면을 볼 수 있습니다. + +| 표면 | 경로 | 확인할 수 있는 것 | +| --- | --- | --- | +| 클럽 소개 | `/clubs/` 또는 `/clubs//about` | 클럽의 공개 소개와 공개 진입 경험 | +| 공개 기록 | `/clubs//records` | 공개된 회차 목록과 archive 흐름 | +| 공개 세션 상세 | `/clubs//sessions/` | 공개 요약, 하이라이트, 한줄평 등 공개 범위에 포함된 기록 | + +운영 fallback 경로는 `https://readmates.pages.dev/clubs/` 형태입니다. 등록된 custom domain은 운영 설정에 따라 달라지므로 이 문서에서는 placeholder만 사용합니다. + +## 추천 관람 순서 + +1. 클럽 소개에서 제품의 공개 첫인상을 확인합니다. +2. 공개 기록 목록에서 회차가 누적되는 방식을 확인합니다. +3. 공개 세션 상세에서 모임 후 기록이 어떻게 읽히는지 확인합니다. +4. README의 Engineering Highlights로 돌아가 공개 화면 뒤의 BFF, publication visibility, notification, AI generation 근거를 확인합니다. + +## 로그인 없이 볼 수 없는 것 + +아래 흐름은 제품 권한상 guest에게 공개하지 않습니다. + +| Private workflow | 공개하지 않는 이유 | 확인 evidence | +| --- | --- | --- | +| 멤버 현재 세션 참여, RSVP, 질문, 서평 작성 | 정식 멤버 권한과 club membership이 필요합니다. | `docs/development/architecture.md`, frontend route guard tests | +| 호스트 세션 생성/수정, 출석 확정, 기록 발행 | 클럽 host 권한이 필요합니다. | host route tests, session server tests, case studies | +| Platform admin onboarding/domain/support access | platform admin 권한이 필요합니다. | platform admin plan/spec, server authorization tests | +| In-app AI 세션 생성 | host 권한, feature flag, provider key, cost/PII guard가 필요합니다. | `docs/case-studies/04-pii-safe-ai-session-generation.md`, AI runbook, `scripts/aigen-pii-check.sh` | +| 수동 알림 발송 | host 권한과 notification outbox pipeline이 필요합니다. | `docs/case-studies/02-notification-pipeline-with-outbox.md`, notification tests | + +## Public-Safety Notes + +- 이 walkthrough는 guest 권한을 넓히지 않습니다. +- 실제 멤버 데이터, private domain, 운영 secret, provider key, deployment state는 사용하지 않습니다. +- Screenshot을 추가할 때는 synthetic 또는 sanitized fixture만 사용합니다. +- Private workflow를 보여줄 필요가 있으면 접근 권한을 열지 않고 테스트, runbook, sanitized 설명으로 연결합니다. +``` + +- [ ] **Step 3: Validate public-safety wording** + +Run: + +```bash +git diff --check -- docs/showcase/guest-mode-walkthrough.md +rg -n "local absolute path|OCID|private key|token-shaped|private domain|real member" docs/showcase/guest-mode-walkthrough.md +``` + +Expected: `git diff --check` has no output. `rg` returns no active secret, local path, or private deployment value. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add docs/showcase/guest-mode-walkthrough.md +git commit -m "docs: add guest mode walkthrough" +``` + +Expected: one commit containing the walkthrough. + +--- + +## Task 4: Write Architecture Evidence Map + +**Files:** + +- Create: `docs/showcase/architecture-evidence.md` + +- [ ] **Step 1: Inspect architecture and case study anchors** + +Run: + +```bash +sed -n '1,220p' docs/development/architecture.md +sed -n '1,200p' docs/case-studies/README.md +``` + +Expected: architecture describes product surfaces, BFF request flow, frontend route-first boundary, API error contract, multi-club context, server package boundaries, auth/session, BFF security, Redis, and public cache. + +- [ ] **Step 2: Create architecture evidence document** + +Create `docs/showcase/architecture-evidence.md`: + +```markdown +# Architecture Evidence + +이 문서는 ReadMates가 단순 CRUD 앱이 아니라 운영형 멀티클럽 제품인 이유를 한 장으로 보여줍니다. 상세 source of truth는 `docs/development/architecture.md`입니다. + +## One-Page Map + +```text +Browser + -> Cloudflare Pages SPA + -> Pages Functions BFF (/api/bff/**, OAuth proxy) + -> Spring Boot API + -> MySQL/Flyway source of truth + -> optional Redis cache/rate-limit/job state + -> optional Kafka/Redpanda notification and AI job pipeline + -> SMTP/in-app notification side effects +``` + +## Evidence Table + +| Product/engineering claim | Why it matters | Evidence | +| --- | --- | --- | +| Browser traffic goes through a same-origin BFF | Keeps browser-facing security policy, trusted headers, OAuth proxying, and cookie handling at the edge boundary. | `docs/development/adr/0001-cloudflare-pages-functions-bff.md`, `docs/case-studies/01-bff-security-and-secret-rotation.md` | +| Club context is scoped by slug or registered host | Multi-club operation needs role, cache, public URL, and OAuth return behavior to stay club-aware. | `docs/case-studies/03-multi-club-domain-platform.md`, `docs/deploy/multi-club-domains.md` | +| Server feature slices follow clean architecture | Controllers parse HTTP; application services own authorization/orchestration; persistence stays behind ports/adapters. | `docs/development/architecture.md`, `ServerArchitectureBoundaryTest` | +| Notifications use transactional outbox | Mutations do not block on SMTP/in-app delivery; retry and audit state are explicit. | `docs/case-studies/02-notification-pipeline-with-outbox.md` | +| AI generation is feature-gated and audited | Transcript handling, provider calls, cost guard, kill switch, and PII policy are operational boundaries. | `docs/case-studies/04-pii-safe-ai-session-generation.md`, `docs/operations/runbooks/ai-session-generation.md`, `scripts/aigen-pii-check.sh` | +| Public release safety is scripted | Public candidates are built and scanned before release assumptions are made. | `scripts/README.md`, `docs/deploy/security-public-repo.md` | + +## Request Flow + +1. Browser requests same-origin SPA or `/api/bff/**`. +2. Pages Functions strips untrusted internal headers and adds trusted BFF headers. +3. Spring validates BFF secret, session cookie, membership, role, visibility, and attendance rules. +4. MySQL/Flyway remains source of truth. +5. Redis and Kafka are optional supporting layers, never the durable source of private transcript or membership truth. + +## What This Document Does Not Replace + +- API and role details: `docs/development/architecture.md` +- Local setup and checks: `docs/development/README.md` +- Release safety details: `scripts/README.md` +- Deployment runbooks: `docs/deploy/README.md` +``` + +- [ ] **Step 3: Validate diagram fence and docs formatting** + +Run: + +```bash +git diff --check -- docs/showcase/architecture-evidence.md +rg -n "```text|```" docs/showcase/architecture-evidence.md +``` + +Expected: `git diff --check` has no output and code fences are balanced. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add docs/showcase/architecture-evidence.md +git commit -m "docs: add architecture evidence map" +``` + +Expected: one commit containing only the architecture evidence doc. + +--- + +## Task 5: Write Engineering Confidence Guide + +**Files:** + +- Create: `docs/showcase/engineering-confidence.md` +- Modify: `docs/development/server-state-migration.md` + +- [ ] **Step 1: Inspect existing quality and migration docs** + +Run: + +```bash +sed -n '1,220p' docs/development/server-state-migration.md +sed -n '1,220p' docs/development/test-guide.md +rg -n "frontend-boundaries|ServerArchitectureBoundaryTest|MySqlFlywayMigrationTest|ServerQueryBudgetTest" front server docs +``` + +Expected: server-state migration lists `host/invitations` as complete and `host/members` as the next candidate. + +- [ ] **Step 2: Create engineering confidence document** + +Create `docs/showcase/engineering-confidence.md`: + +```markdown +# Engineering Confidence + +이 문서는 ReadMates가 커진 뒤에도 변경 가능한 코드베이스로 남기 위해 사용하는 경계, 테스트, 품질 게이트를 정리합니다. + +## Boundary Evidence + +| Boundary | Guardrail | What it prevents | +| --- | --- | --- | +| Frontend route-first architecture | `front/tests/unit/frontend-boundaries.test.ts` | shared가 app/page/feature를 거꾸로 import하거나 feature UI가 route/API를 직접 잡는 회귀 | +| Server clean architecture | `ServerArchitectureBoundaryTest` | web adapter가 persistence/JDBC를 직접 잡거나 application package가 Spring Web/adapter에 의존하는 회귀 | +| CQRS read/write convention | `@ReadOnlyApplicationService` + ArchUnit rules | read-only service가 mutation port나 write transaction을 갖는 회귀 | +| Flyway migration compatibility | `MySqlFlywayMigrationTest` | MySQL-specific migration, collation, FK compatibility 회귀 | +| Query budget | `ServerQueryBudgetTest` | 주요 화면의 accidental N+1 query 회귀 | +| Public release safety | `scripts/build-public-release-candidate.sh`, `scripts/public-release-check.sh` | public candidate에 private state, local path, secret-shaped data가 포함되는 회귀 | + +## Frontend Server-State Migration + +Current source: `docs/development/server-state-migration.md` + +Completed: + +- `host/invitations` — list query, create/revoke mutation, loader handoff + +Next candidates: + +1. `host/members` +2. `host/notifications` +3. `host/sessions` + +Migration rule: route modules own loader/action coordination, UI components stay prop/callback driven, and new Query helpers live under `front/features//queries/`. + +## Server Boundary Follow-Ups + +The session package already has separate draft, lifecycle, attendance, publication, and query services. The next useful server confidence work is transaction boundary documentation and a narrow cleanup of adapter-level transaction annotations where application services already own the transaction. + +## Validation Commands + +Frontend: + +```bash +pnpm --dir front lint +pnpm --dir front test +pnpm --dir front build +``` + +Server: + +```bash +./server/gradlew -p server unitTest +./server/gradlew -p server architectureTest +./server/gradlew -p server check +``` + +Public release: + +```bash +./scripts/build-public-release-candidate.sh +./scripts/public-release-check.sh .tmp/public-release-candidate +``` +``` + +- [ ] **Step 3: Update server-state migration status** + +Modify `docs/development/server-state-migration.md` to add an "이번 분기 계획" section: + +```markdown +## 이번 분기 계획 + +Engineering proof portfolio 분기에서는 다음 순서로 server state migration을 진행합니다. + +1. `host/members` — 멤버 목록과 lifecycle/profile/viewer mutation을 Query invalidation 패턴으로 정리합니다. +2. `host/notifications` — 수동 알림 options/preview/confirm/dispatch ledger를 route-owned state와 Query cache로 분리합니다. +3. `host/sessions` — 세션 목록/read path부터 좁게 시작하고 editor mutation은 별도 pass로 나눕니다. + +각 migration은 UI 컴포넌트가 API를 직접 호출하지 않는다는 route-first 경계를 유지해야 합니다. +``` + +- [ ] **Step 4: Validate** + +Run: + +```bash +git diff --check -- docs/showcase/engineering-confidence.md docs/development/server-state-migration.md +``` + +Expected: no output. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add docs/showcase/engineering-confidence.md docs/development/server-state-migration.md +git commit -m "docs: document engineering confidence evidence" +``` + +Expected: one docs-only commit. + +--- + +## Task 6: Write Operational Proof Guide + +**Files:** + +- Create: `docs/showcase/operational-proof.md` + +- [ ] **Step 1: Inspect release and operations docs** + +Run: + +```bash +sed -n '1,220p' docs/development/release-readiness-review.md +sed -n '1,220p' scripts/README.md +sed -n '1,180p' docs/operations/README.md +sed -n '1,180p' docs/operations/runbooks/README.md +``` + +Expected: release readiness warns that passing tests is not proof that release risk is closed. + +- [ ] **Step 2: Create operational proof document** + +Create `docs/showcase/operational-proof.md`: + +```markdown +# Operational Proof + +이 문서는 ReadMates가 기능 구현 뒤 release, deploy, observability, incident learning까지 어떻게 닫는지 보여주는 reviewer-facing guide입니다. + +## Release Evidence Flow + +```text +Change + -> targeted local checks + -> release readiness review + -> public release candidate build/check + -> changelog/release note update + -> deploy runbook + -> smoke/post-deploy watch + -> postmortem when an incident occurs +``` + +## Evidence Links + +| Stage | Evidence | +| --- | --- | +| Release readiness | `docs/development/release-readiness-review.md` | +| Public release candidate | `scripts/build-public-release-candidate.sh`, `scripts/public-release-check.sh`, `scripts/README.md` | +| Public repository safety | `docs/deploy/security-public-repo.md` | +| Deploy runbooks | `docs/deploy/README.md`, `docs/deploy/release-publish-runbook.md` | +| Observability | `docs/operations/observability/README.md` | +| Post-deploy watch | `docs/operations/runbooks/post-deploy-watch.md` | +| Incident learning | `docs/operations/postmortems/README.md` | + +## Operating Principle + +Passing tests is evidence, not proof that release risk is closed. Release readiness review also checks changelog coverage, operator-facing behavior changes, CI/deploy script risks, security-code hygiene, architecture-test baselines, and public-release safety. + +## Public-Safe Incident Learning + +Incident writeups should explain: + +- trigger and customer/operator impact +- detection path +- rollback or mitigation +- root cause +- prevention added to code, tests, scripts, or runbooks + +Incident writeups must not include real member data, private domains, secrets, raw provider payloads, local paths, or deployment identifiers. +``` + +- [ ] **Step 3: Validate** + +Run: + +```bash +git diff --check -- docs/showcase/operational-proof.md +rg -n "local absolute path|OCID|token-shaped|private key|private domain" docs/showcase/operational-proof.md +``` + +Expected: no whitespace errors and no active private values. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add docs/showcase/operational-proof.md +git commit -m "docs: add operational proof guide" +``` + +Expected: one docs-only commit. + +--- + +## Task 7: Migrate Host Members to TanStack Query + +**Files:** + +- Create: `front/features/host/queries/host-members-queries.ts` +- Modify: `front/src/app/routes/host.tsx` +- Modify: `front/features/host/route/host-members-data.ts` +- Modify: `front/features/host/ui/host-members.tsx` +- Modify: `front/tests/unit/host-members.test.tsx` +- Modify: `docs/development/server-state-migration.md` + +- [ ] **Step 1: Read frontend guide and current invitations pattern** + +Run: + +```bash +sed -n '1,220p' docs/agents/front.md +sed -n '1,220p' front/features/host/queries/host-invitation-queries.ts +sed -n '1,160p' front/features/host/route/host-invitations-data.ts +``` + +Expected: host invitations uses `queryOptions`, `setQueryData`, and invalidates `hostInvitationKeys.all`. + +- [ ] **Step 2: Add host members query helper** + +Create `front/features/host/queries/host-members-queries.ts`: + +```typescript +import type { QueryClient } from "@tanstack/react-query"; +import { queryOptions, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + fetchHostMembers, + submitHostMemberLifecycle, + submitHostMemberProfile, + submitHostViewerAction, +} from "@/features/host/api/host-api"; +import type { + HostMemberListPage, + MemberLifecycleRequest, +} from "@/features/host/api/host-contracts"; +import type { + HostMemberLifecyclePath, + HostViewerAction, +} from "@/features/host/route/host-members-actions"; +import type { ReadmatesApiContext } from "@/shared/api/client"; +import type { PageRequest } from "@/shared/model/paging"; + +export const hostMemberKeys = { + all: ["host", "members"] as const, + list: (page?: PageRequest) => [...hostMemberKeys.all, "list", page ?? {}] as const, +} as const; + +async function fetchHostMemberList( + context?: ReadmatesApiContext, + page?: PageRequest, +): Promise { + return fetchHostMembers(context, page); +} + +export function hostMemberListQuery(page?: PageRequest, context?: ReadmatesApiContext) { + return queryOptions({ + queryKey: hostMemberKeys.list(page), + queryFn: () => fetchHostMemberList(context, page), + }); +} + +export function invalidateHostMembers(client: QueryClient) { + return client.invalidateQueries({ queryKey: hostMemberKeys.all }); +} + +export function useHostMemberLifecycleMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + path, + body, + }: { + membershipId: string; + path: HostMemberLifecyclePath; + body?: MemberLifecycleRequest; + }) => submitHostMemberLifecycle(membershipId, path, body), + onSuccess: () => invalidateHostMembers(client), + }); +} + +export function useHostMemberProfileMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + displayName, + }: { + membershipId: string; + displayName: string; + }) => submitHostMemberProfile(membershipId, displayName), + onSuccess: () => invalidateHostMembers(client), + }); +} + +export function useHostViewerActionMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + action, + }: { + membershipId: string; + action: HostViewerAction; + }) => submitHostViewerAction(membershipId, action), + onSuccess: () => invalidateHostMembers(client), + }); +} +``` + +- [ ] **Step 3: Convert loader to factory and seed query cache** + +Modify `front/features/host/route/host-members-data.ts` so it exports `hostMembersLoaderFactory(client: QueryClient)`: + +```typescript +import type { QueryClient } from "@tanstack/react-query"; +import { + fetchHostMembers, + submitHostMemberLifecycle, + submitHostMemberProfile, + submitHostViewerAction, +} from "@/features/host/api/host-api"; +import type { HostMembersActions } from "@/features/host/route/host-members-actions"; +import { hostMemberListQuery } from "@/features/host/queries/host-members-queries"; +import type { LoaderFunctionArgs } from "react-router-dom"; +import { requireHostLoaderAuth } from "./host-loader-auth"; +import { clubSlugFromLoaderArgs } from "@/shared/auth/member-app-loader"; + +const HOST_MEMBERS_PAGE_LIMIT = 50; + +export function hostMembersLoaderFactory(client: QueryClient) { + return async (args?: LoaderFunctionArgs) => { + await requireHostLoaderAuth(args); + + const page = await fetchHostMembers( + { clubSlug: clubSlugFromLoaderArgs(args) }, + { limit: HOST_MEMBERS_PAGE_LIMIT }, + ); + + client.setQueryData( + hostMemberListQuery({ limit: HOST_MEMBERS_PAGE_LIMIT }).queryKey, + page, + ); + + return page; + }; +} + +export const hostMembersActions = { + loadMembers: (page) => fetchHostMembers(undefined, page), + submitLifecycle: submitHostMemberLifecycle, + submitProfile: submitHostMemberProfile, + submitViewerAction: submitHostViewerAction, +} satisfies HostMembersActions; +``` + +- [ ] **Step 4: Pass query client from host routes** + +Modify the `members` route in `front/src/app/routes/host.tsx` to thread `queryClient` into the loader factory while keeping the surrounding route shape identical to the current code (no new `errorElement` or `hydrateFallbackElement` fields): + +```typescript +{ + path: "members", + lazy: async () => { + const [{ HostMembersRouteElement }, { hostMembersLoaderFactory }] = await Promise.all([ + import("@/src/app/host-route-elements"), + import("@/features/host/route/host-members-data"), + ]); + return { + Component: HostMembersRouteElement, + loader: hostMembersLoaderFactory(queryClient), + }; + }, +} +``` + +The only change versus the current `host.tsx` member route is that `hostMembersLoader` becomes `hostMembersLoaderFactory(queryClient)`. Error/loading fallback wiring stays out of this task; if it should be added, do it in a separate route-UX PR. + +- [ ] **Step 5: Wire `HostMembers` to Query without moving API calls into UI** + +In `front/features/host/ui/host-members.tsx`, import Query helpers: + +```typescript +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { hostMemberListQuery, invalidateHostMembers } from "@/features/host/queries/host-members-queries"; +``` + +Replace the local initial page/member source setup with the same pattern used by host invitations: + +```typescript +const initialPage = normalizeMemberPage(initialMembers); +const queryClient = useQueryClient(); +const listQuery = useQuery({ + ...hostMemberListQuery({ limit: 50 }), + queryFn: async () => normalizeMemberPage(await actions.loadMembers({ limit: 50 })), + initialData: initialPage, +}); +const queryMembers = listQuery.data?.items ?? []; +const [memberRowsState, setMemberRowsState] = useState(() => ({ + source: queryMembers, + members: queryMembers, +})); +const members = memberRowsState.source === queryMembers ? memberRowsState.members : queryMembers; +``` + +After each successful lifecycle, profile, and viewer action path that currently calls `refreshMembers()`, keep the existing UI refresh behavior and add: + +```typescript +await invalidateHostMembers(queryClient); +``` + +Do not call `fetchHostMembers` directly from UI. Continue using `actions.loadMembers`. + +- [ ] **Step 6: Add focused tests** + +Modify `front/tests/unit/host-members.test.tsx` with tests that assert: + +```typescript +it("seeds the host members list through the route loader", async () => { + const fetchMock = renderHostMembersPage(); + + expect(await screen.findByRole("tab", { name: "활성 멤버" })).toBeInTheDocument(); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/bff/host/members?limit=50"), + expect.anything(), + ); +}); +``` + +And: + +```typescript +it("refreshes the Query-backed list after a member profile update", async () => { + const user = userEvent.setup(); + const updated = { ...members[0], displayName: "새이름", accountName: "안멤버1" } satisfies HostMemberListItem; + renderHostMembersPage([memberListItemResponse(updated)]); + + const row = within((await screen.findByText("멤버1")).closest("article") as HTMLElement); + await user.click(row.getByRole("button", { name: "이름 변경" })); + + const dialog = within(screen.getByRole("dialog", { name: "멤버1 이름 수정" })); + await user.clear(dialog.getByLabelText("이름")); + await user.type(dialog.getByLabelText("이름"), "새이름"); + await user.click(dialog.getByRole("button", { name: "저장" })); + + expect(await screen.findByText("새이름")).toBeInTheDocument(); + expect(screen.queryByText("멤버1")).not.toBeInTheDocument(); +}); +``` + +If the existing helper URL assertion differs because BFF path construction is mocked at a lower level, assert the exact current mocked URL used by `renderHostMembersPage()` rather than changing production code for the test. + +- [ ] **Step 7: Update migration status** + +Modify `docs/development/server-state-migration.md`: + +```markdown +## 완료 +- `host/invitations` — list query + create/revoke mutation + loader hand-off +- `host/members` — list query + lifecycle/profile/viewer mutation refresh + loader hand-off +``` + +Also update the `## 후속 후보 (우선순위)` list so it (a) removes `host/members` and (b) reorders the remaining frontend candidates to match the design spec section 9.1 priority (`host/notifications` before `host/sessions`). The intended post-edit shape: + +```markdown +## 후속 후보 (우선순위) +1. `host/notifications` +2. `host/sessions` +3. `current-session` (actions 4개) +4. `archive`, `feedback`, `public` — 읽기 중심, loader 와 결합도 높음 +``` + +This keeps Task 8's premise (notifications is the next slice) consistent with the migration status doc. + +- [ ] **Step 8: Run frontend checks** + +Run: + +```bash +pnpm --dir front test -- host-members +pnpm --dir front lint +pnpm --dir front build +``` + +Expected: all commands pass. + +- [ ] **Step 9: Commit** + +Run: + +```bash +git add front/features/host/queries/host-members-queries.ts \ + front/src/app/routes/host.tsx \ + front/features/host/route/host-members-data.ts \ + front/features/host/ui/host-members.tsx \ + front/tests/unit/host-members.test.tsx \ + docs/development/server-state-migration.md +git commit -m "feat(front): migrate host members to query cache" +``` + +Expected: one frontend confidence commit. + +--- + +## Task 8: Plan Host Notifications Query Migration as a Separate Slice + +**Files:** + +- Create: `docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md` +- Modify: `docs/development/server-state-migration.md` + +- [ ] **Step 1: Inspect current notification route and UI split** + +Run: + +```bash +sed -n '1,220p' front/features/host/route/host-notifications-data.ts +sed -n '1,220p' front/features/host/route/host-notifications-route.tsx +find front/features/host/ui/notifications -maxdepth 1 -type f | sort +``` + +Expected: notifications are larger than host members and need a separate migration plan. + +- [ ] **Step 2: Create a focused notification migration plan** + +Create `docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md` with this header: + +```markdown +# ReadMates Host Notifications Query Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move host notification summary, event ledger, delivery ledger, manual options, preview, confirm, and dispatch ledger reads into TanStack Query without moving API calls into UI components. + +**Architecture:** Keep `front/features/host/route` responsible for loader/action coordination and keep `front/features/host/ui/notifications` prop/callback driven. Add `front/features/host/queries/host-notification-queries.ts` for query keys, queryOptions, and mutation invalidation helpers. + +**Tech Stack:** React 19, React Router 7, TanStack Query v5, Vitest, Testing Library. + +--- +``` + +Continue the new plan with this body: + +````markdown +## Task 1: Map Current Notification Data Flow + +**Files:** + +- Read: `front/features/host/route/host-notifications-data.ts` +- Read: `front/features/host/route/host-notifications-route.tsx` +- Read: `front/features/host/ui/host-notifications-page.tsx` +- Read: `front/features/host/ui/notifications/manual-notification-workbench.tsx` + +- [ ] **Step 1: Inspect existing route and UI data flow** + +Run: + +```bash +sed -n '1,240p' front/features/host/route/host-notifications-data.ts +sed -n '1,240p' front/features/host/route/host-notifications-route.tsx +sed -n '1,260p' front/features/host/ui/host-notifications-page.tsx +sed -n '1,260p' front/features/host/ui/notifications/manual-notification-workbench.tsx +``` + +Expected: route owns loader data, while UI coordinates several host notification reads and manual dispatch actions. + +## Task 2: Add Notification Query Keys + +**Files:** + +- Create: `front/features/host/queries/host-notification-queries.ts` + +- [ ] **Step 1: Create query key module** + +Create query keys for `summary`, `items(status,page)`, `events(page)`, `deliveries(page)`, `manualOptions(sessionId,search,page)`, and `manualDispatches(sessionId,eventType,page)`. Each key starts with `["host", "notifications"]`. + +- [ ] **Step 2: Add invalidation helpers** + +Add `invalidateHostNotifications(client)` for all host notification state and `invalidateManualNotificationState(client)` for manual options/dispatches. + +## Task 3: Seed Loader Data + +**Files:** + +- Modify: `front/src/app/routes/host.tsx` +- Modify: `front/features/host/route/host-notifications-data.ts` + +- [ ] **Step 1: Convert loader to factory** + +Follow the `hostMembersLoaderFactory(client)` pattern from the engineering proof portfolio plan. Seed summary, events, deliveries, and manual options into Query cache from loader data. + +## Task 4: Move Preview and Confirm to Query Mutations + +**Files:** + +- Modify: `front/features/host/ui/notifications/manual-notification-workbench.tsx` + +- [ ] **Step 1: Keep UI prop-driven** + +Use actions passed from the route for API calls. Do not import `host-api.ts` into UI. Use Query mutations only to track pending state and invalidation. + +- [ ] **Step 2: Preserve preview TTL and resend confirmation** + +After preview success, keep the preview token and selection hash state in the workbench. After confirm success, invalidate manual dispatches and notification summary. + +## Task 5: Test Notification Migration + +**Files:** + +- Modify: `front/tests/unit/host-notifications.test.tsx` + +- [ ] **Step 1: Add regression tests** + +Add these regression tests to `front/tests/unit/host-notifications.test.tsx`: + +```typescript +it("keeps manual preview state when notification queries invalidate", async () => { + // Arrange with the existing manual notification route fixture. + // Preview a manual notification. + // Trigger an invalidation through a successful confirm or process action. + // Assert the preview token, selected template, and target count remain visible until confirm resolves. +}); + +it("requires explicit resend confirmation after query migration", async () => { + // Arrange with a recent manual dispatch fixture for the same session/template. + // Preview the same dispatch. + // Assert confirm is blocked until the resend confirmation control is selected. +}); + +it("refreshes manual dispatch ledger after confirm", async () => { + // Arrange with an empty dispatch ledger. + // Confirm a preview. + // Assert the ledger query refetch shows the new dispatch row. +}); +``` + +Replace the comments with the existing test helper calls in that file; keep the three test names and assertions. + +- [ ] **Step 2: Run checks** + +Run: + +```bash +pnpm --dir front test -- host-notifications +pnpm --dir front lint +pnpm --dir front build +``` + +Expected: all commands pass. +```` + +- [ ] **Step 3: Update migration status** + +Modify `docs/development/server-state-migration.md` so `host/notifications` points to the new detailed plan: + +```markdown +2. `host/notifications` — detailed migration plan: `docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md` +``` + +- [ ] **Step 4: Validate and commit** + +Run: + +```bash +git diff --check -- docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md docs/development/server-state-migration.md +``` + +Commit: + +```bash +git add docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md docs/development/server-state-migration.md +git commit -m "docs: plan host notifications query migration" +``` + +Expected: one planning commit with no frontend runtime changes. + +--- + +## Task 9: Document Server Transaction Boundary Policy + +**Files:** + +- Modify: `docs/development/technical-decisions.md` + +- [ ] **Step 1: Inspect current transaction ownership** + +Run: + +```bash +rg -n "@Transactional" server/src/main/kotlin/com/readmates/session server/src/main/kotlin/com/readmates/notification server/src/main/kotlin/com/readmates/club server/src/main/kotlin/com/readmates/auth +sed -n '1,140p' server/src/main/kotlin/com/readmates/session/application/service/HostSessionDraftCommandService.kt +sed -n '1,140p' server/src/main/kotlin/com/readmates/session/adapter/out/persistence/JdbcHostSessionWriteAdapter.kt +``` + +Expected: session application services own write transactions, while some persistence adapters still carry method-level `@Transactional`. + +- [ ] **Step 2: Add transaction policy note** + +Append this section to `docs/development/technical-decisions.md`: + +```markdown +## Transaction Boundary Policy + +Application services own business transaction boundaries. Controllers parse HTTP and call use cases; persistence adapters execute SQL and mapping. When an application service coordinates more than one write port, the service method owns the transaction so cache invalidation, notification event recording, and state mutation share one visible boundary. + +Adapter-level `@Transactional` is allowed only when the adapter is called by an inbound scheduler, Kafka listener, or other path that does not already pass through an application service transaction. If both service and adapter carry `@Transactional`, the service boundary is treated as the authoritative boundary and the adapter annotation should be removed in a narrow cleanup once tests pin the behavior. + +Isolation is specified only where the operation depends on claim/read-modify-write behavior that needs a non-default guarantee. Existing examples include session/login restoration and notification delivery claiming. New isolation choices must be explained in the service or adjacent decision record. +``` + +- [ ] **Step 3: Validate docs formatting** + +Run: + +```bash +git diff --check -- docs/development/technical-decisions.md +``` + +Expected: no output. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git add docs/development/technical-decisions.md +git commit -m "docs: document transaction boundary policy" +``` + +Expected: one docs-only server-confidence commit. + +--- + +## Task 10: Remove Redundant Host Session Adapter Transactions + +**Files:** + +- Modify: `server/src/main/kotlin/com/readmates/session/adapter/out/persistence/JdbcHostSessionWriteAdapter.kt` + +- [ ] **Step 1: Confirm application services own session write transactions** + +Run: + +```bash +sed -n '1,120p' server/src/main/kotlin/com/readmates/session/application/service/HostSessionDraftCommandService.kt +sed -n '1,140p' server/src/main/kotlin/com/readmates/session/application/service/HostSessionLifecycleService.kt +sed -n '1,80p' server/src/main/kotlin/com/readmates/session/application/service/HostSessionAttendanceService.kt +sed -n '1,80p' server/src/main/kotlin/com/readmates/session/application/service/HostSessionPublicationService.kt +``` + +Expected: create, update, updateVisibility, open, close, publish, delete, confirmAttendance, and upsertPublication are already service-level `@Transactional` operations. + +- [ ] **Step 2: Run current host session tests before refactor** + +Run: + +```bash +./server/gradlew -p server unitTest --tests 'com.readmates.session.application.service.HostSessionServicesTest' +./server/gradlew -p server integrationTest --tests 'com.readmates.session.api.HostSessionControllerDbTest' +``` + +Expected: both commands pass before the refactor. + +- [ ] **Step 3: Remove adapter transaction annotations** + +Modify `server/src/main/kotlin/com/readmates/session/adapter/out/persistence/JdbcHostSessionWriteAdapter.kt`: + +```kotlin +// Remove this import: +import org.springframework.transaction.annotation.Transactional +``` + +Remove the `@Transactional` annotation immediately above each of these methods: + +```kotlin +override fun create(command: HostSessionCommand) +override fun update(command: UpdateHostSessionCommand) +override fun delete(command: HostSessionIdCommand) +override fun confirmAttendance(command: ConfirmAttendanceCommand) +override fun upsertPublication(command: UpsertPublicationCommand) +override fun updateVisibility(command: UpdateHostSessionVisibilityCommand) +override fun open(command: HostSessionIdCommand) +override fun close(command: HostSessionIdCommand) +override fun publish(command: HostSessionIdCommand) +``` + +Do not change SQL, ports, service signatures, cache invalidation, notification recording, or query methods. + +- [ ] **Step 4: Run server checks after refactor** + +Run: + +```bash +./server/gradlew -p server unitTest --tests 'com.readmates.session.application.service.HostSessionServicesTest' +./server/gradlew -p server integrationTest --tests 'com.readmates.session.api.HostSessionControllerDbTest' +./server/gradlew -p server architectureTest +``` + +Expected: all commands pass. + +- [ ] **Step 5: Commit** + +Run: + +```bash +git add server/src/main/kotlin/com/readmates/session/adapter/out/persistence/JdbcHostSessionWriteAdapter.kt +git commit -m "refactor(server): clarify session transaction boundary" +``` + +Expected: one server confidence commit with no API behavior change. + +--- + +## Task 11: Initiative Closeout and Release Readiness Review + +**Files:** + +- Modify: `README.md` if final links or wording need alignment +- Modify: `CHANGELOG.md` if user-visible docs/showcase or quality workflow changes should be noted +- Modify: `docs/showcase/*.md` only for consistency fixes + +- [ ] **Step 1: Review branch scope** + +Run: + +```bash +git status --short --branch +git log --oneline origin/main..HEAD +git diff --stat origin/main..HEAD +git diff --name-only origin/main..HEAD +``` + +Expected: only intended docs, frontend confidence, server confidence, and release-safety files are changed across the initiative branch. + +- [ ] **Step 2: Run release-readiness checks** + +Run: + +```bash +git diff --check origin/main..HEAD +rg -n "^## Unreleased|Engineering proof|showcase|guest-mode|server-state|transaction" CHANGELOG.md README.md docs +``` + +Expected: no whitespace errors. Findings show where the initiative is documented. + +- [ ] **Step 3: Run public release candidate checks** + +Run: + +```bash +./scripts/build-public-release-candidate.sh +./scripts/public-release-check.sh .tmp/public-release-candidate +``` + +Expected: candidate build succeeds and scanner reports no blocking public-safety finding. + +- [ ] **Step 4: Run code checks if Tasks 7 or 10 changed runtime code** + +Frontend code changed: + +```bash +pnpm --dir front lint +pnpm --dir front test +pnpm --dir front build +``` + +Server code changed: + +```bash +./server/gradlew -p server unitTest +./server/gradlew -p server architectureTest +``` + +Expected: touched-surface checks pass. If a command cannot run because local dependencies are unavailable, record the skipped command and exact reason in the final review note. + +- [ ] **Step 5: Produce final release-readiness note** + +Create a short final note in the PR description or release-readiness review comment with: + +```markdown +## Scope + +- Reviewer-facing showcase docs +- README review path +- Engineering confidence evidence +- Operational proof evidence +- Frontend Query migration work completed in this branch +- Server transaction boundary work completed in this branch + +## Validation + +- `git diff --check origin/main..HEAD` +- `./scripts/build-public-release-candidate.sh` +- `./scripts/public-release-check.sh .tmp/public-release-candidate` +- Frontend checks: `pnpm --dir front lint`, `pnpm --dir front test`, `pnpm --dir front build`; skipped commands are listed with the exact local blocker. +- Server checks: `./server/gradlew -p server unitTest`, `./server/gradlew -p server architectureTest`; skipped commands are listed with the exact local blocker. + +## Residual Risk + +- Showcase docs summarize current source-of-truth docs and can become stale if architecture changes without link updates. +- Guest-mode walkthrough depends on public route behavior staying aligned with `docs/development/architecture.md`. +- Host notifications Query migration remains tracked separately if Task 8 was planning-only. +``` + +- [ ] **Step 6: Commit closeout changes** + +Run: + +```bash +git add README.md CHANGELOG.md docs/showcase +git commit -m "docs: close engineering proof portfolio review" +``` + +Expected: commit only if Step 5 revealed actual file changes. If no files changed, do not create an empty commit. + +--- + +## Plan Self-Review + +Spec coverage: + +- Portfolio entry: Tasks 1-2 +- Guest-mode showcase: Task 3 +- Architecture evidence: Task 4 +- Engineering confidence: Tasks 5, 7, 8, 9, 10 +- Operational proof: Task 6 +- Release/public safety verification: Task 11 +- Public safety constraints: global rules plus task validation scans + +No task requires private data, public auth bypass, real deployment state, or external live provider keys. + +Execution rule: implement tasks in order and commit after each task. Do not batch Tasks 1-6 with Tasks 7-10. diff --git a/docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md b/docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md new file mode 100644 index 00000000..257f44cd --- /dev/null +++ b/docs/superpowers/plans/2026-05-17-readmates-host-notifications-query-migration.md @@ -0,0 +1,117 @@ +# ReadMates Host Notifications Query Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move host notification summary, event ledger, delivery ledger, manual options, preview, confirm, and dispatch ledger reads into TanStack Query without moving API calls into UI components. + +**Architecture:** Keep `front/features/host/route` responsible for loader/action coordination and keep `front/features/host/ui/notifications` prop/callback driven. Add `front/features/host/queries/host-notification-queries.ts` for query keys, queryOptions, and mutation invalidation helpers. + +**Tech Stack:** React 19, React Router 7, TanStack Query v5, Vitest, Testing Library. + +--- + +## Task 1: Map Current Notification Data Flow + +**Files:** + +- Read: `front/features/host/route/host-notifications-data.ts` +- Read: `front/features/host/route/host-notifications-route.tsx` +- Read: `front/features/host/ui/host-notifications-page.tsx` +- Read: `front/features/host/ui/notifications/manual-notification-workbench.tsx` + +- [ ] **Step 1: Inspect existing route and UI data flow** + +Run: + +```bash +sed -n '1,240p' front/features/host/route/host-notifications-data.ts +sed -n '1,240p' front/features/host/route/host-notifications-route.tsx +sed -n '1,260p' front/features/host/ui/host-notifications-page.tsx +sed -n '1,260p' front/features/host/ui/notifications/manual-notification-workbench.tsx +``` + +Expected: route owns loader data, while UI coordinates several host notification reads and manual dispatch actions. + +## Task 2: Add Notification Query Keys + +**Files:** + +- Create: `front/features/host/queries/host-notification-queries.ts` + +- [ ] **Step 1: Create query key module** + +Create query keys for `summary`, `items(status,page)`, `events(page)`, `deliveries(page)`, `manualOptions(sessionId,search,page)`, and `manualDispatches(sessionId,eventType,page)`. Each key starts with `["host", "notifications"]`. + +- [ ] **Step 2: Add invalidation helpers** + +Add `invalidateHostNotifications(client)` for all host notification state and `invalidateManualNotificationState(client)` for manual options/dispatches. + +## Task 3: Seed Loader Data + +**Files:** + +- Modify: `front/src/app/routes/host.tsx` +- Modify: `front/features/host/route/host-notifications-data.ts` + +- [ ] **Step 1: Convert loader to factory** + +Follow the `hostMembersLoaderFactory(client)` pattern from the engineering proof portfolio plan. Seed summary, events, deliveries, and manual options into Query cache from loader data. + +## Task 4: Move Preview and Confirm to Query Mutations + +**Files:** + +- Modify: `front/features/host/ui/notifications/manual-notification-workbench.tsx` + +- [ ] **Step 1: Keep UI prop-driven** + +Use actions passed from the route for API calls. Do not import `host-api.ts` into UI. Use Query mutations only to track pending state and invalidation. + +- [ ] **Step 2: Preserve preview TTL and resend confirmation** + +After preview success, keep the preview token and selection hash state in the workbench. After confirm success, invalidate manual dispatches and notification summary. + +## Task 5: Test Notification Migration + +**Files:** + +- Modify: `front/tests/unit/host-notifications.test.tsx` + +- [ ] **Step 1: Add regression tests** + +Add these regression tests to `front/tests/unit/host-notifications.test.tsx`: + +```typescript +it("keeps manual preview state when notification queries invalidate", async () => { + // Arrange with the existing manual notification route fixture. + // Preview a manual notification. + // Trigger an invalidation through a successful confirm or process action. + // Assert the preview token, selected template, and target count remain visible until confirm resolves. +}); + +it("requires explicit resend confirmation after query migration", async () => { + // Arrange with a recent manual dispatch fixture for the same session/template. + // Preview the same dispatch. + // Assert confirm is blocked until the resend confirmation control is selected. +}); + +it("refreshes manual dispatch ledger after confirm", async () => { + // Arrange with an empty dispatch ledger. + // Confirm a preview. + // Assert the ledger query refetch shows the new dispatch row. +}); +``` + +Replace the comments with the existing test helper calls in that file; keep the three test names and assertions. + +- [ ] **Step 2: Run checks** + +Run: + +```bash +pnpm --dir front test -- host-notifications +pnpm --dir front lint +pnpm --dir front build +``` + +Expected: all commands pass. diff --git a/docs/superpowers/specs/2026-05-17-readmates-engineering-proof-portfolio-design.md b/docs/superpowers/specs/2026-05-17-readmates-engineering-proof-portfolio-design.md new file mode 100644 index 00000000..edf06304 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-readmates-engineering-proof-portfolio-design.md @@ -0,0 +1,682 @@ +# ReadMates Engineering Proof Portfolio Design + +작성일: 2026-05-17 +상태: APPROVED DESIGN SPEC +문서 목적: ReadMates를 채용/포트폴리오 리뷰어, 미래 유지보수자, 오픈소스/기술 독자가 짧은 시간 안에 평가할 수 있는 "운영 가능한 풀스택 제품 증거물"로 고도화하는 분기급 설계를 정의한다. + +## 1. 배경 + +ReadMates는 이미 단순 CRUD 애플리케이션 범위를 넘어섰다. 현재 제품은 여러 정기 독서모임의 공개 사이트, 멤버 앱, 호스트 운영 도구, 플랫폼 관리자 콘솔, Google OAuth, Cloudflare Pages Functions BFF, Spring Boot API, MySQL/Flyway, optional Redis/Kafka, 알림 outbox, in-app AI 세션 기록 생성, 공개 릴리즈 safety scan, 운영 runbook을 함께 갖고 있다. + +최근 작업 흐름도 제품 신규 기능보다 운영형 플랫폼으로서의 완성도에 집중되어 있다. + +- in-app AI 세션 생성과 PII-safe 운영 경계 +- 플랫폼 관리자 triage 콘솔 +- release risk remediation +- Flyway collation 사고 후속 정리 +- public release candidate scan +- 디자인 시스템과 architecture boundary 강화 + +현재 강점은 많지만, 외부 리뷰어가 처음 README를 열었을 때 이 강점들이 하나의 주장으로 빠르게 연결되지는 않는다. 문서, case study, ADR, runbook, 테스트, CI, release checklist가 각각 존재하지만 "ReadMates는 어떤 어려운 문제를 어떤 근거로 해결했는가"라는 평가 흐름이 분산되어 있다. + +이번 분기 고도화는 새 기능을 크게 늘리는 프로젝트가 아니다. 핵심은 이미 존재하는 제품과 엔지니어링 자산을 외부 평가 가능한 증거 체계로 묶고, 그 증거를 믿게 만드는 유지보수 품질 작업을 병행하는 것이다. + +## 2. 목표 + +분기 목표는 다음 한 문장으로 고정한다. + +> ReadMates를 "운영 가능한 풀스택 제품을 설계, 출시, 개선할 수 있는 엔지니어링 증거물"로 만든다. + +구체 목표: + +- README에서 5분 안에 제품 문제, 역할 모델, 운영 난이도, 기술 선택, 핵심 증거를 이해할 수 있게 한다. +- 공개 guest-mode 경로를 리뷰어용 walkthrough로 정리하되, 공개 권한을 넓히지 않는다. +- BFF 보안, 알림 outbox, multi-club domain, PII-safe AI 세션 생성, release safety 같은 강점을 case study와 테스트 근거로 연결한다. +- 프론트 서버 상태 관리, 서버 UseCase/transaction 경계, architecture/quality gate를 분기 내 작은 PR 단위로 개선한다. +- release readiness, public release scan, deploy runbook, post-deploy watch, postmortem을 하나의 운영 증거 흐름으로 묶는다. +- 공개 저장소 안전 규칙을 유지한다. 실제 멤버 데이터, secret, private domain, deployment state, OCID, token-shaped example, local absolute path를 추가하지 않는다. + +## 3. 비목표 + +- 신규 대형 제품 기능을 만들지 않는다. +- 게스트 권한을 멤버/호스트/admin/AI workflow까지 넓히지 않는다. +- public bypass, demo auth, fake production admin entrypoint를 만들지 않는다. +- 실제 운영 멤버 데이터나 private 운영값을 데모에 사용하지 않는다. +- 분기 안에 전체 리팩터링 완료를 약속하지 않는다. +- 문서만 보기 좋게 만들고 코드/테스트 근거가 없는 showcase를 만들지 않는다. +- AI provider pricing, external platform limit, current model catalog 같은 변동성 높은 외부 사실을 새로 주장하지 않는다. 필요한 경우 공식 문서로 별도 검증하거나 "재검증 필요"로 표시한다. + +## 4. 주요 대상 + +우선순위: + +1. 채용/포트폴리오 리뷰어 +2. 미래 유지보수자 +3. 오픈소스/기술 독자 + +### 4.1 채용/포트폴리오 리뷰어 + +리뷰어가 확인하고 싶은 질문: + +- 제품이 실제 문제를 푸는가, 아니면 데모성 CRUD인가? +- 한 사람이 프론트, BFF, 백엔드, DB, 배포, 운영까지 연결해 설계할 수 있는가? +- 보안, 권한, 공개 저장소 안전, 장애 대응을 진지하게 다루는가? +- 복잡한 기능을 테스트와 문서로 유지보수 가능하게 만들었는가? + +이번 고도화는 이 질문에 README, walkthrough, case study, test evidence로 답한다. + +### 4.2 미래 유지보수자 + +유지보수자가 확인하고 싶은 질문: + +- 현재 동작의 source of truth는 어디인가? +- 변경할 때 어느 guide를 읽어야 하는가? +- frontend/server/doc boundary는 어떻게 나뉘는가? +- 어떤 테스트가 어떤 회귀를 막는가? +- release risk와 public safety는 어떻게 검증하는가? + +이번 고도화는 문서 구조와 implementation backlog를 통해 유지보수자가 첫 변경을 안전하게 시작하도록 만든다. + +### 4.3 오픈소스/기술 독자 + +기술 독자가 확인하고 싶은 질문: + +- ReadMates에서 배울 수 있는 비자명한 기술 문제는 무엇인가? +- 왜 BFF, outbox, multi-club domain, AI audit/cost guard 같은 선택을 했는가? +- 설계가 코드와 테스트로 이어지는가? + +이번 고도화는 case study와 architecture evidence를 README에서 자연스럽게 발견하게 한다. + +## 5. 설계 원칙 + +### 5.1 Evidence Over Claims + +문서에서 말하는 강점은 코드, 테스트, script, runbook, ADR, case study 중 하나 이상의 근거로 이어져야 한다. + +예: + +- "BFF 보안 경계가 있다"는 주장은 ADR, BFF proxy code, BFF tests, security-public-repo 문서로 이어져야 한다. +- "AI 세션 생성은 PII-safe하게 운영된다"는 주장은 AI runbook, audit/cost guard, PII check script, 관련 tests로 이어져야 한다. +- "릴리즈 안전장치가 있다"는 주장은 build-public-release-candidate script, public-release-check script, release-readiness-review 문서로 이어져야 한다. + +### 5.2 Guest Access Is Not Demo Auth + +기존 guest-mode는 공개 클럽 소개, 공개 기록, 공개 세션 상세를 보여주는 제품 권한 모델이다. 이번 고도화는 이를 리뷰어용 관람 동선으로 정리할 뿐, 접근 권한을 넓히지 않는다. + +게스트가 볼 수 없는 멤버/호스트/platform admin/AI/알림 흐름은 다음 방식으로 설명한다. + +- public-safe walkthrough 문서 +- sanitized screenshot 또는 텍스트 캡처 +- fixture 기반 설명 +- case study +- 테스트와 runbook 근거 + +### 5.3 Code Keeps the Promise + +문서 개편은 코드 품질 작업과 분리되지 않는다. 문서에서 "유지보수 가능하다"고 주장하려면 실제 boundary test, architecture test, lint/build/test, query migration 상태, transaction policy가 함께 정리되어야 한다. + +### 5.4 Small Reviewable PRs + +분기 로드맵은 큰 rewrite가 아니라 reviewer가 이해할 수 있는 작은 PR 단위로 나눈다. + +좋은 단위: + +- README entry flow 재정리 +- guest-mode showcase 문서 추가 +- claim-to-evidence map 추가 +- `host/members` TanStack Query migration +- 서버 transaction boundary 정책 문서화 +- 특정 service의 UseCase 분리 + +나쁜 단위: + +- README와 architecture와 server 리팩터링과 UI 개편을 한 PR에 섞기 +- 모든 frontend server state를 한 번에 migration +- 모든 server transaction annotation을 한 번에 이동 + +### 5.5 Public Repo Safety by Default + +모든 문서와 fixture는 공개 저장소를 기준으로 작성한다. + +금지: + +- 실제 member 이름, 이메일, 메시지, 기록 +- private domain +- real deployment state +- OCI OCID +- secret/token/API key 형태의 문자열 +- local absolute path +- 운영 DB dump나 raw logs + +허용: + +- repo-relative path +- placeholder host (`https://api.example.com`, `host@example.com`) +- synthetic club/member/session fixture +- public fallback domain already documented by the project + +## 6. 분기 로드맵 + +분기는 4개 milestone로 나눈다. 각 milestone은 "보여줄 결과물"과 "그 결과물을 믿게 만드는 코드/검증"을 함께 가진다. + +### 6.1 Milestone 1: Portfolio Entry + +목표: 리뷰어가 README에서 5분 안에 ReadMates의 문제, 제품 표면, 운영 난이도, 기술 선택을 이해한다. + +주요 결과물: + +- README entry narrative 개편 +- case study index 정리 +- architecture evidence map 초안 +- "무엇을 먼저 보면 되는가" section + +범위: + +- README는 entrypoint로 유지하고 source of truth를 대체하지 않는다. +- architecture 상세는 `docs/development/architecture.md`로 link한다. +- deployment 상세는 `docs/deploy/`와 runbook으로 link한다. +- release safety는 script 문서와 release-readiness 문서로 link한다. + +성공 기준: + +- README 상단 1/3 안에서 제품 문제, 대상 역할, 핵심 기술 증거, guest-mode link가 보인다. +- README의 강점 항목은 최소 하나 이상의 evidence link를 가진다. +- 공개 저장소 안전 규칙을 새 문구가 깨지 않는다. + +### 6.2 Milestone 2: Engineering Confidence + +목표: "이 프로젝트는 커졌지만 무너지지 않게 관리되고 있다"는 근거를 만든다. + +주요 결과물: + +- frontend server-state migration 분기 계획 +- `host/members`, `host/sessions`, `host/notifications` 중 2~3개 Query migration 또는 상세 계획화 +- server UseCase/transaction boundary 후보 1~2개 정리 +- architecture/quality gate evidence 문서 정리 + +프론트 후보: + +1. `host/members` + - 운영 빈도가 높고 state mutation이 명확하다. + - route-owned data coordination과 TanStack Query invalidation 패턴을 보여주기 좋다. +2. `host/notifications` + - manual dispatch preview/confirm, dispatch ledger, recipient state가 있어 운영형 UX 근거가 강하다. + - E2E 영향이 있을 수 있어 작은 단위로 접근한다. +3. `host/sessions` + - session lifecycle, visibility, current/upcoming state와 연결되어 제품 의미가 크다. + - migration 범위가 넓을 수 있으므로 slice를 나누어야 한다. + +서버 후보: + +1. Host session command UseCase split + - session draft mutation, lifecycle transition, attendance confirmation, publication update, dashboard query 책임을 더 좁힌다. + - 기존 clean architecture 방향과 맞는다. +2. Transaction boundary policy + - adapter-level `@Transactional`과 application-service transaction owner 정책을 문서화하고, 작은 slice부터 정리한다. +3. Auth package service location cleanup + - `auth/application` 직속 service를 `auth/application/service`로 이동해 package convention을 맞추는 후보. + +성공 기준: + +- migration/cleanup은 "어떤 회귀를 줄이는가"가 문서에 적혀야 한다. +- 각 작업은 관련 guide와 최소 검증 명령을 명시한다. +- route-first/frontend boundary와 server clean architecture boundary를 약화하지 않는다. + +### 6.3 Milestone 3: Operational Proof + +목표: release, deploy, observability, incident response가 분리된 문서가 아니라 하나의 운영 증거 흐름으로 읽힌다. + +주요 결과물: + +- release evidence flow 문서 +- public release candidate scan 설명 정리 +- post-deploy watch/runbook link 정리 +- incident/postmortem index 정리 +- Flyway collation 사고와 AI 운영 리스크 대응을 public-safe learning으로 재구성 + +핵심 흐름: + +```text + +Change + -> local checks + -> release readiness review + -> public release candidate build/check + -> tag/release process + -> deploy runbook + -> smoke/post-deploy watch + -> incident/postmortem if needed +``` + +성공 기준: + +- README 또는 showcase에서 release safety 근거로 진입할 수 있다. +- release-readiness checklist는 "tests passed"만으로 충분하다고 표현하지 않는다. +- public safety scan과 deploy runbook 사이의 역할이 분명하다. +- 운영 학습 사례는 private 운영값 없이 재현 가능한 교훈 중심으로 설명된다. + +### 6.4 Milestone 4: Guest-Mode Showcase & Evidence Path + +목표: 이미 존재하는 guest-mode 공개 경로를 리뷰어가 의도대로 따라가며 제품 역량을 이해하도록 만든다. + +이 milestone은 새 guest 기능이 아니다. 현재 public routes와 guest behavior를 문서화하고, private workflow는 evidence로 보완한다. + +주요 결과물: + +- guest-mode walkthrough 문서 +- public-safe demo club/session narrative +- private workflow evidence section +- screenshot policy 또는 screenshot inventory +- README에서 guest-mode showcase로 가는 entry link + +권한 원칙: + +- 게스트는 공개 클럽 소개, 공개 기록, 공개 세션 상세까지만 본다. +- 멤버/호스트/platform admin/AI/알림 private workflow는 공개 접근을 열지 않는다. +- private workflow는 sanitized screenshot, fixture explanation, case study, test evidence로 설명한다. + +성공 기준: + +- 리뷰어가 로그인 없이 공개 제품 표면을 따라갈 수 있다. +- 리뷰어가 로그인 없이 볼 수 없는 기능도 "어떤 기능이고 어떤 근거로 검증되는지" 이해할 수 있다. +- demo path가 실제 멤버 데이터나 운영 secret을 요구하지 않는다. + +## 7. Evidence Graph + +중심 진입점은 README다. README는 모든 설명을 품지 않고, 평가 흐름을 다음 그래프로 안내한다. + +```text + +README + -> Guest-Mode Showcase + -> Architecture Evidence + -> Case Studies + -> Engineering Confidence + -> Operational Proof + -> Release Safety +``` + +### 7.1 Claim-to-Evidence Map + +| Claim | Primary Evidence | Secondary Evidence | Verification | +| --- | --- | --- | --- | +| Cloudflare BFF가 browser-facing security boundary다 | `docs/development/adr/0001-cloudflare-pages-functions-bff.md`, `docs/development/architecture.md` | BFF proxy tests, security docs | frontend/BFF tests, public release check | +| Multi-club context가 slug/host 기반으로 안전하게 resolve된다 | `docs/case-studies/03-multi-club-domain-platform.md`, architecture docs | domain deploy runbook, host header ADR | server tests, BFF tests | +| 알림 발송은 mutation transaction과 분리되어 운영된다 | `docs/case-studies/02-notification-pipeline-with-outbox.md` | Flyway schema, notification runbook | server tests, outbox/consumer tests | +| AI 세션 생성은 PII-safe 운영 경계를 가진다 | AI runbook, AI design spec, case study | audit/cost guard tests, PII check script | `scripts/aigen-pii-check.sh`, targeted AI tests | +| 공개 저장소 안전이 자동화되어 있다 | `docs/deploy/security-public-repo.md`, `scripts/README.md` | release readiness docs | public release candidate build/check | +| 유지보수 경계가 테스트로 강제된다 | architecture docs, frontend/server agent guides | ArchUnit, frontend boundary tests | `architectureTest`, frontend unit tests | +| 운영 사고를 학습으로 남긴다 | postmortem docs, changelog release notes | release-risk remediation plans | release readiness review | + +이 표는 milestone 1에서 별도 문서 또는 README section으로 정리한다. 각 row는 실제 존재하는 파일 링크만 포함해야 하며, 없는 evidence를 만들었다고 쓰지 않는다. + +## 8. Proposed Document Structure + +### 8.1 README 개편 방향 + +README는 다음 순서를 목표로 한다. + +1. 제품 한 줄 설명 +2. guest-mode로 볼 수 있는 것 +3. 왜 이 프로젝트가 단순 CRUD가 아닌가 +4. 역할별 제품 표면 +5. 핵심 engineering evidence 4~5개 +6. architecture overview +7. how to review this project +8. local setup/checks links +9. source-of-truth links + +README가 피해야 할 것: + +- 모든 runbook 상세를 README에 붙이는 것 +- 이미 architecture 문서가 책임지는 상세 정책을 중복 서술하는 것 +- "최신", "완전", "무결" 같은 검증하기 어려운 표현 +- 실제 운영값이나 private path 노출 + +### 8.2 Guest-Mode Showcase 문서 + +권장 위치: + +- `docs/showcase/guest-mode-walkthrough.md` + +권장 구조: + +1. 목적 +2. 리뷰어가 로그인 없이 볼 수 있는 화면 +3. 공개 클럽 소개 보기 +4. 공개 기록 보기 +5. 공개 세션 상세 보기 +6. 게스트가 볼 수 없는 private workflow +7. private workflow를 확인하는 evidence links +8. public-safety notes + +이 문서는 product walkthrough 문서다. 실제 source of truth는 route code와 architecture 문서다. + +### 8.3 Architecture Evidence 문서 + +권장 위치: + +- `docs/showcase/architecture-evidence.md` + +권장 구조: + +1. One-page architecture map +2. Browser/BFF/Spring/MySQL request path +3. Club context and role boundary +4. Async notification path +5. AI generation path +6. Release/operations path +7. Tests that enforce boundaries + +주의: + +- `docs/development/architecture.md`를 대체하지 않는다. +- 깊은 구현 상세 대신 "왜 이 구조가 운영 제품에 필요한가"를 설명한다. + +### 8.4 Engineering Confidence 문서 + +권장 위치: + +- `docs/showcase/engineering-confidence.md` + +권장 구조: + +1. Boundary tests +2. Server architecture tests +3. Query budget/migration tests +4. Frontend server-state migration state +5. Static analysis/coverage gates +6. Known improvement backlog +7. How to validate a change + +### 8.5 Operational Proof 문서 + +권장 위치: + +- `docs/showcase/operational-proof.md` + +권장 구조: + +1. Release evidence flow +2. Public release candidate checks +3. Deployment runbooks +4. Observability and request correlation +5. Incident/postmortem practice +6. Rollback and residual risk review + +## 9. Technical Improvement Tracks + +### 9.1 Frontend Server-State Track + +현재 상태: + +- TanStack Query v5 provider가 app root에 있다. +- `host/invitations` migration이 완료되어 있다. +- 후속 후보는 `host/members`, `host/sessions`, `host/notifications`, `current-session`, read-heavy public/archive/feedback이다. + +분기 권장 순서: + +1. `host/members` + - 멤버 목록, 상태 변경, display name 변경, viewer/member lifecycle가 있다. + - mutation invalidation 패턴을 보여주기 좋다. +2. `host/notifications` + - manual dispatch options/preview/confirm/ledger가 있어 운영 UX를 보여준다. + - preview TTL, resend confirmation, selected session state 때문에 route-owned coordination이 중요하다. +3. `host/sessions` + - session editor, lifecycle, AI generation, JSON import와 결합되어 있어 크다. + - 한 번에 전환하지 말고 list/read path와 mutation path를 나눠야 한다. + +구현 원칙: + +- 새 server state는 `features//queries/-queries.ts`에 둔다. +- query key는 const tuple factory로 관리한다. +- route loader는 initial data를 query cache에 handoff한다. +- UI component는 query hook을 직접 호출하지 않는다. route 또는 feature container가 props/callback으로 전달한다. +- mutation success는 targeted invalidation을 수행한다. + +검증: + +- `pnpm --dir front lint` +- `pnpm --dir front test` +- `pnpm --dir front build` +- route/auth/BFF/user-flow 영향 시 `pnpm --dir front test:e2e` + +### 9.2 Server Boundary Track + +현재 상태: + +- feature-local clean architecture가 다수 slice에 적용되어 있다. +- ArchUnit boundary test가 application/adapter/domain 의존성을 강제한다. +- CQRS read/write package split convention이 문서화되어 있다. +- 일부 legacy service 책임과 transaction annotation 위치는 후속 정리가 필요하다. + +분기 권장 순서: + +1. Transaction policy documentation + - application service가 transaction owner가 되는 기준 + - adapter-level transaction이 허용되는 예외 + - scheduler/Kafka listener transaction boundary + - MySQL isolation expectation +2. Narrow service split candidate + - Host session command service 책임을 draft/lifecycle/read/attendance/publication으로 나눌 수 있는지 검토 + - 단일 PR에서 interface split만 먼저 수행 가능 +3. Package convention cleanup + - auth service 위치 정리 후보 + - architecture test를 나중에 좁게 추가 + +검증: + +- `./server/gradlew -p server unitTest` +- 변경 표면에 따라 `./server/gradlew -p server integrationTest` +- architecture boundary 변경 시 `./server/gradlew -p server architectureTest` +- PR-level confidence가 필요하면 `./server/gradlew -p server check` + +### 9.3 Release Safety Track + +현재 상태: + +- public release candidate build/check script가 있다. +- release-readiness-review 문서가 있다. +- CHANGELOG가 release evidence를 담고 있다. +- public repo safety를 위한 docs/deploy 문서가 있다. + +분기 권장 작업: + +1. release evidence flow 문서 작성 +2. README에서 release safety를 evidence로 link +3. recent incident learning index 정리 +4. public release check가 무엇을 보장하고 무엇을 보장하지 않는지 명시 + +검증: + +- docs-only change: `git diff --check -- ` +- public/release/deploy docs change: + - `./scripts/build-public-release-candidate.sh` + - `./scripts/public-release-check.sh .tmp/public-release-candidate` + +## 10. Error Handling and Risk Management + +이번 고도화는 기능 runtime error path를 새로 추가하지 않는다. 대신 문서와 계획의 오류를 다음 방식으로 관리한다. + +### 10.1 Stale Claim Risk + +위험: README/showcase가 현재 코드보다 앞서가거나, 이미 바뀐 동작을 오래된 상태로 설명한다. + +대응: + +- 문서 claim은 current code, config, tests, scripts, architecture 문서와 대조한다. +- historical `docs/superpowers` 문서는 현재 동작의 source of truth로 쓰지 않는다. +- 불확실한 내용은 "재확인 필요"로 표시하거나 제외한다. + +### 10.2 Public Safety Risk + +위험: showcase 과정에서 실제 운영값이나 private data를 노출한다. + +대응: + +- fixture와 screenshot은 synthetic 또는 sanitized만 사용한다. +- private workflow는 접근을 열지 않고 evidence로 설명한다. +- public release check와 targeted safety scan을 실행한다. + +### 10.3 Scope Creep Risk + +위험: 포트폴리오 고도화가 대형 제품 개발이나 전면 리팩터링으로 변한다. + +대응: + +- milestone마다 "보여줄 결과물"과 "검증"을 먼저 정의한다. +- technical improvement는 작은 PR 단위로 제한한다. +- 새 기능보다 evidence path를 우선한다. + +### 10.4 Reviewer Confusion Risk + +위험: 문서가 많아져 리뷰어가 무엇을 봐야 할지 더 혼란스러워진다. + +대응: + +- README에 "How to review this project" section을 둔다. +- showcase 문서는 3~4개로 제한한다. +- 각 문서의 첫 section에 "이 문서가 답하는 질문"을 둔다. + +## 11. Testing and Verification Strategy + +### 11.1 Docs-only Verification + +모든 문서 변경: + +```bash +git diff --check -- +``` + +문서가 README, deploy, release, public safety를 건드릴 때: + +```bash +./scripts/build-public-release-candidate.sh +./scripts/public-release-check.sh .tmp/public-release-candidate +``` + +### 11.2 Frontend Verification + +frontend route/state/UI 작업: + +```bash +pnpm --dir front lint +pnpm --dir front test +pnpm --dir front build +``` + +route/auth/BFF/user-flow 변경: + +```bash +pnpm --dir front test:e2e +``` + +### 11.3 Server Verification + +server application/API/persistence 작업: + +```bash +./server/gradlew -p server clean test +``` + +boundary/static analysis/coverage 영향: + +```bash +./server/gradlew -p server check +./server/gradlew -p server architectureTest +``` + +개발 중 fast lane: + +```bash +./server/gradlew -p server unitTest +./server/gradlew -p server integrationTest +``` + +### 11.4 Evidence Verification + +claim-to-evidence map을 작성한 뒤 다음을 점검한다. + +- 모든 claim에 실제 파일 링크가 있는가? +- 링크된 파일이 current source of truth인가, historical plan인가? +- historical plan을 evidence로 쓸 경우 "history/context"로 명확히 표시했는가? +- claim이 code/test/script보다 과장되어 있지 않은가? +- public-safety rule을 새 문구가 깨지 않는가? + +## 12. Milestone Acceptance Criteria + +### Milestone 1 Acceptance + +- README가 entrypoint로 재정리되어 있다. +- "How to review this project" 흐름이 있다. +- 핵심 engineering evidence가 case study/test/runbook으로 연결된다. +- `git diff --check`와 public safety scan이 실행되었거나, 실행하지 못한 이유가 기록된다. + +### Milestone 2 Acceptance + +- frontend server-state migration 후보가 최신 상태로 정리되어 있다. +- 최소 1개 이상의 migration/cleanup PR이 merged 가능 단위로 계획되거나 완료되어 있다. +- server transaction/UseCase boundary 후보가 구체 파일/검증 단위로 좁혀져 있다. +- 변경된 코드 표면의 lint/test/build 또는 server test가 실행된다. + +### Milestone 3 Acceptance + +- release evidence flow 문서가 있다. +- release readiness, public release candidate scan, deploy runbook, post-deploy watch가 연결된다. +- 운영 학습 사례가 public-safe하게 요약되어 있다. +- deploy/release docs 변경 시 public release candidate checks가 실행된다. + +### Milestone 4 Acceptance + +- guest-mode walkthrough가 있다. +- 공개 접근 범위와 private workflow evidence가 구분되어 있다. +- private workflow를 보기 위해 demo auth나 public bypass가 필요하지 않다. +- screenshot/fixture가 public-safe하다. + +## 13. Implementation Planning Notes + +상세 구현 계획은 이 스펙 승인 후 별도 implementation plan으로 작성한다. 구현 계획은 다음 단위로 나누는 것이 적절하다. + +1. Documentation/showcase PRs + - README entry flow + - guest-mode walkthrough + - architecture evidence + - engineering confidence + - operational proof +2. Frontend confidence PRs + - `host/members` Query migration + - `host/notifications` Query migration 또는 detailed plan + - related tests +3. Server confidence PRs + - transaction policy doc + - one narrow UseCase split candidate + - related tests/architecture checks +4. Release safety PRs + - release evidence flow + - public scan wording and docs cross-links + - postmortem/incident learning index + +각 implementation task는 다음을 반드시 포함한다. + +- touched surface +- source files +- non-goals +- public safety constraints +- exact validation commands +- rollback or residual risk note + +## 14. Open Questions Resolved in Brainstorming + +- 우선순위는 D/C/B 중 D를 주축으로 하고 C, B를 보조한다. +- 리뷰 대상 우선순위는 채용/포트폴리오 리뷰어, 미래 유지보수자, 오픈소스/기술 독자 순이다. +- 시간 단위는 분기급 master plan이다. +- 접근 방식은 Engineering Proof Portfolio다. +- Milestone 4는 새 guest 기능이 아니라 기존 guest-mode를 리뷰어용 evidence path로 정리하는 것이다. + +## 15. Final Design Summary + +ReadMates는 이미 제품 기능과 운영 장치가 많다. 이번 분기 고도화의 핵심은 더 많은 기능을 붙이는 것이 아니라, 제품과 코드와 운영 기록을 하나의 평가 가능한 증거 흐름으로 연결하는 것이다. + +리뷰어는 README에서 시작해 guest-mode 공개 화면을 보고, private workflow는 sanitized evidence로 이해하고, architecture/case study/test/runbook을 통해 엔지니어링 주장을 검증한다. 유지보수자는 같은 문서 흐름에서 source of truth와 검증 명령을 찾는다. + +이 설계가 성공하면 ReadMates는 "많이 만든 프로젝트"가 아니라 "운영 가능한 제품을 설계하고 지속적으로 안전하게 개선한 프로젝트"로 읽힌다. diff --git a/front/features/host/aigen/ui/AiGenerateTab.test.tsx b/front/features/host/aigen/ui/AiGenerateTab.test.tsx index 9af2b15d..112b5ca5 100644 --- a/front/features/host/aigen/ui/AiGenerateTab.test.tsx +++ b/front/features/host/aigen/ui/AiGenerateTab.test.tsx @@ -369,6 +369,21 @@ describe("AiGenerateTab", () => { }); }); + it("shows an unavailable state when club AI defaults cannot be loaded", async () => { + mockedClubDefault.mockReset(); + mockedClubDefault.mockRejectedValueOnce(new Error("AI generation is disabled")); + + const { Wrapper } = createWrapper(); + render( + + {}} /> + , + ); + + expect(await screen.findByRole("status")).toHaveTextContent("AI 생성을 사용할 수 없습니다"); + expect(screen.queryByRole("button", { name: "생성 시작" })).not.toBeInTheDocument(); + }); + it("returns ERROR → IDLE when retry is clicked", async () => { mockedStart.mockResolvedValue({ jobId: "job-1", diff --git a/front/features/host/aigen/ui/AiGenerateTab.tsx b/front/features/host/aigen/ui/AiGenerateTab.tsx index a0684bee..3761e16e 100644 --- a/front/features/host/aigen/ui/AiGenerateTab.tsx +++ b/front/features/host/aigen/ui/AiGenerateTab.tsx @@ -210,6 +210,19 @@ export function AiGenerateTab({ sessionId, clubSlug, onCommitted }: AiGenerateTa ); } + if (clubDefaultsQuery.isError) { + return ( +
+

+ AI 생성을 사용할 수 없습니다. 외부 JSON 가져오기로 세션 기록을 저장할 수 있습니다. +

+

+ 모델 설정, provider 상태, 비용 한도, 운영 kill switch를 확인하세요. +

+
+ ); + } + return (
- {/* TODO(task_3_4): wire to real cost/cap data once the endpoint exists. */} -
-
예상 비용
-
- USD
- -
남은 한도
-
-
-
- diff --git a/front/features/host/api/host-api.ts b/front/features/host/api/host-api.ts index 32c9f39f..96af4e46 100644 --- a/front/features/host/api/host-api.ts +++ b/front/features/host/api/host-api.ts @@ -3,7 +3,6 @@ import type { CreatedSessionResponse, CreateHostInvitationRequest, CurrentSessionResponse, - FeedbackDocumentResponse, HostAttendanceUpdate, HostDashboardResponse, HostInvitationListPage, @@ -269,13 +268,6 @@ export function publishHostSession(sessionId: string) { }) as Promise }>; } -export function uploadHostSessionFeedbackDocument(sessionId: string, formData: FormData) { - return readmatesFetchResponse(`/api/host/sessions/${encodeURIComponent(sessionId)}/feedback-document`, { - method: "POST", - body: formData, - }) as Promise }>; -} - export function previewHostSessionImport(sessionId: string, request: SessionImportRequest) { return readmatesFetch( `/api/host/sessions/${encodeURIComponent(sessionId)}/session-import/preview`, diff --git a/front/features/host/index.ts b/front/features/host/index.ts index 22d1f740..d54e2e31 100644 --- a/front/features/host/index.ts +++ b/front/features/host/index.ts @@ -39,7 +39,7 @@ export { } from "@/features/host/route/host-members-route"; export { hostMembersActions, - hostMembersLoader, + hostMembersLoaderFactory, } from "@/features/host/route/host-members-data"; export { HostInvitationsRoute, diff --git a/front/features/host/model/host-dashboard-model.ts b/front/features/host/model/host-dashboard-model.ts index ca46f0c3..841a3b5d 100644 --- a/front/features/host/model/host-dashboard-model.ts +++ b/front/features/host/model/host-dashboard-model.ts @@ -187,7 +187,7 @@ export function getHostDashboardPublicationFeedbackRows( { label: "피드백 문서", value: feedbackPending > 0 ? `${feedbackPending}개 대기` : "대기 없음", - helper: feedbackPending > 0 ? "회차 피드백 문서 업로드가 필요합니다." : "문서 등록 대기 중인 이전 세션이 없습니다.", + helper: feedbackPending > 0 ? "회차 세션 기록 패키지 저장이 필요합니다." : "문서 등록 대기 중인 이전 세션이 없습니다.", tone: feedbackPending > 0 ? "warn" : "ok", }, ]; diff --git a/front/features/host/queries/host-members-queries.ts b/front/features/host/queries/host-members-queries.ts new file mode 100644 index 00000000..923e364f --- /dev/null +++ b/front/features/host/queries/host-members-queries.ts @@ -0,0 +1,85 @@ +import type { QueryClient } from "@tanstack/react-query"; +import { queryOptions, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + fetchHostMembers, + submitHostMemberLifecycle, + submitHostMemberProfile, + submitHostViewerAction, +} from "@/features/host/api/host-api"; +import type { + HostMemberListPage, + MemberLifecycleRequest, +} from "@/features/host/api/host-contracts"; +import type { + HostMemberLifecyclePath, + HostViewerAction, +} from "@/features/host/route/host-members-actions"; +import type { ReadmatesApiContext } from "@/shared/api/client"; +import type { PageRequest } from "@/shared/model/paging"; + +export const hostMemberKeys = { + all: ["host", "members"] as const, + list: (page?: PageRequest) => [...hostMemberKeys.all, "list", page ?? {}] as const, +} as const; + +async function fetchHostMemberList( + context?: ReadmatesApiContext, + page?: PageRequest, +): Promise { + return fetchHostMembers(context, page); +} + +export function hostMemberListQuery(page?: PageRequest, context?: ReadmatesApiContext) { + return queryOptions({ + queryKey: hostMemberKeys.list(page), + queryFn: () => fetchHostMemberList(context, page), + }); +} + +export function invalidateHostMembers(client: QueryClient) { + return client.invalidateQueries({ queryKey: hostMemberKeys.all }); +} + +export function useHostMemberLifecycleMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + path, + body, + }: { + membershipId: string; + path: HostMemberLifecyclePath; + body?: MemberLifecycleRequest; + }) => submitHostMemberLifecycle(membershipId, path, body), + onSuccess: () => invalidateHostMembers(client), + }); +} + +export function useHostMemberProfileMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + displayName, + }: { + membershipId: string; + displayName: string; + }) => submitHostMemberProfile(membershipId, displayName), + onSuccess: () => invalidateHostMembers(client), + }); +} + +export function useHostViewerActionMutation() { + const client = useQueryClient(); + return useMutation({ + mutationFn: async ({ + membershipId, + action, + }: { + membershipId: string; + action: HostViewerAction; + }) => submitHostViewerAction(membershipId, action), + onSuccess: () => invalidateHostMembers(client), + }); +} diff --git a/front/features/host/route/host-members-data.ts b/front/features/host/route/host-members-data.ts index db29d76a..fe946883 100644 --- a/front/features/host/route/host-members-data.ts +++ b/front/features/host/route/host-members-data.ts @@ -1,18 +1,40 @@ +import type { QueryClient } from "@tanstack/react-query"; +import type { LoaderFunctionArgs } from "react-router-dom"; import { fetchHostMembers, submitHostMemberLifecycle, submitHostMemberProfile, submitHostViewerAction, } from "@/features/host/api/host-api"; +import type { HostMemberListItem, HostMemberListPage } from "@/features/host/api/host-contracts"; +import { hostMemberListQuery } from "@/features/host/queries/host-members-queries"; import type { HostMembersActions } from "@/features/host/route/host-members-actions"; -import type { LoaderFunctionArgs } from "react-router-dom"; -import { requireHostLoaderAuth } from "./host-loader-auth"; import { clubSlugFromLoaderArgs } from "@/shared/auth/member-app-loader"; +import { requireHostLoaderAuth } from "./host-loader-auth"; + +const HOST_MEMBERS_PAGE_LIMIT = 50; + +function normalizeMemberPage(value: HostMemberListPage | HostMemberListItem[]): HostMemberListPage { + return Array.isArray(value) ? { items: value, nextCursor: null } : value; +} + +export function hostMembersLoaderFactory(client: QueryClient) { + return async (args?: LoaderFunctionArgs) => { + await requireHostLoaderAuth(args); + + const raw = await fetchHostMembers( + { clubSlug: clubSlugFromLoaderArgs(args) }, + { limit: HOST_MEMBERS_PAGE_LIMIT }, + ); + const page = normalizeMemberPage(raw); -export async function hostMembersLoader(args?: LoaderFunctionArgs) { - await requireHostLoaderAuth(args); + client.setQueryData( + hostMemberListQuery({ limit: HOST_MEMBERS_PAGE_LIMIT }).queryKey, + page, + ); - return fetchHostMembers({ clubSlug: clubSlugFromLoaderArgs(args) }); + return page; + }; } export const hostMembersActions = { diff --git a/front/features/host/route/host-session-editor-actions.ts b/front/features/host/route/host-session-editor-actions.ts index 70731616..b1be7ec0 100644 --- a/front/features/host/route/host-session-editor-actions.ts +++ b/front/features/host/route/host-session-editor-actions.ts @@ -1,6 +1,5 @@ import type { AttendanceStatus, - FeedbackDocumentResponse, HostSessionDeletionPreviewResponse, HostSessionDetailResponse, SessionImportCommitResponse, @@ -25,7 +24,6 @@ export type HostSessionEditorActions = { sessionId: string, attendance: Array<{ membershipId: string; attendanceStatus: AttendanceStatus }>, ) => Promise; - uploadFeedbackDocument: (sessionId: string, formData: FormData) => Promise>; previewSessionImport: (sessionId: string, request: SessionImportRequest) => Promise; commitSessionImport: (sessionId: string, request: SessionImportRequest) => Promise; }; diff --git a/front/features/host/route/host-session-editor-data.ts b/front/features/host/route/host-session-editor-data.ts index 621197ed..306f7787 100644 --- a/front/features/host/route/host-session-editor-data.ts +++ b/front/features/host/route/host-session-editor-data.ts @@ -12,7 +12,6 @@ import { saveHostSessionAttendance, saveHostSessionPublication, updateHostSession, - uploadHostSessionFeedbackDocument, } from "@/features/host/api/host-api"; import type { HostSessionEditorActions } from "@/features/host/route/host-session-editor-actions"; import { requireHostLoaderAuth } from "./host-loader-auth"; @@ -44,7 +43,6 @@ export const hostSessionEditorActions = { sessionId === null ? createHostSession(request) : updateHostSession(sessionId, request), savePublication: saveHostSessionPublication, updateAttendance: saveHostSessionAttendance, - uploadFeedbackDocument: uploadHostSessionFeedbackDocument, previewSessionImport: previewHostSessionImport, commitSessionImport: commitHostSessionImport, } satisfies HostSessionEditorActions; diff --git a/front/features/host/ui/host-members.tsx b/front/features/host/ui/host-members.tsx index 8a0d8d60..0ec0d307 100644 --- a/front/features/host/ui/host-members.tsx +++ b/front/features/host/ui/host-members.tsx @@ -1,5 +1,6 @@ import { type CSSProperties, useMemo, useRef, useState } from "react"; import { useInRouterContext, useLocation } from "react-router-dom"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { CurrentSessionPolicy, HostMemberProfileErrorCode, @@ -10,6 +11,10 @@ import type { MemberLifecycleResponse, ViewerMember, } from "@/features/host/model/host-view-types"; +import { + hostMemberKeys, + hostMemberListQuery, +} from "@/features/host/queries/host-members-queries"; import type { PageRequest } from "@/shared/model/paging"; import { scopedAppLinkTarget } from "@/shared/routing/scoped-app-link-target"; import { LifecyclePolicyDialog } from "./members/member-approval-actions"; @@ -100,7 +105,31 @@ async function hostProfileErrorCodeFromResponse(response: Response): Promise normalizeMemberPage(await actions.loadMembers({ limit: 50 })), + initialData: propPage, + }); + // Track the prop and query page identities we have already consumed. When + // either changes identity we move the source-of-truth forward. + const queryPage = listQuery.data ?? propPage; + const [seen, setSeen] = useState<{ prop: HostMemberListItem[]; query: HostMemberListItem[]; active: HostMemberListPage }>(() => ({ + prop: propPage.items, + query: queryPage.items, + active: queryPage, + })); + let nextSeen = seen; + if (propPage.items !== seen.prop) { + nextSeen = { prop: propPage.items, query: queryPage.items, active: propPage }; + } else if (queryPage.items !== seen.query) { + nextSeen = { prop: propPage.items, query: queryPage.items, active: queryPage }; + } + if (nextSeen !== seen) { + setSeen(nextSeen); + } + const initialPage = nextSeen.active; const [memberRowsState, setMemberRowsState] = useState(() => ({ source: initialPage.items, members: initialPage.items, @@ -117,7 +146,6 @@ export default function HostMembers({ initialMembers, actions, LinkComponent = D const [message, setMessage] = useState(null); const pendingActionsRef = useRef>(new Set()); const dialogTriggerRef = useRef(null); - const refreshRequestIdRef = useRef(0); const setMembers = (update: MemberRowsUpdate) => { setMemberRowsState((current) => { @@ -187,20 +215,10 @@ export default function HostMembers({ initialMembers, actions, LinkComponent = D }; const refreshMembers = async () => { - const requestId = refreshRequestIdRef.current + 1; - refreshRequestIdRef.current = requestId; - - try { - const nextPage = normalizeMemberPage(await actions.loadMembers({ limit: 50 })); - if (requestId === refreshRequestIdRef.current) { - setMembers(nextPage.items); - setNextCursor(nextPage.nextCursor); - } - } catch (error) { - if (requestId === refreshRequestIdRef.current) { - throw error; - } - } + await queryClient.invalidateQueries( + { queryKey: hostMemberKeys.all }, + { throwOnError: true }, + ); }; const loadMoreMembers = async () => { diff --git a/front/features/host/ui/host-session-editor.tsx b/front/features/host/ui/host-session-editor.tsx index c10ace69..99fdd1f7 100644 --- a/front/features/host/ui/host-session-editor.tsx +++ b/front/features/host/ui/host-session-editor.tsx @@ -9,10 +9,8 @@ import { useRef, useState, } from "react"; -import { AiGenerateTab } from "@/features/host/aigen/ui/AiGenerateTab"; import type { AttendanceStatus, - FeedbackDocumentResponse, HostSessionDeletionPreviewResponse, HostSessionDetailResponse, ManualNotificationDispatchListItem, @@ -36,7 +34,6 @@ import { readmatesReturnState as defaultReadmatesReturnState } from "@/shared/ro import type { ReadmatesReturnState, ReadmatesReturnTarget } from "@/shared/routing/readmates-route-state"; import { scopedAppLinkTarget } from "@/shared/routing/scoped-app-link-target"; import { HostSessionDeletionPreviewDialog } from "./host-session-deletion-preview"; -import { HostSessionFeedbackUpload } from "./host-session-feedback-upload"; import { AttendancePanel } from "./session-editor/attendance-panel"; import { BasicSessionPanel } from "./session-editor/basic-session-panel"; import { DocumentStatePanel } from "./session-editor/document-state-panel"; @@ -61,8 +58,10 @@ import { type MobileEditorSection, } from "./session-editor/mobile-editor-tabs"; import { PublicationPanel } from "./session-editor/publication-panel"; -import { SessionImportPanel } from "./session-editor/session-import-panel"; -import { Panel } from "./session-editor/session-editor-panel"; +import { + SessionRecordCompletionPanel, + type SessionRecordCompletionMode, +} from "./session-editor/session-record-completion-panel"; export type { HostSessionEditorLinkComponent } from "./session-editor/session-editor-links"; @@ -84,17 +83,17 @@ function scopedHostRedirectHref(href: string) { return scopedAppLinkTarget(globalThis.location.pathname, href); } -type ImportMode = "json" | "aigen"; +type ImportMode = SessionRecordCompletionMode; function readInitialImportMode(): ImportMode { if (typeof window === "undefined") { - return "json"; + return "aigen"; } try { const params = new URLSearchParams(window.location.search); - return params.get("aigen") === "1" ? "aigen" : "json"; + return params.get("records") === "json" ? "json" : "aigen"; } catch { - return "json"; + return "aigen"; } } @@ -104,10 +103,12 @@ function writeImportModeToUrl(mode: ImportMode) { } try { const params = new URLSearchParams(window.location.search); - if (mode === "aigen") { - params.set("aigen", "1"); - } else { + if (mode === "json") { + params.set("records", "json"); params.delete("aigen"); + } else { + params.set("aigen", "1"); + params.delete("records"); } const search = params.toString(); const nextUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash ?? ""}`; @@ -217,7 +218,6 @@ export default function HostSessionEditor({ // --------------------------------------------------------------------------- // Refs // --------------------------------------------------------------------------- - const feedbackDocumentInputRef = useRef(null); const deleteTriggerRef = useRef(null); const deleteRestoreFocusRef = useRef(null); const committedAttendanceStatusesRef = useRef>( @@ -651,49 +651,6 @@ export default function HostSessionEditor({ [session, actions, flash], ); - const uploadFeedbackDocument = useCallback( - async (event: ChangeEvent) => { - const input = event.currentTarget; - const file = input.files?.[0]; - if (!file) { - return; - } - - if (!session) { - input.value = ""; - return; - } - - const formData = new FormData(); - formData.append("file", file); - - try { - const response = await actions.uploadFeedbackDocument(session.sessionId, formData); - - if (!response.ok) { - flash("피드백 문서 업로드에 실패했습니다. 파일 형식과 권한을 확인해 주세요"); - return; - } - - const uploaded = (await response.json()) as FeedbackDocumentResponse; - dispatch({ - type: "FEEDBACK_DOCUMENT_UPDATED", - feedbackDocument: { - uploaded: true, - fileName: uploaded.fileName, - uploadedAt: uploaded.uploadedAt, - }, - }); - flash("피드백 문서가 업로드되었습니다"); - } catch { - flash("피드백 문서 업로드에 실패했습니다. 네트워크 연결을 확인한 뒤 다시 시도하세요"); - } finally { - input.value = ""; - } - }, - [session, actions, flash], - ); - const previewSessionImport = useCallback( async (event: ChangeEvent) => { const input = event.currentTarget; @@ -816,7 +773,7 @@ export default function HostSessionEditor({ : "저장되었습니다. 이전 화면으로 이동합니다." : saveState === "error" ? "저장에 실패했습니다. 입력값을 확인한 뒤 다시 시도하세요." - : "기본 정보 저장, 기록 공개 범위 저장, 피드백 문서 업로드는 각각 별도로 처리됩니다."} + : "기본 정보 저장, 기록 공개 범위 저장, 세션 기록 패키지 저장은 각각 별도로 처리됩니다."}
@@ -936,80 +893,24 @@ export default function HostSessionEditor({ onUpdateAttendance={updateAttendance} /> - {canShowImportModeToggle ? ( -
- {([ - { mode: "json" as const, label: "외부 도구 JSON 업로드" }, - { mode: "aigen" as const, label: "AI 결과 가져오기" }, - ]).map(({ mode, label }) => { - const selected = effectiveImportMode === mode; - return ( - - ); - })} -
- ) : null} - - {effectiveImportMode === "aigen" && sessionIdForAigen && clubSlug ? ( - - - - ) : ( - - )} - - - - + sessionId={session?.sessionId} + clubSlug={clubSlug} + mode={effectiveImportMode} + canUseAigen={canShowImportModeToggle} + feedbackDocument={feedbackDocumentForPanel} + previewState={feedbackPreviewState} + LinkComponent={LinkComponent} + recordVisibility={recordVisibility} + preview={sessionImportPreview} + status={sessionImportStatus} + error={sessionImportError} + onModeChange={handleImportModeChange} + onAigenCommitted={handleAigenCommitted} + onFileSelected={previewSessionImport} + onCommit={commitSessionImport} + />