From 3f4981fe7ee668f5af72c093c132f4a1a69dbe57 Mon Sep 17 00:00:00 2001 From: sinsehwan Date: Tue, 13 May 2025 17:34:03 +0900 Subject: [PATCH 1/2] auth modify & board implement --- build.gradle | 8 ++ .../auth/controller/UserController.java | 95 +++++-------------- .../devlog/auth/dto/UserLoginForm.java | 1 + .../java/apptive/devlog/auth/entity/User.java | 28 +++--- .../auth/repository/UserRepository.java | 4 + .../devlog/auth/service/UserService.java | 22 +++-- .../devlog/auth/session/SessionConst.java | 5 - .../board/controller/BoardController.java | 88 +++++++++++++++++ .../apptive/devlog/board/entity/Comment.java | 20 +++- .../apptive/devlog/board/entity/Post.java | 15 ++- .../apptive/devlog/board/entity/PostData.java | 2 - .../apptive/devlog/board/entity/Reply.java | 21 ++-- .../board/repository/CommentRepository.java | 10 ++ .../board/repository/PostDataRepository.java | 10 ++ .../board/repository/PostRepository.java | 11 +++ .../board/repository/ReplyRepository.java | 10 ++ .../devlog/board/service/CommentService.java | 63 ++++++++++++ .../devlog/board/service/PostDataService.java | 50 ++++++++++ .../devlog/board/service/PostService.java | 64 +++++++++++++ .../devlog/board/service/ReplyService.java | 50 ++++++++++ .../devlog/config/CustomUserDetails.java | 40 ++++++++ .../config/CustomUserDetailsService.java | 23 +++++ .../devlog/config/WebSecurityConfig.java | 66 +++++++++++++ src/main/resources/application.properties | 13 ++- src/main/resources/templates/users/login.html | 6 +- .../resources/templates/users/signup.html | 22 ++--- .../resources/templates/users/userHome.html | 2 +- 27 files changed, 618 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/apptive/devlog/auth/session/SessionConst.java create mode 100644 src/main/java/apptive/devlog/board/controller/BoardController.java create mode 100644 src/main/java/apptive/devlog/board/repository/CommentRepository.java create mode 100644 src/main/java/apptive/devlog/board/repository/PostDataRepository.java create mode 100644 src/main/java/apptive/devlog/board/repository/PostRepository.java create mode 100644 src/main/java/apptive/devlog/board/repository/ReplyRepository.java create mode 100644 src/main/java/apptive/devlog/board/service/CommentService.java create mode 100644 src/main/java/apptive/devlog/board/service/PostDataService.java create mode 100644 src/main/java/apptive/devlog/board/service/PostService.java create mode 100644 src/main/java/apptive/devlog/board/service/ReplyService.java create mode 100644 src/main/java/apptive/devlog/config/CustomUserDetails.java create mode 100644 src/main/java/apptive/devlog/config/CustomUserDetailsService.java create mode 100644 src/main/java/apptive/devlog/config/WebSecurityConfig.java diff --git a/build.gradle b/build.gradle index 37bb98c..902dc48 100644 --- a/build.gradle +++ b/build.gradle @@ -31,10 +31,18 @@ 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' + + 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..eb8450b 100644 --- a/src/main/java/apptive/devlog/auth/controller/UserController.java +++ b/src/main/java/apptive/devlog/auth/controller/UserController.java @@ -5,11 +5,12 @@ 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.ui.Model; import org.springframework.validation.BindingResult; @@ -23,112 +24,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..63ca222 100644 --- a/src/main/java/apptive/devlog/auth/repository/UserRepository.java +++ b/src/main/java/apptive/devlog/auth/repository/UserRepository.java @@ -7,4 +7,8 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + + 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..b8d1359 100644 --- a/src/main/java/apptive/devlog/auth/service/UserService.java +++ b/src/main/java/apptive/devlog/auth/service/UserService.java @@ -12,24 +12,32 @@ 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 false; + return userRepository.existsByNickname(nickname); } } 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..36a2a7a --- /dev/null +++ b/src/main/java/apptive/devlog/board/controller/BoardController.java @@ -0,0 +1,88 @@ +package apptive.devlog.board.controller; + +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 jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/board") +@RequiredArgsConstructor +public class BoardController { + private final PostService postService; + private final CommentService commentService; + private final ReplyService replyService; + + // 게시물 생성 + @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.setPost(postService.get(postId)); + Comment createdComment = commentService.create(comment); + return ResponseEntity.status(HttpStatus.CREATED).body(createdComment); // 201 + } + + // 게시물 댓글 목록 조회 + + //getNotDeleted 를 써서 stream으로 뽑아내는 것과 + // getNotDeletedForPost 등 애초에 Repository랑 Service계층에서 함수를 매우 많이 만들어서 + // controller에서는 함수만 쓰는 방식 + // 후자가 최적화에는 유리해 보이는데 + // 이를 위해서 이름이 매우 긴 메소드를 일일이 명세해서 사용해야 하는가 + @GetMapping("/posts/{postId}/comments") + public ResponseEntity> getCommentsForPost(@PathVariable Long postId) { + List comments = commentService.getNotDeleted() + .stream() + .filter(comment -> comment.getPost().getId().equals(postId)) + .toList(); + 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..3f29771 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..53e84b4 --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/CommentRepository.java @@ -0,0 +1,10 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.board.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + List findByIsDeletedFalse(); +} 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..2f33319 --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/PostDataRepository.java @@ -0,0 +1,10 @@ +package apptive.devlog.board.repository; + +import apptive.devlog.board.entity.PostData; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PostDataRepository extends JpaRepository { + List findByIsDeletedFalse(); +} 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..d17e574 --- /dev/null +++ b/src/main/java/apptive/devlog/board/repository/PostRepository.java @@ -0,0 +1,11 @@ +package apptive.devlog.board.repository; + +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..e5a07d7 --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/CommentService.java @@ -0,0 +1,63 @@ +package apptive.devlog.board.service; + +import apptive.devlog.board.entity.Comment; +import apptive.devlog.board.repository.CommentRepository; +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; + + // 생성 + public Comment create(Comment comment){ + comment.setCreatedAt(LocalDate.now()); + comment.setUpdatedAt(LocalDate.now()); + comment.setIsDeleted(false); + comment.setLikeCount(0L); + return commentRepository.save(comment); + } + + // 수정 + 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(); + } +} 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..c994a46 --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/PostDataService.java @@ -0,0 +1,50 @@ +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(); + } + + // 유효한 데이터 조회 + public List getNotDeleted() { + return postDataRepository.findByIsDeletedFalse(); + } +} 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..2ab18ac --- /dev/null +++ b/src/main/java/apptive/devlog/board/service/PostService.java @@ -0,0 +1,64 @@ +package apptive.devlog.board.service; + +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 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/WebSecurityConfig.java b/src/main/java/apptive/devlog/config/WebSecurityConfig.java new file mode 100644 index 0000000..82f5e53 --- /dev/null +++ b/src/main/java/apptive/devlog/config/WebSecurityConfig.java @@ -0,0 +1,66 @@ +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("/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/resources/application.properties b/src/main/resources/application.properties index 2a9adaf..13ea02f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,12 @@ 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 + 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 @@

로그인

- +