diff --git a/.gitignore b/.gitignore index 7e36cd5..92e065b 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,5 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/windows,macos,git,java,maven,eclipse,intellij,gradle,netbeans,visualstudiocode -application.yaml \ No newline at end of file +application.yaml +env.properties \ No newline at end of file diff --git a/build.gradle b/build.gradle index 37bb98c..57ef77d 100644 --- a/build.gradle +++ b/build.gradle @@ -31,10 +31,21 @@ dependencies { compileOnly 'org.projectlombok:lombok' //runtimeOnly 'com.mysql:mysql-connector-j' + // h2 + implementation 'com.h2database:h2' + // validation implementation 'jakarta.validation:jakarta.validation-api:3.0.2' implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' + // spring security + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + + // java mail sender + implementation 'org.springframework.boot:spring-boot-starter-mail' + + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/apptive/devlog/auth/controller/UserController.java b/src/main/java/apptive/devlog/auth/controller/UserController.java index 4ea48e7..7d476d5 100644 --- a/src/main/java/apptive/devlog/auth/controller/UserController.java +++ b/src/main/java/apptive/devlog/auth/controller/UserController.java @@ -5,12 +5,14 @@ import apptive.devlog.auth.dto.UserSaveForm; import apptive.devlog.auth.entity.User; import apptive.devlog.auth.service.UserService; -import apptive.devlog.auth.session.SessionConst; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; @@ -23,112 +25,62 @@ public class UserController { private final UserService userService; - /** - * login 진입 페이지 - * @param model - * @return - */ + // 로그인 시 진입하는 user home @GetMapping - public String loginHome(@SessionAttribute(name = SessionConst.LOGIN_USER, required = false) UserLoginForm loginUser, Model model){ - - // 세션 정보가 있으면 유저 정보 표기 - if(loginUser == null){ - return "users/home"; - } - else{ - model.addAttribute("userLoginForm", loginUser); - return "users/userHome"; - } + public String loginHome(@AuthenticationPrincipal UserDetails userDetails, Model model){ + model.addAttribute("user", userDetails); + return "users/userHome"; } - /** - * - * @param model - * @return - */ + // 회원가입 @GetMapping("/signup") - public String signupForm(Model model){ - model.addAttribute("user", new User()); + public String signupForm(Model model) { + model.addAttribute("user", new UserSaveForm()); return "users/signup"; } - /** - * - * @param form - * @param bindingResult - * @return - */ @PostMapping("/signup") public String signup(@Validated @ModelAttribute("user") UserSaveForm form, BindingResult bindingResult){ - // 이메일 중복 체크 - if(form.getEmail() != null){ - if(userService.isEmailDuplicated(form.getEmail())){ - bindingResult.rejectValue("email", "duplicated", "이미 가입된 이메일입니다."); - } - } - - // 닉네임 중복 체크 - if(form.getNickname() != null){ - if(userService.isNicknameDuplicated(form.getNickname())){ - bindingResult.rejectValue("nickname", "duplicated", "이미 사용 중인 닉네임입니다."); - } - } - - // Validation - if(bindingResult.hasErrors()){ + if(bindingResult.hasErrors()) { log.info("errors = {}", bindingResult); - return "/users/home"; + return "users/signup"; } - // 성공 로직 - User user = new User(form); + User user = new User(); + user.setEmail(form.getEmail()); + user.setPassword(form.getPassword()); + user.setName(form.getName()); + user.setNickname(form.getNickname()); + user.setBirth(form.getBirth()); + user.setGender(form.getGender()); userService.signup(user); return "redirect:/users/home"; } - /** - * - * @param model - * @return - */ + // 로그인 @GetMapping("/login") public String loginForm(Model model){ - model.addAttribute(new User()); + model.addAttribute("user", new UserLoginForm()); return "users/login"; } - /** - * - * @param form - * @param bindingResult - * @return - */ @PostMapping("/login") - public String login(@Validated @ModelAttribute("user") UserLoginForm form, BindingResult bindingResult, HttpServletRequest request){ - // 1차 유효성 검사 - if(bindingResult.hasErrors()){ + public String login(@Validated @ModelAttribute("user") UserLoginForm form, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { log.info("errors = {}", bindingResult); return "users/login"; } - // 로그인 처리 - HttpSession session = request.getSession(); - session.setAttribute(SessionConst.LOGIN_USER, form); - return "redirect:/main"; } + // 로그아웃 @PostMapping("/logout") public String logout(HttpServletRequest request){ - HttpSession session = request.getSession(false); - - if(session != null){ - session.invalidate(); - } return "redirect:/main"; } } diff --git a/src/main/java/apptive/devlog/auth/dto/UserLoginForm.java b/src/main/java/apptive/devlog/auth/dto/UserLoginForm.java index 2de9fca..33a3289 100644 --- a/src/main/java/apptive/devlog/auth/dto/UserLoginForm.java +++ b/src/main/java/apptive/devlog/auth/dto/UserLoginForm.java @@ -7,6 +7,7 @@ @Data public class UserLoginForm { + @NotBlank(message = "이메일은 필수입니다.") @Email(message = "이메일 형식이 아닙니다.") private String email; @NotBlank diff --git a/src/main/java/apptive/devlog/auth/entity/User.java b/src/main/java/apptive/devlog/auth/entity/User.java index 4d178f9..c23db82 100644 --- a/src/main/java/apptive/devlog/auth/entity/User.java +++ b/src/main/java/apptive/devlog/auth/entity/User.java @@ -10,6 +10,8 @@ import java.time.LocalDateTime; @Entity +// springSecurity user 인증 정보와 테이블 겹쳐서 수정 +@Table(name = "users") @Getter @Setter public class User { @@ -34,30 +36,26 @@ public class User { private Gender gender; + @Column(nullable = true) + private String role; + public User() {} - public User(String email, String password, String name, String nickname, LocalDate birth, Gender gender){ + public User(String email, String password, String name, String nickname, LocalDate birth, Gender gender, String role){ this.email = email; this.password = password; this.name = name; this.nickname = nickname; this.birth = birth; this.gender = gender; + this.role = role; } - // 생성자 필요서 다시 생각해보기 - public User(UserLoginForm form){ - this.email = form.getEmail(); - this.password = form.getPassword(); - } - - public User(UserSaveForm form){ - this.email = form.getEmail(); - this.password = form.getPassword(); - this.name = form.getName(); - this.nickname = form.getNickname(); - this.birth = form.getBirth(); - this.gender = form.getGender(); + // role 기본 값 설정 + @PrePersist + public void prePersist() { + if (this.role == null) { + this.role = "user"; + } } - } diff --git a/src/main/java/apptive/devlog/auth/repository/UserRepository.java b/src/main/java/apptive/devlog/auth/repository/UserRepository.java index 0127d9f..28d7987 100644 --- a/src/main/java/apptive/devlog/auth/repository/UserRepository.java +++ b/src/main/java/apptive/devlog/auth/repository/UserRepository.java @@ -7,4 +7,9 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByName(String name); + + boolean existsByEmail(String email); + + boolean existsByNickname(String nickname); } diff --git a/src/main/java/apptive/devlog/auth/service/UserService.java b/src/main/java/apptive/devlog/auth/service/UserService.java index c79c0ec..719eb3c 100644 --- a/src/main/java/apptive/devlog/auth/service/UserService.java +++ b/src/main/java/apptive/devlog/auth/service/UserService.java @@ -4,32 +4,56 @@ import apptive.devlog.auth.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.NoSuchElementException; +import java.util.Optional; @Service +@Transactional @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; public User signup(User user){ + // 서비스에서 exception throw + if(isEmailDuplicated(user.getEmail())){ + throw new IllegalArgumentException("중복된 이메일입니다."); + } + if(isNicknameDuplicated(user.getNickname())){ + throw new IllegalArgumentException("중복된 닉네임입니다."); + } + return userRepository.save(user); } + //로그인 public User login(String email, String password){ return userRepository.findByEmail(email) - .filter(m->m.getPassword().equals(password)) + .filter(m -> m.getPassword().equals(password)) .orElse(null); } + // 이메일 중복 체크 public boolean isEmailDuplicated(String email){ - // 중복 체크 - - return false; + // 중복 체크 처리 + return userRepository.existsByEmail(email); } + // 닉네임 중복 체크 public boolean isNicknameDuplicated(String nickname){ - // 중복 체크 + return userRepository.existsByNickname(nickname); + } + + public User getUser(String name){ + Optional user = userRepository.findByName(name); - return false; + if(user.isEmpty()){ + throw new NoSuchElementException("해당 사용자는 존재하지 않습니다."); + } + else{ + return user.get(); + } } } diff --git a/src/main/java/apptive/devlog/auth/session/SessionConst.java b/src/main/java/apptive/devlog/auth/session/SessionConst.java deleted file mode 100644 index 8415296..0000000 --- a/src/main/java/apptive/devlog/auth/session/SessionConst.java +++ /dev/null @@ -1,5 +0,0 @@ -package apptive.devlog.auth.session; - -public class SessionConst { - public static final String LOGIN_USER = "loginUser"; -} diff --git a/src/main/java/apptive/devlog/board/controller/BoardController.java b/src/main/java/apptive/devlog/board/controller/BoardController.java new file mode 100644 index 0000000..b62a900 --- /dev/null +++ b/src/main/java/apptive/devlog/board/controller/BoardController.java @@ -0,0 +1,90 @@ +package apptive.devlog.board.controller; + +import apptive.devlog.auth.entity.User; +import apptive.devlog.auth.service.UserService; +import apptive.devlog.board.entity.Comment; +import apptive.devlog.board.entity.Post; +import apptive.devlog.board.service.CommentService; +import apptive.devlog.board.service.PostService; +import apptive.devlog.board.service.ReplyService; +import apptive.devlog.mail.controller.MailController; +import apptive.devlog.mail.service.MailService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/board") +@RequiredArgsConstructor +public class BoardController { + private final PostService postService; + private final CommentService commentService; + private final ReplyService replyService; + private final MailService mailService; + private final UserService userService; + + // 게시물 생성 + @PostMapping("/posts") + public ResponseEntity createPost(@RequestBody Post post, HttpServletRequest request) { + // 게시물 생성 + Post createdPost = postService.create(post); + return ResponseEntity.status(HttpStatus.CREATED).body(createdPost); // 201 + } + + // 게시물 조회 + @GetMapping("/posts/{id}") + public ResponseEntity getPost(@PathVariable Long id){ + Post post = postService.get(id); + return ResponseEntity.ok(post); // 200 + } + + // 유효한 게시물 목록 조회 + @GetMapping("/posts") + public ResponseEntity> getAllPosts() { + List posts = postService.getNotDeleted(); + return ResponseEntity.ok(posts); // 200 + } + + // 게시물 수정 + @PutMapping("/posts/{id}") + public ResponseEntity updatePost(@PathVariable Long id, @RequestBody Post updatedPost){ + Post post = postService.update(id, updatedPost); + return ResponseEntity.ok(post); // 200 + } + + // 게시물 삭제 + @DeleteMapping("/posts/{id}") + public ResponseEntity deletePost(@PathVariable Long id){ + postService.delete(id); + return ResponseEntity.noContent().build(); // 200 + } + + // 댓글 생성 + @PostMapping("/posts/{postId}/comments") + public ResponseEntity createComment(@PathVariable Long postId, @RequestBody Comment comment){ + // 게시물에 댓글 연결 후 댓글 알림 + Comment createdComment = commentService.createAndNotify(postId, comment); + return ResponseEntity.status(HttpStatus.CREATED).body(createdComment); // 201 + } + + // 게시물 댓글 목록 조회 + + // commentService.getValidCommentByPostId() 사용 + @GetMapping("/posts/{postId}/comments") + public ResponseEntity> getCommentsForPost(@PathVariable Long postId) { + List comments = commentService.getValidCommentByPostId(postId); + if(comments.isEmpty()) { + return ResponseEntity.noContent().build(); // 404 + } + + return ResponseEntity.ok(comments); // 200 ok + } + +} diff --git a/src/main/java/apptive/devlog/board/entity/Comment.java b/src/main/java/apptive/devlog/board/entity/Comment.java index f23d12e..991d7e0 100644 --- a/src/main/java/apptive/devlog/board/entity/Comment.java +++ b/src/main/java/apptive/devlog/board/entity/Comment.java @@ -1,19 +1,33 @@ package apptive.devlog.board.entity; +import apptive.devlog.auth.entity.User; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; import java.time.LocalDate; +import java.util.List; @Entity +@Getter @Setter public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long userId; - //post foreign key - private Long postId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "post_id") + private Post post; + private LocalDate createdAt; private LocalDate updatedAt; private Long likeCount; private Boolean isDeleted; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL) + private List replyList; } diff --git a/src/main/java/apptive/devlog/board/entity/Post.java b/src/main/java/apptive/devlog/board/entity/Post.java index 87c435f..4f57e8f 100644 --- a/src/main/java/apptive/devlog/board/entity/Post.java +++ b/src/main/java/apptive/devlog/board/entity/Post.java @@ -1,10 +1,12 @@ package apptive.devlog.board.entity; +import apptive.devlog.auth.entity.User; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import java.time.LocalDate; +import java.util.List; @Entity @Getter @Setter @@ -12,13 +14,20 @@ public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long userId; - //postData foreign key - private Long dataId; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @OneToOne + @JoinColumn(name = "postData_id") + private PostData postData; private LocalDate createdAt; private LocalDate updatedAt; private Long likeCount; private Boolean isDeleted; + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) + private List commentList; } diff --git a/src/main/java/apptive/devlog/board/entity/PostData.java b/src/main/java/apptive/devlog/board/entity/PostData.java index 161db25..d8cce2e 100644 --- a/src/main/java/apptive/devlog/board/entity/PostData.java +++ b/src/main/java/apptive/devlog/board/entity/PostData.java @@ -7,8 +7,6 @@ import lombok.Getter; import lombok.Setter; -import java.time.LocalDate; - @Entity @Getter @Setter public class PostData { diff --git a/src/main/java/apptive/devlog/board/entity/Reply.java b/src/main/java/apptive/devlog/board/entity/Reply.java index bb4ae25..25c627d 100644 --- a/src/main/java/apptive/devlog/board/entity/Reply.java +++ b/src/main/java/apptive/devlog/board/entity/Reply.java @@ -1,24 +1,31 @@ package apptive.devlog.board.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import apptive.devlog.auth.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; import java.time.LocalDate; @Entity +@Getter @Setter public class Reply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long userId; - //comment foreign key - private Long commentId; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "comment_id") + private Comment comment; private LocalDate createdAt; private LocalDate updatedAt; private Long likeCount; private Boolean isDeleted; + + } diff --git a/src/main/java/apptive/devlog/board/repository/CommentRepository.java b/src/main/java/apptive/devlog/board/repository/CommentRepository.java new file mode 100644 index 0000000..f760b88 --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/CommentRepository.java @@ -0,0 +1,15 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.board.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByIsDeletedFalse(); + + @Query("SELECT c From Comment c Where c.post.id = :postId AND c.isDeleted = false") + List findValidByPostId(@Param("postId") Long postId); +} diff --git a/src/main/java/apptive/devlog/board/repository/PostDataRepository.java b/src/main/java/apptive/devlog/board/repository/PostDataRepository.java new file mode 100644 index 0000000..1b185cb --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/PostDataRepository.java @@ -0,0 +1,11 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.auth.entity.User; +import apptive.devlog.board.entity.PostData; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface PostDataRepository extends JpaRepository { +} diff --git a/src/main/java/apptive/devlog/board/repository/PostRepository.java b/src/main/java/apptive/devlog/board/repository/PostRepository.java new file mode 100644 index 0000000..ecc1f65 --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/PostRepository.java @@ -0,0 +1,12 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.auth.entity.User; +import apptive.devlog.board.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PostRepository extends JpaRepository { + List findByIsDeletedFalse(); + List findByUserId(Long userId); +} diff --git a/src/main/java/apptive/devlog/board/repository/ReplyRepository.java b/src/main/java/apptive/devlog/board/repository/ReplyRepository.java new file mode 100644 index 0000000..7c037aa --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/ReplyRepository.java @@ -0,0 +1,10 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.board.entity.Reply; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReplyRepository extends JpaRepository { + List findByCommentId(Long commentId); +} diff --git a/src/main/java/apptive/devlog/board/service/CommentService.java b/src/main/java/apptive/devlog/board/service/CommentService.java new file mode 100644 index 0000000..90d5b09 --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/CommentService.java @@ -0,0 +1,81 @@ +package apptive.devlog.board.service; + +import apptive.devlog.board.entity.Comment; +import apptive.devlog.board.entity.Post; +import apptive.devlog.board.repository.CommentRepository; +import apptive.devlog.board.repository.PostRepository; +import apptive.devlog.mail.service.MailService; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CommentService { + private final CommentRepository commentRepository; + private final PostService postService; + private final MailService mailService; + + // 댓글 생성, 이메일 notify + public Comment createAndNotify(Long postId, Comment comment){ + Post post = postService.get(postId); + comment.setPost(post); + + comment.setCreatedAt(LocalDate.now()); + comment.setUpdatedAt(LocalDate.now()); + comment.setIsDeleted(false); + comment.setLikeCount(0L); + + Comment savedComment = commentRepository.save(comment); + + //이메일 전송 + mailService.sendMimeMessage(post.getUser().getEmail()); + + return savedComment; + } + + // 수정 + public Comment update(Long id, Comment updated){ + Comment comment = commentRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 댓글을 찾을 수 없습니다.") + ); + comment.setUpdatedAt(LocalDate.now()); + comment.setIsDeleted(updated.getIsDeleted()); + comment.setLikeCount(updated.getLikeCount()); + return commentRepository.save(comment); + } + + // 삭제 - soft delete + public void delete(Long id){ + Comment comment = commentRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 댓글을 찾을 수 없습니다.") + ); + comment.setIsDeleted(true); + commentRepository.save(comment); + } + + // 단일 조회. 삭제 요소도 조회 가능 + public Comment get(Long id){ + return commentRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 댓글을 찾을 수 없습니다.") + ); + } + + + // 작성된 모든 댓글 조회 + public List getAll(){ + return commentRepository.findAll(); + } + + // 유효한 댓글만 조회 + public List getNotDeleted(){ + return commentRepository.findByIsDeletedFalse(); + } + + public List getValidCommentByPostId(Long postId){ + return commentRepository.findValidByPostId(postId); + } +} diff --git a/src/main/java/apptive/devlog/board/service/PostDataService.java b/src/main/java/apptive/devlog/board/service/PostDataService.java new file mode 100644 index 0000000..6f6be04 --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/PostDataService.java @@ -0,0 +1,45 @@ +package apptive.devlog.board.service; + +import apptive.devlog.board.entity.PostData; +import apptive.devlog.board.repository.PostDataRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PostDataService { + private final PostDataRepository postDataRepository; + + public PostData create(PostData data) { + return postDataRepository.save(data); + } + + public PostData update(Long id, PostData updated) { + PostData data = postDataRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.") + ); + data.setTitle(updated.getTitle()); + data.setContent(updated.getContent()); + return postDataRepository.save(data); + } + + // 삭제 - soft delete + public void delete(Long id) { + postDataRepository.deleteById(id); + } + + // 단일 조회. 삭제 요소도 조회 가능 + public PostData get(Long id) { + return postDataRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.") + ); + } + + // 모든 데이터 조회 + public List getAll() { + return postDataRepository.findAll(); + } +} diff --git a/src/main/java/apptive/devlog/board/service/PostService.java b/src/main/java/apptive/devlog/board/service/PostService.java new file mode 100644 index 0000000..7ca8354 --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/PostService.java @@ -0,0 +1,72 @@ +package apptive.devlog.board.service; + +import apptive.devlog.auth.entity.User; +import apptive.devlog.auth.repository.UserRepository; +import apptive.devlog.board.entity.Post; +import apptive.devlog.board.entity.PostData; +import apptive.devlog.board.repository.PostDataRepository; +import apptive.devlog.board.repository.PostRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PostService { + private final PostRepository postRepository; + + // 생성 후 이메일 전송 + public Post create(Post post){ + post.setCreatedAt(LocalDate.now()); + post.setUpdatedAt(LocalDate.now()); + post.setIsDeleted(false); + post.setLikeCount(0L); + + return postRepository.save(post); + } + + // 수정 + public Post update(Long id, Post updated){ + Post post = postRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.")); + post.setUpdatedAt(LocalDate.now()); + post.setLikeCount(updated.getLikeCount()); + post.setIsDeleted(updated.getIsDeleted()); + return postRepository.save(post); + } + + // 삭제 - soft delete + public void delete(Long id) { + Post post = postRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.") + ); + post.setIsDeleted(true); + postRepository.save(post); + } + + // 단일 조회. 삭제 요소도 조회 가능 + public Post get(Long id){ + return postRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.")); + } + + public User getUserById(Long id){ + Post post = postRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.") + ); + return post.getUser(); + } + + //모든 게시글 조회 + public List getAll() { + return postRepository.findAll(); + } + + // 유효한 게시글만 조회 + public List getNotDeleted() { + return postRepository.findByIsDeletedFalse(); + } +} diff --git a/src/main/java/apptive/devlog/board/service/ReplyService.java b/src/main/java/apptive/devlog/board/service/ReplyService.java new file mode 100644 index 0000000..3b5623c --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/ReplyService.java @@ -0,0 +1,50 @@ +package apptive.devlog.board.service; + +import apptive.devlog.board.entity.Reply; +import apptive.devlog.board.repository.ReplyRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +public class ReplyService { + private final ReplyRepository replyRepository; + + public Reply create(Reply reply){ + reply.setCreatedAt(LocalDate.now()); + reply.setCreatedAt(LocalDate.now()); + reply.setIsDeleted(false); + reply.setLikeCount(0L); + + return replyRepository.save(reply); + } + + public Reply update(Long id, Reply updated) { + Reply reply = replyRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 답글을 찾을 수 없습니다.") + ); + reply.setUpdatedAt(LocalDate.now()); + reply.setLikeCount(updated.getLikeCount()); + reply.setIsDeleted(updated.getIsDeleted()); + return replyRepository.save(reply); + } + + // 삭제 - soft delete + public void delete(Long id) { + Reply reply = replyRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 답글을 찾을 수 없습니다.") + ); + + reply.setIsDeleted(true); + replyRepository.save(reply); + } + + public Reply get(Long id){ + return replyRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException("해당 게시글을 찾을 수 없습니다.")); + } + +} diff --git a/src/main/java/apptive/devlog/config/CustomUserDetails.java b/src/main/java/apptive/devlog/config/CustomUserDetails.java new file mode 100644 index 0000000..61824a1 --- /dev/null +++ b/src/main/java/apptive/devlog/config/CustomUserDetails.java @@ -0,0 +1,40 @@ +package apptive.devlog.config; + +import apptive.devlog.auth.entity.User; +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.List; + +public class CustomUserDetails implements UserDetails { + + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole())); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + // username 대신 email 사용 + return user.getEmail(); + } + + // 나머지는 기본적으로 true로 설정 + @Override public boolean isAccountNonExpired() {return true;} + @Override public boolean isAccountNonLocked() {return true;} + @Override public boolean isCredentialsNonExpired() {return true;} + @Override public boolean isEnabled() {return true;} +} diff --git a/src/main/java/apptive/devlog/config/CustomUserDetailsService.java b/src/main/java/apptive/devlog/config/CustomUserDetailsService.java new file mode 100644 index 0000000..daf18da --- /dev/null +++ b/src/main/java/apptive/devlog/config/CustomUserDetailsService.java @@ -0,0 +1,23 @@ +package apptive.devlog.config; + +import apptive.devlog.auth.entity.User; +import apptive.devlog.auth.repository.UserRepository; +import lombok.RequiredArgsConstructor; +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; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException{ + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + return new CustomUserDetails(user); + } +} diff --git a/src/main/java/apptive/devlog/config/MailConfig.java b/src/main/java/apptive/devlog/config/MailConfig.java new file mode 100644 index 0000000..82ac2a8 --- /dev/null +++ b/src/main/java/apptive/devlog/config/MailConfig.java @@ -0,0 +1,9 @@ +package apptive.devlog.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@PropertySource("classpath:env.properties") +public class MailConfig { +} diff --git a/src/main/java/apptive/devlog/config/WebSecurityConfig.java b/src/main/java/apptive/devlog/config/WebSecurityConfig.java new file mode 100644 index 0000000..e4d9e67 --- /dev/null +++ b/src/main/java/apptive/devlog/config/WebSecurityConfig.java @@ -0,0 +1,67 @@ +package apptive.devlog.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class WebSecurityConfig { + + private final CustomUserDetailsService customUserDetailsService; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // CSRF 보호 비활성화(테스트용) + .csrf(csrf -> csrf.disable()) + .headers(headers -> headers + .frameOptions(frameOptions -> frameOptions.sameOrigin()) // H2 콘솔용 설정 + ) + + // URL 별 접근 권한 + .authorizeHttpRequests((auth) -> auth + // 로그인/회원가입 페이지 인증 없이 접근 허용 + .requestMatchers("/users/login", "/users/signup").permitAll() + .requestMatchers("/api/board").permitAll() // board api 테스트용 + .requestMatchers("/mail/html").permitAll() // mail api 테스트용 + .requestMatchers("/h2-console/**").permitAll() // H2 콘솔용 설정 + .anyRequest().authenticated() + ) + .formLogin((form) -> form + .loginPage("/users/login") + // 로그인 성공 시 이동 url + .defaultSuccessUrl("/users/home", true) + // 로그인 페이지 인증 없이 접근 허용 + .permitAll() + ) + .logout((logout) -> logout + // 로그아웃 성공 이동 url + .logoutSuccessUrl("/users/login?logout") + // 로그아웃 인증 없이 접근 허용 + .permitAll() + ); + + return http.build(); + } + + // 인증 관리자 설정 (AuthenticationManager 빈 등록) + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + // 비밀번호 인코딩(BCrypt) + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/apptive/devlog/mail/controller/MailController.java b/src/main/java/apptive/devlog/mail/controller/MailController.java new file mode 100644 index 0000000..b2f363a --- /dev/null +++ b/src/main/java/apptive/devlog/mail/controller/MailController.java @@ -0,0 +1,24 @@ +package apptive.devlog.mail.controller; + +import apptive.devlog.mail.service.MailService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/mail") +@RequiredArgsConstructor +public class MailController { + private final MailService mailService; + + @GetMapping("/simple") + public void sendSimpleMailMessage(String email) { + mailService.sendSimpleMailMessage(email); + } + + @GetMapping("/html") + public void sendMimeMessage(String email) { + mailService.sendMimeMessage(email); + } +} diff --git a/src/main/java/apptive/devlog/mail/service/MailService.java b/src/main/java/apptive/devlog/mail/service/MailService.java new file mode 100644 index 0000000..ef4e475 --- /dev/null +++ b/src/main/java/apptive/devlog/mail/service/MailService.java @@ -0,0 +1,75 @@ +package apptive.devlog.mail.service; + +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@Slf4j +@RequiredArgsConstructor +public class MailService { + + private final JavaMailSender javaMailSender; + + public void sendSimpleMailMessage(String email) { + SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); + + try{ + simpleMailMessage.setTo(email); + simpleMailMessage.setSubject("메일 제목 : 작성 완료"); + simpleMailMessage.setText("내용 : 글 작성 완료"); + + javaMailSender.send(simpleMailMessage); + + log.info("메일 발송 성공!"); + } + catch (Exception e){ + log.info("메일 발송 실패!"); + throw new RuntimeException(e); + } + } + + public void sendMimeMessage(String email) { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + + try{ + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); + + //수신자 + mimeMessageHelper.setTo(email); + + mimeMessageHelper.setSubject("댓글 알림 메시지입니다."); + + String content = """ + + + +
+

댓글 알림

+
+
+

작성하신 글에 댓글 또는 대댓글이 추가되었습니다.

+
+
+
+ + + """; + mimeMessageHelper.setText(content, true); + + javaMailSender.send(mimeMessage); + + log.info("메일 발송 성공!"); + } + catch (Exception e){ + log.info("메일 발송 실패!"); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2a9adaf..0f8fd4e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,23 @@ spring.application.name=devlog -spring.datasource.url=none -spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.h2.console.enabled=true + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# java mail sender +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${mail.username} +spring.mail.password=${mail.password} +# ?? ? ?? ?? +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.timeout=5000 +# SMTP ?? ?? ?? ?? +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true diff --git a/src/main/resources/env.properties b/src/main/resources/env.properties new file mode 100644 index 0000000..5f7d948 --- /dev/null +++ b/src/main/resources/env.properties @@ -0,0 +1,3 @@ +mail.username=sk124590@gmail.com +mail.password=atdemcsjqkcnzewc +#maxevrjivrhgqgcc \ No newline at end of file diff --git a/src/main/resources/templates/users/login.html b/src/main/resources/templates/users/login.html index 0e48773..5cfc909 100644 --- a/src/main/resources/templates/users/login.html +++ b/src/main/resources/templates/users/login.html @@ -14,9 +14,9 @@
- +

로그인

- +
@@ -35,7 +35,7 @@

로그인

- +