{"id":49260582,"url":"https://github.com/beyondwin/readmates","last_synced_at":"2026-04-25T07:03:47.865Z","repository":{"id":353032116,"uuid":"1217625562","full_name":"beyondwin/ReadMates","owner":"beyondwin","description":"Portfolio-ready public release of ReadMates","archived":false,"fork":false,"pushed_at":"2026-04-22T06:08:48.000Z","size":575,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-22T08:14:37.251Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://readmates.pages.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beyondwin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-22T04:18:49.000Z","updated_at":"2026-04-22T06:08:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/beyondwin/ReadMates","commit_stats":null,"previous_names":["beyondwin/readmates"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/beyondwin/ReadMates","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondwin%2FReadMates","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondwin%2FReadMates/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondwin%2FReadMates/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondwin%2FReadMates/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beyondwin","download_url":"https://codeload.github.com/beyondwin/ReadMates/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beyondwin%2FReadMates/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32253252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T04:23:17.126Z","status":"ssl_error","status_checked_at":"2026-04-25T04:21:53.360Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-25T07:03:44.714Z","updated_at":"2026-04-25T07:03:47.846Z","avatar_url":"https://github.com/beyondwin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ReadMates\n\nReadMates는 정기 독서모임의 세션 준비, 참여 관리, 기록 공개, 피드백 문서 열람을 하나의 흐름으로 묶은 멤버십 풀스택 웹 서비스입니다.\n\n- Demo: [https://readmates.pages.dev](https://readmates.pages.dev)\n- Stack: `React 19`, `TypeScript`, `Vite`, `Cloudflare Pages Functions`, `Kotlin`, `Spring Boot`, `Spring Security`, `MySQL`, `Flyway`\n- Scope: 정기 독서모임의 현재·예정 회차 준비부터 참여 관리, 기록 공개, 피드백 문서 열람까지 아우르는 운영형 서비스\n- Highlight: Google OAuth, 서버 측 session cookie, Cloudflare BFF 보안 경계, 현재/예정 세션 공개 범위, 역할 기반 권한 제어, 피드백 문서 접근 제어, Playwright E2E, 공개 릴리즈 후보 scan\n\n이 저장소는 외부 공개를 전제로 정리되어 있습니다. 운영 secret, 실제 멤버 데이터, private deployment state, DB dump, 로컬 경로, OCI OCID는 문서와 예시에 포함하지 않습니다.\n\n## 왜 만들었나\n\n소규모 독서모임은 규모가 작아도 운영 정보가 쉽게 흩어집니다. 모임 공지, RSVP, 읽은 분량, 질문, 하이라이트, 한줄평, 장문 서평, 참석 기록, 모임 후 피드백 문서가 채팅방과 문서 도구 사이에 분산되면 다음 회차를 준비하거나 지난 기록을 다시 찾기 어렵습니다.\n\nReadMates는 이 문제를 단순 게시판이나 CRUD 목록으로 풀지 않습니다. 공개 사이트, 멤버 앱, 호스트 운영 도구, 공개 기록, 참석자 전용 피드백 문서를 하나의 제품 흐름으로 연결해 세션 전후의 실제 운영을 줄이는 데 초점을 둡니다.\n\n## 역할별 기능\n\n| 역할 | 할 수 있는 일 |\n| --- | --- |\n| 게스트 | 로그인 없이 공개 소개, 공개 기록, 공개 세션 상세를 볼 수 있습니다. |\n| 둘러보기 멤버 | 초대 없이 Google로 로그인한 계정입니다. 비공개 세션 기록, 현재 세션 현황, 멤버 공개 예정 세션을 읽을 수 있지만 RSVP, 체크인, 질문/서평 작성, 피드백 문서 열람, 호스트 도구는 제한됩니다. |\n| 정식 멤버 | 초대 링크를 수락했거나 호스트가 전환한 계정입니다. 현재 세션 참여, 예정 세션 확인, RSVP, 읽은 분량 제출, 질문, 한줄평, 장문 서평 작성, 본인 표시 이름 변경이 가능하며 참석한 회차의 피드백 문서를 읽을 수 있습니다. |\n| 호스트 | 정식 멤버 권한에 운영 권한이 추가됩니다. 초대 생성, 둘러보기 멤버 전환, 멤버 상태와 표시 이름 관리, 예정 세션 생성/수정, 공개 범위 설정, 현재 세션 시작, 참석 확정, 공개 기록 발행, 피드백 문서 업로드를 수행합니다. |\n\n비밀번호 로그인과 비밀번호 재설정 엔드포인트는 현재 운영 경로에서 제외되어 `410 Gone`을 반환합니다. 실제 로그인은 Google OAuth를 사용하며, 로컬 개발에서는 fixture 기반 dev-login을 사용할 수 있습니다.\n\n## 아키텍처 요약\n\n```text\nBrowser\n  |\n  | same-origin SPA, /api/bff/**, /oauth2/**, /login/oauth2/**\n  v\nCloudflare Pages\n  |-- Vite SPA\n  |-- Pages Functions BFF and OAuth proxy\n        |-- /api/bff/** -\u003e Spring /api/**\n        |-- /oauth2/authorization/** -\u003e Spring OAuth start\n        |-- /login/oauth2/code/** -\u003e Spring OAuth callback\n  |\n  | X-Readmates-Bff-Secret, forwarded cookies\n  v\nSpring Boot API\n  |-- Google OAuth success handling\n  |-- HttpOnly readmates_session cookie\n  |-- membership, role, and session authorization\n  |-- feedback document parsing and access control\n  |-- Flyway migrations\n  v\nMySQL\n```\n\n프로덕션에서 브라우저는 Spring API origin을 직접 신뢰 경계로 사용하지 않습니다. 브라우저는 같은 origin의 Cloudflare Pages Functions에 요청하고, BFF가 Spring `/api/**`로 전달하면서 `X-Readmates-Bff-Secret`을 붙입니다. 직접 API origin 예시는 문서에서 `https://api.example.com` 같은 placeholder만 사용합니다.\n\n## 주요 기술적 의사결정\n\n- Google OAuth와 서버 측 session cookie: 로그인 성공 후 Spring이 `readmates_session`을 발급합니다. Cookie는 `HttpOnly`, `SameSite=Lax`, production `Secure`로 설정하고, 서버는 token 원문이 아니라 hash를 `auth_sessions`에 저장합니다.\n- Cloudflare Pages Functions BFF: SPA와 API 호출을 같은 origin으로 묶고, `/api/bff/**` 요청만 Spring `/api/**`로 전달합니다. OAuth 시작과 callback도 Pages Functions proxy를 거쳐 public demo origin에서 자연스럽게 동작합니다.\n- BFF secret과 origin/referrer 경계: Spring은 API 요청의 `X-Readmates-Bff-Secret`을 검증합니다. `POST`, `PUT`, `PATCH`, `DELETE` 요청은 허용된 `Origin` 또는 `Referer`도 요구합니다. BFF secret은 Cloudflare Pages Functions와 Spring runtime 설정에만 두고 브라우저 bundle에 넣지 않습니다.\n- 역할 기반 접근 제어: `게스트`, `둘러보기 멤버`, `정식 멤버`, `호스트` 상태에 따라 route와 API 권한을 분리합니다. 읽기 가능한 화면과 쓰기 가능한 operation을 별도로 제한합니다.\n- 현재/예정 세션 관리: `sessions.state`는 `DRAFT`, `OPEN`, `PUBLISHED` 같은 운영 단계를 구분하고, `sessions.visibility`는 `HOST_ONLY`, `MEMBER`, `PUBLIC` 공개 범위의 DB source of truth입니다. 호스트는 여러 예정 `DRAFT` 세션을 준비하고 멤버 공개 여부를 바꿀 수 있지만, 현재 `OPEN` 세션은 클럽당 하나만 시작할 수 있습니다.\n- 멤버 프로필 경계: 화면에 쓰는 표시 이름은 API에서는 `displayName`으로 주고받고, 현재 club membership의 `memberships.short_name`에 저장합니다. 본인은 `/api/me/profile`, 호스트는 `/api/host/members/{membershipId}/profile`로 같은 클럽 멤버의 표시 이름을 정리할 수 있고, 같은 클럽 안 중복과 예약어를 서버에서 막습니다.\n- 피드백 문서 접근 제어: 호스트가 Markdown 피드백 문서를 업로드하면 서버가 파싱해 typed response로 제공합니다. 호스트는 전체 문서를 관리할 수 있고, 정식 멤버는 본인이 참석한 회차의 피드백 문서만 읽을 수 있습니다. 둘러보기 멤버와 미참석자는 locked state를 봅니다.\n- 공개 기록 경계: public route/API에는 `public_session_publications.visibility=PUBLIC`인 기록과 공개 가능한 하이라이트/한줄평만 노출합니다. 멤버 홈의 예정 세션은 `/api/sessions/upcoming`에서 `MEMBER`/`PUBLIC` `DRAFT`만 반환하고, `HOST_ONLY` draft는 멤버/둘러보기 화면, archive, notes, public surface에 노출하지 않습니다. 현재 세션 참여, private notes, meeting data, feedback document 본문은 인증과 권한 검사를 통과해야 접근할 수 있습니다.\n- 프론트엔드 route-first 구조: React Router route module이 loader/action, API 호출, 모델 조립, UI props assembly를 담당합니다. 실제 source root는 `front/src/app`, `front/src/pages`, `front/features`, `front/shared`이며, feature는 `api`, `model`, `route`, `ui` 책임으로 나누고 UI는 props와 callback만 받아 렌더링합니다.\n- 서버 클린 아키텍처 전환: `publication`, `archive`, `feedback`, `session`, `note`, `auth`의 운영 API surface는 `adapter.in.web -\u003e application.port.in -\u003e application.service -\u003e application.port.out -\u003e adapter.out.persistence` 경계를 따릅니다. Shared health endpoint는 `shared.adapter.in.web`에 있고, disabled password/password-reset/dev-invitation endpoint는 `410 Gone` stub으로 남습니다. Boundary test는 전환된 application package의 Spring JDBC/DAO 의존과 web adapter의 persistence 세부 의존을 막습니다.\n- 공개 릴리즈 안전성: `scripts/build-public-release-candidate.sh`로 공개 릴리즈 후보를 만든 뒤 `scripts/public-release-check.sh`로 private path, local workstation path, OCI OCID, GitHub token, OpenAI/API-key-shaped token, real-looking DB/BFF/OAuth secret, Gmail 주소 등을 검사합니다.\n\n## AI-assisted 운영 콘텐츠\n\nReadMates의 피드백, 하이라이트, 한줄평은 앱 외부 운영 워크플로우에서 AI로 정리된 운영 산출물입니다. ReadMates는 그 산출물을 저장, 파싱, 권한 검증, 공개하고 세션 기록과 피드백 문서로 보여줍니다.\n\n현재 ReadMates 앱, 서버, 프론트엔드는 AI API를 직접 호출하지 않습니다. 즉, 제품 기능은 in-app AI generation이 아니라 외부에서 정리된 콘텐츠를 안전하게 운영하고 노출하는 흐름입니다.\n\n## 기술 스택\n\n| 영역 | 기술 |\n| --- | --- |\n| Frontend | `React 19`, `React Router 7`, `TypeScript`, `Vite` |\n| Frontend tests | `Vitest`, `Testing Library`, `Playwright` |\n| Edge/BFF | `Cloudflare Pages Functions` |\n| Backend | `Kotlin`, `Spring Boot`, `Spring Security`, `OAuth2 Client`, `JPA`, `Flyway` |\n| Database | `MySQL 8` compatible database, `Testcontainers MySQL` |\n| Deployment | `Cloudflare Pages`, `OCI Compute`, `OCI MySQL HeatWave`, `systemd`, `Caddy` |\n\n## 검증 방식\n\n주요 검증 명령은 아래와 같습니다. 상세 설명은 [테스트 가이드](docs/development/test-guide.md)를 참고합니다.\n\n```bash\npnpm --dir front lint\npnpm --dir front test\npnpm --dir front build\npnpm --dir front test:e2e\n./server/gradlew -p server clean test\n```\n\n공개 릴리즈 후보 검증은 배포 절차와 별개로 실행합니다.\n\n```bash\n./scripts/build-public-release-candidate.sh\n./scripts/public-release-check.sh .tmp/public-release-candidate\n```\n\n## 로컬 실행 요약\n\n자세한 로컬 실행 절차는 [docs/development/local-setup.md](docs/development/local-setup.md)에 정리합니다.\n\n필수 도구:\n\n- `JDK 21`\n- `Node.js 24` 권장\n- `pnpm`\n- `Docker Compose` 또는 `MySQL 8` compatible database\n\n요약 명령:\n\n```bash\npnpm --dir front install --frozen-lockfile\ndocker compose up -d mysql\n```\n\n```bash\nSPRING_PROFILES_ACTIVE=dev \\\nSPRING_DATASOURCE_URL='jdbc:mysql://localhost:3306/\u003clocal-db-name\u003e?serverTimezone=UTC' \\\nREADMATES_APP_BASE_URL=http://localhost:5173 \\\nREADMATES_ALLOWED_ORIGINS=http://localhost:5173 \\\nREADMATES_BFF_SECRET='\u003clocal-bff-secret\u003e' \\\n./server/gradlew -p server bootRun\n```\n\n```bash\nREADMATES_API_BASE_URL=http://localhost:8080 \\\nREADMATES_BFF_SECRET='\u003clocal-bff-secret\u003e' \\\npnpm --dir front dev\n```\n\n브라우저에서 `http://localhost:5173`을 엽니다. Dev profile은 sample seed data와 dev-login을 로컬 개발용으로만 제공합니다.\n\n## 문서 링크\n\n| 목적 | 문서 |\n| --- | --- |\n| 개발자 문서 허브 | [docs/development/README.md](docs/development/README.md) |\n| 로컬 실행 | [docs/development/local-setup.md](docs/development/local-setup.md) |\n| 아키텍처 상세 | [docs/development/architecture.md](docs/development/architecture.md) |\n| 테스트 가이드 | [docs/development/test-guide.md](docs/development/test-guide.md) |\n| 릴리즈 관리와 CHANGELOG | [docs/development/release-management.md](docs/development/release-management.md), [CHANGELOG.md](CHANGELOG.md) |\n| 배포 문서 허브 | [docs/deploy/README.md](docs/deploy/README.md) |\n| Cloudflare Pages | [docs/deploy/cloudflare-pages.md](docs/deploy/cloudflare-pages.md) |\n| SPA fallback | [docs/deploy/cloudflare-pages-spa.md](docs/deploy/cloudflare-pages-spa.md) |\n| OCI backend | [docs/deploy/oci-backend.md](docs/deploy/oci-backend.md) |\n| OCI MySQL HeatWave | [docs/deploy/oci-mysql-heatwave.md](docs/deploy/oci-mysql-heatwave.md) |\n| 공개 저장소 보안과 release safety | [docs/deploy/security-public-repo.md](docs/deploy/security-public-repo.md) |\n| Release helper scripts | [scripts/README.md](scripts/README.md) |\n\nREADME는 포트폴리오 첫 화면 역할에 집중하고, 상세 로컬 실행 절차와 테스트, 아키텍처, 배포 runbook은 위 문서로 분리합니다.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeyondwin%2Freadmates","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeyondwin%2Freadmates","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeyondwin%2Freadmates/lists"}