Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
e0a62ec
docs(docs): 회원 프로필 정책 정리
wantkdd Aug 31, 2026
eedc8d5
chore(server): 이메일 발송 환경 안내 정리
wantkdd Aug 31, 2026
ae734cd
chore(server): 이메일 발송 기반 구성
wantkdd Aug 31, 2026
6bb38b5
feat(server): Gmail 인증 메일 발송 구현
wantkdd Aug 31, 2026
e29e8e5
refactor(server): 인증 에러 코드 명명 통일
wantkdd Aug 31, 2026
2ad14d0
refactor(server): 회원 에러 코드 명명 통일
wantkdd Aug 31, 2026
429eb03
feat(server): 회원 계정 상태 스키마 구성
wantkdd Aug 31, 2026
ca8bc7c
feat(server): 이메일 인증 코드 저장 구조 구성
wantkdd Aug 31, 2026
d56b773
feat(server): 프로필 공개 범위 스키마 구성
wantkdd Aug 31, 2026
141be75
feat(server): 탈퇴 계정 정리 기반 구성
wantkdd Aug 31, 2026
98cf4f4
feat(server): 프로필 집계 인덱스 및 조회 구성
wantkdd Aug 31, 2026
9d3327b
feat(server): 이메일 인증 코드 처리 구현
wantkdd Aug 31, 2026
fa77c98
feat(server): 탈퇴 계정 복구 챌린지 구현
wantkdd Aug 31, 2026
8035e73
feat(server): 로그인 계정 상태 응답 구성
wantkdd Aug 31, 2026
55bdce4
feat(server): 인증 상태 전환 API 연결
wantkdd Aug 31, 2026
d82b2c7
feat(server): 리프레시 토큰 쿠키 보안 정리
wantkdd Aug 31, 2026
409791b
feat(server): 비활성 계정 접근 차단
wantkdd Aug 31, 2026
95cf5f1
feat(server): 회원 계정 생명주기 구현
wantkdd Aug 31, 2026
2b9f954
feat(server): 마이페이지 조회 계약 구현
wantkdd Aug 31, 2026
0abee7e
feat(server): 회원 프로필 변경 API 구현
wantkdd Aug 31, 2026
b6b5558
feat(server): 이탈 회원 표시 정책 적용
wantkdd Aug 31, 2026
a86e712
feat(server): 회의 참여자 표시 정보 확장
wantkdd Aug 31, 2026
a1c99f7
feat(server): 마이페이지 활동 통계 조회 구현
wantkdd Aug 31, 2026
d335b1d
refactor(server): 회의 조회 서비스 경계 정리
wantkdd Aug 31, 2026
c83b9a8
refactor(server): 팀 컨트롤러 회원 에러 코드 적용
wantkdd Aug 31, 2026
3d17d2d
Merge remote-tracking branch 'origin/develop' into feat/member-profile
wantkdd Sep 7, 2026
3bd1635
docs(docs): PR-18 리뷰 판단 근거 기록
whylog-dev Sep 7, 2026
3e13111
feat(web): 회원 프로필 타입 및 입력 제한 정의
wantkdd Sep 8, 2026
8243cc2
feat(web): 인증 상태별 응답 타입 정의
wantkdd Sep 8, 2026
fe131b5
feat(web): 회원 인증 API 및 화면 경로 정의
wantkdd Sep 8, 2026
e0fce00
feat(web): 회원 정보 조회 및 변경 API 연결
wantkdd Sep 8, 2026
0cbad0d
refactor(web): 세션 경계 캐시 초기화 통합
wantkdd Sep 8, 2026
1d3d1cd
feat(web): 가입 이미지 검증 및 인증 정보 보존
wantkdd Sep 8, 2026
ee1a313
feat(web): 인증 대기 이미지 임시 보존 및 정리
wantkdd Sep 8, 2026
a66df98
fix(web): 인증 복원 실패 시 세션 정리
wantkdd Sep 8, 2026
13adbff
feat(web): 비밀번호 표시 전환 공용 입력 추가
wantkdd Sep 8, 2026
d61de02
refactor(web): 인증 안내와 카드 공용화
wantkdd Sep 8, 2026
b41715e
refactor(web): 결정 카드 공용 컴포넌트 연결
wantkdd Sep 8, 2026
4bdbb68
refactor(web): 타임라인 및 커밋 상세 카드 연결
wantkdd Sep 8, 2026
5841292
fix(web): 확인 모달 반응형 및 키보드 접근성 보완
wantkdd Sep 8, 2026
2210fac
feat(web): 이메일 인증 입력 및 안내 화면 추가
wantkdd Sep 8, 2026
83d646a
feat(web): 이메일 인증 및 가입 이미지 등록 연결
wantkdd Sep 8, 2026
02bbd00
feat(web): 가입 후 이메일 인증 흐름 연결
wantkdd Sep 8, 2026
1658038
feat(web): 탈퇴 계정 복구 정보 및 화면 추가
wantkdd Sep 8, 2026
c7e55f7
feat(web): 탈퇴 계정 복구 요청 연결
wantkdd Sep 8, 2026
117cc02
feat(web): 로그인 상태별 인증 및 복구 분기
wantkdd Sep 8, 2026
3ded579
feat(web): 회원 정보 및 참여 프로젝트 조회 훅 추가
wantkdd Sep 8, 2026
c36f01a
fix(web): 회원 변경 후 관련 표시 캐시 갱신
wantkdd Sep 8, 2026
30479e7
feat(web): 프로필 요약 및 아바타 표시 통합
wantkdd Sep 8, 2026
ad440c6
feat(web): 참여 프로젝트 카드 및 추가 조회 표시
wantkdd Sep 8, 2026
0578daa
feat(web): 마이페이지 이름 및 공개 설정 저장
wantkdd Sep 8, 2026
a829381
feat(web): 마이페이지 프로필 편집 영역 추가
wantkdd Sep 8, 2026
9405051
feat(web): 현재 비밀번호 확인 및 변경 연결
wantkdd Sep 8, 2026
e37f756
feat(web): 회원 탈퇴 확인 및 요청 연결
wantkdd Sep 8, 2026
b808762
feat(web): 마이페이지 활동 및 계정 관리 탭 구성
wantkdd Sep 8, 2026
e2de2c3
feat(web): 타인 프로필 조회 및 공개 범위 표시
wantkdd Sep 8, 2026
bffb1a9
refactor(web): 팀 설정 개인 프로필 편집 제거
wantkdd Sep 8, 2026
926ea7e
feat(web): 인증 및 회원 화면 라우트 연결
wantkdd Sep 8, 2026
c585706
feat(web): 사이드바 마이페이지 메뉴 정의
wantkdd Sep 8, 2026
63ae82e
feat(web): 전역 메뉴 이동 및 로그아웃 확인 연결
wantkdd Sep 8, 2026
7f50e18
fix(web): 마이페이지에서 팀 화면 이동 경로 보정
wantkdd Sep 8, 2026
53588b7
fix(web): 회원 표시 정보 nullable 응답 반영
wantkdd Sep 8, 2026
539dd64
feat(web): 결정 참여자 프로필 진입 연결
wantkdd Sep 8, 2026
4ff7015
refactor(web): 이동 완료한 결정 전용 카드 제거
wantkdd Sep 8, 2026
2274abb
feat(web): 회의 발화자 프로필 표시 정보 연결
wantkdd Sep 8, 2026
e9bdf81
feat(web): 회의 참여자 및 발화자 프로필 진입 연결
wantkdd Sep 8, 2026
294368e
docs(docs): PR-18 리뷰 판단 근거 기록
whylog-dev Sep 8, 2026
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
9 changes: 9 additions & 0 deletions docs/domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ WhyLog는 팀 회의를 녹음·전사하고 AI로 논의 주제와 결정 근
- GitHub 액세스 토큰은 AES로 암호화해 저장한다(`AESCryptoConverter`).
- GitHub API가 401을 반환하면 저장된 토큰을 즉시 폐기한다(`clearGithubToken`) — 만료된 토큰을 계속 재사용하지 않는다.

### 회원 프로필·계정 (Member)

- 마이페이지(`myInfo`)는 본인 전용이고, 멤버 프로필(`profile`)은 다른 사용자가 보는 공개 화면이다.
- 프로필 기본 정보(이미지, 이름, 이메일)는 공개하되, 활동 영역은 `PUBLIC`일 때만 보인다. `PRIVATE`이면 활동 영역 대신 비공개 안내를 보여준다.
- 가입 이메일은 변경하지 않는다. 이메일 인증 전(`UNVERIFIED`)에는 인증 화면 외 서비스 사용을 막는다.
- 회원 탈퇴는 최소 30일 동안 `INACTIVE`로 보관하고, 이후 처음 도래하는 자정에 개인정보를 제거해 `WITHDRAW`로 남긴다. 보관 중에는 같은 계정으로 로그인해 복구할 수 있다.
- 나간 사용자는 팀 멤버십만 비활성화된 사용자이고, 탈퇴 사용자는 계정 상태가 `INACTIVE` 또는 `WITHDRAW`인 사용자다.
- 탈퇴 요청 시 OWNER가 다른 활성 팀원이 있는 팀을 소유 중이면 막는다. OWNER 혼자 남은 팀만 있으면 탈퇴할 수 있고, 팀 삭제는 팀 도메인에서 별도로 정한다.

### 커밋 · 레포지토리 (Git)

- 커밋은 `(repository_id, hash)` 조합으로 유일해야 한다.
Expand Down
38 changes: 38 additions & 0 deletions docs/pr-reviews/PR-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# PR-18 AI 리뷰 기록

- PR: https://github.com/WhyLog-App/WhyLog/pull/18
- 제목: feat: 회원 프로필 및 로그인·마이페이지 구현
- 브랜치: `develop` ← `feat/member-profile`
- HEAD: `e9bdf812220a34f50199d3f7d5ee6c94824971ad`
- 입력 digest: `2e404ec5049efd31f198a998483b22ebfa8f059dfe65f2bfc11e437a36381074`
- 모델: OpenAI `gpt-5.6-terra`
- 상태: **PASS**
- 생성 시각(UTC): 2026-09-08T00:54:17+00:00

## 요약

회원 프로필·인증·탈퇴 흐름과 회의/결정 표시 정보 확장을 위한 서버·문서 변경을 검토했습니다. 제공된 변경 diff에서 명확한 차단 이슈나 확정적인 개선 항목은 확인하지 못했습니다.

## 이번 실행에서 새로 발견됨

없음

## 이전 실행부터 계속 남아있음

없음

## 현재까지 사라짐(자동 추정)

|상태|구분|위치|제목|이유|근거|권장 처리|반영 HEAD|
|---|---|---|---|---|---|---|---|
|resolved|차단|server/src/main/java/com/whylog/server/domain/meeting/service/MeetingQueryService.java|조회 트랜잭션에서 S3 클라이언트를 호출함|클래스 레벨 `@Transactional(readOnly = true)`가 적용된 상태에서 `profileImageUrl`이 `s3Client.getFileUrl(...)`을 호출합니다. S3 처리 지연·실패 동안 DB 트랜잭션과 커넥션이 유지되어 커넥션 고갈 및 조회 지연을 유발할 수 있습니다.|server/AGENTS.md 트랜잭션: 외부 호출(S3, FastAPI, LiveKit, GitHub API)을 트랜잭션 안에서 하지 않는다.|트랜잭션에서는 멤버 표시 정보와 이미지 키만 조회한 뒤 종료하고, S3 URL 생성은 트랜잭션 밖의 별도 비즈니스 경계에서 수행하도록 분리하세요.|e9bdf812220a34f50199d3f7d5ee6c94824971ad|

## 실행 이력

|HEAD|상태|모델|전체|신규|계속|해결|시각|
|---|---|---|---:|---:|---:|---:|---|
|e9bdf812220a|PASS|OpenAI gpt-5.6-terra||||1|2026-09-08T00:54:17+00:00|
|3d17d2d7a13b|BLOCKED|OpenAI gpt-5.6-terra|1|1|||2026-09-07T04:03:28+00:00|

<!-- whylog-ai-pr-review-state {"findings":[],"head_sha":"e9bdf812220a34f50199d3f7d5ee6c94824971ad","history":[{"generated_at":"2026-09-08T00:54:17+00:00","head_sha":"e9bdf812220a34f50199d3f7d5ee6c94824971ad","model":"gpt-5.6-terra","new":0,"ongoing":0,"provider":"OpenAI","resolved":1,"status":"PASS","total":0},{"generated_at":"2026-09-07T04:03:28+00:00","head_sha":"3d17d2d7a13b7bed7f0be57aebae6e6f5b1e071d","model":"gpt-5.6-terra","new":1,"ongoing":0,"provider":"OpenAI","resolved":0,"status":"BLOCKED","total":1}],"model":"gpt-5.6-terra","pr":{"author":"wantkdd","base":"develop","head":"feat/member-profile","number":18,"title":"feat: 회원 프로필 및 로그인·마이페이지 구현","url":"https://github.com/WhyLog-App/WhyLog/pull/18"},"provider":"OpenAI","resolved":[{"file":"server/src/main/java/com/whylog/server/domain/meeting/service/MeetingQueryService.java","fingerprint":"fbe1e208fc5bb9d99dc35364","kind":"blocking","line":null,"reason":"클래스 레벨 `@Transactional(readOnly = true)`가 적용된 상태에서 `profileImageUrl`이 `s3Client.getFileUrl(...)`을 호출합니다. S3 처리 지연·실패 동안 DB 트랜잭션과 커넥션이 유지되어 커넥션 고갈 및 조회 지연을 유발할 수 있습니다.","recommendation":"트랜잭션에서는 멤버 표시 정보와 이미지 키만 조회한 뒤 종료하고, S3 URL 생성은 트랜잭션 밖의 별도 비즈니스 경계에서 수행하도록 분리하세요.","resolved_by_head_sha":"e9bdf812220a34f50199d3f7d5ee6c94824971ad","rule_reference":"server/AGENTS.md 트랜잭션: 외부 호출(S3, FastAPI, LiveKit, GitHub API)을 트랜잭션 안에서 하지 않는다.","status":"resolved","title":"조회 트랜잭션에서 S3 클라이언트를 호출함"}],"review_input_digest":"2e404ec5049efd31f198a998483b22ebfa8f059dfe65f2bfc11e437a36381074","schema":1,"status":"PASS","summary":"회원 프로필·인증·탈퇴 흐름과 회의/결정 표시 정보 확장을 위한 서버·문서 변경을 검토했습니다. 제공된 변경 diff에서 명확한 차단 이슈나 확정적인 개선 항목은 확인하지 못했습니다."} -->
<!-- whylog-ai-pr-review-signature 65605d3c118b6ffc8ce1961c1e7cbfac6bcc48cffd04ab50e0d023cdb6a2e0bd -->
7 changes: 7 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000
JWT_SECRET=local-dev-jwt-secret-please-change-32chars
JWT_ACCESS_TOKEN_EXPIRE_TIME=3600000
JWT_REFRESH_TOKEN_EXPIRE_TIME=1209600000
# HTTPS 운영 환경에서는 true, 로컬 HTTP에서는 false
REFRESH_TOKEN_COOKIE_SECURE=false

# 이메일 인증 발송(Gmail SMTP)
GMAIL_SMTP_USERNAME=your-sender@gmail.com
GMAIL_SMTP_APP_PASSWORD=
EMAIL_VERIFICATION_CODE_SECRET=local-dev-email-verification-secret-please-change-32chars

# LiveKit 기능을 실제로 쓰지 않으면 기본값(application.yaml)으로 대체됨
LIVEKIT_URL=wss://example.livekit.invalid
Expand Down
15 changes: 13 additions & 2 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,21 @@

```bash
cp .env.example .env
# .env를 열어 값 채우기 (JWT_SECRET, GITHUB_TOKEN_ENCRYPTION_KEY, AWS_*, FAST_API_BASE_URL 등)
# 실제 기능(S3 업로드·GitHub 연동·LiveKit)을 안 쓸 거면 .env.example의 더미값 그대로 둬도 서버는 기동됩니다.
# .env를 열어 값 채우기 (JWT_SECRET, GMAIL_SMTP_*, EMAIL_VERIFICATION_CODE_SECRET 등)
```

### 이메일 인증 발송 설정

가입 인증 메일은 Gmail SMTP로 발송하므로 실제 서버 실행 전에 아래 값을 채웁니다.

- `GMAIL_SMTP_USERNAME`: 실제 발신에 사용할 Gmail 주소
- `GMAIL_SMTP_APP_PASSWORD`: Google 계정 일반 비밀번호가 아니라 Gmail SMTP용 Google 앱 비밀번호입니다.
- `EMAIL_VERIFICATION_CODE_SECRET`: 인증 코드 HMAC에 사용할 32바이트 이상의 임의 문자열

Google 계정의 2단계 인증을 켠 뒤 앱 비밀번호를 발급해 `.env` 또는 배포 환경변수에만 저장합니다. 실제 값은 Git에 커밋하지 않습니다.

- Google 앱 비밀번호 안내: <https://support.google.com/accounts/answer/185833>

### 1) Docker Compose로 실행

로컬에 MySQL/Redis를 직접 설치하지 않고 컨테이너로 띄웁니다.
Expand Down
1 change: 1 addition & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies {
implementation 'org.flywaydb:flyway-mysql'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-aop'
compileOnly 'org.projectlombok:lombok'
Expand Down
5 changes: 4 additions & 1 deletion server/scripts/check-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ set -a
source .env
set +a

# application.yaml에서 기본값(:) 없이 필수로 요구하는 값만 검사합니다.
# 서버 기동과 핵심 기능에 필요한 값을 검사합니다.
# DEV_REDIS_PASSWORD는 필수지만 로컬 Redis에 비밀번호가 없으면 빈 값이 정상이라 제외합니다.
REQUIRED_VARS=(
DEV_DB_URL
Expand All @@ -22,6 +22,9 @@ REQUIRED_VARS=(
DEV_REDIS_PORT
CORS_ALLOWED_ORIGINS
JWT_SECRET
GMAIL_SMTP_USERNAME
GMAIL_SMTP_APP_PASSWORD
EMAIL_VERIFICATION_CODE_SECRET
GITHUB_TOKEN_ENCRYPTION_KEY
AWS_S3_BUCKET
AWS_S3_ACCESS_KEY
Expand Down
9 changes: 5 additions & 4 deletions server/src/main/java/com/whylog/server/ServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package com.whylog.server.domain.decision.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

public class ApplicationResponse {


@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand Down Expand Up @@ -70,7 +68,6 @@ public static class DecisionTimelineItemDTO {

@Schema(description = "타임라인 내용", example = "장애 이슈 제기")
private String content;

}

@Getter
Expand All @@ -83,20 +80,25 @@ public static class DecisionContextItemDTO {
@Schema(description = "타임라인 시간", example = "2026-03-24T12:28:00")
private String time;

@Schema(description = "발화자 ID", example = "1", nullable = true)
@Schema(
description = "프로필 링크용 발화자 ID. 나간 사용자/탈퇴한 사용자는 null",
example = "1",
nullable = true)
private Long memberId;

@Schema(description = "발화자 이름", example = "김주뇽", nullable = true)
private String memberName;

@Schema(description = "발화자 프로필 사진", example = "https://example.com/profile.jpg", nullable = true)
@Schema(
description = "발화자 프로필 사진",
example = "https://example.com/profile.jpg",
nullable = true)
private String profileImage;

@Schema(description = "대화 내용", example = "아니 우리 이거 버그난다니까?!?@??@")
private String dialogueContent;
}


@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -109,7 +111,6 @@ public static class DecisionReasonItemDTO {

@Schema(description = "근거 내용", example = "운영복잡 우려로 보류")
private String title;

}

@Getter
Expand Down Expand Up @@ -216,5 +217,4 @@ public static class CommitConnectionResponseDTO {
@Schema(description = "연결된 커밋 ID 목록", example = "[1, 2, 3]")
private List<Long> commitIds;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.whylog.server.domain.decision.repository;

import com.whylog.server.domain.decision.entity.Decision;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -12,13 +14,15 @@ public interface DecisionRepository extends JpaRepository<Decision, Long> {
Optional<Decision> findByMeetingId(Long meetingId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
@Query(
"""
UPDATE Decision d
SET d.reliabilityScore = :reliabilityScore
WHERE d.id = :decisionId
""")
void updateReliabilityScore(@Param("decisionId") Long decisionId,
@Param("reliabilityScore") Integer reliabilityScore);
void updateReliabilityScore(
@Param("decisionId") Long decisionId,
@Param("reliabilityScore") Integer reliabilityScore);

@Modifying
@Query("DELETE FROM Application a WHERE a.decision.meeting.team.id = :teamId")
Expand All @@ -35,4 +39,31 @@ void updateReliabilityScore(@Param("decisionId") Long decisionId,
@Modifying
@Query("DELETE FROM Decision d WHERE d.meeting.id = :meetingId")
void deleteByMeetingId(@Param("meetingId") Long meetingId);

@Query(
"""
SELECT new com.whylog.server.domain.decision.repository.DecisionRepository$RecentDecisionRow(
d.id,
t.id,
t.name,
m.name,
d.createdAt
)
FROM Decision d
JOIN d.meeting m
JOIN m.team t
JOIN m.meetingMembers mm
JOIN TeamMember tm ON tm.team = t AND tm.member.id = :memberId AND tm.active = true
WHERE mm.member.id = :memberId
ORDER BY d.createdAt DESC, d.id DESC
""")
List<RecentDecisionRow> findRecentDecisionRowsByMemberId(
@Param("memberId") Long memberId, org.springframework.data.domain.Pageable pageable);

record RecentDecisionRow(
Long decisionId,
Long projectId,
String projectName,
String name,
LocalDateTime createdAt) {}
}
Loading