From ab84e4d4bbc38a661df3d9c2c7e1ed2beb8477d3 Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:28:41 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=94=A5=20remove:=20=ED=95=99=EC=8A=B5?= =?UTF-8?q?=EC=9A=A9=20String=20=EB=8D=94=EB=AF=B8=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC/=EC=84=9C=EB=B9=84=EC=8A=A4/DTO=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blog/controller/StringController.java | 23 ------------------- .../com/example/blog/dto/StringRequest.java | 11 --------- .../com/example/blog/dto/StringResponse.java | 16 ------------- .../example/blog/service/StringService.java | 12 ---------- 4 files changed, 62 deletions(-) delete mode 100644 jihoonkang/src/main/java/com/example/blog/controller/StringController.java delete mode 100644 jihoonkang/src/main/java/com/example/blog/dto/StringRequest.java delete mode 100644 jihoonkang/src/main/java/com/example/blog/dto/StringResponse.java delete mode 100644 jihoonkang/src/main/java/com/example/blog/service/StringService.java diff --git a/jihoonkang/src/main/java/com/example/blog/controller/StringController.java b/jihoonkang/src/main/java/com/example/blog/controller/StringController.java deleted file mode 100644 index 8de7e98..0000000 --- a/jihoonkang/src/main/java/com/example/blog/controller/StringController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.blog.controller; - -import com.example.blog.dto.ApiResponse; -import com.example.blog.dto.StringRequest; -import com.example.blog.dto.StringResponse; -import com.example.blog.service.StringService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -public class StringController { - - private final StringService stringService; - - @PostMapping("/api/v1/strings/repeat") - public ApiResponse repeat(@RequestBody @Valid StringRequest request) { - return ApiResponse.success(stringService.repeat(request.getValue())); - } -} diff --git a/jihoonkang/src/main/java/com/example/blog/dto/StringRequest.java b/jihoonkang/src/main/java/com/example/blog/dto/StringRequest.java deleted file mode 100644 index 20d035d..0000000 --- a/jihoonkang/src/main/java/com/example/blog/dto/StringRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.blog.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Getter; - -@Getter -public class StringRequest { - - @NotBlank(message = "value는 필수 입력값입니다.") - private String value; -} diff --git a/jihoonkang/src/main/java/com/example/blog/dto/StringResponse.java b/jihoonkang/src/main/java/com/example/blog/dto/StringResponse.java deleted file mode 100644 index 2f786de..0000000 --- a/jihoonkang/src/main/java/com/example/blog/dto/StringResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.blog.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class StringResponse { - - @JsonProperty("string_one") - private String stringOne; - - @JsonProperty("string_two") - private String stringTwo; -} diff --git a/jihoonkang/src/main/java/com/example/blog/service/StringService.java b/jihoonkang/src/main/java/com/example/blog/service/StringService.java deleted file mode 100644 index 4a8b0ee..0000000 --- a/jihoonkang/src/main/java/com/example/blog/service/StringService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.blog.service; - -import com.example.blog.dto.StringResponse; -import org.springframework.stereotype.Service; - -@Service -public class StringService { - - public StringResponse repeat(String value) { - return new StringResponse(value, value); - } -} From b29ac67f2ad7e5f59c9cd1a2a44eef4d24599272 Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:28:46 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(global):=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=9D=91=EB=8B=B5/=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiResponse를 dto에서 global/response로 이동 - ErrorCode enum 기반 BusinessException 도입 - BusinessException, MissingRequestHeader, TypeMismatch 등 핸들러 추가 --- .../controller/GlobalExceptionHandler.java | 22 -------- .../com/example/blog/dto/ApiResponse.java | 21 -------- .../global/exception/BusinessException.java | 14 ++++++ .../blog/global/exception/ErrorCode.java | 25 ++++++++++ .../exception/GlobalExceptionHandler.java | 50 +++++++++++++++++++ .../global/exception/NotFoundException.java | 8 +++ .../blog/global/response/ApiResponse.java | 21 ++++++++ 7 files changed, 118 insertions(+), 43 deletions(-) delete mode 100644 jihoonkang/src/main/java/com/example/blog/controller/GlobalExceptionHandler.java delete mode 100644 jihoonkang/src/main/java/com/example/blog/dto/ApiResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/global/exception/BusinessException.java create mode 100644 jihoonkang/src/main/java/com/example/blog/global/exception/ErrorCode.java create mode 100644 jihoonkang/src/main/java/com/example/blog/global/exception/GlobalExceptionHandler.java create mode 100644 jihoonkang/src/main/java/com/example/blog/global/exception/NotFoundException.java create mode 100644 jihoonkang/src/main/java/com/example/blog/global/response/ApiResponse.java diff --git a/jihoonkang/src/main/java/com/example/blog/controller/GlobalExceptionHandler.java b/jihoonkang/src/main/java/com/example/blog/controller/GlobalExceptionHandler.java deleted file mode 100644 index c3f7f28..0000000 --- a/jihoonkang/src/main/java/com/example/blog/controller/GlobalExceptionHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.blog.controller; - -import com.example.blog.dto.ApiResponse; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(MethodArgumentNotValidException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ApiResponse handleValidationException(MethodArgumentNotValidException e) { - String message = e.getBindingResult().getFieldErrors().stream() - .findFirst() - .map(error -> error.getDefaultMessage()) - .orElse("잘못된 요청입니다."); - return ApiResponse.error(message); - } -} diff --git a/jihoonkang/src/main/java/com/example/blog/dto/ApiResponse.java b/jihoonkang/src/main/java/com/example/blog/dto/ApiResponse.java deleted file mode 100644 index 7fd214c..0000000 --- a/jihoonkang/src/main/java/com/example/blog/dto/ApiResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.blog.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class ApiResponse { - - private String status; - private String message; - private T data; - - public static ApiResponse success(T data) { - return new ApiResponse<>("success", "요청이 성공적으로 처리되었습니다.", data); - } - - public static ApiResponse error(String message) { - return new ApiResponse<>("error", message, null); - } -} diff --git a/jihoonkang/src/main/java/com/example/blog/global/exception/BusinessException.java b/jihoonkang/src/main/java/com/example/blog/global/exception/BusinessException.java new file mode 100644 index 0000000..bb23f48 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/global/exception/BusinessException.java @@ -0,0 +1,14 @@ +package com.example.blog.global.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/global/exception/ErrorCode.java b/jihoonkang/src/main/java/com/example/blog/global/exception/ErrorCode.java new file mode 100644 index 0000000..061aaff --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/global/exception/ErrorCode.java @@ -0,0 +1,25 @@ +package com.example.blog.global.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "사용자를 찾을 수 없습니다."), + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "U002", "이미 사용 중인 이메일입니다."), + + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "게시글을 찾을 수 없습니다."), + + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "C001", "댓글을 찾을 수 없습니다."), + + FORBIDDEN_ACCESS(HttpStatus.FORBIDDEN, "A001", "접근 권한이 없습니다."), + + INVALID_REPORT_TARGET(HttpStatus.BAD_REQUEST, "R001", "유효하지 않은 신고 대상입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/jihoonkang/src/main/java/com/example/blog/global/exception/GlobalExceptionHandler.java b/jihoonkang/src/main/java/com/example/blog/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..8a36533 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package com.example.blog.global.exception; + +import com.example.blog.global.response.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +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 org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusinessException(BusinessException e) { + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ApiResponse.error(e.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getDefaultMessage()) + .orElse("잘못된 요청입니다."); + return ResponseEntity.badRequest().body(ApiResponse.error(message)); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleHttpMessageNotReadable(HttpMessageNotReadableException e) { + return ResponseEntity.badRequest().body(ApiResponse.error("요청 본문을 읽을 수 없습니다.")); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handleTypeMismatch(MethodArgumentTypeMismatchException e) { + return ResponseEntity.badRequest().body(ApiResponse.error("요청 파라미터 타입이 올바르지 않습니다.")); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity> handleMissingHeader(MissingRequestHeaderException e) { + return ResponseEntity.badRequest().body(ApiResponse.error(e.getHeaderName() + " 헤더가 필요합니다.")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + return ResponseEntity.internalServerError().body(ApiResponse.error("서버 내부 오류가 발생했습니다.")); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/global/exception/NotFoundException.java b/jihoonkang/src/main/java/com/example/blog/global/exception/NotFoundException.java new file mode 100644 index 0000000..0782ac0 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/global/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package com.example.blog.global.exception; + +public class NotFoundException extends BusinessException { + + public NotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/global/response/ApiResponse.java b/jihoonkang/src/main/java/com/example/blog/global/response/ApiResponse.java new file mode 100644 index 0000000..7590d7f --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/global/response/ApiResponse.java @@ -0,0 +1,21 @@ +package com.example.blog.global.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ApiResponse { + + private String status; + private String message; + private T data; + + public static ApiResponse success(T data) { + return new ApiResponse<>("success", "요청이 성공적으로 처리되었습니다.", data); + } + + public static ApiResponse error(String message) { + return new ApiResponse<>("error", message, null); + } +} From 2a75954e0710f11e159eeb4354b60770d01f82ab Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:28:50 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20feat(user):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20CRUD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User 엔티티에 정적 팩토리/update 메서드 추가 - UserRepository, UserService, UserController 작성 - 이메일 중복 검사, Bean Validation 적용 --- .../user/controller/UserController.java | 44 +++++++++++++++++ .../domain/user/dto/UserCreateRequest.java | 25 ++++++++++ .../blog/domain/user/dto/UserResponse.java | 26 ++++++++++ .../domain/user/dto/UserUpdateRequest.java | 13 +++++ .../example/blog/domain/user/entity/User.java | 18 +++++++ .../user/repository/UserRepository.java | 9 ++++ .../blog/domain/user/service/UserService.java | 49 +++++++++++++++++++ 7 files changed, 184 insertions(+) create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/controller/UserController.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserCreateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserUpdateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/repository/UserRepository.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/user/service/UserService.java diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/controller/UserController.java b/jihoonkang/src/main/java/com/example/blog/domain/user/controller/UserController.java new file mode 100644 index 0000000..a1851f2 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/controller/UserController.java @@ -0,0 +1,44 @@ +package com.example.blog.domain.user.controller; + +import com.example.blog.domain.user.dto.UserCreateRequest; +import com.example.blog.domain.user.dto.UserResponse; +import com.example.blog.domain.user.dto.UserUpdateRequest; +import com.example.blog.domain.user.service.UserService; +import com.example.blog.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create(@RequestBody @Valid UserCreateRequest request) { + return ApiResponse.success(userService.create(request)); + } + + @GetMapping("/{userId}") + public ApiResponse findById(@PathVariable Long userId) { + return ApiResponse.success(userService.findById(userId)); + } + + @PatchMapping("/{userId}") + public ApiResponse update( + @PathVariable Long userId, + @RequestBody @Valid UserUpdateRequest request + ) { + return ApiResponse.success(userService.update(userId, request)); + } + + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable Long userId) { + userService.delete(userId); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserCreateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserCreateRequest.java new file mode 100644 index 0000000..c0490e4 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserCreateRequest.java @@ -0,0 +1,25 @@ +package com.example.blog.domain.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UserCreateRequest( + + @NotBlank(message = "이름은 필수입니다.") + @Size(max = 50, message = "이름은 50자 이하여야 합니다.") + String username, + + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + @Size(max = 100, message = "이메일은 100자 이하여야 합니다.") + String email, + + @NotBlank(message = "비밀번호는 필수입니다.") + @Size(max = 200, message = "비밀번호는 200자 이하여야 합니다.") + String password, + + @Size(max = 200, message = "프로필 URL은 200자 이하여야 합니다.") + String profileUrl + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserResponse.java b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserResponse.java new file mode 100644 index 0000000..6b5cb58 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserResponse.java @@ -0,0 +1,26 @@ +package com.example.blog.domain.user.dto; + +import com.example.blog.domain.user.entity.User; + +import java.time.LocalDateTime; + +public record UserResponse( + Long userId, + String username, + String email, + String profileUrl, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + + public static UserResponse from(User user) { + return new UserResponse( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getProfileUrl(), + user.getCreatedAt(), + user.getUpdatedAt() + ); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserUpdateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserUpdateRequest.java new file mode 100644 index 0000000..7f49903 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/dto/UserUpdateRequest.java @@ -0,0 +1,13 @@ +package com.example.blog.domain.user.dto; + +import jakarta.validation.constraints.Size; + +public record UserUpdateRequest( + + @Size(max = 50, message = "이름은 50자 이하여야 합니다.") + String username, + + @Size(max = 200, message = "프로필 URL은 200자 이하여야 합니다.") + String profileUrl + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/entity/User.java b/jihoonkang/src/main/java/com/example/blog/domain/user/entity/User.java index f9a0da7..883264d 100644 --- a/jihoonkang/src/main/java/com/example/blog/domain/user/entity/User.java +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/entity/User.java @@ -39,4 +39,22 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user") private List comments = new ArrayList<>(); + + public static User of(String username, String email, String password, String profileUrl) { + User user = new User(); + user.username = username; + user.email = email; + user.password = password; + user.profileUrl = profileUrl; + return user; + } + + public void update(String username, String profileUrl) { + if (username != null) { + this.username = username; + } + if (profileUrl != null) { + this.profileUrl = profileUrl; + } + } } diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/repository/UserRepository.java b/jihoonkang/src/main/java/com/example/blog/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..aa159bb --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.example.blog.domain.user.repository; + +import com.example.blog.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + boolean existsByEmail(String email); +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/user/service/UserService.java b/jihoonkang/src/main/java/com/example/blog/domain/user/service/UserService.java new file mode 100644 index 0000000..fcf6542 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/user/service/UserService.java @@ -0,0 +1,49 @@ +package com.example.blog.domain.user.service; + +import com.example.blog.domain.user.dto.UserCreateRequest; +import com.example.blog.domain.user.dto.UserResponse; +import com.example.blog.domain.user.dto.UserUpdateRequest; +import com.example.blog.domain.user.entity.User; +import com.example.blog.domain.user.repository.UserRepository; +import com.example.blog.global.exception.BusinessException; +import com.example.blog.global.exception.ErrorCode; +import com.example.blog.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class UserService { + + private final UserRepository userRepository; + + public UserResponse create(UserCreateRequest request) { + if (userRepository.existsByEmail(request.email())) { + throw new BusinessException(ErrorCode.EMAIL_ALREADY_EXISTS); + } + User user = User.of(request.username(), request.email(), request.password(), request.profileUrl()); + return UserResponse.from(userRepository.save(user)); + } + + @Transactional(readOnly = true) + public UserResponse findById(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + return UserResponse.from(user); + } + + public UserResponse update(Long userId, UserUpdateRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + user.update(request.username(), request.profileUrl()); + return UserResponse.from(user); + } + + public void delete(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + userRepository.delete(user); + } +} From 66624c4aaf75c679df59cb4edc31a593fe27ff68 Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:29:02 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20feat(post):=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20CRUD=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Post 엔티티에 정적 팩토리/update 메서드 추가 - PostRepository, PostService, PostController 작성 - status/페이지네이션 필터, 작성자 권한 검증 --- .../post/controller/PostController.java | 61 ++++++++++++++ .../domain/post/dto/PostCreateRequest.java | 16 ++++ .../domain/post/dto/PostListResponse.java | 10 +++ .../blog/domain/post/dto/PostResponse.java | 30 +++++++ .../domain/post/dto/PostUpdateRequest.java | 14 ++++ .../example/blog/domain/post/entity/Post.java | 21 +++++ .../post/repository/PostRepository.java | 11 +++ .../blog/domain/post/service/PostService.java | 79 +++++++++++++++++++ 8 files changed, 242 insertions(+) create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/controller/PostController.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostCreateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostListResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostUpdateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/repository/PostRepository.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/post/service/PostService.java diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/controller/PostController.java b/jihoonkang/src/main/java/com/example/blog/domain/post/controller/PostController.java new file mode 100644 index 0000000..be6fd78 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/controller/PostController.java @@ -0,0 +1,61 @@ +package com.example.blog.domain.post.controller; + +import com.example.blog.domain.post.dto.PostCreateRequest; +import com.example.blog.domain.post.dto.PostListResponse; +import com.example.blog.domain.post.dto.PostResponse; +import com.example.blog.domain.post.dto.PostUpdateRequest; +import com.example.blog.domain.post.service.PostService; +import com.example.blog.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create( + @RequestHeader("X-User-Id") Long userId, + @RequestBody @Valid PostCreateRequest request + ) { + return ApiResponse.success(postService.create(userId, request)); + } + + @GetMapping + public ApiResponse findAll( + @RequestParam(required = false) String status, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + return ApiResponse.success(postService.findAll(status, page, size)); + } + + @GetMapping("/{postId}") + public ApiResponse findById(@PathVariable Long postId) { + return ApiResponse.success(postService.findById(postId)); + } + + @PatchMapping("/{postId}") + public ApiResponse update( + @RequestHeader("X-User-Id") Long userId, + @PathVariable Long postId, + @RequestBody @Valid PostUpdateRequest request + ) { + return ApiResponse.success(postService.update(userId, postId, request)); + } + + @DeleteMapping("/{postId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete( + @RequestHeader("X-User-Id") Long userId, + @PathVariable Long postId + ) { + postService.delete(userId, postId); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostCreateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostCreateRequest.java new file mode 100644 index 0000000..6cdff46 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostCreateRequest.java @@ -0,0 +1,16 @@ +package com.example.blog.domain.post.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record PostCreateRequest( + + @NotBlank(message = "제목은 필수입니다.") + @Size(max = 200, message = "제목은 200자 이하여야 합니다.") + String title, + + String content, + + String status + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostListResponse.java b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostListResponse.java new file mode 100644 index 0000000..0ea78fc --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostListResponse.java @@ -0,0 +1,10 @@ +package com.example.blog.domain.post.dto; + +import java.util.List; + +public record PostListResponse( + List items, + int page, + int size, + long totalElements +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostResponse.java b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostResponse.java new file mode 100644 index 0000000..868cb32 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostResponse.java @@ -0,0 +1,30 @@ +package com.example.blog.domain.post.dto; + +import com.example.blog.domain.post.entity.Post; + +import java.time.LocalDateTime; + +public record PostResponse( + Long postId, + Long userId, + String username, + String title, + String content, + String status, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + + public static PostResponse from(Post post) { + return new PostResponse( + post.getId(), + post.getUser().getId(), + post.getUser().getUsername(), + post.getTitle(), + post.getContent(), + post.getStatus(), + post.getCreatedAt(), + post.getUpdatedAt() + ); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostUpdateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostUpdateRequest.java new file mode 100644 index 0000000..edd4d60 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/dto/PostUpdateRequest.java @@ -0,0 +1,14 @@ +package com.example.blog.domain.post.dto; + +import jakarta.validation.constraints.Size; + +public record PostUpdateRequest( + + @Size(max = 200, message = "제목은 200자 이하여야 합니다.") + String title, + + String content, + + String status + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/entity/Post.java b/jihoonkang/src/main/java/com/example/blog/domain/post/entity/Post.java index ac155a2..81dd305 100644 --- a/jihoonkang/src/main/java/com/example/blog/domain/post/entity/Post.java +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/entity/Post.java @@ -37,4 +37,25 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private List comments = new ArrayList<>(); + + public static Post of(User user, String title, String content, String status) { + Post post = new Post(); + post.user = user; + post.title = title; + post.content = content; + post.status = status != null ? status : "PUBLISHED"; + return post; + } + + public void update(String title, String content, String status) { + if (title != null) { + this.title = title; + } + if (content != null) { + this.content = content; + } + if (status != null) { + this.status = status; + } + } } diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/repository/PostRepository.java b/jihoonkang/src/main/java/com/example/blog/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..d7789a9 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/repository/PostRepository.java @@ -0,0 +1,11 @@ +package com.example.blog.domain.post.repository; + +import com.example.blog.domain.post.entity.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + + Page findByStatus(String status, Pageable pageable); +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/post/service/PostService.java b/jihoonkang/src/main/java/com/example/blog/domain/post/service/PostService.java new file mode 100644 index 0000000..3260e95 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/post/service/PostService.java @@ -0,0 +1,79 @@ +package com.example.blog.domain.post.service; + +import com.example.blog.domain.post.dto.PostCreateRequest; +import com.example.blog.domain.post.dto.PostListResponse; +import com.example.blog.domain.post.dto.PostResponse; +import com.example.blog.domain.post.dto.PostUpdateRequest; +import com.example.blog.domain.post.entity.Post; +import com.example.blog.domain.post.repository.PostRepository; +import com.example.blog.domain.user.entity.User; +import com.example.blog.domain.user.repository.UserRepository; +import com.example.blog.global.exception.BusinessException; +import com.example.blog.global.exception.ErrorCode; +import com.example.blog.global.exception.NotFoundException; +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; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostService { + + private final PostRepository postRepository; + private final UserRepository userRepository; + + public PostResponse create(Long userId, PostCreateRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + Post post = Post.of(user, request.title(), request.content(), request.status()); + return PostResponse.from(postRepository.save(post)); + } + + @Transactional(readOnly = true) + public PostListResponse findAll(String status, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending()); + Page posts; + if (status != null && !status.isBlank()) { + posts = postRepository.findByStatus(status, pageable); + } else { + posts = postRepository.findAll(pageable); + } + List items = posts.getContent().stream() + .map(PostResponse::from) + .toList(); + return new PostListResponse(items, page, size, posts.getTotalElements()); + } + + @Transactional(readOnly = true) + public PostResponse findById(Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + return PostResponse.from(post); + } + + public PostResponse update(Long userId, Long postId, PostUpdateRequest request) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + if (!post.getUser().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_ACCESS); + } + post.update(request.title(), request.content(), request.status()); + return PostResponse.from(post); + } + + public void delete(Long userId, Long postId) { + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + if (!post.getUser().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_ACCESS); + } + postRepository.delete(post); + } +} From 57214a663666ba259a3299fd96a28cd1a7eba83d Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:29:06 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20feat(comment):=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80/=EB=8C=80=EB=8C=93=EA=B8=80=20CRUD=20API=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 엔티티에 정적 팩토리/update 메서드 추가 - CommentRepository, CommentService, CommentController 작성 - 1뎁스 대댓글 제한, 작성자 권한 검증 --- .../comment/controller/CommentController.java | 53 +++++++++++++ .../comment/dto/CommentCreateRequest.java | 12 +++ .../domain/comment/dto/CommentResponse.java | 34 ++++++++ .../comment/dto/CommentUpdateRequest.java | 10 +++ .../blog/domain/comment/entity/Comment.java | 13 +++ .../comment/repository/CommentRepository.java | 11 +++ .../comment/service/CommentService.java | 79 +++++++++++++++++++ 7 files changed, 212 insertions(+) create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/controller/CommentController.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentCreateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentUpdateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/repository/CommentRepository.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/comment/service/CommentService.java diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/controller/CommentController.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..6e91e42 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/controller/CommentController.java @@ -0,0 +1,53 @@ +package com.example.blog.domain.comment.controller; + +import com.example.blog.domain.comment.dto.CommentCreateRequest; +import com.example.blog.domain.comment.dto.CommentResponse; +import com.example.blog.domain.comment.dto.CommentUpdateRequest; +import com.example.blog.domain.comment.service.CommentService; +import com.example.blog.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + + @PostMapping("/api/v1/posts/{postId}/comments") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create( + @RequestHeader("X-User-Id") Long userId, + @PathVariable Long postId, + @RequestBody @Valid CommentCreateRequest request + ) { + return ApiResponse.success(commentService.create(userId, postId, request)); + } + + @GetMapping("/api/v1/posts/{postId}/comments") + public ApiResponse> findByPostId(@PathVariable Long postId) { + return ApiResponse.success(commentService.findByPostId(postId)); + } + + @PatchMapping("/api/v1/comments/{commentId}") + public ApiResponse update( + @RequestHeader("X-User-Id") Long userId, + @PathVariable Long commentId, + @RequestBody @Valid CommentUpdateRequest request + ) { + return ApiResponse.success(commentService.update(userId, commentId, request)); + } + + @DeleteMapping("/api/v1/comments/{commentId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete( + @RequestHeader("X-User-Id") Long userId, + @PathVariable Long commentId + ) { + commentService.delete(userId, commentId); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentCreateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentCreateRequest.java new file mode 100644 index 0000000..efc94cb --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentCreateRequest.java @@ -0,0 +1,12 @@ +package com.example.blog.domain.comment.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CommentCreateRequest( + + @NotBlank(message = "댓글 내용은 필수입니다.") + String content, + + Long parentCommentId + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentResponse.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentResponse.java new file mode 100644 index 0000000..d0ffa3e --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentResponse.java @@ -0,0 +1,34 @@ +package com.example.blog.domain.comment.dto; + +import com.example.blog.domain.comment.entity.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +public record CommentResponse( + Long commentId, + Long postId, + Long userId, + String username, + String content, + Long parentCommentId, + List replies, + LocalDateTime createdAt +) { + + public static CommentResponse from(Comment comment) { + List replies = comment.getReplies().stream() + .map(CommentResponse::from) + .toList(); + return new CommentResponse( + comment.getId(), + comment.getPost().getId(), + comment.getUser().getId(), + comment.getUser().getUsername(), + comment.getContent(), + comment.getParentComment() != null ? comment.getParentComment().getId() : null, + replies, + comment.getCreatedAt() + ); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentUpdateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentUpdateRequest.java new file mode 100644 index 0000000..7293fe1 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/dto/CommentUpdateRequest.java @@ -0,0 +1,10 @@ +package com.example.blog.domain.comment.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CommentUpdateRequest( + + @NotBlank(message = "댓글 내용은 필수입니다.") + String content + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/entity/Comment.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/entity/Comment.java index 9e1a7c8..6e4d218 100644 --- a/jihoonkang/src/main/java/com/example/blog/domain/comment/entity/Comment.java +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/entity/Comment.java @@ -39,4 +39,17 @@ public class Comment extends BaseEntity { @OneToMany(mappedBy = "parentComment") private List replies = new ArrayList<>(); + + public static Comment of(User user, Post post, String content, Comment parentComment) { + Comment comment = new Comment(); + comment.user = user; + comment.post = post; + comment.content = content; + comment.parentComment = parentComment; + return comment; + } + + public void update(String content) { + this.content = content; + } } diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/repository/CommentRepository.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..791682e --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/repository/CommentRepository.java @@ -0,0 +1,11 @@ +package com.example.blog.domain.comment.repository; + +import com.example.blog.domain.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + + List findByPost_IdAndParentCommentIsNull(Long postId); +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/comment/service/CommentService.java b/jihoonkang/src/main/java/com/example/blog/domain/comment/service/CommentService.java new file mode 100644 index 0000000..8295636 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/comment/service/CommentService.java @@ -0,0 +1,79 @@ +package com.example.blog.domain.comment.service; + +import com.example.blog.domain.comment.dto.CommentCreateRequest; +import com.example.blog.domain.comment.dto.CommentResponse; +import com.example.blog.domain.comment.dto.CommentUpdateRequest; +import com.example.blog.domain.comment.entity.Comment; +import com.example.blog.domain.comment.repository.CommentRepository; +import com.example.blog.domain.post.entity.Post; +import com.example.blog.domain.post.repository.PostRepository; +import com.example.blog.domain.user.entity.User; +import com.example.blog.domain.user.repository.UserRepository; +import com.example.blog.global.exception.BusinessException; +import com.example.blog.global.exception.ErrorCode; +import com.example.blog.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class CommentService { + + private final CommentRepository commentRepository; + private final PostRepository postRepository; + private final UserRepository userRepository; + + public CommentResponse create(Long userId, Long postId, CommentCreateRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + + Comment parentComment = null; + if (request.parentCommentId() != null) { + parentComment = commentRepository.findById(request.parentCommentId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.COMMENT_NOT_FOUND)); + if (!parentComment.getPost().getId().equals(postId)) { + throw new BusinessException(ErrorCode.INVALID_REPORT_TARGET); + } + if (parentComment.getParentComment() != null) { + throw new BusinessException(ErrorCode.INVALID_REPORT_TARGET); + } + } + + Comment comment = Comment.of(user, post, request.content(), parentComment); + return CommentResponse.from(commentRepository.save(comment)); + } + + @Transactional(readOnly = true) + public List findByPostId(Long postId) { + postRepository.findById(postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + return commentRepository.findByPost_IdAndParentCommentIsNull(postId).stream() + .map(CommentResponse::from) + .toList(); + } + + public CommentResponse update(Long userId, Long commentId, CommentUpdateRequest request) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException(ErrorCode.COMMENT_NOT_FOUND)); + if (!comment.getUser().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_ACCESS); + } + comment.update(request.content()); + return CommentResponse.from(comment); + } + + public void delete(Long userId, Long commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new NotFoundException(ErrorCode.COMMENT_NOT_FOUND)); + if (!comment.getUser().getId().equals(userId)) { + throw new BusinessException(ErrorCode.FORBIDDEN_ACCESS); + } + commentRepository.delete(comment); + } +} From bdc42c7f7716cfe7bd206c7b8b5e66069d55d872 Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:29:11 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20feat(report):=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=EB=93=B1=EB=A1=9D/=EC=A1=B0=ED=9A=8C=20API=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 - Report 엔티티에 정적 팩토리 메서드 추가 - ReportRepository, ReportService, ReportController 작성 - POST/COMMENT 대상 분기 처리 및 검증 --- .../report/controller/ReportController.java | 34 +++++++++++ .../report/dto/ReportCreateRequest.java | 17 ++++++ .../domain/report/dto/ReportResponse.java | 32 ++++++++++ .../blog/domain/report/entity/Report.java | 9 +++ .../report/repository/ReportRepository.java | 7 +++ .../domain/report/service/ReportService.java | 59 +++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/report/controller/ReportController.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportCreateRequest.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportResponse.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/report/repository/ReportRepository.java create mode 100644 jihoonkang/src/main/java/com/example/blog/domain/report/service/ReportService.java diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/controller/ReportController.java b/jihoonkang/src/main/java/com/example/blog/domain/report/controller/ReportController.java new file mode 100644 index 0000000..5b2c7bf --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/controller/ReportController.java @@ -0,0 +1,34 @@ +package com.example.blog.domain.report.controller; + +import com.example.blog.domain.report.dto.ReportCreateRequest; +import com.example.blog.domain.report.dto.ReportResponse; +import com.example.blog.domain.report.service.ReportService; +import com.example.blog.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/reports") +@RequiredArgsConstructor +public class ReportController { + + private final ReportService reportService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create( + @RequestHeader("X-User-Id") Long reporterId, + @RequestBody @Valid ReportCreateRequest request + ) { + return ApiResponse.success(reportService.create(reporterId, request)); + } + + @GetMapping + public ApiResponse> findAll() { + return ApiResponse.success(reportService.findAll()); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportCreateRequest.java b/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportCreateRequest.java new file mode 100644 index 0000000..a8aaaa0 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportCreateRequest.java @@ -0,0 +1,17 @@ +package com.example.blog.domain.report.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ReportCreateRequest( + + @NotBlank(message = "신고 대상 유형은 필수입니다.") + String targetType, + + @NotNull(message = "신고 대상 ID는 필수입니다.") + Long targetId, + + @NotBlank(message = "신고 사유는 필수입니다.") + String reason + +) {} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportResponse.java b/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportResponse.java new file mode 100644 index 0000000..79a20dd --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/dto/ReportResponse.java @@ -0,0 +1,32 @@ +package com.example.blog.domain.report.dto; + +import com.example.blog.domain.report.entity.Report; + +import java.time.LocalDateTime; + +public record ReportResponse( + Long reportId, + Long reporterId, + String reporterUsername, + String targetType, + Long targetId, + String reason, + LocalDateTime createdAt +) { + + public static ReportResponse from(Report report) { + String targetType = report.getPost() != null ? "POST" : "COMMENT"; + Long targetId = report.getPost() != null + ? report.getPost().getId() + : report.getComment().getId(); + return new ReportResponse( + report.getId(), + report.getReporter().getId(), + report.getReporter().getUsername(), + targetType, + targetId, + report.getReason(), + report.getCreatedAt() + ); + } +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/entity/Report.java b/jihoonkang/src/main/java/com/example/blog/domain/report/entity/Report.java index 2e7e54c..2f313f0 100644 --- a/jihoonkang/src/main/java/com/example/blog/domain/report/entity/Report.java +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/entity/Report.java @@ -34,4 +34,13 @@ public class Report extends BaseEntity { @Column(nullable = false) private String reason; + + public static Report of(User reporter, Post post, Comment comment, String reason) { + Report report = new Report(); + report.reporter = reporter; + report.post = post; + report.comment = comment; + report.reason = reason; + return report; + } } diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/repository/ReportRepository.java b/jihoonkang/src/main/java/com/example/blog/domain/report/repository/ReportRepository.java new file mode 100644 index 0000000..1488760 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/repository/ReportRepository.java @@ -0,0 +1,7 @@ +package com.example.blog.domain.report.repository; + +import com.example.blog.domain.report.entity.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { +} diff --git a/jihoonkang/src/main/java/com/example/blog/domain/report/service/ReportService.java b/jihoonkang/src/main/java/com/example/blog/domain/report/service/ReportService.java new file mode 100644 index 0000000..9b8ca85 --- /dev/null +++ b/jihoonkang/src/main/java/com/example/blog/domain/report/service/ReportService.java @@ -0,0 +1,59 @@ +package com.example.blog.domain.report.service; + +import com.example.blog.domain.comment.entity.Comment; +import com.example.blog.domain.comment.repository.CommentRepository; +import com.example.blog.domain.post.entity.Post; +import com.example.blog.domain.post.repository.PostRepository; +import com.example.blog.domain.report.dto.ReportCreateRequest; +import com.example.blog.domain.report.dto.ReportResponse; +import com.example.blog.domain.report.entity.Report; +import com.example.blog.domain.report.repository.ReportRepository; +import com.example.blog.domain.user.entity.User; +import com.example.blog.domain.user.repository.UserRepository; +import com.example.blog.global.exception.BusinessException; +import com.example.blog.global.exception.ErrorCode; +import com.example.blog.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class ReportService { + + private final ReportRepository reportRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final CommentRepository commentRepository; + + public ReportResponse create(Long reporterId, ReportCreateRequest request) { + User reporter = userRepository.findById(reporterId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + + Post post = null; + Comment comment = null; + + if ("POST".equalsIgnoreCase(request.targetType())) { + post = postRepository.findById(request.targetId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.POST_NOT_FOUND)); + } else if ("COMMENT".equalsIgnoreCase(request.targetType())) { + comment = commentRepository.findById(request.targetId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.COMMENT_NOT_FOUND)); + } else { + throw new BusinessException(ErrorCode.INVALID_REPORT_TARGET); + } + + Report report = Report.of(reporter, post, comment, request.reason()); + return ReportResponse.from(reportRepository.save(report)); + } + + @Transactional(readOnly = true) + public List findAll() { + return reportRepository.findAll().stream() + .map(ReportResponse::from) + .toList(); + } +} From d0d795376627596682628cc822c6efd13a959a3b Mon Sep 17 00:00:00 2001 From: theSnackOverflow Date: Tue, 7 Apr 2026 23:29:15 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=94=A7=20chore(config):=20Jackson=20s?= =?UTF-8?q?nake=5Fcase=20=EB=B0=8F=20Swagger=20UI=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jihoonkang/src/main/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jihoonkang/src/main/resources/application.properties b/jihoonkang/src/main/resources/application.properties index 423bef7..24a15a6 100644 --- a/jihoonkang/src/main/resources/application.properties +++ b/jihoonkang/src/main/resources/application.properties @@ -9,3 +9,7 @@ spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true spring.h2.console.enabled=true + +spring.jackson.property-naming-strategy=SNAKE_CASE + +springdoc.swagger-ui.path=/swagger-ui.html