Skip to content

Commit c79dd2a

Browse files
authored
feat: 설문조사 구현
close: #27 # 변경점 👍 1. 설문조사를 위한 api를 구현했습니다. 2. SMTP를 활용해서 설문조사를 작성하면 비동기로 이메일이 전송됩니다. # 사진 <img width="1169" height="365" alt="스크린샷 2025-12-01 오후 5 49 04" src="https://github.com/user-attachments/assets/d77c6fa8-a9bc-4c31-9fa4-69feaa8456b4" /> # 고민되는 부분 설문조사를 내용을 일단 DB에 저장하기는 하는데, 이걸 계속 남겨둬야할지 스케줄링으로 삭제해야할지 고민입니다. (개인적인 생각으로는 그래도 남겨두는게 좋지 않을지..)
1 parent ef0362a commit c79dd2a

File tree

14 files changed

+374
-0
lines changed

14 files changed

+374
-0
lines changed

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ dependencies {
6262

6363
// aws
6464
implementation 'software.amazon.awssdk:s3:2.20.0'
65+
66+
// smtp
67+
implementation 'org.springframework.boot:spring-boot-starter-mail'
68+
69+
// template engine for mail
70+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
71+
6572
}
6673

6774
tasks.named('test') {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package apptive.team5.config;
2+
3+
import apptive.team5.global.AsyncExceptionHandler;
4+
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.scheduling.annotation.AsyncConfigurer;
8+
import org.springframework.scheduling.annotation.EnableAsync;
9+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
10+
11+
import java.util.concurrent.Executor;
12+
import java.util.concurrent.ThreadPoolExecutor;
13+
14+
@EnableAsync
15+
@Configuration
16+
public class AsyncConfig implements AsyncConfigurer {
17+
18+
@Bean("surveyMailSend")
19+
public Executor surveyMailExecutor() {
20+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
21+
executor.setCorePoolSize(10);
22+
executor.setMaxPoolSize(50);
23+
executor.setQueueCapacity(1000);
24+
executor.setThreadNamePrefix("SurveyMail-");
25+
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
26+
executor.initialize();
27+
return executor;
28+
}
29+
30+
@Override
31+
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
32+
return new AsyncExceptionHandler();
33+
}
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package apptive.team5.global;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
import java.lang.reflect.Method;
8+
9+
@Configuration
10+
@Slf4j
11+
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
12+
13+
@Override
14+
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
15+
log.info("예외 메서드 = {}, 예외 메세지 {}", method.getName(), ex.getMessage());
16+
17+
}
18+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package apptive.team5.mail.service;
2+
3+
import jakarta.mail.internet.MimeMessage;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.mail.javamail.JavaMailSender;
7+
import org.springframework.mail.javamail.MimeMessageHelper;
8+
import org.springframework.scheduling.annotation.Async;
9+
import org.springframework.stereotype.Service;
10+
import org.thymeleaf.context.Context;
11+
import org.thymeleaf.spring6.SpringTemplateEngine;
12+
13+
@Service
14+
@RequiredArgsConstructor
15+
public class MailService {
16+
17+
private final JavaMailSender javaMailSender;
18+
private final SpringTemplateEngine templateEngine;
19+
@Value("${spring.mail.survey.email}")
20+
private String surveySubscribeEmail;
21+
@Value("${spring.mail.username}")
22+
private String senderEmail;
23+
24+
@Async("surveyMailSend")
25+
public void sendSurveyMailMessage(String content) {
26+
27+
try {
28+
29+
MimeMessage message = javaMailSender.createMimeMessage();
30+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
31+
helper.setTo(surveySubscribeEmail);
32+
helper.setSubject("KillingPart 설문조사가 도착했습니다.");
33+
helper.setFrom(senderEmail, "KillingPart");
34+
helper.setText(setContext(content), true);
35+
36+
javaMailSender.send(message);
37+
38+
} catch (Exception e) {
39+
throw new RuntimeException(e);
40+
}
41+
42+
}
43+
44+
public String setContext(String content) {
45+
46+
Context context = new Context();
47+
context.setVariable("content", content);
48+
return templateEngine.process("survey", context);
49+
50+
}
51+
}
52+
53+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package apptive.team5.survey.controller;
2+
3+
import apptive.team5.mail.service.MailService;
4+
import apptive.team5.survey.domain.SurveyEntity;
5+
import apptive.team5.survey.dto.SurveyCreateRequestDto;
6+
import apptive.team5.survey.service.SurveyService;
7+
import jakarta.validation.Valid;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@RestController
18+
@RequestMapping("/api/surveys")
19+
@RequiredArgsConstructor
20+
public class SurveyController {
21+
22+
private final SurveyService surveyService;
23+
private final MailService mailService;
24+
25+
@PostMapping
26+
public ResponseEntity<Void> createSurvey(@AuthenticationPrincipal Long userId,
27+
@Valid @RequestBody SurveyCreateRequestDto surveyCreateRequestDto) {
28+
29+
SurveyEntity survey = surveyService.save(surveyCreateRequestDto, userId);
30+
mailService.sendSurveyMailMessage(survey.getContent());
31+
32+
return ResponseEntity.status(HttpStatus.CREATED).build();
33+
34+
}
35+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package apptive.team5.survey.domain;
2+
3+
import apptive.team5.user.domain.UserEntity;
4+
import jakarta.persistence.*;
5+
import lombok.AccessLevel;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Entity
10+
@Getter
11+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12+
public class SurveyEntity {
13+
14+
@Id
15+
@GeneratedValue(strategy = GenerationType.IDENTITY)
16+
private Long id;
17+
18+
@Column(columnDefinition = "TEXT", nullable = false)
19+
private String content;
20+
21+
@ManyToOne(optional = false, fetch = FetchType.LAZY)
22+
private UserEntity user;
23+
24+
public SurveyEntity(String content, UserEntity user) {
25+
this.content = content;
26+
this.user = user;
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package apptive.team5.survey.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
6+
public record SurveyCreateRequestDto(
7+
8+
@NotBlank(message = "빈 값은 입력이 불가능합니다.")
9+
@Size(min = 1, max = 1000, message = "1자 이상 1000자 이하로 작성 가능합니다.")
10+
String content
11+
) {
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package apptive.team5.survey.repository;
2+
3+
import apptive.team5.survey.domain.SurveyEntity;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
public interface SurveyRepository extends JpaRepository<SurveyEntity, Long> {
9+
10+
@Modifying(clearAutomatically = true)
11+
@Query("delete from SurveyEntity s where s.user.id = :userId")
12+
void deleteByUserId(Long userId);
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package apptive.team5.survey.service;
2+
3+
import apptive.team5.survey.domain.SurveyEntity;
4+
import apptive.team5.survey.repository.SurveyRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
@Service
10+
@Transactional
11+
@RequiredArgsConstructor
12+
public class SurveyLowService {
13+
14+
private final SurveyRepository surveyRepository;
15+
16+
public SurveyEntity save(SurveyEntity surveyEntity) {
17+
return surveyRepository.save(surveyEntity);
18+
}
19+
20+
public void deleteByUserId(Long userId) {
21+
surveyRepository.deleteByUserId(userId);
22+
}
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package apptive.team5.survey.service;
2+
3+
import apptive.team5.survey.domain.SurveyEntity;
4+
import apptive.team5.survey.dto.SurveyCreateRequestDto;
5+
import apptive.team5.user.domain.UserEntity;
6+
import apptive.team5.user.service.UserLowService;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
@Service
12+
@Transactional
13+
@RequiredArgsConstructor
14+
public class SurveyService {
15+
16+
private final SurveyLowService surveyLowService;
17+
private final UserLowService userLowService;
18+
19+
public SurveyEntity save(SurveyCreateRequestDto surveyCreateRequestDto, Long userId) {
20+
21+
UserEntity userEntity = userLowService.getReferenceById(userId);
22+
23+
return surveyLowService.save(new SurveyEntity(surveyCreateRequestDto.content(), userEntity));
24+
}
25+
26+
}

0 commit comments

Comments
 (0)