From 95886ea9e0bc8ae5e83182b0b3f2f39ceb752a9e Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:20:28 +0900 Subject: [PATCH 01/52] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20member=20=EB=8F=84=EB=A9=94=EC=9D=B8=20exception=20=EB=BC=88?= =?UTF-8?q?=EB=8C=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + ...4\355\230\204_\353\252\251\353\241\235.md" | 209 ++++++++++++++++++ .../MemberExceptionHandler.java | 23 ++ .../dto/MemberErrorResponse.java | 7 + .../exception/exceptions/MemberErrorCode.java | 19 ++ .../exception/exceptions/MemberException.java | 14 ++ 6 files changed, 277 insertions(+) create mode 100644 "docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" create mode 100644 src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java create mode 100644 src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java create mode 100644 src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java create mode 100644 src/main/java/com/board/member/exception/exceptions/MemberException.java diff --git a/build.gradle b/build.gradle index dfd0328..87c16d8 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,11 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { diff --git "a/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" "b/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" new file mode 100644 index 0000000..4bd04d6 --- /dev/null +++ "b/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" @@ -0,0 +1,209 @@ +## API + +### 유저 CRUD + +* 회원가입 기능. + * endPoint : [POST, /api/members] + * 설명 : 이름, 닉네임, 아이디, 비밀번호 저장. / 아이디 중복 확인 기능, 닉네임 중복 확인 기능 포함 + * Exception : 아이디 중복 시, 닉네임 중복 시 + * request : + ~~~ + { + "memberName": "juna", + "memberNickName": "juju", + "memberLoginId": "aaa", + "memberPassword": "ssssss" + } + ~~~ + * response : url location 으로 반환 + + +* 로그인 기능. + * endPoint : [POST, /api/members/login] + * 설명 : 로그인 기능. 쿠키 생성, 세션 생성. + * Exception : 아이디 혹은 비밀번호가 일치하지 않을 경우. + * request : + ~~~ + { + "memberLoginId": "aaa", + "memberPassword": "ssssss" + } + ~~~ + * response : X + +### **모든 마이페이지 기능은 로그인 한 유저만 접근할 수 있도록 구현** + +* 정보 수정 기능.(마이페이지) + * endPoint : [PATCH, /api/members] + * 설명 : 이름, 닉네임, 아이디, 비밀번호 수정. + * Exception : 로그인 정보 가져왔을 때 값이 없을 경우. + * request : + ~~~ + { + "memberName": "hun", + "memberNickName": "hhun", + "memberLoginId": "ddd", + "memberPassword": "sssss" + } + ~~~ + * response : x + +* 멤버 정보 조회 기능(마이페이지) + * endPoint : [GET, /api/members] + * 설명 : 유저 정보 가져옴. + * Exception : 로그인 정보 가져왔을 때 값이 없을 경우. + * request : x + * response : member + + +* 계정 탈퇴 기능(마이페이지) + * endPoint : [DELETE, /api/members] + * 설명 : 유저 탈퇴. + * Exception : 로그인 정보 가져왔을 때 값이 없을 경우. + * request : x + * response : member + +### 게시물 CRUD + +* 글 작성 기능. (해당 유저만 작성할 수 있도록.) + * endPoint : [POST, /api/articles] + * 설명 : 글 작성 + * Exception : 쿠키나 세션 가져왔을 때 값이 없을 경우. + * request : + ~~~ + { + "title": "가가가", + "content": "나나나" + } + ~~~ + * response : url location 으로 반환 + + +* 모든 글 조회 기능 (유저 아니여도 접근 가능) + * endPoint : [GET, /api/articles] + * 설명 : 글 모두 끌어오기 + * Exception : 작성된 글이 없을 경우 + * request : x + * response : 조회된 모든 글 반환 + + +* 글 하나만 조회하는 기능 (유저 아니어도 접근 가능) + * endPoint : [GET, api/articles/{articleId}] + * 설명 : 해당 글 조회하기 + * Exception : 작성된 글이 없을 경우 + * request : x + * response : 게시글 반환 + + +* 글 수정하는 기능 (해당 글쓴 유저만 접근 가능) + * endPoint : [GET, api/articles/{articleId}] + * 설명 : 해당 글 조회하기 + * Exception : 작성된 글이 없을 경우, 해당 게시물에 대한 권한이 없을 경우 + * request : + ~~~ + { + "title": "가", + "content": "나" + } + ~~~ + * response : 수정된 글 반환 + + +* 게시물 삭제 기능(해당 글쓴 유저만 삭제 가능) + * endPoint : [GET, api/articles/{articleId}] + * 설명 : 해당 글 삭제하기 + * Exception : 작성된 글이 없을 경우, 해당 게시물에 대한 권한이 없을 경우 + * request : X + * response : 삭제된 게시물 반환 + + +* 유저 글 모아보는 기능(마이페이지) + * endPoint : [GET, api/members/articles] + * 설명 : 유저에 해당하는 글만 모아 보기 + * Exception : 작성된 글이 없을 경우 + * request : X + * response : 유저의 모든 게시물 반환 + + +### 댓글 CRUD + + +* 댓글 달기 기능.(유저만 접근 가능) + * endPoint : [POST, api/articles/{articleId}/comments] + * 설명 : 댓글 작성하기 + * Exception : 작성된 글이 없을 경우, 로그인 상태 아닐 경우 + * request : + ~~~ + { + "content": "노잼" + } + ~~~ + * response : 게시글 url 반환 (api/articles/{articleId}) + + +* 게시물에 대한 댓글 조회 기능.(유저 아니어도 접근 가능) + * endPoint : [GET, api/articles/{articleId}/comments] + * 설명 : 게시물에 대한 댓글 조회하기 + * Exception : 작성된 글이 없을 경우, 댓글 없을 경우 + * request : x + * response : + ~~~ + { + "commentResponses": [ + { + "articleId": 1, + "memberNickName": "juj", + "content": "노잼s" + }, + { + "articleId": 1, + "memberNickName": "juj", + "content": "노잼잼" + } + ] + } + ~~~ + + +* 댓글 수정 (해당 댓글 작성자만 수정 가능) + * endPoint : [PATCH, api/comments/{commentId}] + * 설명 : 댓글 수정하기 + * Exception : 작성된 글이 없을 경우, 댓글 없을 경우 + * request : + ~~~ + { + "content": "게시판" + } + ~~~ + * response : + ~~~ + { + "articleId": 1, + "memberNickName": "juj", + "content": "게시판" + } + ~~~ + + +* 댓글 삭제 (해당 댓글 작성자만 삭제 가능) + * endPoint : [DELETE, api/comments/{commentId}] + * 설명 : 댓글 삭제하기 + * Exception : 작성된 글이 없을 경우, 댓글 없을 경우 + * request : x + * response : + ~~~ + { + "articleId": 1, + "memberNickName": "juj", + "content": "게시판" + } + ~~~ + + +* 유저 댓글 모아 보는 기능(마이페이지) + * endPoint : [GET, api/members/comments] + * 설명 : 유저 댓글 조회하기 + * Exception : 작성된 글이 없을 경우, 댓글 없을 경우 + * request : + * response : 유저의 모든 댓글 +--- diff --git a/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java b/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java new file mode 100644 index 0000000..3b3194a --- /dev/null +++ b/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java @@ -0,0 +1,23 @@ +package com.board.member.exception.exceptionhandler; + +import com.board.member.exception.exceptionhandler.dto.MemberErrorResponse; +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class MemberExceptionHandler { + + @ExceptionHandler(MemberException.class) + public ResponseEntity handleException(MemberException e) { + MemberErrorCode memberErrorCode = e.getErrorCode(); + MemberErrorResponse response = new MemberErrorResponse( + memberErrorCode.getCustomCode(), + memberErrorCode.getMessage() + ); + + return ResponseEntity.status(memberErrorCode.getHttpStatus()).body(response); + } +} diff --git a/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java b/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java new file mode 100644 index 0000000..6c034df --- /dev/null +++ b/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java @@ -0,0 +1,7 @@ +package com.board.member.exception.exceptionhandler.dto; + +public record MemberErrorResponse( + String customCode, + String message +) { +} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java new file mode 100644 index 0000000..b1527a1 --- /dev/null +++ b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java @@ -0,0 +1,19 @@ +package com.board.member.exception.exceptions; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum MemberErrorCode { + ; + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; + + MemberErrorCode(HttpStatus httpStatus, String customCode, String message) { + this.httpStatus = httpStatus; + this.customCode = customCode; + this.message = message; + } +} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberException.java b/src/main/java/com/board/member/exception/exceptions/MemberException.java new file mode 100644 index 0000000..7afaab5 --- /dev/null +++ b/src/main/java/com/board/member/exception/exceptions/MemberException.java @@ -0,0 +1,14 @@ +package com.board.member.exception.exceptions; + +import lombok.Getter; + +@Getter +public class MemberException extends RuntimeException{ + + private final MemberErrorCode errorCode; + + public MemberException(MemberErrorCode errorCode) { + super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); + this.errorCode = errorCode; + } +} From efe1e89c74748a8c7d161ee8fbcbbb6e9a63e5ce Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:59:26 +0900 Subject: [PATCH 02/52] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원 가입 시, jwt를 발급하여 바로 로그인 할 수 있도록 로직 구현 --- build.gradle | 4 +- docs/spring_annotation.md | 46 ++++++++++++++++++ ...4\355\230\204_\353\252\251\353\241\235.md" | 11 ++++- .../Infrastructure/auth/JwtTokenProvider.java | 48 +++++++++++++++++++ .../controller/auth/AuthController.java | 27 +++++++++++ .../auth/dto/request/SignUpRequest.java | 9 ++++ .../auth/dto/response/SignUpResponse.java | 7 +++ .../member/domain/auth/TokenProvider.java | 9 ++++ .../board/member/domain/member/Member.java | 44 +++++++++++++++++ .../exception/exceptions/MemberErrorCode.java | 5 +- .../member/repository/MemberRepository.java | 12 +++++ .../member/service/auth/AuthService.java | 40 ++++++++++++++++ .../auth/exception/ExistLoginIdException.java | 11 +++++ .../exception/ExistNickNameException.java | 11 +++++ src/main/resources/application.properties | 3 ++ 15 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 docs/spring_annotation.md create mode 100644 src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java create mode 100644 src/main/java/com/board/member/controller/auth/AuthController.java create mode 100644 src/main/java/com/board/member/controller/auth/dto/request/SignUpRequest.java create mode 100644 src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java create mode 100644 src/main/java/com/board/member/domain/auth/TokenProvider.java create mode 100644 src/main/java/com/board/member/domain/member/Member.java create mode 100644 src/main/java/com/board/member/repository/MemberRepository.java create mode 100644 src/main/java/com/board/member/service/auth/AuthService.java create mode 100644 src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java create mode 100644 src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java diff --git a/build.gradle b/build.gradle index 87c16d8..02c861b 100644 --- a/build.gradle +++ b/build.gradle @@ -41,9 +41,7 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' //jwt - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'com.auth0:java-jwt:4.2.1' } tasks.named('test') { diff --git a/docs/spring_annotation.md b/docs/spring_annotation.md new file mode 100644 index 0000000..0c238d5 --- /dev/null +++ b/docs/spring_annotation.md @@ -0,0 +1,46 @@ +## **Java Bean Validation: `@NotNull`, `@NotEmpty`, `@NotBlank` 차이점** + +--- + +### 1. @NotNull + +* null 값만 허용하지 않음 +* 빈 값("")과 공백(" ")은 허용됨 + +### 2. @NotEmpty + +* null 값과 빈 값("") 모두 허용되지 않음 +* 공백(" ")은 허용됨 + +### 3. @NotBlank + +* null 값, 빈 값(""), 공백(" ") 모두 허용되지 않음 + +**회원 가입 시, @NotBlank 어노테이션 활용하는 것이 좋을 것 같음** + + +## **Lombok의 생성자 어노테이션 비교 (`@AllArgsConstructor`, `@RequiredArgsConstructor`, `@NoArgsConstructor`)** + +--- + +### 1. @AllArgsConstructor + +* 클래스의 모든 필드를 초기화하는 생성자를 자동 생성 +* @NonNull 필드가 null이면 NullPointerException 발생 + +### 2. @RequiredArgsConstructor + +* final 필드 및 @NonNull 필드만 포함하는 생성자를 생성 +* 초기화된 final 필드는 생성자에서 제외됨 + +### 3. @NoArgsConstructor + +* 매개변수가 없는 기본 생성자를 생성 +* final 필드가 있을 경우 force = true 옵션 필요 + +### JPA 에서 기본 생성자가 필요한 이유(protected 쓰는 이유까지) + +1. JPA는 데이터베이스에서 조회한 결과를 기반으로 엔티티 객체를 생성해야 합니다. +2. Reflection을 이용하여 객체를 만들기 때문에, 기본 생성자가 반드시 필요합니다. +3. 외부에서 기본 생성자를 직접 호출하는 것을 막기 위해 protected 사용합니다. +3. 만약 기본 생성자가 없으면 InstantiationException 오류 발생! diff --git "a/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" "b/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" index 4bd04d6..add865e 100644 --- "a/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" +++ "b/docs/\352\270\260\353\212\245_\352\265\254\355\230\204_\353\252\251\353\241\235.md" @@ -3,7 +3,7 @@ ### 유저 CRUD * 회원가입 기능. - * endPoint : [POST, /api/members] + * endPoint : [POST, /api/members/signUp] * 설명 : 이름, 닉네임, 아이디, 비밀번호 저장. / 아이디 중복 확인 기능, 닉네임 중복 확인 기능 포함 * Exception : 아이디 중복 시, 닉네임 중복 시 * request : @@ -15,7 +15,14 @@ "memberPassword": "ssssss" } ~~~ - * response : url location 으로 반환 + * response : url location, + ~~~ + { + "memberId": "1", + "token": "jwtToken" + } + ~~~ + * 로그인 기능. diff --git a/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java b/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java new file mode 100644 index 0000000..3044d11 --- /dev/null +++ b/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java @@ -0,0 +1,48 @@ +package com.board.member.Infrastructure.auth; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.board.member.domain.auth.TokenProvider; +import java.util.Date; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtTokenProvider implements TokenProvider { + + private final Algorithm algorithm; + private final long expirationPeriod; + + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, @Value("${jwt.expiration-period}") long expirationPeriod) { + this.algorithm = Algorithm.HMAC256(secretKey); + this.expirationPeriod = expirationPeriod; + } + + @Override + public String create(Long memberId) { + return JWT.create() + .withClaim("memberId", memberId) + .withIssuedAt(issuedDate()) + .withExpiresAt(expiredDate()) + .withJWTId(UUID.randomUUID().toString()) + .sign(algorithm); + } + + private Date issuedDate() { + return new Date(System.currentTimeMillis()); + } + + private Date expiredDate() { + return new Date(System.currentTimeMillis() + expirationPeriod * 1000L); //2시간 + } + + @Override + public DecodedJWT verifyToken(String token) { + JWTVerifier verifier = JWT.require(algorithm) + .build(); + return verifier.verify(token); + } +} diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java new file mode 100644 index 0000000..02284ff --- /dev/null +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -0,0 +1,27 @@ +package com.board.member.controller.auth; + +import com.board.member.controller.auth.dto.request.SignUpRequest; +import com.board.member.controller.auth.dto.response.SignUpResponse; +import com.board.member.service.auth.AuthService; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/member/signUp") + public ResponseEntity createMember(@RequestBody SignUpRequest signUpRequest) { + SignUpResponse response = authService.signUp(signUpRequest); + URI location = URI.create("/api/members/" + response.memberId()); + return ResponseEntity.created(location).body(response); + } +} diff --git a/src/main/java/com/board/member/controller/auth/dto/request/SignUpRequest.java b/src/main/java/com/board/member/controller/auth/dto/request/SignUpRequest.java new file mode 100644 index 0000000..d2641a0 --- /dev/null +++ b/src/main/java/com/board/member/controller/auth/dto/request/SignUpRequest.java @@ -0,0 +1,9 @@ +package com.board.member.controller.auth.dto.request; + +public record SignUpRequest( + String memberName, + String memberNickName, + String loginId, + String password +) { +} diff --git a/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java b/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java new file mode 100644 index 0000000..9fa5ad5 --- /dev/null +++ b/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java @@ -0,0 +1,7 @@ +package com.board.member.controller.auth.dto.response; + +public record SignUpResponse( + Long memberId, + String token +) { +} diff --git a/src/main/java/com/board/member/domain/auth/TokenProvider.java b/src/main/java/com/board/member/domain/auth/TokenProvider.java new file mode 100644 index 0000000..b251740 --- /dev/null +++ b/src/main/java/com/board/member/domain/auth/TokenProvider.java @@ -0,0 +1,9 @@ +package com.board.member.domain.auth; + +import com.auth0.jwt.interfaces.DecodedJWT; + +public interface TokenProvider { + + String create(Long memberId); + DecodedJWT verifyToken(String token); +} diff --git a/src/main/java/com/board/member/domain/member/Member.java b/src/main/java/com/board/member/domain/member/Member.java new file mode 100644 index 0000000..3356172 --- /dev/null +++ b/src/main/java/com/board/member/domain/member/Member.java @@ -0,0 +1,44 @@ +package com.board.member.domain.member; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @NotBlank + private String memberName; + + @Column(nullable = false) + @NotBlank + private String memberNickName; + + @Column(nullable = false) + @NotBlank + private String memberLoginId; + + @Column(nullable = false) + @NotBlank + private String memberPassword; + + public Member(String memberName, String memberNickName, String memberLoginId, String memberPassword) { + this.memberName = memberName; + this.memberNickName = memberNickName; + this.memberLoginId = memberLoginId; + this.memberPassword = memberPassword; + } +} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java index b1527a1..3b354ba 100644 --- a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java +++ b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java @@ -5,7 +5,10 @@ @Getter public enum MemberErrorCode { - ; + + DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "M001", "이미 존재하는 아이디 입니다."), + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "M002", "이미 존재하는 닉네임 입니다."); + private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/member/repository/MemberRepository.java b/src/main/java/com/board/member/repository/MemberRepository.java new file mode 100644 index 0000000..88ca1ab --- /dev/null +++ b/src/main/java/com/board/member/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package com.board.member.repository; + +import com.board.member.domain.member.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MemberRepository extends JpaRepository { + + boolean existsByMemberNickName(String memberNickName); + boolean existsByMemberLoginId(String loginId); +} diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java new file mode 100644 index 0000000..41e9ad5 --- /dev/null +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -0,0 +1,40 @@ +package com.board.member.service.auth; + +import com.board.member.controller.auth.dto.request.SignUpRequest; +import com.board.member.controller.auth.dto.response.SignUpResponse; +import com.board.member.domain.auth.TokenProvider; +import com.board.member.domain.member.Member; +import com.board.member.repository.MemberRepository; +import com.board.member.service.auth.exception.ExistLoginIdException; +import com.board.member.service.auth.exception.ExistNickNameException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final MemberRepository memberRepository; + private final TokenProvider tokenProvider; + + public SignUpResponse signUp(SignUpRequest request) { + checkDuplicateLoginId(request.loginId()); + checkDuplicateNickName(request.memberNickName()); + Member member = new Member(request.memberName(), request.memberNickName(), request.loginId(), request.password()); + memberRepository.save(member); + + return new SignUpResponse(member.getId(), tokenProvider.create(member.getId())); + } + + private void checkDuplicateLoginId(String loginId) { + if(memberRepository.existsByMemberLoginId(loginId)) { + throw new ExistLoginIdException(); + } + } + + private void checkDuplicateNickName(String memberNickName) { + if(memberRepository.existsByMemberNickName(memberNickName)) { + throw new ExistNickNameException(); + } + } +} diff --git a/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java b/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java new file mode 100644 index 0000000..3cf634e --- /dev/null +++ b/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java @@ -0,0 +1,11 @@ +package com.board.member.service.auth.exception; + +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; + +public class ExistLoginIdException extends MemberException { + + public ExistLoginIdException() { + super(MemberErrorCode.DUPLICATE_LOGIN_ID); + } +} diff --git a/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java b/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java new file mode 100644 index 0000000..55c349d --- /dev/null +++ b/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java @@ -0,0 +1,11 @@ +package com.board.member.service.auth.exception; + +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; + +public class ExistNickNameException extends MemberException { + + public ExistNickNameException() { + super(MemberErrorCode.DUPLICATE_NICKNAME); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index efe2ab5..3da31aa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,4 @@ spring.application.name=board + +jwt.secret: secretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey +jwt.expiration-period: 7200 From d35c08349a9ad102984db4c2e58626b46deb9fa4 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:26:11 +0900 Subject: [PATCH 03/52] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spring_cs.md | 23 +++++++++++++++++ .../controller/auth/AuthController.java | 19 +++++++++++--- .../auth/dto/request/LoginRequest.java | 7 ++++++ .../board/member/domain/member/Member.java | 8 ++++++ .../exception/NotMatchPasswordException.java | 11 ++++++++ .../exception/exceptions/MemberErrorCode.java | 4 ++- .../member/repository/MemberRepository.java | 2 ++ .../member/service/auth/AuthService.java | 25 ++++++++++++++++--- ...ion.java => NotExistLoginIdException.java} | 4 +-- .../exception/NotExistMemberException.java | 11 ++++++++ ...on.java => NotExistNickNameException.java} | 4 +-- 11 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 docs/spring_cs.md create mode 100644 src/main/java/com/board/member/controller/auth/dto/request/LoginRequest.java create mode 100644 src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java rename src/main/java/com/board/member/service/auth/exception/{ExistLoginIdException.java => NotExistLoginIdException.java} (69%) create mode 100644 src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java rename src/main/java/com/board/member/service/auth/exception/{ExistNickNameException.java => NotExistNickNameException.java} (68%) diff --git a/docs/spring_cs.md b/docs/spring_cs.md new file mode 100644 index 0000000..e87d9ec --- /dev/null +++ b/docs/spring_cs.md @@ -0,0 +1,23 @@ +## @Transactional(readOnly = true) 를 사용한 경우 vs 사용하지 않은 경우 + +--- + +### 1. @Transactional(readOnly = true)를 사용하지 않은 경우 + +1. 트랜잭션이 시작됩니다. +2. SELECT 쿼리를 실행하여 데이터 조회합니다. +3. 트랜잭션이 쓰기 모드이므로 변경 감지(Dirty Checking)가 활성화됩니다. +4. 조회한 엔티티를 영속성 컨텍스트(Persistence Context)에 저장합니다. +5. JPA가 Book 엔티티를 관리하기 시작합니다. +6. 트랜잭션이 종료되면, 변경된 엔티티가 있으면 UPDATE 쿼리를 실행합니다. +7. 변경이 없어도 flush()가 호출되면서 DB와 동기화 수행합니다. → 불필요한 성능 저하 가능 + + +### 2. @Transactional(readOnly = true) 사용한 경우 + +1. 트랜잭션이 시작됩니다. (READ ONLY) +2. SELECT 쿼리를 실행하여 데이터 조회합니다. +3. 쓰기 관련 기능(변경 감지, flush, dirty checking)이 비활성화 됩니다. +4. JPA가 엔티티를 관리하지 않습니다. (영속성 컨텍스트 제외) +5. 트랜잭션이 종료됩니다. + diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java index 02284ff..8871683 100644 --- a/src/main/java/com/board/member/controller/auth/AuthController.java +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -1,27 +1,38 @@ package com.board.member.controller.auth; +import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.service.auth.AuthService; import java.net.URI; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; 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; @RestController -@RequestMapping("/api") +@RequestMapping("/api/members") @RequiredArgsConstructor public class AuthController { private final AuthService authService; - @PostMapping("/member/signUp") - public ResponseEntity createMember(@RequestBody SignUpRequest signUpRequest) { - SignUpResponse response = authService.signUp(signUpRequest); + @PostMapping("/signUp") + public ResponseEntity signUp(@RequestBody SignUpRequest request) { + SignUpResponse response = authService.signUp(request); URI location = URI.create("/api/members/" + response.memberId()); return ResponseEntity.created(location).body(response); } + + @GetMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("Authorization", "Bearer " + authService.login(request)); + return ResponseEntity.status(HttpStatus.OK).body(httpHeaders); + } } diff --git a/src/main/java/com/board/member/controller/auth/dto/request/LoginRequest.java b/src/main/java/com/board/member/controller/auth/dto/request/LoginRequest.java new file mode 100644 index 0000000..ed20876 --- /dev/null +++ b/src/main/java/com/board/member/controller/auth/dto/request/LoginRequest.java @@ -0,0 +1,7 @@ +package com.board.member.controller.auth.dto.request; + +public record LoginRequest( + String loginId, + String password +) { +} diff --git a/src/main/java/com/board/member/domain/member/Member.java b/src/main/java/com/board/member/domain/member/Member.java index 3356172..28a3999 100644 --- a/src/main/java/com/board/member/domain/member/Member.java +++ b/src/main/java/com/board/member/domain/member/Member.java @@ -1,11 +1,13 @@ package com.board.member.domain.member; +import com.board.member.domain.member.exception.NotMatchPasswordException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotBlank; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -41,4 +43,10 @@ public Member(String memberName, String memberNickName, String memberLoginId, St this.memberLoginId = memberLoginId; this.memberPassword = memberPassword; } + + public void checkPassword(String password) { + if(!Objects.equals(memberPassword, password)) { + throw new NotMatchPasswordException(); + } + } } diff --git a/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java b/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java new file mode 100644 index 0000000..f8c1ae8 --- /dev/null +++ b/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java @@ -0,0 +1,11 @@ +package com.board.member.domain.member.exception; + +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; + +public class NotMatchPasswordException extends MemberException { + + public NotMatchPasswordException() { + super(MemberErrorCode.NOT_MATCH_PASSWORD); + } +} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java index 3b354ba..f0999aa 100644 --- a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java +++ b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java @@ -7,7 +7,9 @@ public enum MemberErrorCode { DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "M001", "이미 존재하는 아이디 입니다."), - DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "M002", "이미 존재하는 닉네임 입니다."); + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "M002", "이미 존재하는 닉네임 입니다."), + NOT_MATCH_PASSWORD(HttpStatus.CONFLICT, "M003", "비밀번호가 일치하지 않습니다."), + NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "M004", "유저가 존재하지 않습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/board/member/repository/MemberRepository.java b/src/main/java/com/board/member/repository/MemberRepository.java index 88ca1ab..fb4f7ac 100644 --- a/src/main/java/com/board/member/repository/MemberRepository.java +++ b/src/main/java/com/board/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.board.member.repository; import com.board.member.domain.member.Member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,4 +10,5 @@ public interface MemberRepository extends JpaRepository { boolean existsByMemberNickName(String memberNickName); boolean existsByMemberLoginId(String loginId); + Optional findMemberByMemberLoginId(String loginId); } diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 41e9ad5..3bd0352 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -1,17 +1,21 @@ package com.board.member.service.auth; +import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.domain.auth.TokenProvider; import com.board.member.domain.member.Member; import com.board.member.repository.MemberRepository; -import com.board.member.service.auth.exception.ExistLoginIdException; -import com.board.member.service.auth.exception.ExistNickNameException; +import com.board.member.service.auth.exception.NotExistLoginIdException; +import com.board.member.service.auth.exception.NotExistMemberException; +import com.board.member.service.auth.exception.NotExistNickNameException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional public class AuthService { private final MemberRepository memberRepository; @@ -26,15 +30,28 @@ public SignUpResponse signUp(SignUpRequest request) { return new SignUpResponse(member.getId(), tokenProvider.create(member.getId())); } + @Transactional(readOnly = true) + public String login(LoginRequest request) { + Member member = findMemberByLoginId(request.loginId()); + member.checkPassword(request.password()); + + return tokenProvider.create(member.getId()); + } + + private Member findMemberByLoginId(String loginId) { + return memberRepository.findMemberByMemberLoginId(loginId) + .orElseThrow(NotExistMemberException::new); + } + private void checkDuplicateLoginId(String loginId) { if(memberRepository.existsByMemberLoginId(loginId)) { - throw new ExistLoginIdException(); + throw new NotExistLoginIdException(); } } private void checkDuplicateNickName(String memberNickName) { if(memberRepository.existsByMemberNickName(memberNickName)) { - throw new ExistNickNameException(); + throw new NotExistNickNameException(); } } } diff --git a/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java b/src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java similarity index 69% rename from src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java rename to src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java index 3cf634e..a286957 100644 --- a/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java +++ b/src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java @@ -3,9 +3,9 @@ import com.board.member.exception.exceptions.MemberErrorCode; import com.board.member.exception.exceptions.MemberException; -public class ExistLoginIdException extends MemberException { +public class NotExistLoginIdException extends MemberException { - public ExistLoginIdException() { + public NotExistLoginIdException() { super(MemberErrorCode.DUPLICATE_LOGIN_ID); } } diff --git a/src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java b/src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java new file mode 100644 index 0000000..5e20c8e --- /dev/null +++ b/src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java @@ -0,0 +1,11 @@ +package com.board.member.service.auth.exception; + +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; + +public class NotExistMemberException extends MemberException { + + public NotExistMemberException() { + super(MemberErrorCode.NOT_EXIST_MEMBER); + } +} diff --git a/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java b/src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java similarity index 68% rename from src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java rename to src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java index 55c349d..b81bcad 100644 --- a/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java +++ b/src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java @@ -3,9 +3,9 @@ import com.board.member.exception.exceptions.MemberErrorCode; import com.board.member.exception.exceptions.MemberException; -public class ExistNickNameException extends MemberException { +public class NotExistNickNameException extends MemberException { - public ExistNickNameException() { + public NotExistNickNameException() { super(MemberErrorCode.DUPLICATE_NICKNAME); } } From 10c682d31d52351087dff8f3df600c6a456e61ef Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:03:56 +0900 Subject: [PATCH 04/52] =?UTF-8?q?feat:=20interceptor=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/global/config/WebMvcConfig.java | 20 +++++++++++++++++++ .../GlobalExceptionHandler.java | 18 +++++++++++++++++ .../dto/GlobalErrorResponse.java | 7 +++++++ .../exception/exceptions/GlobalErrorCode.java | 19 ++++++++++++++++++ .../exception/exceptions/GlobalException.java | 14 +++++++++++++ .../global/interceptor/AuthInterceptor.java | 19 ++++++++++++++++++ .../exception/NotFoundTokenException.java | 11 ++++++++++ .../controller/auth/AuthController.java | 2 +- src/main/resources/application-local.yml | 18 +++++++++++++++++ src/main/resources/application.properties | 4 ++-- 10 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/board/global/config/WebMvcConfig.java create mode 100644 src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java create mode 100644 src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java create mode 100644 src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java create mode 100644 src/main/java/com/board/global/exception/exceptions/GlobalException.java create mode 100644 src/main/java/com/board/global/interceptor/AuthInterceptor.java create mode 100644 src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java create mode 100644 src/main/resources/application-local.yml diff --git a/src/main/java/com/board/global/config/WebMvcConfig.java b/src/main/java/com/board/global/config/WebMvcConfig.java new file mode 100644 index 0000000..da4f84f --- /dev/null +++ b/src/main/java/com/board/global/config/WebMvcConfig.java @@ -0,0 +1,20 @@ +package com.board.global.config; + +import com.board.global.interceptor.AuthInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final AuthInterceptor interceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(interceptor) + .addPathPatterns("/api/articles/**") + .addPathPatterns("/api/comments/**") + .addPathPatterns("/api/members/**"); + } +} diff --git a/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java b/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java new file mode 100644 index 0000000..82f43df --- /dev/null +++ b/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java @@ -0,0 +1,18 @@ +package com.board.global.exception.exceptionhandler; + +import com.board.global.exception.exceptionhandler.dto.GlobalErrorResponse; +import com.board.global.exception.exceptions.GlobalErrorCode; +import com.board.global.exception.exceptions.GlobalException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +public class GlobalExceptionHandler { + + @ExceptionHandler(GlobalException.class) + public ResponseEntity handleException(GlobalException e) { + GlobalErrorCode errorCode = e.getErrorCode(); + GlobalErrorResponse response = new GlobalErrorResponse(errorCode.getCustomCode(), errorCode.getMessage()); + + return ResponseEntity.status(errorCode.getHttpStatus()).body(response); + } +} diff --git a/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java b/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java new file mode 100644 index 0000000..571f542 --- /dev/null +++ b/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java @@ -0,0 +1,7 @@ +package com.board.global.exception.exceptionhandler.dto; + +public record GlobalErrorResponse( + String customCode, + String message +) { +} diff --git a/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java b/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java new file mode 100644 index 0000000..77797ab --- /dev/null +++ b/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java @@ -0,0 +1,19 @@ +package com.board.global.exception.exceptions; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum GlobalErrorCode { + NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND, "I001", "토큰을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; + + GlobalErrorCode(HttpStatus httpStatus, String customCode, String message) { + this.httpStatus = httpStatus; + this.customCode = customCode; + this.message = message; + } +} diff --git a/src/main/java/com/board/global/exception/exceptions/GlobalException.java b/src/main/java/com/board/global/exception/exceptions/GlobalException.java new file mode 100644 index 0000000..bd8585b --- /dev/null +++ b/src/main/java/com/board/global/exception/exceptions/GlobalException.java @@ -0,0 +1,14 @@ +package com.board.global.exception.exceptions; + +import lombok.Getter; + +@Getter +public class GlobalException extends RuntimeException { + + private final GlobalErrorCode errorCode; + + public GlobalException(GlobalErrorCode errorCode) { + super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/board/global/interceptor/AuthInterceptor.java b/src/main/java/com/board/global/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..5dadfe8 --- /dev/null +++ b/src/main/java/com/board/global/interceptor/AuthInterceptor.java @@ -0,0 +1,19 @@ +package com.board.global.interceptor; + +import com.board.global.interceptor.exception.NotFoundTokenException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Optional; +import org.springframework.web.servlet.HandlerInterceptor; + +public class AuthInterceptor implements HandlerInterceptor { + + private static final String TOKEN_HEADER_NAME = "Authorization"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) + .orElseThrow(NotFoundTokenException::new); + return tokenHeader.startsWith("Bearer "); + } +} diff --git a/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java b/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java new file mode 100644 index 0000000..786b181 --- /dev/null +++ b/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java @@ -0,0 +1,11 @@ +package com.board.global.interceptor.exception; + +import com.board.global.exception.exceptions.GlobalErrorCode; +import com.board.global.exception.exceptions.GlobalException; + +public class NotFoundTokenException extends GlobalException { + + public NotFoundTokenException() { + super(GlobalErrorCode.NOT_FOUND_TOKEN); + } +} diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java index 8871683..dba6565 100644 --- a/src/main/java/com/board/member/controller/auth/AuthController.java +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/members") +@RequestMapping("/api") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..7c731d9 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,18 @@ +spring: + h2: + console: + enabled: true + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL + username: sa + password: sa + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + show_sql: true + default_batch_fetch_size: 100 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3da31aa..74d6d73 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ spring.application.name=board -jwt.secret: secretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey -jwt.expiration-period: 7200 +jwt.secret=secretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey +jwt.expiration-period=7200 From 242e533a5fcb6c01ef2b76e6623c0540f3b86607 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:54:22 +0900 Subject: [PATCH 05/52] =?UTF-8?q?feat:=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/exceptions/GlobalErrorCode.java | 6 +++- .../global/interceptor/AuthInterceptor.java | 2 ++ .../global/resolver/AuthArgumentResolver.java | 35 +++++++++++++++++++ .../global/resolver/annotation/Auth.java | 11 ++++++ .../exception/TokenExpirationException.java | 11 ++++++ .../exception/TokenInvalidException.java | 11 ++++++ .../exception/TokenVerificationException.java | 11 ++++++ .../exception/exceptions/MemberErrorCode.java | 1 - .../member/service/auth/AuthService.java | 27 ++++++++++++++ 9 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/board/global/resolver/AuthArgumentResolver.java create mode 100644 src/main/java/com/board/global/resolver/annotation/Auth.java create mode 100644 src/main/java/com/board/global/resolver/exception/TokenExpirationException.java create mode 100644 src/main/java/com/board/global/resolver/exception/TokenInvalidException.java create mode 100644 src/main/java/com/board/global/resolver/exception/TokenVerificationException.java diff --git a/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java b/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java index 77797ab..2264406 100644 --- a/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java +++ b/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java @@ -5,7 +5,11 @@ @Getter public enum GlobalErrorCode { - NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND, "I001", "토큰을 찾을 수 없습니다."); + + NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND, "T001", "토큰을 찾을 수 없습니다."), + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "T002", "토큰이 유효하지 않습니다."), + CAN_NOT_VERIFY_TOKEN(HttpStatus.BAD_REQUEST, "T003", "토큰을 검증할 수 없습니다."), + EXPIRED_TOKEN(HttpStatus.CONFLICT, "T004", "토큰 유효기간이 만료되었습니다."); private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/global/interceptor/AuthInterceptor.java b/src/main/java/com/board/global/interceptor/AuthInterceptor.java index 5dadfe8..789fa1d 100644 --- a/src/main/java/com/board/global/interceptor/AuthInterceptor.java +++ b/src/main/java/com/board/global/interceptor/AuthInterceptor.java @@ -4,8 +4,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Optional; +import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +@Component public class AuthInterceptor implements HandlerInterceptor { private static final String TOKEN_HEADER_NAME = "Authorization"; diff --git a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java new file mode 100644 index 0000000..a96ae64 --- /dev/null +++ b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java @@ -0,0 +1,35 @@ +package com.board.global.resolver; + +import com.board.global.resolver.annotation.Auth; +import com.board.member.service.auth.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String TOKEN_HEADER_NAME = "Authorization"; + + private final AuthService service; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Auth.class) && parameter.getParameterType() == Long.class; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String tokenHeader = request.getHeader(TOKEN_HEADER_NAME); + String token = tokenHeader.substring(7); + return service.verifyAndExtractToken(token); + } +} diff --git a/src/main/java/com/board/global/resolver/annotation/Auth.java b/src/main/java/com/board/global/resolver/annotation/Auth.java new file mode 100644 index 0000000..108be29 --- /dev/null +++ b/src/main/java/com/board/global/resolver/annotation/Auth.java @@ -0,0 +1,11 @@ +package com.board.global.resolver.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Auth { +} diff --git a/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java b/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java new file mode 100644 index 0000000..dccf63f --- /dev/null +++ b/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java @@ -0,0 +1,11 @@ +package com.board.global.resolver.exception; + +import com.board.global.exception.exceptions.GlobalErrorCode; +import com.board.global.exception.exceptions.GlobalException; + +public class TokenExpirationException extends GlobalException { + + public TokenExpirationException() { + super(GlobalErrorCode.EXPIRED_TOKEN); + } +} diff --git a/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java b/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java new file mode 100644 index 0000000..2b874d2 --- /dev/null +++ b/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java @@ -0,0 +1,11 @@ +package com.board.global.resolver.exception; + +import com.board.global.exception.exceptions.GlobalErrorCode; +import com.board.global.exception.exceptions.GlobalException; + +public class TokenInvalidException extends GlobalException { + + public TokenInvalidException() { + super(GlobalErrorCode.INVALID_TOKEN); + } +} diff --git a/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java b/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java new file mode 100644 index 0000000..51879f0 --- /dev/null +++ b/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java @@ -0,0 +1,11 @@ +package com.board.global.resolver.exception; + +import com.board.global.exception.exceptions.GlobalErrorCode; +import com.board.global.exception.exceptions.GlobalException; + +public class TokenVerificationException extends GlobalException { + + public TokenVerificationException() { + super(GlobalErrorCode.CAN_NOT_VERIFY_TOKEN); + } +} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java index f0999aa..c74ffb3 100644 --- a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java +++ b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java @@ -11,7 +11,6 @@ public enum MemberErrorCode { NOT_MATCH_PASSWORD(HttpStatus.CONFLICT, "M003", "비밀번호가 일치하지 않습니다."), NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "M004", "유저가 존재하지 않습니다."); - private final HttpStatus httpStatus; private final String customCode; private final String message; diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 3bd0352..891aa57 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -1,5 +1,10 @@ package com.board.member.service.auth; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.board.global.resolver.exception.TokenExpirationException; +import com.board.global.resolver.exception.TokenVerificationException; import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; import com.board.member.controller.auth.dto.response.SignUpResponse; @@ -9,6 +14,8 @@ import com.board.member.service.auth.exception.NotExistLoginIdException; import com.board.member.service.auth.exception.NotExistMemberException; import com.board.member.service.auth.exception.NotExistNickNameException; +import com.board.global.resolver.exception.TokenInvalidException; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -38,6 +45,26 @@ public String login(LoginRequest request) { return tokenProvider.create(member.getId()); } + public Long verifyAndExtractToken(String token) { + try { + return extractToken(token); + } catch (JWTDecodeException e) { + throw new TokenVerificationException(); + } catch (TokenExpiredException e) { + throw new TokenExpirationException(); + } + } + + private Long extractToken(String token) { + return verifyToken(token).getClaim("memberId") + .asLong(); + } + + private DecodedJWT verifyToken(String token) { + return Optional.of(tokenProvider.verifyToken(token)) + .orElseThrow(TokenInvalidException::new); + } + private Member findMemberByLoginId(String loginId) { return memberRepository.findMemberByMemberLoginId(loginId) .orElseThrow(NotExistMemberException::new); From 0a4e8bb8799872817e2c6d08b7bdd461994297b2 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:55:34 +0900 Subject: [PATCH 06/52] =?UTF-8?q?feat:=20=EB=A6=AC=EC=A1=B8=EB=B2=84=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/global/config/WebMvcConfig.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/board/global/config/WebMvcConfig.java b/src/main/java/com/board/global/config/WebMvcConfig.java index da4f84f..8fad362 100644 --- a/src/main/java/com/board/global/config/WebMvcConfig.java +++ b/src/main/java/com/board/global/config/WebMvcConfig.java @@ -1,7 +1,10 @@ package com.board.global.config; import com.board.global.interceptor.AuthInterceptor; +import com.board.global.resolver.AuthArgumentResolver; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,6 +12,7 @@ public class WebMvcConfig implements WebMvcConfigurer { private final AuthInterceptor interceptor; + private final AuthArgumentResolver resolver; @Override public void addInterceptors(InterceptorRegistry registry) { @@ -17,4 +21,9 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/api/comments/**") .addPathPatterns("/api/members/**"); } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(resolver); + } } From 66961ed1c2cb7a319e8298976036ee96f6b25282 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:14:02 +0900 Subject: [PATCH 07/52] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=9A=8C=EC=9B=90=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/global/config/WebMvcConfig.java | 2 ++ .../controller/member/MemberController.java | 23 ++++++++++++++++ .../member/dto/reponse/MemberResponse.java | 9 +++++++ .../member/repository/MemberRepository.java | 1 + .../member/service/member/MemberService.java | 27 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 src/main/java/com/board/member/controller/member/MemberController.java create mode 100644 src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java create mode 100644 src/main/java/com/board/member/service/member/MemberService.java diff --git a/src/main/java/com/board/global/config/WebMvcConfig.java b/src/main/java/com/board/global/config/WebMvcConfig.java index 8fad362..e117774 100644 --- a/src/main/java/com/board/global/config/WebMvcConfig.java +++ b/src/main/java/com/board/global/config/WebMvcConfig.java @@ -4,10 +4,12 @@ import com.board.global.resolver.AuthArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +@Component @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java new file mode 100644 index 0000000..aa38adf --- /dev/null +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -0,0 +1,23 @@ +package com.board.member.controller.member; + +import com.board.global.resolver.annotation.Auth; +import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.service.member.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + + @GetMapping("/members") + public ResponseEntity showMember(@Auth Long memberId) { + return ResponseEntity.ok(memberService.getMember(memberId)); + } +} diff --git a/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java b/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java new file mode 100644 index 0000000..ab20514 --- /dev/null +++ b/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java @@ -0,0 +1,9 @@ +package com.board.member.controller.member.dto.reponse; + +public record MemberResponse( + String mame, + String nickName, + String id, + String password +) { +} diff --git a/src/main/java/com/board/member/repository/MemberRepository.java b/src/main/java/com/board/member/repository/MemberRepository.java index fb4f7ac..d72b26a 100644 --- a/src/main/java/com/board/member/repository/MemberRepository.java +++ b/src/main/java/com/board/member/repository/MemberRepository.java @@ -11,4 +11,5 @@ public interface MemberRepository extends JpaRepository { boolean existsByMemberNickName(String memberNickName); boolean existsByMemberLoginId(String loginId); Optional findMemberByMemberLoginId(String loginId); + Member findMemberById(Long id); } diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java new file mode 100644 index 0000000..36592fa --- /dev/null +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -0,0 +1,27 @@ +package com.board.member.service.member; + +import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.domain.member.Member; +import com.board.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class MemberService { + + private final MemberRepository memberRepository; + + @Transactional(readOnly = true) + public MemberResponse getMember(Long memberId) { + Member member = memberRepository.findMemberById(memberId); + return new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword() + ); + } +} From aa4cf67aeee65e1d6e4fde0ebe2e1b09b24fe58d Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:43:43 +0900 Subject: [PATCH 08/52] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/member/MemberController.java | 10 ++++++++- .../member/dto/request/MemberRequest.java | 9 ++++++++ .../board/member/domain/member/Member.java | 15 +++++++++++++ .../exception/exceptions/MemberErrorCode.java | 3 ++- .../member/repository/MemberRepository.java | 2 +- .../member/service/auth/AuthService.java | 12 +++++------ ...eption.java => ExistLoginIdException.java} | 4 ++-- ...ption.java => ExistNickNameException.java} | 4 ++-- ...ion.java => NotMatchLoginIdException.java} | 6 +++--- .../member/service/member/MemberService.java | 21 +++++++++++++++++-- .../exception/NotFoundMemberException.java | 11 ++++++++++ 11 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/board/member/controller/member/dto/request/MemberRequest.java rename src/main/java/com/board/member/service/auth/exception/{NotExistLoginIdException.java => ExistLoginIdException.java} (69%) rename src/main/java/com/board/member/service/auth/exception/{NotExistNickNameException.java => ExistNickNameException.java} (68%) rename src/main/java/com/board/member/service/auth/exception/{NotExistMemberException.java => NotMatchLoginIdException.java} (54%) create mode 100644 src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index aa38adf..86b2e2c 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -2,10 +2,13 @@ import com.board.global.resolver.annotation.Auth; import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.service.member.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,6 +21,11 @@ public class MemberController { @GetMapping("/members") public ResponseEntity showMember(@Auth Long memberId) { - return ResponseEntity.ok(memberService.getMember(memberId)); + return ResponseEntity.ok(memberService.showMember(memberId)); + } + + @PatchMapping("/members") + public ResponseEntity updateMember(@Auth Long memberId, @RequestBody MemberRequest request) { + return ResponseEntity.ok(memberService.updateMember(memberId, request)); } } diff --git a/src/main/java/com/board/member/controller/member/dto/request/MemberRequest.java b/src/main/java/com/board/member/controller/member/dto/request/MemberRequest.java new file mode 100644 index 0000000..2187fc5 --- /dev/null +++ b/src/main/java/com/board/member/controller/member/dto/request/MemberRequest.java @@ -0,0 +1,9 @@ +package com.board.member.controller.member.dto.request; + +public record MemberRequest( + String name, + String nickName, + String id, + String password +) { +} diff --git a/src/main/java/com/board/member/domain/member/Member.java b/src/main/java/com/board/member/domain/member/Member.java index 28a3999..88fa851 100644 --- a/src/main/java/com/board/member/domain/member/Member.java +++ b/src/main/java/com/board/member/domain/member/Member.java @@ -44,6 +44,21 @@ public Member(String memberName, String memberNickName, String memberLoginId, St this.memberPassword = memberPassword; } + public void update(String memberName, String memberNickName, String memberLoginId, String memberPassword) { + if (memberName != null) { + this.memberName = memberName; + } + if (memberNickName != null) { + this.memberNickName = memberNickName; + } + if (memberLoginId != null) { + this.memberLoginId = memberLoginId; + } + if (memberPassword != null) { + this.memberPassword = memberPassword; + } + } + public void checkPassword(String password) { if(!Objects.equals(memberPassword, password)) { throw new NotMatchPasswordException(); diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java index c74ffb3..5a3624e 100644 --- a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java +++ b/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java @@ -9,7 +9,8 @@ public enum MemberErrorCode { DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "M001", "이미 존재하는 아이디 입니다."), DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "M002", "이미 존재하는 닉네임 입니다."), NOT_MATCH_PASSWORD(HttpStatus.CONFLICT, "M003", "비밀번호가 일치하지 않습니다."), - NOT_EXIST_MEMBER(HttpStatus.NOT_FOUND, "M004", "유저가 존재하지 않습니다."); + NOT_MATCH_LOGIN_ID(HttpStatus.NOT_FOUND, "M004", "아이디가 틀렸습니다."), + NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "M005", "유저를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/member/repository/MemberRepository.java b/src/main/java/com/board/member/repository/MemberRepository.java index d72b26a..33406dd 100644 --- a/src/main/java/com/board/member/repository/MemberRepository.java +++ b/src/main/java/com/board/member/repository/MemberRepository.java @@ -11,5 +11,5 @@ public interface MemberRepository extends JpaRepository { boolean existsByMemberNickName(String memberNickName); boolean existsByMemberLoginId(String loginId); Optional findMemberByMemberLoginId(String loginId); - Member findMemberById(Long id); + Optional findMemberById(Long id); } diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 891aa57..1b9041e 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -11,9 +11,9 @@ import com.board.member.domain.auth.TokenProvider; import com.board.member.domain.member.Member; import com.board.member.repository.MemberRepository; -import com.board.member.service.auth.exception.NotExistLoginIdException; -import com.board.member.service.auth.exception.NotExistMemberException; -import com.board.member.service.auth.exception.NotExistNickNameException; +import com.board.member.service.auth.exception.ExistLoginIdException; +import com.board.member.service.auth.exception.NotMatchLoginIdException; +import com.board.member.service.auth.exception.ExistNickNameException; import com.board.global.resolver.exception.TokenInvalidException; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -67,18 +67,18 @@ private DecodedJWT verifyToken(String token) { private Member findMemberByLoginId(String loginId) { return memberRepository.findMemberByMemberLoginId(loginId) - .orElseThrow(NotExistMemberException::new); + .orElseThrow(NotMatchLoginIdException::new); } private void checkDuplicateLoginId(String loginId) { if(memberRepository.existsByMemberLoginId(loginId)) { - throw new NotExistLoginIdException(); + throw new ExistLoginIdException(); } } private void checkDuplicateNickName(String memberNickName) { if(memberRepository.existsByMemberNickName(memberNickName)) { - throw new NotExistNickNameException(); + throw new ExistNickNameException(); } } } diff --git a/src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java b/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java similarity index 69% rename from src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java rename to src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java index a286957..3cf634e 100644 --- a/src/main/java/com/board/member/service/auth/exception/NotExistLoginIdException.java +++ b/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java @@ -3,9 +3,9 @@ import com.board.member.exception.exceptions.MemberErrorCode; import com.board.member.exception.exceptions.MemberException; -public class NotExistLoginIdException extends MemberException { +public class ExistLoginIdException extends MemberException { - public NotExistLoginIdException() { + public ExistLoginIdException() { super(MemberErrorCode.DUPLICATE_LOGIN_ID); } } diff --git a/src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java b/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java similarity index 68% rename from src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java rename to src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java index b81bcad..55c349d 100644 --- a/src/main/java/com/board/member/service/auth/exception/NotExistNickNameException.java +++ b/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java @@ -3,9 +3,9 @@ import com.board.member.exception.exceptions.MemberErrorCode; import com.board.member.exception.exceptions.MemberException; -public class NotExistNickNameException extends MemberException { +public class ExistNickNameException extends MemberException { - public NotExistNickNameException() { + public ExistNickNameException() { super(MemberErrorCode.DUPLICATE_NICKNAME); } } diff --git a/src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java b/src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java similarity index 54% rename from src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java rename to src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java index 5e20c8e..6a48c02 100644 --- a/src/main/java/com/board/member/service/auth/exception/NotExistMemberException.java +++ b/src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java @@ -3,9 +3,9 @@ import com.board.member.exception.exceptions.MemberErrorCode; import com.board.member.exception.exceptions.MemberException; -public class NotExistMemberException extends MemberException { +public class NotMatchLoginIdException extends MemberException { - public NotExistMemberException() { - super(MemberErrorCode.NOT_EXIST_MEMBER); + public NotMatchLoginIdException() { + super(MemberErrorCode.NOT_MATCH_LOGIN_ID); } } diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index 36592fa..c245ff8 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -1,8 +1,10 @@ package com.board.member.service.member; import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.domain.member.Member; import com.board.member.repository.MemberRepository; +import com.board.member.service.member.exception.NotFoundMemberException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,8 +17,8 @@ public class MemberService { private final MemberRepository memberRepository; @Transactional(readOnly = true) - public MemberResponse getMember(Long memberId) { - Member member = memberRepository.findMemberById(memberId); + public MemberResponse showMember(Long memberId) { + Member member = getMember(memberId); return new MemberResponse( member.getMemberName(), member.getMemberNickName(), @@ -24,4 +26,19 @@ public MemberResponse getMember(Long memberId) { member.getMemberPassword() ); } + + public MemberResponse updateMember(Long memberId, MemberRequest request) { + Member member = getMember(memberId); + member.update(request.name(), request.nickName(), request.id(), request.password()); + return new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword()); + } + + private Member getMember(Long memberId) { + return memberRepository.findMemberById(memberId) + .orElseThrow(NotFoundMemberException::new); + } } diff --git a/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java b/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java new file mode 100644 index 0000000..7564785 --- /dev/null +++ b/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java @@ -0,0 +1,11 @@ +package com.board.member.service.member.exception; + +import com.board.member.exception.exceptions.MemberErrorCode; +import com.board.member.exception.exceptions.MemberException; + +public class NotFoundMemberException extends MemberException { + + public NotFoundMemberException() { + super(MemberErrorCode.NOT_FOUND_MEMBER); + } +} From 6d60d48c0d885a8a9b7d5655ab61ac8c784ca2dd Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:52:12 +0900 Subject: [PATCH 09/52] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B3=84=EC=A0=95=20=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/member/MemberController.java | 6 ++++++ .../board/member/service/member/MemberService.java | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index 86b2e2c..0acfb68 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -6,6 +6,7 @@ import com.board.member.service.member.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -28,4 +29,9 @@ public ResponseEntity showMember(@Auth Long memberId) { public ResponseEntity updateMember(@Auth Long memberId, @RequestBody MemberRequest request) { return ResponseEntity.ok(memberService.updateMember(memberId, request)); } + + @DeleteMapping("/members") + public ResponseEntity deleteMember(@Auth Long memberId) { + return ResponseEntity.ok(memberService.deleteMember(memberId)); + } } diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index c245ff8..191c725 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -34,7 +34,19 @@ public MemberResponse updateMember(Long memberId, MemberRequest request) { member.getMemberName(), member.getMemberNickName(), member.getMemberLoginId(), - member.getMemberPassword()); + member.getMemberPassword() + ); + } + + public MemberResponse deleteMember(Long memberId) { + Member member = getMember(memberId); + memberRepository.delete(member); + return new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword() + ); } private Member getMember(Long memberId) { From 8d6a82414b4ac7ccc86b922ef40399f376e995c6 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:17:21 +0900 Subject: [PATCH 10/52] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 25 +++++++++++++ .../dto/request/ArticleRequest.java | 7 ++++ .../dto/response/ArticleResponse.java | 9 +++++ .../com/board/article/domain/Article.java | 37 +++++++++++++++++++ .../ArticleExceptionHandler.java | 23 ++++++++++++ .../dto/ArticleErrorResponse.java | 7 ++++ .../exceptions/ArticleErrorCode.java | 19 ++++++++++ .../exceptions/ArticleException.java | 14 +++++++ .../article/repository/ArticleRepository.java | 7 ++++ .../board/article/service/ArticleService.java | 27 ++++++++++++++ 10 files changed, 175 insertions(+) create mode 100644 src/main/java/com/board/article/controller/ArticleController.java create mode 100644 src/main/java/com/board/article/controller/dto/request/ArticleRequest.java create mode 100644 src/main/java/com/board/article/controller/dto/response/ArticleResponse.java create mode 100644 src/main/java/com/board/article/domain/Article.java create mode 100644 src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java create mode 100644 src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java create mode 100644 src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java create mode 100644 src/main/java/com/board/article/exception/exceptions/ArticleException.java create mode 100644 src/main/java/com/board/article/repository/ArticleRepository.java create mode 100644 src/main/java/com/board/article/service/ArticleService.java diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java new file mode 100644 index 0000000..af35004 --- /dev/null +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -0,0 +1,25 @@ +package com.board.article.controller; + +import com.board.article.controller.dto.request.ArticleRequest; +import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.service.ArticleService; +import com.board.global.resolver.annotation.Auth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class ArticleController { + + private final ArticleService articleService; + + @PostMapping("/articles") + public ResponseEntity createArticle(@RequestBody ArticleRequest request, @Auth Long memberId) { + return ResponseEntity.ok(articleService.createArticle(request, memberId)); + } +} diff --git a/src/main/java/com/board/article/controller/dto/request/ArticleRequest.java b/src/main/java/com/board/article/controller/dto/request/ArticleRequest.java new file mode 100644 index 0000000..2b444b6 --- /dev/null +++ b/src/main/java/com/board/article/controller/dto/request/ArticleRequest.java @@ -0,0 +1,7 @@ +package com.board.article.controller.dto.request; + +public record ArticleRequest( + String title, + String content +) { +} diff --git a/src/main/java/com/board/article/controller/dto/response/ArticleResponse.java b/src/main/java/com/board/article/controller/dto/response/ArticleResponse.java new file mode 100644 index 0000000..277c68d --- /dev/null +++ b/src/main/java/com/board/article/controller/dto/response/ArticleResponse.java @@ -0,0 +1,9 @@ +package com.board.article.controller.dto.response; + +public record ArticleResponse( + Long articleId, + Long memberId, + String title, + String content +) { +} diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java new file mode 100644 index 0000000..75459ff --- /dev/null +++ b/src/main/java/com/board/article/domain/Article.java @@ -0,0 +1,37 @@ +package com.board.article.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@Getter +public class Article { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private Long memberId; + + @Column(nullable = false) + @NotBlank + private String title; + + @Column(nullable = false) + @NotBlank + private String content; + + public Article(Long memberId, String title, String content) { + this.memberId = memberId; + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java b/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java new file mode 100644 index 0000000..48dd493 --- /dev/null +++ b/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java @@ -0,0 +1,23 @@ +package com.board.article.exception.exceptionhandler; + +import com.board.article.exception.exceptionhandler.dto.ArticleErrorResponse; +import com.board.article.exception.exceptions.ArticleErrorCode; +import com.board.article.exception.exceptions.ArticleException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class ArticleExceptionHandler { + + @ExceptionHandler(ArticleException.class) + public ResponseEntity handleException(ArticleException e) { + ArticleErrorCode articleErrorCode = e.getErrorCode(); + ArticleErrorResponse response = new ArticleErrorResponse( + articleErrorCode.getCustomCode(), + articleErrorCode.getMessage() + ); + + return ResponseEntity.status(articleErrorCode.getHttpStatus()).body(response); + } +} diff --git a/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java b/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java new file mode 100644 index 0000000..805f43c --- /dev/null +++ b/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java @@ -0,0 +1,7 @@ +package com.board.article.exception.exceptionhandler.dto; + +public record ArticleErrorResponse( + String customCode, + String message +) { +} diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java new file mode 100644 index 0000000..ccd13c8 --- /dev/null +++ b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java @@ -0,0 +1,19 @@ +package com.board.article.exception.exceptions; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ArticleErrorCode { + ; + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; + + ArticleErrorCode(HttpStatus httpStatus, String customCode, String message) { + this.httpStatus = httpStatus; + this.customCode = customCode; + this.message = message; + } +} diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleException.java b/src/main/java/com/board/article/exception/exceptions/ArticleException.java new file mode 100644 index 0000000..3d8b8f7 --- /dev/null +++ b/src/main/java/com/board/article/exception/exceptions/ArticleException.java @@ -0,0 +1,14 @@ +package com.board.article.exception.exceptions; + +import lombok.Getter; + +@Getter +public class ArticleException extends RuntimeException{ + + private final ArticleErrorCode errorCode; + + public ArticleException(ArticleErrorCode errorCode) { + super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java new file mode 100644 index 0000000..6eac1f7 --- /dev/null +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -0,0 +1,7 @@ +package com.board.article.repository; + +import com.board.article.domain.Article; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleRepository extends JpaRepository { +} diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java new file mode 100644 index 0000000..1e40e50 --- /dev/null +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -0,0 +1,27 @@ +package com.board.article.service; + +import com.board.article.controller.dto.request.ArticleRequest; +import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.domain.Article; +import com.board.article.repository.ArticleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ArticleService { + + private final ArticleRepository articleRepository; + + public ArticleResponse createArticle(ArticleRequest request, Long memberId) { + Article article = new Article(memberId, request.title(), request.content()); + articleRepository.save(article); + + return new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + } +} From 278f6bc1febdf844bd5c8a045ca5e5bd16d161e2 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 00:03:56 +0900 Subject: [PATCH 11/52] =?UTF-8?q?feat:=20api=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=8D=94=EB=AF=B8=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/loader/ArticleDataLoader.java | 19 ++++++++++++++++++ .../board/member/loader/MemberDataLoader.java | 20 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/com/board/article/loader/ArticleDataLoader.java create mode 100644 src/main/java/com/board/member/loader/MemberDataLoader.java diff --git a/src/main/java/com/board/article/loader/ArticleDataLoader.java b/src/main/java/com/board/article/loader/ArticleDataLoader.java new file mode 100644 index 0000000..9e2c9da --- /dev/null +++ b/src/main/java/com/board/article/loader/ArticleDataLoader.java @@ -0,0 +1,19 @@ +package com.board.article.loader; + +import com.board.article.domain.Article; +import com.board.article.repository.ArticleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ArticleDataLoader implements CommandLineRunner { + + private final ArticleRepository repository; + + @Override + public void run(String... args) throws Exception { + repository.save(new Article(1L, "신형만에 관하여", "아내는 신봉선")); + } +} diff --git a/src/main/java/com/board/member/loader/MemberDataLoader.java b/src/main/java/com/board/member/loader/MemberDataLoader.java new file mode 100644 index 0000000..d2ebc77 --- /dev/null +++ b/src/main/java/com/board/member/loader/MemberDataLoader.java @@ -0,0 +1,20 @@ +package com.board.member.loader; + +import com.board.member.domain.member.Member; +import com.board.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberDataLoader implements CommandLineRunner { + + private final MemberRepository repository; + + @Override + public void run(String... args) throws Exception { + repository.save(new Member("신짱구", "짱구", "aaa", "password123")); + repository.save(new Member("신짱아", "짱아", "sss", "password456")); + } +} From 56e5bda1605b50a362b8418119a3ef87af25d151 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 00:20:07 +0900 Subject: [PATCH 12/52] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 13 +++++++++- .../dto/response/ArticleResponses.java | 8 ++++++ .../article/loader/ArticleDataLoader.java | 1 + .../board/article/service/ArticleService.java | 26 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/board/article/controller/dto/response/ArticleResponses.java diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index af35004..49a69f1 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -2,10 +2,13 @@ import com.board.article.controller.dto.request.ArticleRequest; import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.service.ArticleService; import com.board.global.resolver.annotation.Auth; +import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,6 +23,14 @@ public class ArticleController { @PostMapping("/articles") public ResponseEntity createArticle(@RequestBody ArticleRequest request, @Auth Long memberId) { - return ResponseEntity.ok(articleService.createArticle(request, memberId)); + ArticleResponse response = articleService.createArticle(request, memberId); + URI location = URI.create("/api/articles/" + response.articleId()); + return ResponseEntity.created(location) + .body(response); + } + + @GetMapping("/articles") + public ResponseEntity showAllArticles() { + return ResponseEntity.ok(articleService.showAllArticles()); } } diff --git a/src/main/java/com/board/article/controller/dto/response/ArticleResponses.java b/src/main/java/com/board/article/controller/dto/response/ArticleResponses.java new file mode 100644 index 0000000..9ee8be7 --- /dev/null +++ b/src/main/java/com/board/article/controller/dto/response/ArticleResponses.java @@ -0,0 +1,8 @@ +package com.board.article.controller.dto.response; + +import java.util.List; + +public record ArticleResponses( + List articleResponses +) { +} diff --git a/src/main/java/com/board/article/loader/ArticleDataLoader.java b/src/main/java/com/board/article/loader/ArticleDataLoader.java index 9e2c9da..3d03241 100644 --- a/src/main/java/com/board/article/loader/ArticleDataLoader.java +++ b/src/main/java/com/board/article/loader/ArticleDataLoader.java @@ -15,5 +15,6 @@ public class ArticleDataLoader implements CommandLineRunner { @Override public void run(String... args) throws Exception { repository.save(new Article(1L, "신형만에 관하여", "아내는 신봉선")); + repository.save(new Article(1L, "신짱구", "바보")); } } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 1e40e50..dc2af4d 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -2,13 +2,18 @@ import com.board.article.controller.dto.request.ArticleRequest; import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; import com.board.article.repository.ArticleRepository; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional public class ArticleService { private final ArticleRepository articleRepository; @@ -24,4 +29,25 @@ public ArticleResponse createArticle(ArticleRequest request, Long memberId) { article.getContent() ); } + + public ArticleResponses showAllArticles() { + List articleResponses = new ArrayList<>(); + List
articles = getAllArticles(); + + for (Article article : articles) { + articleResponses.add(new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )); + } + + return new ArticleResponses(articleResponses); + } + + @Transactional(readOnly = true) + public List
getAllArticles() { + return articleRepository.findAll(); + } } From e2c2facc978f72cb2ef18ab98c3fe7d1f6f497d9 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 00:45:05 +0900 Subject: [PATCH 13/52] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/spring_annotation.md | 41 +++++++++++++++++++ .../article/controller/ArticleController.java | 6 +++ .../exceptions/ArticleErrorCode.java | 3 +- .../board/article/service/ArticleService.java | 18 ++++++++ .../exception/NotFoundArticleException.java | 11 +++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/board/article/service/exception/NotFoundArticleException.java diff --git a/docs/spring_annotation.md b/docs/spring_annotation.md index 0c238d5..84fad7a 100644 --- a/docs/spring_annotation.md +++ b/docs/spring_annotation.md @@ -44,3 +44,44 @@ 2. Reflection을 이용하여 객체를 만들기 때문에, 기본 생성자가 반드시 필요합니다. 3. 외부에서 기본 생성자를 직접 호출하는 것을 막기 위해 protected 사용합니다. 3. 만약 기본 생성자가 없으면 InstantiationException 오류 발생! + + +## @RequestParam vs @PathVariable 차이점 + +--- + +### 1. @RequestParam + +* `@RequestParam`은 **쿼리 문자열**에서 값을 추출합니다. - (쿼리 파라미터) +* 예시 +~~~ +@GetMapping("/foos") +@ResponseBody +public String getFooByIdUsingQueryParam(@RequestParam String id) { + return "ID: " + id; +} +~~~ + +해당 요청을 처리하는 URL는 다음과 같습니다. - > `http://localhost:8080/spring-mvc-basics/foos?id=abc` + +* @RequestParam은 URL 디코딩되어 값을 추출합니다. +`http://localhost:8080/foos?id=ab+c` -> `ab c` 를 추출합니다. (+ 가 공백으로 디코딩 됌) +* @RequestParam은 필터링이나 검색 조건을 전달할 때 유용합니다. + +### 2. @PathVariable + +* `@PathVariable`은 **URI 경로**에서 값을 추출합니다. - (URI 경로) +* 예시 +~~~ +@GetMapping({"/myfoos/optional", "/myfoos/optional/{id}"}) +@ResponseBody +public String getFooByOptionalId(@PathVariable(required = false) String id){ + return "ID: " + id; +} +~~~ + +해당 요청을 처리하는 URL 은 다음과 같습니다. -> `http://localhost:8080/spring-mvc-basics/myfoos/optional/abc` + +* @PathVariable은 URI 경로에서 값을 추출하기 때문에 값이 인코딩되지 않습니다. +`http://localhost:8080/foos/id=ab+c` -> `ab+b` 값을 정확하게 추출합니다. +* @PathVariable은 주로 리소스를 식별할 때 유용합니다. diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index 49a69f1..6162862 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,4 +34,9 @@ public ResponseEntity createArticle(@RequestBody ArticleRequest public ResponseEntity showAllArticles() { return ResponseEntity.ok(articleService.showAllArticles()); } + + @GetMapping("/articles/{articleId}") + public ResponseEntity showArticle(@PathVariable Long articleId) { + return ResponseEntity.ok(articleService.showArticle(articleId)); + } } diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java index ccd13c8..992f2ef 100644 --- a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java +++ b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java @@ -5,7 +5,8 @@ @Getter public enum ArticleErrorCode { - ; + + NOT_FOUND_ARTICLE(HttpStatus.NOT_FOUND, "A001", "게시글을 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index dc2af4d..bc18552 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -5,6 +5,7 @@ import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; import com.board.article.repository.ArticleRepository; +import com.board.article.service.exception.NotFoundArticleException; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -46,8 +47,25 @@ public ArticleResponses showAllArticles() { return new ArticleResponses(articleResponses); } + public ArticleResponse showArticle(Long articleId) { + Article article = getArticle(articleId); + + return new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + } + @Transactional(readOnly = true) public List
getAllArticles() { return articleRepository.findAll(); } + + @Transactional(readOnly = true) + public Article getArticle(Long articleId) { + return articleRepository.findById(articleId) + .orElseThrow(NotFoundArticleException::new); + } } diff --git a/src/main/java/com/board/article/service/exception/NotFoundArticleException.java b/src/main/java/com/board/article/service/exception/NotFoundArticleException.java new file mode 100644 index 0000000..a78cb1a --- /dev/null +++ b/src/main/java/com/board/article/service/exception/NotFoundArticleException.java @@ -0,0 +1,11 @@ +package com.board.article.service.exception; + +import com.board.article.exception.exceptions.ArticleErrorCode; +import com.board.article.exception.exceptions.ArticleException; + +public class NotFoundArticleException extends ArticleException { + + public NotFoundArticleException() { + super(ArticleErrorCode.NOT_FOUND_ARTICLE); + } +} From 8c4ab2e3be45c82ba2db9a7f194ce0d0f5702ec4 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 00:53:17 +0900 Subject: [PATCH 14/52] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 5 +++ .../article/repository/ArticleRepository.java | 3 ++ .../board/article/service/ArticleService.java | 38 +++++++++++++------ 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index 6162862..077fe8a 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -39,4 +39,9 @@ public ResponseEntity showAllArticles() { public ResponseEntity showArticle(@PathVariable Long articleId) { return ResponseEntity.ok(articleService.showArticle(articleId)); } + + @GetMapping("/members/me/articles") + public ResponseEntity showMemberArticles(@Auth Long memberId) { + return ResponseEntity.ok(articleService.showMemberArticles(memberId)); + } } diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java index 6eac1f7..727e7af 100644 --- a/src/main/java/com/board/article/repository/ArticleRepository.java +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -1,7 +1,10 @@ package com.board.article.repository; import com.board.article.domain.Article; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository { + + List
findArticleByMemberId(Long memberId); } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index bc18552..9e22f60 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -6,7 +6,6 @@ import com.board.article.domain.Article; import com.board.article.repository.ArticleRepository; import com.board.article.service.exception.NotFoundArticleException; -import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -32,17 +31,14 @@ public ArticleResponse createArticle(ArticleRequest request, Long memberId) { } public ArticleResponses showAllArticles() { - List articleResponses = new ArrayList<>(); - List
articles = getAllArticles(); - - for (Article article : articles) { - articleResponses.add(new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - )); - } + List articleResponses = getAllArticles().stream() + .map(article -> new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )) + .toList(); return new ArticleResponses(articleResponses); } @@ -58,6 +54,19 @@ public ArticleResponse showArticle(Long articleId) { ); } + public ArticleResponses showMemberArticles(Long memberId) { + List articleResponses = getMemberArticles(memberId).stream() + .map(article -> new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )) + .toList(); + + return new ArticleResponses(articleResponses); + } + @Transactional(readOnly = true) public List
getAllArticles() { return articleRepository.findAll(); @@ -68,4 +77,9 @@ public Article getArticle(Long articleId) { return articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); } + + @Transactional(readOnly = true) + public List
getMemberArticles(Long memberId) { + return articleRepository.findArticleByMemberId(memberId); + } } From a4d046beb0a06c1ccc77bcfc2c749628bb4bc167 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:22:51 +0900 Subject: [PATCH 15/52] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 6 ++++++ .../com/board/article/domain/Article.java | 9 ++++++++ .../exceptions/ArticleErrorCode.java | 3 ++- .../board/article/service/ArticleService.java | 21 +++++++++++++++++++ .../ForbiddenAccessArticleException.java | 11 ++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index 077fe8a..7a4ab7d 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -44,4 +45,9 @@ public ResponseEntity showArticle(@PathVariable Long articleId) public ResponseEntity showMemberArticles(@Auth Long memberId) { return ResponseEntity.ok(articleService.showMemberArticles(memberId)); } + + @PatchMapping("/articles/{articleId}") + public ResponseEntity updateArticle(@RequestBody ArticleRequest request, @PathVariable Long articleId, @Auth Long memberId) { + return ResponseEntity.ok(articleService.updateArticle(request, articleId, memberId)); + } } diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index 75459ff..dfe2ea7 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -34,4 +34,13 @@ public Article(Long memberId, String title, String content) { this.title = title; this.content = content; } + + public void update(String title, String content) { + if (title != null) { + this.title = title; + } + if (content != null) { + this.content = content; + } + } } diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java index 992f2ef..96fecd3 100644 --- a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java +++ b/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java @@ -6,7 +6,8 @@ @Getter public enum ArticleErrorCode { - NOT_FOUND_ARTICLE(HttpStatus.NOT_FOUND, "A001", "게시글을 찾을 수 없습니다."); + NOT_FOUND_ARTICLE(HttpStatus.NOT_FOUND, "A001", "게시글을 찾을 수 없습니다."), + FORBIDDEN_ACCESS_ARTICLE(HttpStatus.BAD_REQUEST, "A002", "게시글에 대한 권한을 가지고 있지 않습니다."); private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 9e22f60..7752acd 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -5,8 +5,10 @@ import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; import com.board.article.repository.ArticleRepository; +import com.board.article.service.exception.ForbiddenAccessArticleException; import com.board.article.service.exception.NotFoundArticleException; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -67,6 +69,25 @@ public ArticleResponses showMemberArticles(Long memberId) { return new ArticleResponses(articleResponses); } + public ArticleResponse updateArticle(ArticleRequest request, Long articleId, Long memberId) { + Article article = getArticle(articleId); + validateAccessAboutArticle(memberId, article); + article.update(request.title(), request.content()); + + return new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + } + + private void validateAccessAboutArticle(Long memberId, Article article) { + if(!Objects.equals(article.getMemberId(), memberId)) { + throw new ForbiddenAccessArticleException(); + } + } + @Transactional(readOnly = true) public List
getAllArticles() { return articleRepository.findAll(); diff --git a/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java b/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java new file mode 100644 index 0000000..e5a693a --- /dev/null +++ b/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java @@ -0,0 +1,11 @@ +package com.board.article.service.exception; + +import com.board.article.exception.exceptions.ArticleErrorCode; +import com.board.article.exception.exceptions.ArticleException; + +public class ForbiddenAccessArticleException extends ArticleException { + + public ForbiddenAccessArticleException() { + super(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE); + } +} From 8c76392e701c58e5316912c48ba8d491a062247b Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:27:07 +0900 Subject: [PATCH 16/52] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/article/controller/ArticleController.java | 6 ++++++ .../com/board/article/service/ArticleService.java | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index 7a4ab7d..ae0e20f 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -8,6 +8,7 @@ import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -50,4 +51,9 @@ public ResponseEntity showMemberArticles(@Auth Long memberId) public ResponseEntity updateArticle(@RequestBody ArticleRequest request, @PathVariable Long articleId, @Auth Long memberId) { return ResponseEntity.ok(articleService.updateArticle(request, articleId, memberId)); } + + @DeleteMapping("/articles/{articleId}") + public ResponseEntity deleteArticle(@PathVariable Long articleId, @Auth Long memberId) { + return ResponseEntity.ok(articleService.deleteArticle(articleId, memberId)); + } } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 7752acd..5570b39 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -82,6 +82,19 @@ public ArticleResponse updateArticle(ArticleRequest request, Long articleId, Lon ); } + public ArticleResponse deleteArticle(Long articleId, Long memberId) { + Article article = getArticle(articleId); + validateAccessAboutArticle(memberId, article); + articleRepository.delete(article); + + return new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + } + private void validateAccessAboutArticle(Long memberId, Article article) { if(!Objects.equals(article.getMemberId(), memberId)) { throw new ForbiddenAccessArticleException(); From 45e43f531623a5303c59996c6ab12b71d5e4936d Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:26:11 +0900 Subject: [PATCH 17/52] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/article/domain/Article.java | 3 +- .../comment/controller/CommentController.java | 29 +++++++++++++++ .../dto/reponse/CommentResponse.java | 8 +++++ .../dto/request/CommentRequest.java | 6 ++++ .../com/board/comment/domain/Comment.java | 35 +++++++++++++++++++ .../CommentExceptionHandler.java | 23 ++++++++++++ .../dto/CommentErrorResponse.java | 7 ++++ .../exceptions/CommentErrorCode.java | 19 ++++++++++ .../exceptions/CommentException.java | 14 ++++++++ .../comment/repository/CommentRepository.java | 9 +++++ .../board/comment/service/CommentService.java | 26 ++++++++++++++ 11 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/board/comment/controller/CommentController.java create mode 100644 src/main/java/com/board/comment/controller/dto/reponse/CommentResponse.java create mode 100644 src/main/java/com/board/comment/controller/dto/request/CommentRequest.java create mode 100644 src/main/java/com/board/comment/domain/Comment.java create mode 100644 src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java create mode 100644 src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java create mode 100644 src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java create mode 100644 src/main/java/com/board/comment/exception/exceptions/CommentException.java create mode 100644 src/main/java/com/board/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/board/comment/service/CommentService.java diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index dfe2ea7..f77ddf3 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -6,11 +6,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class Article { diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java new file mode 100644 index 0000000..2375877 --- /dev/null +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -0,0 +1,29 @@ +package com.board.comment.controller; + +import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.service.CommentService; +import com.board.global.resolver.annotation.Auth; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +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; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/articles/{articleId}/comments") + public ResponseEntity createComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long articleId) { + CommentResponse response = commentService.createComment(request, memberId, articleId); + URI location = URI.create("/api/articles/" + response.articleId()); + return ResponseEntity.created(location).body(response); + } +} diff --git a/src/main/java/com/board/comment/controller/dto/reponse/CommentResponse.java b/src/main/java/com/board/comment/controller/dto/reponse/CommentResponse.java new file mode 100644 index 0000000..9de028b --- /dev/null +++ b/src/main/java/com/board/comment/controller/dto/reponse/CommentResponse.java @@ -0,0 +1,8 @@ +package com.board.comment.controller.dto.reponse; + +public record CommentResponse( + Long memberId, + Long articleId, + String content +) { +} diff --git a/src/main/java/com/board/comment/controller/dto/request/CommentRequest.java b/src/main/java/com/board/comment/controller/dto/request/CommentRequest.java new file mode 100644 index 0000000..1866c38 --- /dev/null +++ b/src/main/java/com/board/comment/controller/dto/request/CommentRequest.java @@ -0,0 +1,6 @@ +package com.board.comment.controller.dto.request; + +public record CommentRequest( + String content +) { +} diff --git a/src/main/java/com/board/comment/domain/Comment.java b/src/main/java/com/board/comment/domain/Comment.java new file mode 100644 index 0000000..c1f3128 --- /dev/null +++ b/src/main/java/com/board/comment/domain/Comment.java @@ -0,0 +1,35 @@ +package com.board.comment.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private Long memberId; + + @Column + private Long articleId; + + @Column(nullable = false) + private String content; + + public Comment(Long memberId, Long articleId, String content) { + this.memberId = memberId; + this.articleId = articleId; + this.content = content; + } +} diff --git a/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java b/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java new file mode 100644 index 0000000..e7c16d1 --- /dev/null +++ b/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java @@ -0,0 +1,23 @@ +package com.board.comment.exception.exceptionhandler; + +import com.board.comment.exception.exceptionhandler.dto.CommentErrorResponse; +import com.board.comment.exception.exceptions.CommentErrorCode; +import com.board.comment.exception.exceptions.CommentException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class CommentExceptionHandler { + + @ExceptionHandler(CommentException.class) + public ResponseEntity handleException(CommentException e) { + CommentErrorCode commentErrorCode = e.getErrorCode(); + CommentErrorResponse response = new CommentErrorResponse( + commentErrorCode.getCustomCode(), + commentErrorCode.getMessage() + ); + + return ResponseEntity.status(commentErrorCode.getHttpStatus()).body(response); + } +} diff --git a/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java b/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java new file mode 100644 index 0000000..e76afb2 --- /dev/null +++ b/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java @@ -0,0 +1,7 @@ +package com.board.comment.exception.exceptionhandler.dto; + +public record CommentErrorResponse( + String customCode, + String message +) { +} diff --git a/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java b/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java new file mode 100644 index 0000000..c3ca062 --- /dev/null +++ b/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java @@ -0,0 +1,19 @@ +package com.board.comment.exception.exceptions; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum CommentErrorCode { + ; + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; + + CommentErrorCode(HttpStatus httpStatus, String customCode, String message) { + this.httpStatus = httpStatus; + this.customCode = customCode; + this.message = message; + } +} diff --git a/src/main/java/com/board/comment/exception/exceptions/CommentException.java b/src/main/java/com/board/comment/exception/exceptions/CommentException.java new file mode 100644 index 0000000..64d7fe7 --- /dev/null +++ b/src/main/java/com/board/comment/exception/exceptions/CommentException.java @@ -0,0 +1,14 @@ +package com.board.comment.exception.exceptions; + +import lombok.Getter; + +@Getter +public class CommentException extends RuntimeException{ + + private final CommentErrorCode errorCode; + + public CommentException(CommentErrorCode errorCode) { + super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java new file mode 100644 index 0000000..9a09227 --- /dev/null +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -0,0 +1,9 @@ +package com.board.comment.repository; + +import com.board.comment.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java new file mode 100644 index 0000000..2dfb6e6 --- /dev/null +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -0,0 +1,26 @@ +package com.board.comment.service; + +import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.domain.Comment; +import com.board.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + + public CommentResponse createComment(CommentRequest request, Long memberId, Long articleId) { + Comment comment = new Comment(memberId, articleId, request.content()); + commentRepository.save(comment); + + return new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + } +} From 71d4d4c3b6a13a957658bdc12f989f97d24d4d71 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:42:35 +0900 Subject: [PATCH 18/52] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 7 ++++++ .../dto/reponse/CommentResponses.java | 8 +++++++ .../comment/loader/CommentDataLoader.java | 22 +++++++++++++++++++ .../comment/repository/CommentRepository.java | 3 +++ .../board/comment/service/CommentService.java | 21 ++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 src/main/java/com/board/comment/controller/dto/reponse/CommentResponses.java create mode 100644 src/main/java/com/board/comment/loader/CommentDataLoader.java diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 2375877..97bff85 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -1,12 +1,14 @@ package com.board.comment.controller; import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.service.CommentService; import com.board.global.resolver.annotation.Auth; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -26,4 +28,9 @@ public ResponseEntity createComment(@RequestBody CommentRequest URI location = URI.create("/api/articles/" + response.articleId()); return ResponseEntity.created(location).body(response); } + + @GetMapping("/articles/{articleId}/comments") + public ResponseEntity showArticleComments(@PathVariable Long articleId) { + return ResponseEntity.ok(commentService.showArticleComments(articleId)); + } } diff --git a/src/main/java/com/board/comment/controller/dto/reponse/CommentResponses.java b/src/main/java/com/board/comment/controller/dto/reponse/CommentResponses.java new file mode 100644 index 0000000..3b1450f --- /dev/null +++ b/src/main/java/com/board/comment/controller/dto/reponse/CommentResponses.java @@ -0,0 +1,8 @@ +package com.board.comment.controller.dto.reponse; + +import java.util.List; + +public record CommentResponses( + List commentResponses +) { +} diff --git a/src/main/java/com/board/comment/loader/CommentDataLoader.java b/src/main/java/com/board/comment/loader/CommentDataLoader.java new file mode 100644 index 0000000..ffdc7f0 --- /dev/null +++ b/src/main/java/com/board/comment/loader/CommentDataLoader.java @@ -0,0 +1,22 @@ +package com.board.comment.loader; + +import com.board.comment.domain.Comment; +import com.board.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CommentDataLoader implements CommandLineRunner { + + private final CommentRepository repository; + + @Override + public void run(String... args) throws Exception { + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); + } +} diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 9a09227..92ff2af 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -1,9 +1,12 @@ package com.board.comment.repository; import com.board.comment.domain.Comment; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CommentRepository extends JpaRepository { + + List findAllByArticleId(Long articleId); } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 2dfb6e6..69c6f89 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -1,14 +1,18 @@ package com.board.comment.service; import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional public class CommentService { private final CommentRepository commentRepository; @@ -23,4 +27,21 @@ public CommentResponse createComment(CommentRequest request, Long memberId, Long comment.getContent() ); } + + public CommentResponses showArticleComments(Long articleId) { + List commentResponses = getArticleComments(articleId).stream() + .map(comment -> new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + )) + .toList(); + + return new CommentResponses(commentResponses); + } + + @Transactional(readOnly = true) + public List getArticleComments(Long articleId) { + return commentRepository.findAllByArticleId(articleId); + } } From 65c75e41dcc9a888f70267a2d7b5b6b0085d180d Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:49:14 +0900 Subject: [PATCH 19/52] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EC=9D=98=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 5 +++++ .../comment/repository/CommentRepository.java | 1 + .../board/comment/service/CommentService.java | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 97bff85..a3effce 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -33,4 +33,9 @@ public ResponseEntity createComment(@RequestBody CommentRequest public ResponseEntity showArticleComments(@PathVariable Long articleId) { return ResponseEntity.ok(commentService.showArticleComments(articleId)); } + + @GetMapping("members/me/comments") + public ResponseEntity showMemberComments(@Auth Long memberId) { + return ResponseEntity.ok(commentService.showMemberArticles(memberId)); + } } diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 92ff2af..6129c64 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -9,4 +9,5 @@ public interface CommentRepository extends JpaRepository { List findAllByArticleId(Long articleId); + List findAllByMemberId(Long memberId); } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 69c6f89..85ccb10 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -40,8 +40,25 @@ public CommentResponses showArticleComments(Long articleId) { return new CommentResponses(commentResponses); } + public CommentResponses showMemberArticles(Long memberId) { + List commentResponses = getMemberComments(memberId).stream() + .map(comment -> new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + )) + .toList(); + + return new CommentResponses(commentResponses); + } + @Transactional(readOnly = true) public List getArticleComments(Long articleId) { return commentRepository.findAllByArticleId(articleId); } + + @Transactional(readOnly = true) + public List getMemberComments(Long memberId) { + return commentRepository.findAllByMemberId(memberId); + } } From 3f8b55433628898e70e6e385c764848b0d04fac9 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:20:02 +0900 Subject: [PATCH 20/52] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 6 +++++ .../com/board/comment/domain/Comment.java | 6 +++++ .../exceptions/CommentErrorCode.java | 4 ++- .../board/comment/service/CommentService.java | 27 +++++++++++++++++++ .../ForbiddenAccessCommentException.java | 11 ++++++++ .../exception/NotFoundCommentException.java | 11 ++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java create mode 100644 src/main/java/com/board/comment/service/exception/NotFoundCommentException.java diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index a3effce..44d5104 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -38,4 +39,9 @@ public ResponseEntity showArticleComments(@PathVariable Long a public ResponseEntity showMemberComments(@Auth Long memberId) { return ResponseEntity.ok(commentService.showMemberArticles(memberId)); } + + @PatchMapping("/comments/{commentId}") + public ResponseEntity updateComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long commentId) { + return ResponseEntity.ok(commentService.updateComment(request, memberId, commentId)); + } } diff --git a/src/main/java/com/board/comment/domain/Comment.java b/src/main/java/com/board/comment/domain/Comment.java index c1f3128..3a15139 100644 --- a/src/main/java/com/board/comment/domain/Comment.java +++ b/src/main/java/com/board/comment/domain/Comment.java @@ -32,4 +32,10 @@ public Comment(Long memberId, Long articleId, String content) { this.articleId = articleId; this.content = content; } + + public void update(String content) { + if (content != null) { + this.content = content; + } + } } diff --git a/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java b/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java index c3ca062..2084cba 100644 --- a/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java +++ b/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java @@ -5,7 +5,9 @@ @Getter public enum CommentErrorCode { - ; + + NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "C001", "댓글을 찾을 수 없습니다."), + FORBIDDEN_ACCESS_COMMENT(HttpStatus.BAD_REQUEST, "C002", "댓글에 대한 권한이 없습니다."); private final HttpStatus httpStatus; private final String customCode; diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 85ccb10..47e30ab 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -5,7 +5,10 @@ import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; +import com.board.comment.service.exception.ForbiddenAccessCommentException; +import com.board.comment.service.exception.NotFoundCommentException; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -52,6 +55,24 @@ public CommentResponses showMemberArticles(Long memberId) { return new CommentResponses(commentResponses); } + public CommentResponse updateComment(CommentRequest request, Long memberId, Long commentId) { + Comment comment = getComment(commentId); + validateAccessAboutComment(memberId, comment); + comment.update(request.content()); + + return new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + } + + private void validateAccessAboutComment(Long memberId, Comment comment) { + if(!Objects.equals(comment.getMemberId(), memberId)) { + throw new ForbiddenAccessCommentException(); + } + } + @Transactional(readOnly = true) public List getArticleComments(Long articleId) { return commentRepository.findAllByArticleId(articleId); @@ -61,4 +82,10 @@ public List getArticleComments(Long articleId) { public List getMemberComments(Long memberId) { return commentRepository.findAllByMemberId(memberId); } + + @Transactional(readOnly = true) + public Comment getComment(Long commentId) { + return commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + } } diff --git a/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java b/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java new file mode 100644 index 0000000..954e3bb --- /dev/null +++ b/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java @@ -0,0 +1,11 @@ +package com.board.comment.service.exception; + +import com.board.comment.exception.exceptions.CommentErrorCode; +import com.board.comment.exception.exceptions.CommentException; + +public class ForbiddenAccessCommentException extends CommentException { + + public ForbiddenAccessCommentException() { + super(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT); + } +} diff --git a/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java b/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java new file mode 100644 index 0000000..c0029ab --- /dev/null +++ b/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java @@ -0,0 +1,11 @@ +package com.board.comment.service.exception; + +import com.board.comment.exception.exceptions.CommentErrorCode; +import com.board.comment.exception.exceptions.CommentException; + +public class NotFoundCommentException extends CommentException { + + public NotFoundCommentException() { + super(CommentErrorCode.NOT_FOUND_COMMENT); + } +} From 1ee872c4b7362c8d64014197b9c78229724ad6b2 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:24:44 +0900 Subject: [PATCH 21/52] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 6 ++++++ .../com/board/comment/service/CommentService.java | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 44d5104..01ffc23 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -8,6 +8,7 @@ import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -44,4 +45,9 @@ public ResponseEntity showMemberComments(@Auth Long memberId) public ResponseEntity updateComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long commentId) { return ResponseEntity.ok(commentService.updateComment(request, memberId, commentId)); } + + @DeleteMapping("/comments/{commentId}") + public ResponseEntity deleteComment(@Auth Long memberId, @PathVariable Long commentId) { + return ResponseEntity.ok(commentService.deleteComment(memberId, commentId)); + } } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 47e30ab..6b4f7e8 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -67,8 +67,20 @@ public CommentResponse updateComment(CommentRequest request, Long memberId, Long ); } + public CommentResponse deleteComment(Long memberId, Long commentId) { + Comment comment = getComment(commentId); + validateAccessAboutComment(memberId, comment); + commentRepository.delete(comment); + + return new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + } + private void validateAccessAboutComment(Long memberId, Comment comment) { - if(!Objects.equals(comment.getMemberId(), memberId)) { + if (!Objects.equals(comment.getMemberId(), memberId)) { throw new ForbiddenAccessCommentException(); } } From f2e6d77e69b4a3d857fda635e31df13110ea4be2 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:53:07 +0900 Subject: [PATCH 22/52] =?UTF-8?q?feat:=20swagger=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openAPI 를 이용한 swagger를 세팅하기 위해 버전 다운그레이드 --- build.gradle | 7 ++++-- .../board/global/config/SwaggerConfig.java | 23 +++++++++++++++++++ .../controller/auth/AuthController.java | 3 +-- .../controller/member/MemberController.java | 5 ++++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/board/global/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 02c861b..f285959 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.3' + id 'org.springframework.boot' version '3.3.3' id 'io.spring.dependency-management' version '1.1.7' id 'org.asciidoctor.jvm.convert' version '3.3.2' } @@ -40,8 +40,11 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //jwt + // jwt implementation 'com.auth0:java-jwt:4.2.1' + + // swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' } tasks.named('test') { diff --git a/src/main/java/com/board/global/config/SwaggerConfig.java b/src/main/java/com/board/global/config/SwaggerConfig.java new file mode 100644 index 0000000..1459840 --- /dev/null +++ b/src/main/java/com/board/global/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.board.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "Board API", + description = "Swagger 연습을 위한 Open API 문서" + ) +) +@SecurityScheme( + name = "BearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" +) +public class SwaggerConfig { +} diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java index dba6565..f447bd6 100644 --- a/src/main/java/com/board/member/controller/auth/AuthController.java +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -9,7 +9,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -29,7 +28,7 @@ public ResponseEntity signUp(@RequestBody SignUpRequest request) return ResponseEntity.created(location).body(response); } - @GetMapping("/login") + @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest request) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Authorization", "Bearer " + authService.login(request)); diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index 0acfb68..ac7a902 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -4,6 +4,8 @@ import com.board.member.controller.member.dto.reponse.MemberResponse; import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.service.member.MemberService; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -16,6 +18,9 @@ @RestController @RequestMapping("/api") @RequiredArgsConstructor +@OpenAPIDefinition( + info = @Info(title = "My API", version = "1.0", description = "API Documentation") +) public class MemberController { private final MemberService memberService; From 520580527af38f3cc941b3ee3569b918cc23ac2f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:32:20 +0900 Subject: [PATCH 23/52] =?UTF-8?q?feat:=20ArticleRepositoryTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/test.md | 122 ++++++++++++++++++ .../article/loader/ArticleTestDataLoader.java | 25 ++++ .../repository/ArticleRepositoryTest.java | 35 +++++ 3 files changed, 182 insertions(+) create mode 100644 docs/test.md create mode 100644 src/test/java/com/board/article/loader/ArticleTestDataLoader.java create mode 100644 src/test/java/com/board/article/repository/ArticleRepositoryTest.java diff --git a/docs/test.md b/docs/test.md new file mode 100644 index 0000000..ac052c9 --- /dev/null +++ b/docs/test.md @@ -0,0 +1,122 @@ +# Spring Test + +--- + +## @DataJpaTest와 JUnit을 사용한 Repository 테스트 + +--- + +### 개요 + +* Spring Boot 애플리케이션에서 Spring Data JPA를 사용하여 데이터베이스를 다룰 때, JPA Repository가 제대로 동작하는지 테스트하는 것이 중요합니다. + +---- + +### @DataJpaTest + +* @DataJpaTest 란? -> JPA Repository 테스트를 위한 애너테이션입니다. +* 최소한의 Spring 컨텍스트만 로드하여 테스트 속도를 높이고, EntityManager와 TestEntityManager를 자동 제공합니다. +~~~ +EntityManager란? + +-> JPA(Java Persistence API)에서 제공하는 기본적인 데이터베이스 관리 도구입니다. + Spring Boot에서 JPA를 사용할 때, 일반적으로 EntityManager를 사용하여 데이터베이스 작업을 수행합니다. + +EntityManager의 주요 기능 + +1. 엔티티 저장: persist(entity) +2. 엔티티 수정: merge(entity) +3. 엔티티 삭제: remove(entity) +4. 엔티티 조회: find(Class, primaryKey), createQuery() + +----------------------------------------------------------------------------------------- +EntityTestManager란? + +-> 테스트용으로 제공되는 EntityManager입니다. + Spring Boot의 @DataJpaTest에서 사용할 수 있으며, 일반 EntityManager보다 테스트에 최적화되어 있습니다. + +TestEntityManager의 특징 + +1. 테스트 중 사용할 수 있도록 간편한 메서드 제공 +2. 트랜잭션이 자동 롤백되어 데이터가 남지 않음 +3. flush()를 명시적으로 호출하여 즉시 쿼리를 실행 가능 + +~~~ +* 애플리케이션 전체를 로드하지 않고, JPA 관련 Bean들만 로드하여 테스트 성능을 최적화합니다. + +--- + +### @DataJpaTest의 선택적 파라미터 + +**properties 속성** +* 테스트 환경에서 특정 Spring Boot 설정 값을 변경할 수 있습니다. +~~~ +@DataJpaTest(properties = { + "spring.datasource.url=jdbc:h2:mem:testdb", + "spring.jpa.hibernate.ddl-auto=create-drop" +}) +public class UserRepositoryTest { + // 테스트 메서드 작성 +} +~~~ + +**showSql 속성** +* SQL 쿼리 로그를 출력할지 설정하는 옵션입니다. +~~~ +@DataJpaTest(showSql = false) +public class UserRepositoryTest { + // 테스트 메서드 작성 +} +~~~ + +**includeFilters와 excludeFilters** +* 테스트할 컴포넌트(클래스)를 선택적으로 포함하거나 제외할 수 있습니다. + +--- + +### 주요 기능 + +* 테스트 환경 구성 + * @DataJpaTest는 테스트를 위한 DB 환경을 자동으로 구성해줍니다. + * 일반적으로 H2 인메모리 데이터베이스를 사용하여, 테스트 후 자동으로 데이터가 삭제됩니다. + +* 의존성 주입 + * @DataJpaTest를 사용하면 Repository 빈이 자동으로 주입되어 테스트할 수 있습니다. + +* 기본적으로 트랜잭션 롤백 + * 각 테스트 메서드는 트랜잭션 내부에서 실행되며, 테스트가 끝나면 데이터가 자동으로 롤백됩니다. + * 테스트 간 데이터가 격리(독립적) 되므로, 테스트 실행 순서에 영향을 받지 않습니다. + +--- + +### Repository 테스트 구현 + +* 테스트 라이프사이클 관리 +~~~ +private User testUser; + +@BeforeEach +public void setUp() { + testUser = new User(); + testUser.setUsername("testuser"); + testUser.setPassword("password"); + userRepository.save(testUser); +} + +@AfterEach +public void tearDown() { + userRepository.delete(testUser); +} +~~~ +1. @BeforeEach : 각 테스트 시작 전 실행 → 테스트 데이터를 초기화 +2. @AfterEach : 각 테스트 종료 후 실행 → 테스트 데이터 정리(삭제) + + +출처: baeldung + + + + + + + diff --git a/src/test/java/com/board/article/loader/ArticleTestDataLoader.java b/src/test/java/com/board/article/loader/ArticleTestDataLoader.java new file mode 100644 index 0000000..d5379ce --- /dev/null +++ b/src/test/java/com/board/article/loader/ArticleTestDataLoader.java @@ -0,0 +1,25 @@ +package com.board.article.loader; + +import com.board.article.domain.Article; +import com.board.article.repository.ArticleRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class ArticleTestDataLoader { + + private final ArticleRepository repository; + + public ArticleTestDataLoader(ArticleRepository repository) { + this.repository = repository; + } + + @Bean + public CommandLineRunner testData() { + return args -> { + repository.save(new Article(1L, "신형만에 관하여", "아내는 신봉선")); + repository.save(new Article(1L, "신짱구", "바보")); + }; + } +} diff --git a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java new file mode 100644 index 0000000..c69f8ea --- /dev/null +++ b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java @@ -0,0 +1,35 @@ +package com.board.article.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.board.article.domain.Article; +import com.board.article.loader.ArticleTestDataLoader; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(ArticleTestDataLoader.class) +class ArticleRepositoryTest { + + @Autowired + private ArticleRepository articleRepository; + + @Test + @DisplayName("멤버 아이디에 해당하는 게시글을 가져온다.") + void findArticleByMemberId() { + // given + Long memberId = 1L; + + // when + List
articles = articleRepository.findArticleByMemberId(memberId); + + // then + assertThat(articles).hasSize(2); + assertThat(articles).extracting(Article::getTitle) + .containsExactly("신형만에 관하여", "신짱구"); + } +} From 77fffbbfb3c5a0971d14953381d3b7a12d876129 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:05:00 +0900 Subject: [PATCH 24/52] =?UTF-8?q?feat:=20MemberRepositoryTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/loader/MemberTestDataLoader.java | 25 ++++++ .../repository/MemberRepositoryTest.java | 86 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/test/java/com/board/member/loader/MemberTestDataLoader.java create mode 100644 src/test/java/com/board/member/repository/MemberRepositoryTest.java diff --git a/src/test/java/com/board/member/loader/MemberTestDataLoader.java b/src/test/java/com/board/member/loader/MemberTestDataLoader.java new file mode 100644 index 0000000..8651e64 --- /dev/null +++ b/src/test/java/com/board/member/loader/MemberTestDataLoader.java @@ -0,0 +1,25 @@ +package com.board.member.loader; + +import com.board.member.domain.member.Member; +import com.board.member.repository.MemberRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class MemberTestDataLoader { + + private final MemberRepository repository; + + MemberTestDataLoader(MemberRepository memberRepository) { + this.repository = memberRepository; + } + + @Bean + public CommandLineRunner testData() { + return args -> { + repository.save(new Member("신짱구", "짱구", "aaa", "password123")); + repository.save(new Member("신짱아", "짱아", "sss", "password456")); + }; + } +} diff --git a/src/test/java/com/board/member/repository/MemberRepositoryTest.java b/src/test/java/com/board/member/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..c684222 --- /dev/null +++ b/src/test/java/com/board/member/repository/MemberRepositoryTest.java @@ -0,0 +1,86 @@ +package com.board.member.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.board.member.domain.member.Member; +import com.board.member.loader.MemberTestDataLoader; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(MemberTestDataLoader.class) +class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + private Member member; + + @BeforeEach + void set() { + member = new Member("신짱구", "짱구", "aaa", "password123"); + } + + @Test + @DisplayName("특정 닉네임에 해당하는 유저가 있는지 판단한다.") + void existsByMemberNickName() { + // given + String nickName = "짱구"; + + // when + boolean isExistMember = memberRepository.existsByMemberNickName(nickName); + + // then + assertThat(isExistMember).isTrue(); + } + + @Test + @DisplayName("특정 아이디에 해당하는 유저가 있는지 판단한다.") + void existsByMemberLoginId() { + // given + String nickName = "aaa"; + + // when + boolean isExistMember = memberRepository.existsByMemberLoginId(nickName); + + // then + assertThat(isExistMember).isTrue(); + } + + @Test + @DisplayName("특정 로그인 아이디에 해당하는 유저를 조회한다.") + void findMemberByMemberLoginId() { + // given + String id = "aaa"; + + // when + Optional foundMember = memberRepository.findMemberByMemberLoginId(id); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get()).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(member); + } + + @Test + @DisplayName("특정 아이디에 해당하는 유저를 조회한다.") + void findMemberById() { + // given + Long memberId = 1L; + + // when + Optional foundMember = memberRepository.findMemberById(memberId); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get()).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(member); + } +} From 90637c56444bc36cf7c3d6bf27d1f34d7232bb9c Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:20:54 +0900 Subject: [PATCH 25/52] =?UTF-8?q?feat:=20CommentRepositoryTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/loader/CommentTestDataLoader.java | 27 ++++++++++ .../repository/CommentRepositoryTest.java | 50 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/test/java/com/board/comment/loader/CommentTestDataLoader.java create mode 100644 src/test/java/com/board/comment/repository/CommentRepositoryTest.java diff --git a/src/test/java/com/board/comment/loader/CommentTestDataLoader.java b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java new file mode 100644 index 0000000..dc92b0d --- /dev/null +++ b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java @@ -0,0 +1,27 @@ +package com.board.comment.loader; + +import com.board.comment.domain.Comment; +import com.board.comment.repository.CommentRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class CommentTestDataLoader { + + private final CommentRepository repository; + + public CommentTestDataLoader(CommentRepository repository) { + this.repository = repository; + } + + @Bean + public CommandLineRunner testData() { + return args -> { + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); + }; + } +} diff --git a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java new file mode 100644 index 0000000..3acc967 --- /dev/null +++ b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java @@ -0,0 +1,50 @@ +package com.board.comment.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.board.comment.domain.Comment; +import com.board.comment.loader.CommentTestDataLoader; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(CommentTestDataLoader.class) +class CommentRepositoryTest { + + @Autowired + private CommentRepository commentRepository; + + @Test + @DisplayName("게시글 아이디에 해당하는 모든 댓글을 조회한다.") + void findAllByArticleId() { + // given + Long articleId = 1L; + + // when + List comments = commentRepository.findAllByArticleId(articleId); + + // then + assertThat(comments).hasSize(2) + .extracting(Comment::getContent) + .containsExactly("첫 번째 게시글 댓글 1", "첫 번째 게시글 댓글 2"); + } + + @Test + @DisplayName("멤버 아이디에 해당하는 모든 댓글을 조회한다.") + void findAllByMemberId() { + // given + Long memberId = 1L; + + // when + List comments = commentRepository.findAllByMemberId(memberId); + + // then + assertThat(comments).hasSize(4) + .extracting(Comment::getMemberId) + .containsExactly(1L, 1L, 1L, 1L); + } +} From 73545ccebc68a9c7afd90035e624c89480155015 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Tue, 8 Apr 2025 23:13:45 +0900 Subject: [PATCH 26/52] =?UTF-8?q?refactor:=20prefix=20endpoint=20=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20.yml=20=ED=8C=8C=EC=9D=BC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EA=B7=B8=EC=97=90=20=EB=94=B0=EB=A5=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 12 ++++++++++++ .../board/article/controller/ArticleController.java | 9 ++++++--- .../board/comment/controller/CommentController.java | 9 ++++++--- .../board/member/controller/auth/AuthController.java | 9 ++++++--- .../member/controller/member/MemberController.java | 2 -- src/main/resources/application.yml | 3 +++ 6 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 docs/code_review.md create mode 100644 src/main/resources/application.yml diff --git a/docs/code_review.md b/docs/code_review.md new file mode 100644 index 0000000..c6bb6c0 --- /dev/null +++ b/docs/code_review.md @@ -0,0 +1,12 @@ +# /api 는 클래스 레벨보다는 yml로 따로 빼는 건 어떨까요? + +--- +## 문제점 + +* 개발할 때는 /api 경로로 접근하지만 배포 환경에서는 /prod 경로로 접근한다고 가정한다. -> @RequestMapping("/api")를 전부 /prod로 바꿔야 한다. +* 또한 URI.create(~~) 부분도 모두 수정해야 한다. + +## 해결 + +* application.yml 파일에 경로를 미리 설정한다. +* URI 부분은 ServletUriComponentsBuilder의 fromCurrentRequestUri() 메서드를 사용하여 해결한다. -> 해당 메서드는 현재 요청을 보내는 경로를 받아오는 기능을 제공한다. diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index ae0e20f..b744d5c 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -14,12 +14,11 @@ import org.springframework.web.bind.annotation.PathVariable; 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; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @RestController @RequiredArgsConstructor -@RequestMapping("/api") public class ArticleController { private final ArticleService articleService; @@ -27,7 +26,11 @@ public class ArticleController { @PostMapping("/articles") public ResponseEntity createArticle(@RequestBody ArticleRequest request, @Auth Long memberId) { ArticleResponse response = articleService.createArticle(request, memberId); - URI location = URI.create("/api/articles/" + response.articleId()); + URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() + .path("/{id}") + .buildAndExpand(response.articleId()) + .toUri(); + return ResponseEntity.created(location) .body(response); } diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 01ffc23..072e51b 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -14,11 +14,10 @@ import org.springframework.web.bind.annotation.PathVariable; 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; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @RestController -@RequestMapping("/api") @RequiredArgsConstructor public class CommentController { @@ -27,7 +26,11 @@ public class CommentController { @PostMapping("/articles/{articleId}/comments") public ResponseEntity createComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long articleId) { CommentResponse response = commentService.createComment(request, memberId, articleId); - URI location = URI.create("/api/articles/" + response.articleId()); + URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() + .path("/{id}") + .buildAndExpand(response.articleId()) + .toUri(); + return ResponseEntity.created(location).body(response); } diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java index f447bd6..5e9412b 100644 --- a/src/main/java/com/board/member/controller/auth/AuthController.java +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -11,11 +11,10 @@ import org.springframework.http.ResponseEntity; 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; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @RestController -@RequestMapping("/api") @RequiredArgsConstructor public class AuthController { @@ -24,7 +23,11 @@ public class AuthController { @PostMapping("/signUp") public ResponseEntity signUp(@RequestBody SignUpRequest request) { SignUpResponse response = authService.signUp(request); - URI location = URI.create("/api/members/" + response.memberId()); + URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() + .path("/{id}") + .buildAndExpand(response.memberId()) + .toUri(); + return ResponseEntity.created(location).body(response); } diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index ac7a902..7479928 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -12,11 +12,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api") @RequiredArgsConstructor @OpenAPIDefinition( info = @Info(title = "My API", version = "1.0", description = "API Documentation") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f1b2bbc --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,3 @@ +server: + servlet: + context-path: /api From 9e49589b55256336efdbf40a02679db17f446fa1 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Wed, 9 Apr 2025 00:30:37 +0900 Subject: [PATCH 27/52] =?UTF-8?q?refactor:=20error=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=AC=EC=82=AC=EC=9A=A9=EC=84=B1=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=A0=84=EC=B2=B4=EC=A0=81=EC=9D=B8=20error=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 43 +++++++++++++++++++ .../{exceptions => }/ArticleErrorCode.java | 22 ++++++++-- .../article/exception/ArticleException.java | 11 +++++ .../ArticleExceptionHandler.java | 23 ---------- .../dto/ArticleErrorResponse.java | 7 --- .../exceptions/ArticleException.java | 14 ------ .../board/article/service/ArticleService.java | 8 ++-- .../ForbiddenAccessArticleException.java | 11 ----- .../exception/NotFoundArticleException.java | 11 ----- .../{exceptions => }/CommentErrorCode.java | 22 ++++++++-- .../comment/exception/CommentException.java | 11 +++++ .../CommentExceptionHandler.java | 23 ---------- .../dto/CommentErrorResponse.java | 7 --- .../exceptions/CommentException.java | 14 ------ .../board/comment/service/CommentService.java | 8 ++-- .../ForbiddenAccessCommentException.java | 11 ----- .../exception/NotFoundCommentException.java | 11 ----- .../BaseExceptionHandler.java | 23 ++++++++++ .../exceptionHandler/dto/ErrorResponse.java | 7 +++ .../exception/exceptions/BaseErrorCode.java | 10 +++++ .../exception/exceptions/BaseException.java | 15 +++++++ .../{exceptions => }/GlobalErrorCode.java | 22 ++++++++-- .../global/exception/GlobalException.java | 11 +++++ .../GlobalExceptionHandler.java | 18 -------- .../dto/GlobalErrorResponse.java | 7 --- .../exception/exceptions/GlobalException.java | 14 ------ .../global/interceptor/AuthInterceptor.java | 5 ++- .../exception/NotFoundTokenException.java | 11 ----- .../exception/TokenExpirationException.java | 4 +- .../exception/TokenInvalidException.java | 4 +- .../exception/TokenVerificationException.java | 4 +- .../exception/NotMatchPasswordException.java | 4 +- .../{exceptions => }/MemberErrorCode.java | 22 ++++++++-- .../member/exception/MemberException.java | 11 +++++ .../MemberExceptionHandler.java | 23 ---------- .../dto/MemberErrorResponse.java | 7 --- .../exception/exceptions/MemberException.java | 14 ------ .../member/service/auth/AuthService.java | 11 +++-- .../auth/exception/ExistLoginIdException.java | 11 ----- .../exception/ExistNickNameException.java | 11 ----- .../exception/NotMatchLoginIdException.java | 11 ----- .../member/service/member/MemberService.java | 5 ++- .../exception/NotFoundMemberException.java | 11 ----- 43 files changed, 241 insertions(+), 312 deletions(-) rename src/main/java/com/board/article/exception/{exceptions => }/ArticleErrorCode.java (59%) create mode 100644 src/main/java/com/board/article/exception/ArticleException.java delete mode 100644 src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java delete mode 100644 src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java delete mode 100644 src/main/java/com/board/article/exception/exceptions/ArticleException.java delete mode 100644 src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java delete mode 100644 src/main/java/com/board/article/service/exception/NotFoundArticleException.java rename src/main/java/com/board/comment/exception/{exceptions => }/CommentErrorCode.java (58%) create mode 100644 src/main/java/com/board/comment/exception/CommentException.java delete mode 100644 src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java delete mode 100644 src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java delete mode 100644 src/main/java/com/board/comment/exception/exceptions/CommentException.java delete mode 100644 src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java delete mode 100644 src/main/java/com/board/comment/service/exception/NotFoundCommentException.java create mode 100644 src/main/java/com/board/common/exception/exceptionHandler/BaseExceptionHandler.java create mode 100644 src/main/java/com/board/common/exception/exceptionHandler/dto/ErrorResponse.java create mode 100644 src/main/java/com/board/common/exception/exceptions/BaseErrorCode.java create mode 100644 src/main/java/com/board/common/exception/exceptions/BaseException.java rename src/main/java/com/board/global/exception/{exceptions => }/GlobalErrorCode.java (65%) create mode 100644 src/main/java/com/board/global/exception/GlobalException.java delete mode 100644 src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java delete mode 100644 src/main/java/com/board/global/exception/exceptions/GlobalException.java delete mode 100644 src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java rename src/main/java/com/board/member/exception/{exceptions => }/MemberErrorCode.java (67%) create mode 100644 src/main/java/com/board/member/exception/MemberException.java delete mode 100644 src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java delete mode 100644 src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java delete mode 100644 src/main/java/com/board/member/exception/exceptions/MemberException.java delete mode 100644 src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java delete mode 100644 src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java delete mode 100644 src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java delete mode 100644 src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java diff --git a/docs/code_review.md b/docs/code_review.md index c6bb6c0..23ff891 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -10,3 +10,46 @@ * application.yml 파일에 경로를 미리 설정한다. * URI 부분은 ServletUriComponentsBuilder의 fromCurrentRequestUri() 메서드를 사용하여 해결한다. -> 해당 메서드는 현재 요청을 보내는 경로를 받아오는 기능을 제공한다. + +# 에러 코드 재사용성 + +--- +## 문제점(현재 에러 코드 구조) + +~~~ +public class GlobalExceptionHandler { + @ExceptionHandler(GlobalException.class) + public ResponseEntity handleException(GlobalException e) { + } +} + +@Getter +public enum GlobalErrorCode { + +} + +@Getter +public class GlobalException extends RuntimeException { + + private final GlobalErrorCode errorCode; + + public GlobalException(GlobalErrorCode errorCode) { + } +} +~~~ + +* 현재 이 3단계 구조로, 특정 관심사 패키지마다 exception 을 관리하고 있음 +* 이렇게 할 경우, 패키지마다 계속해서 exception 을 생성 및 관리해야함 -> 코드 재활용이 안됌. + +## 해결책 + +* common 패키지에, 공통적으로 사용되는 errorCode 를 interface화 시킴. +* 이후 interface(errorCode) 를 사용한 BaseException 클래스를 생성 -> 다른 패키지 exception 에서 BaseException 상속받아 사용할 수 있음 +* 그렇기에 exception handler 도 하나로만 관리 가능 + +## 결론 + +* ErrorCode 인터페이스 하나 파서 관리 +* BaseException 생성, 그리고 이 클래스를 이용한 BaseExceptionHandler 생성 -> 핸들러 하나만 사용 가능 +* MemberException, ArticleException 등등 BaseException 을 상속받아 BaseExceptionHandler 에서 에러 처리 가능 +* ErrorCode 추가될때마다 exception 을 추가할 필요가 없어짐. diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java b/src/main/java/com/board/article/exception/ArticleErrorCode.java similarity index 59% rename from src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java rename to src/main/java/com/board/article/exception/ArticleErrorCode.java index 96fecd3..0df0898 100644 --- a/src/main/java/com/board/article/exception/exceptions/ArticleErrorCode.java +++ b/src/main/java/com/board/article/exception/ArticleErrorCode.java @@ -1,10 +1,9 @@ -package com.board.article.exception.exceptions; +package com.board.article.exception; -import lombok.Getter; +import com.board.common.exception.exceptions.BaseErrorCode; import org.springframework.http.HttpStatus; -@Getter -public enum ArticleErrorCode { +public enum ArticleErrorCode implements BaseErrorCode { NOT_FOUND_ARTICLE(HttpStatus.NOT_FOUND, "A001", "게시글을 찾을 수 없습니다."), FORBIDDEN_ACCESS_ARTICLE(HttpStatus.BAD_REQUEST, "A002", "게시글에 대한 권한을 가지고 있지 않습니다."); @@ -18,4 +17,19 @@ public enum ArticleErrorCode { this.customCode = customCode; this.message = message; } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String customCode() { + return customCode; + } + + @Override + public String message() { + return message; + } } diff --git a/src/main/java/com/board/article/exception/ArticleException.java b/src/main/java/com/board/article/exception/ArticleException.java new file mode 100644 index 0000000..0b7a8cb --- /dev/null +++ b/src/main/java/com/board/article/exception/ArticleException.java @@ -0,0 +1,11 @@ +package com.board.article.exception; + +import com.board.common.exception.exceptions.BaseErrorCode; +import com.board.common.exception.exceptions.BaseException; + +public class ArticleException extends BaseException { + + public ArticleException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java b/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java deleted file mode 100644 index 48dd493..0000000 --- a/src/main/java/com/board/article/exception/exceptionhandler/ArticleExceptionHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.board.article.exception.exceptionhandler; - -import com.board.article.exception.exceptionhandler.dto.ArticleErrorResponse; -import com.board.article.exception.exceptions.ArticleErrorCode; -import com.board.article.exception.exceptions.ArticleException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class ArticleExceptionHandler { - - @ExceptionHandler(ArticleException.class) - public ResponseEntity handleException(ArticleException e) { - ArticleErrorCode articleErrorCode = e.getErrorCode(); - ArticleErrorResponse response = new ArticleErrorResponse( - articleErrorCode.getCustomCode(), - articleErrorCode.getMessage() - ); - - return ResponseEntity.status(articleErrorCode.getHttpStatus()).body(response); - } -} diff --git a/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java b/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java deleted file mode 100644 index 805f43c..0000000 --- a/src/main/java/com/board/article/exception/exceptionhandler/dto/ArticleErrorResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.board.article.exception.exceptionhandler.dto; - -public record ArticleErrorResponse( - String customCode, - String message -) { -} diff --git a/src/main/java/com/board/article/exception/exceptions/ArticleException.java b/src/main/java/com/board/article/exception/exceptions/ArticleException.java deleted file mode 100644 index 3d8b8f7..0000000 --- a/src/main/java/com/board/article/exception/exceptions/ArticleException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.board.article.exception.exceptions; - -import lombok.Getter; - -@Getter -public class ArticleException extends RuntimeException{ - - private final ArticleErrorCode errorCode; - - public ArticleException(ArticleErrorCode errorCode) { - super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); - this.errorCode = errorCode; - } -} diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 5570b39..a63e2d8 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -4,9 +4,9 @@ import com.board.article.controller.dto.response.ArticleResponse; import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; +import com.board.article.exception.ArticleErrorCode; +import com.board.article.exception.ArticleException; import com.board.article.repository.ArticleRepository; -import com.board.article.service.exception.ForbiddenAccessArticleException; -import com.board.article.service.exception.NotFoundArticleException; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -97,7 +97,7 @@ public ArticleResponse deleteArticle(Long articleId, Long memberId) { private void validateAccessAboutArticle(Long memberId, Article article) { if(!Objects.equals(article.getMemberId(), memberId)) { - throw new ForbiddenAccessArticleException(); + throw new ArticleException(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE); } } @@ -109,7 +109,7 @@ public List
getAllArticles() { @Transactional(readOnly = true) public Article getArticle(Long articleId) { return articleRepository.findById(articleId) - .orElseThrow(NotFoundArticleException::new); + .orElseThrow(() -> new ArticleException(ArticleErrorCode.NOT_FOUND_ARTICLE)); } @Transactional(readOnly = true) diff --git a/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java b/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java deleted file mode 100644 index e5a693a..0000000 --- a/src/main/java/com/board/article/service/exception/ForbiddenAccessArticleException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.article.service.exception; - -import com.board.article.exception.exceptions.ArticleErrorCode; -import com.board.article.exception.exceptions.ArticleException; - -public class ForbiddenAccessArticleException extends ArticleException { - - public ForbiddenAccessArticleException() { - super(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE); - } -} diff --git a/src/main/java/com/board/article/service/exception/NotFoundArticleException.java b/src/main/java/com/board/article/service/exception/NotFoundArticleException.java deleted file mode 100644 index a78cb1a..0000000 --- a/src/main/java/com/board/article/service/exception/NotFoundArticleException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.article.service.exception; - -import com.board.article.exception.exceptions.ArticleErrorCode; -import com.board.article.exception.exceptions.ArticleException; - -public class NotFoundArticleException extends ArticleException { - - public NotFoundArticleException() { - super(ArticleErrorCode.NOT_FOUND_ARTICLE); - } -} diff --git a/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java b/src/main/java/com/board/comment/exception/CommentErrorCode.java similarity index 58% rename from src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java rename to src/main/java/com/board/comment/exception/CommentErrorCode.java index 2084cba..b18a00d 100644 --- a/src/main/java/com/board/comment/exception/exceptions/CommentErrorCode.java +++ b/src/main/java/com/board/comment/exception/CommentErrorCode.java @@ -1,10 +1,9 @@ -package com.board.comment.exception.exceptions; +package com.board.comment.exception; -import lombok.Getter; +import com.board.common.exception.exceptions.BaseErrorCode; import org.springframework.http.HttpStatus; -@Getter -public enum CommentErrorCode { +public enum CommentErrorCode implements BaseErrorCode { NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "C001", "댓글을 찾을 수 없습니다."), FORBIDDEN_ACCESS_COMMENT(HttpStatus.BAD_REQUEST, "C002", "댓글에 대한 권한이 없습니다."); @@ -18,4 +17,19 @@ public enum CommentErrorCode { this.customCode = customCode; this.message = message; } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String customCode() { + return customCode; + } + + @Override + public String message() { + return message; + } } diff --git a/src/main/java/com/board/comment/exception/CommentException.java b/src/main/java/com/board/comment/exception/CommentException.java new file mode 100644 index 0000000..2a725ba --- /dev/null +++ b/src/main/java/com/board/comment/exception/CommentException.java @@ -0,0 +1,11 @@ +package com.board.comment.exception; + +import com.board.common.exception.exceptions.BaseErrorCode; +import com.board.common.exception.exceptions.BaseException; + +public class CommentException extends BaseException { + + public CommentException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java b/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java deleted file mode 100644 index e7c16d1..0000000 --- a/src/main/java/com/board/comment/exception/exceptionhandler/CommentExceptionHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.board.comment.exception.exceptionhandler; - -import com.board.comment.exception.exceptionhandler.dto.CommentErrorResponse; -import com.board.comment.exception.exceptions.CommentErrorCode; -import com.board.comment.exception.exceptions.CommentException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class CommentExceptionHandler { - - @ExceptionHandler(CommentException.class) - public ResponseEntity handleException(CommentException e) { - CommentErrorCode commentErrorCode = e.getErrorCode(); - CommentErrorResponse response = new CommentErrorResponse( - commentErrorCode.getCustomCode(), - commentErrorCode.getMessage() - ); - - return ResponseEntity.status(commentErrorCode.getHttpStatus()).body(response); - } -} diff --git a/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java b/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java deleted file mode 100644 index e76afb2..0000000 --- a/src/main/java/com/board/comment/exception/exceptionhandler/dto/CommentErrorResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.board.comment.exception.exceptionhandler.dto; - -public record CommentErrorResponse( - String customCode, - String message -) { -} diff --git a/src/main/java/com/board/comment/exception/exceptions/CommentException.java b/src/main/java/com/board/comment/exception/exceptions/CommentException.java deleted file mode 100644 index 64d7fe7..0000000 --- a/src/main/java/com/board/comment/exception/exceptions/CommentException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.board.comment.exception.exceptions; - -import lombok.Getter; - -@Getter -public class CommentException extends RuntimeException{ - - private final CommentErrorCode errorCode; - - public CommentException(CommentErrorCode errorCode) { - super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); - this.errorCode = errorCode; - } -} diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 6b4f7e8..d224ce3 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -4,9 +4,9 @@ import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; +import com.board.comment.exception.CommentErrorCode; +import com.board.comment.exception.CommentException; import com.board.comment.repository.CommentRepository; -import com.board.comment.service.exception.ForbiddenAccessCommentException; -import com.board.comment.service.exception.NotFoundCommentException; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -81,7 +81,7 @@ public CommentResponse deleteComment(Long memberId, Long commentId) { private void validateAccessAboutComment(Long memberId, Comment comment) { if (!Objects.equals(comment.getMemberId(), memberId)) { - throw new ForbiddenAccessCommentException(); + throw new CommentException(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT); } } @@ -98,6 +98,6 @@ public List getMemberComments(Long memberId) { @Transactional(readOnly = true) public Comment getComment(Long commentId) { return commentRepository.findById(commentId) - .orElseThrow(NotFoundCommentException::new); + .orElseThrow(() -> new CommentException(CommentErrorCode.NOT_FOUND_COMMENT)); } } diff --git a/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java b/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java deleted file mode 100644 index 954e3bb..0000000 --- a/src/main/java/com/board/comment/service/exception/ForbiddenAccessCommentException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.comment.service.exception; - -import com.board.comment.exception.exceptions.CommentErrorCode; -import com.board.comment.exception.exceptions.CommentException; - -public class ForbiddenAccessCommentException extends CommentException { - - public ForbiddenAccessCommentException() { - super(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT); - } -} diff --git a/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java b/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java deleted file mode 100644 index c0029ab..0000000 --- a/src/main/java/com/board/comment/service/exception/NotFoundCommentException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.comment.service.exception; - -import com.board.comment.exception.exceptions.CommentErrorCode; -import com.board.comment.exception.exceptions.CommentException; - -public class NotFoundCommentException extends CommentException { - - public NotFoundCommentException() { - super(CommentErrorCode.NOT_FOUND_COMMENT); - } -} diff --git a/src/main/java/com/board/common/exception/exceptionHandler/BaseExceptionHandler.java b/src/main/java/com/board/common/exception/exceptionHandler/BaseExceptionHandler.java new file mode 100644 index 0000000..4ec57ef --- /dev/null +++ b/src/main/java/com/board/common/exception/exceptionHandler/BaseExceptionHandler.java @@ -0,0 +1,23 @@ +package com.board.common.exception.exceptionHandler; + +import com.board.common.exception.exceptionHandler.dto.ErrorResponse; +import com.board.common.exception.exceptions.BaseErrorCode; +import com.board.common.exception.exceptions.BaseException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class BaseExceptionHandler { + + @ExceptionHandler(BaseException.class) + public ResponseEntity handleException(BaseException e) { + BaseErrorCode baseErrorCode = e.baseErrorCode(); + ErrorResponse response = new ErrorResponse( + baseErrorCode.customCode(), + baseErrorCode.message() + ); + + return ResponseEntity.status(baseErrorCode.httpStatus()).body(response); + } +} diff --git a/src/main/java/com/board/common/exception/exceptionHandler/dto/ErrorResponse.java b/src/main/java/com/board/common/exception/exceptionHandler/dto/ErrorResponse.java new file mode 100644 index 0000000..78dff96 --- /dev/null +++ b/src/main/java/com/board/common/exception/exceptionHandler/dto/ErrorResponse.java @@ -0,0 +1,7 @@ +package com.board.common.exception.exceptionHandler.dto; + +public record ErrorResponse( + String customCode, + String message +) { +} diff --git a/src/main/java/com/board/common/exception/exceptions/BaseErrorCode.java b/src/main/java/com/board/common/exception/exceptions/BaseErrorCode.java new file mode 100644 index 0000000..255ecf4 --- /dev/null +++ b/src/main/java/com/board/common/exception/exceptions/BaseErrorCode.java @@ -0,0 +1,10 @@ +package com.board.common.exception.exceptions; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + HttpStatus httpStatus(); + String customCode(); + String message(); +} diff --git a/src/main/java/com/board/common/exception/exceptions/BaseException.java b/src/main/java/com/board/common/exception/exceptions/BaseException.java new file mode 100644 index 0000000..295b063 --- /dev/null +++ b/src/main/java/com/board/common/exception/exceptions/BaseException.java @@ -0,0 +1,15 @@ +package com.board.common.exception.exceptions; + +public class BaseException extends RuntimeException{ + + private final BaseErrorCode errorCode; + + public BaseException(BaseErrorCode errorCode) { + super(errorCode.customCode() + ": " + errorCode.message()); + this.errorCode = errorCode; + } + + public BaseErrorCode baseErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java b/src/main/java/com/board/global/exception/GlobalErrorCode.java similarity index 65% rename from src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java rename to src/main/java/com/board/global/exception/GlobalErrorCode.java index 2264406..e09ad50 100644 --- a/src/main/java/com/board/global/exception/exceptions/GlobalErrorCode.java +++ b/src/main/java/com/board/global/exception/GlobalErrorCode.java @@ -1,10 +1,9 @@ -package com.board.global.exception.exceptions; +package com.board.global.exception; -import lombok.Getter; +import com.board.common.exception.exceptions.BaseErrorCode; import org.springframework.http.HttpStatus; -@Getter -public enum GlobalErrorCode { +public enum GlobalErrorCode implements BaseErrorCode { NOT_FOUND_TOKEN(HttpStatus.NOT_FOUND, "T001", "토큰을 찾을 수 없습니다."), INVALID_TOKEN(HttpStatus.BAD_REQUEST, "T002", "토큰이 유효하지 않습니다."), @@ -20,4 +19,19 @@ public enum GlobalErrorCode { this.customCode = customCode; this.message = message; } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String customCode() { + return customCode; + } + + @Override + public String message() { + return message; + } } diff --git a/src/main/java/com/board/global/exception/GlobalException.java b/src/main/java/com/board/global/exception/GlobalException.java new file mode 100644 index 0000000..cf9ad2e --- /dev/null +++ b/src/main/java/com/board/global/exception/GlobalException.java @@ -0,0 +1,11 @@ +package com.board.global.exception; + +import com.board.common.exception.exceptions.BaseErrorCode; +import com.board.common.exception.exceptions.BaseException; + +public class GlobalException extends BaseException { + + public GlobalException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java b/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java deleted file mode 100644 index 82f43df..0000000 --- a/src/main/java/com/board/global/exception/exceptionhandler/GlobalExceptionHandler.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.board.global.exception.exceptionhandler; - -import com.board.global.exception.exceptionhandler.dto.GlobalErrorResponse; -import com.board.global.exception.exceptions.GlobalErrorCode; -import com.board.global.exception.exceptions.GlobalException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; - -public class GlobalExceptionHandler { - - @ExceptionHandler(GlobalException.class) - public ResponseEntity handleException(GlobalException e) { - GlobalErrorCode errorCode = e.getErrorCode(); - GlobalErrorResponse response = new GlobalErrorResponse(errorCode.getCustomCode(), errorCode.getMessage()); - - return ResponseEntity.status(errorCode.getHttpStatus()).body(response); - } -} diff --git a/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java b/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java deleted file mode 100644 index 571f542..0000000 --- a/src/main/java/com/board/global/exception/exceptionhandler/dto/GlobalErrorResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.board.global.exception.exceptionhandler.dto; - -public record GlobalErrorResponse( - String customCode, - String message -) { -} diff --git a/src/main/java/com/board/global/exception/exceptions/GlobalException.java b/src/main/java/com/board/global/exception/exceptions/GlobalException.java deleted file mode 100644 index bd8585b..0000000 --- a/src/main/java/com/board/global/exception/exceptions/GlobalException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.board.global.exception.exceptions; - -import lombok.Getter; - -@Getter -public class GlobalException extends RuntimeException { - - private final GlobalErrorCode errorCode; - - public GlobalException(GlobalErrorCode errorCode) { - super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); - this.errorCode = errorCode; - } -} diff --git a/src/main/java/com/board/global/interceptor/AuthInterceptor.java b/src/main/java/com/board/global/interceptor/AuthInterceptor.java index 789fa1d..1aa5100 100644 --- a/src/main/java/com/board/global/interceptor/AuthInterceptor.java +++ b/src/main/java/com/board/global/interceptor/AuthInterceptor.java @@ -1,6 +1,7 @@ package com.board.global.interceptor; -import com.board.global.interceptor.exception.NotFoundTokenException; +import com.board.global.exception.GlobalErrorCode; +import com.board.global.exception.GlobalException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Optional; @@ -15,7 +16,7 @@ public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) - .orElseThrow(NotFoundTokenException::new); + .orElseThrow(() -> new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN)); return tokenHeader.startsWith("Bearer "); } } diff --git a/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java b/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java deleted file mode 100644 index 786b181..0000000 --- a/src/main/java/com/board/global/interceptor/exception/NotFoundTokenException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.global.interceptor.exception; - -import com.board.global.exception.exceptions.GlobalErrorCode; -import com.board.global.exception.exceptions.GlobalException; - -public class NotFoundTokenException extends GlobalException { - - public NotFoundTokenException() { - super(GlobalErrorCode.NOT_FOUND_TOKEN); - } -} diff --git a/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java b/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java index dccf63f..1bd32ba 100644 --- a/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java +++ b/src/main/java/com/board/global/resolver/exception/TokenExpirationException.java @@ -1,7 +1,7 @@ package com.board.global.resolver.exception; -import com.board.global.exception.exceptions.GlobalErrorCode; -import com.board.global.exception.exceptions.GlobalException; +import com.board.global.exception.GlobalErrorCode; +import com.board.global.exception.GlobalException; public class TokenExpirationException extends GlobalException { diff --git a/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java b/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java index 2b874d2..66af9fb 100644 --- a/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java +++ b/src/main/java/com/board/global/resolver/exception/TokenInvalidException.java @@ -1,7 +1,7 @@ package com.board.global.resolver.exception; -import com.board.global.exception.exceptions.GlobalErrorCode; -import com.board.global.exception.exceptions.GlobalException; +import com.board.global.exception.GlobalErrorCode; +import com.board.global.exception.GlobalException; public class TokenInvalidException extends GlobalException { diff --git a/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java b/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java index 51879f0..1281f6c 100644 --- a/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java +++ b/src/main/java/com/board/global/resolver/exception/TokenVerificationException.java @@ -1,7 +1,7 @@ package com.board.global.resolver.exception; -import com.board.global.exception.exceptions.GlobalErrorCode; -import com.board.global.exception.exceptions.GlobalException; +import com.board.global.exception.GlobalErrorCode; +import com.board.global.exception.GlobalException; public class TokenVerificationException extends GlobalException { diff --git a/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java b/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java index f8c1ae8..5ffa9b2 100644 --- a/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java +++ b/src/main/java/com/board/member/domain/member/exception/NotMatchPasswordException.java @@ -1,7 +1,7 @@ package com.board.member.domain.member.exception; -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; +import com.board.member.exception.MemberErrorCode; +import com.board.member.exception.MemberException; public class NotMatchPasswordException extends MemberException { diff --git a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java b/src/main/java/com/board/member/exception/MemberErrorCode.java similarity index 67% rename from src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java rename to src/main/java/com/board/member/exception/MemberErrorCode.java index 5a3624e..67549ee 100644 --- a/src/main/java/com/board/member/exception/exceptions/MemberErrorCode.java +++ b/src/main/java/com/board/member/exception/MemberErrorCode.java @@ -1,10 +1,9 @@ -package com.board.member.exception.exceptions; +package com.board.member.exception; -import lombok.Getter; +import com.board.common.exception.exceptions.BaseErrorCode; import org.springframework.http.HttpStatus; -@Getter -public enum MemberErrorCode { +public enum MemberErrorCode implements BaseErrorCode { DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "M001", "이미 존재하는 아이디 입니다."), DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "M002", "이미 존재하는 닉네임 입니다."), @@ -21,4 +20,19 @@ public enum MemberErrorCode { this.customCode = customCode; this.message = message; } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String customCode() { + return customCode; + } + + @Override + public String message() { + return message; + } } diff --git a/src/main/java/com/board/member/exception/MemberException.java b/src/main/java/com/board/member/exception/MemberException.java new file mode 100644 index 0000000..42d3105 --- /dev/null +++ b/src/main/java/com/board/member/exception/MemberException.java @@ -0,0 +1,11 @@ +package com.board.member.exception; + +import com.board.common.exception.exceptions.BaseErrorCode; +import com.board.common.exception.exceptions.BaseException; + +public class MemberException extends BaseException { + + public MemberException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java b/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java deleted file mode 100644 index 3b3194a..0000000 --- a/src/main/java/com/board/member/exception/exceptionhandler/MemberExceptionHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.board.member.exception.exceptionhandler; - -import com.board.member.exception.exceptionhandler.dto.MemberErrorResponse; -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class MemberExceptionHandler { - - @ExceptionHandler(MemberException.class) - public ResponseEntity handleException(MemberException e) { - MemberErrorCode memberErrorCode = e.getErrorCode(); - MemberErrorResponse response = new MemberErrorResponse( - memberErrorCode.getCustomCode(), - memberErrorCode.getMessage() - ); - - return ResponseEntity.status(memberErrorCode.getHttpStatus()).body(response); - } -} diff --git a/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java b/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java deleted file mode 100644 index 6c034df..0000000 --- a/src/main/java/com/board/member/exception/exceptionhandler/dto/MemberErrorResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.board.member.exception.exceptionhandler.dto; - -public record MemberErrorResponse( - String customCode, - String message -) { -} diff --git a/src/main/java/com/board/member/exception/exceptions/MemberException.java b/src/main/java/com/board/member/exception/exceptions/MemberException.java deleted file mode 100644 index 7afaab5..0000000 --- a/src/main/java/com/board/member/exception/exceptions/MemberException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.board.member.exception.exceptions; - -import lombok.Getter; - -@Getter -public class MemberException extends RuntimeException{ - - private final MemberErrorCode errorCode; - - public MemberException(MemberErrorCode errorCode) { - super(errorCode.getCustomCode() + ": " + errorCode.getMessage()); - this.errorCode = errorCode; - } -} diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 1b9041e..6cd512d 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -10,10 +10,9 @@ import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.domain.auth.TokenProvider; import com.board.member.domain.member.Member; +import com.board.member.exception.MemberErrorCode; +import com.board.member.exception.MemberException; import com.board.member.repository.MemberRepository; -import com.board.member.service.auth.exception.ExistLoginIdException; -import com.board.member.service.auth.exception.NotMatchLoginIdException; -import com.board.member.service.auth.exception.ExistNickNameException; import com.board.global.resolver.exception.TokenInvalidException; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -67,18 +66,18 @@ private DecodedJWT verifyToken(String token) { private Member findMemberByLoginId(String loginId) { return memberRepository.findMemberByMemberLoginId(loginId) - .orElseThrow(NotMatchLoginIdException::new); + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_MATCH_LOGIN_ID)); } private void checkDuplicateLoginId(String loginId) { if(memberRepository.existsByMemberLoginId(loginId)) { - throw new ExistLoginIdException(); + throw new MemberException(MemberErrorCode.DUPLICATE_LOGIN_ID); } } private void checkDuplicateNickName(String memberNickName) { if(memberRepository.existsByMemberNickName(memberNickName)) { - throw new ExistNickNameException(); + throw new MemberException(MemberErrorCode.DUPLICATE_NICKNAME); } } } diff --git a/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java b/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java deleted file mode 100644 index 3cf634e..0000000 --- a/src/main/java/com/board/member/service/auth/exception/ExistLoginIdException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.member.service.auth.exception; - -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; - -public class ExistLoginIdException extends MemberException { - - public ExistLoginIdException() { - super(MemberErrorCode.DUPLICATE_LOGIN_ID); - } -} diff --git a/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java b/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java deleted file mode 100644 index 55c349d..0000000 --- a/src/main/java/com/board/member/service/auth/exception/ExistNickNameException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.member.service.auth.exception; - -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; - -public class ExistNickNameException extends MemberException { - - public ExistNickNameException() { - super(MemberErrorCode.DUPLICATE_NICKNAME); - } -} diff --git a/src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java b/src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java deleted file mode 100644 index 6a48c02..0000000 --- a/src/main/java/com/board/member/service/auth/exception/NotMatchLoginIdException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.member.service.auth.exception; - -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; - -public class NotMatchLoginIdException extends MemberException { - - public NotMatchLoginIdException() { - super(MemberErrorCode.NOT_MATCH_LOGIN_ID); - } -} diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index 191c725..dbb52a6 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -3,8 +3,9 @@ import com.board.member.controller.member.dto.reponse.MemberResponse; import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.domain.member.Member; +import com.board.member.exception.MemberErrorCode; +import com.board.member.exception.MemberException; import com.board.member.repository.MemberRepository; -import com.board.member.service.member.exception.NotFoundMemberException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -51,6 +52,6 @@ public MemberResponse deleteMember(Long memberId) { private Member getMember(Long memberId) { return memberRepository.findMemberById(memberId) - .orElseThrow(NotFoundMemberException::new); + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER)); } } diff --git a/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java b/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java deleted file mode 100644 index 7564785..0000000 --- a/src/main/java/com/board/member/service/member/exception/NotFoundMemberException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.board.member.service.member.exception; - -import com.board.member.exception.exceptions.MemberErrorCode; -import com.board.member.exception.exceptions.MemberException; - -public class NotFoundMemberException extends MemberException { - - public NotFoundMemberException() { - super(MemberErrorCode.NOT_FOUND_MEMBER); - } -} From 1b81f1724f2a0d2198813e0d560161a34a07a45c Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:44:18 +0900 Subject: [PATCH 28/52] =?UTF-8?q?feat:=20memberServiceTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/test.md | 12 ++ .../member/dto/reponse/MemberResponse.java | 2 +- .../service/member/MemberServiceTest.java | 106 ++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/board/member/service/member/MemberServiceTest.java diff --git a/docs/test.md b/docs/test.md index ac052c9..dd17419 100644 --- a/docs/test.md +++ b/docs/test.md @@ -116,7 +116,19 @@ public void tearDown() { +## Service layer Test +--- + +### 1. UnitTest + +* Service layer 만 테스트를 진행한다. +* 외부 의존성은 모두 mocking 해서 처리한다. +* 테스트 속도가 빠르고 로직을 집중적으로 테스트 할 수 있다. +### 2. IntegrationTest(Spring Context 전체를 올림) +* ``@SpringBootTest`` 와 ``@Transactional`` 어노테이션을 사용하여 테스트 진행한다. +* 실제 Bean 들을 다 올리기 때문에 실제 동작을 더 잘 검증할 수 있다. +* 하지만 속도 느리고 무겁다. diff --git a/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java b/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java index ab20514..cd7054e 100644 --- a/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java +++ b/src/main/java/com/board/member/controller/member/dto/reponse/MemberResponse.java @@ -1,7 +1,7 @@ package com.board.member.controller.member.dto.reponse; public record MemberResponse( - String mame, + String name, String nickName, String id, String password diff --git a/src/test/java/com/board/member/service/member/MemberServiceTest.java b/src/test/java/com/board/member/service/member/MemberServiceTest.java new file mode 100644 index 0000000..c751af3 --- /dev/null +++ b/src/test/java/com/board/member/service/member/MemberServiceTest.java @@ -0,0 +1,106 @@ +package com.board.member.service.member; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.controller.member.dto.request.MemberRequest; +import com.board.member.domain.member.Member; +import com.board.member.exception.MemberErrorCode; +import com.board.member.exception.MemberException; +import com.board.member.repository.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("NonAsciiCharacters") +class MemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private MemberService memberService; + + private Member member; + + @BeforeEach + void set() { + member = new Member("신짱구", "짱구", "aaa", "password123"); + } + + @Nested + class 정상_동작_테스트를_진행한다 { + + @Test + @DisplayName("멤버 아이디에 해당하는 유저를 보여준다.") + void showMember() { + // given + Long memberId = 1L; + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(member)); + + // when + MemberResponse response = memberService.showMember(memberId); + + // then + assertThat(response.name()).isEqualTo("신짱구"); + } + + @Test + void updateMember() { + // given + Long memberId = 1L; + MemberRequest request = new MemberRequest( + "홍길동", + "길동", + "sss", + "123" + ); + given(memberRepository.findMemberById(memberId)).willReturn(Optional.ofNullable(member)); + + // when + MemberResponse response = memberService.updateMember(memberId, request); + + // then + assertThat(response.name()).isEqualTo("홍길동"); + } + + @Test + void deleteMember() { + // given + Long memberId = 1L; + given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(member)); + + // when + MemberResponse response = memberService.deleteMember(memberId); + + // then + assertThat(response.name()).isEqualTo("신짱구"); + } + } + + @Nested + class 예외_처리_테스트를_진행한다 { + + @Test + @DisplayName("멤버 아이디에 해당하는 유저가 없을 경우 예외를 반환한다.") + void showMemberException() { + // given + Long memberId = 2L; + given(memberRepository.findMemberById(memberId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> memberService.showMember(memberId)) + .isInstanceOf(MemberException.class) + .hasMessageContaining(MemberErrorCode.NOT_FOUND_MEMBER.message()); + } + } +} From ba530b02574d9ae830a8c7c75af4744022205650 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:14:59 +0900 Subject: [PATCH 29/52] =?UTF-8?q?feat:=20CommentServiceTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentServiceTest.java | 222 ++++++++++++++++++ .../service/member/MemberServiceTest.java | 2 + 2 files changed, 224 insertions(+) create mode 100644 src/test/java/com/board/comment/service/CommentServiceTest.java diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..b030c4e --- /dev/null +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -0,0 +1,222 @@ +package com.board.comment.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.reponse.CommentResponses; +import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.domain.Comment; +import com.board.comment.exception.CommentErrorCode; +import com.board.comment.exception.CommentException; +import com.board.comment.repository.CommentRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("NonAsciiCharacters") +class CommentServiceTest { + + @Mock + private CommentRepository commentRepository; + + @InjectMocks + private CommentService commentService; + + private Comment comment; + private List comments; + + @BeforeEach + void set() { + comment = new Comment(1L, 1L, "첫 번째 게시글 댓글 1"); + comments = List.of(comment); + } + + @Nested + class 정상_동작_테스트를_진행한다 { + + @Test + @DisplayName("댓글을 저장한다.") + void createComment() { + // given + CommentRequest request = new CommentRequest("첫 번째 게시글 댓글1"); + Long memberId = 1L; + Long articleId = 1L; + + given(commentRepository.save(any(Comment.class))).willReturn(comment); + + // when + CommentResponse response = commentService.createComment(request, memberId, articleId); + + // then + assertThat(response) + .extracting(CommentResponse::content, CommentResponse::memberId, CommentResponse::articleId) + .containsExactly("첫 번째 게시글 댓글1", memberId, articleId); + } + + @Test + @DisplayName("게시글에 해당하는 모든 댓글을 조회한다.") + void showArticleComments() { + // given + Long articleId = 1L; + given(commentRepository.findAllByArticleId(articleId)).willReturn(comments); + + // when + CommentResponses responses = commentService.showArticleComments(articleId); + + // then + assertThat(responses.commentResponses()).hasSize(1) + .extracting(CommentResponse::content) + .containsExactly("첫 번째 게시글 댓글 1"); + } + + @Test + @DisplayName("유저 아이디에 해당하는 모든 댓글을 조회한다.") + void showMemberArticles() { + // given + Long memberId = 1L; + given(commentRepository.findAllByMemberId(memberId)).willReturn(comments); + + // when + CommentResponses responses = commentService.showMemberArticles(memberId); + + // then + assertThat(responses.commentResponses()).hasSize(1) + .extracting(CommentResponse::content) + .containsExactly("첫 번째 게시글 댓글 1"); + } + + @Test + @DisplayName("댓글을 업데이트 한다.") + void updateComment() { + // given + Long commentId = 1L; + Long memberId = 1L; + CommentRequest request = new CommentRequest("수정된 게시글"); + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when + CommentResponse response = commentService.updateComment(request, memberId, commentId); + + // then + assertThat(response.content()).isEqualTo("수정된 게시글"); + } + + @Test + @DisplayName("댓글을 삭제한다.") + void deleteComment() { + // given + Long memberId = 1L; + Long commentId = 1L; + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when + CommentResponse response = commentService.deleteComment(memberId, commentId); + + // then + assertThat(response.content()).isEqualTo("첫 번째 게시글 댓글 1"); + } + + @Test + @DisplayName("게시글에 해당하는 모든 댓글을 조회한다.") + void getArticleComments() { + // given + Long articleId = 1L; + given(commentRepository.findAllByArticleId(articleId)).willReturn(comments); + + // when + List responses = commentService.getArticleComments(articleId); + + // then + assertThat(responses).hasSize(1) + .extracting(Comment::getArticleId) + .containsExactly(1L); + } + + @Test + @DisplayName("유저가 작성한 모든 댓글을 조회한다.") + void getMemberComments() { + // given + Long memberId = 1L; + given(commentRepository.findAllByMemberId(memberId)).willReturn(comments); + + // when + List responses = commentService.getMemberComments(memberId); + + // then + assertThat(responses).hasSize(1) + .extracting(Comment::getArticleId) + .containsExactly(1L); + } + + @Test + @DisplayName("특정 댓글을 조회한다.") + void getComment() { + // given + Long commentId = 1L; + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when + Comment response = commentService.getComment(commentId); + + // then + assertThat(response.getContent()).isEqualTo("첫 번째 게시글 댓글 1"); + } + } + + @Nested + class 예외_처리_테스트를_진행한다 { + + @Test + @DisplayName("존재하지 않는 댓글을 조회한다.") + void getCommentException() { + // given + Long commentId = 3L; + given(commentRepository.findById(commentId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.getComment(commentId)) + .isInstanceOf(CommentException.class) + .hasMessageContaining(CommentErrorCode.NOT_FOUND_COMMENT.message()); + } + + @Test + @DisplayName("댓글 작성자가 아닌 유저가 댓글을 수정하려 할 경우 예외가 발생한다.") + void forbiddenUpdateComment() { + // given + Long memberId = 2L; + Long commentId = 1L; + CommentRequest request = new CommentRequest("댓글 수정"); + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> commentService.updateComment(request, memberId, commentId)) + .isInstanceOf(CommentException.class) + .hasMessageContaining(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT.message()); + } + + @Test + @DisplayName("댓글 작성자가 아닌 유저가 댓글을 삭제하려 할 경우 예외가 발생한다.") + void forbiddenDeleteComment() { + // given + Long memberId = 2L; + Long commentId = 1L; + given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); + + // when & then + assertThatThrownBy(() -> commentService.deleteComment(memberId, commentId)) + .isInstanceOf(CommentException.class) + .hasMessageContaining(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT.message()); + } + } +} diff --git a/src/test/java/com/board/member/service/member/MemberServiceTest.java b/src/test/java/com/board/member/service/member/MemberServiceTest.java index c751af3..1ca5e49 100644 --- a/src/test/java/com/board/member/service/member/MemberServiceTest.java +++ b/src/test/java/com/board/member/service/member/MemberServiceTest.java @@ -55,6 +55,7 @@ void showMember() { } @Test + @DisplayName("멤버 정보를 업데이트 한다.") void updateMember() { // given Long memberId = 1L; @@ -74,6 +75,7 @@ void updateMember() { } @Test + @DisplayName("멤버를 삭제한다.") void deleteMember() { // given Long memberId = 1L; From 00747c47d429ec458a798bb5760b62ce699290f6 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:15:26 +0900 Subject: [PATCH 30/52] =?UTF-8?q?feat:=20ArticleServiceTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/service/ArticleServiceTest.java | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 src/test/java/com/board/article/service/ArticleServiceTest.java diff --git a/src/test/java/com/board/article/service/ArticleServiceTest.java b/src/test/java/com/board/article/service/ArticleServiceTest.java new file mode 100644 index 0000000..f2a4ef6 --- /dev/null +++ b/src/test/java/com/board/article/service/ArticleServiceTest.java @@ -0,0 +1,232 @@ +package com.board.article.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.board.article.controller.dto.request.ArticleRequest; +import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.controller.dto.response.ArticleResponses; +import com.board.article.domain.Article; +import com.board.article.exception.ArticleErrorCode; +import com.board.article.exception.ArticleException; +import com.board.article.repository.ArticleRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("NonAsciiCharacters") +class ArticleServiceTest { + + @Mock + private ArticleRepository articleRepository; + + @InjectMocks + private ArticleService articleService; + + private Article article; + private List
articles; + + @BeforeEach + void set() { + article = new Article(1L, "제목1", "내용1"); + articles = List.of(article); + } + + @Nested + class 정상_동작_테스트를_진행한다 { + + @Test + @DisplayName("게시글을 생성한다.") + void createArticle() { + // given + ArticleRequest request = new ArticleRequest("제목1", "내용1"); + Long memberId = 1L; + given(articleRepository.save(any(Article.class))).willReturn(article); + + // when + ArticleResponse response = articleService.createArticle(request, memberId); + + // then + assertThat(response) + .extracting(ArticleResponse::title, ArticleResponse::content, ArticleResponse::memberId) + .containsExactly("제목1", "내용1", memberId); + } + + @Test + @DisplayName("모든 게시글을 조회한다.") + void showAllArticles() { + // given + given(articleRepository.findAll()).willReturn(articles); + + // when + ArticleResponses responses = articleService.showAllArticles(); + + // then + assertThat(responses.articleResponses()).hasSize(1) + .extracting(ArticleResponse::title) + .containsExactly("제목1"); + } + + @Test + @DisplayName("게시글을 단건 조회한다.") + void showArticle() { + // given + Long articleId = 1L; + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when + ArticleResponse response = articleService.showArticle(articleId); + + // then + assertThat(response.title()).isEqualTo("제목1"); + } + + @Test + @DisplayName("유저가 작성한 게시글을 모두 조회한다.") + void showMemberArticles() { + // given + Long memberId = 1L; + given(articleRepository.findArticleByMemberId(memberId)).willReturn(articles); + + // when + ArticleResponses responses = articleService.showMemberArticles(memberId); + + // then + assertThat(responses.articleResponses()).hasSize(1) + .extracting(ArticleResponse::title) + .containsExactly("제목1"); + } + + @Test + @DisplayName("게시글을 수정한다.") + void updateArticle() { + // given + Long articleId = 1L; + Long memberId = 1L; + ArticleRequest request = new ArticleRequest("수정된 제목", "수정된 내용"); + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when + ArticleResponse response = articleService.updateArticle(request, articleId, memberId); + + // then + assertThat(response.title()).isEqualTo("수정된 제목"); + } + + @Test + @DisplayName("게시글을 삭제한다.") + void deleteArticle() { + // given + Long articleId = 1L; + Long memberId = 1L; + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when + ArticleResponse response = articleService.deleteArticle(articleId, memberId); + + // then + assertThat(response.title()).isEqualTo("제목1"); + } + + @Test + @DisplayName("모든 게시글을 가져온다.") + void getAllArticles() { + // given + given(articleRepository.findAll()).willReturn(articles); + + // when + List
response = articleService.getAllArticles(); + + // then + assertThat(response).hasSize(1) + .extracting(Article::getTitle) + .containsExactly("제목1"); + } + + @Test + @DisplayName("특정 게시글을 가져온다.") + void getArticle() { + // given + Long articleId = 1L; + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when + Article response = articleService.getArticle(articleId); + + // then + assertThat(response.getTitle()).isEqualTo("제목1"); + } + + @Test + @DisplayName("유저가 작성한 게시글을 가져온다.") + void getMemberArticles() { + // given + Long memberId = 1L; + given(articleRepository.findArticleByMemberId(memberId)).willReturn(articles); + + // when + List
response = articleService.getMemberArticles(memberId); + + // then + assertThat(response).hasSize(1) + .extracting(Article::getTitle) + .containsExactly("제목1"); + } + } + + @Nested + class 예외_처리_테스트를_진행한다 { + + @Test + @DisplayName("존재하지 않는 게시글을 조회할 경우 예외가 발생한다.") + void notFoundArticle() { + // given + Long articleId = 3L; + given(articleRepository.findById(articleId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> articleService.getArticle(articleId)) + .isInstanceOf(ArticleException.class) + .hasMessageContaining(ArticleErrorCode.NOT_FOUND_ARTICLE.message()); + } + + @Test + @DisplayName("게시글 작성자가 아닌 사용자가 게시글을 수정하려 할 경우 예외가 발생한다.") + void forbiddenUpdateArticle() { + // given + Long articleId = 1L; + Long memberId = 2L; + ArticleRequest request = new ArticleRequest("수정 제목", "수정 내용"); + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when & then + assertThatThrownBy(() -> articleService.updateArticle(request, articleId, memberId)) + .isInstanceOf(ArticleException.class) + .hasMessageContaining(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE.message()); + } + + @Test + @DisplayName("게시글 작성자가 아닌 사용자가 게시글을 삭제하려 할 경우 예외가 발생한다.") + void forbiddenDeleteArticle() { + // given + Long articleId = 1L; + Long memberId = 2L; + given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); + + // when & then + assertThatThrownBy(() -> articleService.deleteArticle(articleId, memberId)) + .isInstanceOf(ArticleException.class) + .hasMessageContaining(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE.message()); + } + } +} From 8d5944ba75f9ca668ee3c3118a933663fbf92023 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:46:05 +0900 Subject: [PATCH 31/52] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B4=80=EB=A0=A8=20=EC=B1=85=EC=9E=84?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AuthService 에 있는 tokenProvider 로직 분리 --- .../global/resolver/AuthArgumentResolver.java | 9 +++--- .../Infrastructure/auth/JwtTokenProvider.java | 32 ++++++++++++++++--- .../member/domain/auth/TokenProvider.java | 4 +-- .../member/service/auth/AuthService.java | 27 ---------------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java index a96ae64..f19d71a 100644 --- a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java +++ b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java @@ -1,7 +1,7 @@ package com.board.global.resolver; import com.board.global.resolver.annotation.Auth; -import com.board.member.service.auth.AuthService; +import com.board.member.Infrastructure.auth.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; @@ -16,8 +16,9 @@ public class AuthArgumentResolver implements HandlerMethodArgumentResolver { private static final String TOKEN_HEADER_NAME = "Authorization"; + private static final int TOKEN_BODY_DELIMITER = 7; - private final AuthService service; + private final JwtTokenProvider tokenProvider; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -29,7 +30,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); String tokenHeader = request.getHeader(TOKEN_HEADER_NAME); - String token = tokenHeader.substring(7); - return service.verifyAndExtractToken(token); + String token = tokenHeader.substring(TOKEN_BODY_DELIMITER); + return tokenProvider.extractMemberId(token); } } diff --git a/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java b/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java index 3044d11..2feb080 100644 --- a/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java +++ b/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java @@ -3,9 +3,15 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; +import com.board.global.resolver.exception.TokenExpirationException; +import com.board.global.resolver.exception.TokenInvalidException; +import com.board.global.resolver.exception.TokenVerificationException; import com.board.member.domain.auth.TokenProvider; import java.util.Date; +import java.util.Optional; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -16,7 +22,8 @@ public class JwtTokenProvider implements TokenProvider { private final Algorithm algorithm; private final long expirationPeriod; - public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, @Value("${jwt.expiration-period}") long expirationPeriod) { + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, + @Value("${jwt.expiration-period}") long expirationPeriod) { this.algorithm = Algorithm.HMAC256(secretKey); this.expirationPeriod = expirationPeriod; } @@ -31,6 +38,17 @@ public String create(Long memberId) { .sign(algorithm); } + @Override + public Long extractMemberId(String token) { + try { + return extractToken(token); + } catch (JWTDecodeException e) { + throw new TokenVerificationException(); + } catch (TokenExpiredException e) { + throw new TokenExpirationException(); + } + } + private Date issuedDate() { return new Date(System.currentTimeMillis()); } @@ -39,10 +57,16 @@ private Date expiredDate() { return new Date(System.currentTimeMillis() + expirationPeriod * 1000L); //2시간 } - @Override - public DecodedJWT verifyToken(String token) { + private Long extractToken(String token) { + return verifyToken(token).getClaim("memberId") + .asLong(); + } + + private DecodedJWT verifyToken(String token) { JWTVerifier verifier = JWT.require(algorithm) .build(); - return verifier.verify(token); + + return Optional.of(verifier.verify(token)) + .orElseThrow(TokenInvalidException::new); } } diff --git a/src/main/java/com/board/member/domain/auth/TokenProvider.java b/src/main/java/com/board/member/domain/auth/TokenProvider.java index b251740..4a4f17b 100644 --- a/src/main/java/com/board/member/domain/auth/TokenProvider.java +++ b/src/main/java/com/board/member/domain/auth/TokenProvider.java @@ -1,9 +1,7 @@ package com.board.member.domain.auth; -import com.auth0.jwt.interfaces.DecodedJWT; - public interface TokenProvider { String create(Long memberId); - DecodedJWT verifyToken(String token); + Long extractMemberId(String token); } diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 6cd512d..77086ee 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -1,10 +1,5 @@ package com.board.member.service.auth; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.board.global.resolver.exception.TokenExpirationException; -import com.board.global.resolver.exception.TokenVerificationException; import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; import com.board.member.controller.auth.dto.response.SignUpResponse; @@ -13,8 +8,6 @@ import com.board.member.exception.MemberErrorCode; import com.board.member.exception.MemberException; import com.board.member.repository.MemberRepository; -import com.board.global.resolver.exception.TokenInvalidException; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -44,26 +37,6 @@ public String login(LoginRequest request) { return tokenProvider.create(member.getId()); } - public Long verifyAndExtractToken(String token) { - try { - return extractToken(token); - } catch (JWTDecodeException e) { - throw new TokenVerificationException(); - } catch (TokenExpiredException e) { - throw new TokenExpirationException(); - } - } - - private Long extractToken(String token) { - return verifyToken(token).getClaim("memberId") - .asLong(); - } - - private DecodedJWT verifyToken(String token) { - return Optional.of(tokenProvider.verifyToken(token)) - .orElseThrow(TokenInvalidException::new); - } - private Member findMemberByLoginId(String loginId) { return memberRepository.findMemberByMemberLoginId(loginId) .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_MATCH_LOGIN_ID)); From 912c548a96262b3cdc289b0bccb6809b8a4bb43f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:11:08 +0900 Subject: [PATCH 32/52] =?UTF-8?q?feat:=20AuthServiceTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/service/auth/AuthServiceTest.java | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 src/test/java/com/board/member/service/auth/AuthServiceTest.java diff --git a/src/test/java/com/board/member/service/auth/AuthServiceTest.java b/src/test/java/com/board/member/service/auth/AuthServiceTest.java new file mode 100644 index 0000000..3a7e058 --- /dev/null +++ b/src/test/java/com/board/member/service/auth/AuthServiceTest.java @@ -0,0 +1,146 @@ +package com.board.member.service.auth; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.board.member.controller.auth.dto.request.LoginRequest; +import com.board.member.controller.auth.dto.request.SignUpRequest; +import com.board.member.controller.auth.dto.response.SignUpResponse; +import com.board.member.domain.auth.TokenProvider; +import com.board.member.domain.member.Member; +import com.board.member.exception.MemberErrorCode; +import com.board.member.exception.MemberException; +import com.board.member.repository.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("NonAsciiCharacters") +class AuthServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private TokenProvider tokenProvider; + + @InjectMocks + private AuthService authService; + + private Member member; + private SignUpRequest signUpRequest; + private LoginRequest loginRequest; + + @BeforeEach + void setUp() { + member = new Member("홍길동", "길동이", "gildong123", "1234"); + ReflectionTestUtils.setField(member, "id", 1L); + signUpRequest = new SignUpRequest("홍길동", "길동이", "gildong123", "1234"); + loginRequest = new LoginRequest("gildong123", "1234"); + } + + @Nested + class 회원가입_테스트 { + + @Test + @DisplayName("정상적으로 회원가입을 수행한다.") + void signUpSuccess() { + // given + given(memberRepository.existsByMemberLoginId("gildong123")).willReturn(false); + given(memberRepository.existsByMemberNickName("길동이")).willReturn(false); + given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { + Member saved = invocation.getArgument(0); + ReflectionTestUtils.setField(saved, "id", 1L); + return saved; + }); + given(tokenProvider.create(1L)).willReturn("fake-jwt-token"); + + // when + SignUpResponse response = authService.signUp(signUpRequest); + + // then + assertThat(response) + .extracting(SignUpResponse::memberId, SignUpResponse::token) + .containsExactly(1L, "fake-jwt-token"); + } + + @Test + @DisplayName("중복된 아이디로 회원가입할 경우 예외가 발생한다.") + void signUpDuplicateLoginId() { + // given + given(memberRepository.existsByMemberLoginId("gildong123")).willReturn(true); + + // when & then + assertThatThrownBy(() -> authService.signUp(signUpRequest)) + .isInstanceOf(MemberException.class) + .hasMessageContaining(MemberErrorCode.DUPLICATE_LOGIN_ID.message()); + } + + @Test + @DisplayName("중복된 닉네임으로 회원가입할 경우 예외가 발생한다.") + void signUpDuplicateNickName() { + // given + given(memberRepository.existsByMemberLoginId("gildong123")).willReturn(false); + given(memberRepository.existsByMemberNickName("길동이")).willReturn(true); + + // when & then + assertThatThrownBy(() -> authService.signUp(signUpRequest)) + .isInstanceOf(MemberException.class) + .hasMessageContaining(MemberErrorCode.DUPLICATE_NICKNAME.message()); + } + } + + @Nested + class 로그인_테스트 { + + @Test + @DisplayName("정상적으로 로그인을 수행한다.") + void loginSuccess() { + // given + Long memberId = 1L; + given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.of(member)); + given(tokenProvider.create(memberId)).willReturn("fake-jwt-token"); + + // when + String token = authService.login(loginRequest); + + // then + assertThat(token).isEqualTo("fake-jwt-token"); + } + + @Test + @DisplayName("존재하지 않는 아이디로 로그인 시도 시 예외가 발생한다.") + void loginInvalidLoginId() { + // given + given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> authService.login(loginRequest)) + .isInstanceOf(MemberException.class) + .hasMessageContaining(MemberErrorCode.NOT_MATCH_LOGIN_ID.message()); + } + + @Test + @DisplayName("비밀번호가 일치하지 않을 경우 예외가 발생한다.") + void loginInvalidPassword() { + // given + Member wrongPasswordMember = new Member("홍길동", "길동이", "gildong123", "다른비밀번호"); + given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.of(wrongPasswordMember)); + + // when & then + assertThatThrownBy(() -> authService.login(loginRequest)) + .isInstanceOf(MemberException.class) + .hasMessageContaining(MemberErrorCode.NOT_MATCH_PASSWORD.message()); + } + } +} From 7ae9e51ad08d881da85031058d1048d7aab93c08 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:18:32 +0900 Subject: [PATCH 33/52] =?UTF-8?q?feat:=20MemberControllerTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/MemberControllerTest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/test/java/com/board/member/controller/member/MemberControllerTest.java diff --git a/src/test/java/com/board/member/controller/member/MemberControllerTest.java b/src/test/java/com/board/member/controller/member/MemberControllerTest.java new file mode 100644 index 0000000..7d0b2c6 --- /dev/null +++ b/src/test/java/com/board/member/controller/member/MemberControllerTest.java @@ -0,0 +1,91 @@ +package com.board.member.controller.member; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.global.resolver.AuthArgumentResolver; +import com.board.member.controller.member.dto.reponse.MemberResponse; +import com.board.member.controller.member.dto.request.MemberRequest; +import com.board.member.service.member.MemberService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(MemberController.class) +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MemberService memberService; + + @MockBean + private AuthArgumentResolver authArgumentResolver; + + private MemberResponse response; + + @BeforeEach + void set() throws Exception { + response = new MemberResponse("신짱구", "짱구", "aaa", "password123"); + given(authArgumentResolver.supportsParameter(any())).willReturn(true); + given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); + } + + @Test + @DisplayName("유저 정보를 조회한다.") + void showMember() throws Exception { + // given + Long memberId = 1L; + given(memberService.showMember(memberId)).willReturn(response); + + // when & then + mockMvc.perform(get("/members") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(memberId))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("신짱구")); + } + + @Test + @DisplayName("유저 정보를 수정한다.") + void updateMember() throws Exception { + // given + MemberRequest request = new MemberRequest("신짱구", "짱구", "newId", "newPassword"); + MemberResponse updatedResponse = new MemberResponse("신짱구", "짱구", "newId", "newPassword"); + given(memberService.updateMember(1L, request)).willReturn(updatedResponse); + + // when & then + mockMvc.perform(patch("/members") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value("newId")); + } + + @Test + @DisplayName("유저 정보를 삭제한다.") + void deleteMember() throws Exception { + // given + given(memberService.deleteMember(1L)).willReturn(response); + + // when & then + mockMvc.perform(delete("/members")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("신짱구")); + } +} From 12703b3c8e54542a82e777678492d74930525ef9 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:53:49 +0900 Subject: [PATCH 34/52] =?UTF-8?q?feat:=20AuthControllerTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/auth/AuthControllerTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/test/java/com/board/member/controller/auth/AuthControllerTest.java diff --git a/src/test/java/com/board/member/controller/auth/AuthControllerTest.java b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java new file mode 100644 index 0000000..11e72ce --- /dev/null +++ b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java @@ -0,0 +1,81 @@ +package com.board.member.controller.auth; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.member.Infrastructure.auth.JwtTokenProvider; +import com.board.member.controller.auth.dto.request.LoginRequest; +import com.board.member.controller.auth.dto.request.SignUpRequest; +import com.board.member.controller.auth.dto.response.SignUpResponse; +import com.board.member.service.auth.AuthService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(AuthController.class) +class AuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private JwtTokenProvider jwtTokenProvider; + + @MockBean + private AuthService authService; + + private SignUpResponse signUpResponse; + + @BeforeEach + void set() { + signUpResponse = new SignUpResponse(1L, "token"); + } + + @Test + @DisplayName("회원가입을 진행한다.") + void signUp() throws Exception { + // given + SignUpRequest request = new SignUpRequest("신짱구", "짱구", "aaa", "password123"); + given(authService.signUp(request)).willReturn(signUpResponse); + given(jwtTokenProvider.create(1L)).willReturn("token"); + + // when & then + mockMvc.perform(post("/signUp") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost/signUp/1")) + .andExpect(jsonPath("$.memberId").value(1L)) + .andExpect(jsonPath("$.token").value("token")); + } + + @Test + @DisplayName("로그인을 진행한다.") + void login() throws Exception { + // given + LoginRequest request = new LoginRequest("aaa", "111"); + String token = "token"; + given(authService.login(request)).willReturn(token); + given(jwtTokenProvider.create(1L)).willReturn(token); + + // when & then + mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(content().string("Authorization=Bearer+token")); + } +} From a8fedceed87997e8e5fa65f9977ed23ab66ce212 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:49:25 +0900 Subject: [PATCH 35/52] =?UTF-8?q?feat:=20ArticleControllerTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ArticleControllerTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/test/java/com/board/article/controller/ArticleControllerTest.java diff --git a/src/test/java/com/board/article/controller/ArticleControllerTest.java b/src/test/java/com/board/article/controller/ArticleControllerTest.java new file mode 100644 index 0000000..4b1f6d9 --- /dev/null +++ b/src/test/java/com/board/article/controller/ArticleControllerTest.java @@ -0,0 +1,134 @@ +package com.board.article.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.article.controller.dto.request.ArticleRequest; +import com.board.article.controller.dto.response.ArticleResponse; +import com.board.article.controller.dto.response.ArticleResponses; +import com.board.article.service.ArticleService; +import com.board.global.resolver.AuthArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(ArticleController.class) +class ArticleControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ArticleService articleService; + + @MockBean + private AuthArgumentResolver authArgumentResolver; + + private ArticleResponse articleResponse; + private ArticleResponses articleResponses; + + @BeforeEach + void setUp() throws Exception { + given(authArgumentResolver.supportsParameter(any())).willReturn(true); + given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); + articleResponse = new ArticleResponse(1L, 1L, "제목", "내용"); + articleResponses = new ArticleResponses(List.of(articleResponse)); + } + + @Test + @DisplayName("게시글을 생성한다.") + void createArticle() throws Exception { + // given + ArticleRequest request = new ArticleRequest("제목", "내용"); + given(articleService.createArticle(any(), any())).willReturn(articleResponse); + + // when & then + mockMvc.perform(post("/articles") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost/articles/1")) + .andExpect(jsonPath("$.articleId").value(1L)); + } + + @Test + @DisplayName("모든 게시글을 조회한다.") + void showAllArticles() throws Exception { + // given + given(articleService.showAllArticles()).willReturn(articleResponses); + + // when & then + mockMvc.perform(get("/articles")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); + } + + @Test + @DisplayName("게시글 하나를 조회한다.") + void showArticle() throws Exception { + // given + given(articleService.showArticle(1L)).willReturn(articleResponse); + + // when & then + mockMvc.perform(get("/articles/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.articleId").value(1L)); + } + + @Test + @DisplayName("회원의 게시글을 조회한다.") + void showMemberArticles() throws Exception { + // given + given(articleService.showMemberArticles(1L)).willReturn(articleResponses); + + // when & then + mockMvc.perform(get("/members/me/articles")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); + } + + @Test + @DisplayName("게시글을 수정한다.") + void updateArticle() throws Exception { + // given + ArticleRequest request = new ArticleRequest("수정된 제목", "수정된 내용"); + ArticleResponse response = new ArticleResponse(1L, 1L, "수정된 제목", "수정된 내용"); + given(articleService.updateArticle(any(), any(), any())).willReturn(response); + + // when & then + mockMvc.perform(patch("/articles/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("수정된 제목")); + } + + @Test + @DisplayName("게시글을 삭제한다.") + void deleteArticle() throws Exception { + // given + given(articleService.deleteArticle(1L, 1L)).willReturn(articleResponse); + + // when & then + mockMvc.perform(delete("/articles/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.articleId").value(1L)); + } +} From d91b26eadba1633c1fdd84e8be1ee3313ea02c2c Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Thu, 24 Apr 2025 23:58:17 +0900 Subject: [PATCH 36/52] =?UTF-8?q?feat:=20CommentControllerTest=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CommentControllerTest.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/test/java/com/board/comment/controller/CommentControllerTest.java diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java new file mode 100644 index 0000000..ea15da8 --- /dev/null +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -0,0 +1,120 @@ +package com.board.comment.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.comment.controller.dto.reponse.CommentResponse; +import com.board.comment.controller.dto.reponse.CommentResponses; +import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.service.CommentService; +import com.board.global.resolver.AuthArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CommentController.class) +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private CommentService commentService; + + @MockBean + private AuthArgumentResolver authArgumentResolver; + + private CommentResponse response; + private CommentResponses responses; + + @BeforeEach + void setUp() throws Exception { + response = new CommentResponse(1L, 1L, "댓글 내용"); + responses = new CommentResponses(List.of(response)); + given(authArgumentResolver.supportsParameter(any())).willReturn(true); + given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); + } + + @Test + @DisplayName("댓글을 생성한다.") + void createComment() throws Exception { + // given + CommentRequest request = new CommentRequest("댓글 내용"); + given(commentService.createComment(any(), any(), any())).willReturn(response); + + // when & then + mockMvc.perform(post("/articles/1/comments") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.content").value("댓글 내용")); + } + + @Test + @DisplayName("게시글의 댓글들을 조회한다.") + void showArticleComments() throws Exception { + // given + given(commentService.showArticleComments(1L)).willReturn(responses); + + // when & then + mockMvc.perform(get("/articles/1/comments")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.commentResponses[0].content").value("댓글 내용")); + } + + @Test + @DisplayName("회원이 작성한 댓글들을 조회한다.") + void showMemberComments() throws Exception { + // given + given(commentService.showMemberArticles(1L)).willReturn(responses); + + // when & then + mockMvc.perform(get("/members/me/comments")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.commentResponses[0].content").value("댓글 내용")); + } + + @Test + @DisplayName("댓글을 수정한다.") + void updateComment() throws Exception { + // given + CommentRequest request = new CommentRequest("수정된 댓글"); + CommentResponse updatedResponse = new CommentResponse(1L, 1L,"수정된 댓글"); + given(commentService.updateComment(any(), any(), any())).willReturn(updatedResponse); + + // when & then + mockMvc.perform(patch("/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 댓글")); + } + + @Test + @DisplayName("댓글을 삭제한다.") + void deleteComment() throws Exception { + // given + given(commentService.deleteComment(1L, 1L)).willReturn(response); + + // when & then + mockMvc.perform(delete("/comments/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("댓글 내용")); + } +} From 56eb3149c9ab5e06282b1a33851067b4a3b93ac3 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:45:04 +0900 Subject: [PATCH 37/52] =?UTF-8?q?feat:=20offset=20paging=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 13 +++++ .../article/controller/ArticleController.java | 9 ++- .../article/repository/ArticleRepository.java | 5 +- .../board/article/service/ArticleService.java | 13 +++-- .../comment/controller/CommentController.java | 9 ++- .../comment/repository/CommentRepository.java | 4 +- .../board/comment/service/CommentService.java | 13 +++-- .../controller/ArticleControllerTest.java | 9 ++- .../repository/ArticleRepositoryTest.java | 10 +++- .../article/service/ArticleServiceTest.java | 58 ++++--------------- .../controller/CommentControllerTest.java | 8 ++- .../repository/CommentRepositoryTest.java | 9 ++- .../comment/service/CommentServiceTest.java | 45 ++++---------- 13 files changed, 100 insertions(+), 105 deletions(-) diff --git a/docs/code_review.md b/docs/code_review.md index 23ff891..48d069c 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -53,3 +53,16 @@ public class GlobalException extends RuntimeException { * BaseException 생성, 그리고 이 클래스를 이용한 BaseExceptionHandler 생성 -> 핸들러 하나만 사용 가능 * MemberException, ArticleException 등등 BaseException 을 상속받아 BaseExceptionHandler 에서 에러 처리 가능 * ErrorCode 추가될때마다 exception 을 추가할 필요가 없어짐. + +--- + +# Offset Paging + +--- +## 장점 +* 구현이 쉽다 -> JPA에서 바로 Pageable 객체로 페이징 가능 +* 페이지 번호가 있는 프론트 구현 시, 잘 맞는다. + +## 단점 +* OFFSET 100000 같이 뒤 페이지로 갈수록 느려짐 → 인덱스가 있어도 느림 +* 페이징 도중 데이터가 추가/삭제되면 순서가 뒤틀릴 수 있음 (같은 댓글이 여러 번 보이거나 사라짐) diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index b744d5c..d686854 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -46,8 +47,12 @@ public ResponseEntity showArticle(@PathVariable Long articleId) } @GetMapping("/members/me/articles") - public ResponseEntity showMemberArticles(@Auth Long memberId) { - return ResponseEntity.ok(articleService.showMemberArticles(memberId)); + public ResponseEntity showMemberArticles( + @Auth Long memberId, + @RequestParam int page, + @RequestParam int size + ) { + return ResponseEntity.ok(articleService.showMemberArticles(memberId, page, size)); } @PatchMapping("/articles/{articleId}") diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java index 727e7af..b6a6e28 100644 --- a/src/main/java/com/board/article/repository/ArticleRepository.java +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -1,10 +1,11 @@ package com.board.article.repository; import com.board.article.domain.Article; -import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository { - List
findArticleByMemberId(Long memberId); + Page
findArticleByMemberId(Long memberId, Pageable pageable); } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index a63e2d8..7fee465 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -10,6 +10,10 @@ import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; +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; @@ -56,8 +60,8 @@ public ArticleResponse showArticle(Long articleId) { ); } - public ArticleResponses showMemberArticles(Long memberId) { - List articleResponses = getMemberArticles(memberId).stream() + public ArticleResponses showMemberArticles(Long memberId, int page, int size) { + List articleResponses = getMemberArticles(memberId, page, size).stream() .map(article -> new ArticleResponse( article.getId(), article.getMemberId(), @@ -113,7 +117,8 @@ public Article getArticle(Long articleId) { } @Transactional(readOnly = true) - public List
getMemberArticles(Long memberId) { - return articleRepository.findArticleByMemberId(memberId); + public Page
getMemberArticles(Long memberId, int page, int size) { + Pageable articlePageable = PageRequest.of(page, size, Sort.by("id").descending()); // 최신 게시글 부터 + return articleRepository.findArticleByMemberId(memberId, articlePageable); } } diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 072e51b..958fb92 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; @@ -40,8 +41,12 @@ public ResponseEntity showArticleComments(@PathVariable Long a } @GetMapping("members/me/comments") - public ResponseEntity showMemberComments(@Auth Long memberId) { - return ResponseEntity.ok(commentService.showMemberArticles(memberId)); + public ResponseEntity showMemberComments( + @Auth Long memberId, + @RequestParam int page, + @RequestParam int size + ) { + return ResponseEntity.ok(commentService.showMemberComments(memberId, page, size)); } @PatchMapping("/comments/{commentId}") diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 6129c64..034ec06 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -2,6 +2,8 @@ import com.board.comment.domain.Comment; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,5 +11,5 @@ public interface CommentRepository extends JpaRepository { List findAllByArticleId(Long articleId); - List findAllByMemberId(Long memberId); + Page findAllByMemberId(Long memberId, Pageable pageable); } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index d224ce3..13e96dc 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -10,6 +10,10 @@ import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; +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; @@ -43,8 +47,8 @@ public CommentResponses showArticleComments(Long articleId) { return new CommentResponses(commentResponses); } - public CommentResponses showMemberArticles(Long memberId) { - List commentResponses = getMemberComments(memberId).stream() + public CommentResponses showMemberComments(Long memberId, int page, int size) { + List commentResponses = getMemberComments(memberId, page, size).stream() .map(comment -> new CommentResponse( comment.getMemberId(), comment.getArticleId(), @@ -91,8 +95,9 @@ public List getArticleComments(Long articleId) { } @Transactional(readOnly = true) - public List getMemberComments(Long memberId) { - return commentRepository.findAllByMemberId(memberId); + public Page getMemberComments(Long memberId, int page, int size) { + Pageable commentPageable = PageRequest.of(page, size, Sort.by("id").descending()); + return commentRepository.findAllByMemberId(memberId, commentPageable); } @Transactional(readOnly = true) diff --git a/src/test/java/com/board/article/controller/ArticleControllerTest.java b/src/test/java/com/board/article/controller/ArticleControllerTest.java index 4b1f6d9..62744ac 100644 --- a/src/test/java/com/board/article/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/article/controller/ArticleControllerTest.java @@ -96,10 +96,15 @@ void showArticle() throws Exception { @DisplayName("회원의 게시글을 조회한다.") void showMemberArticles() throws Exception { // given - given(articleService.showMemberArticles(1L)).willReturn(articleResponses); + Long memberId = 1L; + int page = 0; + int size = 10; + given(articleService.showMemberArticles(memberId, page, size)).willReturn(articleResponses); // when & then - mockMvc.perform(get("/members/me/articles")) + mockMvc.perform(get("/members/me/articles") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) .andExpect(status().isOk()) .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); } diff --git a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java index c69f8ea..5c75314 100644 --- a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java +++ b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java @@ -4,12 +4,15 @@ import com.board.article.domain.Article; import com.board.article.loader.ArticleTestDataLoader; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @DataJpaTest @Import(ArticleTestDataLoader.class) @@ -19,13 +22,14 @@ class ArticleRepositoryTest { private ArticleRepository articleRepository; @Test - @DisplayName("멤버 아이디에 해당하는 게시글을 가져온다.") + @DisplayName("멤버 아이디에 해당하는 게시글을 가져온다.(offset-paging)") void findArticleByMemberId() { // given + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); Long memberId = 1L; // when - List
articles = articleRepository.findArticleByMemberId(memberId); + Page
articles = articleRepository.findArticleByMemberId(memberId, pageable); // then assertThat(articles).hasSize(2); diff --git a/src/test/java/com/board/article/service/ArticleServiceTest.java b/src/test/java/com/board/article/service/ArticleServiceTest.java index f2a4ef6..6025fd2 100644 --- a/src/test/java/com/board/article/service/ArticleServiceTest.java +++ b/src/test/java/com/board/article/service/ArticleServiceTest.java @@ -22,6 +22,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @ExtendWith(MockitoExtension.class) @SuppressWarnings("NonAsciiCharacters") @@ -96,10 +101,14 @@ void showArticle() { void showMemberArticles() { // given Long memberId = 1L; - given(articleRepository.findArticleByMemberId(memberId)).willReturn(articles); + int page = 0; + int size = 10; + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + Page
articlePage = new PageImpl<>(articles, pageable, articles.size()); + given(articleRepository.findArticleByMemberId(memberId, pageable)).willReturn(articlePage); // when - ArticleResponses responses = articleService.showMemberArticles(memberId); + ArticleResponses responses = articleService.showMemberArticles(memberId, page, size); // then assertThat(responses.articleResponses()).hasSize(1) @@ -137,51 +146,6 @@ void deleteArticle() { // then assertThat(response.title()).isEqualTo("제목1"); } - - @Test - @DisplayName("모든 게시글을 가져온다.") - void getAllArticles() { - // given - given(articleRepository.findAll()).willReturn(articles); - - // when - List
response = articleService.getAllArticles(); - - // then - assertThat(response).hasSize(1) - .extracting(Article::getTitle) - .containsExactly("제목1"); - } - - @Test - @DisplayName("특정 게시글을 가져온다.") - void getArticle() { - // given - Long articleId = 1L; - given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); - - // when - Article response = articleService.getArticle(articleId); - - // then - assertThat(response.getTitle()).isEqualTo("제목1"); - } - - @Test - @DisplayName("유저가 작성한 게시글을 가져온다.") - void getMemberArticles() { - // given - Long memberId = 1L; - given(articleRepository.findArticleByMemberId(memberId)).willReturn(articles); - - // when - List
response = articleService.getMemberArticles(memberId); - - // then - assertThat(response).hasSize(1) - .extracting(Article::getTitle) - .containsExactly("제목1"); - } } @Nested diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index ea15da8..7af0429 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -82,10 +82,14 @@ void showArticleComments() throws Exception { @DisplayName("회원이 작성한 댓글들을 조회한다.") void showMemberComments() throws Exception { // given - given(commentService.showMemberArticles(1L)).willReturn(responses); + int page = 0; + int size = 10; + given(commentService.showMemberComments(1L, page, size)).willReturn(responses); // when & then - mockMvc.perform(get("/members/me/comments")) + mockMvc.perform(get("/members/me/comments") + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) .andExpect(status().isOk()) .andExpect(jsonPath("$.commentResponses[0].content").value("댓글 내용")); } diff --git a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java index 3acc967..dda7dd5 100644 --- a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java +++ b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java @@ -10,6 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @DataJpaTest @Import(CommentTestDataLoader.class) @@ -34,13 +38,14 @@ void findAllByArticleId() { } @Test - @DisplayName("멤버 아이디에 해당하는 모든 댓글을 조회한다.") + @DisplayName("멤버 아이디에 해당하는 모든 댓글을 조회한다.(offset-paging)") void findAllByMemberId() { // given Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); // when - List comments = commentRepository.findAllByMemberId(memberId); + Page comments = commentRepository.findAllByMemberId(memberId, pageable); // then assertThat(comments).hasSize(4) diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index b030c4e..5d142c8 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -22,6 +22,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @ExtendWith(MockitoExtension.class) @SuppressWarnings("NonAsciiCharacters") @@ -85,10 +90,14 @@ void showArticleComments() { void showMemberArticles() { // given Long memberId = 1L; - given(commentRepository.findAllByMemberId(memberId)).willReturn(comments); + int page = 0; + int size = 10; + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + Page commentPage = new PageImpl<>(comments, pageable, comments.size()); + given(commentRepository.findAllByMemberId(memberId, pageable)).willReturn(commentPage); // when - CommentResponses responses = commentService.showMemberArticles(memberId); + CommentResponses responses = commentService.showMemberComments(memberId, page, size); // then assertThat(responses.commentResponses()).hasSize(1) @@ -127,38 +136,6 @@ void deleteComment() { assertThat(response.content()).isEqualTo("첫 번째 게시글 댓글 1"); } - @Test - @DisplayName("게시글에 해당하는 모든 댓글을 조회한다.") - void getArticleComments() { - // given - Long articleId = 1L; - given(commentRepository.findAllByArticleId(articleId)).willReturn(comments); - - // when - List responses = commentService.getArticleComments(articleId); - - // then - assertThat(responses).hasSize(1) - .extracting(Comment::getArticleId) - .containsExactly(1L); - } - - @Test - @DisplayName("유저가 작성한 모든 댓글을 조회한다.") - void getMemberComments() { - // given - Long memberId = 1L; - given(commentRepository.findAllByMemberId(memberId)).willReturn(comments); - - // when - List responses = commentService.getMemberComments(memberId); - - // then - assertThat(responses).hasSize(1) - .extracting(Comment::getArticleId) - .containsExactly(1L); - } - @Test @DisplayName("특정 댓글을 조회한다.") void getComment() { From 37b83138a42fc315a59e2f56a60f06de6828a5df Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Fri, 25 Apr 2025 23:25:34 +0900 Subject: [PATCH 38/52] =?UTF-8?q?feat:=20no-offset=20paging=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 1 + .../article/controller/ArticleController.java | 7 +++++-- .../article/repository/ArticleRepository.java | 3 +++ .../board/article/service/ArticleService.java | 14 +++++++++----- .../comment/controller/CommentController.java | 8 ++++++-- .../comment/repository/CommentRepository.java | 3 ++- .../board/comment/service/CommentService.java | 14 +++++++++----- .../controller/ArticleControllerTest.java | 8 ++++++-- .../repository/ArticleRepositoryTest.java | 17 +++++++++++++++++ .../article/service/ArticleServiceTest.java | 7 +++++-- .../controller/CommentControllerTest.java | 8 ++++++-- .../repository/CommentRepositoryTest.java | 8 +++++--- .../comment/service/CommentServiceTest.java | 8 ++++++-- 13 files changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/code_review.md b/docs/code_review.md index 48d069c..f67a345 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -66,3 +66,4 @@ public class GlobalException extends RuntimeException { ## 단점 * OFFSET 100000 같이 뒤 페이지로 갈수록 느려짐 → 인덱스가 있어도 느림 * 페이징 도중 데이터가 추가/삭제되면 순서가 뒤틀릴 수 있음 (같은 댓글이 여러 번 보이거나 사라짐) + diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index d686854..7db18ec 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -37,8 +37,11 @@ public ResponseEntity createArticle(@RequestBody ArticleRequest } @GetMapping("/articles") - public ResponseEntity showAllArticles() { - return ResponseEntity.ok(articleService.showAllArticles()); + public ResponseEntity showAllArticles( + @RequestParam Long lastId, + @RequestParam int size + ) { + return ResponseEntity.ok(articleService.showAllArticles(lastId, size)); } @GetMapping("/articles/{articleId}") diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java index b6a6e28..cfb953f 100644 --- a/src/main/java/com/board/article/repository/ArticleRepository.java +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -1,6 +1,7 @@ package com.board.article.repository; import com.board.article.domain.Article; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,4 +9,6 @@ public interface ArticleRepository extends JpaRepository { Page
findArticleByMemberId(Long memberId, Pageable pageable); + + List
findByIdLessThanOrderByIdDesc(Long lastId, Pageable pageable); } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 7fee465..47417ab 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -22,6 +22,9 @@ @Transactional public class ArticleService { + private static final String PAGE_SORT_DELIMITER = "id"; + private static final int NO_OFFSET_PAGING_PAGE = 0; + private final ArticleRepository articleRepository; public ArticleResponse createArticle(ArticleRequest request, Long memberId) { @@ -36,8 +39,8 @@ public ArticleResponse createArticle(ArticleRequest request, Long memberId) { ); } - public ArticleResponses showAllArticles() { - List articleResponses = getAllArticles().stream() + public ArticleResponses showAllArticles(Long lastId, int size) { + List articleResponses = getAllArticles(lastId, size).stream() .map(article -> new ArticleResponse( article.getId(), article.getMemberId(), @@ -106,8 +109,9 @@ private void validateAccessAboutArticle(Long memberId, Article article) { } @Transactional(readOnly = true) - public List
getAllArticles() { - return articleRepository.findAll(); + public List
getAllArticles(Long lastId, int size) { + Pageable articlePageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); + return articleRepository.findByIdLessThanOrderByIdDesc(lastId, articlePageable); } @Transactional(readOnly = true) @@ -118,7 +122,7 @@ public Article getArticle(Long articleId) { @Transactional(readOnly = true) public Page
getMemberArticles(Long memberId, int page, int size) { - Pageable articlePageable = PageRequest.of(page, size, Sort.by("id").descending()); // 최신 게시글 부터 + Pageable articlePageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); // 최신 게시글 부터 return articleRepository.findArticleByMemberId(memberId, articlePageable); } } diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 958fb92..0574b46 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -36,8 +36,12 @@ public ResponseEntity createComment(@RequestBody CommentRequest } @GetMapping("/articles/{articleId}/comments") - public ResponseEntity showArticleComments(@PathVariable Long articleId) { - return ResponseEntity.ok(commentService.showArticleComments(articleId)); + public ResponseEntity showArticleComments( + @PathVariable Long articleId, + @RequestParam Long lastId, + @RequestParam int size + ) { + return ResponseEntity.ok(commentService.showArticleComments(articleId, lastId, size)); } @GetMapping("members/me/comments") diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 034ec06..1f19b2a 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -10,6 +10,7 @@ @Repository public interface CommentRepository extends JpaRepository { - List findAllByArticleId(Long articleId); + List findByArticleIdAndIdLessThanOrderByIdDesc(Long articleId, Long lastId, Pageable pageable); + Page findAllByMemberId(Long memberId, Pageable pageable); } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 13e96dc..acc2b52 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -22,6 +22,9 @@ @Transactional public class CommentService { + private static final String PAGE_SORT_DELIMITER = "id"; + private static final int NO_OFFSET_PAGING_PAGE = 0; + private final CommentRepository commentRepository; public CommentResponse createComment(CommentRequest request, Long memberId, Long articleId) { @@ -35,8 +38,8 @@ public CommentResponse createComment(CommentRequest request, Long memberId, Long ); } - public CommentResponses showArticleComments(Long articleId) { - List commentResponses = getArticleComments(articleId).stream() + public CommentResponses showArticleComments(Long articleId, Long lastId, int size) { + List commentResponses = getArticleComments(articleId, lastId, size).stream() .map(comment -> new CommentResponse( comment.getMemberId(), comment.getArticleId(), @@ -90,13 +93,14 @@ private void validateAccessAboutComment(Long memberId, Comment comment) { } @Transactional(readOnly = true) - public List getArticleComments(Long articleId) { - return commentRepository.findAllByArticleId(articleId); + public List getArticleComments(Long articleId, Long lastId, int size) { + Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); + return commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, commentPageable); } @Transactional(readOnly = true) public Page getMemberComments(Long memberId, int page, int size) { - Pageable commentPageable = PageRequest.of(page, size, Sort.by("id").descending()); + Pageable commentPageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return commentRepository.findAllByMemberId(memberId, commentPageable); } diff --git a/src/test/java/com/board/article/controller/ArticleControllerTest.java b/src/test/java/com/board/article/controller/ArticleControllerTest.java index 62744ac..ffeb741 100644 --- a/src/test/java/com/board/article/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/article/controller/ArticleControllerTest.java @@ -72,10 +72,14 @@ void createArticle() throws Exception { @DisplayName("모든 게시글을 조회한다.") void showAllArticles() throws Exception { // given - given(articleService.showAllArticles()).willReturn(articleResponses); + Long lastId = 0L; + int size = 5; + given(articleService.showAllArticles(lastId, size)).willReturn(articleResponses); // when & then - mockMvc.perform(get("/articles")) + mockMvc.perform(get("/articles") + .param("lastId", String.valueOf(lastId)) + .param("size", String.valueOf(size))) .andExpect(status().isOk()) .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); } diff --git a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java index 5c75314..2806eff 100644 --- a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java +++ b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java @@ -4,6 +4,7 @@ import com.board.article.domain.Article; import com.board.article.loader.ArticleTestDataLoader; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -36,4 +37,20 @@ void findArticleByMemberId() { assertThat(articles).extracting(Article::getTitle) .containsExactly("신형만에 관하여", "신짱구"); } + + @Test + @DisplayName("lastId보다 작은 ID를 가진 게시글을 내림차순으로 가져온다. (no-offset-paging)") + void findByIdLessThanOrderByIdDesc() { + // given + Long lastId = 2L; + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + + // when + List
articles = articleRepository.findByIdLessThanOrderByIdDesc(lastId, pageable); + + // then + assertThat(articles).hasSize(1); + assertThat(articles).extracting(Article::getContent) + .containsExactly("아내는 신봉선"); + } } diff --git a/src/test/java/com/board/article/service/ArticleServiceTest.java b/src/test/java/com/board/article/service/ArticleServiceTest.java index 6025fd2..30ee85b 100644 --- a/src/test/java/com/board/article/service/ArticleServiceTest.java +++ b/src/test/java/com/board/article/service/ArticleServiceTest.java @@ -71,10 +71,13 @@ void createArticle() { @DisplayName("모든 게시글을 조회한다.") void showAllArticles() { // given - given(articleRepository.findAll()).willReturn(articles); + Long lastId = 0L; + int size = 10; + given(articleRepository.findByIdLessThanOrderByIdDesc(lastId, PageRequest.of(0, size, Sort.by("id").descending()))) + .willReturn(articles); // when - ArticleResponses responses = articleService.showAllArticles(); + ArticleResponses responses = articleService.showAllArticles(lastId, size); // then assertThat(responses.articleResponses()).hasSize(1) diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index 7af0429..db0ec17 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -70,10 +70,14 @@ void createComment() throws Exception { @DisplayName("게시글의 댓글들을 조회한다.") void showArticleComments() throws Exception { // given - given(commentService.showArticleComments(1L)).willReturn(responses); + Long lastId = 3L; + int size = 5; + given(commentService.showArticleComments(1L, lastId, size)).willReturn(responses); // when & then - mockMvc.perform(get("/articles/1/comments")) + mockMvc.perform(get("/articles/1/comments") + .param("lastId", String.valueOf(lastId)) + .param("size", String.valueOf(size))) .andExpect(status().isOk()) .andExpect(jsonPath("$.commentResponses[0].content").value("댓글 내용")); } diff --git a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java index dda7dd5..5f9ab8e 100644 --- a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java +++ b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java @@ -23,18 +23,20 @@ class CommentRepositoryTest { private CommentRepository commentRepository; @Test - @DisplayName("게시글 아이디에 해당하는 모든 댓글을 조회한다.") + @DisplayName("lastId보다 작은 ID를 가진 댓글을 내림차순으로 가져온다. (no-offset-paging)") void findAllByArticleId() { // given Long articleId = 1L; + Long lastId = 3L; + Pageable pageable = PageRequest.of(0, 5, Sort.by("id").descending()); // when - List comments = commentRepository.findAllByArticleId(articleId); + List comments = commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, pageable); // then assertThat(comments).hasSize(2) .extracting(Comment::getContent) - .containsExactly("첫 번째 게시글 댓글 1", "첫 번째 게시글 댓글 2"); + .containsExactly("첫 번째 게시글 댓글 2", "첫 번째 게시글 댓글 1"); } @Test diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index 5d142c8..a5ef5ce 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -74,10 +74,14 @@ void createComment() { void showArticleComments() { // given Long articleId = 1L; - given(commentRepository.findAllByArticleId(articleId)).willReturn(comments); + Long lastId = 0L; + int size = 5; + Pageable pageable = PageRequest.of(0, size, Sort.by("id").descending()); + given(commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, pageable)) + .willReturn(comments); // when - CommentResponses responses = commentService.showArticleComments(articleId); + CommentResponses responses = commentService.showArticleComments(articleId, lastId, size); // then assertThat(responses.commentResponses()).hasSize(1) From 34eaf88e2b77098843c019bd1d213c75d91df43e Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 00:23:05 +0900 Subject: [PATCH 39/52] =?UTF-8?q?fix:=20http=20method=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EB=B9=84=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EB=8F=84=20=EA=B2=8C=EC=8B=9C=EA=B8=80=EC=9D=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/global/config/WebMvcConfig.java | 4 ++-- .../global/interceptor/AuthInterceptor.java | 19 ++++++++++++++++--- .../global/resolver/AuthArgumentResolver.java | 10 ++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/board/global/config/WebMvcConfig.java b/src/main/java/com/board/global/config/WebMvcConfig.java index e117774..812a4d9 100644 --- a/src/main/java/com/board/global/config/WebMvcConfig.java +++ b/src/main/java/com/board/global/config/WebMvcConfig.java @@ -19,9 +19,9 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor) - .addPathPatterns("/api/articles/**") .addPathPatterns("/api/comments/**") - .addPathPatterns("/api/members/**"); + .addPathPatterns("/api/members/**") + .addPathPatterns("/api/articles/**"); } @Override diff --git a/src/main/java/com/board/global/interceptor/AuthInterceptor.java b/src/main/java/com/board/global/interceptor/AuthInterceptor.java index 1aa5100..88f24d6 100644 --- a/src/main/java/com/board/global/interceptor/AuthInterceptor.java +++ b/src/main/java/com/board/global/interceptor/AuthInterceptor.java @@ -4,6 +4,7 @@ import com.board.global.exception.GlobalException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Objects; import java.util.Optional; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -12,11 +13,23 @@ public class AuthInterceptor implements HandlerInterceptor { private static final String TOKEN_HEADER_NAME = "Authorization"; + private static final String TOKEN_START_NAME = "Bearer "; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) - .orElseThrow(() -> new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN)); - return tokenHeader.startsWith("Bearer "); + if(isRestrictHttpMethod(request)) { + String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) + .orElseThrow(() -> new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN)); + + return tokenHeader.startsWith(TOKEN_START_NAME); + } + + return true; + } + + private boolean isRestrictHttpMethod(HttpServletRequest request) { + return Objects.equals(request.getMethod(), "POST") || + Objects.equals(request.getMethod(), "DELETE") || + Objects.equals(request.getMethod(), "PATCH"); } } diff --git a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java index f19d71a..36f9634 100644 --- a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java +++ b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java @@ -1,5 +1,7 @@ package com.board.global.resolver; +import com.board.global.exception.GlobalErrorCode; +import com.board.global.exception.GlobalException; import com.board.global.resolver.annotation.Auth; import com.board.member.Infrastructure.auth.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; @@ -16,6 +18,7 @@ public class AuthArgumentResolver implements HandlerMethodArgumentResolver { private static final String TOKEN_HEADER_NAME = "Authorization"; + private static final String TOKEN_START_NAME = "BEARER"; private static final int TOKEN_BODY_DELIMITER = 7; private final JwtTokenProvider tokenProvider; @@ -30,7 +33,14 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); String tokenHeader = request.getHeader(TOKEN_HEADER_NAME); + validateToken(tokenHeader); String token = tokenHeader.substring(TOKEN_BODY_DELIMITER); return tokenProvider.extractMemberId(token); } + + private void validateToken(String token) { + if(token == null || !token.startsWith(TOKEN_START_NAME)) { + throw new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN); + } + } } From 3537df3c159271c3ab2b6c5733fee86a4c6f8ecb Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:19:49 +0900 Subject: [PATCH 40/52] =?UTF-8?q?refactor:=20interceptor=20allow=20uri=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/interceptor/AuthInterceptor.java | 19 +++++++++---------- .../global/resolver/AuthArgumentResolver.java | 2 +- .../{auth => }/JwtTokenProvider.java | 2 +- .../controller/auth/AuthControllerTest.java | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) rename src/main/java/com/board/member/Infrastructure/{auth => }/JwtTokenProvider.java (98%) diff --git a/src/main/java/com/board/global/interceptor/AuthInterceptor.java b/src/main/java/com/board/global/interceptor/AuthInterceptor.java index 88f24d6..7f1bdc9 100644 --- a/src/main/java/com/board/global/interceptor/AuthInterceptor.java +++ b/src/main/java/com/board/global/interceptor/AuthInterceptor.java @@ -14,22 +14,21 @@ public class AuthInterceptor implements HandlerInterceptor { private static final String TOKEN_HEADER_NAME = "Authorization"; private static final String TOKEN_START_NAME = "Bearer "; + private static final String ALLOW_HTTP_METHOD = "GET"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - if(isRestrictHttpMethod(request)) { - String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) - .orElseThrow(() -> new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN)); - - return tokenHeader.startsWith(TOKEN_START_NAME); + if(isAllowHttpMethod(request)) { + return true; } - return true; + String tokenHeader = Optional.ofNullable(request.getHeader(TOKEN_HEADER_NAME)) + .orElseThrow(() -> new GlobalException(GlobalErrorCode.NOT_FOUND_TOKEN)); + + return tokenHeader.startsWith(TOKEN_START_NAME); } - private boolean isRestrictHttpMethod(HttpServletRequest request) { - return Objects.equals(request.getMethod(), "POST") || - Objects.equals(request.getMethod(), "DELETE") || - Objects.equals(request.getMethod(), "PATCH"); + private boolean isAllowHttpMethod(HttpServletRequest request) { + return Objects.equals(request.getMethod(), ALLOW_HTTP_METHOD); } } diff --git a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java index 36f9634..bfa5afc 100644 --- a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java +++ b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java @@ -3,7 +3,7 @@ import com.board.global.exception.GlobalErrorCode; import com.board.global.exception.GlobalException; import com.board.global.resolver.annotation.Auth; -import com.board.member.Infrastructure.auth.JwtTokenProvider; +import com.board.member.Infrastructure.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; diff --git a/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java b/src/main/java/com/board/member/Infrastructure/JwtTokenProvider.java similarity index 98% rename from src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java rename to src/main/java/com/board/member/Infrastructure/JwtTokenProvider.java index 2feb080..5431eeb 100644 --- a/src/main/java/com/board/member/Infrastructure/auth/JwtTokenProvider.java +++ b/src/main/java/com/board/member/Infrastructure/JwtTokenProvider.java @@ -1,4 +1,4 @@ -package com.board.member.Infrastructure.auth; +package com.board.member.Infrastructure; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; diff --git a/src/test/java/com/board/member/controller/auth/AuthControllerTest.java b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java index 11e72ce..c6cc6f8 100644 --- a/src/test/java/com/board/member/controller/auth/AuthControllerTest.java +++ b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java @@ -7,7 +7,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.board.member.Infrastructure.auth.JwtTokenProvider; +import com.board.member.Infrastructure.JwtTokenProvider; import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; import com.board.member.controller.auth.dto.response.SignUpResponse; From 4b913db10d8ff6312c2495ad26e161065c7c379e Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:34:41 +0900 Subject: [PATCH 41/52] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=95=94=ED=98=B8=ED=99=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../com/board/global/config/AuthConfig.java | 15 ++++++++++++++ .../board/member/loader/MemberDataLoader.java | 6 ++++-- .../member/service/auth/AuthService.java | 20 +++++++++++++++---- 4 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/board/global/config/AuthConfig.java diff --git a/build.gradle b/build.gradle index f285959..fa4e7b7 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,9 @@ dependencies { // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + + // password encoder + implementation 'org.springframework.security:spring-security-crypto' } tasks.named('test') { diff --git a/src/main/java/com/board/global/config/AuthConfig.java b/src/main/java/com/board/global/config/AuthConfig.java new file mode 100644 index 0000000..2e6f669 --- /dev/null +++ b/src/main/java/com/board/global/config/AuthConfig.java @@ -0,0 +1,15 @@ +package com.board.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class AuthConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/board/member/loader/MemberDataLoader.java b/src/main/java/com/board/member/loader/MemberDataLoader.java index d2ebc77..d4ba012 100644 --- a/src/main/java/com/board/member/loader/MemberDataLoader.java +++ b/src/main/java/com/board/member/loader/MemberDataLoader.java @@ -4,6 +4,7 @@ import com.board.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @Component @@ -11,10 +12,11 @@ public class MemberDataLoader implements CommandLineRunner { private final MemberRepository repository; + private final PasswordEncoder passwordEncoder; @Override public void run(String... args) throws Exception { - repository.save(new Member("신짱구", "짱구", "aaa", "password123")); - repository.save(new Member("신짱아", "짱아", "sss", "password456")); + repository.save(new Member("신짱구", "짱구", "aaa", passwordEncoder.encode("password123"))); + repository.save(new Member("신짱아", "짱아", "sss", passwordEncoder.encode("password456"))); } } diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index 77086ee..a158db0 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -9,6 +9,7 @@ import com.board.member.exception.MemberException; import com.board.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,11 +20,16 @@ public class AuthService { private final MemberRepository memberRepository; private final TokenProvider tokenProvider; + private final PasswordEncoder passwordEncoder; public SignUpResponse signUp(SignUpRequest request) { checkDuplicateLoginId(request.loginId()); checkDuplicateNickName(request.memberNickName()); - Member member = new Member(request.memberName(), request.memberNickName(), request.loginId(), request.password()); + Member member = new Member( + request.memberName(), + request.memberNickName(), + request.loginId(), + passwordEncoder.encode(request.password())); memberRepository.save(member); return new SignUpResponse(member.getId(), tokenProvider.create(member.getId())); @@ -32,7 +38,7 @@ public SignUpResponse signUp(SignUpRequest request) { @Transactional(readOnly = true) public String login(LoginRequest request) { Member member = findMemberByLoginId(request.loginId()); - member.checkPassword(request.password()); + checkPassword(request.password(), member.getMemberPassword()); return tokenProvider.create(member.getId()); } @@ -42,14 +48,20 @@ private Member findMemberByLoginId(String loginId) { .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_MATCH_LOGIN_ID)); } + private void checkPassword(String requestPassword, String password) { + if (!passwordEncoder.matches(requestPassword, password)) { + throw new MemberException(MemberErrorCode.NOT_MATCH_PASSWORD); + } + } + private void checkDuplicateLoginId(String loginId) { - if(memberRepository.existsByMemberLoginId(loginId)) { + if (memberRepository.existsByMemberLoginId(loginId)) { throw new MemberException(MemberErrorCode.DUPLICATE_LOGIN_ID); } } private void checkDuplicateNickName(String memberNickName) { - if(memberRepository.existsByMemberNickName(memberNickName)) { + if (memberRepository.existsByMemberNickName(memberNickName)) { throw new MemberException(MemberErrorCode.DUPLICATE_NICKNAME); } } From 07cb5e0ae9dbb9087f685702be0228cc4356339f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:08:01 +0900 Subject: [PATCH 42/52] =?UTF-8?q?refactor:=20DDD=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/article/domain/Article.java | 9 +++++++++ .../com/board/article/service/ArticleService.java | 13 +++---------- src/main/java/com/board/comment/domain/Comment.java | 9 +++++++++ .../com/board/comment/service/CommentService.java | 11 ++--------- .../java/com/board/member/domain/member/Member.java | 8 -------- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index f77ddf3..9c63641 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -1,11 +1,14 @@ package com.board.article.domain; +import com.board.article.exception.ArticleErrorCode; +import com.board.article.exception.ArticleException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotBlank; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -44,4 +47,10 @@ public void update(String title, String content) { this.content = content; } } + + public void validateAccessAboutArticle(Long memberId) { + if(!Objects.equals(this.memberId, memberId)) { + throw new ArticleException(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE); + } + } } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 47417ab..79f0d07 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -8,7 +8,6 @@ import com.board.article.exception.ArticleException; import com.board.article.repository.ArticleRepository; import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -78,7 +77,7 @@ public ArticleResponses showMemberArticles(Long memberId, int page, int size) { public ArticleResponse updateArticle(ArticleRequest request, Long articleId, Long memberId) { Article article = getArticle(articleId); - validateAccessAboutArticle(memberId, article); + article.validateAccessAboutArticle(memberId); article.update(request.title(), request.content()); return new ArticleResponse( @@ -91,7 +90,7 @@ public ArticleResponse updateArticle(ArticleRequest request, Long articleId, Lon public ArticleResponse deleteArticle(Long articleId, Long memberId) { Article article = getArticle(articleId); - validateAccessAboutArticle(memberId, article); + article.validateAccessAboutArticle(memberId); articleRepository.delete(article); return new ArticleResponse( @@ -102,12 +101,6 @@ public ArticleResponse deleteArticle(Long articleId, Long memberId) { ); } - private void validateAccessAboutArticle(Long memberId, Article article) { - if(!Objects.equals(article.getMemberId(), memberId)) { - throw new ArticleException(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE); - } - } - @Transactional(readOnly = true) public List
getAllArticles(Long lastId, int size) { Pageable articlePageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); @@ -122,7 +115,7 @@ public Article getArticle(Long articleId) { @Transactional(readOnly = true) public Page
getMemberArticles(Long memberId, int page, int size) { - Pageable articlePageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); // 최신 게시글 부터 + Pageable articlePageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return articleRepository.findArticleByMemberId(memberId, articlePageable); } } diff --git a/src/main/java/com/board/comment/domain/Comment.java b/src/main/java/com/board/comment/domain/Comment.java index 3a15139..dc620d3 100644 --- a/src/main/java/com/board/comment/domain/Comment.java +++ b/src/main/java/com/board/comment/domain/Comment.java @@ -1,10 +1,13 @@ package com.board.comment.domain; +import com.board.comment.exception.CommentErrorCode; +import com.board.comment.exception.CommentException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -38,4 +41,10 @@ public void update(String content) { this.content = content; } } + + public void validateAccessAboutComment(Long memberId) { + if(!Objects.equals(this.memberId, memberId)) { + throw new CommentException(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT); + } + } } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index acc2b52..d3fc9ac 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -8,7 +8,6 @@ import com.board.comment.exception.CommentException; import com.board.comment.repository.CommentRepository; import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -64,7 +63,7 @@ public CommentResponses showMemberComments(Long memberId, int page, int size) { public CommentResponse updateComment(CommentRequest request, Long memberId, Long commentId) { Comment comment = getComment(commentId); - validateAccessAboutComment(memberId, comment); + comment.validateAccessAboutComment(memberId); comment.update(request.content()); return new CommentResponse( @@ -76,7 +75,7 @@ public CommentResponse updateComment(CommentRequest request, Long memberId, Long public CommentResponse deleteComment(Long memberId, Long commentId) { Comment comment = getComment(commentId); - validateAccessAboutComment(memberId, comment); + comment.validateAccessAboutComment(memberId); commentRepository.delete(comment); return new CommentResponse( @@ -86,12 +85,6 @@ public CommentResponse deleteComment(Long memberId, Long commentId) { ); } - private void validateAccessAboutComment(Long memberId, Comment comment) { - if (!Objects.equals(comment.getMemberId(), memberId)) { - throw new CommentException(CommentErrorCode.FORBIDDEN_ACCESS_COMMENT); - } - } - @Transactional(readOnly = true) public List getArticleComments(Long articleId, Long lastId, int size) { Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); diff --git a/src/main/java/com/board/member/domain/member/Member.java b/src/main/java/com/board/member/domain/member/Member.java index 88fa851..3f88b60 100644 --- a/src/main/java/com/board/member/domain/member/Member.java +++ b/src/main/java/com/board/member/domain/member/Member.java @@ -1,13 +1,11 @@ package com.board.member.domain.member; -import com.board.member.domain.member.exception.NotMatchPasswordException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotBlank; -import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -58,10 +56,4 @@ public void update(String memberName, String memberNickName, String memberLoginI this.memberPassword = memberPassword; } } - - public void checkPassword(String password) { - if(!Objects.equals(memberPassword, password)) { - throw new NotMatchPasswordException(); - } - } } From c4b57e49ccc2513ea2e93ee047da7d71ed537c8f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 02:43:35 +0900 Subject: [PATCH 43/52] =?UTF-8?q?refactor:=20Member=20=EC=97=AD=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/auth/AuthController.java | 9 ++--- .../auth/dto/response/SignUpResponse.java | 3 +- .../controller/member/MemberController.java | 32 +++++++++++++++-- .../member/service/auth/AuthService.java | 23 +++++------- .../member/service/member/MemberService.java | 36 +++++-------------- .../controller/auth/AuthControllerTest.java | 16 ++------- .../member/MemberControllerTest.java | 14 ++++---- .../member/service/auth/AuthServiceTest.java | 27 ++++++++------ .../service/member/MemberServiceTest.java | 15 ++++---- 9 files changed, 84 insertions(+), 91 deletions(-) diff --git a/src/main/java/com/board/member/controller/auth/AuthController.java b/src/main/java/com/board/member/controller/auth/AuthController.java index 5e9412b..4c73355 100644 --- a/src/main/java/com/board/member/controller/auth/AuthController.java +++ b/src/main/java/com/board/member/controller/auth/AuthController.java @@ -22,19 +22,20 @@ public class AuthController { @PostMapping("/signUp") public ResponseEntity signUp(@RequestBody SignUpRequest request) { - SignUpResponse response = authService.signUp(request); + Long memberId = authService.signUp(request.loginId(), request.memberName(), request.memberNickName(), + request.password()); URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() .path("/{id}") - .buildAndExpand(response.memberId()) + .buildAndExpand(memberId) .toUri(); - return ResponseEntity.created(location).body(response); + return ResponseEntity.created(location).body(new SignUpResponse(memberId)); } @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest request) { HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.set("Authorization", "Bearer " + authService.login(request)); + httpHeaders.set("Authorization", "Bearer " + authService.login(request.loginId(), request.password())); return ResponseEntity.status(HttpStatus.OK).body(httpHeaders); } } diff --git a/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java b/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java index 9fa5ad5..284facf 100644 --- a/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java +++ b/src/main/java/com/board/member/controller/auth/dto/response/SignUpResponse.java @@ -1,7 +1,6 @@ package com.board.member.controller.auth.dto.response; public record SignUpResponse( - Long memberId, - String token + Long memberId ) { } diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index 7479928..611d90c 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -3,6 +3,7 @@ import com.board.global.resolver.annotation.Auth; import com.board.member.controller.member.dto.reponse.MemberResponse; import com.board.member.controller.member.dto.request.MemberRequest; +import com.board.member.domain.member.Member; import com.board.member.service.member.MemberService; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Info; @@ -25,16 +26,41 @@ public class MemberController { @GetMapping("/members") public ResponseEntity showMember(@Auth Long memberId) { - return ResponseEntity.ok(memberService.showMember(memberId)); + Member member = memberService.getMember(memberId); + MemberResponse response = new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword() + ); + + return ResponseEntity.ok(response); } @PatchMapping("/members") public ResponseEntity updateMember(@Auth Long memberId, @RequestBody MemberRequest request) { - return ResponseEntity.ok(memberService.updateMember(memberId, request)); + Member member = memberService.updateMember(memberId, request.name(), request.nickName(), request.id(), + request.password()); + MemberResponse response = new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword() + ); + + return ResponseEntity.ok(response); } @DeleteMapping("/members") public ResponseEntity deleteMember(@Auth Long memberId) { - return ResponseEntity.ok(memberService.deleteMember(memberId)); + Member member = memberService.deleteMember(memberId); + MemberResponse response = new MemberResponse( + member.getMemberName(), + member.getMemberNickName(), + member.getMemberLoginId(), + member.getMemberPassword() + ); + + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/board/member/service/auth/AuthService.java b/src/main/java/com/board/member/service/auth/AuthService.java index a158db0..1af611a 100644 --- a/src/main/java/com/board/member/service/auth/AuthService.java +++ b/src/main/java/com/board/member/service/auth/AuthService.java @@ -1,8 +1,5 @@ package com.board.member.service.auth; -import com.board.member.controller.auth.dto.request.LoginRequest; -import com.board.member.controller.auth.dto.request.SignUpRequest; -import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.domain.auth.TokenProvider; import com.board.member.domain.member.Member; import com.board.member.exception.MemberErrorCode; @@ -22,23 +19,19 @@ public class AuthService { private final TokenProvider tokenProvider; private final PasswordEncoder passwordEncoder; - public SignUpResponse signUp(SignUpRequest request) { - checkDuplicateLoginId(request.loginId()); - checkDuplicateNickName(request.memberNickName()); - Member member = new Member( - request.memberName(), - request.memberNickName(), - request.loginId(), - passwordEncoder.encode(request.password())); + public Long signUp(String loginId, String name, String nickName, String password) { + checkDuplicateLoginId(loginId); + checkDuplicateNickName(nickName); + Member member = new Member(name, nickName, loginId, passwordEncoder.encode(password)); memberRepository.save(member); - return new SignUpResponse(member.getId(), tokenProvider.create(member.getId())); + return member.getId(); } @Transactional(readOnly = true) - public String login(LoginRequest request) { - Member member = findMemberByLoginId(request.loginId()); - checkPassword(request.password(), member.getMemberPassword()); + public String login(String loginId, String password) { + Member member = findMemberByLoginId(loginId); + checkPassword(password, member.getMemberPassword()); return tokenProvider.create(member.getId()); } diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index dbb52a6..83dd797 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -1,7 +1,5 @@ package com.board.member.service.member; -import com.board.member.controller.member.dto.reponse.MemberResponse; -import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.domain.member.Member; import com.board.member.exception.MemberErrorCode; import com.board.member.exception.MemberException; @@ -17,40 +15,22 @@ public class MemberService { private final MemberRepository memberRepository; - @Transactional(readOnly = true) - public MemberResponse showMember(Long memberId) { + public Member updateMember(Long memberId, String requestName, String requestNickName, String requestId, String requestPassword) { Member member = getMember(memberId); - return new MemberResponse( - member.getMemberName(), - member.getMemberNickName(), - member.getMemberLoginId(), - member.getMemberPassword() - ); - } + member.update(requestName, requestNickName, requestId, requestPassword); - public MemberResponse updateMember(Long memberId, MemberRequest request) { - Member member = getMember(memberId); - member.update(request.name(), request.nickName(), request.id(), request.password()); - return new MemberResponse( - member.getMemberName(), - member.getMemberNickName(), - member.getMemberLoginId(), - member.getMemberPassword() - ); + return member; } - public MemberResponse deleteMember(Long memberId) { + public Member deleteMember(Long memberId) { Member member = getMember(memberId); memberRepository.delete(member); - return new MemberResponse( - member.getMemberName(), - member.getMemberNickName(), - member.getMemberLoginId(), - member.getMemberPassword() - ); + + return member; } - private Member getMember(Long memberId) { + @Transactional(readOnly = true) + public Member getMember(Long memberId) { return memberRepository.findMemberById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER)); } diff --git a/src/test/java/com/board/member/controller/auth/AuthControllerTest.java b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java index c6cc6f8..33319c8 100644 --- a/src/test/java/com/board/member/controller/auth/AuthControllerTest.java +++ b/src/test/java/com/board/member/controller/auth/AuthControllerTest.java @@ -10,10 +10,8 @@ import com.board.member.Infrastructure.JwtTokenProvider; import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; -import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.service.auth.AuthService; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,19 +35,12 @@ class AuthControllerTest { @MockBean private AuthService authService; - private SignUpResponse signUpResponse; - - @BeforeEach - void set() { - signUpResponse = new SignUpResponse(1L, "token"); - } - @Test @DisplayName("회원가입을 진행한다.") void signUp() throws Exception { // given SignUpRequest request = new SignUpRequest("신짱구", "짱구", "aaa", "password123"); - given(authService.signUp(request)).willReturn(signUpResponse); + given(authService.signUp(request.loginId(), request.memberName(), request.memberNickName(), request.password())).willReturn(1L); given(jwtTokenProvider.create(1L)).willReturn("token"); // when & then @@ -58,8 +49,7 @@ void signUp() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(header().string("Location", "http://localhost/signUp/1")) - .andExpect(jsonPath("$.memberId").value(1L)) - .andExpect(jsonPath("$.token").value("token")); + .andExpect(jsonPath("$.memberId").value(1L)); } @Test @@ -68,7 +58,7 @@ void login() throws Exception { // given LoginRequest request = new LoginRequest("aaa", "111"); String token = "token"; - given(authService.login(request)).willReturn(token); + given(authService.login(request.loginId(), request.password())).willReturn(token); given(jwtTokenProvider.create(1L)).willReturn(token); // when & then diff --git a/src/test/java/com/board/member/controller/member/MemberControllerTest.java b/src/test/java/com/board/member/controller/member/MemberControllerTest.java index 7d0b2c6..706e4e8 100644 --- a/src/test/java/com/board/member/controller/member/MemberControllerTest.java +++ b/src/test/java/com/board/member/controller/member/MemberControllerTest.java @@ -9,8 +9,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.board.global.resolver.AuthArgumentResolver; -import com.board.member.controller.member.dto.reponse.MemberResponse; import com.board.member.controller.member.dto.request.MemberRequest; +import com.board.member.domain.member.Member; import com.board.member.service.member.MemberService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; @@ -37,11 +37,11 @@ class MemberControllerTest { @MockBean private AuthArgumentResolver authArgumentResolver; - private MemberResponse response; + private Member member; @BeforeEach void set() throws Exception { - response = new MemberResponse("신짱구", "짱구", "aaa", "password123"); + member = new Member("신짱구", "짱구", "aaa", "password123"); given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); } @@ -51,7 +51,7 @@ void set() throws Exception { void showMember() throws Exception { // given Long memberId = 1L; - given(memberService.showMember(memberId)).willReturn(response); + given(memberService.getMember(memberId)).willReturn(member); // when & then mockMvc.perform(get("/members") @@ -66,8 +66,8 @@ void showMember() throws Exception { void updateMember() throws Exception { // given MemberRequest request = new MemberRequest("신짱구", "짱구", "newId", "newPassword"); - MemberResponse updatedResponse = new MemberResponse("신짱구", "짱구", "newId", "newPassword"); - given(memberService.updateMember(1L, request)).willReturn(updatedResponse); + Member updatedResponse = new Member("신짱구", "짱구", "newId", "newPassword"); + given(memberService.updateMember(1L, request.name(), request.nickName(), request.id(), request.password())).willReturn(updatedResponse); // when & then mockMvc.perform(patch("/members") @@ -81,7 +81,7 @@ void updateMember() throws Exception { @DisplayName("유저 정보를 삭제한다.") void deleteMember() throws Exception { // given - given(memberService.deleteMember(1L)).willReturn(response); + given(memberService.deleteMember(1L)).willReturn(member); // when & then mockMvc.perform(delete("/members")) diff --git a/src/test/java/com/board/member/service/auth/AuthServiceTest.java b/src/test/java/com/board/member/service/auth/AuthServiceTest.java index 3a7e058..20afb74 100644 --- a/src/test/java/com/board/member/service/auth/AuthServiceTest.java +++ b/src/test/java/com/board/member/service/auth/AuthServiceTest.java @@ -7,7 +7,6 @@ import com.board.member.controller.auth.dto.request.LoginRequest; import com.board.member.controller.auth.dto.request.SignUpRequest; -import com.board.member.controller.auth.dto.response.SignUpResponse; import com.board.member.domain.auth.TokenProvider; import com.board.member.domain.member.Member; import com.board.member.exception.MemberErrorCode; @@ -22,6 +21,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @@ -34,6 +34,9 @@ class AuthServiceTest { @Mock private TokenProvider tokenProvider; + @Mock + private PasswordEncoder passwordEncoder; + @InjectMocks private AuthService authService; @@ -58,20 +61,19 @@ void signUpSuccess() { // given given(memberRepository.existsByMemberLoginId("gildong123")).willReturn(false); given(memberRepository.existsByMemberNickName("길동이")).willReturn(false); + given(passwordEncoder.encode("1234")).willReturn("encoded-password"); given(memberRepository.save(any(Member.class))).willAnswer(invocation -> { Member saved = invocation.getArgument(0); ReflectionTestUtils.setField(saved, "id", 1L); return saved; }); - given(tokenProvider.create(1L)).willReturn("fake-jwt-token"); // when - SignUpResponse response = authService.signUp(signUpRequest); + Long memberId = authService.signUp(signUpRequest.loginId(), signUpRequest.memberName(), signUpRequest.memberNickName(), + signUpRequest.password()); // then - assertThat(response) - .extracting(SignUpResponse::memberId, SignUpResponse::token) - .containsExactly(1L, "fake-jwt-token"); + assertThat(memberId).isEqualTo(1L); } @Test @@ -81,7 +83,8 @@ void signUpDuplicateLoginId() { given(memberRepository.existsByMemberLoginId("gildong123")).willReturn(true); // when & then - assertThatThrownBy(() -> authService.signUp(signUpRequest)) + assertThatThrownBy(() -> authService.signUp(signUpRequest.loginId(), signUpRequest.memberName(), signUpRequest.memberNickName(), + signUpRequest.password())) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.DUPLICATE_LOGIN_ID.message()); } @@ -94,7 +97,8 @@ void signUpDuplicateNickName() { given(memberRepository.existsByMemberNickName("길동이")).willReturn(true); // when & then - assertThatThrownBy(() -> authService.signUp(signUpRequest)) + assertThatThrownBy(() -> authService.signUp(signUpRequest.loginId(), signUpRequest.memberName(), signUpRequest.memberNickName(), + signUpRequest.password())) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.DUPLICATE_NICKNAME.message()); } @@ -109,10 +113,11 @@ void loginSuccess() { // given Long memberId = 1L; given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.of(member)); + given(passwordEncoder.matches("1234", "1234")).willReturn(true); given(tokenProvider.create(memberId)).willReturn("fake-jwt-token"); // when - String token = authService.login(loginRequest); + String token = authService.login(loginRequest.loginId(), loginRequest.password()); // then assertThat(token).isEqualTo("fake-jwt-token"); @@ -125,7 +130,7 @@ void loginInvalidLoginId() { given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> authService.login(loginRequest)) + assertThatThrownBy(() -> authService.login(loginRequest.loginId(), loginRequest.password())) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.NOT_MATCH_LOGIN_ID.message()); } @@ -138,7 +143,7 @@ void loginInvalidPassword() { given(memberRepository.findMemberByMemberLoginId("gildong123")).willReturn(Optional.of(wrongPasswordMember)); // when & then - assertThatThrownBy(() -> authService.login(loginRequest)) + assertThatThrownBy(() -> authService.login(loginRequest.loginId(), loginRequest.password())) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.NOT_MATCH_PASSWORD.message()); } diff --git a/src/test/java/com/board/member/service/member/MemberServiceTest.java b/src/test/java/com/board/member/service/member/MemberServiceTest.java index 1ca5e49..c4d4df0 100644 --- a/src/test/java/com/board/member/service/member/MemberServiceTest.java +++ b/src/test/java/com/board/member/service/member/MemberServiceTest.java @@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; -import com.board.member.controller.member.dto.reponse.MemberResponse; import com.board.member.controller.member.dto.request.MemberRequest; import com.board.member.domain.member.Member; import com.board.member.exception.MemberErrorCode; @@ -48,10 +47,10 @@ void showMember() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(member)); // when - MemberResponse response = memberService.showMember(memberId); + Member response = memberService.getMember(memberId); // then - assertThat(response.name()).isEqualTo("신짱구"); + assertThat(response.getMemberName()).isEqualTo("신짱구"); } @Test @@ -68,10 +67,10 @@ void updateMember() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.ofNullable(member)); // when - MemberResponse response = memberService.updateMember(memberId, request); + Member response = memberService.updateMember(memberId, request.name(), request.nickName(), request.id(), request.password()); // then - assertThat(response.name()).isEqualTo("홍길동"); + assertThat(response.getMemberName()).isEqualTo("홍길동"); } @Test @@ -82,10 +81,10 @@ void deleteMember() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(member)); // when - MemberResponse response = memberService.deleteMember(memberId); + Member response = memberService.deleteMember(memberId); // then - assertThat(response.name()).isEqualTo("신짱구"); + assertThat(response.getMemberName()).isEqualTo("신짱구"); } } @@ -100,7 +99,7 @@ void showMemberException() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> memberService.showMember(memberId)) + assertThatThrownBy(() -> memberService.getMember(memberId)) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.NOT_FOUND_MEMBER.message()); } From 78137ccb12a6ee2f5885d2eef8a7798f33b6a7b4 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 03:01:19 +0900 Subject: [PATCH 44/52] =?UTF-8?q?refactor:=20Comment=20=EC=97=AD=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 55 ++++++++++++++++--- .../board/comment/service/CommentService.java | 52 +++--------------- .../controller/CommentControllerTest.java | 24 +++++--- .../comment/service/CommentServiceTest.java | 26 ++++----- 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 0574b46..09b04c9 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -3,10 +3,13 @@ import com.board.comment.controller.dto.reponse.CommentResponse; import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.domain.Comment; import com.board.comment.service.CommentService; import com.board.global.resolver.annotation.Auth; import java.net.URI; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -25,8 +28,15 @@ public class CommentController { private final CommentService commentService; @PostMapping("/articles/{articleId}/comments") - public ResponseEntity createComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long articleId) { - CommentResponse response = commentService.createComment(request, memberId, articleId); + public ResponseEntity createComment(@RequestBody CommentRequest request, @Auth Long memberId, + @PathVariable Long articleId) { + Comment comment = commentService.createComment(request.content(), memberId, articleId); + CommentResponse response = new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() .path("/{id}") .buildAndExpand(response.articleId()) @@ -41,7 +51,15 @@ public ResponseEntity showArticleComments( @RequestParam Long lastId, @RequestParam int size ) { - return ResponseEntity.ok(commentService.showArticleComments(articleId, lastId, size)); + List comments = commentService.getArticleComments(articleId, lastId, size); + List responses = comments.stream() + .map(comment -> new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + )).toList(); + + return ResponseEntity.ok(new CommentResponses(responses)); } @GetMapping("members/me/comments") @@ -50,16 +68,39 @@ public ResponseEntity showMemberComments( @RequestParam int page, @RequestParam int size ) { - return ResponseEntity.ok(commentService.showMemberComments(memberId, page, size)); + Page comments = commentService.getMemberComments(memberId, page, size); + List responses = comments.stream() + .map(comment -> new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + )).toList(); + + return ResponseEntity.ok(new CommentResponses(responses)); } @PatchMapping("/comments/{commentId}") - public ResponseEntity updateComment(@RequestBody CommentRequest request, @Auth Long memberId, @PathVariable Long commentId) { - return ResponseEntity.ok(commentService.updateComment(request, memberId, commentId)); + public ResponseEntity updateComment(@RequestBody CommentRequest request, @Auth Long memberId, + @PathVariable Long commentId) { + Comment comment = commentService.updateComment(request, memberId, commentId); + CommentResponse response = new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + + return ResponseEntity.ok(response); } @DeleteMapping("/comments/{commentId}") public ResponseEntity deleteComment(@Auth Long memberId, @PathVariable Long commentId) { - return ResponseEntity.ok(commentService.deleteComment(memberId, commentId)); + Comment comment = commentService.deleteComment(memberId, commentId); + CommentResponse response = new CommentResponse( + comment.getMemberId(), + comment.getArticleId(), + comment.getContent() + ); + + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index d3fc9ac..7362f89 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -1,7 +1,5 @@ package com.board.comment.service; -import com.board.comment.controller.dto.reponse.CommentResponse; -import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; @@ -26,63 +24,27 @@ public class CommentService { private final CommentRepository commentRepository; - public CommentResponse createComment(CommentRequest request, Long memberId, Long articleId) { - Comment comment = new Comment(memberId, articleId, request.content()); + public Comment createComment(String content, Long memberId, Long articleId) { + Comment comment = new Comment(memberId, articleId, content); commentRepository.save(comment); - return new CommentResponse( - comment.getMemberId(), - comment.getArticleId(), - comment.getContent() - ); + return comment; } - public CommentResponses showArticleComments(Long articleId, Long lastId, int size) { - List commentResponses = getArticleComments(articleId, lastId, size).stream() - .map(comment -> new CommentResponse( - comment.getMemberId(), - comment.getArticleId(), - comment.getContent() - )) - .toList(); - - return new CommentResponses(commentResponses); - } - - public CommentResponses showMemberComments(Long memberId, int page, int size) { - List commentResponses = getMemberComments(memberId, page, size).stream() - .map(comment -> new CommentResponse( - comment.getMemberId(), - comment.getArticleId(), - comment.getContent() - )) - .toList(); - - return new CommentResponses(commentResponses); - } - - public CommentResponse updateComment(CommentRequest request, Long memberId, Long commentId) { + public Comment updateComment(CommentRequest request, Long memberId, Long commentId) { Comment comment = getComment(commentId); comment.validateAccessAboutComment(memberId); comment.update(request.content()); - return new CommentResponse( - comment.getMemberId(), - comment.getArticleId(), - comment.getContent() - ); + return comment; } - public CommentResponse deleteComment(Long memberId, Long commentId) { + public Comment deleteComment(Long memberId, Long commentId) { Comment comment = getComment(commentId); comment.validateAccessAboutComment(memberId); commentRepository.delete(comment); - return new CommentResponse( - comment.getMemberId(), - comment.getArticleId(), - comment.getContent() - ); + return comment; } @Transactional(readOnly = true) diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index db0ec17..2b26026 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -9,9 +9,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.board.comment.controller.dto.reponse.CommentResponse; -import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; +import com.board.comment.domain.Comment; import com.board.comment.service.CommentService; import com.board.global.resolver.AuthArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,6 +21,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -40,13 +44,13 @@ class CommentControllerTest { @MockBean private AuthArgumentResolver authArgumentResolver; - private CommentResponse response; - private CommentResponses responses; + private Comment response; + private List responses; @BeforeEach void setUp() throws Exception { - response = new CommentResponse(1L, 1L, "댓글 내용"); - responses = new CommentResponses(List.of(response)); + response = new Comment(1L, 1L, "댓글 내용"); + responses = List.of(response); given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); } @@ -72,7 +76,7 @@ void showArticleComments() throws Exception { // given Long lastId = 3L; int size = 5; - given(commentService.showArticleComments(1L, lastId, size)).willReturn(responses); + given(commentService.getArticleComments(1L, lastId, size)).willReturn(responses); // when & then mockMvc.perform(get("/articles/1/comments") @@ -88,7 +92,9 @@ void showMemberComments() throws Exception { // given int page = 0; int size = 10; - given(commentService.showMemberComments(1L, page, size)).willReturn(responses); + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + Page commentPage = new PageImpl<>(responses, pageable, responses.size()); + given(commentService.getMemberComments(1L, page, size)).willReturn(commentPage); // when & then mockMvc.perform(get("/members/me/comments") @@ -103,7 +109,7 @@ void showMemberComments() throws Exception { void updateComment() throws Exception { // given CommentRequest request = new CommentRequest("수정된 댓글"); - CommentResponse updatedResponse = new CommentResponse(1L, 1L,"수정된 댓글"); + Comment updatedResponse = new Comment(1L, 1L,"수정된 댓글"); given(commentService.updateComment(any(), any(), any())).willReturn(updatedResponse); // when & then diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index a5ef5ce..780bd03 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -5,8 +5,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import com.board.comment.controller.dto.reponse.CommentResponse; -import com.board.comment.controller.dto.reponse.CommentResponses; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; @@ -61,11 +59,11 @@ void createComment() { given(commentRepository.save(any(Comment.class))).willReturn(comment); // when - CommentResponse response = commentService.createComment(request, memberId, articleId); + Comment response = commentService.createComment(request.content(), memberId, articleId); // then assertThat(response) - .extracting(CommentResponse::content, CommentResponse::memberId, CommentResponse::articleId) + .extracting(Comment::getContent, Comment::getMemberId, Comment::getArticleId) .containsExactly("첫 번째 게시글 댓글1", memberId, articleId); } @@ -81,11 +79,11 @@ void showArticleComments() { .willReturn(comments); // when - CommentResponses responses = commentService.showArticleComments(articleId, lastId, size); + List responses = commentService.getArticleComments(articleId, lastId, size); // then - assertThat(responses.commentResponses()).hasSize(1) - .extracting(CommentResponse::content) + assertThat(responses).hasSize(1) + .extracting(Comment::getContent) .containsExactly("첫 번째 게시글 댓글 1"); } @@ -101,11 +99,11 @@ void showMemberArticles() { given(commentRepository.findAllByMemberId(memberId, pageable)).willReturn(commentPage); // when - CommentResponses responses = commentService.showMemberComments(memberId, page, size); + Page responses = commentService.getMemberComments(memberId, page, size); // then - assertThat(responses.commentResponses()).hasSize(1) - .extracting(CommentResponse::content) + assertThat(responses).hasSize(1) + .extracting(Comment::getContent) .containsExactly("첫 번째 게시글 댓글 1"); } @@ -119,10 +117,10 @@ void updateComment() { given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); // when - CommentResponse response = commentService.updateComment(request, memberId, commentId); + Comment response = commentService.updateComment(request, memberId, commentId); // then - assertThat(response.content()).isEqualTo("수정된 게시글"); + assertThat(response.getContent()).isEqualTo("수정된 게시글"); } @Test @@ -134,10 +132,10 @@ void deleteComment() { given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); // when - CommentResponse response = commentService.deleteComment(memberId, commentId); + Comment response = commentService.deleteComment(memberId, commentId); // then - assertThat(response.content()).isEqualTo("첫 번째 게시글 댓글 1"); + assertThat(response.getContent()).isEqualTo("첫 번째 게시글 댓글 1"); } @Test From b9430f7b3b3810ef8e11f21a689e45a4904e8c96 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 26 Apr 2025 03:20:00 +0900 Subject: [PATCH 45/52] =?UTF-8?q?refactor:=20Article=20=EC=97=AD=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 70 ++++++++++++++++-- .../board/article/service/ArticleService.java | 71 +++---------------- .../controller/ArticleControllerTest.java | 32 +++++---- .../article/service/ArticleServiceTest.java | 32 ++++----- 4 files changed, 106 insertions(+), 99 deletions(-) diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index 7db18ec..cb154ea 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -3,10 +3,13 @@ import com.board.article.controller.dto.request.ArticleRequest; import com.board.article.controller.dto.response.ArticleResponse; import com.board.article.controller.dto.response.ArticleResponses; +import com.board.article.domain.Article; import com.board.article.service.ArticleService; import com.board.global.resolver.annotation.Auth; import java.net.URI; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -26,7 +29,14 @@ public class ArticleController { @PostMapping("/articles") public ResponseEntity createArticle(@RequestBody ArticleRequest request, @Auth Long memberId) { - ArticleResponse response = articleService.createArticle(request, memberId); + Article article = articleService.createArticle(memberId, request.title(), request.content()); + ArticleResponse response = new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() .path("/{id}") .buildAndExpand(response.articleId()) @@ -41,12 +51,29 @@ public ResponseEntity showAllArticles( @RequestParam Long lastId, @RequestParam int size ) { - return ResponseEntity.ok(articleService.showAllArticles(lastId, size)); + List
articles = articleService.getAllArticles(lastId, size); + List responses = articles.stream() + .map(article -> new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )).toList(); + + return ResponseEntity.ok(new ArticleResponses(responses)); } @GetMapping("/articles/{articleId}") public ResponseEntity showArticle(@PathVariable Long articleId) { - return ResponseEntity.ok(articleService.showArticle(articleId)); + Article article = articleService.getArticle(articleId); + ArticleResponse response = new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + + return ResponseEntity.ok(response); } @GetMapping("/members/me/articles") @@ -55,16 +82,45 @@ public ResponseEntity showMemberArticles( @RequestParam int page, @RequestParam int size ) { - return ResponseEntity.ok(articleService.showMemberArticles(memberId, page, size)); + Page
articles = articleService.getMemberArticles(memberId, page, size); + List responses = articles.stream() + .map(article -> new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )).toList(); + + return ResponseEntity.ok(new ArticleResponses(responses)); } @PatchMapping("/articles/{articleId}") - public ResponseEntity updateArticle(@RequestBody ArticleRequest request, @PathVariable Long articleId, @Auth Long memberId) { - return ResponseEntity.ok(articleService.updateArticle(request, articleId, memberId)); + public ResponseEntity updateArticle( + @RequestBody ArticleRequest request, + @PathVariable Long articleId, + @Auth Long memberId + ) { + Article article = articleService.updateArticle(articleId, memberId, request.title(), request.content()); + ArticleResponse response = new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + + return ResponseEntity.ok(response); } @DeleteMapping("/articles/{articleId}") public ResponseEntity deleteArticle(@PathVariable Long articleId, @Auth Long memberId) { - return ResponseEntity.ok(articleService.deleteArticle(articleId, memberId)); + Article article = articleService.deleteArticle(articleId, memberId); + ArticleResponse response = new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + ); + + return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index 79f0d07..e05fa4d 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -1,8 +1,5 @@ package com.board.article.service; -import com.board.article.controller.dto.request.ArticleRequest; -import com.board.article.controller.dto.response.ArticleResponse; -import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; @@ -26,79 +23,27 @@ public class ArticleService { private final ArticleRepository articleRepository; - public ArticleResponse createArticle(ArticleRequest request, Long memberId) { - Article article = new Article(memberId, request.title(), request.content()); + public Article createArticle(Long memberId, String title, String content) { + Article article = new Article(memberId, title, content); articleRepository.save(article); - return new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - ); + return article; } - public ArticleResponses showAllArticles(Long lastId, int size) { - List articleResponses = getAllArticles(lastId, size).stream() - .map(article -> new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - )) - .toList(); - - return new ArticleResponses(articleResponses); - } - - public ArticleResponse showArticle(Long articleId) { - Article article = getArticle(articleId); - - return new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - ); - } - - public ArticleResponses showMemberArticles(Long memberId, int page, int size) { - List articleResponses = getMemberArticles(memberId, page, size).stream() - .map(article -> new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - )) - .toList(); - - return new ArticleResponses(articleResponses); - } - - public ArticleResponse updateArticle(ArticleRequest request, Long articleId, Long memberId) { + public Article updateArticle(Long articleId, Long memberId, String title, String content) { Article article = getArticle(articleId); article.validateAccessAboutArticle(memberId); - article.update(request.title(), request.content()); + article.update(title, content); - return new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - ); + return article; } - public ArticleResponse deleteArticle(Long articleId, Long memberId) { + public Article deleteArticle(Long articleId, Long memberId) { Article article = getArticle(articleId); article.validateAccessAboutArticle(memberId); articleRepository.delete(article); - return new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - ); + return article; } @Transactional(readOnly = true) diff --git a/src/test/java/com/board/article/controller/ArticleControllerTest.java b/src/test/java/com/board/article/controller/ArticleControllerTest.java index ffeb741..384569e 100644 --- a/src/test/java/com/board/article/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/article/controller/ArticleControllerTest.java @@ -11,8 +11,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.board.article.controller.dto.request.ArticleRequest; -import com.board.article.controller.dto.response.ArticleResponse; -import com.board.article.controller.dto.response.ArticleResponses; +import com.board.article.domain.Article; import com.board.article.service.ArticleService; import com.board.global.resolver.AuthArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; @@ -23,7 +22,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(ArticleController.class) @@ -41,15 +46,16 @@ class ArticleControllerTest { @MockBean private AuthArgumentResolver authArgumentResolver; - private ArticleResponse articleResponse; - private ArticleResponses articleResponses; + private Article articleResponse; + private List
articleResponses; @BeforeEach void setUp() throws Exception { given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); - articleResponse = new ArticleResponse(1L, 1L, "제목", "내용"); - articleResponses = new ArticleResponses(List.of(articleResponse)); + articleResponse = new Article(1L, "제목", "내용"); + ReflectionTestUtils.setField(articleResponse, "id", 1L); + articleResponses = List.of(articleResponse); } @Test @@ -57,7 +63,7 @@ void setUp() throws Exception { void createArticle() throws Exception { // given ArticleRequest request = new ArticleRequest("제목", "내용"); - given(articleService.createArticle(any(), any())).willReturn(articleResponse); + given(articleService.createArticle(any(), any(), any())).willReturn(articleResponse); // when & then mockMvc.perform(post("/articles") @@ -74,7 +80,7 @@ void showAllArticles() throws Exception { // given Long lastId = 0L; int size = 5; - given(articleService.showAllArticles(lastId, size)).willReturn(articleResponses); + given(articleService.getAllArticles(lastId, size)).willReturn(articleResponses); // when & then mockMvc.perform(get("/articles") @@ -88,7 +94,7 @@ void showAllArticles() throws Exception { @DisplayName("게시글 하나를 조회한다.") void showArticle() throws Exception { // given - given(articleService.showArticle(1L)).willReturn(articleResponse); + given(articleService.getArticle(1L)).willReturn(articleResponse); // when & then mockMvc.perform(get("/articles/1")) @@ -103,7 +109,9 @@ void showMemberArticles() throws Exception { Long memberId = 1L; int page = 0; int size = 10; - given(articleService.showMemberArticles(memberId, page, size)).willReturn(articleResponses); + Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); + Page
articlePage = new PageImpl<>(articleResponses, pageable, articleResponses.size()); + given(articleService.getMemberArticles(memberId, page, size)).willReturn(articlePage); // when & then mockMvc.perform(get("/members/me/articles") @@ -118,8 +126,8 @@ void showMemberArticles() throws Exception { void updateArticle() throws Exception { // given ArticleRequest request = new ArticleRequest("수정된 제목", "수정된 내용"); - ArticleResponse response = new ArticleResponse(1L, 1L, "수정된 제목", "수정된 내용"); - given(articleService.updateArticle(any(), any(), any())).willReturn(response); + Article response = new Article(1L, "수정된 제목", "수정된 내용"); + given(articleService.updateArticle(any(), any(), any(), any())).willReturn(response); // when & then mockMvc.perform(patch("/articles/1") diff --git a/src/test/java/com/board/article/service/ArticleServiceTest.java b/src/test/java/com/board/article/service/ArticleServiceTest.java index 30ee85b..bc116ce 100644 --- a/src/test/java/com/board/article/service/ArticleServiceTest.java +++ b/src/test/java/com/board/article/service/ArticleServiceTest.java @@ -6,8 +6,6 @@ import static org.mockito.BDDMockito.given; import com.board.article.controller.dto.request.ArticleRequest; -import com.board.article.controller.dto.response.ArticleResponse; -import com.board.article.controller.dto.response.ArticleResponses; import com.board.article.domain.Article; import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; @@ -59,11 +57,11 @@ void createArticle() { given(articleRepository.save(any(Article.class))).willReturn(article); // when - ArticleResponse response = articleService.createArticle(request, memberId); + Article response = articleService.createArticle(memberId, request.title(), request.content()); // then assertThat(response) - .extracting(ArticleResponse::title, ArticleResponse::content, ArticleResponse::memberId) + .extracting(Article::getTitle, Article::getContent, Article::getMemberId) .containsExactly("제목1", "내용1", memberId); } @@ -77,11 +75,11 @@ void showAllArticles() { .willReturn(articles); // when - ArticleResponses responses = articleService.showAllArticles(lastId, size); + List
responses = articleService.getAllArticles(lastId, size); // then - assertThat(responses.articleResponses()).hasSize(1) - .extracting(ArticleResponse::title) + assertThat(responses).hasSize(1) + .extracting(Article::getTitle) .containsExactly("제목1"); } @@ -93,10 +91,10 @@ void showArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); // when - ArticleResponse response = articleService.showArticle(articleId); + Article response = articleService.getArticle(articleId); // then - assertThat(response.title()).isEqualTo("제목1"); + assertThat(response.getTitle()).isEqualTo("제목1"); } @Test @@ -111,11 +109,11 @@ void showMemberArticles() { given(articleRepository.findArticleByMemberId(memberId, pageable)).willReturn(articlePage); // when - ArticleResponses responses = articleService.showMemberArticles(memberId, page, size); + Page
responses = articleService.getMemberArticles(memberId, page, size); // then - assertThat(responses.articleResponses()).hasSize(1) - .extracting(ArticleResponse::title) + assertThat(responses).hasSize(1) + .extracting(Article::getTitle) .containsExactly("제목1"); } @@ -129,10 +127,10 @@ void updateArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); // when - ArticleResponse response = articleService.updateArticle(request, articleId, memberId); + Article response = articleService.updateArticle(articleId, memberId, request.title(), request.content()); // then - assertThat(response.title()).isEqualTo("수정된 제목"); + assertThat(response.getTitle()).isEqualTo("수정된 제목"); } @Test @@ -144,10 +142,10 @@ void deleteArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); // when - ArticleResponse response = articleService.deleteArticle(articleId, memberId); + Article response = articleService.deleteArticle(articleId, memberId); // then - assertThat(response.title()).isEqualTo("제목1"); + assertThat(response.getTitle()).isEqualTo("제목1"); } } @@ -177,7 +175,7 @@ void forbiddenUpdateArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); // when & then - assertThatThrownBy(() -> articleService.updateArticle(request, articleId, memberId)) + assertThatThrownBy(() -> articleService.updateArticle(articleId, memberId, request.title(), request.content())) .isInstanceOf(ArticleException.class) .hasMessageContaining(ArticleErrorCode.FORBIDDEN_ACCESS_ARTICLE.message()); } From fcc6f761f02c19cf4084668c93a1433faa9cc79b Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:24:08 +0900 Subject: [PATCH 46/52] =?UTF-8?q?feat:=20Article=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C,=20Comment=20=EB=8F=84=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EB=90=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 24 +++++++++++++++++++ .../com/board/article/domain/Article.java | 9 +++++++ .../comment/controller/CommentController.java | 10 ++++---- .../com/board/comment/domain/Comment.java | 14 +++++++---- .../comment/loader/CommentDataLoader.java | 13 ++++++---- .../board/comment/service/CommentService.java | 6 ++++- .../global/resolver/AuthArgumentResolver.java | 2 +- .../controller/CommentControllerTest.java | 10 +++++--- .../comment/loader/CommentTestDataLoader.java | 16 +++++++++---- .../comment/service/CommentServiceTest.java | 16 ++++++++++--- 10 files changed, 94 insertions(+), 26 deletions(-) diff --git a/docs/code_review.md b/docs/code_review.md index f67a345..1709e07 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -67,3 +67,27 @@ public class GlobalException extends RuntimeException { * OFFSET 100000 같이 뒤 페이지로 갈수록 느려짐 → 인덱스가 있어도 느림 * 페이징 도중 데이터가 추가/삭제되면 순서가 뒤틀릴 수 있음 (같은 댓글이 여러 번 보이거나 사라짐) + +# ```@ManyToOne``` vs 간접참조 + +--- + +## @ManyToOne + +* 객체를 통째로 다루기 떄문에 저장이 간편하다. +* 그렇기에 객체에 직접적으로 바로 접근이 가능하다. +* 하지만 연관관계 이슈 때문에 객체 조회 시 N+1 문제가 발생할 수 있다. +* 따라서 관계에 따라 접근 전략을 모색해야 한다. + +## 간접참조 + +* id 값만 저장하면 된다.(단순하고 가볍게 관리 가능) +* 따라서, id 값을 통해 조회를 또 다시 해야한다. +* 하지만 이는 객체지향과는 조금 거리가 멀다.(객체가 아닌, 단순 id 값만 가지고 있기 때문이다.) + +# ORM의 탄생 배경 + +--- + +* ORM 은 DB를 테이블처럼 다루지 말고 객체 처럼 다루자는 목표로 등장. +* diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index 9c63641..dee4e2d 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -2,12 +2,17 @@ import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; +import com.board.comment.domain.Comment; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; @@ -33,6 +38,10 @@ public class Article { @NotBlank private String content; + @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE) + private List comments = new ArrayList<>(); + + public Article(Long memberId, String title, String content) { this.memberId = memberId; this.title = title; diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 09b04c9..878df25 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -33,7 +33,7 @@ public ResponseEntity createComment(@RequestBody CommentRequest Comment comment = commentService.createComment(request.content(), memberId, articleId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticleId(), + comment.getArticle().getId(), comment.getContent() ); @@ -55,7 +55,7 @@ public ResponseEntity showArticleComments( List responses = comments.stream() .map(comment -> new CommentResponse( comment.getMemberId(), - comment.getArticleId(), + comment.getArticle().getId(), comment.getContent() )).toList(); @@ -72,7 +72,7 @@ public ResponseEntity showMemberComments( List responses = comments.stream() .map(comment -> new CommentResponse( comment.getMemberId(), - comment.getArticleId(), + comment.getArticle().getId(), comment.getContent() )).toList(); @@ -85,7 +85,7 @@ public ResponseEntity updateComment(@RequestBody CommentRequest Comment comment = commentService.updateComment(request, memberId, commentId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticleId(), + comment.getArticle().getId(), comment.getContent() ); @@ -97,7 +97,7 @@ public ResponseEntity deleteComment(@Auth Long memberId, @PathV Comment comment = commentService.deleteComment(memberId, commentId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticleId(), + comment.getArticle().getId(), comment.getContent() ); diff --git a/src/main/java/com/board/comment/domain/Comment.java b/src/main/java/com/board/comment/domain/Comment.java index dc620d3..168da92 100644 --- a/src/main/java/com/board/comment/domain/Comment.java +++ b/src/main/java/com/board/comment/domain/Comment.java @@ -1,12 +1,16 @@ package com.board.comment.domain; +import com.board.article.domain.Article; import com.board.comment.exception.CommentErrorCode; import com.board.comment.exception.CommentException; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; @@ -24,15 +28,17 @@ public class Comment { @Column private Long memberId; - @Column - private Long articleId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "articleId", nullable = false) + private Article article; + @Column(nullable = false) private String content; - public Comment(Long memberId, Long articleId, String content) { + public Comment(Long memberId, Article article, String content) { this.memberId = memberId; - this.articleId = articleId; + this.article = article; this.content = content; } diff --git a/src/main/java/com/board/comment/loader/CommentDataLoader.java b/src/main/java/com/board/comment/loader/CommentDataLoader.java index ffdc7f0..98913ab 100644 --- a/src/main/java/com/board/comment/loader/CommentDataLoader.java +++ b/src/main/java/com/board/comment/loader/CommentDataLoader.java @@ -1,5 +1,7 @@ package com.board.comment.loader; +import com.board.article.domain.Article; +import com.board.article.repository.ArticleRepository; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; import lombok.RequiredArgsConstructor; @@ -11,12 +13,15 @@ public class CommentDataLoader implements CommandLineRunner { private final CommentRepository repository; + private final ArticleRepository articleRepository; @Override public void run(String... args) throws Exception { - repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); - repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); - repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); - repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); + Article article1 = articleRepository.save(new Article(1L, "제목", "내용")); + Article article2 = articleRepository.save(new Article(1L, "제제목", "내내용")); + repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 2")); } } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index 7362f89..afd32de 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -1,5 +1,7 @@ package com.board.comment.service; +import com.board.article.domain.Article; +import com.board.article.service.ArticleService; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; @@ -23,9 +25,11 @@ public class CommentService { private static final int NO_OFFSET_PAGING_PAGE = 0; private final CommentRepository commentRepository; + private final ArticleService articleService; public Comment createComment(String content, Long memberId, Long articleId) { - Comment comment = new Comment(memberId, articleId, content); + Article article = articleService.getArticle(articleId); + Comment comment = new Comment(memberId, article, content); commentRepository.save(comment); return comment; diff --git a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java index bfa5afc..4c218e1 100644 --- a/src/main/java/com/board/global/resolver/AuthArgumentResolver.java +++ b/src/main/java/com/board/global/resolver/AuthArgumentResolver.java @@ -18,7 +18,7 @@ public class AuthArgumentResolver implements HandlerMethodArgumentResolver { private static final String TOKEN_HEADER_NAME = "Authorization"; - private static final String TOKEN_START_NAME = "BEARER"; + private static final String TOKEN_START_NAME = "Bearer "; private static final int TOKEN_BODY_DELIMITER = 7; private final JwtTokenProvider tokenProvider; diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index 2b26026..6228fde 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -9,6 +9,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.board.article.domain.Article; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.service.CommentService; @@ -27,6 +28,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(CommentController.class) @@ -46,10 +48,12 @@ class CommentControllerTest { private Comment response; private List responses; - + private Article article; @BeforeEach void setUp() throws Exception { - response = new Comment(1L, 1L, "댓글 내용"); + article = new Article(1L, "제목", "내용"); + ReflectionTestUtils.setField(article, "id", 1L); + response = new Comment(1L, article, "댓글 내용"); responses = List.of(response); given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); @@ -109,7 +113,7 @@ void showMemberComments() throws Exception { void updateComment() throws Exception { // given CommentRequest request = new CommentRequest("수정된 댓글"); - Comment updatedResponse = new Comment(1L, 1L,"수정된 댓글"); + Comment updatedResponse = new Comment(1L, article,"수정된 댓글"); given(commentService.updateComment(any(), any(), any())).willReturn(updatedResponse); // when & then diff --git a/src/test/java/com/board/comment/loader/CommentTestDataLoader.java b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java index dc92b0d..7a7c9ff 100644 --- a/src/test/java/com/board/comment/loader/CommentTestDataLoader.java +++ b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java @@ -1,5 +1,7 @@ package com.board.comment.loader; +import com.board.article.domain.Article; +import com.board.article.repository.ArticleRepository; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; import org.springframework.boot.CommandLineRunner; @@ -10,18 +12,22 @@ public class CommentTestDataLoader { private final CommentRepository repository; + private final ArticleRepository articleRepository; - public CommentTestDataLoader(CommentRepository repository) { + public CommentTestDataLoader(CommentRepository repository, ArticleRepository articleRepository) { this.repository = repository; + this.articleRepository = articleRepository; } @Bean public CommandLineRunner testData() { return args -> { - repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); - repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); - repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); - repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); + Article article1 = articleRepository.save(new Article(1L, "제목", "내용")); + Article article2 = articleRepository.save(new Article(1L, "제제목", "내내용")); + repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 2")); }; } } diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index 780bd03..218d1a5 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -5,6 +5,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import com.board.article.domain.Article; +import com.board.article.service.ArticleService; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; @@ -25,6 +27,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @SuppressWarnings("NonAsciiCharacters") @@ -33,15 +36,21 @@ class CommentServiceTest { @Mock private CommentRepository commentRepository; + @Mock + private ArticleService articleService; + @InjectMocks private CommentService commentService; private Comment comment; private List comments; + private Article article; @BeforeEach void set() { - comment = new Comment(1L, 1L, "첫 번째 게시글 댓글 1"); + article = new Article(1L, "제목", "내용"); + ReflectionTestUtils.setField(article, "id", 1L); + comment = new Comment(1L, article, "첫 번째 게시글 댓글 1"); comments = List.of(comment); } @@ -57,14 +66,15 @@ void createComment() { Long articleId = 1L; given(commentRepository.save(any(Comment.class))).willReturn(comment); + given(articleService.getArticle(articleId)).willReturn(article); // when Comment response = commentService.createComment(request.content(), memberId, articleId); // then assertThat(response) - .extracting(Comment::getContent, Comment::getMemberId, Comment::getArticleId) - .containsExactly("첫 번째 게시글 댓글1", memberId, articleId); + .extracting(Comment::getContent, Comment::getMemberId, Comment::getArticle) + .containsExactly("첫 번째 게시글 댓글1", memberId, article); } @Test From 3eabf85506fc920bb93c0e6a9031b3a0762e3a60 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 27 Apr 2025 01:29:06 +0900 Subject: [PATCH 47/52] =?UTF-8?q?feat:=20Member=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C,=20Article=20=EC=9E=91=EC=84=B1=EC=9E=90=EC=99=80=20C?= =?UTF-8?q?omment=20=EC=9E=91=EC=84=B1=EC=9E=90=20=EC=95=8C=20=EC=88=98=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/article/domain/Article.java | 3 +-- .../article/repository/ArticleRepository.java | 7 ++++++ .../board/article/service/ArticleService.java | 4 +++ .../comment/repository/CommentRepository.java | 7 ++++++ .../board/comment/service/CommentService.java | 4 +++ .../member/service/member/MemberService.java | 4 +++ .../member/event/MemberDeletedEvent.java | 6 +++++ .../member/event/MemberEventListener.java | 25 +++++++++++++++++++ 8 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/board/member/service/member/event/MemberDeletedEvent.java create mode 100644 src/main/java/com/board/member/service/member/event/MemberEventListener.java diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index dee4e2d..7c4129c 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -11,7 +11,6 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotBlank; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import lombok.AccessLevel; @@ -39,7 +38,7 @@ public class Article { private String content; @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE) - private List comments = new ArrayList<>(); + private List comments; public Article(Long memberId, String title, String content) { diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java index cfb953f..2dc55ea 100644 --- a/src/main/java/com/board/article/repository/ArticleRepository.java +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -5,10 +5,17 @@ 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.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ArticleRepository extends JpaRepository { Page
findArticleByMemberId(Long memberId, Pageable pageable); List
findByIdLessThanOrderByIdDesc(Long lastId, Pageable pageable); + + @Modifying + @Query("update Article a set a.memberId = null where a.memberId = :memberId") + void updateMemberIdToNUll(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index e05fa4d..c46e0d5 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -46,6 +46,10 @@ public Article deleteArticle(Long articleId, Long memberId) { return article; } + public void updateMemberIdToNUll(Long memberId) { + articleRepository.updateMemberIdToNUll(memberId); + } + @Transactional(readOnly = true) public List
getAllArticles(Long lastId, int size) { Pageable articlePageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 1f19b2a..6d38e65 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -5,6 +5,9 @@ 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.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -13,4 +16,8 @@ public interface CommentRepository extends JpaRepository { List findByArticleIdAndIdLessThanOrderByIdDesc(Long articleId, Long lastId, Pageable pageable); Page findAllByMemberId(Long memberId, Pageable pageable); + + @Modifying + @Query("update Comment c set c.memberId = null where c.memberId = :memberId") + void updateMemberIdToNull(@Param("memberId") Long memberId); } diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index afd32de..be77173 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -51,6 +51,10 @@ public Comment deleteComment(Long memberId, Long commentId) { return comment; } + public void updateMemberIdToNull(Long memberId) { + commentRepository.updateMemberIdToNull(memberId); + } + @Transactional(readOnly = true) public List getArticleComments(Long articleId, Long lastId, int size) { Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index 83dd797..dfa4d90 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -4,7 +4,9 @@ import com.board.member.exception.MemberErrorCode; import com.board.member.exception.MemberException; import com.board.member.repository.MemberRepository; +import com.board.member.service.member.event.MemberDeletedEvent; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,6 +16,7 @@ public class MemberService { private final MemberRepository memberRepository; + private final ApplicationEventPublisher eventPublisher; public Member updateMember(Long memberId, String requestName, String requestNickName, String requestId, String requestPassword) { Member member = getMember(memberId); @@ -25,6 +28,7 @@ public Member updateMember(Long memberId, String requestName, String requestNick public Member deleteMember(Long memberId) { Member member = getMember(memberId); memberRepository.delete(member); + eventPublisher.publishEvent(new MemberDeletedEvent(memberId)); return member; } diff --git a/src/main/java/com/board/member/service/member/event/MemberDeletedEvent.java b/src/main/java/com/board/member/service/member/event/MemberDeletedEvent.java new file mode 100644 index 0000000..c4097a5 --- /dev/null +++ b/src/main/java/com/board/member/service/member/event/MemberDeletedEvent.java @@ -0,0 +1,6 @@ +package com.board.member.service.member.event; + +public record MemberDeletedEvent( + Long memberId +) { +} diff --git a/src/main/java/com/board/member/service/member/event/MemberEventListener.java b/src/main/java/com/board/member/service/member/event/MemberEventListener.java new file mode 100644 index 0000000..856570e --- /dev/null +++ b/src/main/java/com/board/member/service/member/event/MemberEventListener.java @@ -0,0 +1,25 @@ +package com.board.member.service.member.event; + +import com.board.article.service.ArticleService; +import com.board.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class MemberEventListener { + + private final ArticleService articleService; + private final CommentService commentService; + + @Transactional + @EventListener + public void handleMemberDeletedEvent(MemberDeletedEvent event) { + Long memberId = event.memberId(); + + articleService.updateMemberIdToNUll(memberId); + commentService.updateMemberIdToNull(memberId); + } +} From d0b81ea7f208355fffd640f5a2322ed6c0afedc0 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sat, 17 May 2025 23:39:59 +0900 Subject: [PATCH 48/52] =?UTF-8?q?refactor:=20get=20/=20find=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/article/controller/ArticleController.java | 6 +++--- .../com/board/article/service/ArticleService.java | 10 +++++----- .../board/comment/controller/CommentController.java | 4 ++-- .../com/board/comment/service/CommentService.java | 12 ++++++------ .../member/controller/member/MemberController.java | 2 +- .../board/member/service/member/MemberService.java | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index cb154ea..a808391 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -51,7 +51,7 @@ public ResponseEntity showAllArticles( @RequestParam Long lastId, @RequestParam int size ) { - List
articles = articleService.getAllArticles(lastId, size); + List
articles = articleService.findAllArticles(lastId, size); List responses = articles.stream() .map(article -> new ArticleResponse( article.getId(), @@ -65,7 +65,7 @@ public ResponseEntity showAllArticles( @GetMapping("/articles/{articleId}") public ResponseEntity showArticle(@PathVariable Long articleId) { - Article article = articleService.getArticle(articleId); + Article article = articleService.findArticle(articleId); ArticleResponse response = new ArticleResponse( article.getId(), article.getMemberId(), @@ -82,7 +82,7 @@ public ResponseEntity showMemberArticles( @RequestParam int page, @RequestParam int size ) { - Page
articles = articleService.getMemberArticles(memberId, page, size); + Page
articles = articleService.findMemberArticles(memberId, page, size); List responses = articles.stream() .map(article -> new ArticleResponse( article.getId(), diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index c46e0d5..e2f32b1 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -31,7 +31,7 @@ public Article createArticle(Long memberId, String title, String content) { } public Article updateArticle(Long articleId, Long memberId, String title, String content) { - Article article = getArticle(articleId); + Article article = findArticle(articleId); article.validateAccessAboutArticle(memberId); article.update(title, content); @@ -39,7 +39,7 @@ public Article updateArticle(Long articleId, Long memberId, String title, String } public Article deleteArticle(Long articleId, Long memberId) { - Article article = getArticle(articleId); + Article article = findArticle(articleId); article.validateAccessAboutArticle(memberId); articleRepository.delete(article); @@ -51,19 +51,19 @@ public void updateMemberIdToNUll(Long memberId) { } @Transactional(readOnly = true) - public List
getAllArticles(Long lastId, int size) { + public List
findAllArticles(Long lastId, int size) { Pageable articlePageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return articleRepository.findByIdLessThanOrderByIdDesc(lastId, articlePageable); } @Transactional(readOnly = true) - public Article getArticle(Long articleId) { + public Article findArticle(Long articleId) { return articleRepository.findById(articleId) .orElseThrow(() -> new ArticleException(ArticleErrorCode.NOT_FOUND_ARTICLE)); } @Transactional(readOnly = true) - public Page
getMemberArticles(Long memberId, int page, int size) { + public Page
findMemberArticles(Long memberId, int page, int size) { Pageable articlePageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return articleRepository.findArticleByMemberId(memberId, articlePageable); } diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 878df25..4cee204 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -51,7 +51,7 @@ public ResponseEntity showArticleComments( @RequestParam Long lastId, @RequestParam int size ) { - List comments = commentService.getArticleComments(articleId, lastId, size); + List comments = commentService.findArticleComments(articleId, lastId, size); List responses = comments.stream() .map(comment -> new CommentResponse( comment.getMemberId(), @@ -68,7 +68,7 @@ public ResponseEntity showMemberComments( @RequestParam int page, @RequestParam int size ) { - Page comments = commentService.getMemberComments(memberId, page, size); + Page comments = commentService.findMemberComments(memberId, page, size); List responses = comments.stream() .map(comment -> new CommentResponse( comment.getMemberId(), diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index be77173..b2bf9a1 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -28,7 +28,7 @@ public class CommentService { private final ArticleService articleService; public Comment createComment(String content, Long memberId, Long articleId) { - Article article = articleService.getArticle(articleId); + Article article = articleService.findArticle(articleId); Comment comment = new Comment(memberId, article, content); commentRepository.save(comment); @@ -36,7 +36,7 @@ public Comment createComment(String content, Long memberId, Long articleId) { } public Comment updateComment(CommentRequest request, Long memberId, Long commentId) { - Comment comment = getComment(commentId); + Comment comment = findComment(commentId); comment.validateAccessAboutComment(memberId); comment.update(request.content()); @@ -44,7 +44,7 @@ public Comment updateComment(CommentRequest request, Long memberId, Long comment } public Comment deleteComment(Long memberId, Long commentId) { - Comment comment = getComment(commentId); + Comment comment = findComment(commentId); comment.validateAccessAboutComment(memberId); commentRepository.delete(comment); @@ -56,19 +56,19 @@ public void updateMemberIdToNull(Long memberId) { } @Transactional(readOnly = true) - public List getArticleComments(Long articleId, Long lastId, int size) { + public List findArticleComments(Long articleId, Long lastId, int size) { Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, commentPageable); } @Transactional(readOnly = true) - public Page getMemberComments(Long memberId, int page, int size) { + public Page findMemberComments(Long memberId, int page, int size) { Pageable commentPageable = PageRequest.of(page, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return commentRepository.findAllByMemberId(memberId, commentPageable); } @Transactional(readOnly = true) - public Comment getComment(Long commentId) { + public Comment findComment(Long commentId) { return commentRepository.findById(commentId) .orElseThrow(() -> new CommentException(CommentErrorCode.NOT_FOUND_COMMENT)); } diff --git a/src/main/java/com/board/member/controller/member/MemberController.java b/src/main/java/com/board/member/controller/member/MemberController.java index 611d90c..168c3b9 100644 --- a/src/main/java/com/board/member/controller/member/MemberController.java +++ b/src/main/java/com/board/member/controller/member/MemberController.java @@ -26,7 +26,7 @@ public class MemberController { @GetMapping("/members") public ResponseEntity showMember(@Auth Long memberId) { - Member member = memberService.getMember(memberId); + Member member = memberService.findMember(memberId); MemberResponse response = new MemberResponse( member.getMemberName(), member.getMemberNickName(), diff --git a/src/main/java/com/board/member/service/member/MemberService.java b/src/main/java/com/board/member/service/member/MemberService.java index dfa4d90..8f454bf 100644 --- a/src/main/java/com/board/member/service/member/MemberService.java +++ b/src/main/java/com/board/member/service/member/MemberService.java @@ -19,14 +19,14 @@ public class MemberService { private final ApplicationEventPublisher eventPublisher; public Member updateMember(Long memberId, String requestName, String requestNickName, String requestId, String requestPassword) { - Member member = getMember(memberId); + Member member = findMember(memberId); member.update(requestName, requestNickName, requestId, requestPassword); return member; } public Member deleteMember(Long memberId) { - Member member = getMember(memberId); + Member member = findMember(memberId); memberRepository.delete(member); eventPublisher.publishEvent(new MemberDeletedEvent(memberId)); @@ -34,7 +34,7 @@ public Member deleteMember(Long memberId) { } @Transactional(readOnly = true) - public Member getMember(Long memberId) { + public Member findMember(Long memberId) { return memberRepository.findMemberById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER)); } From 5e146bc38f2403384c2d91759a8fbe354bc6cd3f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 18 May 2025 14:55:26 +0900 Subject: [PATCH 49/52] =?UTF-8?q?refactor:=20article=20paging=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EA=B0=92=EC=9D=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/controller/ArticleController.java | 29 ++---- .../dto/response/PageArticleResponse.java | 29 ++++++ .../article/repository/ArticleRepository.java | 3 +- .../board/article/service/ArticleService.java | 3 +- .../controller/ArticleControllerTest.java | 88 ++++++++++--------- .../repository/ArticleRepositoryTest.java | 3 +- .../article/service/ArticleServiceTest.java | 21 +++-- .../controller/CommentControllerTest.java | 4 +- .../comment/service/CommentServiceTest.java | 10 +-- .../member/MemberControllerTest.java | 2 +- .../service/member/MemberServiceTest.java | 4 +- 11 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/board/article/controller/dto/response/PageArticleResponse.java diff --git a/src/main/java/com/board/article/controller/ArticleController.java b/src/main/java/com/board/article/controller/ArticleController.java index a808391..8846eae 100644 --- a/src/main/java/com/board/article/controller/ArticleController.java +++ b/src/main/java/com/board/article/controller/ArticleController.java @@ -2,12 +2,11 @@ import com.board.article.controller.dto.request.ArticleRequest; import com.board.article.controller.dto.response.ArticleResponse; -import com.board.article.controller.dto.response.ArticleResponses; +import com.board.article.controller.dto.response.PageArticleResponse; import com.board.article.domain.Article; import com.board.article.service.ArticleService; import com.board.global.resolver.annotation.Auth; import java.net.URI; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -47,20 +46,12 @@ public ResponseEntity createArticle(@RequestBody ArticleRequest } @GetMapping("/articles") - public ResponseEntity showAllArticles( + public ResponseEntity showAllArticles( @RequestParam Long lastId, @RequestParam int size ) { - List
articles = articleService.findAllArticles(lastId, size); - List responses = articles.stream() - .map(article -> new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - )).toList(); - - return ResponseEntity.ok(new ArticleResponses(responses)); + Page
articles = articleService.findAllArticles(lastId, size); + return ResponseEntity.ok(new PageArticleResponse(articles)); } @GetMapping("/articles/{articleId}") @@ -77,21 +68,13 @@ public ResponseEntity showArticle(@PathVariable Long articleId) } @GetMapping("/members/me/articles") - public ResponseEntity showMemberArticles( + public ResponseEntity showMemberArticles( @Auth Long memberId, @RequestParam int page, @RequestParam int size ) { Page
articles = articleService.findMemberArticles(memberId, page, size); - List responses = articles.stream() - .map(article -> new ArticleResponse( - article.getId(), - article.getMemberId(), - article.getTitle(), - article.getContent() - )).toList(); - - return ResponseEntity.ok(new ArticleResponses(responses)); + return ResponseEntity.ok(new PageArticleResponse(articles)); } @PatchMapping("/articles/{articleId}") diff --git a/src/main/java/com/board/article/controller/dto/response/PageArticleResponse.java b/src/main/java/com/board/article/controller/dto/response/PageArticleResponse.java new file mode 100644 index 0000000..4b55545 --- /dev/null +++ b/src/main/java/com/board/article/controller/dto/response/PageArticleResponse.java @@ -0,0 +1,29 @@ +package com.board.article.controller.dto.response; + +import com.board.article.domain.Article; +import java.util.List; +import org.springframework.data.domain.Page; + +public record PageArticleResponse( + List articleResponses, + int pageNumber, + int pageSize, + long totalElements, + int totalPages +) { + + public PageArticleResponse(Page
page) { + this( + page.map(article -> new ArticleResponse( + article.getId(), + article.getMemberId(), + article.getTitle(), + article.getContent() + )).getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} diff --git a/src/main/java/com/board/article/repository/ArticleRepository.java b/src/main/java/com/board/article/repository/ArticleRepository.java index 2dc55ea..157eacb 100644 --- a/src/main/java/com/board/article/repository/ArticleRepository.java +++ b/src/main/java/com/board/article/repository/ArticleRepository.java @@ -1,7 +1,6 @@ package com.board.article.repository; import com.board.article.domain.Article; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,7 +12,7 @@ public interface ArticleRepository extends JpaRepository { Page
findArticleByMemberId(Long memberId, Pageable pageable); - List
findByIdLessThanOrderByIdDesc(Long lastId, Pageable pageable); + Page
findByIdLessThanOrderByIdDesc(Long lastId, Pageable pageable); @Modifying @Query("update Article a set a.memberId = null where a.memberId = :memberId") diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index e2f32b1..b99021a 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -4,7 +4,6 @@ import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; import com.board.article.repository.ArticleRepository; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -51,7 +50,7 @@ public void updateMemberIdToNUll(Long memberId) { } @Transactional(readOnly = true) - public List
findAllArticles(Long lastId, int size) { + public Page
findAllArticles(Long lastId, int size) { Pageable articlePageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return articleRepository.findByIdLessThanOrderByIdDesc(lastId, articlePageable); } diff --git a/src/test/java/com/board/article/controller/ArticleControllerTest.java b/src/test/java/com/board/article/controller/ArticleControllerTest.java index 384569e..b0e5b85 100644 --- a/src/test/java/com/board/article/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/article/controller/ArticleControllerTest.java @@ -1,6 +1,7 @@ package com.board.article.controller; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -46,106 +47,109 @@ class ArticleControllerTest { @MockBean private AuthArgumentResolver authArgumentResolver; - private Article articleResponse; - private List
articleResponses; + private Article article; + private Page
articlePage; @BeforeEach void setUp() throws Exception { given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); - articleResponse = new Article(1L, "제목", "내용"); - ReflectionTestUtils.setField(articleResponse, "id", 1L); - articleResponses = List.of(articleResponse); + + article = new Article(1L, "제목", "내용"); + ReflectionTestUtils.setField(article, "id", 1L); + + List
articles = List.of(article); + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + articlePage = new PageImpl<>(articles, pageable, articles.size()); } @Test @DisplayName("게시글을 생성한다.") void createArticle() throws Exception { - // given ArticleRequest request = new ArticleRequest("제목", "내용"); - given(articleService.createArticle(any(), any(), any())).willReturn(articleResponse); + given(articleService.createArticle(anyLong(), any(), any())).willReturn(article); - // when & then mockMvc.perform(post("/articles") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(header().string("Location", "http://localhost/articles/1")) - .andExpect(jsonPath("$.articleId").value(1L)); + .andExpect(jsonPath("$.articleId").value(1L)) + .andExpect(jsonPath("$.title").value("제목")) + .andExpect(jsonPath("$.content").value("내용")); } @Test @DisplayName("모든 게시글을 조회한다.") void showAllArticles() throws Exception { - // given - Long lastId = 0L; - int size = 5; - given(articleService.getAllArticles(lastId, size)).willReturn(articleResponses); + given(articleService.findAllArticles(0L, 5)).willReturn(articlePage); - // when & then mockMvc.perform(get("/articles") - .param("lastId", String.valueOf(lastId)) - .param("size", String.valueOf(size))) + .param("lastId", "0") + .param("size", "5")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); + .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)) + .andExpect(jsonPath("$.articleResponses[0].title").value("제목")) + .andExpect(jsonPath("$.articleResponses[0].content").value("내용")) + .andExpect(jsonPath("$.pageNumber").value(0)) + .andExpect(jsonPath("$.pageSize").value(10)) + .andExpect(jsonPath("$.totalElements").value(1)) + .andExpect(jsonPath("$.totalPages").value(1)); } @Test @DisplayName("게시글 하나를 조회한다.") void showArticle() throws Exception { - // given - given(articleService.getArticle(1L)).willReturn(articleResponse); + given(articleService.findArticle(1L)).willReturn(article); - // when & then mockMvc.perform(get("/articles/1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.articleId").value(1L)); + .andExpect(jsonPath("$.articleId").value(1L)) + .andExpect(jsonPath("$.title").value("제목")) + .andExpect(jsonPath("$.content").value("내용")); } @Test @DisplayName("회원의 게시글을 조회한다.") void showMemberArticles() throws Exception { - // given - Long memberId = 1L; - int page = 0; - int size = 10; - Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); - Page
articlePage = new PageImpl<>(articleResponses, pageable, articleResponses.size()); - given(articleService.getMemberArticles(memberId, page, size)).willReturn(articlePage); - - // when & then + given(articleService.findMemberArticles(1L, 0, 10)).willReturn(articlePage); + mockMvc.perform(get("/members/me/articles") - .param("page", String.valueOf(page)) - .param("size", String.valueOf(size))) + .param("page", "0") + .param("size", "10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)); + .andExpect(jsonPath("$.articleResponses[0].articleId").value(1L)) + .andExpect(jsonPath("$.articleResponses[0].title").value("제목")) + .andExpect(jsonPath("$.articleResponses[0].content").value("내용")); } @Test @DisplayName("게시글을 수정한다.") void updateArticle() throws Exception { - // given ArticleRequest request = new ArticleRequest("수정된 제목", "수정된 내용"); - Article response = new Article(1L, "수정된 제목", "수정된 내용"); - given(articleService.updateArticle(any(), any(), any(), any())).willReturn(response); + Article updatedArticle = new Article(1L, "수정된 제목", "수정된 내용"); + ReflectionTestUtils.setField(updatedArticle, "id", 1L); + + given(articleService.updateArticle(anyLong(), anyLong(), any(), any())).willReturn(updatedArticle); - // when & then mockMvc.perform(patch("/articles/1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.title").value("수정된 제목")); + .andExpect(jsonPath("$.articleId").value(1L)) + .andExpect(jsonPath("$.title").value("수정된 제목")) + .andExpect(jsonPath("$.content").value("수정된 내용")); } @Test @DisplayName("게시글을 삭제한다.") void deleteArticle() throws Exception { - // given - given(articleService.deleteArticle(1L, 1L)).willReturn(articleResponse); + given(articleService.deleteArticle(1L, 1L)).willReturn(article); - // when & then mockMvc.perform(delete("/articles/1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.articleId").value(1L)); + .andExpect(jsonPath("$.articleId").value(1L)) + .andExpect(jsonPath("$.title").value("제목")) + .andExpect(jsonPath("$.content").value("내용")); } } diff --git a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java index 2806eff..73684bb 100644 --- a/src/test/java/com/board/article/repository/ArticleRepositoryTest.java +++ b/src/test/java/com/board/article/repository/ArticleRepositoryTest.java @@ -4,7 +4,6 @@ import com.board.article.domain.Article; import com.board.article.loader.ArticleTestDataLoader; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -46,7 +45,7 @@ void findByIdLessThanOrderByIdDesc() { Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); // when - List
articles = articleRepository.findByIdLessThanOrderByIdDesc(lastId, pageable); + Page
articles = articleRepository.findByIdLessThanOrderByIdDesc(lastId, pageable); // then assertThat(articles).hasSize(1); diff --git a/src/test/java/com/board/article/service/ArticleServiceTest.java b/src/test/java/com/board/article/service/ArticleServiceTest.java index bc116ce..e12d550 100644 --- a/src/test/java/com/board/article/service/ArticleServiceTest.java +++ b/src/test/java/com/board/article/service/ArticleServiceTest.java @@ -37,14 +37,17 @@ class ArticleServiceTest { private ArticleService articleService; private Article article; - private List
articles; + private Page
articlePage; @BeforeEach void set() { article = new Article(1L, "제목1", "내용1"); - articles = List.of(article); + List
articleList = List.of(article); + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + articlePage = new PageImpl<>(articleList, pageable, articleList.size()); } + @Nested class 정상_동작_테스트를_진행한다 { @@ -71,11 +74,12 @@ void showAllArticles() { // given Long lastId = 0L; int size = 10; - given(articleRepository.findByIdLessThanOrderByIdDesc(lastId, PageRequest.of(0, size, Sort.by("id").descending()))) - .willReturn(articles); + Pageable pageable = PageRequest.of(0, size, Sort.by("id").descending()); + given(articleRepository.findByIdLessThanOrderByIdDesc(lastId, pageable)) + .willReturn(articlePage); // when - List
responses = articleService.getAllArticles(lastId, size); + Page
responses = articleService.findAllArticles(lastId, size); // then assertThat(responses).hasSize(1) @@ -91,7 +95,7 @@ void showArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.of(article)); // when - Article response = articleService.getArticle(articleId); + Article response = articleService.findArticle(articleId); // then assertThat(response.getTitle()).isEqualTo("제목1"); @@ -105,11 +109,10 @@ void showMemberArticles() { int page = 0; int size = 10; Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); - Page
articlePage = new PageImpl<>(articles, pageable, articles.size()); given(articleRepository.findArticleByMemberId(memberId, pageable)).willReturn(articlePage); // when - Page
responses = articleService.getMemberArticles(memberId, page, size); + Page
responses = articleService.findMemberArticles(memberId, page, size); // then assertThat(responses).hasSize(1) @@ -160,7 +163,7 @@ void notFoundArticle() { given(articleRepository.findById(articleId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> articleService.getArticle(articleId)) + assertThatThrownBy(() -> articleService.findArticle(articleId)) .isInstanceOf(ArticleException.class) .hasMessageContaining(ArticleErrorCode.NOT_FOUND_ARTICLE.message()); } diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index 6228fde..b5a551e 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -80,7 +80,7 @@ void showArticleComments() throws Exception { // given Long lastId = 3L; int size = 5; - given(commentService.getArticleComments(1L, lastId, size)).willReturn(responses); + given(commentService.findArticleComments(1L, lastId, size)).willReturn(responses); // when & then mockMvc.perform(get("/articles/1/comments") @@ -98,7 +98,7 @@ void showMemberComments() throws Exception { int size = 10; Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); Page commentPage = new PageImpl<>(responses, pageable, responses.size()); - given(commentService.getMemberComments(1L, page, size)).willReturn(commentPage); + given(commentService.findMemberComments(1L, page, size)).willReturn(commentPage); // when & then mockMvc.perform(get("/members/me/comments") diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index 218d1a5..0c7e437 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -66,7 +66,7 @@ void createComment() { Long articleId = 1L; given(commentRepository.save(any(Comment.class))).willReturn(comment); - given(articleService.getArticle(articleId)).willReturn(article); + given(articleService.findArticle(articleId)).willReturn(article); // when Comment response = commentService.createComment(request.content(), memberId, articleId); @@ -89,7 +89,7 @@ void showArticleComments() { .willReturn(comments); // when - List responses = commentService.getArticleComments(articleId, lastId, size); + List responses = commentService.findArticleComments(articleId, lastId, size); // then assertThat(responses).hasSize(1) @@ -109,7 +109,7 @@ void showMemberArticles() { given(commentRepository.findAllByMemberId(memberId, pageable)).willReturn(commentPage); // when - Page responses = commentService.getMemberComments(memberId, page, size); + Page responses = commentService.findMemberComments(memberId, page, size); // then assertThat(responses).hasSize(1) @@ -156,7 +156,7 @@ void getComment() { given(commentRepository.findById(commentId)).willReturn(Optional.of(comment)); // when - Comment response = commentService.getComment(commentId); + Comment response = commentService.findComment(commentId); // then assertThat(response.getContent()).isEqualTo("첫 번째 게시글 댓글 1"); @@ -174,7 +174,7 @@ void getCommentException() { given(commentRepository.findById(commentId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> commentService.getComment(commentId)) + assertThatThrownBy(() -> commentService.findComment(commentId)) .isInstanceOf(CommentException.class) .hasMessageContaining(CommentErrorCode.NOT_FOUND_COMMENT.message()); } diff --git a/src/test/java/com/board/member/controller/member/MemberControllerTest.java b/src/test/java/com/board/member/controller/member/MemberControllerTest.java index 706e4e8..bf1d14c 100644 --- a/src/test/java/com/board/member/controller/member/MemberControllerTest.java +++ b/src/test/java/com/board/member/controller/member/MemberControllerTest.java @@ -51,7 +51,7 @@ void set() throws Exception { void showMember() throws Exception { // given Long memberId = 1L; - given(memberService.getMember(memberId)).willReturn(member); + given(memberService.findMember(memberId)).willReturn(member); // when & then mockMvc.perform(get("/members") diff --git a/src/test/java/com/board/member/service/member/MemberServiceTest.java b/src/test/java/com/board/member/service/member/MemberServiceTest.java index c4d4df0..d63555d 100644 --- a/src/test/java/com/board/member/service/member/MemberServiceTest.java +++ b/src/test/java/com/board/member/service/member/MemberServiceTest.java @@ -47,7 +47,7 @@ void showMember() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.of(member)); // when - Member response = memberService.getMember(memberId); + Member response = memberService.findMember(memberId); // then assertThat(response.getMemberName()).isEqualTo("신짱구"); @@ -99,7 +99,7 @@ void showMemberException() { given(memberRepository.findMemberById(memberId)).willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> memberService.getMember(memberId)) + assertThatThrownBy(() -> memberService.findMember(memberId)) .isInstanceOf(MemberException.class) .hasMessageContaining(MemberErrorCode.NOT_FOUND_MEMBER.message()); } From e1c23f1b05e6939d4339a33dfaacca1e23a80fe6 Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 18 May 2025 15:22:29 +0900 Subject: [PATCH 50/52] =?UTF-8?q?refactor:=20comment=20paging=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=EA=B0=92=EC=9D=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 27 ++++-------------- .../dto/reponse/PageCommentResponse.java | 28 +++++++++++++++++++ .../comment/repository/CommentRepository.java | 3 +- .../board/comment/service/CommentService.java | 3 +- .../controller/CommentControllerTest.java | 13 +++++---- .../repository/CommentRepositoryTest.java | 3 +- .../comment/service/CommentServiceTest.java | 7 +++-- 7 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index 4cee204..c3a53ae 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -1,13 +1,12 @@ package com.board.comment.controller; import com.board.comment.controller.dto.reponse.CommentResponse; -import com.board.comment.controller.dto.reponse.CommentResponses; +import com.board.comment.controller.dto.reponse.PageCommentResponse; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.service.CommentService; import com.board.global.resolver.annotation.Auth; import java.net.URI; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -46,37 +45,23 @@ public ResponseEntity createComment(@RequestBody CommentRequest } @GetMapping("/articles/{articleId}/comments") - public ResponseEntity showArticleComments( + public ResponseEntity showArticleComments( @PathVariable Long articleId, @RequestParam Long lastId, @RequestParam int size ) { - List comments = commentService.findArticleComments(articleId, lastId, size); - List responses = comments.stream() - .map(comment -> new CommentResponse( - comment.getMemberId(), - comment.getArticle().getId(), - comment.getContent() - )).toList(); - - return ResponseEntity.ok(new CommentResponses(responses)); + Page comments = commentService.findArticleComments(articleId, lastId, size); + return ResponseEntity.ok(new PageCommentResponse(comments)); } @GetMapping("members/me/comments") - public ResponseEntity showMemberComments( + public ResponseEntity showMemberComments( @Auth Long memberId, @RequestParam int page, @RequestParam int size ) { Page comments = commentService.findMemberComments(memberId, page, size); - List responses = comments.stream() - .map(comment -> new CommentResponse( - comment.getMemberId(), - comment.getArticle().getId(), - comment.getContent() - )).toList(); - - return ResponseEntity.ok(new CommentResponses(responses)); + return ResponseEntity.ok(new PageCommentResponse(comments)); } @PatchMapping("/comments/{commentId}") diff --git a/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java b/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java new file mode 100644 index 0000000..2825372 --- /dev/null +++ b/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java @@ -0,0 +1,28 @@ +package com.board.comment.controller.dto.reponse; + +import com.board.comment.domain.Comment; +import java.util.List; +import org.springframework.data.domain.Page; + +public record PageCommentResponse( + List commentResponses, + int pageNumber, + int pageSize, + long totalElements, + int totalPages +) { + + public PageCommentResponse(Page page) { + this( + page.map(comment -> new CommentResponse( + comment.getMemberId(), + comment.getArticle().getId(), + comment.getContent() + )).getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages() + ); + } +} diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 6d38e65..4cc3abf 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -1,7 +1,6 @@ package com.board.comment.repository; import com.board.comment.domain.Comment; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,7 +12,7 @@ @Repository public interface CommentRepository extends JpaRepository { - List findByArticleIdAndIdLessThanOrderByIdDesc(Long articleId, Long lastId, Pageable pageable); + Page findByArticleIdAndIdLessThanOrderByIdDesc(Long articleId, Long lastId, Pageable pageable); Page findAllByMemberId(Long memberId, Pageable pageable); diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index b2bf9a1..edeced9 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -7,7 +7,6 @@ import com.board.comment.exception.CommentErrorCode; import com.board.comment.exception.CommentException; import com.board.comment.repository.CommentRepository; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -56,7 +55,7 @@ public void updateMemberIdToNull(Long memberId) { } @Transactional(readOnly = true) - public List findArticleComments(Long articleId, Long lastId, int size) { + public Page findArticleComments(Long articleId, Long lastId, int size) { Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); return commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, commentPageable); } diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index b5a551e..4d42d96 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -47,14 +47,19 @@ class CommentControllerTest { private AuthArgumentResolver authArgumentResolver; private Comment response; - private List responses; private Article article; + private Page commentPage; @BeforeEach void setUp() throws Exception { article = new Article(1L, "제목", "내용"); ReflectionTestUtils.setField(article, "id", 1L); + response = new Comment(1L, article, "댓글 내용"); - responses = List.of(response); + List responses = List.of(response); + + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + commentPage = new PageImpl<>(responses, pageable, responses.size()); + given(authArgumentResolver.supportsParameter(any())).willReturn(true); given(authArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(1L); } @@ -80,7 +85,7 @@ void showArticleComments() throws Exception { // given Long lastId = 3L; int size = 5; - given(commentService.findArticleComments(1L, lastId, size)).willReturn(responses); + given(commentService.findArticleComments(1L, lastId, size)).willReturn(commentPage); // when & then mockMvc.perform(get("/articles/1/comments") @@ -96,8 +101,6 @@ void showMemberComments() throws Exception { // given int page = 0; int size = 10; - Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending()); - Page commentPage = new PageImpl<>(responses, pageable, responses.size()); given(commentService.findMemberComments(1L, page, size)).willReturn(commentPage); // when & then diff --git a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java index 5f9ab8e..f70f1be 100644 --- a/src/test/java/com/board/comment/repository/CommentRepositoryTest.java +++ b/src/test/java/com/board/comment/repository/CommentRepositoryTest.java @@ -4,7 +4,6 @@ import com.board.comment.domain.Comment; import com.board.comment.loader.CommentTestDataLoader; -import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -31,7 +30,7 @@ void findAllByArticleId() { Pageable pageable = PageRequest.of(0, 5, Sort.by("id").descending()); // when - List comments = commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, pageable); + Page comments = commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, pageable); // then assertThat(comments).hasSize(2) diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index 0c7e437..de11e64 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -45,6 +45,7 @@ class CommentServiceTest { private Comment comment; private List comments; private Article article; + private Page commentPage; @BeforeEach void set() { @@ -52,6 +53,8 @@ void set() { ReflectionTestUtils.setField(article, "id", 1L); comment = new Comment(1L, article, "첫 번째 게시글 댓글 1"); comments = List.of(comment); + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + commentPage = new PageImpl<>(comments, pageable, comments.size()); } @Nested @@ -86,10 +89,10 @@ void showArticleComments() { int size = 5; Pageable pageable = PageRequest.of(0, size, Sort.by("id").descending()); given(commentRepository.findByArticleIdAndIdLessThanOrderByIdDesc(articleId, lastId, pageable)) - .willReturn(comments); + .willReturn(commentPage); // when - List responses = commentService.findArticleComments(articleId, lastId, size); + Page responses = commentService.findArticleComments(articleId, lastId, size); // then assertThat(responses).hasSize(1) From aaade75665231493a8b704861e99439cded6554f Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 18 May 2025 15:29:00 +0900 Subject: [PATCH 51/52] =?UTF-8?q?docs:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=9D=EA=B0=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 2 +- docs/test.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/code_review.md b/docs/code_review.md index 1709e07..b3f8c74 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -64,7 +64,7 @@ public class GlobalException extends RuntimeException { * 페이지 번호가 있는 프론트 구현 시, 잘 맞는다. ## 단점 -* OFFSET 100000 같이 뒤 페이지로 갈수록 느려짐 → 인덱스가 있어도 느림 +* OFFSET 100000 같이 뒤 페이지로 갈수록 느려짐 → 인덱스가 있어도 느림 // 해당 문제는 커서 기반 페이징을 통해 해결 가능 -> offset 은 처음부터 모든 column을 조회해서 속도가 느리지만 커서 기반 페이징은 특정 id 를 기준으로 해당 부분부터 조회하기에 빠르다 * 페이징 도중 데이터가 추가/삭제되면 순서가 뒤틀릴 수 있음 (같은 댓글이 여러 번 보이거나 사라짐) diff --git a/docs/test.md b/docs/test.md index dd17419..8819816 100644 --- a/docs/test.md +++ b/docs/test.md @@ -132,3 +132,12 @@ public void tearDown() { * 실제 Bean 들을 다 올리기 때문에 실제 동작을 더 잘 검증할 수 있다. * 하지만 속도 느리고 무겁다. + +## Service Layer Unit 테스트의 문제점 + +---- +* 보통 서비스 계층에서 단위 테스트를 진행하면 mocking 을 통해 진행한다. +* but mocking 은 테스트가 성공한다는 전제를 가지고 있음(행위 기반이면) +* 하지만 해당 로직이 틀린 로직이였다면 ? -> 해당 테스트는 깨진 테스트 +* 따라서 해당 문제를 방지하기 위해서 -> 서비스 코드를 작성과 동시에 moking 테스트를 진행하여 올바른 반환값을 확인 + 통합 테스트로 의도하는 대로 동작하는 지 확인하는 방법이 필요 +* 결론은 moking 하는 테스트와 통합테스트 둘 다 하는 것이 맞는 것 같다. From be1818461283bcf37e2459ee9c663a2855420acb Mon Sep 17 00:00:00 2001 From: Orange flavored banana <106858113+moonwhistle@users.noreply.github.com> Date: Sun, 18 May 2025 17:00:16 +0900 Subject: [PATCH 52/52] =?UTF-8?q?feat:=20article=20/=20comment=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=84=EC=A0=91=EC=B0=B8=EC=A1=B0=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20article=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C,=20comment=20=EB=8F=84=20=EC=82=AD=EC=A0=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code_review.md | 39 ++++++++++++++++++- .../com/board/article/domain/Article.java | 8 ---- .../board/article/service/ArticleService.java | 4 ++ .../service/event/ArticleDeletedEvent.java | 6 +++ .../service/event/ArticleEventListener.java | 22 +++++++++++ .../comment/controller/CommentController.java | 6 +-- .../dto/reponse/PageCommentResponse.java | 2 +- .../com/board/comment/domain/Comment.java | 13 ++----- .../comment/loader/CommentDataLoader.java | 13 ++----- .../comment/repository/CommentRepository.java | 3 ++ .../board/comment/service/CommentService.java | 13 ++++--- .../controller/CommentControllerTest.java | 10 +---- .../comment/loader/CommentTestDataLoader.java | 16 +++----- .../comment/service/CommentServiceTest.java | 16 ++------ 14 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 src/main/java/com/board/article/service/event/ArticleDeletedEvent.java create mode 100644 src/main/java/com/board/article/service/event/ArticleEventListener.java diff --git a/docs/code_review.md b/docs/code_review.md index b3f8c74..79eebf5 100644 --- a/docs/code_review.md +++ b/docs/code_review.md @@ -90,4 +90,41 @@ public class GlobalException extends RuntimeException { --- * ORM 은 DB를 테이블처럼 다루지 말고 객체 처럼 다루자는 목표로 등장. -* + + +# 객체 관계에 따른 차이 + +--- + +## 단방향 매핑 (OneToMany or ManyToOne 한쪽만) + +* Comment → Article만 있는 구조 or Article -> Comment 만 있는 구조 +* 조회나 저장할 때 한쪽에서만 조작 가능 +~~~ +@Entity +public class Comment { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id") + private Article article; +} + +댓글을 저장할 때 어떤 게시글에 달리는지는 필요함 → Comment → Article +하지만 Article 입장에서는 굳이 댓글들을 알고 있을 필요 없음 +~~~ + +## 양방향 매핑 +* Article → Comment, Comment → Article 모두 존재 +* 양쪽이 서로를 참조하는 구조 +~~~ +// Article +@OneToMany(mappedBy = "article", cascade = CascadeType.ALL) +private List comments; + +// Comment +@ManyToOne(fetch = FetchType.LAZY) +@JoinColumn(name = "article_id") +private Article article; + +게시글 상세 페이지에서 댓글도 같이 조회하고 싶다 or 게시글 삭제 시 댓글도 자동 삭제되게 하고 싶다 +~~~ + diff --git a/src/main/java/com/board/article/domain/Article.java b/src/main/java/com/board/article/domain/Article.java index 7c4129c..9c63641 100644 --- a/src/main/java/com/board/article/domain/Article.java +++ b/src/main/java/com/board/article/domain/Article.java @@ -2,16 +2,12 @@ import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; -import com.board.comment.domain.Comment; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotBlank; -import java.util.List; import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; @@ -37,10 +33,6 @@ public class Article { @NotBlank private String content; - @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE) - private List comments; - - public Article(Long memberId, String title, String content) { this.memberId = memberId; this.title = title; diff --git a/src/main/java/com/board/article/service/ArticleService.java b/src/main/java/com/board/article/service/ArticleService.java index b99021a..a912e67 100644 --- a/src/main/java/com/board/article/service/ArticleService.java +++ b/src/main/java/com/board/article/service/ArticleService.java @@ -4,7 +4,9 @@ import com.board.article.exception.ArticleErrorCode; import com.board.article.exception.ArticleException; import com.board.article.repository.ArticleRepository; +import com.board.article.service.event.ArticleDeletedEvent; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -21,6 +23,7 @@ public class ArticleService { private static final int NO_OFFSET_PAGING_PAGE = 0; private final ArticleRepository articleRepository; + private final ApplicationEventPublisher eventPublisher; public Article createArticle(Long memberId, String title, String content) { Article article = new Article(memberId, title, content); @@ -41,6 +44,7 @@ public Article deleteArticle(Long articleId, Long memberId) { Article article = findArticle(articleId); article.validateAccessAboutArticle(memberId); articleRepository.delete(article); + eventPublisher.publishEvent(new ArticleDeletedEvent(articleId)); return article; } diff --git a/src/main/java/com/board/article/service/event/ArticleDeletedEvent.java b/src/main/java/com/board/article/service/event/ArticleDeletedEvent.java new file mode 100644 index 0000000..90d2121 --- /dev/null +++ b/src/main/java/com/board/article/service/event/ArticleDeletedEvent.java @@ -0,0 +1,6 @@ +package com.board.article.service.event; + +public record ArticleDeletedEvent( + Long articleId +) { +} diff --git a/src/main/java/com/board/article/service/event/ArticleEventListener.java b/src/main/java/com/board/article/service/event/ArticleEventListener.java new file mode 100644 index 0000000..387d6fe --- /dev/null +++ b/src/main/java/com/board/article/service/event/ArticleEventListener.java @@ -0,0 +1,22 @@ +package com.board.article.service.event; + +import com.board.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class ArticleEventListener { + + private final CommentService commentService; + + @Transactional + @EventListener + public void handleMemberDeletedEvent(ArticleDeletedEvent event) { + Long articleId = event.articleId(); + + commentService.deleteCommentByArticle(articleId); + } +} diff --git a/src/main/java/com/board/comment/controller/CommentController.java b/src/main/java/com/board/comment/controller/CommentController.java index c3a53ae..55a9066 100644 --- a/src/main/java/com/board/comment/controller/CommentController.java +++ b/src/main/java/com/board/comment/controller/CommentController.java @@ -32,7 +32,7 @@ public ResponseEntity createComment(@RequestBody CommentRequest Comment comment = commentService.createComment(request.content(), memberId, articleId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticle().getId(), + comment.getArticleId(), comment.getContent() ); @@ -70,7 +70,7 @@ public ResponseEntity updateComment(@RequestBody CommentRequest Comment comment = commentService.updateComment(request, memberId, commentId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticle().getId(), + comment.getArticleId(), comment.getContent() ); @@ -82,7 +82,7 @@ public ResponseEntity deleteComment(@Auth Long memberId, @PathV Comment comment = commentService.deleteComment(memberId, commentId); CommentResponse response = new CommentResponse( comment.getMemberId(), - comment.getArticle().getId(), + comment.getArticleId(), comment.getContent() ); diff --git a/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java b/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java index 2825372..fa8a4df 100644 --- a/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java +++ b/src/main/java/com/board/comment/controller/dto/reponse/PageCommentResponse.java @@ -16,7 +16,7 @@ public PageCommentResponse(Page page) { this( page.map(comment -> new CommentResponse( comment.getMemberId(), - comment.getArticle().getId(), + comment.getArticleId(), comment.getContent() )).getContent(), page.getNumber(), diff --git a/src/main/java/com/board/comment/domain/Comment.java b/src/main/java/com/board/comment/domain/Comment.java index 168da92..7e95f64 100644 --- a/src/main/java/com/board/comment/domain/Comment.java +++ b/src/main/java/com/board/comment/domain/Comment.java @@ -1,16 +1,12 @@ package com.board.comment.domain; -import com.board.article.domain.Article; import com.board.comment.exception.CommentErrorCode; import com.board.comment.exception.CommentException; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; @@ -28,17 +24,16 @@ public class Comment { @Column private Long memberId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "articleId", nullable = false) - private Article article; + @Column + private Long articleId; @Column(nullable = false) private String content; - public Comment(Long memberId, Article article, String content) { + public Comment(Long memberId, Long articleId, String content) { this.memberId = memberId; - this.article = article; + this.articleId = articleId; this.content = content; } diff --git a/src/main/java/com/board/comment/loader/CommentDataLoader.java b/src/main/java/com/board/comment/loader/CommentDataLoader.java index 98913ab..ffdc7f0 100644 --- a/src/main/java/com/board/comment/loader/CommentDataLoader.java +++ b/src/main/java/com/board/comment/loader/CommentDataLoader.java @@ -1,7 +1,5 @@ package com.board.comment.loader; -import com.board.article.domain.Article; -import com.board.article.repository.ArticleRepository; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; import lombok.RequiredArgsConstructor; @@ -13,15 +11,12 @@ public class CommentDataLoader implements CommandLineRunner { private final CommentRepository repository; - private final ArticleRepository articleRepository; @Override public void run(String... args) throws Exception { - Article article1 = articleRepository.save(new Article(1L, "제목", "내용")); - Article article2 = articleRepository.save(new Article(1L, "제제목", "내내용")); - repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 1")); - repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 2")); - repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 1")); - repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); } } diff --git a/src/main/java/com/board/comment/repository/CommentRepository.java b/src/main/java/com/board/comment/repository/CommentRepository.java index 4cc3abf..ee74ee6 100644 --- a/src/main/java/com/board/comment/repository/CommentRepository.java +++ b/src/main/java/com/board/comment/repository/CommentRepository.java @@ -1,6 +1,7 @@ package com.board.comment.repository; import com.board.comment.domain.Comment; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -12,6 +13,8 @@ @Repository public interface CommentRepository extends JpaRepository { + List findByArticleId(Long articleId); + Page findByArticleIdAndIdLessThanOrderByIdDesc(Long articleId, Long lastId, Pageable pageable); Page findAllByMemberId(Long memberId, Pageable pageable); diff --git a/src/main/java/com/board/comment/service/CommentService.java b/src/main/java/com/board/comment/service/CommentService.java index edeced9..cc297ab 100644 --- a/src/main/java/com/board/comment/service/CommentService.java +++ b/src/main/java/com/board/comment/service/CommentService.java @@ -1,12 +1,11 @@ package com.board.comment.service; -import com.board.article.domain.Article; -import com.board.article.service.ArticleService; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; import com.board.comment.exception.CommentException; import com.board.comment.repository.CommentRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -24,11 +23,9 @@ public class CommentService { private static final int NO_OFFSET_PAGING_PAGE = 0; private final CommentRepository commentRepository; - private final ArticleService articleService; public Comment createComment(String content, Long memberId, Long articleId) { - Article article = articleService.findArticle(articleId); - Comment comment = new Comment(memberId, article, content); + Comment comment = new Comment(memberId, articleId, content); commentRepository.save(comment); return comment; @@ -54,6 +51,12 @@ public void updateMemberIdToNull(Long memberId) { commentRepository.updateMemberIdToNull(memberId); } + public void deleteCommentByArticle(Long articleId) { + List comment = commentRepository.findByArticleId(articleId); + + commentRepository.deleteAll(comment); + } + @Transactional(readOnly = true) public Page findArticleComments(Long articleId, Long lastId, int size) { Pageable commentPageable = PageRequest.of(NO_OFFSET_PAGING_PAGE, size, Sort.by(PAGE_SORT_DELIMITER).descending()); diff --git a/src/test/java/com/board/comment/controller/CommentControllerTest.java b/src/test/java/com/board/comment/controller/CommentControllerTest.java index 4d42d96..ecf069f 100644 --- a/src/test/java/com/board/comment/controller/CommentControllerTest.java +++ b/src/test/java/com/board/comment/controller/CommentControllerTest.java @@ -9,7 +9,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.board.article.domain.Article; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.service.CommentService; @@ -28,7 +27,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(CommentController.class) @@ -47,14 +45,10 @@ class CommentControllerTest { private AuthArgumentResolver authArgumentResolver; private Comment response; - private Article article; private Page commentPage; @BeforeEach void setUp() throws Exception { - article = new Article(1L, "제목", "내용"); - ReflectionTestUtils.setField(article, "id", 1L); - - response = new Comment(1L, article, "댓글 내용"); + response = new Comment(1L, 1L, "댓글 내용"); List responses = List.of(response); Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); @@ -116,7 +110,7 @@ void showMemberComments() throws Exception { void updateComment() throws Exception { // given CommentRequest request = new CommentRequest("수정된 댓글"); - Comment updatedResponse = new Comment(1L, article,"수정된 댓글"); + Comment updatedResponse = new Comment(1L, 1L,"수정된 댓글"); given(commentService.updateComment(any(), any(), any())).willReturn(updatedResponse); // when & then diff --git a/src/test/java/com/board/comment/loader/CommentTestDataLoader.java b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java index 7a7c9ff..dc92b0d 100644 --- a/src/test/java/com/board/comment/loader/CommentTestDataLoader.java +++ b/src/test/java/com/board/comment/loader/CommentTestDataLoader.java @@ -1,7 +1,5 @@ package com.board.comment.loader; -import com.board.article.domain.Article; -import com.board.article.repository.ArticleRepository; import com.board.comment.domain.Comment; import com.board.comment.repository.CommentRepository; import org.springframework.boot.CommandLineRunner; @@ -12,22 +10,18 @@ public class CommentTestDataLoader { private final CommentRepository repository; - private final ArticleRepository articleRepository; - public CommentTestDataLoader(CommentRepository repository, ArticleRepository articleRepository) { + public CommentTestDataLoader(CommentRepository repository) { this.repository = repository; - this.articleRepository = articleRepository; } @Bean public CommandLineRunner testData() { return args -> { - Article article1 = articleRepository.save(new Article(1L, "제목", "내용")); - Article article2 = articleRepository.save(new Article(1L, "제제목", "내내용")); - repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 1")); - repository.save(new Comment(1L, article1, "첫 번째 게시글 댓글 2")); - repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 1")); - repository.save(new Comment(1L, article2, "두 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 1L, "첫 번째 게시글 댓글 2")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 1")); + repository.save(new Comment(1L, 2L, "두 번째 게시글 댓글 2")); }; } } diff --git a/src/test/java/com/board/comment/service/CommentServiceTest.java b/src/test/java/com/board/comment/service/CommentServiceTest.java index de11e64..062422f 100644 --- a/src/test/java/com/board/comment/service/CommentServiceTest.java +++ b/src/test/java/com/board/comment/service/CommentServiceTest.java @@ -5,8 +5,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import com.board.article.domain.Article; -import com.board.article.service.ArticleService; import com.board.comment.controller.dto.request.CommentRequest; import com.board.comment.domain.Comment; import com.board.comment.exception.CommentErrorCode; @@ -27,7 +25,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @SuppressWarnings("NonAsciiCharacters") @@ -36,22 +33,16 @@ class CommentServiceTest { @Mock private CommentRepository commentRepository; - @Mock - private ArticleService articleService; - @InjectMocks private CommentService commentService; private Comment comment; private List comments; - private Article article; private Page commentPage; @BeforeEach void set() { - article = new Article(1L, "제목", "내용"); - ReflectionTestUtils.setField(article, "id", 1L); - comment = new Comment(1L, article, "첫 번째 게시글 댓글 1"); + comment = new Comment(1L, 1L, "첫 번째 게시글 댓글 1"); comments = List.of(comment); Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); commentPage = new PageImpl<>(comments, pageable, comments.size()); @@ -69,15 +60,14 @@ void createComment() { Long articleId = 1L; given(commentRepository.save(any(Comment.class))).willReturn(comment); - given(articleService.findArticle(articleId)).willReturn(article); // when Comment response = commentService.createComment(request.content(), memberId, articleId); // then assertThat(response) - .extracting(Comment::getContent, Comment::getMemberId, Comment::getArticle) - .containsExactly("첫 번째 게시글 댓글1", memberId, article); + .extracting(Comment::getContent, Comment::getMemberId, Comment::getArticleId) + .containsExactly("첫 번째 게시글 댓글1", memberId, articleId); } @Test