From 09a8e4cbcc1a53380ac51fd50187d2cafd4708d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B5=90=ED=98=95?= Date: Tue, 7 Apr 2026 23:15:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat]=203=EC=A3=BC=EC=B0=A8=20=EC=A0=84?= =?UTF-8?q?=EC=97=AD=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC=EC=A1=B0?= =?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 --- .../common/exception/GeneralException.java | 12 ++++ .../exception/GlobalExceptionHandler.java | 70 +++++++++++++++++++ .../common/response/ErrorCode.java | 33 +++++++++ .../common/response/GlobalResponse.java | 46 ++++++++++++ .../common/response/SuccessCode.java | 21 ++++++ 5 files changed, 182 insertions(+) create mode 100644 src/main/java/com/example/leets_project/common/exception/GeneralException.java create mode 100644 src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/leets_project/common/response/ErrorCode.java create mode 100644 src/main/java/com/example/leets_project/common/response/GlobalResponse.java create mode 100644 src/main/java/com/example/leets_project/common/response/SuccessCode.java diff --git a/src/main/java/com/example/leets_project/common/exception/GeneralException.java b/src/main/java/com/example/leets_project/common/exception/GeneralException.java new file mode 100644 index 0000000..9282dbb --- /dev/null +++ b/src/main/java/com/example/leets_project/common/exception/GeneralException.java @@ -0,0 +1,12 @@ +package com.example.leets_project.common.exception; + +import com.example.leets_project.common.response.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException{ + + private ErrorCode errorCode; +} diff --git a/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java b/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..d1dc9b6 --- /dev/null +++ b/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,70 @@ +package com.example.leets_project.common.exception; + +import com.example.leets_project.common.response.ErrorCode; +import com.example.leets_project.common.response.GlobalResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(GeneralException.class) + public ResponseEntity handleGeneralException(GeneralException e) { + ErrorCode errorCode = e.getErrorCode(); + + return GlobalResponse.onFailure(errorCode); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException e) { + ErrorCode errorCode = ErrorCode.VALIDATION_FAILED; + + String errorMessage = e.getBindingResult() + .getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(",")); + + return GlobalResponse.onFailure(errorCode, errorMessage); + } + + @ExceptionHandler(com.fasterxml.jackson.core.JsonParseException.class) // JSON 파싱 오류 처리 + public ResponseEntity handleJsonParseException( + com.fasterxml.jackson.core.JsonParseException e) { + log.error("JSON 파싱 오류: {}", e.getMessage()); + return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED); + } + + @ExceptionHandler(com.fasterxml.jackson.databind.JsonMappingException.class) // JSON 매핑 오류 처리 + public ResponseEntity handleJsonMappingException( + com.fasterxml.jackson.databind.JsonMappingException e) { + log.error("JSON 매핑 오류: {}", e.getMessage()); + return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED); + } + + // X-USER-ID 헤더 누락 + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handleMissingHeader(MissingRequestHeaderException e) { + log.warn("필수 헤더 누락 - {}", e.getHeaderName()); + return GlobalResponse.onFailure(ErrorCode.UNAUTHORIZED); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception e) { + ErrorCode errorCode = + ErrorCode.INTERNAL_ERROR; + log.error("Unexpected Error Occured"); + log.error(e.getMessage(), e); + log.error(e.getClass().getSimpleName()); + + return GlobalResponse.onFailure(errorCode); + } +} diff --git a/src/main/java/com/example/leets_project/common/response/ErrorCode.java b/src/main/java/com/example/leets_project/common/response/ErrorCode.java new file mode 100644 index 0000000..7842820 --- /dev/null +++ b/src/main/java/com/example/leets_project/common/response/ErrorCode.java @@ -0,0 +1,33 @@ +package com.example.leets_project.common.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + + // COMMON + VALIDATION_FAILED(HttpStatus.BAD_REQUEST, "COMMON_4000", "유효하지 않은 값입니다."), + INVALID_INPUT(HttpStatus.BAD_REQUEST, "COMMON_4001", "잘못된 입력입니다."), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "COMMON_4002", "허용되지 않은 요청입니다."), + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_5000", "서버 오류입니다."), + + // AUTH + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH_4000", "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "AUTH_4001", "접근 권한이 없습니다."), + + // USER + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_4040", "사용자를 찾을 수 없습니다."), + + // POST + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_4040", "게시글을 찾을 수 없습니다."), + POST_INVALID(HttpStatus.BAD_REQUEST, "POST_4001", "게시글 입력값이 올바르지 않습니다."), + POST_FORBIDDEN(HttpStatus.FORBIDDEN, "POST_4003", "작성자만 수정/삭제할 수 있습니다."); + + private final HttpStatus status; + private final String code; + private final String message; + +} diff --git a/src/main/java/com/example/leets_project/common/response/GlobalResponse.java b/src/main/java/com/example/leets_project/common/response/GlobalResponse.java new file mode 100644 index 0000000..b408fb4 --- /dev/null +++ b/src/main/java/com/example/leets_project/common/response/GlobalResponse.java @@ -0,0 +1,46 @@ +package com.example.leets_project.common.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; + +@Getter +@RequiredArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class GlobalResponse { + + private final Boolean isSuccess; // 성공, 실패 여부 + + private final String code; // 상태 코드 + + private final String message; // 구체적인 메시지 + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final Object result; //반환할 객체 + + public static ResponseEntity onSuccess(SuccessCode successCode, Object result){ + return new ResponseEntity<>( + new GlobalResponse(true, successCode.getCode(), successCode.getMessage(), result), + successCode.getStatus()); + } + + public static ResponseEntity onSuccess(SuccessCode successCode){ + return new ResponseEntity<>( + new GlobalResponse(true, successCode.getCode(), successCode.getMessage(), null), + successCode.getStatus()); + } + + public static ResponseEntity onFailure(ErrorCode errorCode, Object result){ + return new ResponseEntity<>( + new GlobalResponse(false, errorCode.getCode(), errorCode.getMessage(), result), + errorCode.getStatus()); + } + + public static ResponseEntity onFailure(ErrorCode errorCode){ + return new ResponseEntity<>( + new GlobalResponse(false, errorCode.getCode(), errorCode.getMessage(), null), + errorCode.getStatus()); + } +} diff --git a/src/main/java/com/example/leets_project/common/response/SuccessCode.java b/src/main/java/com/example/leets_project/common/response/SuccessCode.java new file mode 100644 index 0000000..94b2dd5 --- /dev/null +++ b/src/main/java/com/example/leets_project/common/response/SuccessCode.java @@ -0,0 +1,21 @@ +package com.example.leets_project.common.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum SuccessCode { + + // POST + POST_LIST(HttpStatus.OK, "POST_2000", "게시글 목록 조회 성공"), + POST_DETAIL(HttpStatus.OK, "POST_2001", "게시글 조회 성공"), + POST_CREATE(HttpStatus.CREATED, "POST_2010", "게시글 생성 성공"), + POST_UPDATE(HttpStatus.OK, "POST_2002", "게시글 수정 성공"), + POST_DELETE(HttpStatus.OK, "POST_2003", "게시글 삭제 성공"); + + private final HttpStatus status; + private final String code; + private final String message; +} From 940fa6f3ee8cc1349b4fa79aa311436f2596dd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B5=90=ED=98=95?= Date: Wed, 8 Apr 2026 03:49:36 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat]=203=EC=A3=BC=EC=B0=A8=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20CRUD=20service,controller,dto=20=EB=B0=8F?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=93=B1=EB=A1=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 50 ++++---- .../common/response/ErrorCode.java | 3 +- .../common/response/SuccessCode.java | 6 +- .../leets_project/domain/comment/Comment.java | 6 +- .../domain/post/{ => entity}/Post.java | 19 ++- .../post/repository/PostRepository.java | 7 ++ .../domain/post/service/PostService.java | 111 ++++++++++++++++++ .../post/web/controller/PostController.java | 79 +++++++++++++ .../post/web/dto/PostCreateRequest.java | 22 ++++ .../post/web/dto/PostCreateResponse.java | 26 ++++ .../post/web/dto/PostDeleteResponse.java | 17 +++ .../post/web/dto/PostDetailResponse.java | 35 ++++++ .../domain/post/web/dto/PostListResponse.java | 34 ++++++ .../post/web/dto/PostListWrapperResponse.java | 20 ++++ .../post/web/dto/PostUpdateRequest.java | 20 ++++ .../post/web/dto/PostUpdateResponse.java | 32 +++++ .../leets_project/domain/user/User.java | 6 +- .../user/repository/UserRepository.java | 13 ++ .../domain/user/service/UserService.java | 41 +++++++ .../user/web/controller/UserController.java | 34 ++++++ .../user/web/dto/UserCreateRequest.java | 23 ++++ .../user/web/dto/UserCreateResponse.java | 24 ++++ 22 files changed, 582 insertions(+), 46 deletions(-) rename src/main/java/com/example/leets_project/domain/post/{ => entity}/Post.java (68%) create mode 100644 src/main/java/com/example/leets_project/domain/post/repository/PostRepository.java create mode 100644 src/main/java/com/example/leets_project/domain/post/service/PostService.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/controller/PostController.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateRequest.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostDeleteResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostDetailResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostListResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostListWrapperResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateRequest.java create mode 100644 src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateResponse.java create mode 100644 src/main/java/com/example/leets_project/domain/user/repository/UserRepository.java create mode 100644 src/main/java/com/example/leets_project/domain/user/service/UserService.java create mode 100644 src/main/java/com/example/leets_project/domain/user/web/controller/UserController.java create mode 100644 src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateRequest.java create mode 100644 src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateResponse.java diff --git a/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java b/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java index d1dc9b6..d093770 100644 --- a/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/leets_project/common/exception/GlobalExceptionHandler.java @@ -16,55 +16,47 @@ @Slf4j public class GlobalExceptionHandler { + // 커스텀 예외 @ExceptionHandler(GeneralException.class) public ResponseEntity handleGeneralException(GeneralException e) { - ErrorCode errorCode = e.getErrorCode(); - - return GlobalResponse.onFailure(errorCode); + log.warn("GeneralException: [{}] {}", e.getErrorCode().getCode(), e.getErrorCode().getMessage()); + return GlobalResponse.onFailure(e.getErrorCode()); } - + // Validation 에러 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException e) { - ErrorCode errorCode = ErrorCode.VALIDATION_FAILED; - - String errorMessage = e.getBindingResult() - .getAllErrors() + String errorDetail = e.getBindingResult() + .getFieldErrors() .stream() - .map(DefaultMessageSourceResolvable::getDefaultMessage) - .collect(Collectors.joining(",")); + .map(error -> "[" + error.getField() + "]: " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); - return GlobalResponse.onFailure(errorCode, errorMessage); + log.warn("Validation Failed: {}", errorDetail); + return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED, errorDetail); } - @ExceptionHandler(com.fasterxml.jackson.core.JsonParseException.class) // JSON 파싱 오류 처리 - public ResponseEntity handleJsonParseException( - com.fasterxml.jackson.core.JsonParseException e) { - log.error("JSON 파싱 오류: {}", e.getMessage()); - return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED); - } + // JSON 처리 에러 + @ExceptionHandler({com.fasterxml.jackson.core.JsonParseException.class, + com.fasterxml.jackson.databind.JsonMappingException.class}) + public ResponseEntity handleJsonException(Exception e) { + log.error("JSON 처리 오류: {}", e.getMessage()); - @ExceptionHandler(com.fasterxml.jackson.databind.JsonMappingException.class) // JSON 매핑 오류 처리 - public ResponseEntity handleJsonMappingException( - com.fasterxml.jackson.databind.JsonMappingException e) { - log.error("JSON 매핑 오류: {}", e.getMessage()); - return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED); + return GlobalResponse.onFailure(ErrorCode.VALIDATION_FAILED, "잘못된 JSON 형식입니다."); } // X-USER-ID 헤더 누락 @ExceptionHandler(MissingRequestHeaderException.class) public ResponseEntity handleMissingHeader(MissingRequestHeaderException e) { log.warn("필수 헤더 누락 - {}", e.getHeaderName()); - return GlobalResponse.onFailure(ErrorCode.UNAUTHORIZED); + + return GlobalResponse.onFailure(ErrorCode.UNAUTHORIZED, "필수 헤더가 누락되었습니다."); } + // 모든 예외 @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception e) { - ErrorCode errorCode = - ErrorCode.INTERNAL_ERROR; - log.error("Unexpected Error Occured"); - log.error(e.getMessage(), e); - log.error(e.getClass().getSimpleName()); + log.error("Unexpected Error", e); - return GlobalResponse.onFailure(errorCode); + return GlobalResponse.onFailure(ErrorCode.INTERNAL_ERROR); } } diff --git a/src/main/java/com/example/leets_project/common/response/ErrorCode.java b/src/main/java/com/example/leets_project/common/response/ErrorCode.java index 7842820..c753b90 100644 --- a/src/main/java/com/example/leets_project/common/response/ErrorCode.java +++ b/src/main/java/com/example/leets_project/common/response/ErrorCode.java @@ -20,7 +20,8 @@ public enum ErrorCode { // USER USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_4040", "사용자를 찾을 수 없습니다."), - + USER_EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "USER_4001", "이미 사용 중인 이메일입니다."), + USER_NICKNAME_ALREADY_EXISTS(HttpStatus.CONFLICT, "USER_4002", "이미 사용 중인 닉네임입니다."), // POST POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_4040", "게시글을 찾을 수 없습니다."), POST_INVALID(HttpStatus.BAD_REQUEST, "POST_4001", "게시글 입력값이 올바르지 않습니다."), diff --git a/src/main/java/com/example/leets_project/common/response/SuccessCode.java b/src/main/java/com/example/leets_project/common/response/SuccessCode.java index 94b2dd5..ad7132e 100644 --- a/src/main/java/com/example/leets_project/common/response/SuccessCode.java +++ b/src/main/java/com/example/leets_project/common/response/SuccessCode.java @@ -8,12 +8,14 @@ @AllArgsConstructor public enum SuccessCode { + // USER + USER_CREATE(HttpStatus.CREATED, "USER_2010", "유저 생성 성공"), // POST POST_LIST(HttpStatus.OK, "POST_2000", "게시글 목록 조회 성공"), POST_DETAIL(HttpStatus.OK, "POST_2001", "게시글 조회 성공"), - POST_CREATE(HttpStatus.CREATED, "POST_2010", "게시글 생성 성공"), POST_UPDATE(HttpStatus.OK, "POST_2002", "게시글 수정 성공"), - POST_DELETE(HttpStatus.OK, "POST_2003", "게시글 삭제 성공"); + POST_DELETE(HttpStatus.OK, "POST_2003", "게시글 삭제 성공"), + POST_CREATE(HttpStatus.CREATED, "POST_2010", "게시글 생성 성공"); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/leets_project/domain/comment/Comment.java b/src/main/java/com/example/leets_project/domain/comment/Comment.java index 820b5a1..a953d07 100644 --- a/src/main/java/com/example/leets_project/domain/comment/Comment.java +++ b/src/main/java/com/example/leets_project/domain/comment/Comment.java @@ -1,17 +1,13 @@ package com.example.leets_project.domain.comment; import com.example.leets_project.common.entity.BaseEntity; -import com.example.leets_project.domain.post.Post; +import com.example.leets_project.domain.post.entity.Post; import com.example.leets_project.domain.user.User; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; @Entity @Table(name = "comments") diff --git a/src/main/java/com/example/leets_project/domain/post/Post.java b/src/main/java/com/example/leets_project/domain/post/entity/Post.java similarity index 68% rename from src/main/java/com/example/leets_project/domain/post/Post.java rename to src/main/java/com/example/leets_project/domain/post/entity/Post.java index e9d8209..cca8311 100644 --- a/src/main/java/com/example/leets_project/domain/post/Post.java +++ b/src/main/java/com/example/leets_project/domain/post/entity/Post.java @@ -1,6 +1,8 @@ -package com.example.leets_project.domain.post; +package com.example.leets_project.domain.post.entity; import com.example.leets_project.common.entity.BaseEntity; +import com.example.leets_project.common.exception.GeneralException; +import com.example.leets_project.common.response.ErrorCode; import com.example.leets_project.domain.comment.Comment; import com.example.leets_project.domain.user.User; import jakarta.persistence.*; @@ -8,10 +10,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -49,4 +48,16 @@ public Post(User user, String title, String content, String description) { this.content = content; this.description = description; } + public void updatePost(String title, String content, String description, Long requesterId) { + validateOwner(requesterId); + this.title = title; + this.content = content; + this.description = description; + } + + public void validateOwner(Long userId) { + if (!this.user.getId().equals(userId)) { + throw new GeneralException(ErrorCode.POST_FORBIDDEN); + } + } } diff --git a/src/main/java/com/example/leets_project/domain/post/repository/PostRepository.java b/src/main/java/com/example/leets_project/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..9098941 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.example.leets_project.domain.post.repository; + +import com.example.leets_project.domain.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/leets_project/domain/post/service/PostService.java b/src/main/java/com/example/leets_project/domain/post/service/PostService.java new file mode 100644 index 0000000..9fe415a --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/service/PostService.java @@ -0,0 +1,111 @@ +package com.example.leets_project.domain.post.service; + +import com.example.leets_project.common.exception.GeneralException; +import com.example.leets_project.common.response.ErrorCode; +import com.example.leets_project.domain.post.entity.Post; +import com.example.leets_project.domain.post.repository.PostRepository; +import com.example.leets_project.domain.post.web.dto.*; +import com.example.leets_project.domain.user.User; +import com.example.leets_project.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class PostService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + + private static final int MIN_PAGE = 0; + private static final int MAX_PAGE = 10; + + // 1. 게시글 생성 + @Transactional + public PostCreateResponse createPost(Long currentUserId, PostCreateRequest request) { + // 사용자 존재 확인 + User user = findUserOrThrow(currentUserId); + + Post post = Post.builder() + .user(user) + .title(request.getTitle()) + .content(request.getContent()) + .description(request.getDescription()) + .build(); + + Post savedPost = postRepository.save(post); + + return PostCreateResponse.from(savedPost); + } + + // 2. 게시글 목록 조회 (페이징) + public Page getPosts(int page, int size) { + + validatePageRange(page); + PageRequest pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + + return postRepository.findAll(pageable) + .map(PostListResponse::from); + } + + // 3. 게시글 상세 조회 + public PostDetailResponse getPostDetail(Long postId) { + + Post post = findPostOrThrow(postId); + + return PostDetailResponse.from(post); + } + + // 4. 게시글 수정 + @Transactional + public PostUpdateResponse updatePost(Long postId, Long currentUserId, PostUpdateRequest request) { + + Post post = findPostOrThrow(postId); + + post.updatePost( + request.getTitle(), + request.getContent(), + request.getDescription(), + currentUserId + ); + + return PostUpdateResponse.from(post); + } + + // 5. 게시글 삭제 + @Transactional + public PostDeleteResponse deletePost(Long postId, Long currentUserId) { + + Post post = findPostOrThrow(postId); + + post.validateOwner(currentUserId); + + postRepository.delete(post); + + return PostDeleteResponse.of(postId); + } + + // 공통 검증 로직 + // 게시글 조회 + private Post findPostOrThrow(Long postId) { + return postRepository.findById(postId) + .orElseThrow(() -> new GeneralException(ErrorCode.POST_NOT_FOUND)); + } + + // 사용자 조회 + private User findUserOrThrow(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND)); + } + + private void validatePageRange(int page) { + if (page < MIN_PAGE || page > MAX_PAGE) { + throw new GeneralException(ErrorCode.POST_INVALID); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/leets_project/domain/post/web/controller/PostController.java b/src/main/java/com/example/leets_project/domain/post/web/controller/PostController.java new file mode 100644 index 0000000..077af18 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/controller/PostController.java @@ -0,0 +1,79 @@ +package com.example.leets_project.domain.post.web.controller; + +import com.example.leets_project.common.response.GlobalResponse; +import com.example.leets_project.common.response.SuccessCode; +import com.example.leets_project.domain.post.service.PostService; +import com.example.leets_project.domain.post.web.dto.*; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name ="POST API", description = "게시글 생성,조회,수정,삭제 관련 API ") +@RestController +@RequiredArgsConstructor +@RequestMapping("/posts") +public class PostController { + + private final PostService postService; + + // 게시물 생성 + @Operation(summary = "게시글 생성", description = "새로운 게시글을 등록합니다.") + @PostMapping + public ResponseEntity createPost(@RequestHeader("X-USER-ID") Long currentUserId, + @RequestBody @Valid PostCreateRequest request){ + + PostCreateResponse response = postService.createPost(currentUserId, request); + + return GlobalResponse.onSuccess(SuccessCode.POST_CREATE, response); + } + + // 2. 게시글 목록 조회 + @Operation(summary = "게시글 목록 조회", description = "게시글 목록을 페이징으로 조회합니다.") + @GetMapping + public ResponseEntity getPosts(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + + Page postPage = postService.getPosts(page, size); + PostListWrapperResponse response = PostListWrapperResponse.of(postPage); + + return GlobalResponse.onSuccess(SuccessCode.POST_LIST, response); + } + + // 3. 게시글 상세 조회 + @Operation(summary = "게시글 상세 조회", description = "특정 게시글의 상세 내용을 조회합니다.") + @GetMapping("/{postId}") + public ResponseEntity getPostDetail(@PathVariable Long postId) { + + PostDetailResponse response = postService.getPostDetail(postId); + + return GlobalResponse.onSuccess(SuccessCode.POST_DETAIL, response); + } + + // 4. 게시글 수정 + @Operation(summary = "게시글 수정", description = "게시글을 수정합니다.") + @PutMapping("/{postId}") + public ResponseEntity updatePost(@PathVariable Long postId, + @RequestHeader("X-USER-ID") Long currentUserId, + @RequestBody @Valid PostUpdateRequest request) { + + PostUpdateResponse response = postService.updatePost(postId, currentUserId, request); + + return GlobalResponse.onSuccess(SuccessCode.POST_UPDATE, response); + } + + // 5. 게시글 삭제 + @Operation(summary = "게시글 삭제", description = "게시글을 삭제합니다.") + @DeleteMapping("/{postId}") + public ResponseEntity deletePost(@PathVariable Long postId, + @RequestHeader("X-USER-ID") Long currentUserId) { + + PostDeleteResponse response = postService.deletePost(postId, currentUserId); + + return GlobalResponse.onSuccess(SuccessCode.POST_DELETE, response); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateRequest.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateRequest.java new file mode 100644 index 0000000..5d3957f --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateRequest.java @@ -0,0 +1,22 @@ +package com.example.leets_project.domain.post.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PostCreateRequest { + + private Long userId; + + @NotBlank(message = "제목은 필수입니다.") + private String title; + + @NotBlank(message = "내용은 필수입니다.") + private String content; + + private String description; +} \ No newline at end of file diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateResponse.java new file mode 100644 index 0000000..a2b3234 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostCreateResponse.java @@ -0,0 +1,26 @@ +package com.example.leets_project.domain.post.web.dto; + +import com.example.leets_project.domain.post.entity.Post; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class PostCreateResponse { + + private Long postId; + private String title; + private String authorNickname; + private LocalDateTime createdAt; + + public static PostCreateResponse from(Post post) { + return PostCreateResponse.builder() + .postId(post.getId()) + .title(post.getTitle()) + .authorNickname(post.getUser().getNickname()) + .createdAt(post.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostDeleteResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostDeleteResponse.java new file mode 100644 index 0000000..907f899 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostDeleteResponse.java @@ -0,0 +1,17 @@ +package com.example.leets_project.domain.post.web.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PostDeleteResponse { + + private Long postId; + + public static PostDeleteResponse of(Long postId) { + return PostDeleteResponse.builder() + .postId(postId) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostDetailResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostDetailResponse.java new file mode 100644 index 0000000..255184f --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostDetailResponse.java @@ -0,0 +1,35 @@ +package com.example.leets_project.domain.post.web.dto; + +import com.example.leets_project.domain.post.entity.Post; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class PostDetailResponse { + private Long postId; + private Long userId; + + private String title; + private String content; + private String description; + private String authorNickname; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static PostDetailResponse from(Post post) { + return PostDetailResponse.builder() + .postId(post.getId()) + .userId(post.getUser().getId()) + .title(post.getTitle()) + .content(post.getContent()) + .description(post.getDescription()) + .authorNickname(post.getUser().getNickname()) + .createdAt(post.getCreatedAt()) + .updatedAt(post.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostListResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostListResponse.java new file mode 100644 index 0000000..3f40d37 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostListResponse.java @@ -0,0 +1,34 @@ +package com.example.leets_project.domain.post.web.dto; + +import com.example.leets_project.domain.post.entity.Post; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class PostListResponse { + + private Long postId; + private Long userId; + + private String title; + private String description; + private String authorNickname; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static PostListResponse from(Post post) { + return PostListResponse.builder() + .postId(post.getId()) + .userId(post.getUser().getId()) + .title(post.getTitle()) + .description(post.getDescription()) + .authorNickname(post.getUser().getNickname()) + .createdAt(post.getCreatedAt()) + .updatedAt(post.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostListWrapperResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostListWrapperResponse.java new file mode 100644 index 0000000..062dc39 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostListWrapperResponse.java @@ -0,0 +1,20 @@ +package com.example.leets_project.domain.post.web.dto; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +public class PostListWrapperResponse { + + private List posts; + + public static PostListWrapperResponse of(Page page) { + return PostListWrapperResponse.builder() + .posts(page.getContent()) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateRequest.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateRequest.java new file mode 100644 index 0000000..cad5bcc --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateRequest.java @@ -0,0 +1,20 @@ +package com.example.leets_project.domain.post.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class PostUpdateRequest { + + @NotBlank(message = "제목은 필수입니다.") + private String title; + + @NotBlank(message = "내용은 필수입니다.") + private String content; + + private String description; +} diff --git a/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateResponse.java b/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateResponse.java new file mode 100644 index 0000000..fd4c3fd --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/post/web/dto/PostUpdateResponse.java @@ -0,0 +1,32 @@ +package com.example.leets_project.domain.post.web.dto; + +import com.example.leets_project.domain.post.entity.Post; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class PostUpdateResponse { + + private Long postId; + + private String title; + private String content; + private String description; + + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static PostUpdateResponse from(Post post) { + return PostUpdateResponse.builder() + .postId(post.getId()) + .title(post.getTitle()) + .content(post.getContent()) + .description(post.getDescription()) + .createdAt(post.getCreatedAt()) + .updatedAt(post.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/com/example/leets_project/domain/user/User.java b/src/main/java/com/example/leets_project/domain/user/User.java index d7f8037..232a366 100644 --- a/src/main/java/com/example/leets_project/domain/user/User.java +++ b/src/main/java/com/example/leets_project/domain/user/User.java @@ -2,17 +2,13 @@ import com.example.leets_project.common.entity.BaseEntity; import com.example.leets_project.domain.comment.Comment; -import com.example.leets_project.domain.post.Post; +import com.example.leets_project.domain.post.entity.Post; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.springframework.data.domain.Auditable; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/example/leets_project/domain/user/repository/UserRepository.java b/src/main/java/com/example/leets_project/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..71a9ed7 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/user/repository/UserRepository.java @@ -0,0 +1,13 @@ +package com.example.leets_project.domain.user.repository; + +import com.example.leets_project.domain.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + // 이메일 중복 확인 + boolean existsByEmail(String email); + + // 닉네임 중복 확인 + boolean existsByNickname(String nickname); +} diff --git a/src/main/java/com/example/leets_project/domain/user/service/UserService.java b/src/main/java/com/example/leets_project/domain/user/service/UserService.java new file mode 100644 index 0000000..49a07ee --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/user/service/UserService.java @@ -0,0 +1,41 @@ +package com.example.leets_project.domain.user.service; + +import com.example.leets_project.common.exception.GeneralException; +import com.example.leets_project.common.response.ErrorCode; +import com.example.leets_project.domain.user.User; +import com.example.leets_project.domain.user.repository.UserRepository; +import com.example.leets_project.domain.user.web.dto.UserCreateRequest; +import com.example.leets_project.domain.user.web.dto.UserCreateResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public UserCreateResponse createUser(UserCreateRequest request) { + // 1. 이메일 중복 검증 + if (userRepository.existsByEmail(request.getEmail())) { + throw new GeneralException(ErrorCode.USER_EMAIL_ALREADY_EXISTS); + } + // 2. 닉네임 중복 검증 + if (userRepository.existsByNickname(request.getNickname())) { + throw new GeneralException(ErrorCode.USER_NICKNAME_ALREADY_EXISTS); + } + + User user = User.builder() + .name(request.getName()) + .email(request.getEmail()) + .nickname(request.getNickname()) + .build(); + + User savedUser = userRepository.save(user); + + return UserCreateResponse.from(savedUser); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/leets_project/domain/user/web/controller/UserController.java b/src/main/java/com/example/leets_project/domain/user/web/controller/UserController.java new file mode 100644 index 0000000..8135555 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/user/web/controller/UserController.java @@ -0,0 +1,34 @@ +package com.example.leets_project.domain.user.web.controller; + +import com.example.leets_project.common.response.GlobalResponse; +import com.example.leets_project.common.response.SuccessCode; +import com.example.leets_project.domain.user.service.UserService; +import com.example.leets_project.domain.user.web.dto.UserCreateRequest; +import com.example.leets_project.domain.user.web.dto.UserCreateResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +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; + +@Tag(name ="USER API", description = "유저 생성 및 관리 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + + private final UserService userService; + + @Operation(summary = "유저 등록", description = "새로운 유저를 등록합니다.") + @PostMapping + public ResponseEntity createUser(@RequestBody @Valid UserCreateRequest request) { + + UserCreateResponse response = userService.createUser(request); + + return GlobalResponse.onSuccess(SuccessCode.USER_CREATE, response); + } +} diff --git a/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateRequest.java b/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateRequest.java new file mode 100644 index 0000000..b9996e3 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateRequest.java @@ -0,0 +1,23 @@ +package com.example.leets_project.domain.user.web.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class UserCreateRequest { + + @NotBlank(message = "이름은 필수 입력 값입니다.") + private String name; + + @NotBlank(message = "이메일은 필수 입력 값입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + private String email; + + @NotBlank(message = "닉네임은 필수 입력 값입니다.") + private String nickname; +} diff --git a/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateResponse.java b/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateResponse.java new file mode 100644 index 0000000..ce932e9 --- /dev/null +++ b/src/main/java/com/example/leets_project/domain/user/web/dto/UserCreateResponse.java @@ -0,0 +1,24 @@ +package com.example.leets_project.domain.user.web.dto; + +import com.example.leets_project.domain.user.User; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserCreateResponse { + + private Long id; + private String name; + private String email; + private String nickname; + + public static UserCreateResponse from(User user) { + return UserCreateResponse.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .nickname(user.getNickname()) + .build(); + } +} From cf8ef766c83268c4ef3e2e2d6b23af02b00a2925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B5=90=ED=98=95?= Date: Wed, 8 Apr 2026 04:21:34 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[feat]=203=EC=A3=BC=EC=B0=A8=20Validation?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8771984..9bc56fa 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-validation' // Swagger 의존성 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5' }