From 6f9fc3730a427c2a2f57f694d3d1f02b8d4380a1 Mon Sep 17 00:00:00 2001 From: SangwanYu Date: Wed, 19 Aug 2026 01:07:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?docs(docs):=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=EC=9D=84=20=EC=84=A0=ED=96=89=20=ED=99=95=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constraint: 구현 전 Server/Web guardian 계약 검토가 필요하다 Rejected: 코드 구현 동시 진행 | 계약 리뷰 전 스키마/API 변경을 고정하지 않기 위해 보류한다 Confidence: high Scope-risk: moderate Directive: 구현은 self/public DTO와 계정 생명주기 계약 승인 후 진행한다 Tested: git diff --stat으로 docs/architecture.md, docs/domain.md, docs/decisions/2026-08.md 문서 변경만 확인했다 Not-tested: 문서 계약 변경만 포함해 애플리케이션 빌드와 테스트는 실행하지 않았다 --- docs/architecture.md | 41 +++++++++++++++++++++++++++++++++++++++ docs/decisions/2026-08.md | 10 ++++++++++ docs/domain.md | 37 ++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index f892eda..bb2d7f2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -63,6 +63,47 @@ GitHub는 webhook이 아니라 **Server가 사용자 요청 시 끌어오는(pul AI 추천 매칭과 별개로, Web에서 사용자가 적용사항에 커밋을 직접 확정 연결하거나 해제할 수 있다. 이 확정 연결은 재분석으로도 지워지지 않으며, 이후 추천 매칭 계산 시 그 커밋을 후보에서 제외한다. 두 관계의 차이는 `docs/domain.md`의 "추천 매칭 vs 확정 연결" 참조. +## 서비스 흐름 5 — 마이페이지·멤버 프로필 (Web ↔ Server) + +- 프로필 API는 본인용과 공개용을 분리한다. 본인 API만 이메일, 계정 기능, 최근 완료 회의·결정, GitHub 재연결/오류 상태를 담고, 공개 API는 이름·이미지·활성 프로젝트와 승인된 집계만 담는다. +- 계정 생명주기는 `UNVERIFIED → ACTIVE → WITHDRAWAL_PENDING → TOMBSTONED`로 확장한다. 기존 `Member` 행과 FK는 보존하고, 계정 탈퇴 완료 시 PII·인증정보만 제거한다. +- 프로필 프로젝트 목록은 `TeamMember.active = true`인 팀만 사용한다. 기존 팀 설정 기능과 프로젝트 내부 과거 기록은 이 계약으로 바꾸지 않는다. +- 통계는 완료 회의(`endDateTime != null`)만 대상으로 한다. 회의 시간은 회의별 분 반올림 없이 초 단위 차이를 합산하고, 저장 커밋 수는 프로젝트의 모든 Repository에 저장된 Commit 수 합계만 제공한다. 개인 커밋 통계는 만들지 않는다. +- `POST /api/members/me/profile/refresh`는 request body 없이 본인 프로필 갱신을 요청한다. `ACTIVE` 본인의 GitHub 토큰으로 접근 가능한 모든 활성 프로젝트·저장소를 enqueue하고, 타인 프로필 조회는 DB 스냅샷만 반환한다. + +## 마이페이지·멤버 프로필 구현 경계 + +### API·스키마 경계 + +- API/DTO는 `SelfProfileResponseDTO`와 `MemberPublicProfileResponseDTO`처럼 self/public을 분리한다. public DTO에는 이메일, 계정 상태, 최근 기록, provider 오류, 저장소 URL, token/reconnect 정보를 직렬화하지 않는다. +- 스키마 변경은 additive로 시작한다: `member.account_status`, `email_verified_at`, `purge_at`, 이메일 인증 토큰, 이메일 변경 예약/토큰, email outbox, external cleanup outbox. 기존 회원은 backfill에서 `ACTIVE`, `email_verified_at = createdAt`, `purge_at = null`로 채운다. +- signup/login/email-change 이메일은 service에서 `trim` + `Locale.ROOT` lowercase로 정규화하고, 기존 `member.email` unique 컬럼에 정규화 값을 저장한다. Migration은 정규화 전 중복을 먼저 감사하고, 중복이 있으면 계정을 병합하지 않고 중단한다. +- 인증·이메일 변경·복구 토큰은 원문을 저장하지 않고 해시된 일회용 토큰으로 저장한다. 각 token table은 `token_hash` unique를 가진다. pending email canonical value는 pending 상태 동안 unique여야 한다. +- 필수 index는 `member(email)` unique, token member/status/expiry 조회 index, outbox `status/next_attempt_at` index, member `account_status/purge_at` cleanup index다. `TeamMember(member_id, active)`와 회의 완료/profile aggregate index는 query plan으로 검토한다. +- 팀 설정, 친구, 채팅, 차단, 팀/친구 공개 범위, 개인 커밋 매핑·통계는 이번 계약에 포함하지 않는다. + +### 트랜잭션 경계 + +- 이메일 인증·변경 토큰과 email outbox는 같은 DB 트랜잭션에 기록하고, 실제 이메일 발송은 커밋 이후 worker가 처리한다. +- S3 프로필 이미지 삭제는 DB 트랜잭션에서 참조 제거와 cleanup outbox만 기록하고, 실제 객체 삭제는 커밋 이후 worker가 처리한다. 실패는 재시도 가능한 상태로 남긴다. +- GitHub refresh는 self-only 권한 확인과 DB 스냅샷 응답을 분리하고, GitHub IO를 DB 트랜잭션 밖에서 실행한다. enqueue는 member/repository 단위로 dedupe하고, repository 단위 cooldown과 member 단위 rate limit을 적용한다. 토큰·권한 실패는 기존 저장값을 지우지 않고 본인 응답에만 복구 정보를 노출한다. +- 탈퇴 요청은 유일 OWNER 검사, `WITHDRAWAL_PENDING` 전환, 세션·GitHub 토큰 폐기를 한 트랜잭션에서 처리한다. 30일 purge는 멱등 scheduler가 `TOMBSTONED` 전환과 PII 제거를 처리한다. + +### rollout·migration 순서 + +1. 이메일 정규화 중복을 감사한 뒤 additive 컬럼·토큰·outbox 테이블을 추가하고 기존 회원을 `ACTIVE`, `email_verified_at = createdAt`, `purge_at = null`로 backfill한다. 중복이 있으면 migration을 중단한다. +2. self/public DTO와 접근 정책 경계를 먼저 도입해 기존 팀 설정 응답과 호환성을 확인한다. 새 profile GET/update Web 흐름이 live될 때까지 기존 `/api/members/profile-image`는 compatibility alias로 유지한다. +3. 이메일 인증·이메일 변경·비밀번호 변경·탈퇴/복구 생명주기를 순차 적용한다. +4. 완료 회의 초 단위 집계와 프로젝트 저장 커밋 합계 projection을 추가한다. +5. 본인 전용 GitHub refresh와 outbox worker를 연결하고 부분 실패 상태를 검증한다. +6. Web 라우트(`/mypage`, `/members/:memberId`, 인증/복구 확인 화면)를 연결한다. + +### guardian review points + +- Server guardian: public/self DTO, 계정 상태 전이, 이메일 점유 unique/index, outbox transaction, purge FK 보존을 승인한다. +- Web guardian: `/mypage`와 `/members/:memberId` 필드 노출 차이, 모바일 확장 상태, 기존 팀 설정 UI 호환성을 승인한다. +- 공용 계약 변경은 Server/Web 양쪽 가디언 확인 뒤 적용한다. 이메일 발송 업체나 운영 자격증명은 이 문서에서 정하지 않는다. + ## 경계와 소유권 - **ai를 직접 호출하는 곳은 Server 안의 한 경계 뿐이다.** 도메인 서비스가 ai SDK나 HTTP 호출을 직접 들고 있지 않고, 전용 클라이언트를 주입받아 쓴다. LiveKit·S3·GitHub 연동도 같은 원칙으로 별도 경계에 모여 있다. diff --git a/docs/decisions/2026-08.md b/docs/decisions/2026-08.md index 09a0d81..a838010 100644 --- a/docs/decisions/2026-08.md +++ b/docs/decisions/2026-08.md @@ -15,3 +15,13 @@ - 실시간 회의는 LiveKit 대신 RTC를 직접 구현한다. - 담당을 상완(로그인/마이페이지), 혜성(실시간 회의), 준용(결정사항·적용사항 대시보드), 윤지(팀+알림)로 배정한다. - 다음 회의는 2026년 8월 13일(목)로 조율하며, 확정은 카카오톡으로 한다. + +## 2026-08-19 + +- 마이페이지와 멤버 프로필은 본인 API/DTO와 공개 API/DTO를 분리하고, 이메일·계정 상태·최근 기록은 본인 응답에만 포함한다. +- 회원 계정 생명주기는 `UNVERIFIED`, `ACTIVE`, `WITHDRAWAL_PENDING`, `TOMBSTONED`를 additive로 도입하고 기존 회원은 `ACTIVE`·이메일 인증 완료로 backfill한다. +- 프로필 프로젝트 목록은 `TeamMember.active = true`만 포함하고 기존 팀 설정 기능은 유지한다. +- 프로필 통계는 완료 회의 초 단위 누적과 프로젝트 저장 커밋 수 합계만 제공하며 개인 커밋 통계는 만들지 않는다. +- 가입 인증·이메일 변경·복구 토큰은 해시된 일회용 토큰으로 저장하고 이메일 발송과 S3 삭제는 커밋 이후 outbox로 처리한다. +- 본인 GitHub refresh만 외부 IO를 수행하고 타인 프로필 조회는 DB 스냅샷만 반환한다. +- 친구·채팅·차단·팀/친구 공개 범위와 이메일 발송 업체 선정은 이번 마이페이지·멤버 프로필 범위에서 제외한다. diff --git a/docs/domain.md b/docs/domain.md index a4a1720..effed85 100644 --- a/docs/domain.md +++ b/docs/domain.md @@ -20,10 +20,17 @@ WhyLog는 팀 회의를 녹음·전사하고 AI로 논의 주제와 결정 근 | 결정 타임라인 (DecisionTimeline) | 결정이 도출되기까지의 단계별 서술. 이슈제기 → 대안논의 → 적용합의 3단계로 고정 | | 신뢰도 (reliabilityScore / confidence) | 결정 또는 커밋 매칭 결과의 신뢰 정도를 나타내는 점수. Decision 자체의 신뢰도와 커밋 매칭 신뢰도는 서로 다른 값이니 혼동하지 않는다 | | 커밋 매칭 (CommitConnection) | 적용사항(Application)과 실제 Git 커밋을 연결하는 관계 | +| 프로젝트 | 제품 문구에서 `Team`을 가리키는 이름 | +| 활성 프로젝트 | `TeamMember.active = true`인 프로젝트. 프로필 프로젝트 목록과 집계 대상은 활성 프로젝트만 포함한다 | +| 계정 상태 (accountStatus) | `UNVERIFIED`, `ACTIVE`, `WITHDRAWAL_PENDING`, `TOMBSTONED` 중 하나인 회원 생명주기 상태 | +| 본인 프로필 | 로그인 사용자가 자기 계정 정보와 계정 관리 기능을 보는 비공개 프로필 | +| 공개 멤버 프로필 | 로그인 사용자가 다른 멤버의 허용된 기본 정보와 집계만 보는 제한 공개 프로필 | +| 저장 커밋 수 | GitHub 전체 커밋 수가 아니라 Server DB에 동기화되어 저장된 커밋 수 | ## 핵심 엔티티와 관계 -- Team 1—N Meeting, Repository / Team N—N Member (TeamMember 조인, role: OWNER·MEMBER) +- Member 1—N TeamMember / Member는 `accountStatus`로 계정 생명주기를 가진다 +- Team 1—N Meeting, Repository / Team N—N Member (TeamMember 조인, role: OWNER·MEMBER, active) - Meeting 1—N Dialogue, MeetingMember(role: OWNER·GENERAL) / Meeting 1—1 MeetingAnalysis, Decision - Decision 1—N Application, DecisionBase, DecisionTimeline, DecisionCommits - Application N—N DecisionBase / DecisionTimeline / DecisionCommits (join 엔티티가 reason·confidence를 보유) @@ -63,10 +70,38 @@ WhyLog는 팀 회의를 녹음·전사하고 AI로 논의 주제와 결정 근 - 팀 삭제는 반드시 해당 팀의 OWNER만 할 수 있다. - 팀을 삭제하면 그 팀의 회의들도 함께 정리되고(`meetingCleanupService`), 삭제 커밋 이후 진행중이던 실시간 회의 방을 닫는다. +### 멤버 프로필과 계정 생명주기 (Member) + +- 계정 상태는 `UNVERIFIED`(이메일 미인증), `ACTIVE`(정상 사용), `WITHDRAWAL_PENDING`(30일 탈퇴 유예), `TOMBSTONED`(PII·인증정보 제거 완료)만 사용한다. +- 기존 회원은 migration에서 `ACTIVE`, `email_verified_at = createdAt`, `purge_at = null`로 backfill한다. +- `UNVERIFIED` 회원은 일반 JWT를 받을 수 없고 서비스 화면에 접근할 수 없다. 미인증 보존 기한이 지나면 cleanup이 이메일 점유를 해제한다. +- `WITHDRAWAL_PENDING` 회원은 일반 서비스에 접근할 수 없고 복구 challenge만 사용할 수 있다. 30일 유예 안에는 `ACTIVE`로 복구할 수 있으며, 유예 중 타인 프로필과 허용된 과거 이력은 기존 표시를 유지한다. 유예 중 같은 이메일의 신규 가입은 복구로 안내한다. +- `TOMBSTONED` 회원의 직접 프로필은 `404`다. FK와 공유 이력은 유지하고, 허용된 프로젝트 과거 화면에는 `탈퇴한 사용자`로 표시한다. +- 본인 프로필 DTO에만 이메일, 계정 상태·기능, 최근 완료 회의·결정, GitHub 재연결·오류 정보를 포함한다. 공개 멤버 프로필 DTO에는 이 필드를 포함하지 않는다. +- 프로필 프로젝트 목록은 `TeamMember.active = true`인 프로젝트만 포함한다. 프로젝트에서 나가면 양쪽 프로필 목록에서 즉시 빠지고, 프로젝트 내부 과거 기록에는 `나간 사용자`로 남는다. +- 탈퇴 요청은 `ACTIVE` 회원만 가능하다. 하나라도 유일 OWNER인 활성 프로젝트가 있으면 탈퇴를 거부한다. +- 탈퇴 완료 cleanup은 이메일, 비밀번호, 이름, 프로필 이미지 참조, GitHub 토큰 등 PII·인증정보를 제거하되 회의·발화·결정 이력은 삭제하지 않는다. +- signup/login/email-change 이메일은 `trim` + `Locale.ROOT` lowercase로 정규화한 값을 `member.email`에 저장한다. 정규화 migration은 중복 계정을 병합하지 않고 중복 발견 시 중단한다. +- 가입 인증, 이메일 변경, 복구 토큰은 해시된 일회용 토큰이며 기한을 가진다. 이메일 변경은 현재 비밀번호 재확인과 새 이메일 인증이 모두 끝나야 적용된다. +- 비밀번호나 이메일을 변경하면 기존 refresh token을 폐기한다. +- 이메일 발송과 S3 이미지 삭제는 outbox로 기록한 뒤 커밋 이후 처리한다. 실패는 성공으로 간주하지 않고 재시도 가능해야 한다. + +### 마이페이지·공개 프로필 통계 + +- 통계는 활성 프로젝트별로만 제공한다. +- 회의 수와 누적 회의 시간은 완료 회의(`endDateTime != null`)만 포함한다. 진행 중 회의는 제외한다. +- 누적 회의 시간은 `startDateTime`과 `endDateTime`의 차이를 초 단위로 모두 합산한 뒤 화면에서 포맷한다. 회의별 분 단위 절삭·반올림 후 합산하지 않는다. +- 개인 회의 통계는 대상 멤버가 `MeetingMember`로 참여한 완료 회의만 센다. +- 프로젝트 회의 통계는 프로젝트의 완료 회의 전체를 센다. +- 저장 커밋 수는 프로젝트의 모든 Repository에 저장된 Commit 수 합계다. 개인 커밋 수나 개인 커밋 매핑은 제공하지 않는다. +- 본인 프로필에만 최신 완료 회의와 최신 결정을 기본 5개씩 제공한다. 공개 멤버 프로필에는 최근 기록과 커밋 피드를 제공하지 않는다. +- 공개 멤버 프로필의 저장 커밋 최신성은 집계 내부 메타데이터 `SYNCED`, `STALE`, `UNAVAILABLE`과 기준시각만 노출한다. provider 오류, 토큰 상태, 저장소 URL, 내부 job ID는 노출하지 않는다. + ### GitHub 연동 (Member) - GitHub 액세스 토큰은 AES로 암호화해 저장한다(`AESCryptoConverter`). - GitHub API가 401을 반환하면 저장된 토큰을 즉시 폐기한다(`clearGithubToken`) — 만료된 토큰을 계속 재사용하지 않는다. +- 프로필용 GitHub refresh는 `ACTIVE` 본인만 request body 없이 요청할 수 있고 DB 트랜잭션 밖에서 실행한다. 본인의 GitHub 토큰으로 접근 가능한 모든 활성 프로젝트·저장소를 enqueue하며, member/repository dedupe, repository cooldown, member rate limit을 적용한다. 타인 프로필 조회는 외부 동기화를 시작하지 않는다. ### 커밋 · 레포지토리 (Git) From c4997a336c968ecbe54594cec4dc22faf09fb04f Mon Sep 17 00:00:00 2001 From: whylog-dev Date: Wed, 19 Aug 2026 01:09:21 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(docs):=20PR-15=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8=20=EA=B7=BC=EA=B1=B0=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pr-reviews/PR-15.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/pr-reviews/PR-15.md diff --git a/docs/pr-reviews/PR-15.md b/docs/pr-reviews/PR-15.md new file mode 100644 index 0000000..bc1d0c0 --- /dev/null +++ b/docs/pr-reviews/PR-15.md @@ -0,0 +1,35 @@ +# PR-15 AI 리뷰 기록 + +- PR: https://github.com/WhyLog-App/WhyLog/pull/15 +- 제목: feat: 마이페이지와 멤버 프로필 구현 +- 브랜치: `develop` ← `feat/member-profile` +- HEAD: `6f9fc3730a427c2a2f57f694d3d1f02b8d4380a1` +- 입력 digest: `f0cb1475d156bfd23216c5fb2b2ab11138fe4d0b16c0d07c58f0a8ba6a13fd70` +- 모델: Google `gemini-3.6-flash` +- 상태: **PASS** +- 생성 시각(UTC): 2026-08-18T16:09:17+00:00 + +## 요약 + +마이페이지 및 멤버 프로필 기능을 도입하기 위한 도메인 용어, 계정 생명주기, 아키텍처 흐름 및 구현 경계 계약을 docs/ 문서에 추가한 PR입니다. 문서 작성 규칙 및 AGENTS.md를 준수하고 있어 문제 항목이 없습니다. + +## 이번 실행에서 새로 발견됨 + +없음 + +## 이전 실행부터 계속 남아있음 + +없음 + +## 현재까지 사라짐(자동 추정) + +없음 + +## 실행 이력 + +|HEAD|상태|모델|전체|신규|계속|해결|시각| +|---|---|---|---:|---:|---:|---:|---| +|6f9fc3730a42|PASS|Google gemini-3.6-flash|||||2026-08-18T16:09:17+00:00| + + +