: 매일 저녁, 하루를 마무리하며 작성 !
: ⭕ 지식 위주, 학습한 것을 노트 정리한다고 생각하고 작성하면서 머리 속 흩어져있는 지식들을 정리 !
class Solution {
public int[] solution(String s) {
int[] answer = new int[s.length()];
for(int i=0;i<s.length();i++){
if(i !=0){
int idx = s.substring(0,i).lastIndexOf(s.charAt(i));
if(idx != -1){
answer[i] = i-idx;
}
else{
answer[i] = idx;
}
}
else{
answer[i] = -1;
}
}
return answer;
}
}
뉴스 피드 프로젝트 전체 구현 완료
// TestDB
package com.sparta.wildcard_newsfeed;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import com.sparta.wildcard_newsfeed.domain.liked.entity.ContentsTypeEnum;
import com.sparta.wildcard_newsfeed.domain.liked.entity.Liked;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.entity.UserRoleEnum;
import com.sparta.wildcard_newsfeed.domain.user.entity.UserStatusEnum;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class TestDB {
private final InitService initService;
@PostConstruct
public void init() {
initService.dbInit1();
}
@Component
@Transactional
@RequiredArgsConstructor
static class InitService {
private final EntityManager em;
@Autowired
PasswordEncoder passwordEncoder;
public void dbInit1() {
// 유저 1
User user = User.builder()
.usercode("testid1234")
.password(passwordEncoder.encode("currentPWD999!"))
.name("B15_user1")
.email("test@naver.com")
.introduce("B15_user1의 하고 싶은 말")
.userRoleEnum(UserRoleEnum.USER)
.userStatus(UserStatusEnum.UNAUTHORIZED)
.authUserAt(LocalDateTime.now())
.userRoleEnum(UserRoleEnum.USER)
.build();
save(user);
// 유저 2
User user2 = User.builder()
.usercode("testid5678")
.password(passwordEncoder.encode("currentPWD999!"))
.name("B15_user2")
.email("test2@naver.com")
.introduce("B15_user2의 하고 싶은 말")
.userRoleEnum(UserRoleEnum.USER)
.userStatus(UserStatusEnum.UNAUTHORIZED)
.authUserAt(LocalDateTime.now())
.userRoleEnum(UserRoleEnum.USER)
.build();
save(user2);
List<Post> postList = new ArrayList<>();
// 유저 1의 게시물
initPosts(user, postList);
// 유저 2의 게시물
initPosts(user2, postList);
savePosts(postList);
List<Comment> commentList = new ArrayList<>();
// 유저 1의 댓글
initComments(user, postList, commentList);
// 유저 2의 댓글
initComments(user2, postList, commentList);
saveComments(commentList);
// 사용자 10명을 추가하고 50% 확률로 게시물에 좋아요 추가
addLikeToPost(postList);
}
private void addLikeToPost(List<Post> postList) {
List<User> userList = createUsers();
for (User user : userList) {
for (Post post : postList) {
if (Math.random() < 0.5) {
post.setLikeCount(post.getLikeCount() + 1);
Liked liked = new Liked(user, post.getId(), ContentsTypeEnum.POST);
em.persist(post);
em.persist(liked);
}
}
}
}
private List<User> createUsers() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = User.builder()
.usercode("dummyUser" + i)
.password(passwordEncoder.encode("currentPWD999!"))
.name("dummyUser" + i)
.email("test@naver.com")
.introduce("dummyUser" + i)
.userRoleEnum(UserRoleEnum.USER)
.userStatus(UserStatusEnum.UNAUTHORIZED)
.authUserAt(LocalDateTime.now())
.userRoleEnum(UserRoleEnum.USER)
.build();
save(user);
userList.add(user);
}
return userList;
}
private void initPosts(User user, List<Post> postList) {
for (int i = 0; i < 33; i++) {
Post post = new Post();
post.setUser(user);
post.setTitle("테스트 제목" + i);
post.setContent("테스트 내용" + i);
post.setLikeCount(0L);
post.setTestDateTime();
postList.add(post);
}
}
private void initComments(User user, List<Post> postList, List<Comment> commentList) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
Comment comment = Comment.builder()
.content(user.getName() + "의 게시물" + i + "의 댓글" + j)
.user(user)
.post(postList.get(i))
.likeCount(0L)
.build();
comment.testDataInit();
commentList.add(comment);
}
}
}
public void save(Object... objects) {
for (Object object : objects) {
em.persist(object);
}
}
public void savePosts(List<Post> list) {
for (Post post : list) {
em.persist(post);
}
}
public void saveComments(List<Comment> list) {
for (Comment comment : list) {
em.persist(comment);
}
}
}
}
comfig
// JpaAuditing
package com.sparta.wildcard_newsfeed.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaAuditing {
}
// MailConfig
package com.sparta.wildcard_newsfeed.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
@Configuration
public class MailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());
return mailSender;
}
private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
return properties;
}
}
// S3Config
package com.sparta.wildcard_newsfeed.config;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
// SwaggerConfiguration
package com.sparta.wildcard_newsfeed.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfiguration {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components())
.info(apiInfo());
}
public Info apiInfo() {
return new Info()
.title("WildCard API")
.description("WildCard News Feed")
.version("1.0.0");
}
}
// SwaggerConstants
package com.sparta.wildcard_newsfeed.config;
public class SwaggerConstants {
public static final String[] SWAGGER_PATTERNS = {
"/swagger-ui/**", "/swagger-resources/**", "/swagger-ui.html", "/v3/api-docs",
"/v3/api-docs/**", "/webjars/**"
};
}
// WebSecurityConfig
package com.sparta.wildcard_newsfeed.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.security.AuthenticationUserService;
import com.sparta.wildcard_newsfeed.security.jwt.*;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import static com.sparta.wildcard_newsfeed.config.SwaggerConstants.SWAGGER_PATTERNS;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final AuthenticationConfiguration authenticationConfiguration;
private final AuthenticationUserService authenticationUserService;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
private final JwtLogoutHandler jwtLogoutHandler;
private final UserRepository userRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(objectMapper, jwtUtil, userRepository);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, authenticationUserService, userRepository);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.formLogin(AbstractHttpConfigurer::disable);
http.httpBasic(AbstractHttpConfigurer::disable);
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(SWAGGER_PATTERNS).permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/auth/reissue").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/user/signup").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/user/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/post/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/post/page").permitAll() //게시글 페이지는 로그인x
.anyRequest().authenticated()
);
//security filter
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
//로그아웃
http.logout(logout ->
logout.logoutUrl("/api/v1/user/logout")
.addLogoutHandler(jwtLogoutHandler)
.logoutSuccessHandler(jwtLogoutSuccessHandler)
);
//예외 검증
http.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(jwtAuthenticationEntryPoint)
);
return http.build();
}
}
domain
// CommentController
package com.sparta.wildcard_newsfeed.domain.comment.controller;
import com.sparta.wildcard_newsfeed.domain.comment.dto.CommentRequestDto;
import com.sparta.wildcard_newsfeed.domain.comment.dto.CommentResponseDto;
import com.sparta.wildcard_newsfeed.domain.comment.service.CommentService;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/post/{postId}/comment")
@Tag(name = "Comment 컨트롤러", description = "Comment API")
public class CommentController {
private final CommentService commentService;
// 댓글 추가
@PostMapping
@Operation(summary = "댓글 등록")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "댓글 등록 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<CommentResponseDto>> addComment(
@AuthenticationPrincipal AuthenticationUser user,
@PathVariable(name = "postId") long postId,
@RequestBody CommentRequestDto commentRequestDto
) {
CommentResponseDto commentResponseDto = commentService.addComment(postId, commentRequestDto, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<CommentResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("댓글 등록 성공")
.data(commentResponseDto)
.build());
}
@PutMapping("{commentId}")
@Operation(summary = "댓글 수정")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "댓글 수정 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<CommentResponseDto>> updateComment(
@AuthenticationPrincipal AuthenticationUser user,
@Valid @RequestBody CommentRequestDto commentRequestDto,
@PathVariable(name = "postId") Long postId,
@PathVariable(name = "commentId") long commentId
) {
CommentResponseDto commentResponseDto = commentService.updateComment(postId, commentId, commentRequestDto, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<CommentResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("댓글 수정 성공")
.data(commentResponseDto)
.build());
}
@DeleteMapping("{commentId}")
@Operation(summary = "댓글 삭제")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "댓글 삭제 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<CommentResponseDto>> deleteComment(
@AuthenticationPrincipal AuthenticationUser user,
@PathVariable(name = "postId") long postId,
@PathVariable(name = "commentId") long commentId
) {
commentService.deleteComment(postId, commentId, user.getUsername());
return ResponseEntity.ok()
.body(CommonResponseDto.<CommentResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("댓글 삭제 성공")
.build());
}
}
// CommentRequestDto
package com.sparta.wildcard_newsfeed.domain.comment.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class CommentRequestDto {
@NotBlank(message = "댓글 내용은 필수 입력 값입니다.")
@Schema(description = "댓글 내용", example = "내용")
private String content;
public CommentRequestDto(String content) {
this.content = content;
}
}
// CommentResponseDto
package com.sparta.wildcard_newsfeed.domain.comment.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class CommentResponseDto {
private Long postId;
private Long id;
private String content;
private String username;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
private Long likeCount;
public CommentResponseDto(Comment comment) {
this.id = comment.getId();
this.content = comment.getContent();
this.username = comment.getUser().getName();
this.postId = comment.getPost().getId();
this.createdAt = comment.getCreatedAt();
this.updatedAt = comment.getUpdatedAt();
this.likeCount = comment.getLikeCount();
}
}
// PostWithCommentsResponseDto
package com.sparta.wildcard_newsfeed.domain.comment.dto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostResponseDto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class PostWithCommentsResponseDto {
private PostResponseDto post;
private List<CommentResponseDto> comments;
public PostWithCommentsResponseDto(PostResponseDto post, List<CommentResponseDto> comments) {
this.post = post;
this.comments = comments;
}
}
// Comment
package com.sparta.wildcard_newsfeed.domain.comment.entity;
import com.sparta.wildcard_newsfeed.domain.common.TimeStampEntity;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Comment extends TimeStampEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String content;
@Setter
private Long likeCount;
public Comment(String content, User user, Post post) {
this.content = content;
this.user = user;
this.post = post;
this.likeCount = 0L;
}
public void update(String comment) {
this.content = comment;
}
public void testDataInit() {
super.setDateTimeInit();
}
}
// CommentRepository
package com.sparta.wildcard_newsfeed.domain.comment.repository;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPostId(long postId);
}
// CommentService
package com.sparta.wildcard_newsfeed.domain.comment.service;
import com.sparta.wildcard_newsfeed.domain.comment.dto.CommentRequestDto;
import com.sparta.wildcard_newsfeed.domain.comment.dto.CommentResponseDto;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import com.sparta.wildcard_newsfeed.domain.comment.repository.CommentRepository;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import com.sparta.wildcard_newsfeed.domain.post.repository.PostRepository;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class CommentService {
private final PostRepository postRepository;
private final CommentRepository commentRepository;
private final UserRepository userRepository;
@Transactional
public CommentResponseDto addComment(long postId, CommentRequestDto request, AuthenticationUser user) {
User byUsercode = userRepository.findByUsercode(user.getUsername())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
// DB에 게시물이 존재하지 않는 경우
Post post = findPostById(postId);
Comment comment = commentRepository.save(new Comment(request.getContent(), byUsercode, post));
return new CommentResponseDto(comment);
}
public CommentResponseDto updateComment(long postId, long commentId, CommentRequestDto request, AuthenticationUser user) {
// DB에 게시물이 존재하지 않는 경우
findPostById(postId);
// 해당 댓글이 DB에 존재하지 않는 경우
Comment comment = findCommentById(commentId);
// 작성자가 동일하지 않는 경우
if (!Objects.equals(comment.getUser().getUsercode(), user.getUsername())) {
throw new IllegalArgumentException("작성자만 수정할 수 있습니다.");
}
comment.update(request.getContent());
commentRepository.save(comment);
return new CommentResponseDto(comment);
}
public void deleteComment(long postId, long commentId, String username) {
// DB에 게시물이 존재하지 않는 경우
findPostById(postId);
// 해당 댓글이 DB에 존재하지 않는 경우
Comment comment = findCommentById(commentId);
// 작성자가 동일하지 않는 경우
if (!Objects.equals(comment.getUser().getUsercode(), username)) {
throw new IllegalArgumentException("작성자만 삭제할 수 있습니다.");
}
commentRepository.delete(comment);
}
public List<CommentResponseDto> findAllCommentsByPostId(long postId) {
// 해당 postId와 연관된 댓글을 조회하는 로직 구현
List<Comment> comments = commentRepository.findByPostId(postId);
return comments.stream()
.map(CommentResponseDto::new)
.collect(Collectors.toList());
}
public Comment findCommentById(Long commentId) {
return commentRepository.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("해당 댓글이 존재하지 않습니다."));
}
private Post findPostById(long id) {
return postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다."));
}
public List<CommentResponseDto> findAll() {
List<Comment> commentlist = commentRepository.findAll();
return commentlist.stream()
.sorted(Comparator.comparing(Comment::getCreatedAt).reversed())
.map(CommentResponseDto::new)
.toList();
}
}
// ErrorResponseDto
package com.sparta.wildcard_newsfeed.domain.common.error;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ErrorResponseDto {
private Integer statusCode;
private Object message;
}
// CommonResponseDto
package com.sparta.wildcard_newsfeed.domain.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class CommonResponseDto<T> {
@Schema(description = "상태코드", example = "200")
private Integer statusCode;
@Schema(description = "메세지", example = "성공")
private String message;
@Schema(description = "응답 데이터")
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
}
// TimeStampEntity
package com.sparta.wildcard_newsfeed.domain.common;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeStampEntity {
@Column(updatable = false)
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
protected void setDateTimeInit() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}
// FileService
package com.sparta.wildcard_newsfeed.domain.file.service;
import com.sparta.wildcard_newsfeed.exception.customexception.FileException;
import com.sparta.wildcard_newsfeed.util.FileUtils;
import com.sparta.wildcard_newsfeed.util.S3FileUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {
private final FileUtils fileUtils;
private final S3FileUtils s3FileUtils;
public File saveFileToLocal(MultipartFile multipartFile, String uuidFileName) {
String localLocation = fileUtils.getAbsoluteUploadFolder();
String fullFilePath = localLocation + uuidFileName;
File saveFile = new File(fullFilePath);
try {
multipartFile.transferTo(saveFile);
} catch (IOException e) {
throw new FileException("local에 파일을 저장할 수 없습니다.", e);
}
return saveFile;
}
public String uploadFileToS3(MultipartFile multipartFile) {
String uuidFileName = fileUtils.createUuidFileName(multipartFile.getOriginalFilename());
File savedFile = saveFileToLocal(multipartFile, uuidFileName);
try {
String s3Url = s3FileUtils.uploadFile(uuidFileName, savedFile);
if (!deleteFileFromLocal(savedFile)) {
log.info("S3 업로드 후 로컬 파일 삭제 실패");
}
return s3Url;
} catch (Exception e) {
throw new FileException("S3에 파일 업로드 실패", e);
}
}
public boolean deleteFileFromLocal(File savedFile) {
if (!savedFile.delete()) {
log.error("S3에 파일 저장 후 local 파일 삭제 실패");
return false;
}
return true;
}
}
// LikedController
package com.sparta.wildcard_newsfeed.domain.liked.controller;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.liked.dto.LikedRequestDto;
import com.sparta.wildcard_newsfeed.domain.liked.dto.LikedResponseDto;
import com.sparta.wildcard_newsfeed.domain.liked.service.LikedService;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/like")
@Tag(name = "Liked 컨트롤러", description = "Liked API")
public class LikedController {
private final LikedService likedService;
@PostMapping
@Operation(summary = "좋아요 추가")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "좋아요 추가 성공")
})
public ResponseEntity<CommonResponseDto<LikedResponseDto>> addLike(
@AuthenticationPrincipal AuthenticationUser user,
@RequestBody LikedRequestDto requestDto
) {
LikedResponseDto responseDto = likedService.addLike(requestDto, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<LikedResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("좋아요 추가 성공")
.data(responseDto)
.build());
}
@DeleteMapping
@Operation(summary = "좋아요 제거")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "좋아요 제거 성공")
})
public ResponseEntity<CommonResponseDto<LikedResponseDto>> removeLike(
@AuthenticationPrincipal AuthenticationUser user,
@RequestBody LikedRequestDto requestDto
) {
likedService.removeLike(requestDto, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<LikedResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("좋아요 제거 성공")
.build());
}
}
// LikedRequestDto
package com.sparta.wildcard_newsfeed.domain.liked.dto;
import com.sparta.wildcard_newsfeed.domain.liked.entity.ContentsTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class LikedRequestDto {
@Schema(description = "컨텐츠 ID", example = "2")
private Long contentsId;
@Schema(description = "컨텐츠 타입 (POST || COMMENT)", example = "POST")
private ContentsTypeEnum contentsType;
}
// LikedResponseDto
package com.sparta.wildcard_newsfeed.domain.liked.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.sparta.wildcard_newsfeed.domain.liked.entity.ContentsTypeEnum;
import com.sparta.wildcard_newsfeed.domain.liked.entity.Liked;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class LikedResponseDto {
private Long likeId;
private Long contentsId;
private ContentsTypeEnum contentsType;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
public LikedResponseDto(Liked liked) {
this.likeId = liked.getId();
this.contentsId = liked.getContentsId();
this.contentsType = liked.getContentsType();
this.createdAt = liked.getCreatedAt();
this.updatedAt = liked.getUpdatedAt();
}
}
// ContentsTypeEnum
package com.sparta.wildcard_newsfeed.domain.liked.entity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum ContentsTypeEnum {
COMMENT("comment"),
POST("post");
private final String contentsType;
}
// Liked
package com.sparta.wildcard_newsfeed.domain.liked.entity;
import com.sparta.wildcard_newsfeed.domain.common.TimeStampEntity;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@NoArgsConstructor
public class Liked extends TimeStampEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "contents_id", nullable = false)
private Long contentsId;
@Enumerated(EnumType.STRING)
@Column(name = "content_type", nullable = false)
private ContentsTypeEnum contentsType;
public Liked(User user, Long contentsId, ContentsTypeEnum contentsType) {
this.user = user;
this.contentsId = contentsId;
this.contentsType = contentsType;
}
}
// LikedRepository
package com.sparta.wildcard_newsfeed.domain.liked.repository;
import com.sparta.wildcard_newsfeed.domain.liked.entity.ContentsTypeEnum;
import com.sparta.wildcard_newsfeed.domain.liked.entity.Liked;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface LikedRepository extends JpaRepository<Liked, Long> {
Optional<Liked> findByUserIdAndContentsIdAndContentsType(Long user_id, Long contentsId, ContentsTypeEnum contentsType);
}
// LikedService
package com.sparta.wildcard_newsfeed.domain.liked.service;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import com.sparta.wildcard_newsfeed.domain.comment.repository.CommentRepository;
import com.sparta.wildcard_newsfeed.domain.liked.dto.LikedRequestDto;
import com.sparta.wildcard_newsfeed.domain.liked.dto.LikedResponseDto;
import com.sparta.wildcard_newsfeed.domain.liked.entity.ContentsTypeEnum;
import com.sparta.wildcard_newsfeed.domain.liked.entity.Liked;
import com.sparta.wildcard_newsfeed.domain.liked.repository.LikedRepository;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import com.sparta.wildcard_newsfeed.domain.post.repository.PostRepository;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class LikedService {
private final LikedRepository likedRepository;
private final UserRepository userRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;
@Transactional
public LikedResponseDto addLike(LikedRequestDto requestDto, AuthenticationUser user) {
User currentUser = userRepository.findByUsercode(user.getUsername())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
Optional<Liked> existingLike = likedRepository.findByUserIdAndContentsIdAndContentsType(currentUser.getId(), requestDto.getContentsId(), requestDto.getContentsType());
if (existingLike.isPresent()) {
throw new IllegalArgumentException("이미 좋아요를 눌렀습니다.");
}
// 본인이 작성한 게시물이나 댓글에 좋아요를 남길 수 없습니다.
// POST
if (requestDto.getContentsType() == ContentsTypeEnum.POST) {
Post post = postRepository.findById(requestDto.getContentsId())
.orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다."));
if (post.getUser().getId().equals(currentUser.getId())) {
throw new IllegalArgumentException("본인이 작성한 게시물에는 좋아요를 남길 수 없습니다.");
}
post.setLikeCount(post.getLikeCount() + 1); //변경 감지 -> 따로 save 필요없다.
}
//COMMENT
else if (requestDto.getContentsType() == ContentsTypeEnum.COMMENT) {
Comment comment = commentRepository.findById(requestDto.getContentsId())
.orElseThrow(() -> new IllegalArgumentException("댓글을 찾을 수 없습니다."));
if (comment.getUser().getId().equals(currentUser.getId())) {
throw new IllegalArgumentException("본인이 작성한 댓글에는 좋아요를 남길 수 없습니다.");
}
comment.setLikeCount(comment.getLikeCount() + 1);
}
Liked liked = new Liked(currentUser, requestDto.getContentsId(), requestDto.getContentsType());
likedRepository.save(liked);
return new LikedResponseDto(liked);
}
@Transactional
public void removeLike(LikedRequestDto requestDto, AuthenticationUser user) {
User currentUser = userRepository.findByUsercode(user.getUsername())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
Liked existingLike = likedRepository.findByUserIdAndContentsIdAndContentsType(currentUser.getId(), requestDto.getContentsId(), requestDto.getContentsType())
.orElseThrow(() -> new IllegalArgumentException("좋아요가 존재하지 않습니다."));
// 좋아요 수 감소
// POST
if (requestDto.getContentsType() == ContentsTypeEnum.POST) {
Post post = postRepository.findById(requestDto.getContentsId())
.orElseThrow(() -> new IllegalArgumentException("게시물을 찾을 수 없습니다."));
post.setLikeCount(post.getLikeCount() - 1);
}
// COMMENT
else if (requestDto.getContentsType() == ContentsTypeEnum.COMMENT) {
Comment comment = commentRepository.findById(requestDto.getContentsId())
.orElseThrow(() -> new IllegalArgumentException("댓글을 찾을 수 없습니다."));
comment.setLikeCount(comment.getLikeCount() - 1);
}
likedRepository.delete(existingLike);
}
}
// PostController
package com.sparta.wildcard_newsfeed.domain.post.controller;
import com.sparta.wildcard_newsfeed.domain.comment.dto.CommentResponseDto;
import com.sparta.wildcard_newsfeed.domain.comment.dto.PostWithCommentsResponseDto;
import com.sparta.wildcard_newsfeed.domain.comment.service.CommentService;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostPageRequestDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostPageResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostRequestDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.repository.PostRepository;
import com.sparta.wildcard_newsfeed.domain.post.service.PostService;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/post")
@Tag(name = "Post 컨트롤러", description = "Post API")
public class PostController {
private final PostService postService;
private final CommentService commentService;
private final PostRepository postRepository;
// 게시물 등록
@PostMapping
@Operation(summary = "게시물 등록")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시물 등록 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<PostResponseDto>> addPost(
@AuthenticationPrincipal AuthenticationUser user,
@Valid @ModelAttribute PostRequestDto postRequestDto
) {
PostResponseDto postResponseDto = postService.addPost(postRequestDto, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<PostResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 등록 성공")
.data(postResponseDto)
.build());
}
// 게시물 전체 조회
@GetMapping
@Operation(summary = "게시물 전체 조회")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시물 전체 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<Object>> findAll() {
List<PostResponseDto> posts = postService.findAll();
return ResponseEntity.ok()
.body(CommonResponseDto.builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 전체 조회 성공")
.data(posts)
.build());
}
// // 게시물 단일 조회 + 해당 게시물에 달린 댓글 전체 조회
@GetMapping("/{postId}")
@Operation(summary = "게시물 단일 조회")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시물 단일 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<PostWithCommentsResponseDto>> findById(@PathVariable(name = "postId") long id) {
// 게시물 단일 조회
PostResponseDto post = postService.findById(id);
// 해당 게시물에 달린 댓글 전체 조회
List<CommentResponseDto> comments = commentService.findAllCommentsByPostId(id);
// Post와 Comments를 하나의 객체로 병합
PostWithCommentsResponseDto postWithCommentsResponse = new PostWithCommentsResponseDto(post, comments);
return ResponseEntity.ok()
.body(CommonResponseDto.<PostWithCommentsResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 단일 조회, 댓글 조회 성공")
.data(postWithCommentsResponse)
.build());
}
//게시물 수정
@PutMapping("/{postId}")
@Operation(summary = "게시물 수정")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시물 수정 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<PostResponseDto>> updatePost(
@AuthenticationPrincipal AuthenticationUser user,
@Valid @ModelAttribute PostRequestDto postRequestDto,
@PathVariable Long postId
) {
PostResponseDto postResponseDto = postService.updatePost(postRequestDto, postId, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<PostResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 수정 성공")
.data(postResponseDto)
.build());
}
//게시물 삭제
@DeleteMapping("/{postId}")
@Operation(summary = "게시물 삭제")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시물 삭제 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<PostResponseDto>> deletePost(
@AuthenticationPrincipal AuthenticationUser user,
@Valid @PathVariable Long postId
) {
postService.deletePost(postId, user);
return ResponseEntity.ok()
.body(CommonResponseDto.<PostResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 삭제 성공")
.build());
}
//페이지네이션
@PostMapping("/page")
@Operation(summary = "페이징")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "페이징 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<Page<PostPageResponseDto>>> getPostPage(@Valid @RequestBody PostPageRequestDto requestDto) {
Page<PostPageResponseDto> page = postService.getPostPage(requestDto);
return ResponseEntity.ok()
.body(CommonResponseDto.<Page<PostPageResponseDto>>builder()
.statusCode(HttpStatus.OK.value())
.message("게시물 페이지 조회 성공")
.data(page)
.build());
}
}
// PostPageRequestDto
package com.sparta.wildcard_newsfeed.domain.post.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@NoArgsConstructor
@ToString
public class PostPageRequestDto {
@Schema(description = "선택 페이지", example = "1")
@NotNull(message = "선택 페이지 필수 입력 값입니다.")
@Positive(message = "0이 아닌 양수만 가능합니다.")
private int page; // 현재 페이지 0부터 시작
@Schema(description = "게시글 수", example = "10")
@NotNull(message = "게시글 수 필수 입력 값입니다.")
private int size; // 한 페이지에 보이는 글 개수
@Schema(description = "정렬 기준", example = "CREATE")
@NotNull(message = "정렬 기준 필수 입력 값입니다.")
private String sortBy; // 생성일자 최신순 or 좋아요 많은 순
@Schema(description = "검색 기간 시작일", example = "2024-05-01")
private String firstDate; // 생성일자 최신순 or 좋아요 많은 순
@Schema(description = "검색 기간 마지막일", example = "2024-05-27")
private String lastDate; // 생성일자 최신순 or 좋아요 많은 순
/*
- **페이지네이션**
- 10개씩 페이지네이션하여, 각 페이지 당 뉴스피드 데이터가 10개씩 나오게 합니다.
- **정렬 기능**
- 생성일자 기준 최신순
- 좋아요 많은 순
- **기간별 검색 기능**
- 예) 2024.05.01 ~ 2024.05.27 동안 작성된 뉴스피드 게시물 검색
*/
}
// PostPageResponseDto
package com.sparta.wildcard_newsfeed.domain.post.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
// JPA -> DTO로 매핑할 때 호환이 잘 안되서 인터페이스로 받아야 한다고 한다
public interface PostPageResponseDto {
Long getPostId();
Long getUserId();
String getTitle();
String getContent();
String getName();
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime getCreatedAt();
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime getUpdatedAt();
Long getLikeCount();
}
// PostRequestDto
package com.sparta.wildcard_newsfeed.domain.post.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostRequestDto {
@Schema(description = "게시물 제목", example = "제목")
@NotBlank(message = "제목은 필수 입력 값입니다.")
private String title;
@Schema(description = "게시물 내용", example = "내용")
@NotBlank(message = "내용은 필수 입력 값입니다.")
private String content;
@Schema(description = "업로드 파일")
private List<MultipartFile> files;
}
// PostResponseDto
package com.sparta.wildcard_newsfeed.domain.post.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
public class PostResponseDto {
private Long id;
private String title;
private String content;
private String username;
private List<String> s3Urls;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
private Long likeCount;
public PostResponseDto(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.username = post.getUser().getName();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.likeCount = post.getLikeCount();
}
public PostResponseDto(Post post, List<String> s3Urls) {
this.id = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.username = post.getUser().getName();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.s3Urls = s3Urls;
this.likeCount = post.getLikeCount();
}
}
// Post
package com.sparta.wildcard_newsfeed.domain.post.entity;
import com.sparta.wildcard_newsfeed.domain.comment.entity.Comment;
import com.sparta.wildcard_newsfeed.domain.common.TimeStampEntity;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostRequestDto;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Getter
// Todo 배포시 여기 @Setter 삭제
@Setter
@Entity
@NoArgsConstructor
public class Post extends TimeStampEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@Setter
private Long likeCount;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostMedia> postMedias = new ArrayList<>();
public Post(PostRequestDto postRequestDto, User user) {
this.user = user;
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
this.likeCount = 0L;
}
public void update(PostRequestDto postRequestDto) {
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
}
public void setTestDateTime() {
super.setDateTimeInit();
}
}
// PostMedia
package com.sparta.wildcard_newsfeed.domain.post.entity;
import com.sparta.wildcard_newsfeed.domain.common.TimeStampEntity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Entity
@NoArgsConstructor
public class PostMedia extends TimeStampEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
private String url;
private String type;
}
// PostMediaRepository
package com.sparta.wildcard_newsfeed.domain.post.repository;
import com.sparta.wildcard_newsfeed.domain.post.entity.PostMedia;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostMediaRepository extends JpaRepository<PostMedia, Long> {
List<PostMedia> findByPostId(Long postId);
}
// PostRepository
package com.sparta.wildcard_newsfeed.domain.post.repository;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostPageResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PostRepository extends JpaRepository<Post, Long> {
@Query(value = "select p.id as postId, u.id as userId, p.title, p.content, u.name, " +
"p.created_at as createdAt, p.updated_at as updatedAt, " +
"IF(c.count is null, 0, c.count) as likeCount " +
"from (select id, count(*) as count " +
"from (select a.id, a.title from post a " +
"left join liked b on a.id = b.contents_id " +
"where b.content_type = 'POST') a " +
"group by a.id) c " +
"right join post p on c.id = p.id " +
"left join user u on u.id = p.user_id " +
"where p.created_at between :startDate AND :endDate ", nativeQuery = true)
Page<PostPageResponseDto> findPostPages(@Param("startDate") String startDate,
@Param("endDate") String endDate,
Pageable pageable);
}
// PostService
package com.sparta.wildcard_newsfeed.domain.post.service;
import com.sparta.wildcard_newsfeed.domain.file.service.FileService;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostPageRequestDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostPageResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostRequestDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.entity.Post;
import com.sparta.wildcard_newsfeed.domain.post.entity.PostMedia;
import com.sparta.wildcard_newsfeed.domain.post.repository.PostMediaRepository;
import com.sparta.wildcard_newsfeed.domain.post.repository.PostRepository;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.FileException;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import com.sparta.wildcard_newsfeed.util.FileUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final PostMediaRepository postMediaRepository;
private final FileService fileService;
private final FileUtils fileUtils;
@Transactional
public PostResponseDto addPost(PostRequestDto postRequestDto, AuthenticationUser user) {
User byUsercode = userRepository.findByUsercode(user.getUsername())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
Post post = new Post(postRequestDto, byUsercode);
postRepository.save(post);
List<PostMedia> postMediaList = createPostMediaList(postRequestDto, post);
postMediaRepository.saveAll(postMediaList);
List<String> s3Urls = getS3UrlsFromPostMediaList(postMediaList);
return new PostResponseDto(post, s3Urls);
}
public PostResponseDto findById(long id) {
Post post = findPostById(id);
List<String> s3Urls = getS3UrlsFromPostMediaList(postMediaRepository.findByPostId(id));
return new PostResponseDto(post, s3Urls);
}
public List<PostResponseDto> findAll() {
List<Post> postlist = postRepository.findAll();
return postlist.stream()
.sorted(Comparator.comparing(Post::getCreatedAt).reversed())
.map(PostResponseDto::new)
.toList();
}
@Transactional
public PostResponseDto updatePost(PostRequestDto postRequestDto, Long postId, AuthenticationUser user) {
Post post = findPostById(postId);
validateUser(post, user);
post.update(postRequestDto);
postRepository.save(post);
List<PostMedia> postMediaList = createPostMediaList(postRequestDto, post);
post.getPostMedias().clear();
postMediaRepository.saveAll(postMediaList);
List<String> s3Urls = getS3UrlsFromPostMediaList(postMediaList);
return new PostResponseDto(post, s3Urls);
}
@Transactional
public void deletePost(Long postId, AuthenticationUser user) {
Post post = findPostById(postId);
validateUser(post, user);
postRepository.delete(post);
}
private List<String> getS3UrlsFromPostMediaList(List<PostMedia> postMediaList) {
List<String> s3Urls = new ArrayList<>();
if (!postMediaList.isEmpty()) {
s3Urls = postMediaList.stream().map(PostMedia::getUrl).toList();
}
return s3Urls;
}
private List<PostMedia> createPostMediaList(PostRequestDto postRequestDto, Post post) {
List<PostMedia> postMediaList = new ArrayList<>();
if (!postRequestDto.getFiles().isEmpty()) {
if (postRequestDto.getFiles().size() > 5) {
throw new FileException("한 게시물당 최대 5개까지만 저장 가능합니다.");
}
fileUtils.validFile(postRequestDto.getFiles());
for (MultipartFile file : postRequestDto.getFiles()) {
String s3Url = fileService.uploadFileToS3(file);
PostMedia postMedia = new PostMedia();
postMedia.setUrl(s3Url);
postMedia.setPost(post);
postMedia.setType(fileUtils.extractExtension(file.getOriginalFilename()));
postMediaList.add(postMedia);
}
}
return postMediaList;
}
private Post findPostById(long id) {
return postRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다."));
}
private void validateUser(Post post, AuthenticationUser user) {
if (!post.getUser().getUsercode().equals(user.getUsername())) {
throw new IllegalArgumentException("작성자만 할 수 있습니다.");
}
}
@Transactional
public Page<PostPageResponseDto> getPostPage(PostPageRequestDto requestDto) {
log.info(requestDto.toString());
Sort.Direction direction = Sort.Direction.DESC; //ASC 오름차순 , DESC 내림차순
//- 생성일자 기준 최신 - 좋아요 많은 순
// --- 정렬 방식 ---
//CREATE or LIKED
String sortBy = "created_at";
if (requestDto.getSortBy().equals("CREATE")) {
sortBy = "createdAt";
} else if (requestDto.getSortBy().equals("LIKED")) {
sortBy = "likeCount";
} else
throw new IllegalArgumentException("정렬은 CREATE 또는 LIKED 만 입력 가능합니다.");
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(requestDto.getPage() - 1, requestDto.getSize(), sort);
Page<PostPageResponseDto> postList = null;
//---날짜 부분 ---
LocalDate lastDate = LocalDate.now();
LocalDate firstDate = LocalDate.parse("2000-01-01");
// null 이면 모든 날짜를 조회
if (requestDto.getLastDate() != null && requestDto.getFirstDate() != null) {
// 날짜 정보가 있으면 해당 날짜만 조회
try {
lastDate = LocalDate.parse(requestDto.getLastDate());
firstDate = LocalDate.parse(requestDto.getFirstDate());
} catch (Exception e) {
throw new IllegalArgumentException("날짜 포맷이 정상적이지 않습니다.");
}
}
postList = postRepository.findPostPages(firstDate.atStartOfDay().toString(), lastDate.atTime(LocalTime.MAX).toString(), pageable);
if (postList.getTotalElements() <= 0) {
log.error("페이지 없음");
throw new IllegalArgumentException("페이지가 존재하지 않습니다.");
}
if (postList.getTotalPages() < requestDto.getPage()) {
throw new IllegalArgumentException("유효한 페이지 번호가 아닙니다.");
}
return postList;
}
}
// TokenController
package com.sparta.wildcard_newsfeed.domain.token.controller;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.token.dto.TokenResponseDto;
import com.sparta.wildcard_newsfeed.domain.token.service.TokenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.sparta.wildcard_newsfeed.security.jwt.JwtConstants.ACCESS_TOKEN_HEADER;
import static com.sparta.wildcard_newsfeed.security.jwt.JwtConstants.REFRESH_TOKEN_HEADER;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
@Tag(name = "Token 컨트롤러", description = "Token API")
public class TokenController {
private final TokenService tokenService;
@PostMapping("/reissue")
@Operation(summary = "토큰 재발급")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "토큰 재발급 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto> refreshTokenReissue(HttpServletRequest request) {
log.info("access token 재발급");
String refreshTokenHeader = tokenService.validateTokenExpire(request);
TokenResponseDto responseDto = tokenService.getFindUser(refreshTokenHeader);
CommonResponseDto commonResponseDto = CommonResponseDto.builder()
.message(responseDto.getUsercode() + "님 재발급 성공")
.statusCode(HttpStatus.OK.value())
.build();
return ResponseEntity.ok()
.header(ACCESS_TOKEN_HEADER, responseDto.getAccessToken())
.header(REFRESH_TOKEN_HEADER, responseDto.getRefreshToken())
.body(commonResponseDto);
}
}
// TokenResponseDto
package com.sparta.wildcard_newsfeed.domain.token.dto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserResponseFromTokenDto;
import com.sparta.wildcard_newsfeed.security.jwt.dto.TokenDto;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class TokenResponseDto {
private String usercode;
private String accessToken;
private String refreshToken;
public static TokenResponseDto of(UserResponseFromTokenDto findUser, TokenDto tokenDto) {
return TokenResponseDto.builder()
.usercode(findUser.getUsercode())
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.build();
}
}
// TokenService
package com.sparta.wildcard_newsfeed.domain.token.service;
import com.sparta.wildcard_newsfeed.domain.token.dto.TokenResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserResponseFromTokenDto;
import com.sparta.wildcard_newsfeed.domain.user.service.UserService;
import com.sparta.wildcard_newsfeed.exception.customexception.TokenNotFoundException;
import com.sparta.wildcard_newsfeed.security.jwt.JwtUtil;
import com.sparta.wildcard_newsfeed.security.jwt.dto.TokenDto;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class TokenService {
private final JwtUtil jwtUtil;
private final UserService userService;
public String validateTokenExpire(HttpServletRequest request) {
String accessTokenHeader = jwtUtil.getAccessTokenFromHeader(request);
String refreshTokenHeader = jwtUtil.getRefreshTokenFromHeader(request);
//access 또는 refresh가 없는 경우
if (accessTokenHeader == null || refreshTokenHeader == null) {
throw new TokenNotFoundException("토큰을 찾을 수 없습니다.");
}
//refresh token이 유효하지 않은 경우
if (!jwtUtil.validateToken(request, refreshTokenHeader)) {
throw new TokenNotFoundException("유효하지 않은 토큰입니다.");
}
return refreshTokenHeader;
}
@Transactional
public TokenResponseDto getFindUser(String refreshTokenHeader) {
String usercode = jwtUtil.getUserInfoFromToken(refreshTokenHeader).getSubject();
UserResponseFromTokenDto findUserDto = userService.findByUsercode(usercode);
TokenDto tokenDto = jwtUtil.generateAccessTokenAndRefreshToken(findUserDto.getUsercode());
String refreshTokenValue = tokenDto.getRefreshToken().substring(7);
userService.updateRefreshToken(findUserDto.getUsercode(), refreshTokenValue);
return TokenResponseDto.of(findUserDto, tokenDto);
}
}
// EmailController
package com.sparta.wildcard_newsfeed.domain.user.controller;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.post.dto.PostResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserEmailRequestDto;
import com.sparta.wildcard_newsfeed.domain.user.service.UserService;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/email")
@Tag(name = "Email 컨트롤러", description = "email API")
public class EmailController {
private final UserService userService;
@PostMapping("/verify")
@Operation(summary = "이메일 인증")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "이메일 인증 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<PostResponseDto>> verifyEmailAuth(
@AuthenticationPrincipal AuthenticationUser loginUser,
@Valid @RequestBody UserEmailRequestDto requestDto
) {
userService.verifyAuthCode(loginUser, requestDto);
return ResponseEntity.ok()
.body(CommonResponseDto.<PostResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("이메일 인증 성공")
.build());
}
}
// UserController
package com.sparta.wildcard_newsfeed.domain.user.controller;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserRequestDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserSignupRequestDto;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserSignupResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.service.UserService;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/user")
@Tag(name = "User 컨트롤러", description = "user API")
public class UserController {
private final UserService userService;
@PostMapping("/signup")
@Operation(summary = "회원가입")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원가입 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<UserSignupResponseDto>> signup(@Valid @RequestBody UserSignupRequestDto requestDto) {
UserSignupResponseDto responseDto = userService.signup(requestDto);
return ResponseEntity.ok()
.body(CommonResponseDto.<UserSignupResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("회원가입 성공")
.data(responseDto)
.build());
}
@DeleteMapping("/resign")
@Operation(summary = "회원탈퇴")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "회원탈퇴 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<UserSignupResponseDto>> resign(
@AuthenticationPrincipal AuthenticationUser user,
@Valid @RequestBody Map<String, String> map
) {
userService.resign(user, map.get("password"));
return ResponseEntity.ok()
.body(CommonResponseDto.<UserSignupResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("회원탈퇴 성공")
.build());
}
@GetMapping("/{userId}")
@Operation(summary = "프로필 조회")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "프로필 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<UserResponseDto>> getUser(@PathVariable Long userId) {
UserResponseDto userResponseDto = userService.findById(userId);
return ResponseEntity.ok()
.body(CommonResponseDto.<UserResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("프로필 조회 성공")
.data(userResponseDto)
.build());
}
@PutMapping("/{userId}")
@Operation(summary = "프로필 수정")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "프로필 수정 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<UserResponseDto>> updateUser(
@AuthenticationPrincipal AuthenticationUser loginUser,
@PathVariable Long userId,
@Valid @RequestBody UserRequestDto requestDto
) {
UserResponseDto userResponseDto = userService.updateUser(loginUser, userId, requestDto);
return ResponseEntity.ok()
.body(CommonResponseDto.<UserResponseDto>builder()
.statusCode(HttpStatus.OK.value())
.message("프로필 수정 성공")
.data(userResponseDto)
.build());
}
@PostMapping("/{userId}/profile-image")
@Operation(summary = "프로필 사진 업로드")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "프로필 사진 업로드 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = CommonResponseDto.class)))
})
public ResponseEntity<CommonResponseDto<String>> uploadProfileImage(
@AuthenticationPrincipal AuthenticationUser loginUser,
@PathVariable Long userId,
@RequestParam MultipartFile file
) {
String savedS3Url = userService.uploadProfileImage(loginUser, userId, file);
return ResponseEntity.ok()
.body(CommonResponseDto.<String>builder()
.statusCode(HttpStatus.OK.value())
.message("프로필 사진 업로드 성공")
.data(savedS3Url)
.build());
}
}
// EmailHtmlConstant
package com.sparta.wildcard_newsfeed.domain.user.dto.emailtemplate;
public class EmailHtmlConstant {
private static final String HEADER_TITLE = "<span style=\"font-weight: bold;font-size: 48px;\">" +
"<span style=\"color:#ff8c00\">B15</span>\n" +
"</span>\n";
private static String TAIL = "<hr>\n" +
"<br>\n" +
"<span style=\"font-size:12px;\"> " +
" <br>감사합니다. <br> 오저먹 " +
"</span>\n";
public static String MAIL_BASIC_FORMAT = "<span style=\"font-family:Arial,sans-serif\">\n" +
HEADER_TITLE +
"%s" +
TAIL +
"</span>";
}
// EmailTemplate
package com.sparta.wildcard_newsfeed.domain.user.dto.emailtemplate;
import lombok.Getter;
import static com.sparta.wildcard_newsfeed.domain.user.dto.emailtemplate.EmailHtmlConstant.MAIL_BASIC_FORMAT;
@Getter
public enum EmailTemplate {
AUTH_EMAIL("오저먹 이메일 인증 요청입니다.", MAIL_BASIC_FORMAT.formatted(
" <br>\n" +
" <br>\n" +
" <span style=\"font-size:18px;\">안녕하세요, 오저먹 계정 생성을 환영합니다! <br>요청하신 인증코드는 아래와 같습니다 <br>\n" +
" <span style=\"font-size: 30px;color:#ff8c00;font-weight: bold\">%s</span>\n" +
" <br>\n" +
" <br>\n"
));
private String sub;
private String body;
EmailTemplate(String sub, String body) {
this.sub = sub;
this.body = body;
}
public String formatBody(String authKey) {
return body.formatted(authKey);
}
}
// EmailSendEvent
package com.sparta.wildcard_newsfeed.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
@Builder
public class EmailSendEvent {
private String subject;
private String body;
private String to;
public static EmailSendEvent of(String subject, String body, String to) {
return EmailSendEvent.builder()
.subject(subject)
.body(body)
.to(to)
.build();
}
}
// UserEmailRequestDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class UserEmailRequestDto {
@NotBlank(message = "인증 번호를 입력해 주세요")
@Schema(description = "이메일 인증번호", example = "")
private String authCode;
}
// UserRequestDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
@Getter
public class UserRequestDto {
@Schema(description = "사용자 이름", example = "홍길동")
private String name;
@Schema(description = "사용자 Email", example = "aaa@gmail.com")
@Pattern(regexp = "^[a-zA-Z0-9]+(?:\\.[a-zA-Z0-9]+)*@(?:[a-zA-Z]+\\.)+[a-zA-Z]{2,7}$", message = "이메일 형식이 올바르지 않습니다.")
private String email;
@Schema(description = "한 줄 소개", example = "안녕하세요!")
private String introduce;
@Schema(description = "현재 비밀번호", example = "currentPWD12@@")
@Size(min = 10, message = "비밀번호는 최소 10글자 이상이어야 합니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).*$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자를 최소 1글자씩 포함해야 합니다.")
private String password;
@Schema(description = "바꿀 비밀번호", example = "changePWD77!!")
@Size(min = 10, message = "비밀번호는 최소 10글자 이상이어야 합니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).*$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자를 최소 1글자씩 포함해야 합니다.")
private String changePassword;
public void encryptPassword(String encryptedPassword) {
this.changePassword = encryptedPassword;
}
}
// UserResponseDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class UserResponseDto {
@Schema(description = "사용자 ID")
private String usercode;
@Schema(description = "사용자 이름")
private String name;
@Schema(description = "한 줄 소개")
private String introduce;
@Schema(description = "사용자 Email")
private String email;
@Schema(description = "프로필 사진 주소")
private String profileImageUrl;
public UserResponseDto(User user) {
this.usercode = user.getUsercode();
this.name = user.getName();
this.introduce = user.getIntroduce();
this.email = user.getEmail();
this.profileImageUrl = user.getProfileImageUrl();
}
}
// UserResponseFromTokenDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class UserResponseFromTokenDto {
private String usercode;
public static UserResponseFromTokenDto of(User user) {
return UserResponseFromTokenDto.builder()
.usercode(user.getUsercode())
.build();
}
}
// UserSignupRequestDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
@Getter
@NoArgsConstructor
public class UserSignupRequestDto {
@Schema(description = "사용자 ID", example = "userid1234")
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "대소문자 포함 영문 + 숫자만 입력해 주세요")
@Size(min = 10, max = 20, message = "최소 10자 이상, 20자 이하로 입력해 주세요")
@NotBlank(message = "아이디를 작성해주세요")
private String usercode;
@Schema(description = "사용자 이름", example = "currentPWD12@@")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&]).*$", message = "비밀번호는 대소문자 포함 영문 + 숫자 + 특수문자를 최소 1글자씩 포함해 주세요")
@Size(min = 10, message = "최소 10자 이상 입력해 주세요")
@NotBlank(message = "비밀번호를 작성해주세요")
private String password;
@Schema(description = "사용자 이메일", example = "test@gmail.com")
@Email(regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$",
message = "이메일 형식에 맞지 않습니다.")
@NotBlank(message = "이메일의 입력 값이 없습니다.")
@Length(message = "이메일 입력 범위를 초과하였습니다.")
private String email;
}
// UserSignupResponseDto
package com.sparta.wildcard_newsfeed.domain.user.dto;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class UserSignupResponseDto {
private String usercode;
private String email;
public UserSignupResponseDto(User user) {
usercode = user.getUsercode();
email = user.getEmail();
}
}
// AuthCodeHistory
package com.sparta.wildcard_newsfeed.domain.user.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AuthCodeHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
private String autoCode;
private LocalDateTime expireDate;
@Builder
public AuthCodeHistory(User user, String autoCode, LocalDateTime expireDate) {
this.user = user;
this.autoCode = autoCode;
this.expireDate = expireDate;
}
}
// User
package com.sparta.wildcard_newsfeed.domain.user.entity;
import com.sparta.wildcard_newsfeed.domain.common.TimeStampEntity;
import com.sparta.wildcard_newsfeed.domain.user.dto.UserRequestDto;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends TimeStampEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String usercode; // 사용자 ID
private String password;
private String name; // 이름
private String email;
private String introduce;
@Setter
private String refreshToken;
@Enumerated(EnumType.STRING)
private UserStatusEnum userStatus; //회원상태코드
private LocalDateTime authUserAt; //상태 변경 시간
@Enumerated(EnumType.STRING)
private UserRoleEnum userRoleEnum;
@Setter
private String profileImageUrl;
@Builder
public User(String usercode, String password, String name, String email, String introduce, UserStatusEnum userStatus, LocalDateTime authUserAt, UserRoleEnum userRoleEnum) {
this.usercode = usercode;
this.password = password;
this.name = name;
this.email = email;
this.introduce = introduce;
this.userStatus = userStatus;
this.authUserAt = authUserAt;
this.userRoleEnum = userRoleEnum;
}
/**
* 가입 시 사용
**/
public User(String usercode, String password, String email) {
this.usercode = usercode;
this.password = password;
this.name = usercode;
this.email = email;
//유저 이름의 기본 값은 사용자 ID
this.userStatus = UserStatusEnum.UNAUTHORIZED;
this.authUserAt = LocalDateTime.now();
this.userRoleEnum = UserRoleEnum.USER;
//가입 시 회원상태코드는 정상과 상태 변경 시간 적용
}
/**
* 회원 탈퇴 시 사용
**/
public void setUserStatus(UserStatusEnum userStatus) {
this.userStatus = userStatus;
this.authUserAt = LocalDateTime.now();
}
public void update(UserRequestDto requestDto) {
this.password = requestDto.getChangePassword() != null ? requestDto.getChangePassword() : this.password;
this.name = requestDto.getName() != null ? requestDto.getName() : this.name;
this.email = requestDto.getEmail() != null ? requestDto.getEmail() : this.email;
this.introduce = requestDto.getIntroduce() != null ? requestDto.getIntroduce() : this.introduce;
}
public void updateUserStatus() {
this.userStatus = UserStatusEnum.ENABLED;
this.authUserAt = LocalDateTime.now();
}
}
// UserRoleEnum
package com.sparta.wildcard_newsfeed.domain.user.entity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum UserRoleEnum {
USER("USER"),
ADMIN("ADMIN");
private final String roleValue;
}
// UserStatusEnum
package com.sparta.wildcard_newsfeed.domain.user.entity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum UserStatusEnum {
ENABLED("enable"),
DISABLED("disable"),
UNAUTHORIZED("unauthorized");
private final String status;
}
// AuthCodeRepository
package com.sparta.wildcard_newsfeed.domain.user.repository;
import com.sparta.wildcard_newsfeed.domain.user.entity.AuthCodeHistory;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface AuthCodeRepository extends JpaRepository<AuthCodeHistory, Long> {
Optional<AuthCodeHistory> findTop1ByUserAndAutoCodeOrderByExpireDateDesc(User user, String autoCode);
}
// UserRepository
package com.sparta.wildcard_newsfeed.domain.user.repository;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsercode(String usercode);
Optional<User> findByUsercodeOrEmail(String usercode, String email);
}
// AuthCodeService
package com.sparta.wildcard_newsfeed.domain.user.service;
import com.sparta.wildcard_newsfeed.domain.user.entity.AuthCodeHistory;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.AuthCodeRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeExpireException;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeNoMatchException;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.UUID;
@Slf4j
@Primary
@Service
@RequiredArgsConstructor
public class AuthCodeService {
private final AuthCodeRepository authCodeRepository;
private static final Long MAX_EXPIRE_TIME = 180L;
/**
* 등록
*/
@Transactional
public AuthCodeHistory addAuthCode(User user) {
AuthCodeHistory authCodeHistory = AuthCodeHistory.builder()
.user(user)
.autoCode(createAuthCode())
.expireDate(LocalDateTime.now().plusSeconds(MAX_EXPIRE_TIME))
.build();
return authCodeRepository.save(authCodeHistory);
}
/**
* 조회 및 검증
*/
@Transactional(readOnly = true)
public void findByAutoCode(User user, String authCode) {
AuthCodeHistory findAuthCode = authCodeRepository.findTop1ByUserAndAutoCodeOrderByExpireDateDesc(user, authCode)
.orElseThrow(AuthCodeNotFoundException::new);
if (!authCode.equals(findAuthCode.getAutoCode())) {
throw new AuthCodeNoMatchException();
}
if (!user.equals(findAuthCode.getUser())) {
throw new AuthCodeNoMatchException();
}
if (findAuthCode.getExpireDate().isBefore(LocalDateTime.now())) {
throw new AuthCodeExpireException();
}
}
/**
* Auto 생성
*/
private String createAuthCode() {
return UUID.randomUUID().toString();
}
}
// MailService
package com.sparta.wildcard_newsfeed.domain.user.service;
import com.sparta.wildcard_newsfeed.domain.user.dto.EmailSendEvent;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
@Slf4j
@Primary
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender;
private static final String SENDER_EMAIL = "b15wildcard@gmail.com";
@Async
@TransactionalEventListener
public void sendEmail(EmailSendEvent emailSendEvent) {
log.info("메일 전송 시도 ");
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setFrom(SENDER_EMAIL);
mimeMessageHelper.setTo(emailSendEvent.getTo());
mimeMessageHelper.setSubject(emailSendEvent.getSubject());
mimeMessageHelper.setText(emailSendEvent.getBody(), true);
javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
log.error("메일 전송 실패 ", e);
}
}
}
// UserService
package com.sparta.wildcard_newsfeed.domain.user.service;
import com.sparta.wildcard_newsfeed.domain.file.service.FileService;
import com.sparta.wildcard_newsfeed.domain.user.dto.*;
import com.sparta.wildcard_newsfeed.domain.user.entity.AuthCodeHistory;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.entity.UserStatusEnum;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.UserNotFoundException;
import com.sparta.wildcard_newsfeed.security.AuthenticationUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.Objects;
import static com.sparta.wildcard_newsfeed.domain.user.dto.emailtemplate.EmailTemplate.AUTH_EMAIL;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final AuthCodeService authCodeService;
private final PasswordEncoder passwordEncoder;
private final ApplicationEventPublisher eventPublisher;
private final FileService fileService;
@Transactional
public UserSignupResponseDto signup(UserSignupRequestDto requestDto) {
String usercode = requestDto.getUsercode();
String email = requestDto.getEmail();
userRepository.findByUsercodeOrEmail(usercode, email).ifPresent(u -> {
throw new IllegalArgumentException("이미 가입한 아이디 또는 이메일이 있습니다.");
});
//Bcrypt 암호화
String pwd = passwordEncoder.encode(requestDto.getPassword());
User user = userRepository.save(new User(usercode, pwd, email));
//autocode 생성 및 등록
AuthCodeHistory authCodeHistory = authCodeService.addAuthCode(user);
//메일 생성 후 전송
eventPublisher.publishEvent(EmailSendEvent.of(AUTH_EMAIL.getSub(), AUTH_EMAIL.formatBody(authCodeHistory.getAutoCode()), user.getEmail()));
return new UserSignupResponseDto(user);
}
@Transactional
public void resign(AuthenticationUser user, String password) {
String usercode = user.getUsername();
User findUser = userRepository.findByUsercode(usercode)
.orElseThrow(() -> new NullPointerException("해당하는 회원이 없습니다!!"));
if (findUser.getUserStatus() == UserStatusEnum.DISABLED) {
throw new IllegalArgumentException("이미 탈퇴한 사용자입니다.");
}
if (!passwordEncoder.matches(password, findUser.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다!!");
}
findUser.setUserStatus(UserStatusEnum.DISABLED);
}
@Transactional(readOnly = true)
public UserResponseDto findById(Long userId) {
User findUser = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
if (findUser.getUserStatus() == UserStatusEnum.DISABLED) {
throw new IllegalArgumentException("이미 탈퇴한 사용자입니다.");
}
return new UserResponseDto(findUser);
}
@Transactional
public UserResponseDto updateUser(AuthenticationUser loginUser, Long userId, UserRequestDto requestDto) {
User findUser = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
if (findUser.getUserStatus() == UserStatusEnum.DISABLED) {
throw new IllegalArgumentException("이미 탈퇴한 사용자입니다.");
}
if (!Objects.equals(findUser.getUsercode(), loginUser.getUsername())) {
throw new IllegalArgumentException("사용자가 다릅니다.");
}
if (requestDto.getPassword() != null) {
if (requestDto.getChangePassword() == null) {
throw new IllegalArgumentException("변경할 비밀번호을 입력해 주세요.");
}
}
if (requestDto.getChangePassword() != null) {
if (requestDto.getPassword() == null) {
throw new IllegalArgumentException("현재 비밀번호를 입력해 주세요.");
}
}
if (requestDto.getPassword() != null && requestDto.getChangePassword() != null) {
if (!passwordEncoder.matches(requestDto.getPassword(), loginUser.getPassword())
|| !passwordEncoder.matches(requestDto.getPassword(), findUser.getPassword())) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
if (requestDto.getPassword().equals(requestDto.getChangePassword())) {
throw new IllegalArgumentException("변경하려는 비밀번호와 현재 비밀번호가 같습니다.");
}
}
requestDto.encryptPassword(passwordEncoder.encode(requestDto.getChangePassword()));
findUser.update(requestDto);
User savedUser = userRepository.save(findUser);
return new UserResponseDto(savedUser);
}
@Transactional(readOnly = true)
public UserResponseFromTokenDto findByUsercode(String usercode) {
User user = userRepository.findByUsercode(usercode).orElseThrow(UserNotFoundException::new);
return UserResponseFromTokenDto.of(user);
}
@Transactional
public void updateRefreshToken(String usercode, String refreshToken) {
User user = userRepository.findByUsercode(usercode).orElseThrow(UserNotFoundException::new);
user.setRefreshToken(refreshToken);
}
@Transactional
public void verifyAuthCode(AuthenticationUser loginUser, UserEmailRequestDto requestDto) {
User findUser = userRepository.findByUsercode(loginUser.getUsername()).orElseThrow(UserNotFoundException::new);
authCodeService.findByAutoCode(findUser, requestDto.getAuthCode());
findUser.updateUserStatus();
}
@Transactional
public String uploadProfileImage(AuthenticationUser loginUser, Long userId, MultipartFile file) {
User findUser = userRepository.findByUsercode(loginUser.getUsername())
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
if (!Objects.equals(findUser.getId(), userId)) {
throw new IllegalArgumentException("사용자가 일치하지 않습니다.");
}
String s3Url = fileService.uploadFileToS3(file);
log.info("S3에 저장한 파일 주소: {}", s3Url);
findUser.setProfileImageUrl(s3Url);
return s3Url;
}
}
exception
// AuthCodeExpireException
package com.sparta.wildcard_newsfeed.exception.customexception;
import lombok.Getter;
@Getter
public class AuthCodeExpireException extends RuntimeException {
public AuthCodeExpireException() {
super("유효하지 않은 인증 번호입니다.");
}
}
// AuthCodeNoMatchException
package com.sparta.wildcard_newsfeed.exception.customexception;
import lombok.Getter;
@Getter
public class AuthCodeNoMatchException extends RuntimeException {
public AuthCodeNoMatchException() {
super("일치하지 않는 인증 번호입니다.");
}
}
// AuthCodeNotFoundException
package com.sparta.wildcard_newsfeed.exception.customexception;
import lombok.Getter;
@Getter
public class AuthCodeNotFoundException extends RuntimeException {
public AuthCodeNotFoundException() {
super("찾을 수 없는 인증 번호입니다.");
}
}
// FileException
package com.sparta.wildcard_newsfeed.exception.customexception;
public class FileException extends RuntimeException {
public FileException(String message) {
super(message);
}
public FileException(String message, Throwable cause) {
super(message, cause);
}
}
// FileSizeExceededException
package com.sparta.wildcard_newsfeed.exception.customexception;
public class FileSizeExceededException extends RuntimeException {
public FileSizeExceededException(String fileName, String extension, long currentSize, long maxSize) {
super(createMessage(fileName, extension, currentSize, maxSize));
}
public static String createMessage(String fileName, String extension, long currentSize, long maxSize) {
long decimal = currentSize % (1024 * 1024);
long integerPart = currentSize / (1024 * 1024);
maxSize /= (1024 * 1024);
double decimalPart = (double) decimal / (1024 * 1024);
double combinedSize = integerPart + decimalPart;
double roundedSize = Math.round(combinedSize * 100.0) / 100.0;
return String.format("%s의 확장자는 최대 %dMB까지 저장가능합니다. 문제 생긴 파일: %s(%.2fMB)",
extension, maxSize, fileName, roundedSize);
}
}
// TokenNotFoundException
package com.sparta.wildcard_newsfeed.exception.customexception;
import lombok.Getter;
@Getter
public class TokenNotFoundException extends RuntimeException {
public TokenNotFoundException(String message) {
super(message);
}
}
// UserNotFoundException
package com.sparta.wildcard_newsfeed.exception.customexception;
import lombok.Getter;
@Getter
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super("해당 유저를 찾을 수 없습니다.");
}
}
// EmailExceptionControllerAdvice
package com.sparta.wildcard_newsfeed.exception;
import com.sparta.wildcard_newsfeed.domain.common.error.ErrorResponseDto;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeExpireException;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeNoMatchException;
import com.sparta.wildcard_newsfeed.exception.customexception.AuthCodeNotFoundException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@Getter
@Order(2)
@RestControllerAdvice
public class EmailExceptionControllerAdvice {
/**
* 유효하지 않은 인증 번호
*/
@ExceptionHandler(AuthCodeExpireException.class)
public ResponseEntity<ErrorResponseDto> authCodeExpireException(AuthCodeExpireException e) {
log.info("AuthCodeExpireException {}", e.getClass().getSimpleName());
log.error("{} ", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.FORBIDDEN.value())
.message(e.getMessage())
.build());
}
/**
* 일치하지 않는 인증 번호
*/
@ExceptionHandler(AuthCodeNoMatchException.class)
public ResponseEntity<ErrorResponseDto> authCodeNoMatchException(AuthCodeNoMatchException e) {
log.info("AuthCodeNoMatchException {}", e.getClass().getSimpleName());
log.error("{} ", e.getClass().getSimpleName());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.NOT_FOUND.value())
.message(e.getMessage())
.build());
}
/**
* 찾을 수 없는 인증 번호
*/
@ExceptionHandler(AuthCodeNotFoundException.class)
public ResponseEntity<ErrorResponseDto> authCodeNotFoundException(AuthCodeNotFoundException e) {
log.info("AuthCodeNotFoundException {}", e.getClass().getSimpleName());
log.error("{} ", e.getClass().getSimpleName());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.NOT_FOUND.value())
.message(e.getMessage())
.build());
}
}
// ExceptionControllerAdvice
package com.sparta.wildcard_newsfeed.exception;
import com.sparta.wildcard_newsfeed.domain.common.error.ErrorResponseDto;
import com.sparta.wildcard_newsfeed.exception.customexception.FileException;
import com.sparta.wildcard_newsfeed.exception.customexception.FileSizeExceededException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Order(1)
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponseDto> illegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.NOT_FOUND.value())
.message(e.getMessage())
.build());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> methodArgumentNotValidException(MethodArgumentNotValidException e) {
List<String> errorMessageList = new ArrayList<>();
e.getBindingResult().getAllErrors().forEach(v -> errorMessageList.add(v.getDefaultMessage()));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.BAD_REQUEST.value())
.message(errorMessageList)
.build());
}
@ExceptionHandler(FileException.class)
public ResponseEntity<ErrorResponseDto> fileException(FileException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.BAD_REQUEST.value())
.message(e.getMessage())
.build());
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ErrorResponseDto> maxUploadSizeExceededException(MaxUploadSizeExceededException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.BAD_REQUEST.value())
.message("동영상의 크기가 너무 큽니다. 최대 허용 크기는 200MB입니다.")
.build());
}
@ExceptionHandler(FileSizeExceededException.class)
public ResponseEntity<ErrorResponseDto> fileSizeExceededException(FileSizeExceededException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.BAD_REQUEST.value())
.message(e.getMessage())
.build());
}
}
// TokenExceptionControllerAdvice
package com.sparta.wildcard_newsfeed.exception;
import com.sparta.wildcard_newsfeed.domain.common.error.ErrorResponseDto;
import com.sparta.wildcard_newsfeed.exception.customexception.TokenNotFoundException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@Getter
@Order(1)
@RestControllerAdvice
public class TokenExceptionControllerAdvice {
/**
* 토큰을 찾을 수 없는 경우
*/
@ExceptionHandler(TokenNotFoundException.class)
public ResponseEntity<ErrorResponseDto> illegalArgumentException(IllegalArgumentException e) {
log.error("{} ", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ErrorResponseDto.builder()
.statusCode(HttpStatus.FORBIDDEN.value())
.message(e.getMessage())
.build());
}
}
security
// AuthRequestDto
package com.sparta.wildcard_newsfeed.security.jwt.dto;
import lombok.Getter;
@Getter
public class AuthRequestDto {
private String usercode;
private String password;
}
// TokenDto
package com.sparta.wildcard_newsfeed.security.jwt.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class TokenDto {
private String accessToken;
private String refreshToken;
}
// JwtPropertiesEnum
package com.sparta.wildcard_newsfeed.security.jwt.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum JwtPropertiesEnum {
INVALID_TOKEN("유효하지 않는 JWT 서명 입니다."),
EXPIRED_JWT_TOKEN("만료된 JWT token 입니다."),
UNSUPPORTED_JWT_TOKEN("지원되지 않는 JWT 토큰 입니다."),
JWT_CLAIMS_IS_EMPTY("잘못된 JWT 토큰 입니다.");
private final String errorMessage;
public static JwtPropertiesEnum fromJwtProperties(String fileExtension) {
JwtPropertiesEnum jwtPropertiesEnum = INVALID_TOKEN; //Default message
for (JwtPropertiesEnum value : values()) {
if (value.getErrorMessage().equalsIgnoreCase(fileExtension)) {
jwtPropertiesEnum = value;
}
}
return jwtPropertiesEnum;
}
}
// JwtAuthenticationEntryPoint
package com.sparta.wildcard_newsfeed.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.wildcard_newsfeed.domain.common.error.ErrorResponseDto;
import com.sparta.wildcard_newsfeed.security.jwt.enums.JwtPropertiesEnum;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j(topic = "인증 예외 필터")
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
log.error("Jwt 인증 도중 예외 발생");
String exception = (String) request.getAttribute("jwtException");
JwtPropertiesEnum jwtPropertiesValue = JwtPropertiesEnum.fromJwtProperties(exception);
String errorMessage = jwtPropertiesValue.getErrorMessage();
int statusCode = HttpStatus.FORBIDDEN.value();
ErrorResponseDto responseDto = ErrorResponseDto.builder()
.message(errorMessage)
.statusCode(statusCode)
.build();
String body = objectMapper.writeValueAsString(responseDto);
response.setContentType("application/json;charset=UTF-8");
response.setStatus(statusCode);
response.getWriter().write(body);
}
}
// JwtAuthenticationFilter
package com.sparta.wildcard_newsfeed.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import com.sparta.wildcard_newsfeed.domain.common.error.ErrorResponseDto;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.UserNotFoundException;
import com.sparta.wildcard_newsfeed.security.jwt.dto.AuthRequestDto;
import com.sparta.wildcard_newsfeed.security.jwt.dto.TokenDto;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import static com.sparta.wildcard_newsfeed.security.jwt.JwtConstants.ACCESS_TOKEN_HEADER;
import static com.sparta.wildcard_newsfeed.security.jwt.JwtConstants.REFRESH_TOKEN_HEADER;
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper;
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
public JwtAuthenticationFilter(ObjectMapper objectMapper, JwtUtil jwtUtil, UserRepository userRepository
) {
this.objectMapper = objectMapper;
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
setFilterProcessesUrl("/api/v1/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
AuthRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), AuthRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsercode(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
//TODO 로그인의 요청파라미터가 없는 경우
log.error("attemptAuthentication 예외 발생 {} ", e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
log.info("로그인 성공 및 JWT 토큰 발행");
User user = userRepository.findByUsercode(authResult.getName())
.orElseThrow(UserNotFoundException::new);
TokenDto tokenDto = jwtUtil.generateAccessTokenAndRefreshToken(user.getUsercode());
String refreshTokenValue = tokenDto.getRefreshToken().substring(7);
user.setRefreshToken(refreshTokenValue);
userRepository.save(user);
loginSuccessResponse(response, tokenDto);
}
private void loginSuccessResponse(HttpServletResponse response, TokenDto tokenDto) throws IOException {
CommonResponseDto responseDto = CommonResponseDto.builder()
.message("로그인 성공")
.statusCode(HttpStatus.OK.value())
.build();
String body = objectMapper.writeValueAsString(responseDto);
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json;charset=UTF-8");
response.addHeader(ACCESS_TOKEN_HEADER, tokenDto.getAccessToken());
response.addHeader(REFRESH_TOKEN_HEADER, tokenDto.getRefreshToken());
response.getWriter().write(body);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
log.error("unsuccessfulAuthentication | 로그인 실패");
ErrorResponseDto errorResponseDto = ErrorResponseDto.builder()
.statusCode(HttpStatus.UNAUTHORIZED.value())
.message("로그인 실패하였습니다.")
.build();
String body = objectMapper.writeValueAsString(errorResponseDto);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(body);
}
}
// JwtAuthorizationFilter
package com.sparta.wildcard_newsfeed.security.jwt;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.TokenNotFoundException;
import com.sparta.wildcard_newsfeed.exception.customexception.UserNotFoundException;
import com.sparta.wildcard_newsfeed.security.AuthenticationUserService;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final AuthenticationUserService authenticationUserService;
private final UserRepository userRepository;
public JwtAuthorizationFilter(JwtUtil jwtUtil, AuthenticationUserService authenticationUserService, UserRepository userRepository) {
this.jwtUtil = jwtUtil;
this.authenticationUserService = authenticationUserService;
this.userRepository = userRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String accessTokenValue = jwtUtil.getAccessTokenFromHeader(req);
log.info("access token 검증");
if (StringUtils.hasText(accessTokenValue) && jwtUtil.validateToken(req, accessTokenValue)) {
log.info("refresh token 검증");
String refreshTokenValue = jwtUtil.getRefreshTokenFromHeader(req);
if (StringUtils.hasText(refreshTokenValue) && jwtUtil.validateToken(req, refreshTokenValue)) {
String usercode = jwtUtil.getUserInfoFromToken(refreshTokenValue).getSubject();
User findUser = userRepository.findByUsercode(usercode)
.orElseThrow(UserNotFoundException::new);
if (isValidateUserAndToken(usercode, findUser, refreshTokenValue)) {
//access token 및 refresh token 검증 완료
log.info("Token 인증 완료");
Claims info = jwtUtil.getUserInfoFromToken(accessTokenValue);
setAuthentication(info.getSubject());
}
} else {
log.error("유효하지 않는 Refersh Token");
throw new TokenNotFoundException("토큰에 문제가 생김");
}
}
filterChain.doFilter(req, res);
}
private boolean isValidateUserAndToken(String usercode, User findUser, String refreshTokenValue) {
if (usercode.equals(findUser.getUsercode())
&& refreshTokenValue.equals(findUser.getRefreshToken())) {
return true;
}
return false;
}
// 인증 처리
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String username) {
UserDetails userDetails = authenticationUserService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
// JwtConstants
package com.sparta.wildcard_newsfeed.security.jwt;
public class JwtConstants {
// Header KEY 값
public static final String ACCESS_TOKEN_HEADER = "Authorization";
public static final String REFRESH_TOKEN_HEADER = "Authorization-refresh";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
public static final long ACCESS_TOKEN_TIME = 1000 * 60 * 30 * 60; // 30분
// public static final long ACCESS_TOKEN_TIME = 1000 * 30; // 30초
public static final long REFRESH_TOKEN_TIME = 1000 * 60 * 60 * 24 * 14; // 2주
// public static final long REFRESH_TOKEN_TIME = 1000 * 60; // 60초
}
// JwtLogoutHandler
package com.sparta.wildcard_newsfeed.security.jwt;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.TokenNotFoundException;
import com.sparta.wildcard_newsfeed.exception.customexception.UserNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtLogoutHandler implements LogoutHandler {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
log.info("로그아웃 시도");
String accessTokenValue = jwtUtil.getAccessTokenFromHeader(request);
String refreshTokenValue = jwtUtil.getRefreshTokenFromHeader(request);
if (accessTokenValue == null && refreshTokenValue == null) {
log.error("로그아웃 시도 중 에러 발생");
throw new TokenNotFoundException("토큰을 찾을 수 없습니다.");
}
String usercode = jwtUtil.getUserInfoFromToken(refreshTokenValue).getSubject();
User findUser = userRepository.findByUsercode(usercode).orElseThrow(UserNotFoundException::new);
findUser.setRefreshToken(null);
userRepository.save(findUser);
SecurityContextHolder.clearContext();
}
}
// JwtLogoutSuccessHandler
package com.sparta.wildcard_newsfeed.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.wildcard_newsfeed.domain.common.CommonResponseDto;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
private final ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
log.info("로그아웃 성공");
CommonResponseDto responseDto = CommonResponseDto.builder()
.message("로그아웃 성공하였습니다.")
.statusCode(HttpStatus.OK.value())
.build();
String body = objectMapper.writeValueAsString(responseDto);
response.setContentType("text/html;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(body);
}
}
// JwtUtil
package com.sparta.wildcard_newsfeed.security.jwt;
import com.sparta.wildcard_newsfeed.security.jwt.dto.TokenDto;
import com.sparta.wildcard_newsfeed.security.jwt.enums.JwtPropertiesEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import static com.sparta.wildcard_newsfeed.security.jwt.JwtConstants.*;
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
@Value("${jwt.secret-key}") // Base64 Encode 한 SecretKey
private String secret_key;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secret_key);
key = Keys.hmacShaKeyFor(bytes);
}
public TokenDto generateAccessTokenAndRefreshToken(String username) {
String accessToken = createAccessToken(username);
String refreshToken = createRefreshToken(username);
return TokenDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
// access Token 생성
public String createAccessToken(String userCode) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setId(UUID.randomUUID().toString()) // JWT ID 설정
.setSubject(userCode) // 사용자 식별자값(ID)
.setIssuedAt(new Date(date.getTime())) // 생성시간
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.claim("tokenType", "access")
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
// refresh Token 생성
public String createRefreshToken(String username) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setId(UUID.randomUUID().toString()) // JWT ID 설정
.setSubject(username) // 사용자 식별자값(ID)
.setIssuedAt(new Date(date.getTime())) // 생성시간
.setExpiration(new Date(date.getTime() + REFRESH_TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.claim("tokenType", "refresh")
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
// header 에서 access token
public String getAccessTokenFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader(ACCESS_TOKEN_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// header 에서 refresh token
public String getRefreshTokenFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader(REFRESH_TOKEN_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 검증
public boolean validateToken(HttpServletRequest request, String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명", e);
request.setAttribute("jwtException", JwtPropertiesEnum.INVALID_TOKEN.getErrorMessage());
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token", e);
request.setAttribute("jwtException", JwtPropertiesEnum.EXPIRED_JWT_TOKEN.getErrorMessage());
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰", e);
request.setAttribute("jwtException", JwtPropertiesEnum.UNSUPPORTED_JWT_TOKEN.getErrorMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰", e);
request.setAttribute("jwtException", JwtPropertiesEnum.JWT_CLAIMS_IS_EMPTY.getErrorMessage());
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
// AuthenticationUser
package com.sparta.wildcard_newsfeed.security;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.entity.UserRoleEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Slf4j
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
public class AuthenticationUser implements UserDetails {
/**
* User Entity usercode
*/
private final String usercode;
/**
* User Entity password
*/
private final String password;
/**
* User Entity userRole
*/
@Getter
private UserRoleEnum userRoleEnum;
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.usercode;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userRoleEnum.getRoleValue()));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public static AuthenticationUser of(User user) {
return AuthenticationUser.builder()
.usercode(user.getUsercode())
.password(user.getPassword())
.userRoleEnum(user.getUserRoleEnum())
.build();
}
}
// AuthenticationUserService
package com.sparta.wildcard_newsfeed.security;
import com.sparta.wildcard_newsfeed.domain.user.entity.User;
import com.sparta.wildcard_newsfeed.domain.user.repository.UserRepository;
import com.sparta.wildcard_newsfeed.exception.customexception.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Slf4j(topic = "유저검증")
@Service
@RequiredArgsConstructor
public class AuthenticationUserService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsercode(username)
.orElseThrow(UserNotFoundException::new);
return AuthenticationUser.of(user);
}
}
util
// FileExtensionEnum
package com.sparta.wildcard_newsfeed.util;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public enum FileExtensionEnum {
IMAGE("jpg", "jpeg", "png"),
VIDEO("gif", "mp4", "avi");
private final String[] extensions;
FileExtensionEnum(String... extensions) {
this.extensions = extensions;
}
public static List<String> getSupportedExtensions() {
return Arrays.stream(values()).flatMap(ext -> Stream.of(ext.getExtensions())).toList();
}
public static String joiningAllExtensions() {
return getSupportedExtensions().stream().collect(Collectors.joining(", ", "[", "]"));
}
}
// FileUtils
package com.sparta.wildcard_newsfeed.util;
import com.sparta.wildcard_newsfeed.exception.customexception.FileException;
import com.sparta.wildcard_newsfeed.exception.customexception.FileSizeExceededException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Slf4j
@Component
public class FileUtils {
private final long IMAGE_MAX_SIZE = 10 * 1024L * 1024L; // 10MB
private final long VIDEO_MAX_SIZE = 200 * 1024L * 1024L; // 200MB
@Value("${spring.servlet.multipart.location}")
private String uploadLocation;
public String getAbsoluteUploadFolder() {
File file = new File("");
String currentAbsolutePath = file.getAbsoluteFile() + uploadLocation;
Path path = Paths.get(currentAbsolutePath);
if (!Files.exists(path)) {
try {
Files.createDirectories(path);
} catch (IOException e) {
throw new RuntimeException("사진을 업로드할 폴더를 생성할 수 없습니다.", e);
}
}
return currentAbsolutePath;
}
public String createUuidFileName(String originalFileName) {
String extension = extractExtension(originalFileName);
return UUID.randomUUID() + "." + extension;
}
public String extractOriginalName(String originalFileName) {
return originalFileName.substring(0, originalFileName.indexOf("."));
}
public String extractExtension(String originalFileName) {
int point = originalFileName.lastIndexOf(".");
return originalFileName.substring(point + 1);
}
public void validFile(List<MultipartFile> files) {
for (MultipartFile file : files) {
String originalFilename = file.getOriginalFilename();
long size = file.getSize();
String extension = extractExtension(originalFilename).toLowerCase();
if (Arrays.asList(FileExtensionEnum.IMAGE.getExtensions()).contains(extension)) {
if (size > IMAGE_MAX_SIZE) {
throw new FileSizeExceededException(originalFilename, extension, size, IMAGE_MAX_SIZE);
}
} else if (Arrays.asList(FileExtensionEnum.VIDEO.getExtensions()).contains(extension)) {
if (size > VIDEO_MAX_SIZE) {
throw new FileSizeExceededException(originalFilename, extension, size, VIDEO_MAX_SIZE);
}
} else {
throw new FileException("지원하지 않는 파일 확장자입니다. "
+ FileExtensionEnum.joiningAllExtensions() + "의 확장자만 저장할 수 있습니다.");
}
}
}
}
// S3FileUtils
package com.sparta.wildcard_newsfeed.util;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.sparta.wildcard_newsfeed.config.S3Config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
@Slf4j
@Component
@RequiredArgsConstructor
public class S3FileUtils {
private final S3Config s3Config;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String uploadFile(String uuidFileName, File saveFile) {
s3Config.amazonS3Client().putObject(
new PutObjectRequest(bucket, uuidFileName, saveFile).withCannedAcl(CannedAccessControlList.PublicRead)
);
return s3Config.amazonS3Client().getUrl(bucket, uuidFileName).toString();
}
}
yml setting
spring:
datasource:
url: jdbc:mysql://${DB_URL}/newsfeed
username: ${DB_USER}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
sql:
init:
encoding: UTF-8
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
#show_sql: true
format_sql: true
connection:
CharSet: utf-8
characterEncoding: utf-8
useUnicode: true
logging.level:
root: info
# org.hibernate.SQL: debug
# org.springframework.security: debug
# org.springframework.web: debug
jwt:
secret-key: ${JWT-SECRET-KEY}
server:
port: ${SERVER_PORT}
---
spring:
mail:
host: smtp.gmail.com
port: ${MAIL_PORT}
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
servlet:
multipart:
maxFileSize: 300MB # 파일 하나의 최대 크기
maxRequestSize: 1000MB # 한 번에 최대 업로드 가능 용량
location: /upload/
# S3
cloud:
aws:
s3:
bucket: b15wildcard
stack:
auto: false
region:
static: ap-northeast-2
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}