Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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
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
Loading