diff --git a/docker-compose.yml b/docker-compose.yml index c97c242a..73adf8a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,8 +64,6 @@ services: - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_REGION=${AWS_REGION} - JWT_SECRET=${JWT_SECRET} - depends_on: - - nestjs logging: driver: "json-file" options: @@ -132,12 +130,20 @@ services: caddy.route_2.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_3: /user/v2/* caddy.route_3.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_4: /meeting/v2/* + caddy.route_4: /meeting/v2 caddy.route_4.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_5: /post/v2 + caddy.route_5: /meeting/v2/* caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_6: /comment/v2 + caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_7: /post/v2/* + caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_8: /comment/v2 + caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_9: /comment/v2/* + caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_10: /notice/v2 + caddy.route_10.reverse_proxy: "{{ upstreams 4000 }}" nestjs-blue: image: makerscrew/server:latest @@ -221,12 +227,20 @@ services: caddy.route_2.reverse_proxy: "{{ upstreams 4000 }}" caddy.route_3: /user/v2/* caddy.route_3.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_4: /meeting/v2/* + caddy.route_4: /meeting/v2 caddy.route_4.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_5: /post/v2 + caddy.route_5: /meeting/v2/* caddy.route_5.reverse_proxy: "{{ upstreams 4000 }}" - caddy.route_6: /comment/v2 + caddy.route_6: /post/v2 caddy.route_6.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_7: /post/v2/* + caddy.route_7.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_8: /comment/v2 + caddy.route_8.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_9: /comment/v2/* + caddy.route_9.reverse_proxy: "{{ upstreams 4000 }}" + caddy.route_10: /notice/v2 + caddy.route_10.reverse_proxy: "{{ upstreams 4000 }}" networks: caddy: diff --git a/main/build.gradle b/main/build.gradle index a3c0bdae..b2d70c5b 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -65,6 +65,12 @@ dependencies { // MapStruct implementation 'org.mapstruct:mapstruct:1.5.3.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' + + // queryDsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java new file mode 100644 index 00000000..a5b798f3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Api.java @@ -0,0 +1,75 @@ +package org.sopt.makers.crew.main.comment.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; + +import java.security.Principal; + +import org.sopt.makers.crew.main.comment.v2.dto.query.CommentV2GetCommentsQueryDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +public interface CommentV2Api { + + @Operation(summary = "모임 게시글 댓글 작성") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 댓글 수정") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity updateComment( + @PathVariable Integer commentId, + @Valid @RequestBody CommentV2UpdateCommentBodyDto requestBody, + Principal principal); + + @Operation(summary = "댓글 신고하기") + @ResponseStatus(HttpStatus.CREATED) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + }) + ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal); + + @Operation(summary = "모임 게시글 댓글 삭제") + @ResponseStatus(HttpStatus.NO_CONTENT) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "성공"), + }) + ResponseEntity deleteComment(Principal principal, @PathVariable Integer commentId); + + @Operation(summary = "댓글에서 유저 멘션") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal); + + @Operation(summary = "모임 게시글 댓글 리스트 조회") + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity getComments(@Valid @ModelAttribute CommentV2GetCommentsQueryDto requestBody, Principal principal); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java index e40c5b27..06c0e59d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/CommentV2Controller.java @@ -1,42 +1,101 @@ package org.sopt.makers.crew.main.comment.v2; -import io.swagger.v3.oas.annotations.Operation; -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 java.security.Principal; + import lombok.RequiredArgsConstructor; + +import org.sopt.makers.crew.main.comment.v2.dto.query.CommentV2GetCommentsQueryDto; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2UpdateCommentBodyDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; import org.sopt.makers.crew.main.comment.v2.service.CommentV2Service; import org.sopt.makers.crew.main.common.util.UserUtil; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/comment/v2") @RequiredArgsConstructor @Tag(name = "댓글/대댓글") -public class CommentV2Controller { - - private final CommentV2Service commentV2Service; - - @Operation(summary = "모임 게시글 댓글 작성") - @PostMapping() - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - }) - public ResponseEntity createComment( - @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); - } +public class CommentV2Controller implements CommentV2Api { + + private final CommentV2Service commentV2Service; + + @Override + @PostMapping() + public ResponseEntity createComment( + @Valid @RequestBody CommentV2CreateCommentBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.createComment(requestBody, userId)); + } + + @Override + @PutMapping("/{commentId}") + public ResponseEntity updateComment( + @PathVariable Integer commentId, + @Valid @RequestBody CommentV2UpdateCommentBodyDto requestBody, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok( + commentV2Service.updateComment(commentId, requestBody.getContents(), userId)); + } + + @Override + @PostMapping("/{commentId}/report") + public ResponseEntity reportComment( + @PathVariable Integer commentId, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(commentV2Service.reportComment(commentId, userId)); + } + + @Override + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment( + Principal principal, + @PathVariable Integer commentId) { + Integer userId = UserUtil.getUserId(principal); + + commentV2Service.deleteComment(commentId, userId); + + return ResponseEntity.noContent().build(); + } + + @Override + @PostMapping("/mention") + public ResponseEntity mentionUserInComment( + @Valid @RequestBody CommentV2MentionUserInCommentRequestDto requestBody, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + commentV2Service.mentionUserInComment(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @Override + @GetMapping + public ResponseEntity getComments( + @Valid @ModelAttribute CommentV2GetCommentsQueryDto requestBody, + Principal principal) { + + Integer userId = UserUtil.getUserId(principal); + CommentV2GetCommentsResponseDto commentDtos = commentV2Service.getComments(requestBody.getPostId(), + requestBody.getPage(), requestBody.getTake(), userId); + return ResponseEntity.status(HttpStatus.OK).body(commentDtos); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java new file mode 100644 index 00000000..48bf7f21 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/CommentMapper.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.comment.v2.dto; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.user.User; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + @Mapping(source = "post", target = "post") + @Mapping(source = "requestBody.contents", target = "contents") + @Mapping(source = "user", target = "user") + @Mapping(source = "user.id", target = "userId") + @Mapping(target = "likeCount", constant = "0") + Comment toComment(CommentV2CreateCommentBodyDto requestBody, Post post, User user, int depth, int order, + Integer parentId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java new file mode 100644 index 00000000..a5602c51 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/query/CommentV2GetCommentsQueryDto.java @@ -0,0 +1,20 @@ +package org.sopt.makers.crew.main.comment.v2.dto.query; + +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class CommentV2GetCommentsQueryDto extends PageOptionsDto { + + @NotNull + private final Integer postId; + + @Builder + public CommentV2GetCommentsQueryDto(Integer page, Integer take, Integer postId) { + super(page, take); + this.postId = postId; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java index c7df385f..bdc26c45 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2CreateCommentBodyDto.java @@ -11,12 +11,18 @@ @Schema(description = "댓글 생성 request body dto") public class CommentV2CreateCommentBodyDto { - @Schema(example = "1", required = true, description = "게시글 ID") - @NotNull - private Integer postId; + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; - @Schema(example = "알고보면 쓸데있는 개발 프로세스", required = true, description = "댓글 내용") - @NotEmpty - private String contents; + @Schema(example = "알고보면 쓸데있는 개발 프로세스", required = true, description = "댓글 내용") + @NotEmpty + private String contents; + + @Schema(example = "댓글/대댓글 여부", required = true, description = "true") + private boolean isParent; + + @Schema(example = "대댓글인 경우, 댓글의 id", required = true, description = "1") + private Integer parentCommentId; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java new file mode 100644 index 00000000..5d059ef6 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2MentionUserInCommentRequestDto.java @@ -0,0 +1,31 @@ +package org.sopt.makers.crew.main.comment.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "댓글에서 유저 언급 request body dto") +public class CommentV2MentionUserInCommentRequestDto { + + /** + * 주의!! : 필드명은 userIds 이지만 실제 요청받는 값은 orgId 입니다. + */ + + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private List userIds; + + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private Integer postId; + + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private String content; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java new file mode 100644 index 00000000..f702ed16 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/request/CommentV2UpdateCommentBodyDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.comment.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "댓글 업데이트 request body dto") +public class CommentV2UpdateCommentBodyDto { + + @Schema(example = "알고보면 쓸데있는 개발 프로세스", description = "댓글 내용") + @NotEmpty + private String contents; + +} \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java new file mode 100644 index 00000000..945b5a2c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentDto.java @@ -0,0 +1,44 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import org.sopt.makers.crew.main.entity.comment.Comment; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class CommentDto { + private final Integer id; + private final String contents; + private final CommentWriterDto user; + private final LocalDateTime updatedDate; + private final int likeCount; + private final boolean isLiked; + private final boolean isWriter; + private final int order; + private final List replies; + + @QueryProjection + public CommentDto(Integer id, String contents, CommentWriterDto user, LocalDateTime updatedDate, int likeCount, + boolean isLiked, boolean isWriter, int order, List replies) { + this.id = id; + this.contents = contents; + this.user = user; + this.updatedDate = updatedDate; + this.likeCount = likeCount; + this.isLiked = isLiked; + this.isWriter = isWriter; + this.order = order; + this.replies = replies; + } + + public static CommentDto of(Comment comment, boolean isLiked, boolean isWriter, List replies) { + return new CommentDto(comment.getId(), comment.getContents(), + new CommentWriterDto(comment.getUser().getId(), comment.getUser().getOrgId(), comment.getUser().getName(), + comment.getUser().getProfileImage()), comment.getUpdatedDate(), comment.getLikeCount(), + isLiked, isWriter, comment.getOrder(), replies); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java new file mode 100644 index 00000000..de6c9dfb --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2GetCommentsResponseDto.java @@ -0,0 +1,12 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2GetCommentsResponseDto { + private final List comments; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java new file mode 100644 index 00000000..03340a2c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2ReportCommentResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2ReportCommentResponseDto { + + /** + * 생성된 신고 id + */ + private Integer reportId; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java new file mode 100644 index 00000000..fe7de072 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentV2UpdateCommentResponseDto.java @@ -0,0 +1,24 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class CommentV2UpdateCommentResponseDto { + + /** + * 생성된 댓글 id + */ + private Integer id; + + /** + * 댓글 내용 + */ + private String contents; + + /** + * 업데이트 시각 + */ + private String updateDate; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java new file mode 100644 index 00000000..af69fa40 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/dto/response/CommentWriterDto.java @@ -0,0 +1,21 @@ +package org.sopt.makers.crew.main.comment.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; + +@Getter +public class CommentWriterDto { + private final Integer id; + private final Integer orgId; + private final String name; + private final String profileImage; + + @QueryProjection + public CommentWriterDto(Integer id, Integer orgId, String name, String profileImage) { + this.id = id; + this.orgId = orgId; + this.name = name; + this.profileImage = profileImage; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java index c2a7e78a..39a95535 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2Service.java @@ -1,10 +1,28 @@ package org.sopt.makers.crew.main.comment.v2.service; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; public interface CommentV2Service { - CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId); + CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, + Integer userId); + + CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException; + + void deleteComment(Integer commentId, Integer userId); + + void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, Integer userId); + + CommentV2UpdateCommentResponseDto updateComment(Integer commentId, String contents, + Integer userId); + + CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java index dbbb5d7a..6008f8b4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceImpl.java @@ -1,15 +1,37 @@ package org.sopt.makers.crew.main.comment.v2.service; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_COMMENT_PUSH_NOTIFICATION_TITLE; -import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; + +import org.sopt.makers.crew.main.comment.v2.dto.CommentMapper; import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2CreateCommentBodyDto; +import org.sopt.makers.crew.main.comment.v2.dto.request.CommentV2MentionUserInCommentRequestDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentDto; import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2CreateCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2GetCommentsResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.response.ErrorStatus; +import org.sopt.makers.crew.main.common.util.MentionSecretStringRemover; +import org.sopt.makers.crew.main.common.util.Time; import org.sopt.makers.crew.main.entity.comment.Comment; import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.comment.Comments; +import org.sopt.makers.crew.main.entity.like.LikeRepository; +import org.sopt.makers.crew.main.entity.like.MyLikes; import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.report.Report; +import org.sopt.makers.crew.main.entity.report.ReportRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.internal.notification.PushNotificationService; @@ -22,51 +44,225 @@ @RequiredArgsConstructor @Transactional(readOnly = true) public class CommentV2ServiceImpl implements CommentV2Service { + private static final int IS_REPLY_COMMENT = 1; + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final ReportRepository reportRepository; + private final LikeRepository likeRepository; + + private final PushNotificationService pushNotificationService; + + private final CommentMapper commentMapper; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + private final Time time; + + /** + * 모임 게시글 댓글 작성 + * + * @throws 400 존재하지 않는 게시글일 떄 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public CommentV2CreateCommentResponseDto createComment( + CommentV2CreateCommentBodyDto requestBody, + Integer userId) { + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + User writer = userRepository.findByIdOrThrow(userId); + + int depth = 0; + int order = 0; + Integer parentId = 0; + + boolean isReplyComment = !requestBody.isParent(); + if (isReplyComment) { + validateParentCommentId(requestBody); + depth = 1; + parentId = requestBody.getParentCommentId(); + order = getOrder(parentId); + } + + Comment comment = commentMapper.toComment(requestBody, post, writer, depth, order, + parentId); + + Comment savedComment = commentRepository.save(comment); + post.increaseCommentCount(); + + sendPushNotification(requestBody, post, writer); + + return CommentV2CreateCommentResponseDto.of(savedComment.getId()); + } + + private void sendPushNotification(CommentV2CreateCommentBodyDto requestBody, Post post, + User user) { + User PostWriter = post.getUser(); + String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + String secretStringRemovedContent = MentionSecretStringRemover.removeSecretString( + requestBody.getContents()); + String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", + user.getName(), secretStringRemovedContent); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + } + + private void validateParentCommentId(CommentV2CreateCommentBodyDto requestBody) { + commentRepository.findByIdAndPostIdOrThrow(requestBody.getParentCommentId(), + requestBody.getPostId()); + } + + private int getOrder(Integer parentId) { + Optional recentComment = commentRepository.findFirstByParentIdOrderByOrderDesc( + parentId); + return recentComment.map(comment -> comment.getOrder() + 1).orElse(1); + } + + /** + * 모임 게시글 댓글 수정 + * + * @param commentId 수정할 댓글 ID + * @param contents 수정할 내용 + * @param userId 수정하는 유저 ID + * @return 수정된 댓글 정보 + */ + @Override + @Transactional + public CommentV2UpdateCommentResponseDto updateComment(Integer commentId, + String contents, Integer userId) { + // 1. id를 기반으로 comment를 찾는다. + Comment comment = commentRepository.findByIdOrThrow(commentId); + + // 2. comment의 user_id와 userId가 같은지 확인한다. + comment.validateWriter(userId); + + // 3. comment의 contents를 수정한다. + comment.updateContents(contents); + + // 4. 수정된 comment의 id, contents, updatedDate를 반환한다. + return CommentV2UpdateCommentResponseDto.of(comment.getId(), comment.getContents(), + String.valueOf(time.now())); + } + + @Override + public CommentV2GetCommentsResponseDto getComments(Integer postId, Integer page, Integer take, + Integer userId) { + // TODO : 페이지네이션 구현 + + List comments = commentRepository.findAllByPostIdOrderByCreatedDate(postId); + + MyLikes myLikes = new MyLikes(likeRepository.findAllByUserIdAndPostIdNotNull(userId)); + + Map> replyMap = new HashMap<>(); + comments.stream() + .filter(comment -> !comment.isParentComment()) + .forEach( + comment -> replyMap.computeIfAbsent(comment.getParentId(), k -> new ArrayList<>()) + .add(CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), null))); + + List commentDtos = comments.stream() + .filter(Comment::isParentComment) + .map(comment -> CommentDto.of(comment, myLikes.isLikeComment(comment.getId()), + comment.isWriter(userId), + replyMap.get(comment.getId()))) + .toList(); + + return CommentV2GetCommentsResponseDto.of(commentDtos); + } + + /** + * 댓글 신고하기 + * + * @param commentId 댓글 신고할 댓글 id + * @param userId 신고하는 유저 id + * @return 신고 ID + * @throws BadRequestException 이미 신고한 댓글일 때 + * @apiNote 댓글 신고는 한 댓글당 한번만 가능 + */ + @Override + @Transactional + public CommentV2ReportCommentResponseDto reportComment(Integer commentId, Integer userId) + throws BadRequestException { + Comment comment = commentRepository.findByIdOrThrow(commentId); + User user = userRepository.findByIdOrThrow(userId); + + Optional existingReport = reportRepository.findByCommentAndUser(comment, user); + + if (existingReport.isPresent()) { + throw new BadRequestException(ErrorStatus.ALREADY_REPORTED_COMMENT.getErrorCode()); + } + + Report report = Report.builder() + .comment(comment) + .user(user) + .build(); + + Report savedReport = reportRepository.save(report); + + return CommentV2ReportCommentResponseDto.of(savedReport.getId()); + } - private final PostRepository postRepository; - private final UserRepository userRepository; - private final CommentRepository commentRepository; - private final PushNotificationService pushNotificationService; + /** + * 모임 게시글 댓글 삭제 + * + * @throws ForbiddenException 댓글 작성자가 아닐 때 + * @apiNote 댓글 삭제시 게시글의 댓글 수를 1 감소시킴 + */ + @Override + @Transactional + public void deleteComment(Integer commentId, Integer userId) { + Comment comment = commentRepository.findByIdOrThrow(commentId); + comment.validateWriter(userId); + User user = comment.getUser(); - @Value("${push-notification.web-url}") - private String pushWebUrl; + Post post = postRepository.findByIdOrThrow(comment.getPostId()); + post.decreaseCommentCount(); - /** - * 모임 게시글 댓글 작성 - * - * @throws 400 존재하지 않는 게시글일 떄 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional + Comments childComments = new Comments(commentRepository.findAllByParentIdOrderByOrderDesc(comment.getId())); - public CommentV2CreateCommentResponseDto createComment(CommentV2CreateCommentBodyDto requestBody, - Integer userId) { - Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User user = userRepository.findByIdOrThrow(userId); + if (comment.getDepth() == IS_REPLY_COMMENT || !childComments.hasChild()) { + commentRepository.delete(comment); + return; + } - Comment comment = Comment.builder() - .contents(requestBody.getContents()) - .user(user) - .post(post) - .build(); + comment.deleteParentComment(); + childComments.deleteMention(user.getName(), user.getOrgId().toString()); + } - Comment savedComment = commentRepository.save(comment); + @Override + public void mentionUserInComment(CommentV2MentionUserInCommentRequestDto requestBody, + Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); - User PostWriter = post.getUser(); - String[] userIds = {String.valueOf(PostWriter.getOrgId())}; + String pushNotificationContent = "\"" + requestBody.getContent() + "\""; + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); - String pushNotificationContent = String.format("[%s의 댓글] : \"%s\"", - user.getName(), requestBody.getContents()); - String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + String[] userOrgIds = requestBody.getUserIds().stream() + .map(Object::toString) + .toArray(String[]::new); - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_COMMENT_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + String newCommentMentionPushNotificationTitle = String.format( + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), user.getName()); - pushNotificationService.sendPushNotification(pushRequestDto); + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + newCommentMentionPushNotificationTitle, + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); - return CommentV2CreateCommentResponseDto.of(savedComment.getId()); - } + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java index 98f3d5d7..ef4a1927 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/config/JpaAuditingConfig.java @@ -1,9 +1,21 @@ package org.sopt.makers.crew.main.common.config; +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing @Configuration public class JpaAuditingConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java b/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java index a586c7ee..c1b9172e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/config/SecurityConfig.java @@ -131,7 +131,7 @@ CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins( Arrays.asList("https://playground.sopt.org/", "http://localhost:3000/", - "https://sopt-internal-dev.pages.dev/")); + "https://sopt-internal-dev.pages.dev/", "https://crew.api.dev.sopt.org", "https://crew.api.prod.sopt.org")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "OPTIONS")); configuration.addAllowedHeader("*"); configuration.setAllowCredentials(false); diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java b/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java new file mode 100644 index 00000000..2ab8faa2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/exception/ServerException.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.common.exception; + +import org.springframework.http.HttpStatus; + +public class ServerException extends BaseException { + public ServerException(HttpStatus httpStatus) { + super(httpStatus); + } + + public ServerException(String responseMessage) { + super(HttpStatus.INTERNAL_SERVER_ERROR, responseMessage); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java index 8cb8a178..130043d8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/response/ErrorStatus.java @@ -19,8 +19,10 @@ public enum ErrorStatus { VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."), NOT_FOUND_MEETING("모임이 없습니다."), NOT_FOUND_POST("존재하지 않는 게시글입니다."), + NOT_FOUND_COMMENT("존재하지 않는 댓글입니다."), FULL_MEETING_CAPACITY("정원이 꽉 찼습니다."), ALREADY_APPLIED_MEETING("이미 지원한 모임입니다."), + ALREADY_REPORTED_COMMENT("이미 신고한 댓글입니다."), NOT_IN_APPLY_PERIOD("모임 지원 기간이 아닙니다."), MISSING_GENERATION_PART("내 프로필에서 기수/파트 정보를 입력해주세요."), NOT_ACTIVE_GENERATION("활동 기수가 아닙니다."), @@ -40,6 +42,7 @@ public enum ErrorStatus { /** * 500 SERVER_ERROR */ + NOTIFICATION_SERVER_ERROR("알림 서버에 에러가 발생했습니다."), INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다."); private final String errorCode; diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java new file mode 100644 index 00000000..55319527 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/MentionSecretStringRemover.java @@ -0,0 +1,35 @@ +package org.sopt.makers.crew.main.common.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class MentionSecretStringRemover { + + private static final String PREFIX_PATTERN = "-~!@#"; + private static final String SUFFIX_PATTERN = "\\[\\d+\\]%\\^&\\*\\+"; + + private static final String DELETED_PREFIX_PATTERN = "-~!@#@"; + private static final String DELETED_SUFFIX_PATTERN = "%^&*+"; + + public static String removeSecretString(String content) { + Pattern prefixPattern = Pattern.compile(PREFIX_PATTERN); + Pattern suffixPattern = Pattern.compile(SUFFIX_PATTERN); + + Matcher prefixMatcher = prefixPattern.matcher(content); + content = prefixMatcher.replaceAll(""); + + Matcher suffixMatcher = suffixPattern.matcher(content); + content = suffixMatcher.replaceAll(""); + + return content; + } + + public static String deleteMentionContent(String content, String mentionName, String mentionOrgId) { + String deletedMentionContent = + DELETED_PREFIX_PATTERN + mentionName + "[" + mentionOrgId + "]" + DELETED_SUFFIX_PATTERN; + + return content.replace(deletedMentionContent, "@_"); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java new file mode 100644 index 00000000..e10bac2d --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/RealTime.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile({"local", "dev", "prod"}) +public class RealTime implements Time { + @Override + public LocalDateTime now() { + return LocalDateTime.now(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java new file mode 100644 index 00000000..115a50b2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/Time.java @@ -0,0 +1,8 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +public interface Time { + LocalDateTime now(); + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java b/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java index d5b66e1a..91ce3151 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java +++ b/main/src/main/java/org/sopt/makers/crew/main/common/util/UserUtil.java @@ -1,8 +1,11 @@ package org.sopt.makers.crew.main.common.util; import java.security.Principal; +import java.util.Comparator; +import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.exception.UnAuthorizedException; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; @RequiredArgsConstructor public class UserUtil { @@ -13,4 +16,8 @@ public static Integer getUserId(Principal principal) { } return Integer.valueOf(principal.getName()); } + + public static UserActivityVO getRecentUserActivity(List userActivityVOs){ + return userActivityVOs.stream().max(Comparator.comparingInt(UserActivityVO::getGeneration)).orElse(null); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java index ee9925f7..92fe4795 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/Apply.java @@ -104,7 +104,6 @@ public Apply(EnApplyType type, Meeting meeting, Integer meetingId, User user, In this.content = content; this.appliedDate = LocalDateTime.now(); this.status = EnApplyStatus.WAITING; - this.meeting.addApply(this); } public void updateApplyStatus(EnApplyStatus status) { diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java index 76b74b90..26ca1b56 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java @@ -12,7 +12,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -public interface ApplyRepository extends JpaRepository { +public interface ApplyRepository extends JpaRepository, ApplySearchRepository { @Query("select a from Apply a join fetch a.meeting m where a.userId = :userId and a.status = :statusValue") List findAllByUserIdAndStatus(@Param("userId") Integer userId, @@ -20,6 +20,8 @@ List findAllByUserIdAndStatus(@Param("userId") Integer userId, List findAllByMeetingIdAndStatus(Integer meetingId, EnApplyStatus statusValue); + List findAllByMeetingId(Integer meetingId); + boolean existsByMeetingIdAndUserId(Integer meetingId, Integer userId); @Transactional diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java new file mode 100644 index 00000000..c6ac5a97 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepository.java @@ -0,0 +1,12 @@ +package org.sopt.makers.crew.main.entity.apply; + +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +public interface ApplySearchRepository { + Page findApplyList(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, + Integer meetingCreatorId, Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java new file mode 100644 index 00000000..f47765ae --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplySearchRepositoryImpl.java @@ -0,0 +1,66 @@ +package org.sopt.makers.crew.main.entity.apply; + +import static org.sopt.makers.crew.main.entity.apply.QApply.apply; +import static org.sopt.makers.crew.main.entity.user.QUser.user; + +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplicantDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.QApplyInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ApplySearchRepositoryImpl implements ApplySearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findApplyList(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + List content = getContent(queryCommand, pageable, meetingId, meetingCreatorId, userId); + JPAQuery countQuery = getCount(queryCommand, meetingId); + + return PageableExecutionUtils.getPage(content, + PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private List getContent(MeetingGetAppliesQueryDto queryCommand, Pageable pageable, Integer meetingId, Integer meetingCreatorId, Integer userId) { + boolean isStudyCreator = Objects.equals(meetingCreatorId, userId); + return queryFactory + .select(new QApplyInfoDto( + apply.id, isStudyCreator ? apply.content : Expressions.constant(""), + apply.appliedDate, apply.status, + new QApplicantDto(user.id, user.name, user.orgId, user.activities, user.profileImage, + user.phone))) + .from(apply) + .innerJoin(apply.user, user) + .where( + apply.meetingId.eq(meetingId), + apply.status.in(queryCommand.getStatus()) + ) + .orderBy(queryCommand.getDate().equals("desc") ? apply.appliedDate.desc() : apply.appliedDate.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + private JPAQuery getCount(MeetingGetAppliesQueryDto queryCommand, Integer meetingId) { + return queryFactory + .select(apply.count()) + .from(apply) + .where( + apply.meetingId.eq(meetingId), + apply.status.in(queryCommand.getStatus()) + ); + + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java index 48832145..76d44d4d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comment.java @@ -1,6 +1,7 @@ package org.sopt.makers.crew.main.entity.comment; -import jakarta.persistence.CascadeType; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -10,18 +11,18 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; +import jakarta.persistence.PostPersist; import jakarta.persistence.Table; + import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.makers.crew.main.entity.like.Like; + +import org.sopt.makers.crew.main.common.exception.ForbiddenException; import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -30,133 +31,134 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@EntityListeners(AuditingEntityListener.class) +@EntityListeners({AuditingEntityListener.class, Comment.CommentListener.class}) @Table(name = "comment") public class Comment { - /** - * 댓글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - - /** - * 댓글 내용 - */ - @Column(nullable = false) - private String contents; - - /** - * 댓글 깊이 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int depth; - - /** - * 댓글 순서 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int order; - - /** - * 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int userId; - - /** - * 댓글이 속한 게시글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId", nullable = false) - private Post post; - - /** - * 댓글이 속한 게시글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글에 대한 좋아요 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 댓글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 부모 댓글 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parentId") - private Comment parent; - - /** - * 부모 댓글의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private int parentId; - - /** - * 자식 댓글 목록 - */ - @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE) - private List children; - - /** - * 댓글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Comment(String contents, User user, Post post, Comment parent) { - this.contents = contents; - this.user = user; - this.post = post; - this.parent = parent; - this.depth = 0; - this.order = 0; - this.likeCount = 0; - this.post.addComment(this); - } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addChildrenComment(Comment comment) { - this.children.add(comment); - } - - public void addReport(Report report) { - this.reports.add(report); - } + private static final int PARENT_COMMENT = 0; + private static final String DELETE_COMMENT_CONTENT = "삭제된 댓글입니다."; + + + /** + * 댓글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 댓글 내용 + */ + @Column(nullable = false) + private String contents; + + /** + * 댓글/대댓글 구분자 (0 = 댓글, 1 = 대댓글) + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int depth; + + /** + * 댓글 순서 (댓글일 경우 0, 대댓글은 1부터 시작) + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int order; + + /** + * 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 댓글이 속한 게시글 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "postId", nullable = false) + private Post post; + + /** + * 댓글이 속한 게시글의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer postId; + + /** + * 댓글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + /** + * 부모 댓글의 고유 식별자 + */ + private Integer parentId; + + public static class CommentListener { + @PostPersist + public void setParentId(Comment comment) { + if (comment.depth == PARENT_COMMENT) { // 댓글일 경우 + comment.parentId = comment.id; + } + } + } + + @Builder + public Comment(String contents, int depth, int order, User user, Integer userId, Post post, Integer postId, + int likeCount, Integer parentId) { + this.contents = contents; + this.depth = depth; + this.order = order; + this.user = user; + this.userId = userId; + this.post = post; + this.postId = postId; + this.likeCount = likeCount; + this.parentId = parentId; + } + + public void updateContents(String contents) { + this.contents = contents; + } + + public void deleteParentComment() { + this.contents = DELETE_COMMENT_CONTENT; + this.user = null; + this.userId = null; + } + + public void validateWriter(Integer userId) { + if (!isWriter(userId)) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + } + + public boolean isWriter(Integer userId) { + return this.userId.equals(userId); + } + + public boolean isParentComment() { + return this.depth == PARENT_COMMENT; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java index 45e789e3..e23cb8f5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentRepository.java @@ -1,7 +1,29 @@ package org.sopt.makers.crew.main.entity.comment; +import java.util.List; +import java.util.Optional; + +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.response.ErrorStatus; import org.springframework.data.jpa.repository.JpaRepository; -public interface CommentRepository extends JpaRepository { +public interface CommentRepository extends JpaRepository, CommentSearchRepository { + + default Comment findByIdOrThrow(Integer commentId) { + return findById(commentId) + .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); + } + + Optional findFirstByParentIdOrderByOrderDesc(Integer parentId); + + List findAllByParentIdOrderByOrderDesc(Integer parentId); + + Optional findByIdAndPostId(Integer id, Integer postId); + + default Comment findByIdAndPostIdOrThrow(Integer id, Integer postId){ + return findByIdAndPostId(id, postId) + .orElseThrow(() -> new BadRequestException(ErrorStatus.NOT_FOUND_COMMENT.getErrorCode())); + } + List findAllByPostIdOrderByCreatedDate(Integer postId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java new file mode 100644 index 00000000..0eae30f5 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepository.java @@ -0,0 +1,9 @@ +package org.sopt.makers.crew.main.entity.comment; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CommentSearchRepository { + Page findAllByPostIdPagination(Integer postId, int depth, Pageable pageable); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java new file mode 100644 index 00000000..1d19f944 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/CommentSearchRepositoryImpl.java @@ -0,0 +1,50 @@ +package org.sopt.makers.crew.main.entity.comment; + +import static org.sopt.makers.crew.main.entity.comment.QComment.*; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class CommentSearchRepositoryImpl implements CommentSearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findAllByPostIdPagination(Integer postId, int depth, Pageable pageable) { + QComment parentComment = new QComment("parentComment"); + QComment childComment = new QComment("childComment"); + + List content = queryFactory + .select(childComment) + .from(parentComment) + .join(childComment).on(childComment.parentId.eq(parentComment.id)) + .where(parentComment.postId.eq(postId)) + .orderBy(parentComment.createdDate.asc(), childComment.createdDate.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = getCount(postId); + + return PageableExecutionUtils.getPage(content, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private JPAQuery getCount(Integer postId) { + return queryFactory + .select(comment.count()) + .from(comment) + .where(comment.postId.eq(postId)); + + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java new file mode 100644 index 00000000..bb5b91b0 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/comment/Comments.java @@ -0,0 +1,24 @@ +package org.sopt.makers.crew.main.entity.comment; + +import java.util.List; + +import org.sopt.makers.crew.main.common.util.MentionSecretStringRemover; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class Comments { + private final List comments; + + public void deleteMention(String mentionName, String mentionOrgId){ + comments.forEach(comment -> { + String deletedMentionContent = MentionSecretStringRemover.deleteMentionContent(comment.getContents(), + mentionName, mentionOrgId); + comment.updateContents(deletedMentionContent); + }); + } + + public boolean hasChild(){ + return !comments.isEmpty(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java index 984065b6..345f93ff 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/Like.java @@ -3,21 +3,15 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.sopt.makers.crew.main.entity.comment.Comment; -import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -32,7 +26,7 @@ public class Like { */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + private Integer id; /** * 좋아요 누른 날짜 @@ -41,53 +35,29 @@ public class Like { @CreatedDate private LocalDateTime createdDate; - /** - * 좋아요 누른사람 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - /** * 좋아요 누른사람 id */ @Column(insertable = false, updatable = false) - private int userId; - - /** - * 게시글 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "postId") - private Post post; + private Integer userId; /** * 게시글 id - 게시글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int postId; - - /** - * 댓글 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "commentId") - private Comment comment; + private Integer postId; /** * 댓글 id - 댓글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int commentId; + private Integer commentId; @Builder - public Like(User user, int userId, Post post, int postId, Comment comment, - int commentId) { - this.user = user; + public Like(Integer userId, Integer postId, Integer commentId) { this.userId = userId; - this.post = post; this.postId = postId; - this.comment = comment; this.commentId = commentId; } + } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java new file mode 100644 index 00000000..142f20e8 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/LikeRepository.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.entity.like; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { + + List findAllByUserIdAndPostIdNotNull(Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java b/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java new file mode 100644 index 00000000..3f293373 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/like/MyLikes.java @@ -0,0 +1,17 @@ +package org.sopt.makers.crew.main.entity.like; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MyLikes { + private final List myLikes; + + public boolean isLikeComment(Integer commentId){ + return myLikes.stream() + .anyMatch(like -> like.getCommentId().equals(commentId)); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java index 47c6964c..b2968c1d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java @@ -14,15 +14,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + import org.hibernate.annotations.Parameter; import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.apply.Apply; @@ -31,7 +32,6 @@ import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; -import org.sopt.makers.crew.main.entity.post.Post; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -42,200 +42,179 @@ @Table(name = "meeting") public class Meeting { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 개설한 유저 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 유저 id - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 지원 or 초대 정보 - */ - @OneToMany(mappedBy = "meeting", cascade = CascadeType.REMOVE) - private List appliedInfo; - - /** - * 모임 제목 - */ - @Column(nullable = false) - private String title; - - /** - * 모임 카테고리 - */ - @Column(name = "category", nullable = false) - @Convert(converter = MeetingCategoryConverter.class) - private MeetingCategory category; - - /** - * 이미지 - */ - @Column(name = "imageURL", columnDefinition = "jsonb") - @Type(JsonBinaryType.class) - //@JdbcTypeCode(SqlTypes.JSON) - private List imageURL; - - /** - * 모집 시작 기간 - */ - @Column(name = "startDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime startDate; - - /** - * 모집 마감 기간 - */ - @Column(name = "endDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime endDate; - - /** - * 모집 인원 - */ - @Column(name = "capacity", nullable = false) - private Integer capacity; - - /** - * 모임 소개 - */ - @Column(name = "desc", nullable = false) - private String desc; - - /** - * 진행방식 소개 - */ - @Column(name = "processDesc", nullable = false) - private String processDesc; - - /** - * 모임 시작 기간 - */ - @Column(name = "mStartDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime mStartDate; - - /** - * 모임 마감 기간 - */ - @Column(name = "mEndDate", nullable = false, columnDefinition = "TIMESTAMP") - private LocalDateTime mEndDate; - - /** - * 개설자 소개 - */ - @Column(name = "leaderDesc", nullable = false) - private String leaderDesc; - - /** - * 모집 대상 - */ - @Column(name = "targetDesc", nullable = false) - private String targetDesc; - - /** - * 유의 사항 - */ - @Column(name = "note") - private String note; - - /** - * 멘토 필요 여부 - */ - @Column(name = "isMentorNeeded", nullable = false) - private Boolean isMentorNeeded; - - /** - * 활동 기수만 참여 가능한지 여부 - */ - @Column(name = "canJoinOnlyActiveGeneration", nullable = false) - private Boolean canJoinOnlyActiveGeneration; - - /** - * 모임 기수 - */ - @Column(name = "createdGeneration", nullable = false) - private Integer createdGeneration; - - /** - * 대상 활동 기수 - */ - @Column(name = "targetActiveGeneration") - private Integer targetActiveGeneration; - - /** - * 모임 참여 가능한 파트 - */ - @Type(value = EnumArrayType.class, - parameters = @Parameter(name = AbstractArrayType.SQL_ARRAY_TYPE, - value = "meeting_joinableparts_enum")) - @Column(name = "joinableParts", columnDefinition = "meeting_joinableparts_enum[]") - private MeetingJoinablePart[] joinableParts; - - /** - * 게시글 리스트 - */ - @OneToMany(mappedBy = "meeting", cascade = CascadeType.REMOVE) - private List posts; - - @Builder - public Meeting(User user, Integer userId, List appliedInfo, String title, MeetingCategory category, - List imageURL, LocalDateTime startDate, LocalDateTime endDate, Integer capacity, - String desc, String processDesc, LocalDateTime mStartDate, LocalDateTime mEndDate, - String leaderDesc, String targetDesc, String note, Boolean isMentorNeeded, - Boolean canJoinOnlyActiveGeneration, Integer createdGeneration, - Integer targetActiveGeneration, MeetingJoinablePart[] joinableParts) { - this.user = user; - this.userId = userId; - this.appliedInfo = appliedInfo != null ? appliedInfo : new ArrayList<>(); - this.title = title; - this.category = category; - this.imageURL = imageURL; - this.startDate = startDate; - this.endDate = endDate; - this.capacity = capacity; - this.desc = desc; - this.processDesc = processDesc; - this.mStartDate = mStartDate; - this.mEndDate = mEndDate; - this.leaderDesc = leaderDesc; - this.targetDesc = targetDesc; - this.note = note; - this.isMentorNeeded = isMentorNeeded; - this.canJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration; - this.createdGeneration = createdGeneration; - this.targetActiveGeneration = targetActiveGeneration; - this.joinableParts = joinableParts; - } - - public void addApply(Apply apply) { - appliedInfo.add(apply); - } - - public void addPost(Post post) { - posts.add(post); - } - - /** - * 모임 모집상태 확인 - * - * @return 모임 모집상태 - */ - public Integer getMeetingStatus() { - LocalDateTime now = LocalDateTime.now(); - if (now.isBefore(startDate)) { - return EnMeetingStatus.BEFORE_START.getValue(); - } else if (now.isBefore(endDate)) { - return EnMeetingStatus.APPLY_ABLE.getValue(); - } else { - return EnMeetingStatus.RECRUITMENT_COMPLETE.getValue(); - } - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 개설한 유저 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 유저 id + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 모임 제목 + */ + @Column(nullable = false) + private String title; + + /** + * 모임 카테고리 + */ + @Column(name = "category", nullable = false) + @Convert(converter = MeetingCategoryConverter.class) + private MeetingCategory category; + + /** + * 이미지 + */ + @Column(name = "imageURL", columnDefinition = "jsonb") + @Type(JsonBinaryType.class) + //@JdbcTypeCode(SqlTypes.JSON) + private List imageURL; + + /** + * 모집 시작 기간 + */ + @Column(name = "startDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime startDate; + + /** + * 모집 마감 기간 + */ + @Column(name = "endDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime endDate; + + /** + * 모집 인원 + */ + @Column(name = "capacity", nullable = false) + private Integer capacity; + + /** + * 모임 소개 + */ + @Column(name = "desc", nullable = false) + private String desc; + + /** + * 진행방식 소개 + */ + @Column(name = "processDesc", nullable = false) + private String processDesc; + + /** + * 모임 시작 기간 + */ + @Column(name = "mStartDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime mStartDate; + + /** + * 모임 마감 기간 + */ + @Column(name = "mEndDate", nullable = false, columnDefinition = "TIMESTAMP") + private LocalDateTime mEndDate; + + /** + * 개설자 소개 + */ + @Column(name = "leaderDesc", nullable = false) + private String leaderDesc; + + /** + * 모집 대상 + */ + @Column(name = "targetDesc", nullable = false) + private String targetDesc; + + /** + * 유의 사항 + */ + @Column(name = "note") + private String note; + + /** + * 멘토 필요 여부 + */ + @Column(name = "isMentorNeeded", nullable = false) + private Boolean isMentorNeeded; + + /** + * 활동 기수만 참여 가능한지 여부 + */ + @Column(name = "canJoinOnlyActiveGeneration", nullable = false) + private Boolean canJoinOnlyActiveGeneration; + + /** + * 모임 기수 + */ + @Column(name = "createdGeneration", nullable = false) + private Integer createdGeneration; + + /** + * 대상 활동 기수 + */ + @Column(name = "targetActiveGeneration") + private Integer targetActiveGeneration; + + /** + * 모임 참여 가능한 파트 + */ + @Type(value = EnumArrayType.class, + parameters = @Parameter(name = AbstractArrayType.SQL_ARRAY_TYPE, + value = "meeting_joinableparts_enum")) + @Column(name = "joinableParts", columnDefinition = "meeting_joinableparts_enum[]") + private MeetingJoinablePart[] joinableParts; + + @Builder + public Meeting(User user, Integer userId, List appliedInfo, String title, MeetingCategory category, + List imageURL, LocalDateTime startDate, LocalDateTime endDate, Integer capacity, + String desc, String processDesc, LocalDateTime mStartDate, LocalDateTime mEndDate, + String leaderDesc, String targetDesc, String note, Boolean isMentorNeeded, + Boolean canJoinOnlyActiveGeneration, Integer createdGeneration, + Integer targetActiveGeneration, MeetingJoinablePart[] joinableParts) { + this.user = user; + this.userId = userId; + this.title = title; + this.category = category; + this.imageURL = imageURL; + this.startDate = startDate; + this.endDate = endDate; + this.capacity = capacity; + this.desc = desc; + this.processDesc = processDesc; + this.mStartDate = mStartDate; + this.mEndDate = mEndDate; + this.leaderDesc = leaderDesc; + this.targetDesc = targetDesc; + this.note = note; + this.isMentorNeeded = isMentorNeeded; + this.canJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration; + this.createdGeneration = createdGeneration; + this.targetActiveGeneration = targetActiveGeneration; + this.joinableParts = joinableParts; + } + + /** + * 모임 모집상태 확인 + * + * @return 모임 모집상태 + */ + public Integer getMeetingStatus() { + LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(startDate)) { + return EnMeetingStatus.BEFORE_START.getValue(); + } else if (now.isBefore(endDate)) { + return EnMeetingStatus.APPLY_ABLE.getValue(); + } else { + return EnMeetingStatus.RECRUITMENT_COMPLETE.getValue(); + } + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java index a3824a0d..43f7b61a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/Notice.java @@ -26,7 +26,7 @@ public class Notice { */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; + private Integer id; /** * 공지사항 제목 diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java new file mode 100644 index 00000000..684654f0 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/notice/NoticeRepository.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.entity.notice; + +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeRepository extends JpaRepository { + + List findByExposeStartDateBeforeAndExposeEndDateAfter(LocalDateTime now1, LocalDateTime now2); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java index ef8bc778..55fcb12c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/Post.java @@ -11,20 +11,18 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; + import org.hibernate.annotations.Type; import org.sopt.makers.crew.main.entity.comment.Comment; -import org.sopt.makers.crew.main.entity.like.Like; import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.report.Report; import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -37,130 +35,107 @@ @Table(name = "post") public class Post { - /** - * 게시글의 고유 식별자 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - /** - * 게시글 제목 - */ - @Column(nullable = false) - private String title; - - /** - * 게시글 내용 - */ - @Column(nullable = false, columnDefinition = "TEXT") - private String contents; - - /** - * 게시글 작성일 - */ - @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") - @CreatedDate - private LocalDateTime createdDate; - - /** - * 게시글 수정일 - */ - @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") - @LastModifiedDate - private LocalDateTime updatedDate; - - /** - * 조회수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int viewCount; - - /** - * 이미지 리스트 - */ - @Column(name = "images", columnDefinition = "text[]") - @Type(StringArrayType.class) - private String[] images; - - /** - * 작성자 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "userId", nullable = false) - private User user; - - /** - * 작성자의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer userId; - - /** - * 게시글이 속한 미팅 정보 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "meetingId", nullable = false) - private Meeting meeting; - - /** - * 게시글이 속한 미팅의 고유 식별자 - */ - @Column(insertable = false, updatable = false) - private Integer meetingId; - - /** - * 게시글에 달린 댓글 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List comments = new ArrayList<>(); - - /** - * 게시글에 달린 댓글 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int commentCount; - - /** - * 게시글에 대한 좋아요 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 게시글에 대한 좋아요 수 - */ - @Column(nullable = false, columnDefinition = "int default 0") - private int likeCount; - - /** - * 게시글에 대한 신고 목록 - */ - @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE) - private List reports; - - @Builder - public Post(String title, String contents, String[] images, User user, Meeting meeting) { - this.title = title; - this.contents = contents; - this.viewCount = 0; - this.images = images; - this.user = user; - this.meeting = meeting; - this.commentCount = 0; - this.likeCount = 0; - } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addComment(Comment comment) { - this.comments.add(comment); - this.commentCount++; - } - - public void addReport(Report report) { - this.reports.add(report); - } + /** + * 게시글의 고유 식별자 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + /** + * 게시글 제목 + */ + @Column(nullable = false) + private String title; + + /** + * 게시글 내용 + */ + @Column(nullable = false, columnDefinition = "TEXT") + private String contents; + + /** + * 게시글 작성일 + */ + @Column(name = "createdDate", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime createdDate; + + /** + * 게시글 수정일 + */ + @Column(name = "updatedDate", nullable = false, columnDefinition = "TIMESTAMP") + @LastModifiedDate + private LocalDateTime updatedDate; + + /** + * 조회수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int viewCount; + + /** + * 이미지 리스트 + */ + @Column(name = "images", columnDefinition = "text[]") + @Type(StringArrayType.class) + private String[] images; + + /** + * 작성자 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId", nullable = false) + private User user; + + /** + * 작성자의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer userId; + + /** + * 게시글이 속한 미팅 정보 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meetingId", nullable = false) + private Meeting meeting; + + /** + * 게시글이 속한 미팅의 고유 식별자 + */ + @Column(insertable = false, updatable = false) + private Integer meetingId; + + /** + * 게시글에 달린 댓글 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int commentCount; + + /** + * 게시글에 대한 좋아요 수 + */ + @Column(nullable = false, columnDefinition = "int default 0") + private int likeCount; + + @Builder + public Post(String title, String contents, String[] images, User user, Meeting meeting) { + this.title = title; + this.contents = contents; + this.viewCount = 0; + this.images = images; + this.user = user; + this.meeting = meeting; + this.commentCount = 0; + this.likeCount = 0; + } + + public void increaseCommentCount() { + this.commentCount++; + } + + public void decreaseCommentCount() { + this.commentCount--; + } } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java index 816719c7..ef23d79b 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostRepository.java @@ -6,12 +6,14 @@ import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.springframework.data.jpa.repository.JpaRepository; -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, PostSearchRepository { - Optional findById(Integer postId); + Optional findById(Integer postId); - default Post findByIdOrThrow(Integer postId) { - return findById(postId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); - } + default Post findByIdOrThrow(Integer postId) { + return findById(postId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_POST.getErrorCode())); + } + + Optional findFirstByMeetingIdOrderByIdDesc(Integer meetingId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java new file mode 100644 index 00000000..9fbf50a6 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepository.java @@ -0,0 +1,10 @@ +package org.sopt.makers.crew.main.entity.post; + +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PostSearchRepository { + Page findPostList(PostGetPostsCommand queryCommand, Pageable pageable, Integer userId); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java new file mode 100644 index 00000000..befc4d88 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/post/PostSearchRepositoryImpl.java @@ -0,0 +1,129 @@ +package org.sopt.makers.crew.main.entity.post; + +import static org.sopt.makers.crew.main.entity.comment.QComment.comment; +import static org.sopt.makers.crew.main.entity.like.QLike.like; +import static org.sopt.makers.crew.main.entity.meeting.QMeeting.meeting; +import static org.sopt.makers.crew.main.entity.post.QPost.post; +import static org.sopt.makers.crew.main.entity.user.QUser.user; + +import com.querydsl.core.group.GroupBy; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.CommenterThumbnails; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailBaseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostDetailBaseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostMeetingDto; +import org.sopt.makers.crew.main.post.v2.dto.response.QPostWriterInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostSearchRepositoryImpl implements PostSearchRepository { + private final JPAQueryFactory queryFactory; + + @Override + public Page findPostList(PostGetPostsCommand queryCommand, Pageable pageable, + Integer userId) { + Integer meetingId = queryCommand.getMeetingId().orElse(null); + + List content = getContent(pageable, meetingId, userId); + JPAQuery countQuery = getCount(meetingId); + + return PageableExecutionUtils.getPage(content, + PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()), countQuery::fetchFirst); + } + + private List getContent(Pageable pageable, Integer meetingId, + Integer userId) { + List responseDtos = new ArrayList<>(); + + List postDetailList = queryFactory + .select(new QPostDetailBaseDto( + post.id, + post.title, + post.contents, + post.createdDate, + post.images, + new QPostWriterInfoDto( + post.user.id, + post.user.orgId, + post.user.name, + post.user.profileImage + ), + post.likeCount, + ExpressionUtils.as( + JPAExpressions.selectFrom(like) + .where(like.postId.eq(post.id).and(like.userId.eq(userId))) + .exists() + , "isLiked" + ), + post.viewCount, + post.commentCount, + new QPostMeetingDto( + post.meeting.id, + post.meeting.title, + post.meeting.category + ) + )) + .from(post) + .innerJoin(post.user, user) + .innerJoin(post.meeting, meeting) + .where(meetingIdEq(meetingId)) + .orderBy(post.id.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 모든 게시글 ID를 추출 + List postIds = postDetailList.stream() + .map(PostDetailBaseDto::getId) + .collect(Collectors.toList()); + + // 게시글 ID 리스트를 사용하여 한 번에 모든 댓글 작성자의 프로필 이미지를 조회 + Map> commenterThumbnailsMap = queryFactory + .select(comment.post.id, comment.user.profileImage) + .from(comment) + .where(comment.post.id.in(postIds)) + .groupBy(comment.post.id, comment.user.id, comment.user.profileImage) + .orderBy(comment.post.id.asc(), comment.user.id.asc()) + .limit(3) + .transform(GroupBy.groupBy(comment.post.id).as(GroupBy.list(comment.user.profileImage))); + + // 각 게시글별로 댓글 작성자의 프로필 이미지 리스트를 설정 + for (PostDetailBaseDto postDetail : postDetailList) { + CommenterThumbnails commenterThumbnails = new CommenterThumbnails( + commenterThumbnailsMap.getOrDefault(postDetail.getId(), + Collections.emptyList())); + responseDtos.add(PostDetailResponseDto.of(postDetail, commenterThumbnails)); + } + + return responseDtos; + } + + private JPAQuery getCount(Integer meetingId) { + return queryFactory + .select(post.count()) + .from(post) + .where(meetingIdEq(meetingId)); + } + + private BooleanExpression meetingIdEq(Integer meetingId) { + return meetingId == null ? null : post.meetingId.eq(meetingId); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java b/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java index 9ae43979..2827aee4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/report/Report.java @@ -24,7 +24,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "like") +@Table(name = "report") @EntityListeners(AuditingEntityListener.class) public class Report { /** @@ -65,7 +65,7 @@ public class Report { * 게시글 id - 게시글 좋아요가 아닐 경우 null */ @Column(insertable = false, updatable = false) - private int postId; + private Integer postId; /** * 댓글 diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java new file mode 100644 index 00000000..56071d7d --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/report/ReportRepository.java @@ -0,0 +1,11 @@ +package org.sopt.makers.crew.main.entity.report; + +import java.util.Optional; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + + Optional findByCommentAndUser(Comment comment, User user); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java index e2b19fec..e13ecae5 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/User.java @@ -1,28 +1,22 @@ package org.sopt.makers.crew.main.entity.user; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; + import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; -import java.util.ArrayList; -import java.util.HashSet; +import java.util.Comparator; import java.util.List; -import java.util.Set; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Type; -import org.sopt.makers.crew.main.entity.apply.Apply; -import org.sopt.makers.crew.main.entity.like.Like; -import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.post.Post; -import org.sopt.makers.crew.main.entity.report.Report; +import org.sopt.makers.crew.main.common.exception.ServerException; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; @Entity @@ -54,7 +48,7 @@ public class User { /** * 활동 목록 */ - @Column(name = "activities",columnDefinition = "jsonb") + @Column(name = "activities", columnDefinition = "jsonb") @Type(JsonBinaryType.class) private List activities; @@ -70,38 +64,8 @@ public class User { @Column(name = "phone") private String phone; - /** - * 내가 생성한 모임 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private Set meetings = new HashSet<>(); - - /** - * 내가 지원한 내역 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List applies = new ArrayList<>(); - - /** - * 작성한 게시글 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List posts = new ArrayList<>(); - - /** - * 좋아요 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List likes = new ArrayList<>(); - - /** - * 신고 내역 - */ - @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE) - private List reports = new ArrayList<>(); - @Builder - public User(String name, int orgId, List activities, String profileImage, + public User(String name, Integer orgId, List activities, String profileImage, String phone) { this.name = name; this.orgId = orgId; @@ -110,21 +74,14 @@ public User(String name, int orgId, List activities, String prof this.phone = phone; } - public void addMeeting(Meeting meeting) { - this.meetings.add(meeting); + public void setUserIdForTest(Integer userId) { + this.id = userId; } - public void addApply(Apply apply) { - this.applies.add(apply); + public UserActivityVO getRecentActivityVO(){ + return activities.stream() + .filter(userActivityVO -> userActivityVO.getPart() != null) + .max(Comparator.comparingInt(UserActivityVO::getGeneration)) + .orElseThrow(() -> new ServerException(INTERNAL_SERVER_ERROR.getErrorCode())); } - - public void addLike(Like like) { - this.likes.add(like); - } - - public void addReport(Report report) { - this.reports.add(report); - } - - public void setUserIdForTest(Integer userId){ this.id = userId;} } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java index 8cd6a8ea..0f390f1c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java @@ -2,22 +2,26 @@ import static org.sopt.makers.crew.main.common.response.ErrorStatus.NO_CONTENT_EXCEPTION; +import java.util.List; import java.util.Optional; + import org.sopt.makers.crew.main.common.exception.NoContentException; import org.sopt.makers.crew.main.common.exception.UnAuthorizedException; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { - Optional findByOrgId(Integer orgId); + Optional findByOrgId(Integer orgId); + + default User findByIdOrThrow(Integer userId) { + return findById(userId).orElseThrow(() -> new UnAuthorizedException()); + } - default User findByIdOrThrow(Integer userId) { - return findById(userId).orElseThrow(() -> new UnAuthorizedException()); - } + default User findByOrgIdOrThrow(Integer orgUserId) { + return findByOrgId(orgUserId).orElseThrow( + () -> new NoContentException( + NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 + } - default User findByOrgIdOrThrow(Integer orgUserId) { - return findByOrgId(orgUserId).orElseThrow( - () -> new NoContentException( - NO_CONTENT_EXCEPTION.getErrorCode())); //유저가 아직 모임 서비스를 이용 전이기 때문에 - } + List findByIdIn(List userIds); } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java index 068c951b..7afb2bbe 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/enums/UserPart.java @@ -28,7 +28,7 @@ public enum UserPart { GENERAL_AFFAIRS("총무"), OPERATION_LEADER("운영 팀장"), MEDIA_LEADER("미디어 팀장"), - MAKERS_LEADER("메이커스 리드"); + MAKERS_LEADER("메이커스 팀장"); private final String value; diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java index 83096c9c..f8e92a3d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationEnums.java @@ -7,13 +7,18 @@ @Getter @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public enum PushNotificationEnums { - PUSH_NOTIFICATION_ACTION("send"), + PUSH_NOTIFICATION_ACTION("send"), - PUSH_NOTIFICATION_CATEGORY("NEWS"), + PUSH_NOTIFICATION_CATEGORY("NEWS"), - NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), - NEW_COMMENT_PUSH_NOTIFICATION_TITLE("📢내가 작성한 모임 피드에 새로운 댓글이 달렸어요."), - ; + NEW_POST_PUSH_NOTIFICATION_TITLE("✏️내 모임에 새로운 글이 업로드됐어요."), + NEW_COMMENT_PUSH_NOTIFICATION_TITLE("💬나의 모임 피드에 새로운 댓글이 달렸어요."), - private final String value; + NEW_COMMENT_MENTION_PUSH_NOTIFICATION_TITLE("💬%s님이 회원님을 언급했어요."), + + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE("✏️모임 피드에서 회원님이 언급됐어요."), + + + ; + private final String value; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java index 00749056..947b9b06 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java +++ b/main/src/main/java/org/sopt/makers/crew/main/internal/notification/PushNotificationService.java @@ -1,29 +1,36 @@ package org.sopt.makers.crew.main.internal.notification; +import static org.sopt.makers.crew.main.common.response.ErrorStatus.*; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_ACTION; import java.util.UUID; + import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; -import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationResponseDto; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor +@Slf4j public class PushNotificationService { - @Value("${push-notification.x-api-key}") - private String pushNotificationApiKey; + @Value("${push-notification.x-api-key}") + private String pushNotificationApiKey; - @Value("${push-notification.service}") - private String service; + @Value("${push-notification.service}") + private String service; - private final PushNotificationServerClient pushServerClient; + private final PushNotificationServerClient pushServerClient; - public void sendPushNotification(PushNotificationRequestDto request) { - PushNotificationResponseDto response = - pushServerClient.sendPushNotification(pushNotificationApiKey, - PUSH_NOTIFICATION_ACTION.getValue(), UUID.randomUUID().toString(), service, request); - } + public void sendPushNotification(PushNotificationRequestDto request) { + try { + pushServerClient.sendPushNotification(pushNotificationApiKey, + PUSH_NOTIFICATION_ACTION.getValue(), UUID.randomUUID().toString(), service, request); + }catch (Exception e){ + log.error(NOTIFICATION_SERVER_ERROR.getErrorCode(), e); + } + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java index 9a31956c..4331585e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Api.java @@ -11,9 +11,11 @@ import jakarta.validation.Valid; import java.security.Principal; import java.util.List; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -62,4 +64,11 @@ ResponseEntity applyMeeting(@RequestBody Meeti ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, Principal principal); + + @Operation(summary = "모임 지원자/참여자 조회", description = "모임 지원자/참여자 조회 (모임장이면 지원자, 아니면 참여자 조회)") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "모임 지원자/참여자 조회 성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content),}) + ResponseEntity findApplyList(@PathVariable Integer meetingId, + @ModelAttribute MeetingGetAppliesQueryDto queryCommand, + Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java index 1efa33d7..e05957c8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/MeetingV2Controller.java @@ -6,9 +6,11 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -77,4 +79,16 @@ public ResponseEntity applyMeetingCancel(@PathVariable Integer meetingId, meetingV2Service.applyMeetingCancel(meetingId, userId); return ResponseEntity.status(HttpStatus.OK).build(); } + + @Override + @GetMapping("/{meetingId}/list") + public ResponseEntity findApplyList(@PathVariable Integer meetingId, + @ModelAttribute MeetingGetAppliesQueryDto queryCommand, + Principal principal) { + + Integer userId = UserUtil.getUserId(principal); + + return ResponseEntity.status(HttpStatus.OK) + .body(meetingV2Service.findApplyList(queryCommand, meetingId, userId)); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java new file mode 100644 index 00000000..3edecb09 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/query/MeetingGetAppliesQueryDto.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.query; + +import java.util.Arrays; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; + +@Getter +@Setter +public class MeetingGetAppliesQueryDto extends PageOptionsDto { + + private List status; + private String date; + + @Builder + public MeetingGetAppliesQueryDto(int page, int take, List status, String date) { + super(page, take); + this.status = (status == null || status.isEmpty()) ? + Arrays.asList(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT) : status; + this.date = date == null ? "desc" : date; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java new file mode 100644 index 00000000..4eb516a3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplicantDto.java @@ -0,0 +1,30 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.util.Comparator; +import java.util.List; +import lombok.Getter; +import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; + +@Getter +public class ApplicantDto { + private final Integer id; + private final String name; + private final Integer orgId; + private final UserActivityVO recentActivity; + private final String profileImage; + private final String phone; + + @QueryProjection + public ApplicantDto(Integer id, String name, Integer orgId, List userActivityVOs, String profileImage, + String phone) { + + this.id = id; + this.name = name; + this.orgId = orgId; + this.recentActivity = UserUtil.getRecentUserActivity(userActivityVOs); + this.profileImage = profileImage; + this.phone = phone; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java new file mode 100644 index 00000000..23148265 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/ApplyInfoDto.java @@ -0,0 +1,25 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDateTime; +import lombok.Getter; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; + +@Getter +public class ApplyInfoDto { + private final Integer id; + private final String content; + private final LocalDateTime appliedDate; + private final EnApplyStatus status; + private final ApplicantDto user; + + @QueryProjection + public ApplyInfoDto(Integer id, String content, LocalDateTime appliedDate, EnApplyStatus status, + ApplicantDto user) { + this.id = id; + this.content = content; + this.appliedDate = appliedDate; + this.status = status; + this.user = user; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java new file mode 100644 index 00000000..f1e85883 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingGetApplyListResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; + +@Getter +@AllArgsConstructor(staticName = "of") +public class MeetingGetApplyListResponseDto { + + private final List apply; + private final PageMetaDto meta; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java index c443ce87..3e8d647a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingBannerResponseDto.java @@ -52,7 +52,7 @@ public class MeetingV2GetMeetingBannerResponseDto { /** 가입된 지원자 수 */ private Integer approvedUserCount; /** 개설자 정보 */ - private Optional user; + private MeetingV2GetMeetingBannerResponseUserDto user; /** 미팅 상태 */ private Integer status; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java index e6d2b4f9..5a1362bf 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2Service.java @@ -1,9 +1,11 @@ package org.sopt.makers.crew.main.meeting.v2.service; import java.util.List; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; @@ -21,4 +23,7 @@ MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId); void applyMeetingCancel(Integer meetingId, Integer userId); + + MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryCommand, Integer meetingId, + Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index c237a714..bf613ef8 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -18,7 +18,9 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.BadRequestException; import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; @@ -31,21 +33,27 @@ import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.entity.user.enums.UserPart; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.MeetingMapper; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingGetApplyListResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2ApplyMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2CreateMeetingResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetMeetingBannerResponseUserDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -54,218 +62,236 @@ @Transactional(readOnly = true) public class MeetingV2ServiceImpl implements MeetingV2Service { - private final static int ZERO = 0; - - private final UserRepository userRepository; - private final ApplyRepository applyRepository; - private final MeetingRepository meetingRepository; - - private final MeetingMapper meetingMapper; - private final ApplyMapper applyMapper; - - @Override - public MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( - MeetingV2GetAllMeetingByOrgUserQueryDto queryDto) { - int page = queryDto.getPage(); - int take = queryDto.getTake(); - - Optional user = userRepository.findByOrgId(queryDto.getOrgUserId()); - List userJoinedList = new ArrayList<>(); - - if (!user.isEmpty()) { - User existUser = user.get(); - userJoinedList = Stream - .concat(existUser.getMeetings().stream(), - applyRepository.findAllByUserIdAndStatus(existUser.getId(), EnApplyStatus.APPROVE) - .stream().map(apply -> apply.getMeeting())) - .map(meeting -> MeetingV2GetAllMeetingByOrgUserMeetingDto.of(meeting.getId(), - checkMeetingLeader(meeting, existUser.getId()), meeting.getTitle(), - meeting.getImageURL().get(0).getUrl(), meeting.getCategory().getValue(), - meeting.getMStartDate(), meeting.getMEndDate(), checkActivityStatus(meeting))) - .sorted(Comparator.comparing(MeetingV2GetAllMeetingByOrgUserMeetingDto::getId).reversed()) - .collect(Collectors.toList()); - } - - List pagedUserJoinedList = - userJoinedList.stream().skip((long) (page - 1) * take) // 스킵할 아이템 수 계산 - .limit(take) // 페이지당 아이템 수 제한 - .collect(Collectors.toList()); - PageOptionsDto pageOptionsDto = new PageOptionsDto(page, take); - PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, userJoinedList.size()); - return MeetingV2GetAllMeetingByOrgUserDto.of(pagedUserJoinedList, pageMetaDto); - } - - @Override - public List getMeetingBanner() { - List meetingBanners = this.meetingRepository.findAll() - .stream().sorted(Comparator.comparing(Meeting::getId).reversed()).limit(20).map(meeting -> { - List post = meeting.getPosts().stream() - .sorted(Comparator.comparing(Post::getId).reversed()).limit(1).toList(); - List applies = meeting.getAppliedInfo(); - - Integer applicantCount = applies.size(); - Integer appliedUserCount = applies.stream() - .filter(apply -> apply.getStatus().equals(EnApplyStatus.APPROVE)).toList().size(); - - Optional recentActivityDate = - post.isEmpty() ? Optional.empty() : Optional.of(post.get(0).getCreatedDate()); - - Optional meetingLeader = userRepository - .findById(meeting.getUserId()).map(user -> MeetingV2GetMeetingBannerResponseUserDto - .of(user.getId(), user.getName(), user.getOrgId(), user.getProfileImage())); - - return MeetingV2GetMeetingBannerResponseDto.of(meeting.getId(), meeting.getUserId(), - meeting.getTitle(), meeting.getCategory(), meeting.getImageURL(), - meeting.getMStartDate(), meeting.getMEndDate(), meeting.getStartDate(), - meeting.getEndDate(), - meeting.getCapacity(), recentActivityDate, meeting.getTargetActiveGeneration(), - meeting.getJoinableParts(), applicantCount, appliedUserCount, meetingLeader, - meeting.getMeetingStatus()); - }).toList(); - - return meetingBanners; - } - - - @Override - @Transactional - public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBodyDto requestBody, Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - - if (user.getActivities() == null) { - throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); - } - - if (requestBody.getFiles().size() == ZERO || requestBody.getJoinableParts().length == ZERO) { - throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); - } - - Meeting meeting = meetingMapper.toMeetingEntity(requestBody, - getTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, - user.getId()); - - Meeting savedMeeting = meetingRepository.save(meeting); - return MeetingV2CreateMeetingResponseDto.of(savedMeeting.getId()); - } - - @Override - @Transactional - public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); - User user = userRepository.findByIdOrThrow(userId); - - validateMeetingCapacity(meeting); - validateUserAlreadyApplied(meeting, userId); - validateApplyPeriod(meeting); - validateUserActivities(user); - validateUserJoinableParts(user, meeting); - - Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, - userId); - Apply savedApply = applyRepository.save(apply); - return MeetingV2ApplyMeetingResponseDto.of(savedApply.getId()); - } - - @Override - @Transactional - public void applyMeetingCancel(Integer meetingId, Integer userId) { - boolean exists = applyRepository.existsByMeetingIdAndUserId(meetingId, userId); - - if (!exists) { - throw new BadRequestException(NOT_FOUND_APPLY.getErrorCode()); - } - - applyRepository.deleteByMeetingIdAndUserId(meetingId, userId); - } - - private Boolean checkMeetingLeader(Meeting meeting, Integer userId) { - return meeting.getUserId().equals(userId); - } - - private Boolean checkActivityStatus(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime mStartDate = meeting.getMStartDate(); - LocalDateTime mEndDate = meeting.getMEndDate(); - return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); - } - - private Integer getTargetActiveGeneration(Boolean canJoinOnlyActiveGeneration) { - return canJoinOnlyActiveGeneration ? ACTIVE_GENERATION : null; - } - - private List filterUserActivities(User user, Meeting meeting) { - // 현재 활동기수만 지원 가능할 경우 -> 현재 활동 기수에 해당하는 파트만 필터링 - if (meeting.getTargetActiveGeneration() == ACTIVE_GENERATION && meeting.getCanJoinOnlyActiveGeneration()) { - List filteredActivities = user.getActivities().stream() - .filter(activity -> activity.getGeneration() == ACTIVE_GENERATION) - .collect(Collectors.toList()); - - // 활동 기수가 아닌 경우 예외 처리 - if (filteredActivities.isEmpty()) { - throw new BadRequestException(NOT_ACTIVE_GENERATION.getErrorCode()); - } - - return filteredActivities; - } - return user.getActivities(); - } - - private void validateMeetingCapacity(Meeting meeting) { - List approvedApplies = meeting.getAppliedInfo().stream() - .filter(apply -> EnApplyStatus.APPROVE.equals(apply.getStatus())) - .collect(Collectors.toList()); - - if (approvedApplies.size() >= meeting.getCapacity()) { - throw new BadRequestException(FULL_MEETING_CAPACITY.getErrorCode()); - } - } - - private void validateUserAlreadyApplied(Meeting meeting, Integer userId) { - boolean hasApplied = meeting.getAppliedInfo().stream() - .anyMatch(appliedInfo -> appliedInfo.getUser().getId().equals(userId)); - - if (hasApplied) { - throw new BadRequestException(ALREADY_APPLIED_MEETING.getErrorCode()); - } - } - - private void validateApplyPeriod(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); - if (now.isAfter(meeting.getEndDate()) || now.isBefore(meeting.getStartDate())) { - throw new BadRequestException(NOT_IN_APPLY_PERIOD.getErrorCode()); - } - } - - private void validateUserActivities(User user) { - if (user.getActivities().isEmpty()) { - throw new BadRequestException(MISSING_GENERATION_PART.getErrorCode()); - } - } - - private void validateUserJoinableParts(User user, Meeting meeting) { - if (meeting.getJoinableParts().length == 0) { - return; - } - - List userActivities = filterUserActivities(user, meeting); - List userJoinableParts = userActivities.stream() - .map(UserActivityVO::getPart) - .filter(part -> { - MeetingJoinablePart meetingJoinablePart = UserPartUtil.getMeetingJoinablePart( - UserPart.ofValue(part)); - - // 임원진이라면 패스 - if (meetingJoinablePart == null) { - return true; - } - - return Arrays.stream(meeting.getJoinableParts()) - .anyMatch(joinablePart -> joinablePart == meetingJoinablePart); - }) - .collect(Collectors.toList()); - - if (userJoinableParts.isEmpty()) { - throw new BadRequestException(NOT_TARGET_PART.getErrorCode()); - } - } + private final static int ZERO = 0; + + private final UserRepository userRepository; + private final ApplyRepository applyRepository; + private final MeetingRepository meetingRepository; + private final PostRepository postRepository; + + private final MeetingMapper meetingMapper; + private final ApplyMapper applyMapper; + + @Override + public MeetingV2GetAllMeetingByOrgUserDto getAllMeetingByOrgUser( + MeetingV2GetAllMeetingByOrgUserQueryDto queryDto) { + int page = queryDto.getPage(); + int take = queryDto.getTake(); + + Optional user = userRepository.findByOrgId(queryDto.getOrgUserId()); + List userJoinedList = new ArrayList<>(); + + if (!user.isEmpty()) { + User existUser = user.get(); + List myMeetings = meetingRepository.findAllByUserId(existUser.getId()); + + userJoinedList = Stream + .concat(myMeetings.stream(), + applyRepository.findAllByUserIdAndStatus(existUser.getId(), EnApplyStatus.APPROVE) + .stream().map(apply -> apply.getMeeting())) + .map(meeting -> MeetingV2GetAllMeetingByOrgUserMeetingDto.of(meeting.getId(), + checkMeetingLeader(meeting, existUser.getId()), meeting.getTitle(), + meeting.getImageURL().get(0).getUrl(), meeting.getCategory().getValue(), + meeting.getMStartDate(), meeting.getMEndDate(), checkActivityStatus(meeting))) + .sorted(Comparator.comparing(MeetingV2GetAllMeetingByOrgUserMeetingDto::getId).reversed()) + .collect(Collectors.toList()); + } + + List pagedUserJoinedList = + userJoinedList.stream().skip((long)(page - 1) * take) // 스킵할 아이템 수 계산 + .limit(take) // 페이지당 아이템 수 제한 + .collect(Collectors.toList()); + PageOptionsDto pageOptionsDto = new PageOptionsDto(page, take); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, userJoinedList.size()); + return MeetingV2GetAllMeetingByOrgUserDto.of(pagedUserJoinedList, pageMetaDto); + } + + @Override + public List getMeetingBanner() { + return meetingRepository.findAll() + .stream() + .sorted(Comparator.comparing(Meeting::getId).reversed()) + .limit(20) + .map(meeting -> { + Optional recentPost = postRepository.findFirstByMeetingIdOrderByIdDesc(meeting.getId()); + Optional recentActivityDate = recentPost.map(Post::getCreatedDate); + + List applies = applyRepository.findAllByMeetingId(meeting.getId()); + Integer applicantCount = applies.size(); + Integer appliedUserCount = applies.stream() + .filter(apply -> apply.getStatus().equals(EnApplyStatus.APPROVE)).toList().size(); + + User meetingLeader = userRepository.findByIdOrThrow(meeting.getUserId()); + MeetingV2GetMeetingBannerResponseUserDto meetingLeaderDto = MeetingV2GetMeetingBannerResponseUserDto + .of(meetingLeader.getId(), meetingLeader.getName(), meetingLeader.getOrgId(), + meetingLeader.getProfileImage()); + + return MeetingV2GetMeetingBannerResponseDto.of(meeting.getId(), meeting.getUserId(), + meeting.getTitle(), meeting.getCategory(), meeting.getImageURL(), + meeting.getMStartDate(), meeting.getMEndDate(), meeting.getStartDate(), + meeting.getEndDate(), + meeting.getCapacity(), recentActivityDate, meeting.getTargetActiveGeneration(), + meeting.getJoinableParts(), applicantCount, appliedUserCount, meetingLeaderDto, + meeting.getMeetingStatus()); + }).toList(); + } + + @Override + @Transactional + public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBodyDto requestBody, Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + + if (user.getActivities() == null) { + throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); + } + + if (requestBody.getFiles().size() == ZERO || requestBody.getJoinableParts().length == ZERO) { + throw new BadRequestException(VALIDATION_EXCEPTION.getErrorCode()); + } + + Meeting meeting = meetingMapper.toMeetingEntity(requestBody, + getTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, + user.getId()); + + Meeting savedMeeting = meetingRepository.save(meeting); + return MeetingV2CreateMeetingResponseDto.of(savedMeeting.getId()); + } + + @Override + @Transactional + public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); + User user = userRepository.findByIdOrThrow(userId); + + List applies = applyRepository.findAllByMeetingId(meeting.getId()); + + validateMeetingCapacity(meeting, applies); + validateUserAlreadyApplied(userId, applies); + validateApplyPeriod(meeting); + validateUserActivities(user); + validateUserJoinableParts(user, meeting); + + Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, + userId); + Apply savedApply = applyRepository.save(apply); + return MeetingV2ApplyMeetingResponseDto.of(savedApply.getId()); + } + + @Override + @Transactional + public void applyMeetingCancel(Integer meetingId, Integer userId) { + boolean exists = applyRepository.existsByMeetingIdAndUserId(meetingId, userId); + + if (!exists) { + throw new BadRequestException(NOT_FOUND_APPLY.getErrorCode()); + } + + applyRepository.deleteByMeetingIdAndUserId(meetingId, userId); + } + + @Override + @Transactional(readOnly = true) + public MeetingGetApplyListResponseDto findApplyList(MeetingGetAppliesQueryDto queryCommand, + Integer meetingId, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), + meetingId, meeting.getUserId(), userId); + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, (int)applyInfoDtos.getTotalElements()); + + return MeetingGetApplyListResponseDto.of(applyInfoDtos.getContent(), pageMetaDto); + } + + private Boolean checkMeetingLeader(Meeting meeting, Integer userId) { + return meeting.getUserId().equals(userId); + } + + private Boolean checkActivityStatus(Meeting meeting) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime mStartDate = meeting.getMStartDate(); + LocalDateTime mEndDate = meeting.getMEndDate(); + return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); + } + + private Integer getTargetActiveGeneration(Boolean canJoinOnlyActiveGeneration) { + return canJoinOnlyActiveGeneration ? ACTIVE_GENERATION : null; + } + + private List filterUserActivities(User user, Meeting meeting) { + // 현재 활동기수만 지원 가능할 경우 -> 현재 활동 기수에 해당하는 파트만 필터링 + if (meeting.getTargetActiveGeneration() == ACTIVE_GENERATION && meeting.getCanJoinOnlyActiveGeneration()) { + List filteredActivities = user.getActivities().stream() + .filter(activity -> activity.getGeneration() == ACTIVE_GENERATION) + .collect(Collectors.toList()); + + // 활동 기수가 아닌 경우 예외 처리 + if (filteredActivities.isEmpty()) { + throw new BadRequestException(NOT_ACTIVE_GENERATION.getErrorCode()); + } + + return filteredActivities; + } + return user.getActivities(); + } + + private void validateMeetingCapacity(Meeting meeting, List applies) { + List approvedApplies = applies.stream() + .filter(apply -> EnApplyStatus.APPROVE.equals(apply.getStatus())) + .toList(); + + if (approvedApplies.size() >= meeting.getCapacity()) { + throw new BadRequestException(FULL_MEETING_CAPACITY.getErrorCode()); + } + } + + private void validateUserAlreadyApplied(Integer userId, List applies) { + boolean hasApplied = applies.stream() + .anyMatch(appliedInfo -> appliedInfo.getUser().getId().equals(userId)); + + if (hasApplied) { + throw new BadRequestException(ALREADY_APPLIED_MEETING.getErrorCode()); + } + } + + private void validateApplyPeriod(Meeting meeting) { + LocalDateTime now = LocalDateTime.now(); + if (now.isAfter(meeting.getEndDate()) || now.isBefore(meeting.getStartDate())) { + throw new BadRequestException(NOT_IN_APPLY_PERIOD.getErrorCode()); + } + } + + private void validateUserActivities(User user) { + if (user.getActivities().isEmpty()) { + throw new BadRequestException(MISSING_GENERATION_PART.getErrorCode()); + } + } + + private void validateUserJoinableParts(User user, Meeting meeting) { + if (meeting.getJoinableParts().length == 0) { + return; + } + + List userActivities = filterUserActivities(user, meeting); + List userJoinableParts = userActivities.stream() + .map(UserActivityVO::getPart) + .filter(part -> { + MeetingJoinablePart meetingJoinablePart = UserPartUtil.getMeetingJoinablePart( + UserPart.ofValue(part)); + + // 임원진이라면 패스 + if (meetingJoinablePart == null) { + return true; + } + + return Arrays.stream(meeting.getJoinableParts()) + .anyMatch(joinablePart -> joinablePart == meetingJoinablePart); + }) + .collect(Collectors.toList()); + + if (userJoinableParts.isEmpty()) { + throw new BadRequestException(NOT_TARGET_PART.getErrorCode()); + } + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java new file mode 100644 index 00000000..6f0c5ad2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Api.java @@ -0,0 +1,23 @@ +package org.sopt.makers.crew.main.notice; + +import io.swagger.v3.oas.annotations.Operation; +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 java.util.List; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "공지사항") +public interface NoticeV2Api { + + @Operation(summary = "공지사항 조회") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity> getNotices(); + + @Operation(summary = "공지사항 작성") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity createNotice(@RequestBody NoticeV2CreateRequestDto requestDto); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java new file mode 100644 index 00000000..d8d4e26f --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/NoticeV2Controller.java @@ -0,0 +1,35 @@ +package org.sopt.makers.crew.main.notice; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.sopt.makers.crew.main.notice.service.NoticeV2Service; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/notice/v2") +@RequiredArgsConstructor +public class NoticeV2Controller implements NoticeV2Api { + private final NoticeV2Service noticeService; + + + @Override + @GetMapping + public ResponseEntity> getNotices() { + return ResponseEntity.ok(noticeService.getNotices()); + } + + @Override + @PostMapping + public ResponseEntity createNotice(@RequestBody NoticeV2CreateRequestDto requestDto) { + noticeService.createNotice(requestDto); + return ResponseEntity.ok(null); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java new file mode 100644 index 00000000..15d95a3b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/request/NoticeV2CreateRequestDto.java @@ -0,0 +1,16 @@ +package org.sopt.makers.crew.main.notice.dto.request; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class NoticeV2CreateRequestDto { + private final String title; + private final String subTitle; + private final String contents; + private final LocalDateTime exposeStartDate; + private final LocalDateTime exposeEndDate; + private final String noticeSecretKey; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java new file mode 100644 index 00000000..05a1615c --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/dto/response/NoticeV2GetResponseDto.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.notice.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(staticName = "of") +public class NoticeV2GetResponseDto { + private final Integer id; + private final String title; + private final String subTitle; + private final String contents; + private final LocalDateTime createdDate; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java new file mode 100644 index 00000000..96cc326b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/notice/service/NoticeV2Service.java @@ -0,0 +1,54 @@ +package org.sopt.makers.crew.main.notice.service; + +import static org.sopt.makers.crew.main.common.response.ErrorStatus.FORBIDDEN_EXCEPTION; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.entity.notice.Notice; +import org.sopt.makers.crew.main.entity.notice.NoticeRepository; +import org.sopt.makers.crew.main.notice.dto.request.NoticeV2CreateRequestDto; +import org.sopt.makers.crew.main.notice.dto.response.NoticeV2GetResponseDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeV2Service { + private final NoticeRepository noticeRepository; + + @Value("${notice.secret-key}") + private String noticeSecretKey; + + public List getNotices() { + // Time 클래스로 교체 필요. + List notices = noticeRepository.findByExposeStartDateBeforeAndExposeEndDateAfter(LocalDateTime.now(), + LocalDateTime.now()); + return notices.stream() + .map(notice -> NoticeV2GetResponseDto.of(notice.getId(), notice.getTitle(), notice.getSubTitle(), notice.getContents(), + notice.getCreatedDate())) + .toList(); + } + + @Transactional + public void createNotice(NoticeV2CreateRequestDto requestDto) { + + if (!requestDto.getNoticeSecretKey().equals(noticeSecretKey)) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + + Notice notice = Notice.builder() + .title(requestDto.getTitle()) + .subTitle(requestDto.getSubTitle()) + .contents(requestDto.getContents()) + .exposeStartDate(requestDto.getExposeStartDate()) + .exposeEndDate(requestDto.getExposeEndDate()) + .build(); + + noticeRepository.save(notice); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java new file mode 100644 index 00000000..b300d1f8 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Api.java @@ -0,0 +1,56 @@ +package org.sopt.makers.crew.main.post.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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 java.security.Principal; + +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "게시글") +public interface PostV2Api { + + @Operation(summary = "모임 게시글 작성") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), + }) + ResponseEntity createPost( + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal); + + @Operation(summary = "모임 게시글 목록 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), + }) + @Parameters({ + @Parameter(name = "page", description = "페이지, default = 1", example = "1", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "take", description = "가져올 데이터 개수, default = 12", example = "50", schema = @Schema(type = "integer", format = "int32")), + @Parameter(name = "meetingId", description = "모임 id", example = "0", schema = @Schema(type = "integer", format = "int32"))}) + ResponseEntity getPosts( + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal); + + @Operation(summary = "게시글에서 멘션하기") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + }) + ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal); + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java index 8f075c2f..b35a7947 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/PostV2Controller.java @@ -1,19 +1,23 @@ package org.sopt.makers.crew.main.post.v2; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -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 io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; + import java.security.Principal; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.util.UserUtil; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.sopt.makers.crew.main.post.v2.service.PostV2Service; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,22 +27,36 @@ @RestController @RequestMapping("/post/v2") @RequiredArgsConstructor -@Tag(name = "게시글") -public class PostV2Controller { - - private final PostV2Service postV2Service; - - @Operation(summary = "모임 게시글 작성") - @PostMapping() - @ResponseStatus(HttpStatus.CREATED) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "성공"), - @ApiResponse(responseCode = "400", description = "모임이 없습니다.", content = @Content), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.", content = @Content), - }) - public ResponseEntity createPost( - @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); - } +public class PostV2Controller implements PostV2Api { + + private final PostV2Service postV2Service; + + @Override + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createPost( + @Valid @RequestBody PostV2CreatePostBodyDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(postV2Service.createPost(requestBody, userId)); + } + + @Override + @GetMapping() + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getPosts( + @ModelAttribute @Parameter(hidden = true) PostGetPostsCommand queryCommand, + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(postV2Service.getPosts(queryCommand, userId)); + } + + @Override + @PostMapping("/mention") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity mentionUserInPost( + @Valid @RequestBody PostV2MentionUserInPostRequestDto requestBody, Principal principal) { + Integer userId = UserUtil.getUserId(principal); + postV2Service.mentionUserInPost(requestBody, userId); + return ResponseEntity.status(HttpStatus.OK).build(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java new file mode 100644 index 00000000..e84ef927 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/query/PostGetPostsCommand.java @@ -0,0 +1,17 @@ +package org.sopt.makers.crew.main.post.v2.dto.query; + +import java.util.Optional; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; + +@Getter +public class PostGetPostsCommand extends PageOptionsDto { + + private Optional meetingId; + + public PostGetPostsCommand(Integer meetingId, Integer page, Integer take) { + super(page, take); + this.meetingId = Optional.ofNullable(meetingId); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java new file mode 100644 index 00000000..5e04e711 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/request/PostV2MentionUserInPostRequestDto.java @@ -0,0 +1,27 @@ +package org.sopt.makers.crew.main.post.v2.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "모임 게시글에서 유저 언급 request body dto") +public class PostV2MentionUserInPostRequestDto { + + @Schema(example = "[111, 112, 113]", required = true, description = "언급할 유저 ID") + @NotEmpty + private final List userIds; + + @Schema(example = "1", required = true, description = "게시글 ID") + @NotNull + private final Integer postId; + + @Schema(example = "멘션내용~~", required = true, description = "멘션 내용") + private final String content; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java new file mode 100644 index 00000000..bff64f31 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/CommenterThumbnails.java @@ -0,0 +1,11 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class CommenterThumbnails { + private final List commenterThumbnails; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java new file mode 100644 index 00000000..927fe64b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailBaseDto.java @@ -0,0 +1,38 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class PostDetailBaseDto { + private final Integer id; + private final String title; + private final String contents; + private final LocalDateTime createdDate; + private final String[] images; + private final PostWriterInfoDto user; + private final int likeCount; + //* 본인이 좋아요를 눌렀는지 여부 + private final Boolean isLiked; + private final int viewCount; + private final int commentCount; + private final PostMeetingDto meeting; + + @QueryProjection + public PostDetailBaseDto(Integer id, String title, String contents, LocalDateTime createdDate, String[] images, + PostWriterInfoDto user, int likeCount, boolean isLiked, int viewCount, int commentCount, + PostMeetingDto meeting) { + this.id = id; + this.title = title; + this.contents = contents; + this.createdDate = createdDate; + this.images = images; + this.user = user; + this.likeCount = likeCount; + this.isLiked = isLiked; + this.viewCount = viewCount; + this.commentCount = commentCount; + this.meeting = meeting; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java new file mode 100644 index 00000000..21061cc1 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostDetailResponseDto.java @@ -0,0 +1,42 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class PostDetailResponseDto { + private final Integer id; + private final String title; + private final String contents; + private final LocalDateTime createdDate; + private final String[] images; + private final PostWriterInfoDto user; + private final int likeCount; + private final Boolean isLiked; + private final int viewCount; + private final int commentCount; + private final PostMeetingDto meeting; + private final List commenterThumbnails; + + public static PostDetailResponseDto of(PostDetailBaseDto postDetail, + CommenterThumbnails postTopCommenterThumbnails) { + List thumbnails = postTopCommenterThumbnails.getCommenterThumbnails(); + return PostDetailResponseDto.of( + postDetail.getId(), + postDetail.getTitle(), + postDetail.getContents(), + postDetail.getCreatedDate(), + postDetail.getImages(), + postDetail.getUser(), + postDetail.getLikeCount(), + postDetail.getIsLiked(), + postDetail.getViewCount(), + postDetail.getCommentCount(), + postDetail.getMeeting(), + thumbnails + ); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java new file mode 100644 index 00000000..d22cbd71 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostMeetingDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; + +@Getter +public class PostMeetingDto { + private final Integer id; + private final String title; + private final String category; + + @QueryProjection + public PostMeetingDto(Integer id, String title, MeetingCategory category) { + this.id = id; + this.title = title; + this.category = category.getValue(); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java new file mode 100644 index 00000000..e414007b --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostV2GetPostsResponseDto.java @@ -0,0 +1,14 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; + +@Getter +@AllArgsConstructor(staticName = "of") +public class PostV2GetPostsResponseDto { + + private final List posts; + private final PageMetaDto meta; +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java new file mode 100644 index 00000000..84a6834a --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/dto/response/PostWriterInfoDto.java @@ -0,0 +1,20 @@ +package org.sopt.makers.crew.main.post.v2.dto.response; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; + +@Getter +public class PostWriterInfoDto { + private final Integer id; + private final Integer orgId; + private final String name; + private final String profileImage; + + @QueryProjection + public PostWriterInfoDto(Integer id, Integer orgId, String name, String profileImage) { + this.id = id; + this.orgId = orgId; + this.name = name; + this.profileImage = profileImage; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java index e8047305..3a9ff793 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2Service.java @@ -1,9 +1,16 @@ package org.sopt.makers.crew.main.post.v2.service; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; public interface PostV2Service { - PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); + PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, Integer userId); + + PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId); + + void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java index a00ae877..dee4a582 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/post/v2/service/PostV2ServiceImpl.java @@ -2,12 +2,18 @@ import static java.util.stream.Collectors.toList; import static org.sopt.makers.crew.main.common.response.ErrorStatus.FORBIDDEN_EXCEPTION; +import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.NEW_POST_PUSH_NOTIFICATION_TITLE; import static org.sopt.makers.crew.main.internal.notification.PushNotificationEnums.PUSH_NOTIFICATION_CATEGORY; import java.util.List; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.pagination.dto.PageMetaDto; +import org.sopt.makers.crew.main.common.pagination.dto.PageOptionsDto; +import org.sopt.makers.crew.main.entity.apply.Apply; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; import org.sopt.makers.crew.main.entity.meeting.Meeting; @@ -18,9 +24,15 @@ import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.internal.notification.PushNotificationService; import org.sopt.makers.crew.main.internal.notification.dto.PushNotificationRequestDto; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; import org.sopt.makers.crew.main.post.v2.dto.request.PostV2CreatePostBodyDto; +import org.sopt.makers.crew.main.post.v2.dto.request.PostV2MentionUserInPostRequestDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; import org.sopt.makers.crew.main.post.v2.dto.response.PostV2CreatePostResponseDto; +import org.sopt.makers.crew.main.post.v2.dto.response.PostV2GetPostsResponseDto; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,66 +41,107 @@ @Transactional(readOnly = true) public class PostV2ServiceImpl implements PostV2Service { - private final MeetingRepository meetingRepository; - private final UserRepository userRepository; - private final PostRepository postRepository; - private final ApplyRepository applyRepository; - private final PushNotificationService pushNotificationService; - - @Value("${push-notification.web-url}") - private String pushWebUrl; - - /** - * 모임 게시글 작성 - * - * @throws 403 모임에 속한 유저가 아닌 경우 - * @apiNote 모임에 속한 유저만 작성 가능 - */ - @Override - @Transactional - public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, - Integer userId) { - Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); - User user = userRepository.findByIdOrThrow(userId); - - boolean isInMeeting = meeting.getAppliedInfo().stream() - .anyMatch(apply -> apply.getUserId().equals(userId) - && apply.getStatus() == EnApplyStatus.APPROVE); - - boolean isMeetingCreator = meeting.getUserId().equals(userId); - - if (isInMeeting == false && isMeetingCreator == false) { - throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); - } - - Post post = Post.builder() - .title(requestBody.getTitle()) - .user(user) - .contents(requestBody.getContents()) - .images(requestBody.getImages()) - .meeting(meeting) - .build(); - - Post savedPost = postRepository.save(post); - - List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), - EnApplyStatus.APPROVE) - .stream() - .map(apply -> String.valueOf(apply.getUser().getOrgId())) - .collect(toList()); - - String[] userIds = userIdList.toArray(new String[0]); - String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", - user.getName(), post.getTitle()); - String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); - - PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, - NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), - pushNotificationContent, - PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); - - pushNotificationService.sendPushNotification(pushRequestDto); - - return PostV2CreatePostResponseDto.of(savedPost.getId()); - } + private final MeetingRepository meetingRepository; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final ApplyRepository applyRepository; + private final PushNotificationService pushNotificationService; + + @Value("${push-notification.web-url}") + private String pushWebUrl; + + /** + * 모임 게시글 작성 + * + * @throws 403 모임에 속한 유저가 아닌 경우 + * @apiNote 모임에 속한 유저만 작성 가능 + */ + @Override + @Transactional + public PostV2CreatePostResponseDto createPost(PostV2CreatePostBodyDto requestBody, + Integer userId) { + Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); + User user = userRepository.findByIdOrThrow(userId); + + List applies = applyRepository.findAllByMeetingId(meeting.getId()); + + boolean isInMeeting = applies.stream() + .anyMatch(apply -> apply.getUserId().equals(userId) + && apply.getStatus().equals(EnApplyStatus.APPROVE)); + + boolean isMeetingCreator = meeting.getUserId().equals(userId); + + if (isInMeeting == false && isMeetingCreator == false) { + throw new ForbiddenException(FORBIDDEN_EXCEPTION.getErrorCode()); + } + + Post post = Post.builder() + .title(requestBody.getTitle()) + .user(user) + .contents(requestBody.getContents()) + .images(requestBody.getImages()) + .meeting(meeting) + .build(); + + Post savedPost = postRepository.save(post); + + List userIdList = applyRepository.findAllByMeetingIdAndStatus(meeting.getId(), + EnApplyStatus.APPROVE) + .stream() + .map(apply -> String.valueOf(apply.getUser().getOrgId())) + .collect(toList()); + + String[] userIds = userIdList.toArray(new String[0]); + String pushNotificationContent = String.format("[%s의 새 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/detail?id=" + meeting.getId(); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(userIds, + NEW_POST_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink); + + pushNotificationService.sendPushNotification(pushRequestDto); + + return PostV2CreatePostResponseDto.of(savedPost.getId()); + } + + @Override + @Transactional(readOnly = true) + public PostV2GetPostsResponseDto getPosts(PostGetPostsCommand queryCommand, Integer userId) { + Page meetingPostListDtos = postRepository.findPostList(queryCommand, + PageRequest.of(queryCommand.getPage() - 1, queryCommand.getTake()), userId); + + PageOptionsDto pageOptionsDto = new PageOptionsDto(queryCommand.getPage(), + queryCommand.getTake()); + PageMetaDto pageMetaDto = new PageMetaDto(pageOptionsDto, + (int)meetingPostListDtos.getTotalElements()); + + return PostV2GetPostsResponseDto.of(meetingPostListDtos.getContent(), pageMetaDto); + } + + @Override + public void mentionUserInPost(PostV2MentionUserInPostRequestDto requestBody, Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + Post post = postRepository.findByIdOrThrow(requestBody.getPostId()); + + String pushNotificationContent = String.format("[%s의 글] : \"%s\"", + user.getName(), post.getTitle()); + String pushNotificationWeblink = pushWebUrl + "/post?id=" + post.getId(); + + String[] userOrgIds = userRepository.findByIdIn(requestBody.getUserIds()) + .stream() + .map(mentionedUser -> String.valueOf(mentionedUser.getOrgId())) + .toArray(String[]::new); + + PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of( + userOrgIds, + NEW_POST_MENTION_PUSH_NOTIFICATION_TITLE.getValue(), + pushNotificationContent, + PUSH_NOTIFICATION_CATEGORY.getValue(), + pushNotificationWeblink + ); + + pushNotificationService.sendPushNotification(pushRequestDto); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java new file mode 100644 index 00000000..5d17ecc3 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserApi.java @@ -0,0 +1,28 @@ +package org.sopt.makers.crew.main.user.v2; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +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 java.security.Principal; +import java.util.List; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.springframework.http.ResponseEntity; + +@Tag(name = "사용자") +public interface UserApi { + + @Operation(summary = "내가 속한 모임 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "204", description = "내가 속한 모임 리스트가 없는 경우", content = @Content), + }) + ResponseEntity> getAllMeetingByUser(Principal principal); + + @Operation(summary = "멘션 사용자 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity> getAllMentionUser(Principal principal); +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java index c8aa632a..61f90ef1 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java @@ -1,15 +1,11 @@ package org.sopt.makers.crew.main.user.v2; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -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 java.security.Principal; import java.util.List; import lombok.RequiredArgsConstructor; import org.sopt.makers.crew.main.common.util.UserUtil; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; import org.sopt.makers.crew.main.user.v2.service.UserV2Service; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,21 +17,22 @@ @RestController @RequestMapping("/user/v2") @RequiredArgsConstructor -@Tag(name = "사용자") -public class UserV2Controller { +public class UserV2Controller implements UserApi { - private final UserV2Service userV2Service; + private final UserV2Service userV2Service; - @Operation(summary = "내가 속한 모임 조회") - @GetMapping("/meeting/all") - @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "204", description = "내가 속한 모임 리스트가 없는 경우", content = @Content), - }) - public ResponseEntity> getAllMeetingByUser( - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); - } + @GetMapping("/meeting/all") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getAllMeetingByUser( + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); + } + + @GetMapping("/mention") + public ResponseEntity> getAllMentionUser( + Principal principal) { + UserUtil.getUserId(principal); + return ResponseEntity.ok(userV2Service.getAllMentionUser()); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java new file mode 100644 index 00000000..f25de8b2 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllMentionUserDto.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.user.v2.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(staticName = "of") +public class UserV2GetAllMentionUserDto { + /** + * 주의!! : 필드명은 userId 이지만 실제 응답 해야하는 데이터는 orgId 입니다. + */ + private final Integer userId; + + private final String userName; + private final String recentPart; + private final int recentGeneration; + private final String profileImageUrl; +} + diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java index ecfbe87e..a6710c9d 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java @@ -2,9 +2,11 @@ import java.util.List; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; public interface UserV2Service { List getAllMeetingByUser(Integer userId); + List getAllMentionUser(); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index 804e5f5b..cba42120 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -4,13 +4,18 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; + import lombok.RequiredArgsConstructor; + import org.sopt.makers.crew.main.common.exception.BaseException; import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; +import org.sopt.makers.crew.main.entity.meeting.Meeting; +import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,32 +25,48 @@ @Transactional(readOnly = true) public class UserV2ServiceImpl implements UserV2Service { - private final UserRepository userRepository; - private final ApplyRepository applyRepository; - - @Override - public List getAllMeetingByUser(Integer userId) { - User user = userRepository.findByIdOrThrow(userId); - - List userJoinedList = Stream.concat( - user.getMeetings().stream(), - applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) - .stream() - .map(apply -> apply.getMeeting()) - ) - .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( - meeting.getId(), - meeting.getTitle(), - meeting.getDesc(), - meeting.getImageURL().get(0).getUrl(), - meeting.getCategory().getValue() - )) - .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) - .collect(Collectors.toList()); - - if (userJoinedList.isEmpty()) { - throw new BaseException(HttpStatus.NO_CONTENT); - } - return userJoinedList; - } + private final UserRepository userRepository; + private final ApplyRepository applyRepository; + private final MeetingRepository meetingRepository; + + @Override + public List getAllMeetingByUser(Integer userId) { + User user = userRepository.findByIdOrThrow(userId); + + List myMeetings = meetingRepository.findAllByUserId(user.getId()); + + List userJoinedList = Stream.concat( + myMeetings.stream(), + applyRepository.findAllByUserIdAndStatus(userId, EnApplyStatus.APPROVE) + .stream() + .map(apply -> apply.getMeeting()) + ) + .map(meeting -> UserV2GetAllMeetingByUserMeetingDto.of( + meeting.getId(), + meeting.getTitle(), + meeting.getDesc(), + meeting.getImageURL().get(0).getUrl(), + meeting.getCategory().getValue() + )) + .sorted(Comparator.comparing(UserV2GetAllMeetingByUserMeetingDto::getId).reversed()) + .collect(Collectors.toList()); + + if (userJoinedList.isEmpty()) { + throw new BaseException(HttpStatus.NO_CONTENT); + } + return userJoinedList; + } + + @Override + public List getAllMentionUser() { + + List users = userRepository.findAll(); + + return users.stream() + .filter(user -> user.getActivities() != null) + .map(user -> UserV2GetAllMentionUserDto.of(user.getOrgId(), user.getName(), + user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), + user.getProfileImage())) + .toList(); + } } diff --git a/main/src/main/resources/application-dev.yml b/main/src/main/resources/application-dev.yml index ea96751d..b4013a68 100644 --- a/main/src/main/resources/application-dev.yml +++ b/main/src/main/resources/application-dev.yml @@ -51,4 +51,7 @@ push-notification: web-url: ${DEV_WEB_PAGE_URL} x-api-key: ${DEV_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} - push-server-url: ${DEV_PUSH_SERVER_URL} \ No newline at end of file + push-server-url: ${DEV_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file diff --git a/main/src/main/resources/application-prod.yml b/main/src/main/resources/application-prod.yml index e46dfb7a..4110fa7b 100644 --- a/main/src/main/resources/application-prod.yml +++ b/main/src/main/resources/application-prod.yml @@ -51,3 +51,6 @@ push-notification: x-api-key: ${PROD_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} push-server-url: ${PROD_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file diff --git a/main/src/main/resources/application-test.yml b/main/src/main/resources/application-test.yml index 8415f784..d245c43c 100644 --- a/main/src/main/resources/application-test.yml +++ b/main/src/main/resources/application-test.yml @@ -17,12 +17,14 @@ spring: hibernate: naming: physical-strategy: org.sopt.makers.crew.main.common.config.CamelCaseNamingStrategy - ddl-auto: create + ddl-auto: create-drop properties: hibernate: show_sql: true format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect storage_engine: innodb + defer-datasource-initialization: true jwt: header: Authorization @@ -52,4 +54,7 @@ push-notification: web-url: ${DEV_WEB_PAGE_URL} x-api-key: ${DEV_PUSH_API_KEY} service: ${PUSH_NOTIFICATION_SERVICE} - push-server-url: ${DEV_PUSH_SERVER_URL} \ No newline at end of file + push-server-url: ${DEV_PUSH_SERVER_URL} + +notice: + secret-key : ${NOTICE_SECRET_KEY} \ No newline at end of file diff --git a/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java new file mode 100644 index 00000000..c970c079 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/comment/v2/service/CommentV2ServiceTest.java @@ -0,0 +1,176 @@ +package org.sopt.makers.crew.main.comment.v2.service; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2ReportCommentResponseDto; +import org.sopt.makers.crew.main.comment.v2.dto.response.CommentV2UpdateCommentResponseDto; +import org.sopt.makers.crew.main.common.exception.BadRequestException; +import org.sopt.makers.crew.main.common.exception.ForbiddenException; +import org.sopt.makers.crew.main.common.util.Time; +import org.sopt.makers.crew.main.entity.comment.Comment; +import org.sopt.makers.crew.main.entity.comment.CommentRepository; +import org.sopt.makers.crew.main.entity.post.Post; +import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.entity.report.Report; +import org.sopt.makers.crew.main.entity.report.ReportRepository; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.entity.user.UserFixture; +import org.sopt.makers.crew.main.entity.user.UserRepository; + +@ExtendWith(MockitoExtension.class) +public class CommentV2ServiceTest { + + @InjectMocks + private CommentV2ServiceImpl commentV2Service; + @Mock + private CommentRepository commentRepository; + @Mock + private PostRepository postRepository; + @Mock + private ReportRepository reportRepository; + @Mock + private UserRepository userRepository; + @Mock + private Time time; + + private Comment comment; + + private Post post; + + private User user; + + private Report report; + + @BeforeEach + void init() { + user = UserFixture.createStaticUser(); + user.setUserIdForTest(1); + + String[] images = {"image1", "image2", "image3"}; + this.post = Post.builder().user(user).title("title").contents("contents").images(images) + .build(); + this.comment = Comment.builder() + .contents("contents") + .post(post) + .postId(1) + .user(user) + .userId(1).build(); + + this.report = Report.builder().comment(comment).user(user).build(); + } + + @Nested + class 댓글_수정 { + + @Test + void 성공() { + // given + String updatedContents = "updatedContents"; + LocalDateTime expectedUpdatedDate = time.now(); + + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(LocalDateTime.of(2024, 4, 24, 23, 59)).when(time).now(); + + // when + CommentV2UpdateCommentResponseDto result = commentV2Service.updateComment(comment.getId(), + updatedContents, user.getId()); + + // then + Assertions.assertThat(result.getId()).isEqualTo(comment.getId()); + Assertions.assertThat(result.getContents()).isEqualTo(updatedContents); + Assertions.assertThat(LocalDateTime.parse(result.getUpdateDate())) + .isEqualTo(LocalDateTime.of(2024, 4, 24, 23, 59)); + } + + @Test + void 실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + + // when & then + assertThrows(ForbiddenException.class, () -> { + commentV2Service.updateComment(comment.getId(), "updatedContents", + comment.getUser().getId() + 1); + }); + } + } + + @Nested + class 댓글_삭제 { + + @Test + void 성공() { + // given + int initialCommentCount = post.getCommentCount(); + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(post).when(postRepository).findByIdOrThrow((any())); + + // when + commentV2Service.deleteComment(comment.getId(), user.getId()); + + // then + Assertions.assertThat(commentRepository.findById(comment.getId())) + .isEqualTo(Optional.empty()); + Assertions.assertThat(post.getCommentCount()).isEqualTo(initialCommentCount - 1); + } + + @Test + void 실패_본인_작성_댓글_아님() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + Integer id = comment.getUser().getId(); + // when & then + assertThrows(ForbiddenException.class, () -> { + commentV2Service.deleteComment(0, comment.getUser().getId() + 1); + }); + } + } + + @Nested + class 댓글_신고 { + + @Test + void 댓글_신고_성공() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.empty()).when(reportRepository).findByCommentAndUser(any(), any()); + doReturn(report).when(reportRepository).save(any()); + + // when + CommentV2ReportCommentResponseDto result = commentV2Service.reportComment(comment.getId(), + user.getId()); + + // then + Assertions.assertThat(result.getReportId()).isEqualTo(report.getId()); + } + + @Test + void 댓글_신고_실패_이미_신고한_댓글() { + // given + doReturn(comment).when(commentRepository).findByIdOrThrow(any()); + doReturn(user).when(userRepository).findByIdOrThrow(any()); + doReturn(Optional.of(report)).when(reportRepository).findByCommentAndUser(any(), any()); + + // when & then + assertThrows(BadRequestException.class, () -> { + commentV2Service.reportComment(comment.getId(), user.getId()); + }); + } + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java b/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java new file mode 100644 index 00000000..4af63b6d --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/annotation/IntegratedTest.java @@ -0,0 +1,21 @@ +package org.sopt.makers.crew.main.common.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest +@Transactional +@Testcontainers +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public @interface IntegratedTest { +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java new file mode 100644 index 00000000..c8373182 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/config/TestConfig.java @@ -0,0 +1,19 @@ +package org.sopt.makers.crew.main.common.config; + +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestConfig { + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java b/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java new file mode 100644 index 00000000..827894b6 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/common/util/StubTime.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.common.util; + +import java.time.LocalDateTime; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("test") +public class StubTime implements Time { + @Override + public LocalDateTime now() { + return LocalDateTime.of(2024, 4, 24, 23, 59); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java new file mode 100644 index 00000000..a88f3907 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserEntityTest.java @@ -0,0 +1,51 @@ +package org.sopt.makers.crew.main.entity.user; + +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; + +public class UserEntityTest { + + + @Test + void 최근_기수_조회(){ + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url") + .phone("010-1234-5678") + .build(); + + // when + UserActivityVO recentActivityVO = user.getRecentActivityVO(); + + // then + + Assertions.assertThat(recentActivityVO.getGeneration()).isEqualTo(34); + Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("iOS"); + + } + + @Test + void 최근_기수_조회_잘못된_데이터가_저장된_경우_해당_데이터는_무시한다(){ + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO(null, 34), new UserActivityVO("서버", 33))) + .profileImage("image-url") + .phone("010-1234-5678") + .build(); + + // when + UserActivityVO recentActivityVO = user.getRecentActivityVO(); + + // then + + Assertions.assertThat(recentActivityVO.getGeneration()).isEqualTo(33); + Assertions.assertThat(recentActivityVO.getPart()).isEqualTo("서버"); + } +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java index 1bd8ea9b..21d5a8b6 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/entity/user/UserRepositoryTest.java @@ -2,14 +2,17 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; @DataJpaTest @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Import(TestConfig.class) public class UserRepositoryTest { @Autowired diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java new file mode 100644 index 00000000..3b961566 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/repository/ApplyRepositoryTest.java @@ -0,0 +1,276 @@ +package org.sopt.makers.crew.main.meeting.v2.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; +import org.sopt.makers.crew.main.entity.apply.ApplyRepository; +import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; +import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; +import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; + +@DataJpaTest +@ActiveProfiles("test") +@Import(TestConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@SqlGroup({ + @Sql(value = "/sql/apply-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) + +}) +public class ApplyRepositoryTest { + + @Autowired + private ApplyRepository applyRepository; + + @Test + void 스터디장이_신청자_리스트_최신순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + } + + @Test + void 스터디장이_신청자_리스트_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + } + + @Test + void 스터디장이_신청자_리스트_대기상태만_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(1); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "전할말입니다3", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + } + + @Test + void 스터디장이_신청자_리스트_승인상태만_오래된순으로_조회() { + // given + int page = 1; + int take = 12; + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.APPROVE), "asc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 1; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(2); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "전할말입니다1", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "전할말입니다2", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + } + + + @Test + void 스터디장이_아닌_사람이_신청자_리스트_조회() { + // given + int page = 1; + int take = 12; + MeetingGetAppliesQueryDto queryCommand = new MeetingGetAppliesQueryDto(page, take, List.of(EnApplyStatus.WAITING, EnApplyStatus.APPROVE, EnApplyStatus.REJECT), "desc"); + Integer meetingId = 1; + Integer studyCreatorId = 1; + Integer userId = 2; + + // when + Page applyInfoDtos = applyRepository.findApplyList(queryCommand, PageRequest.of(page - 1, take), + meetingId, studyCreatorId, userId); + + // then + assertThat(applyInfoDtos.getTotalElements()).isEqualTo(3); + assertThat(applyInfoDtos.getSize()).isEqualTo(take); + assertThat(applyInfoDtos.getNumber()).isEqualTo(page-1); + assertThat(applyInfoDtos.getTotalPages()).isEqualTo(1); + + ApplyInfoDto applyInfoDto1 = applyInfoDtos.getContent().get(0); + assertThat(applyInfoDto1) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(3, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 3, 413489000)), EnApplyStatus.WAITING); + assertThat(applyInfoDto1.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(4, "이영지", 1004, "profile4.jpg", "010-5555-5555"); + assertThat(applyInfoDto1.getUser().getRecentActivity().getGeneration()).isEqualTo(32); + + + ApplyInfoDto applyInfoDto2 = applyInfoDtos.getContent().get(1); + assertThat(applyInfoDto2) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(2, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 2, 413489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto2.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(3, "김철수", 1003, "profile3.jpg", "010-3333-4444"); + assertThat(applyInfoDto2.getUser().getRecentActivity().getGeneration()).isEqualTo(34); + + + ApplyInfoDto applyInfoDto3 = applyInfoDtos.getContent().get(2); + assertThat(applyInfoDto3) + .extracting("id", "content", "appliedDate", "status") + .containsExactly(1, "", + LocalDateTime.of(LocalDate.of(2024, 05, 19), LocalTime.of(0, 0, 0, 913489000)), EnApplyStatus.APPROVE); + assertThat(applyInfoDto3.getUser()) + .extracting("id", "name", "orgId", "profileImage", "phone") + .containsExactly(2, "홍길동", 1002, "profile2.jpg", "010-1111-2222"); + assertThat(applyInfoDto3.getUser().getRecentActivity().getGeneration()).isEqualTo(33); + } + + + +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 66da1f4f..95173295 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -115,7 +115,7 @@ void init() { .content("제 지원동기는요") .build(); - meeting.addApply(apply); + //meeting.addApply(apply); apply.updateApplyStatus(EnApplyStatus.APPROVE); applies = new ArrayList<>(); @@ -126,6 +126,7 @@ void init() { void 내모임조회_성공() { // given User user = UserFixture.createStaticUser(); + user.setUserIdForTest(3); doReturn(Optional.of(user)).when(userRepository).findByOrgId(any()); doReturn(applies).when(applyRepository).findAllByUserIdAndStatus(any(), any()); @@ -145,6 +146,7 @@ void init() { @Test public void 모임신청시_정원이찼을때_예외발생() { // given + applies = new ArrayList<>(); for (int i = 0; i < meeting.getCapacity(); i++) { User tempUser = User.builder() .name("user" + i) @@ -162,12 +164,14 @@ void init() { .build(); apply.updateApplyStatus(EnApplyStatus.APPROVE); // 승인 상태로 변경 + applies.add(apply); } - applies = new ArrayList<>(meeting.getAppliedInfo()); + MeetingV2ApplyMeetingDto requestBody = new MeetingV2ApplyMeetingDto(meeting.getId(), "열심히 하겠습니다."); doReturn(meeting).when(meetingRepository).findByIdOrThrow(requestBody.getMeetingId()); doReturn(applyUser).when(userRepository).findByIdOrThrow(applyUser.getId()); + doReturn(applies).when(applyRepository).findAllByMeetingId(any()); // when & then BadRequestException exception = assertThrows(BadRequestException.class, () -> { diff --git a/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java b/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java new file mode 100644 index 00000000..936d44a0 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/post/v2/repository/PostRepositoryTest.java @@ -0,0 +1,131 @@ +package org.sopt.makers.crew.main.post.v2.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.config.TestConfig; +import org.sopt.makers.crew.main.entity.post.PostRepository; +import org.sopt.makers.crew.main.post.v2.dto.query.PostGetPostsCommand; +import org.sopt.makers.crew.main.post.v2.dto.response.PostDetailResponseDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.jdbc.SqlGroup; + +@DataJpaTest +@ActiveProfiles("test") +@Import(TestConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@SqlGroup({ + @Sql(value = "/sql/post-repository-test-data.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = ExecutionPhase.AFTER_TEST_METHOD) + +}) +public class PostRepositoryTest { + @Autowired + private PostRepository postRepository; + + @Test + void 모임_ID로_필터링해서_게시글_목록_조회() { + // given + int page = 1; + int take = 9; + Integer meetingId = 1; + PostGetPostsCommand queryCommand = new PostGetPostsCommand(meetingId, page, take); + Integer userId = 1; + + // when + Page postDetailJsonDtos = postRepository.findPostList(queryCommand, + PageRequest.of(page - 1, take), + userId); + + // then + assertThat(postDetailJsonDtos.getTotalElements()).isEqualTo(3); + assertThat(postDetailJsonDtos.getSize()).isEqualTo(take); + assertThat(postDetailJsonDtos.getNumber()).isEqualTo(page - 1); + assertThat(postDetailJsonDtos.getTotalPages()).isEqualTo(1); + + PostDetailResponseDto postDetailDto1 = postDetailJsonDtos.getContent().get(0); + assertThat(postDetailDto1) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(3, "제목3", "내용3", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 2, 0)), + 0, false, 0); + assertThat(postDetailDto1.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + + PostDetailResponseDto postDetailDto2 = postDetailJsonDtos.getContent().get(1); + assertThat(postDetailDto2) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(2, "제목2", "내용2", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 1, 0)), + 0, false, 0); + assertThat(postDetailDto2.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + + PostDetailResponseDto postDetailDto3 = postDetailJsonDtos.getContent().get(2); + assertThat(postDetailDto3) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(1, "제목1", "내용1", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 0, 0)), + 2, true, 5); + assertThat(postDetailDto3.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + // 댓글 작성자 최대 3명 순서 검증 + assertThat(postDetailDto3.getCommenterThumbnails()) + .containsExactly("profile1.jpg", "profile2.jpg", "profile3.jpg"); + } + + @Test + void 게시글_목록_전체_조회() { + // given + int page = 1; + int take = 9; + PostGetPostsCommand queryCommand = new PostGetPostsCommand(null, page, take); + Integer userId = 1; + + // when + Page postDetailJsonDtos = postRepository.findPostList(queryCommand, + PageRequest.of(page - 1, take), + userId); + + // then + assertThat(postDetailJsonDtos.getTotalElements()).isEqualTo(5); + assertThat(postDetailJsonDtos.getSize()).isEqualTo(take); + assertThat(postDetailJsonDtos.getNumber()).isEqualTo(page - 1); + assertThat(postDetailJsonDtos.getTotalPages()).isEqualTo(1); + + PostDetailResponseDto postDetailDto1 = postDetailJsonDtos.getContent().get(0); + assertThat(postDetailDto1) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(5, "제목5", "내용5", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 4, 0)), + 0, false, 0); + assertThat(postDetailDto1.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(2, "스터디 구합니다2", "스터디"); + + PostDetailResponseDto postDetailDto2 = postDetailJsonDtos.getContent().get(4); + assertThat(postDetailDto2) + .extracting("id", "title", "contents", "createdDate", "likeCount", "isLiked", "commentCount") + .containsExactly(1, "제목1", "내용1", + LocalDateTime.of(LocalDate.of(2024, 06, 11), LocalTime.of(10, 0, 0, 0)), + 2, true, 5); + assertThat(postDetailDto2.getMeeting()) + .extracting("id", "title", "category") + .containsExactly(1, "스터디 구합니다1", "행사"); + } + +} diff --git a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java new file mode 100644 index 00000000..548a2779 --- /dev/null +++ b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java @@ -0,0 +1,123 @@ +package org.sopt.makers.crew.main.user.v2; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.common.annotation.IntegratedTest; +import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.entity.user.UserRepository; +import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.service.UserV2Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +@IntegratedTest +public class UserServiceTest { + + @Autowired + private UserV2Service userV2Service; + + @Autowired + private UserRepository userRepository; + + + @Test + void 멘션_사용자_조회(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly( "홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + + } + + @Test + void 멘션_사용자_조회시_db에_null_저장된_경우(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(null) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(1); + assertThat(allMentionUsers.get(0)) + .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + } + + @Test + void 멘션_사용자_조회시_db에_올바르지_않은_데이터_저장된_경우(){ + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO(null, 30), new UserActivityVO("", 34))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllMentionUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "", 34, "image-url2"); + } +} diff --git a/main/src/test/resources/sql/apply-repository-test-data.sql b/main/src/test/resources/sql/apply-repository-test-data.sql new file mode 100644 index 00000000..3ddff246 --- /dev/null +++ b/main/src/test/resources/sql/apply-repository-test-data.sql @@ -0,0 +1,55 @@ +CREATE TYPE meeting_joinableparts_enum AS ENUM ('PM', 'DESIGN', 'IOS', 'ANDROID', 'SERVER', 'WEB'); + +INSERT INTO "user" (id, name, "orgId", activities, "profileImage", phone) +VALUES (1, '김삼순', 1001, + '[{"part": "서버", "generation": 33}, {"part": "iOS", "generation": 32}]', + 'profile1.jpg', '010-1234-5678'), + (2, '홍길동', 1002, + '[{"part": "기획", "generation": 32}, {"part": "기획", "generation": 29}, {"part": "기획", "generation": 33}, {"part": "기획", "generation": 30}]', + 'profile2.jpg', '010-1111-2222'), + (3, '김철수', 1003, + '[{"part": "웹", "generation": 34}]', + 'profile3.jpg', '010-3333-4444'), + (4, '이영지', 1004, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'); + +create table "meeting" ( + "canJoinOnlyActiveGeneration" boolean not null, + "capacity" integer not null, + "createdGeneration" integer not null, + "id" serial not null, + "isMentorNeeded" boolean not null, + "targetActiveGeneration" integer, + "userId" integer, + "endDate" TIMESTAMP not null, + "mEndDate" TIMESTAMP not null, + "mStartDate" TIMESTAMP not null, + "startDate" TIMESTAMP not null, + "category" varchar(255) not null, + "desc" varchar(255) not null, + "leaderDesc" varchar(255) not null, + "note" varchar(255), + "processDesc" varchar(255) not null, + "targetDesc" varchar(255) not null, + "title" varchar(255) not null, + "imageURL" jsonb, + "joinableParts" meeting_joinableparts_enum[], + primary key ("id") +); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (1, 1, '스터디 구합니다1', '행사', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-19 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO apply (type, "meetingId", "userId", content, "appliedDate", status) +VALUES (0, 1, 2, '전할말입니다1', '2024-05-19 00:00:00.913489', 1), + (0, 1, 3, '전할말입니다2', '2024-05-19 00:00:02.413489', 1), + (0, 1, 4, '전할말입니다3', '2024-05-19 00:00:03.413489', 0); \ No newline at end of file diff --git a/main/src/test/resources/sql/delete-all-data.sql b/main/src/test/resources/sql/delete-all-data.sql new file mode 100644 index 00000000..637ccb04 --- /dev/null +++ b/main/src/test/resources/sql/delete-all-data.sql @@ -0,0 +1,30 @@ +-- 테이블의 데이터 삭제 +DELETE FROM "apply"; +DELETE FROM "comment"; +DELETE FROM "like"; +DELETE FROM "meeting"; +DELETE FROM "notice"; +DELETE FROM "post"; +DELETE FROM "user"; + +-- 시퀀스 초기화 +-- apply 테이블의 시퀀스 초기화 +ALTER SEQUENCE "apply_id_seq" RESTART WITH 1; + +-- comment 테이블의 시퀀스 초기화 +ALTER SEQUENCE "comment_id_seq" RESTART WITH 1; + +-- "like" 테이블의 시퀀스 초기화 +ALTER SEQUENCE "like_id_seq" RESTART WITH 1; + +-- meeting 테이블의 시퀀스 초기화 +ALTER SEQUENCE "meeting_id_seq" RESTART WITH 1; + +-- notice 테이블의 시퀀스 초기화 +ALTER SEQUENCE "notice_id_seq" RESTART WITH 1; + +-- post 테이블의 시퀀스 초기화 +ALTER SEQUENCE "post_id_seq" RESTART WITH 1; + +-- user 테이블의 시퀀스 초기화 +ALTER SEQUENCE "user_id_seq" RESTART WITH 1; \ No newline at end of file diff --git a/main/src/test/resources/sql/post-repository-test-data.sql b/main/src/test/resources/sql/post-repository-test-data.sql new file mode 100644 index 00000000..e2676f94 --- /dev/null +++ b/main/src/test/resources/sql/post-repository-test-data.sql @@ -0,0 +1,82 @@ +CREATE TYPE meeting_joinableparts_enum AS ENUM ('PM', 'DESIGN', 'IOS', 'ANDROID', 'SERVER', 'WEB'); + +INSERT INTO "user" (id, name, "orgId", activities, "profileImage", phone) +VALUES (1, '김삼순', 1001, + '[{"part": "서버", "generation": 33}, {"part": "iOS", "generation": 32}]', + 'profile1.jpg', '010-1234-5678'), + (2, '홍길동', 1002, + '[{"part": "기획", "generation": 32}, {"part": "기획", "generation": 29}, {"part": "기획", "generation": 33}, {"part": "기획", "generation": 30}]', + 'profile2.jpg', '010-1111-2222'), + (3, '김철수', 1003, + '[{"part": "웹", "generation": 34}]', + 'profile3.jpg', '010-3333-4444'), + (4, '이영지', 1004, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'); + +create table "meeting" ( + "canJoinOnlyActiveGeneration" boolean not null, + "capacity" integer not null, + "createdGeneration" integer not null, + "id" serial not null, + "isMentorNeeded" boolean not null, + "targetActiveGeneration" integer, + "userId" integer, + "endDate" TIMESTAMP not null, + "mEndDate" TIMESTAMP not null, + "mStartDate" TIMESTAMP not null, + "startDate" TIMESTAMP not null, + "category" varchar(255) not null, + "desc" varchar(255) not null, + "leaderDesc" varchar(255) not null, + "note" varchar(255), + "processDesc" varchar(255) not null, + "targetDesc" varchar(255) not null, + "title" varchar(255) not null, + "imageURL" jsonb, + "joinableParts" meeting_joinableparts_enum[], + primary key ("id") +); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (1, 1, '스터디 구합니다1', '행사', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-19 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO meeting (id, "userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", "targetDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (2, 1, '스터디 구합니다2', '스터디', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-05-20 00:00:00.000000', '2024-05-25 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-30 00:00:00.000000', '2024-06-01 00:00:00.000000', '스터디장 설명입니다.', + '누구나 들어올 수 있어요.', '시간지키세요.', true, false, 34, 34, '{PM,DESIGN,WEB,ANDROID,IOS,SERVER}'); + +INSERT INTO post (id, title, contents, "createdDate", "updatedDate", "viewCount", images, "userId", "meetingId", "commentCount", "likeCount") +VALUES + (1, '제목1', '내용1', '2024-06-11 10:00:00', '2024-06-11 10:00:000000', 1, NULL, 1, 1, 5, 2), + (2, '제목2', '내용2', '2024-06-11 10:00:01', '2024-06-11 10:00:000001', 2, NULL, 2, 1, 0, 0), + (3, '제목3', '내용3', '2024-06-11 10:00:02', '2024-06-11 10:00:000002', 3, NULL, 3, 1, 0, 0), + (4, '제목4', '내용4', '2024-06-11 10:00:03', '2024-06-11 10:00:000003', 4, NULL, 2, 2, 0, 0), + (5, '제목5', '내용5', '2024-06-11 10:00:04', '2024-06-11 10:00:000004', 5, NULL, 4, 2, 0, 0); + +INSERT INTO comment (id, contents, depth, "order", "createdDate", "updatedDate", "userId", "postId", "likeCount", "parentId") +VALUES + (1, '첫 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:00', '2024-06-12 10:00:000000', 1, 1, 0, NULL), + (2, '두 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:01', '2024-06-12 10:00:000001', 2, 1, 0, NULL), + (3, '세 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:02', '2024-06-12 10:00:000002', 2, 1, 0, NULL), + (4, '네 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:03', '2024-06-12 10:00:000003', 3, 1, 0, NULL), + (5, '다섯 번째 댓글입니다.', 0, 0, '2024-06-12 10:00:04', '2024-06-12 10:00:000004', 4, 1, 0, NULL); + +INSERT INTO "like" (id, "createdDate", "userId", "postId") +VALUES + (1, '2024-06-12 11:00:00', 1, 1), + (2, '2024-06-12 11:00:01', 2, 1); diff --git a/server/src/comment/v1/comment-v1.controller.ts b/server/src/comment/v1/comment-v1.controller.ts index 38a1e3ab..bfb127a0 100644 --- a/server/src/comment/v1/comment-v1.controller.ts +++ b/server/src/comment/v1/comment-v1.controller.ts @@ -43,6 +43,7 @@ export class CommentV1Controller { @ApiOperation({ summary: '모임 게시글 댓글 리스트 조회', + deprecated: true, }) @ApiOkResponseCommon(CommentV1GetCommentsResponseDto) @ApiResponse({ @@ -74,8 +75,12 @@ export class CommentV1Controller { return this.commentV1Service.switchCommentLike({ param, user }); } + /** + * @deprecated + */ @ApiOperation({ summary: '댓글 신고', + deprecated: true, }) @ApiOkResponseCommon(CommentV1ReportCommentResponseDto) @ApiResponse({ @@ -113,8 +118,12 @@ export class CommentV1Controller { return this.commentV1Service.createPostComment({ body, user }); } + /** + * @deprecated + */ @ApiOperation({ summary: '모임 게시글 댓글 수정', + deprecated: true, }) @ApiOkResponseCommon(CommentV1CreateCommentResponseDto) @ApiResponse({ @@ -137,8 +146,12 @@ export class CommentV1Controller { }); } + /** + * @deprecated + */ @ApiOperation({ summary: '모임 게시글 댓글 삭제', + deprecated: true, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, diff --git a/server/src/entity/user/enum/user-part.enum.ts b/server/src/entity/user/enum/user-part.enum.ts index f4b391d4..cdc3f91f 100644 --- a/server/src/entity/user/enum/user-part.enum.ts +++ b/server/src/entity/user/enum/user-part.enum.ts @@ -22,6 +22,6 @@ export enum UserPart { GENERAL_AFFAIRS = '총무', OPERATION_LEADER = '운영 팀장', MEDIA_LEADER = '미디어 팀장', - MAKERS_LEADER = '메이커스 리드', + MAKERS_LEADER = '메이커스 팀장', } diff --git a/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts b/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts index 359cea38..90eb010e 100644 --- a/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts +++ b/server/src/post/v1/dto/get-meeting-post/post-v1-get-post-response.dto.ts @@ -96,9 +96,9 @@ export class PostV1GetPostResponseDto { @IsString() contents: string; - /** 게시글 게시/업데이트 일자 */ + /** 게시글 게시/생성 일자 */ @IsDate() - updatedDate: Date; + createdDate: Date; /** 첨부 이미지 */ @IsOptional() diff --git a/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts b/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts index 9bd73659..d47b8829 100644 --- a/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts +++ b/server/src/post/v1/dto/get-meeting-posts/post-v1-get-posts-response-post.dto.ts @@ -68,9 +68,9 @@ export class PostV1GetPostsResponsePostDto { @IsString() contents: string; - /** 게시글 게시/업데이트 일자 */ + /** 게시글 게시/생성 일자 */ @IsDate() - updatedDate: Date; + createdDate: Date; /** 첨부 이미지 */ @IsOptional() diff --git a/server/src/post/v1/post-v1.controller.ts b/server/src/post/v1/post-v1.controller.ts index 35d64bb1..dca431f6 100644 --- a/server/src/post/v1/post-v1.controller.ts +++ b/server/src/post/v1/post-v1.controller.ts @@ -63,6 +63,7 @@ export class PostV1Controller { @ApiOperation({ summary: '모임 게시글 목록 조회', + deprecated: true, }) @ApiOkResponseCommon(PostV1GetPostsResponseDto) @ApiResponse({ diff --git a/server/src/post/v1/post-v1.service.ts b/server/src/post/v1/post-v1.service.ts index a6bdd092..1fc9fb52 100644 --- a/server/src/post/v1/post-v1.service.ts +++ b/server/src/post/v1/post-v1.service.ts @@ -102,7 +102,7 @@ export class PostV1Service { id: post.id, title: post.title, contents: post.contents, - updatedDate: post.updatedDate, + createdDate: post.createdDate, images: post.images, user: { id: post.user.id, @@ -161,7 +161,7 @@ export class PostV1Service { id: post.id, title: post.title, contents: post.contents, - updatedDate: post.updatedDate, + createdDate: post.createdDate, images: post.images, user: { id: post.user.id,