diff --git a/build.gradle b/build.gradle index 78d1bdae..ace35976 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,11 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/example/sharemind/global/config/QueryDslConfig.java b/src/main/java/com/example/sharemind/global/config/QueryDslConfig.java new file mode 100644 index 00000000..0d8b3cc3 --- /dev/null +++ b/src/main/java/com/example/sharemind/global/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.example.sharemind.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfig { + + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/com/example/sharemind/post/application/PostService.java b/src/main/java/com/example/sharemind/post/application/PostService.java index 3bd17536..f7703ee1 100644 --- a/src/main/java/com/example/sharemind/post/application/PostService.java +++ b/src/main/java/com/example/sharemind/post/application/PostService.java @@ -3,6 +3,8 @@ import com.example.sharemind.post.domain.Post; import com.example.sharemind.post.dto.request.PostCreateRequest; import com.example.sharemind.post.dto.request.PostUpdateRequest; +import com.example.sharemind.post.dto.response.PostGetIsSavedResponse; +import com.example.sharemind.post.dto.response.PostGetResponse; import java.util.List; public interface PostService { @@ -14,4 +16,10 @@ public interface PostService { Post getPostByPostId(Long postId); void updatePost(PostUpdateRequest postUpdateRequest, Long customerId); + + PostGetIsSavedResponse getIsSaved(Long postId); + + PostGetResponse getPost(Long postId); + + List getPostsByCustomer(Boolean filter, Long postId, Long customerId); } diff --git a/src/main/java/com/example/sharemind/post/application/PostServiceImpl.java b/src/main/java/com/example/sharemind/post/application/PostServiceImpl.java index 3ce60de3..13f598ea 100644 --- a/src/main/java/com/example/sharemind/post/application/PostServiceImpl.java +++ b/src/main/java/com/example/sharemind/post/application/PostServiceImpl.java @@ -6,6 +6,8 @@ import com.example.sharemind.post.domain.Post; import com.example.sharemind.post.dto.request.PostCreateRequest; import com.example.sharemind.post.dto.request.PostUpdateRequest; +import com.example.sharemind.post.dto.response.PostGetIsSavedResponse; +import com.example.sharemind.post.dto.response.PostGetResponse; import com.example.sharemind.post.exception.PostErrorCode; import com.example.sharemind.post.exception.PostException; import com.example.sharemind.post.repository.PostRepository; @@ -19,6 +21,8 @@ @Transactional(readOnly = true) public class PostServiceImpl implements PostService { + private static final int POST_CUSTOMER_PAGE_SIZE = 4; + private final PostRepository postRepository; private final CustomerService customerService; @@ -52,4 +56,31 @@ public void updatePost(PostUpdateRequest postUpdateRequest, Long customerId) { post.updatePost(consultCategory, postUpdateRequest.getTitle(), postUpdateRequest.getContent(), postUpdateRequest.getIsCompleted(), customer); } + + @Override + public PostGetIsSavedResponse getIsSaved(Long postId) { + Post post = getPostByPostId(postId); + + if ((post.getIsCompleted() != null) && !post.getIsCompleted()) { + return PostGetIsSavedResponse.of(post); + } else { + return PostGetIsSavedResponse.of(); + } + } + + @Override + public PostGetResponse getPost(Long postId) { + return PostGetResponse.of(getPostByPostId(postId)); + } + + @Override + public List getPostsByCustomer(Boolean filter, Long postId, Long customerId) { + Customer customer = customerService.getCustomerByCustomerId(customerId); + + return postRepository.findAllByCustomerAndIsActivatedIsTrue(customer, filter, postId, + POST_CUSTOMER_PAGE_SIZE).stream() + .map(post -> (post.getIsCompleted() != null && !post.getIsCompleted()) + ? PostGetResponse.ofIsNotCompleted(post) : PostGetResponse.of(post)) + .toList(); + } } diff --git a/src/main/java/com/example/sharemind/post/content/PostStatus.java b/src/main/java/com/example/sharemind/post/content/PostStatus.java index a8c39bcd..a32de0ed 100644 --- a/src/main/java/com/example/sharemind/post/content/PostStatus.java +++ b/src/main/java/com/example/sharemind/post/content/PostStatus.java @@ -7,9 +7,10 @@ @RequiredArgsConstructor public enum PostStatus { - WAITING("상담 대기"), - PROCEEDING("상담 진행 중"), - COMPLETED("상담 마감"), + WAITING("답변 대기"), + PROCEEDING("답변 진행 중"), + COMPLETED("답변 완료"), + CANCELLED("상담 취소"), REPORTED("신고로 인한 게시 중단"); private final String displayName; diff --git a/src/main/java/com/example/sharemind/post/domain/Post.java b/src/main/java/com/example/sharemind/post/domain/Post.java index 28010d85..8faf7185 100644 --- a/src/main/java/com/example/sharemind/post/domain/Post.java +++ b/src/main/java/com/example/sharemind/post/domain/Post.java @@ -40,6 +40,7 @@ public class Post extends BaseEntity { @Enumerated(EnumType.STRING) private ConsultCategory consultCategory; + @Size(max = 50, message = "제목은 최대 50자입니다.") private String title; @Size(max = 1000, message = "상담 내용은 최대 1000자입니다.") @@ -61,6 +62,9 @@ public class Post extends BaseEntity { @Column(name = "total_comment", nullable = false) private Long totalComment; + @Column(name = "total_scrap", nullable = false) + private Long totalScrap; + @Column(name = "is_paid", nullable = false) private Boolean isPaid; @@ -75,6 +79,7 @@ public Post(Customer customer, Long cost, Boolean isPublic) { this.postStatus = PostStatus.WAITING; this.totalLike = 0L; this.totalComment = 0L; + this.totalScrap = 0L; setIsPaid(isPublic); } @@ -112,7 +117,7 @@ private void checkWriteAuthority(Customer customer) { } private void checkUpdatability() { - if (this.isCompleted.equals(true)) { + if (this.isCompleted != null && this.isCompleted.equals(true)) { throw new PostException(PostErrorCode.POST_ALREADY_COMPLETED); } } diff --git a/src/main/java/com/example/sharemind/post/dto/request/PostUpdateRequest.java b/src/main/java/com/example/sharemind/post/dto/request/PostUpdateRequest.java index 93022be4..db09c95b 100644 --- a/src/main/java/com/example/sharemind/post/dto/request/PostUpdateRequest.java +++ b/src/main/java/com/example/sharemind/post/dto/request/PostUpdateRequest.java @@ -19,6 +19,7 @@ public class PostUpdateRequest { @Schema(description = "상담 제목", example = "남자친구의 심리가 궁금해요") @NotBlank(message = "상담 제목은 공백일 수 없습니다.") + @Size(max = 50, message = "제목은 최대 50자입니다.") private String title; @Schema(description = "상담 내용", example = "안녕하세요 어쩌구저쩌구~") diff --git a/src/main/java/com/example/sharemind/post/dto/response/PostGetIsSavedResponse.java b/src/main/java/com/example/sharemind/post/dto/response/PostGetIsSavedResponse.java new file mode 100644 index 00000000..cc57bcde --- /dev/null +++ b/src/main/java/com/example/sharemind/post/dto/response/PostGetIsSavedResponse.java @@ -0,0 +1,36 @@ +package com.example.sharemind.post.dto.response; + +import com.example.sharemind.post.domain.Post; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PostGetIsSavedResponse { + + @Schema(description = "임시저장 메시지 존재하면 true, 아니면 false", example = "true") + private final Boolean isSaved; + + @Schema(description = "마지막 수정일시, isSaved false면 null", example = "2023년 12월 25일 오후 12시 34분", type = "string") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy년 MM월 dd일 a HH시 mm분") + private final LocalDateTime updatedAt; + + @Builder + public PostGetIsSavedResponse(Boolean isSaved, LocalDateTime updatedAt) { + this.isSaved = isSaved; + this.updatedAt = updatedAt; + } + + public static PostGetIsSavedResponse of(Post post) { + return PostGetIsSavedResponse.builder() + .isSaved(true) + .updatedAt(post.getUpdatedAt()) + .build(); + } + + public static PostGetIsSavedResponse of() { + return PostGetIsSavedResponse.builder().build(); + } +} diff --git a/src/main/java/com/example/sharemind/post/dto/response/PostGetResponse.java b/src/main/java/com/example/sharemind/post/dto/response/PostGetResponse.java new file mode 100644 index 00000000..846e136f --- /dev/null +++ b/src/main/java/com/example/sharemind/post/dto/response/PostGetResponse.java @@ -0,0 +1,77 @@ +package com.example.sharemind.post.dto.response; + +import com.example.sharemind.global.utils.TimeUtil; +import com.example.sharemind.post.domain.Post; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PostGetResponse { + + @Schema(description = "일대다 질문 아이디") + private final Long postId; + + @Schema(description = "상담 카테고리", example = "연애갈등") + private final String consultCategory; + + @Schema(description = "제목") + private final String title; + + @Schema(description = "상담 내용") + private final String content; + + @Schema(description = "공개/비공개 여부") + private final Boolean isPublic; + + @Schema(description = "좋아요 수") + private final Long totalLike; + + @Schema(description = "스크랩 수") + private final Long totalScrap; + + @Schema(description = "마지막 업데이트 일시", example = "8분 전") + private final String updatedAt; + + @Builder + public PostGetResponse(Long postId, String consultCategory, String title, String content, + Boolean isPublic, Long totalLike, Long totalScrap, String updatedAt) { + this.postId = postId; + this.consultCategory = consultCategory; + this.title = title; + this.content = content; + this.isPublic = isPublic; + this.totalLike = totalLike; + this.totalScrap = totalScrap; + this.updatedAt = updatedAt; + } + + public static PostGetResponse of(Post post) { + String consultCategory = post.getConsultCategory() == null ? null + : post.getConsultCategory().getDisplayName(); + + return PostGetResponse.builder() + .postId(post.getPostId()) + .consultCategory(consultCategory) + .title(post.getTitle()) + .content(post.getContent()) + .isPublic(post.getIsPublic()) + .totalLike(post.getTotalLike()) + .totalScrap(post.getTotalScrap()) + .updatedAt(TimeUtil.getUpdatedAt(post.getUpdatedAt())) + .build(); + } + + public static PostGetResponse ofIsNotCompleted(Post post) { + return PostGetResponse.builder() + .postId(post.getPostId()) + .consultCategory(null) + .title(null) + .content(null) + .isPublic(post.getIsPublic()) + .totalLike(post.getTotalLike()) + .totalScrap(post.getTotalScrap()) + .updatedAt(TimeUtil.getUpdatedAt(post.getUpdatedAt())) + .build(); + } +} diff --git a/src/main/java/com/example/sharemind/post/presentation/PostController.java b/src/main/java/com/example/sharemind/post/presentation/PostController.java index 8b353dfc..bb8de807 100644 --- a/src/main/java/com/example/sharemind/post/presentation/PostController.java +++ b/src/main/java/com/example/sharemind/post/presentation/PostController.java @@ -5,21 +5,29 @@ import com.example.sharemind.post.application.PostService; import com.example.sharemind.post.dto.request.PostCreateRequest; import com.example.sharemind.post.dto.request.PostUpdateRequest; +import com.example.sharemind.post.dto.response.PostGetIsSavedResponse; +import com.example.sharemind.post.dto.response.PostGetResponse; 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.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "Post Controller", description = "일대다 상담 질문 컨트롤러") @@ -73,4 +81,62 @@ public ResponseEntity updatePost(@Valid @RequestBody PostUpdateRequest pos postService.updatePost(postUpdateRequest, customUserDetails.getCustomer().getCustomerId()); return ResponseEntity.ok().build(); } + + @Operation(summary = "일대다 상담 질문 임시저장 내용 존재 여부 조회", + description = "임시저장 내용 존재 여부 조회") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공(임시저장 내용 존재하지 않으면 수정일시는 null로 반환됨)"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 일대다 질문 아이디로 요청됨", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomExceptionResponse.class)) + ) + }) + @Parameters({ + @Parameter(name = "postId", description = "일대다 질문 아이디") + }) + @GetMapping("/drafts/{postId}") + public ResponseEntity getIsSaved(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getIsSaved(postId)); + } + + @Operation(summary = "일대다 상담 질문 단건 조회", description = "일대다 상담 질문 단건 조회") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 일대다 질문 아이디로 요청됨", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomExceptionResponse.class)) + ) + }) + @Parameters({ + @Parameter(name = "postId", description = "일대다 질문 아이디") + }) + @GetMapping("/{postId}") + public ResponseEntity getPost(@PathVariable Long postId) { + return ResponseEntity.ok(postService.getPost(postId)); + } + + @Operation(summary = "구매자 본인 일대다 상담 리스트 조회", description = """ + - 구매자 상담 탭에서 본인이 작성한 일대다 상담 질문 리스트 조회 + - 주소 형식: /api/v1/posts/customers?filter=true&postId=0""") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomExceptionResponse.class)) + ) + }) + @Parameters({ + @Parameter(name = "filter", description = "완료/취소된 상담 제외: true, 포함: false"), + @Parameter(name = "postId", description = """ + - 조회 결과는 4개씩 반환하며, postId로 구분 + 1. 최초 조회 요청이면 postId는 0 + 2. 2번째 요청부터 postId는 직전 요청의 조회 결과 4개 중 마지막 postId""") + }) + @GetMapping("/customers") + public ResponseEntity> getPostsByCustomer(@RequestParam Boolean filter, + @RequestParam Long postId, + @AuthenticationPrincipal CustomUserDetails customUserDetails) { + return ResponseEntity.ok(postService.getPostsByCustomer(filter, postId, + customUserDetails.getCustomer().getCustomerId())); + } } diff --git a/src/main/java/com/example/sharemind/post/repository/PostCustomRepository.java b/src/main/java/com/example/sharemind/post/repository/PostCustomRepository.java new file mode 100644 index 00000000..93b64f4f --- /dev/null +++ b/src/main/java/com/example/sharemind/post/repository/PostCustomRepository.java @@ -0,0 +1,11 @@ +package com.example.sharemind.post.repository; + +import com.example.sharemind.customer.domain.Customer; +import com.example.sharemind.post.domain.Post; +import java.util.List; + +public interface PostCustomRepository { + + List findAllByCustomerAndIsActivatedIsTrue(Customer customer, Boolean filter, + Long postId, int size); +} diff --git a/src/main/java/com/example/sharemind/post/repository/PostCustomRepositoryImpl.java b/src/main/java/com/example/sharemind/post/repository/PostCustomRepositoryImpl.java new file mode 100644 index 00000000..1fb7a4ad --- /dev/null +++ b/src/main/java/com/example/sharemind/post/repository/PostCustomRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.example.sharemind.post.repository; + +import com.example.sharemind.customer.domain.Customer; +import com.example.sharemind.post.content.PostStatus; +import com.example.sharemind.post.domain.Post; +import com.example.sharemind.post.domain.QPost; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class PostCustomRepositoryImpl implements PostCustomRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final QPost post = QPost.post; + + @Override + public List findAllByCustomerAndIsActivatedIsTrue(Customer customer, Boolean filter, + Long postId, int size) { + return jpaQueryFactory + .selectFrom(post) + .where( + post.customer.eq(customer), + postStatusIn(filter), + post.isPaid.isTrue(), + post.isActivated.isTrue(), + lessThanPostId(postId) + ).orderBy(post.postId.desc()).limit(size).fetch(); + } + + private BooleanExpression postStatusIn(Boolean filter) { + return filter ? post.postStatus.in(PostStatus.WAITING, PostStatus.PROCEEDING, + PostStatus.REPORTED) : null; + } + + private BooleanExpression lessThanPostId(Long postId) { + return postId != 0 ? post.postId.lt(postId) : null; + } +} diff --git a/src/main/java/com/example/sharemind/post/repository/PostRepository.java b/src/main/java/com/example/sharemind/post/repository/PostRepository.java index 3f3cb7ec..51d08302 100644 --- a/src/main/java/com/example/sharemind/post/repository/PostRepository.java +++ b/src/main/java/com/example/sharemind/post/repository/PostRepository.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository; @Repository -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, PostCustomRepository { Optional findByPostIdAndIsActivatedIsTrue(Long postId);