diff --git a/build.gradle b/build.gradle index 04dfb8ed..ceeb29f5 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,9 @@ dependencies { //Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + //Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + } subprojects { diff --git a/module-api/build.gradle b/module-api/build.gradle index 6bce34f1..428c16b3 100644 --- a/module-api/build.gradle +++ b/module-api/build.gradle @@ -28,6 +28,10 @@ dependencies { //Aop implementation 'org.springframework.boot:spring-boot-starter-aop' + + //Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' + } ext { @@ -39,4 +43,7 @@ dependencyManagement { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } +} +tasks.named('test') { + useJUnitPlatform() } \ No newline at end of file diff --git a/module-api/src/main/java/com/mile/controller/moim/MoimController.java b/module-api/src/main/java/com/mile/controller/moim/MoimController.java new file mode 100644 index 00000000..7a791bb1 --- /dev/null +++ b/module-api/src/main/java/com/mile/controller/moim/MoimController.java @@ -0,0 +1,30 @@ +package com.mile.controller.moim; + +import com.mile.dto.SuccessResponse; +import com.mile.exception.message.SuccessMessage; +import com.mile.moim.serivce.MoimService; +import com.mile.moim.serivce.dto.ContentListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/moim") +public class MoimController implements MoimControllerSwagger { + + private final MoimService moimService; + + @GetMapping("/{moimId}") + @Override + public SuccessResponse getTopicsFromMoim( + @PathVariable final Long moimId, + final Principal principal + ) { + return SuccessResponse.of(SuccessMessage.TOPIC_SEARCH_SUCCESS, moimService.getContentsFromMoim(moimId, Long.valueOf(principal.getName()))); + } +} diff --git a/module-api/src/main/java/com/mile/controller/moim/MoimControllerSwagger.java b/module-api/src/main/java/com/mile/controller/moim/MoimControllerSwagger.java new file mode 100644 index 00000000..34ebf734 --- /dev/null +++ b/module-api/src/main/java/com/mile/controller/moim/MoimControllerSwagger.java @@ -0,0 +1,34 @@ +package com.mile.controller.moim; + +import com.mile.dto.ErrorResponse; +import com.mile.dto.SuccessResponse; +import com.mile.moim.serivce.dto.ContentListResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PathVariable; + +import java.security.Principal; + +@Tag(name = "Moim", description = "모임 관련 API") +public interface MoimControllerSwagger { + @Operation(summary = "에디터 상단 글감 조회") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "글감 조회가 완료되었습니다."), + @ApiResponse(responseCode = "403", description = "해당 사용자는 모임에 접근 권한이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "해당 모임의 주제가 존재하지 않습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + } + ) + SuccessResponse getTopicsFromMoim( + @PathVariable final Long moimId, + final Principal principal + ); +} diff --git a/module-api/src/main/java/com/mile/controller/post/PostController.java b/module-api/src/main/java/com/mile/controller/post/PostController.java new file mode 100644 index 00000000..788c1736 --- /dev/null +++ b/module-api/src/main/java/com/mile/controller/post/PostController.java @@ -0,0 +1,38 @@ +package com.mile.controller.post; + +import com.mile.dto.SuccessResponse; +import com.mile.exception.message.SuccessMessage; +import com.mile.post.service.PostService; +import com.mile.post.service.dto.CommentCreateRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.security.Principal; + +@RestController +@RequestMapping("/api/post") +@RequiredArgsConstructor +public class PostController implements PostControllerSwagger{ + + private final PostService postService; + + @PostMapping("/{postId}/comment") + @Override + public SuccessResponse postComment( + @PathVariable final Long postId, + @Valid @RequestBody final CommentCreateRequest commentCreateRequest, + final Principal principal + ) { + postService.createCommentOnPost( + postId, + Long.valueOf(principal.getName()), + commentCreateRequest + ); + return SuccessResponse.of(SuccessMessage.COMMENT_CREATE_SUCCESS); + } +} diff --git a/module-api/src/main/java/com/mile/controller/post/PostControllerSwagger.java b/module-api/src/main/java/com/mile/controller/post/PostControllerSwagger.java new file mode 100644 index 00000000..0bc6b3ae --- /dev/null +++ b/module-api/src/main/java/com/mile/controller/post/PostControllerSwagger.java @@ -0,0 +1,40 @@ +package com.mile.controller.post; + +import com.mile.dto.ErrorResponse; +import com.mile.dto.SuccessResponse; +import com.mile.post.service.dto.CommentCreateRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import java.security.Principal; + +@Tag(name = "Post", description = "게시글 관련 API - 댓글 등록/ 조회 포함") +public interface PostControllerSwagger { + + @Operation(summary = "댓글 작성") + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "댓글 등록이 완료되었습니다."), + @ApiResponse(responseCode = "400", + description = "1. 댓글 최대 입력 길이(500자)를 초과하였습니다.\n" + + "2.댓글에 내용이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "403", description = "해당 사용자는 모임에 접근 권한이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "서버 내부 오류입니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + } + ) + SuccessResponse postComment( + @PathVariable final Long postId, + @Valid @RequestBody final CommentCreateRequest commentCreateRequest, + final Principal principal + ); +} diff --git a/module-api/src/main/java/com/mile/controller/user/UserControllerSwagger.java b/module-api/src/main/java/com/mile/controller/user/UserControllerSwagger.java index cef9bdfa..4b2ec423 100644 --- a/module-api/src/main/java/com/mile/controller/user/UserControllerSwagger.java +++ b/module-api/src/main/java/com/mile/controller/user/UserControllerSwagger.java @@ -21,8 +21,7 @@ public interface UserControllerSwagger { @Operation(summary = "소셜 로그인") @ApiResponses( value = { - @ApiResponse(responseCode = "200", description = "소셜 로그인이 완료되었습니다.", - content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "200", description = "소셜 로그인이 완료되었습니다."), @ApiResponse(responseCode = "400", description = "1. 요청한 값이 유효하지 않습니다.\n" + "2. 인가 코드가 만료되었습니다.\n", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @@ -38,7 +37,7 @@ SuccessResponse login( @Operation(summary = "액세스 토큰 재발급") @ApiResponses( value = { - @ApiResponse(responseCode = "200", description = "액세스 토큰 재발급이 완료되었습니다.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "200", description = "액세스 토큰 재발급이 완료되었습니다."), @ApiResponse(responseCode = "401", description = "리프레시 토큰이 유효하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "404", description = "해당 유저의 리프레시 토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) @@ -51,7 +50,7 @@ SuccessResponse refreshToken( @Operation(summary = "회원 탈퇴") @ApiResponses( value = { - @ApiResponse(responseCode = "200", description = "회원 삭제가 완료되었습니다.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "200", description = "회원 삭제가 완료되었습니다."), @ApiResponse(responseCode = "404", description = "1. 해당 유저의 리프레시 토큰이 존재하지 않습니다.\n" + "2. 해당 유저는 존재하지 않습니다.", @@ -66,7 +65,7 @@ SuccessResponse deleteUser( @Operation(summary = "로그아웃") @ApiResponses( value = { - @ApiResponse(responseCode = "200", description = "로그아웃이 완료되었습니다.", content = @Content(schema = @Schema(implementation = SuccessResponse.class))), + @ApiResponse(responseCode = "200", description = "로그아웃이 완료되었습니다."), @ApiResponse(responseCode = "404", description = "해당 유저의 리프레시 토큰이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "서버 내부 오류입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) } diff --git a/module-api/src/test/java/com/mile/ApiApplicationTests.java b/module-api/src/test/java/com/mile/ApiApplicationTests.java new file mode 100644 index 00000000..e1b0b713 --- /dev/null +++ b/module-api/src/test/java/com/mile/ApiApplicationTests.java @@ -0,0 +1,7 @@ +package com.mile; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApiApplicationTests { +} \ No newline at end of file diff --git a/module-common/src/main/java/com/mile/config/security/SecurityConfig.java b/module-common/src/main/java/com/mile/config/security/SecurityConfig.java index b7896897..0b6f4e1a 100644 --- a/module-common/src/main/java/com/mile/config/security/SecurityConfig.java +++ b/module-common/src/main/java/com/mile/config/security/SecurityConfig.java @@ -24,8 +24,7 @@ public class SecurityConfig { private static final String[] AUTH_WHITELIST = { - "/user/login", - "/user/token-refresh", + "/api/**", "/actuator/health", "/v3/api-docs/**", "/swagger-ui/**", diff --git a/module-common/src/main/java/com/mile/dto/ErrorResponse.java b/module-common/src/main/java/com/mile/dto/ErrorResponse.java index 659506f4..785f6a4f 100644 --- a/module-common/src/main/java/com/mile/dto/ErrorResponse.java +++ b/module-common/src/main/java/com/mile/dto/ErrorResponse.java @@ -9,4 +9,8 @@ public record ErrorResponse( public static ErrorResponse of(final ErrorMessage errorMessage) { return new ErrorResponse(errorMessage.getStatus(), errorMessage.getMessage()); } + + public static ErrorResponse of(final int status, final String message) { + return new ErrorResponse(status, message); + } } diff --git a/module-common/src/main/java/com/mile/exception/message/ErrorMessage.java b/module-common/src/main/java/com/mile/exception/message/ErrorMessage.java index 1c1ccc11..1ad3f992 100644 --- a/module-common/src/main/java/com/mile/exception/message/ErrorMessage.java +++ b/module-common/src/main/java/com/mile/exception/message/ErrorMessage.java @@ -13,18 +13,34 @@ public enum ErrorMessage { */ USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 유저는 존재하지 않습니다."), REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 유저의 리프레시 토큰이 존재하지 않습니다."), + POST_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 글은 존재하지 않습니다."), + + MOIM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 글모임이 존재하지 않습니다."), + CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 모임의 주제가 존재하지 않습니다."), + HANDLER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "요청하신 URL은 정보가 없습니다."), + /* Bad Request */ ENUM_VALUE_BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "요청한 값이 유효하지 않습니다."), AUTHENTICATION_CODE_EXPIRED(HttpStatus.BAD_REQUEST.value(), "인가 코드가 만료되었습니다."), SOCIAL_TYPE_BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "로그인 요청이 유효하지 않습니다."), + + VALIDATION_REQUEST_MISSING_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "요청 값이 유효하지 않습니다."), + + BEARER_LOST_ERROR(HttpStatus.BAD_REQUEST.value(), "토큰의 요청에 Bearer이 담겨 있지 않습니다."), + /* Unauthorized */ ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다."), TOKEN_INCORRECT_ERROR(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 유효하지 않습니다."), TOKEN_VALIDATION_ERROR(HttpStatus.UNAUTHORIZED.value(), "사용자 검증 토큰이 유효하지 안습니다."), + /* + Forbidden + */ + USER_AUTHENTICATE_ERROR(HttpStatus.FORBIDDEN.value(), "해당 사용자는 모임에 접근 권한이 없습니다."), + /* Internal Server Error */ diff --git a/module-common/src/main/java/com/mile/exception/message/SuccessMessage.java b/module-common/src/main/java/com/mile/exception/message/SuccessMessage.java index d4ae4aca..f3ca13b9 100644 --- a/module-common/src/main/java/com/mile/exception/message/SuccessMessage.java +++ b/module-common/src/main/java/com/mile/exception/message/SuccessMessage.java @@ -10,7 +10,15 @@ public enum SuccessMessage { LOGIN_SUCCESS(HttpStatus.OK.value(), "소셜 로그인이 완료되었습니다."), ISSUE_ACCESS_TOKEN_SUCCESS(HttpStatus.OK.value(), "액세스 토큰 재발급이 완료되었습니다."), USER_DELETE_SUCCESS(HttpStatus.OK.value(), "회원 삭제가 완료되었습니다."), - LOGOUT_SUCCESS(HttpStatus.OK.value(), "로그아웃이 완료되었습니다.") + LOGOUT_SUCCESS(HttpStatus.OK.value(), "로그아웃이 완료되었습니다."), + + /* + 201 CREATED + */ + COMMENT_CREATE_SUCCESS(HttpStatus.CREATED.value(), "댓글 등록이 완료되었습니다."), + + + TOPIC_SEARCH_SUCCESS(HttpStatus.OK.value(), "주제 조회가 완료되었습니다."), ; int status; diff --git a/module-common/src/main/java/com/mile/exception/model/ForbiddenException.java b/module-common/src/main/java/com/mile/exception/model/ForbiddenException.java new file mode 100644 index 00000000..6ca4f559 --- /dev/null +++ b/module-common/src/main/java/com/mile/exception/model/ForbiddenException.java @@ -0,0 +1,9 @@ +package com.mile.exception.model; + +import com.mile.exception.message.ErrorMessage; + +public class ForbiddenException extends MileException { + public ForbiddenException(final ErrorMessage errorMessage) { + super(errorMessage); + } +} \ No newline at end of file diff --git a/module-common/src/main/java/com/mile/filter/JwtAuthenticationFilter.java b/module-common/src/main/java/com/mile/filter/JwtAuthenticationFilter.java index 3a14f1a4..a92f7461 100644 --- a/module-common/src/main/java/com/mile/filter/JwtAuthenticationFilter.java +++ b/module-common/src/main/java/com/mile/filter/JwtAuthenticationFilter.java @@ -2,6 +2,7 @@ import com.mile.authentication.UserAuthentication; import com.mile.exception.message.ErrorMessage; +import com.mile.exception.model.BadRequestException; import com.mile.exception.model.JwtValidationException; import com.mile.jwt.JwtTokenProvider; import com.mile.jwt.JwtValidationType; @@ -10,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.lang.NonNull; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -20,6 +22,7 @@ import java.io.IOException; @Component +@Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -49,6 +52,8 @@ private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring("Bearer ".length()); + } else if (StringUtils.hasText(bearerToken) && !bearerToken.startsWith("Bearer ")) { + throw new BadRequestException(ErrorMessage.BEARER_LOST_ERROR); } return null; } diff --git a/module-common/src/main/java/com/mile/handler/GlobalExceptionHandler.java b/module-common/src/main/java/com/mile/handler/GlobalExceptionHandler.java index 154adebd..0f33b500 100644 --- a/module-common/src/main/java/com/mile/handler/GlobalExceptionHandler.java +++ b/module-common/src/main/java/com/mile/handler/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import com.mile.dto.ErrorResponse; import com.mile.exception.message.ErrorMessage; import com.mile.exception.model.BadRequestException; +import com.mile.exception.model.ForbiddenException; import com.mile.exception.model.JwtValidationException; import com.mile.exception.model.NotFoundException; import com.mile.exception.model.UnauthorizedException; @@ -11,8 +12,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; @Slf4j @RestControllerAdvice @@ -38,11 +42,28 @@ public ResponseEntity handleJwtValidationException(final JwtValid return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(e.getErrorMessage())); } + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + FieldError fieldError = e.getBindingResult().getFieldError(); + if (fieldError == null) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(ErrorMessage.VALIDATION_REQUEST_MISSING_EXCEPTION)); + else return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), fieldError.getDefaultMessage())); + } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(final ForbiddenException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse.of(e.getErrorMessage())); + } + @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFoundException(final NotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(e.getErrorMessage())); } + @ExceptionHandler({NoHandlerFoundException.class}) + public ResponseEntity handleNoHandlerFoundException(final NoHandlerFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(ErrorMessage.HANDLER_NOT_FOUND)); + } + @ExceptionHandler(Exception.class) protected ResponseEntity handleException(final Exception error, final HttpServletRequest request) { log.error("================================================NEW==============================================="); diff --git a/module-domain/build.gradle b/module-domain/build.gradle index 79e7fd48..dc293c35 100644 --- a/module-domain/build.gradle +++ b/module-domain/build.gradle @@ -5,6 +5,9 @@ dependencies { implementation project(':module-auth') implementation project(':module-common') + //Swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + // QueryDSL Implementation implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" diff --git a/module-domain/src/main/java/com/mile/comment/domain/Comment.java b/module-domain/src/main/java/com/mile/comment/domain/Comment.java index 27140d63..3c63d1e2 100644 --- a/module-domain/src/main/java/com/mile/comment/domain/Comment.java +++ b/module-domain/src/main/java/com/mile/comment/domain/Comment.java @@ -1,15 +1,23 @@ package com.mile.comment.domain; import com.mile.config.BaseTimeEntity; -import com.mile.post.Post; +import com.mile.post.domain.Post; +import com.mile.post.service.dto.CommentCreateRequest; import com.mile.user.domain.User; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NoArgsConstructor; @Entity +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor public class Comment extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,4 +31,19 @@ public class Comment extends BaseTimeEntity { @ManyToOne private User user; + + public static Comment create( + final Post post, + final User user, + final CommentCreateRequest createRequest, + final boolean anonymous + ) { + return Comment + .builder() + .post(post) + .content(createRequest.content()) + .user(user) + .anonymous(anonymous) + .build(); + } } diff --git a/module-domain/src/main/java/com/mile/comment/repository/CommentRepository.java b/module-domain/src/main/java/com/mile/comment/repository/CommentRepository.java new file mode 100644 index 00000000..b6ec0570 --- /dev/null +++ b/module-domain/src/main/java/com/mile/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.mile.comment.repository; + +import com.mile.comment.domain.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/module-domain/src/main/java/com/mile/comment/service/CommentService.java b/module-domain/src/main/java/com/mile/comment/service/CommentService.java index 4e2718cc..04dd34a4 100644 --- a/module-domain/src/main/java/com/mile/comment/service/CommentService.java +++ b/module-domain/src/main/java/com/mile/comment/service/CommentService.java @@ -1,7 +1,24 @@ package com.mile.comment.service; +import com.mile.comment.domain.Comment; +import com.mile.comment.repository.CommentRepository; +import com.mile.post.domain.Post; +import com.mile.post.service.dto.CommentCreateRequest; +import com.mile.user.domain.User; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class CommentService { + private static boolean ANONYMOUS_TRUE = true; + private final CommentRepository commentRepository; + + public void createComment( + final Post post, + final User user, + final CommentCreateRequest commentCreateRequest + ) { + commentRepository.save(Comment.create(post, user, commentCreateRequest, ANONYMOUS_TRUE)); + } } diff --git a/module-domain/src/main/java/com/mile/config/BaseTimeEntity.java b/module-domain/src/main/java/com/mile/config/BaseTimeEntity.java index 9d958a42..1d4cc793 100644 --- a/module-domain/src/main/java/com/mile/config/BaseTimeEntity.java +++ b/module-domain/src/main/java/com/mile/config/BaseTimeEntity.java @@ -3,9 +3,12 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; + +import lombok.Getter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +@Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseTimeEntity { diff --git a/module-domain/src/main/java/com/mile/curious/domain/Curious.java b/module-domain/src/main/java/com/mile/curious/domain/Curious.java index d5726daf..2ef0fc93 100644 --- a/module-domain/src/main/java/com/mile/curious/domain/Curious.java +++ b/module-domain/src/main/java/com/mile/curious/domain/Curious.java @@ -1,6 +1,6 @@ package com.mile.curious.domain; -import com.mile.post.Post; +import com.mile.post.domain.Post; import com.mile.config.BaseTimeEntity; import com.mile.user.domain.User; import jakarta.persistence.Entity; diff --git a/module-domain/src/main/java/com/mile/moim/domain/Moim.java b/module-domain/src/main/java/com/mile/moim/domain/Moim.java index 981b5fed..acadee6c 100644 --- a/module-domain/src/main/java/com/mile/moim/domain/Moim.java +++ b/module-domain/src/main/java/com/mile/moim/domain/Moim.java @@ -7,8 +7,10 @@ import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import lombok.Getter; @Entity +@Getter public class Moim { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/module-domain/src/main/java/com/mile/moim/repository/MoimRepository.java b/module-domain/src/main/java/com/mile/moim/repository/MoimRepository.java new file mode 100644 index 00000000..b52ac9c8 --- /dev/null +++ b/module-domain/src/main/java/com/mile/moim/repository/MoimRepository.java @@ -0,0 +1,7 @@ +package com.mile.moim.repository; + +import com.mile.moim.domain.Moim; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MoimRepository extends JpaRepository { +} diff --git a/module-domain/src/main/java/com/mile/moim/serivce/MoimService.java b/module-domain/src/main/java/com/mile/moim/serivce/MoimService.java index aa140403..0bd32693 100644 --- a/module-domain/src/main/java/com/mile/moim/serivce/MoimService.java +++ b/module-domain/src/main/java/com/mile/moim/serivce/MoimService.java @@ -1,7 +1,33 @@ package com.mile.moim.serivce; +import com.mile.exception.message.ErrorMessage; +import com.mile.exception.model.ForbiddenException; +import com.mile.moim.serivce.dto.ContentListResponse; +import com.mile.topic.serivce.TopicService; +import com.mile.writerName.serivce.WriterNameService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class MoimService { + private final WriterNameService writerNameService; + private final TopicService topicService; + + public ContentListResponse getContentsFromMoim( + final Long moimId, + final Long userId + ) { + authenticateUserOfMoim(moimId, userId); + return ContentListResponse.of(topicService.getContentsFromMoim(moimId)); + } + + public void authenticateUserOfMoim( + final Long moimId, + final Long userId + ) { + if (!writerNameService.isUserInMoim(moimId, userId)) { + throw new ForbiddenException(ErrorMessage.USER_AUTHENTICATE_ERROR); + } + } } diff --git a/module-domain/src/main/java/com/mile/moim/serivce/dto/ContentListResponse.java b/module-domain/src/main/java/com/mile/moim/serivce/dto/ContentListResponse.java new file mode 100644 index 00000000..0738d0cb --- /dev/null +++ b/module-domain/src/main/java/com/mile/moim/serivce/dto/ContentListResponse.java @@ -0,0 +1,15 @@ +package com.mile.moim.serivce.dto; + +import com.mile.topic.serivce.dto.ContentResponse; + +import java.util.List; + +public record ContentListResponse( + List topics +) { + public static ContentListResponse of( + final List topics + ) { + return new ContentListResponse(topics); + } +} diff --git a/module-domain/src/main/java/com/mile/penNameProfile/serivce/PenNameProfileService.java b/module-domain/src/main/java/com/mile/penNameProfile/serivce/PenNameProfileService.java deleted file mode 100644 index 1622f9d5..00000000 --- a/module-domain/src/main/java/com/mile/penNameProfile/serivce/PenNameProfileService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.mile.penNameProfile.serivce; - -import org.springframework.stereotype.Service; - -@Service -public class PenNameProfileService { -} diff --git a/module-domain/src/main/java/com/mile/post/Post.java b/module-domain/src/main/java/com/mile/post/domain/Post.java similarity index 79% rename from module-domain/src/main/java/com/mile/post/Post.java rename to module-domain/src/main/java/com/mile/post/domain/Post.java index d5cd3ffd..985a9ac5 100644 --- a/module-domain/src/main/java/com/mile/post/Post.java +++ b/module-domain/src/main/java/com/mile/post/domain/Post.java @@ -1,14 +1,17 @@ -package com.mile.post; +package com.mile.post.domain; +import com.mile.config.BaseTimeEntity; import com.mile.topic.domain.Topic; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import lombok.Getter; @Entity -public class Post { +@Getter +public class Post extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/module-domain/src/main/java/com/mile/post/repository/PostRepository.java b/module-domain/src/main/java/com/mile/post/repository/PostRepository.java new file mode 100644 index 00000000..ba2a267d --- /dev/null +++ b/module-domain/src/main/java/com/mile/post/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.mile.post.repository; + +import com.mile.post.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/module-domain/src/main/java/com/mile/post/service/PostService.java b/module-domain/src/main/java/com/mile/post/service/PostService.java new file mode 100644 index 00000000..ce24fc94 --- /dev/null +++ b/module-domain/src/main/java/com/mile/post/service/PostService.java @@ -0,0 +1,52 @@ +package com.mile.post.service; + +import com.mile.comment.service.CommentService; +import com.mile.exception.message.ErrorMessage; +import com.mile.exception.model.NotFoundException; +import com.mile.moim.serivce.MoimService; +import com.mile.post.domain.Post; +import com.mile.post.repository.PostRepository; +import com.mile.post.service.dto.CommentCreateRequest; +import com.mile.user.serivce.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + private final MoimService moimService; + private final CommentService commentService; + private final UserService userService; + + @Transactional + public void createCommentOnPost( + final Long postId, + final Long userId, + final CommentCreateRequest commentCreateRequest + + ) { + Post post = findById(postId); + authenticateUserWithPost(post, userId); + commentService.createComment(post, userService.findById(userId), commentCreateRequest); + } + + + private void authenticateUserWithPost( + final Post post, + final Long userId + ) { + moimService.authenticateUserOfMoim(post.getTopic().getMoim().getId(), userId); + } + + public Post findById( + final Long postId + ) { + return postRepository.findById(postId) + .orElseThrow( + () -> new NotFoundException(ErrorMessage.POST_NOT_FOUND) + ); + } +} diff --git a/module-domain/src/main/java/com/mile/post/service/dto/CommentCreateRequest.java b/module-domain/src/main/java/com/mile/post/service/dto/CommentCreateRequest.java new file mode 100644 index 00000000..1b3e3436 --- /dev/null +++ b/module-domain/src/main/java/com/mile/post/service/dto/CommentCreateRequest.java @@ -0,0 +1,14 @@ +package com.mile.post.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CommentCreateRequest( + + @Schema(description = "댓글 내용", example = "댓글 내용을 입력해주세요") + @NotBlank(message = "댓글에 내용이 없습니다.") + @Size(max = 500, message = "댓글 최대 입력 길이(500자)를 초과하였습니다.") + String content +) { +} diff --git a/module-domain/src/main/java/com/mile/topic/domain/Topic.java b/module-domain/src/main/java/com/mile/topic/domain/Topic.java index 5a0757af..027b1a9f 100644 --- a/module-domain/src/main/java/com/mile/topic/domain/Topic.java +++ b/module-domain/src/main/java/com/mile/topic/domain/Topic.java @@ -1,14 +1,17 @@ package com.mile.topic.domain; +import com.mile.config.BaseTimeEntity; import com.mile.moim.domain.Moim; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import lombok.Getter; @Entity -public class Topic { +@Getter +public class Topic extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -16,7 +19,7 @@ public class Topic { @ManyToOne private Moim moim; + private String keyword; private String topic; - private int week; - private boolean isClosed; + private String description; } diff --git a/module-domain/src/main/java/com/mile/topic/repository/TopicRepository.java b/module-domain/src/main/java/com/mile/topic/repository/TopicRepository.java new file mode 100644 index 00000000..72e678df --- /dev/null +++ b/module-domain/src/main/java/com/mile/topic/repository/TopicRepository.java @@ -0,0 +1,11 @@ +package com.mile.topic.repository; + +import com.mile.topic.domain.Topic; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TopicRepository extends JpaRepository { + + public List findByMoimId(final Long moimId); +} diff --git a/module-domain/src/main/java/com/mile/topic/serivce/TopicService.java b/module-domain/src/main/java/com/mile/topic/serivce/TopicService.java index 70b6e999..7ad74a50 100644 --- a/module-domain/src/main/java/com/mile/topic/serivce/TopicService.java +++ b/module-domain/src/main/java/com/mile/topic/serivce/TopicService.java @@ -1,7 +1,55 @@ package com.mile.topic.serivce; +import com.mile.config.BaseTimeEntity; +import com.mile.exception.message.ErrorMessage; +import com.mile.exception.model.NotFoundException; +import com.mile.topic.domain.Topic; +import com.mile.topic.repository.TopicRepository; +import com.mile.topic.serivce.dto.ContentResponse; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + @Service +@RequiredArgsConstructor public class TopicService { + + private final TopicRepository topicRepository; + + + public List getContentsFromMoim( + final Long moimId + ) { + List topicList = sortByCreatedAt(findByMoimId(moimId)); + isContentsEmpty(topicList); + return topicList + .stream() + .map(ContentResponse::of) + .collect(Collectors.toList()); + } + + private void isContentsEmpty( + final List topicList + ) { + if (topicList.isEmpty()) { + throw new NotFoundException(ErrorMessage.CONTENT_NOT_FOUND); + } + } + + private List findByMoimId( + final Long moimId + ) { + return topicRepository.findByMoimId(moimId); + } + + private List sortByCreatedAt( + final List topicList + ) { + return topicList.stream() + .sorted(Comparator.comparing(BaseTimeEntity::getCreatedAt)) + .collect(Collectors.toList()); + } } diff --git a/module-domain/src/main/java/com/mile/topic/serivce/dto/ContentResponse.java b/module-domain/src/main/java/com/mile/topic/serivce/dto/ContentResponse.java new file mode 100644 index 00000000..69cf182a --- /dev/null +++ b/module-domain/src/main/java/com/mile/topic/serivce/dto/ContentResponse.java @@ -0,0 +1,14 @@ +package com.mile.topic.serivce.dto; + +import com.mile.topic.domain.Topic; + +public record ContentResponse( + Long topicId, + String topicName +) { + public static ContentResponse of( + final Topic topic + ) { + return new ContentResponse(topic.getId(), topic.getTopic()); + } +} diff --git a/module-domain/src/main/java/com/mile/user/service/UserService.java b/module-domain/src/main/java/com/mile/user/service/UserService.java index 9c010d18..8f2428c3 100644 --- a/module-domain/src/main/java/com/mile/user/service/UserService.java +++ b/module-domain/src/main/java/com/mile/user/service/UserService.java @@ -118,4 +118,13 @@ private LoginSuccessResponse getTokenDto( return getTokenByUserId(id); } } + + public User findById( + Long userId + ) { + return userRepository.findById(userId) + .orElseThrow( + () -> new NotFoundException(ErrorMessage.USER_NOT_FOUND) + ); + } } diff --git a/module-domain/src/main/java/com/mile/penNameProfile/domain/PenName.java b/module-domain/src/main/java/com/mile/writerName/domain/WriterName.java similarity index 84% rename from module-domain/src/main/java/com/mile/penNameProfile/domain/PenName.java rename to module-domain/src/main/java/com/mile/writerName/domain/WriterName.java index 435f1592..fa112626 100644 --- a/module-domain/src/main/java/com/mile/penNameProfile/domain/PenName.java +++ b/module-domain/src/main/java/com/mile/writerName/domain/WriterName.java @@ -1,4 +1,4 @@ -package com.mile.penNameProfile.domain; +package com.mile.writerName.domain; import com.mile.moim.domain.Moim; import com.mile.user.domain.User; @@ -7,9 +7,11 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import lombok.Getter; @Entity -public class PenName { +@Getter +public class WriterName { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; diff --git a/module-domain/src/main/java/com/mile/writerName/repository/WriterNameRepository.java b/module-domain/src/main/java/com/mile/writerName/repository/WriterNameRepository.java new file mode 100644 index 00000000..5cda835d --- /dev/null +++ b/module-domain/src/main/java/com/mile/writerName/repository/WriterNameRepository.java @@ -0,0 +1,11 @@ +package com.mile.writerName.repository; + +import com.mile.writerName.domain.WriterName; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface WriterNameRepository extends JpaRepository { + + Optional findByMoimIdAndWriterId(final Long moimId, final Long userId); +} \ No newline at end of file diff --git a/module-domain/src/main/java/com/mile/writerName/serivce/WriterNameService.java b/module-domain/src/main/java/com/mile/writerName/serivce/WriterNameService.java new file mode 100644 index 00000000..5e774360 --- /dev/null +++ b/module-domain/src/main/java/com/mile/writerName/serivce/WriterNameService.java @@ -0,0 +1,19 @@ +package com.mile.writerName.serivce; + +import com.mile.writerName.repository.WriterNameRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class WriterNameService { + private final WriterNameRepository writerNameRepository; + + public boolean isUserInMoim( + final Long moimId, + final Long writerId + ) { + return writerNameRepository.findByMoimIdAndWriterId(moimId, writerId).isPresent(); + } + +}