Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 연동도 같은 원칙으로 별도 경계에 모여 있다.
Expand Down
10 changes: 10 additions & 0 deletions docs/decisions/2026-08.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 스냅샷만 반환한다.
- 친구·채팅·차단·팀/친구 공개 범위와 이메일 발송 업체 선정은 이번 마이페이지·멤버 프로필 범위에서 제외한다.
37 changes: 36 additions & 1 deletion docs/domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -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를 보유)
Expand Down Expand Up @@ -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)

Expand Down
35 changes: 35 additions & 0 deletions docs/pr-reviews/PR-15.md
Original file line number Diff line number Diff line change
@@ -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|

<!-- whylog-ai-pr-review-state {"findings":[],"head_sha":"6f9fc3730a427c2a2f57f694d3d1f02b8d4380a1","history":[{"generated_at":"2026-08-18T16:09:17+00:00","head_sha":"6f9fc3730a427c2a2f57f694d3d1f02b8d4380a1","model":"gemini-3.6-flash","new":0,"ongoing":0,"provider":"Google","resolved":0,"status":"PASS","total":0}],"model":"gemini-3.6-flash","pr":{"author":"wantkdd","base":"develop","head":"feat/member-profile","number":15,"title":"feat: 마이페이지와 멤버 프로필 구현","url":"https://github.com/WhyLog-App/WhyLog/pull/15"},"provider":"Google","resolved":[],"review_input_digest":"f0cb1475d156bfd23216c5fb2b2ab11138fe4d0b16c0d07c58f0a8ba6a13fd70","schema":1,"status":"PASS","summary":"마이페이지 및 멤버 프로필 기능을 도입하기 위한 도메인 용어, 계정 생명주기, 아키텍처 흐름 및 구현 경계 계약을 docs/ 문서에 추가한 PR입니다. 문서 작성 규칙 및 AGENTS.md를 준수하고 있어 문제 항목이 없습니다."} -->
<!-- whylog-ai-pr-review-signature 42d5cc30e8f2f8d22f96aadec0473cb3a344fb0aa354381f8cad0fdece0356ac -->