Skip to content

Commit 8f1f2cb

Browse files
authored
메인 머지 (10/9) Merge pull request #169
메인 머지 (10/9)
2 parents dfde0a5 + e0f3cc3 commit 8f1f2cb

File tree

97 files changed

+2670
-430
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+2670
-430
lines changed

back/.env.default

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
CUSTOM__JWT__SECRET_KEY=NEED_TO_SET
22
DB_USERNAME=NEED_TO_SET
33
DB_PASSWORD=NEED_TO_SET
4+
MAIL_USERNAME=NEED_TO_SET
5+
MAIL_PASSWORD=NEED_TO_SET

back/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies {
3939
implementation("org.springframework.boot:spring-boot-starter-security")
4040
implementation("org.springframework.boot:spring-boot-starter-web")
4141
implementation ("org.springframework.boot:spring-boot-starter-data-redis")
42+
implementation("org.springframework.boot:spring-boot-starter-mail")
4243

4344
// QueryDSL
4445
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
@@ -65,8 +66,12 @@ dependencies {
6566
implementation ("software.amazon.awssdk:s3:2.25.0")
6667

6768
implementation ("org.springframework.kafka:spring-kafka")
69+
implementation("org.springframework.boot:spring-boot-starter-websocket")
6870

6971
runtimeOnly("com.mysql:mysql-connector-j")
72+
73+
// Sentry
74+
implementation("io.sentry:sentry-spring-boot-starter-jakarta:8.19.1")
7075
}
7176

7277
tasks.withType<Test> {

back/src/main/java/com/back/BackApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.scheduling.annotation.EnableScheduling;
67

78
@SpringBootApplication
89
@EnableJpaAuditing
10+
@EnableScheduling
911
public class BackApplication {
1012

1113
public static void main(String[] args) {

back/src/main/java/com/back/domain/file/video/controller/VideoController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.back.global.rsData.RsData;
77
import io.swagger.v3.oas.annotations.Operation;
88
import lombok.RequiredArgsConstructor;
9+
import org.springframework.security.access.prepost.PreAuthorize;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.RequestParam;
1112
import org.springframework.web.bind.annotation.RestController;
@@ -16,15 +17,16 @@ public class VideoController {
1617
private final FileManager fileManager;
1718

1819
@GetMapping("/videos/upload")
19-
@Operation(summary="업로드용 URL 요청", description="파일 업로드를 위한 Presigned URL을 발급받습니다.")
20+
@PreAuthorize("hasRole('ADMIN')")
21+
@Operation(summary = "업로드용 URL 요청", description = "파일 업로드를 위한 Presigned URL을 발급받습니다.")
2022
public RsData<UploadUrlGetResponse> getUploadUrl() {
2123
PresignedUrlResponse uploadUrl = fileManager.getUploadUrl();
2224
UploadUrlGetResponse response = new UploadUrlGetResponse(uploadUrl.url().toString(), uploadUrl.expiresAt());
2325
return new RsData<>("200", "업로드용 URL 요청완료", response);
2426
}
2527

2628
@GetMapping("/videos/download")
27-
@Operation(summary="다운로드용 URL 요청", description="파일 다운로드를 위한 Presigned URL을 발급받습니다.")
29+
@Operation(summary = "다운로드용 URL 요청", description = "파일 다운로드를 위한 Presigned URL을 발급받습니다.")
2830
public RsData<UploadUrlGetResponse> getDownloadUrls(@RequestParam String objectKey) {
2931
PresignedUrlResponse downloadUrl = fileManager.getDownloadUrl(objectKey);
3032
UploadUrlGetResponse response = new UploadUrlGetResponse(downloadUrl.url().toString(), downloadUrl.expiresAt());
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.back.domain.member.member.email;
2+
3+
import com.back.global.exception.ServiceException;
4+
import jakarta.mail.MessagingException;
5+
import jakarta.mail.internet.MimeMessage;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
9+
import org.springframework.mail.javamail.JavaMailSender;
10+
import org.springframework.mail.javamail.MimeMessageHelper;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.io.UnsupportedEncodingException;
14+
15+
@Slf4j
16+
@Service
17+
@RequiredArgsConstructor
18+
@ConditionalOnBean(JavaMailSender.class)
19+
public class EmailService {
20+
private final JavaMailSender mailSender;
21+
22+
/**
23+
* 간단한 텍스트 이메일 발송
24+
*/
25+
public void sendSimpleEmail(String to, String subject, String text) {
26+
try {
27+
MimeMessage message = mailSender.createMimeMessage();
28+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
29+
30+
helper.setFrom("[email protected]", "JobMate");
31+
helper.setTo(to);
32+
helper.setSubject(subject);
33+
helper.setText(text, false); // false = plain text
34+
35+
mailSender.send(message);
36+
log.info("텍스트 이메일 발송 성공: {}", to);
37+
} catch (MessagingException | UnsupportedEncodingException e) {
38+
log.error("텍스트 이메일 발송 실패: {}", to, e);
39+
throw new ServiceException("500-1", "텍스트 이메일 발송에 실패했습니다.");
40+
}
41+
}
42+
43+
/**
44+
* HTML 이메일 발송
45+
*/
46+
public void sendHtmlEmail(String to, String subject, String htmlContent) {
47+
try {
48+
MimeMessage message = mailSender.createMimeMessage();
49+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
50+
51+
helper.setFrom("[email protected]", "JobMate");
52+
helper.setTo(to);
53+
helper.setSubject(subject);
54+
helper.setText(htmlContent, true); // true = HTML
55+
56+
mailSender.send(message);
57+
log.info("HTML 이메일 발송 성공: {}", to);
58+
} catch (MessagingException | UnsupportedEncodingException e) {
59+
log.error("HTML 이메일 발송 실패: {}", to, e);
60+
throw new ServiceException("500-2", "HTML 이메일 발송에 실패했습니다.");
61+
}
62+
}
63+
64+
/**
65+
* 인증번호 이메일 발송 (멘토 회원가입용)
66+
*/
67+
public void sendVerificationCode(String to, String verificationCode) {
68+
String subject = "[FiveLogic] 멘토 회원가입 인증번호";
69+
String htmlContent = buildVerificationEmailHtml(verificationCode);
70+
sendHtmlEmail(to, subject, htmlContent);
71+
}
72+
73+
/**
74+
* 인증번호 이메일 HTML 템플릿
75+
*/
76+
private String buildVerificationEmailHtml(String verificationCode) {
77+
return """
78+
<!DOCTYPE html>
79+
<html>
80+
<head>
81+
<meta charset="UTF-8">
82+
<style>
83+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
84+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
85+
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
86+
.content { padding: 30px; background-color: #f9f9f9; }
87+
.code-box { background-color: #fff; border: 2px solid #4CAF50; padding: 20px; text-align: center; margin: 20px 0; }
88+
.code { font-size: 32px; font-weight: bold; color: #4CAF50; letter-spacing: 5px; }
89+
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
90+
</style>
91+
</head>
92+
<body>
93+
<div class="container">
94+
<div class="header">
95+
<h1>JobMate 멘토 회원가입</h1>
96+
</div>
97+
<div class="content">
98+
<h2>인증번호 확인</h2>
99+
<p>안녕하세요. FiveLogic입니다.</p>
100+
<p>멘토 회원가입을 위한 인증번호를 안내드립니다.</p>
101+
<p>아래 인증번호를 입력하여 회원가입을 완료해주세요.</p>
102+
103+
<div class="code-box">
104+
<div class="code">%s</div>
105+
</div>
106+
107+
<p><strong>※ 인증번호는 5분간 유효합니다.</strong></p>
108+
<p>본인이 요청하지 않은 경우, 이 메일을 무시하셔도 됩니다.</p>
109+
</div>
110+
<div class="footer">
111+
<p>© 2025 FiveLogic. All rights reserved.</p>
112+
</div>
113+
</div>
114+
</body>
115+
</html>
116+
""".formatted(verificationCode);
117+
}
118+
}

back/src/main/java/com/back/domain/member/member/service/MemberStorage.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.back.domain.member.member.entity.Member;
44
import com.back.domain.member.member.error.MemberErrorCode;
5+
import com.back.domain.member.member.repository.MemberRepository;
56
import com.back.domain.member.mentee.entity.Mentee;
67
import com.back.domain.member.mentee.repository.MenteeRepository;
78
import com.back.domain.member.mentor.entity.Mentor;
@@ -13,7 +14,7 @@
1314
@Component
1415
@RequiredArgsConstructor
1516
public class MemberStorage {
16-
17+
private final MemberRepository memberRepository;
1718
private final MentorRepository mentorRepository;
1819
private final MenteeRepository menteeRepository;
1920

@@ -39,4 +40,9 @@ public Mentee findMenteeByMember(Member member) {
3940
public boolean existsMentorById(Long mentorId) {
4041
return mentorRepository.existsById(mentorId);
4142
}
43+
44+
public Member findMemberByEmail(String email) {
45+
return memberRepository.findByEmail(email)
46+
.orElseThrow(() -> new ServiceException(MemberErrorCode.NOT_FOUND_MEMBER));
47+
}
4248
}

back/src/main/java/com/back/domain/member/member/verification/EmailVerificationService.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,42 @@
11
package com.back.domain.member.member.verification;
22

3+
import com.back.domain.member.member.email.EmailService;
34
import com.back.global.exception.ServiceException;
4-
import lombok.RequiredArgsConstructor;
55
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.beans.factory.annotation.Autowired;
67
import org.springframework.stereotype.Service;
78

89
import java.security.SecureRandom;
910
import java.time.Duration;
1011
import java.util.Optional;
1112

1213
@Service
13-
@RequiredArgsConstructor
1414
@Slf4j
1515
public class EmailVerificationService {
1616
private final VerificationCodeStore codeStore;
17+
private final Optional<EmailService> emailService;
1718
private static final Duration CODE_TTL = Duration.ofMinutes(5);
1819
private static final SecureRandom random = new SecureRandom();
1920

21+
public EmailVerificationService(VerificationCodeStore codeStore,
22+
@Autowired(required = false) EmailService emailService) {
23+
this.codeStore = codeStore;
24+
this.emailService = Optional.ofNullable(emailService);
25+
}
26+
2027
public String generateAndSendCode(String email) {
2128
// 6자리 랜덤 코드 생성
2229
String code = String.format("%06d", random.nextInt(1000000));
2330

2431
// 저장
2532
codeStore.saveCode(email, code, CODE_TTL);
2633

27-
// TODO: 실제 이메일 발송 로직 추가 (JavaMailSender)
28-
log.info("Generated verification code for {}: {}", email, code);
34+
// 이메일 발송 (EmailService가 있을 때만)
35+
emailService.ifPresentOrElse(
36+
service -> service.sendVerificationCode(email, code),
37+
() -> log.info("EmailService not available - verification code: {}", code)
38+
);
39+
log.info("Generated and sent verification code for {}: {}", email, code);
2940

3041
return code; // 테스트용으로 반환 (실제로는 이메일로만 전송)
3142
}
@@ -45,4 +56,4 @@ public void verifyCode(String email, String inputCode) {
4556
codeStore.deleteCode(email);
4657
log.info("Email verification successful for: {}", email);
4758
}
48-
}
59+
}

back/src/main/java/com/back/domain/mentoring/mentoring/controller/ReviewController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class ReviewController {
2525
private final ReviewService reviewService;
2626

2727
@GetMapping("/mentorings/{mentoringId}/reviews")
28-
@Operation(summary = "멘토링 리뷰 조회", description = "멘토링 리뷰 목록을 조회합니다.")
28+
@Operation(summary = "멘토링 리뷰 목록 조회", description = "멘토링 리뷰 목록을 조회합니다.")
2929
public RsData<ReviewPagingResponse> getReviews(
3030
@PathVariable Long mentoringId,
3131
@RequestParam(defaultValue = "0") int page,

back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringDetailDto.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public record MentoringDetailDto(
1717
String bio,
1818
@Schema(description = "멘토링 썸네일")
1919
String thumb,
20+
@Schema(description = "멘토링 평점")
21+
Double rating,
2022
@Schema(description = "생성일")
2123
LocalDateTime createDate,
2224
@Schema(description = "수정일")
@@ -29,6 +31,7 @@ public static MentoringDetailDto from(Mentoring mentoring) {
2931
mentoring.getTagNames(),
3032
mentoring.getBio(),
3133
mentoring.getThumb(),
34+
mentoring.getRating(),
3235
mentoring.getCreateDate(),
3336
mentoring.getModifyDate()
3437
);

back/src/main/java/com/back/domain/mentoring/mentoring/dto/request/MentoringRequest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
import java.util.List;
88

99
public record MentoringRequest(
10-
@Schema(description = "멘토링 제목")
10+
@Schema(description = "멘토링 제목", example = "title")
1111
@NotNull @Size(max = 100)
1212
String title,
1313

1414
@Schema(description = "멘토링 태그", example = "[\"Java\", \"Spring\"]")
1515
List<String> tags,
1616

17-
@Schema(description = "멘토링 소개")
17+
@Schema(description = "멘토링 소개", example = "bio")
1818
@NotNull
1919
String bio,
2020

21-
@Schema(description = "멘토링 썸네일")
21+
@Schema(description = "멘토링 썸네일", example = "test.png")
2222
String thumb
2323
) {
2424
}

0 commit comments

Comments
 (0)