diff --git a/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java b/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java index 8b2f11d..94a6544 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/AdminConsultationController.java @@ -38,8 +38,11 @@ public ResponseEntity getConsultationInfo( public ResponseEntity> getAllConsultations( ConsultationSearchCondition searchCondition, @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + ConsultationSearchCondition condition = searchCondition == null + ? ConsultationSearchCondition.empty() + : searchCondition; PageResponse response = adminConsultationService - .getAllConsultationsBySearchCondition(searchCondition, pageable); + .getAllConsultationsBySearchCondition(condition, pageable); return ResponseEntity.ok(response); } diff --git a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java index 97a397d..1499d84 100644 --- a/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java +++ b/src/main/java/redot/redot_server/domain/admin/controller/docs/AdminConsultationControllerDocs.java @@ -22,7 +22,7 @@ public interface AdminConsultationControllerDocs { content = @Content(schema = @Schema(implementation = ConsultationResponse.class))) ResponseEntity getConsultationInfo(@Parameter(description = "상담 ID", example = "1") Long consultationId); - @Operation(summary = "상담 목록 조회", description = "`email`, `phone`, `status`, `type`, `currentWebsiteUrl` 검색 조건을 조합해 조회하며 `sort=createdAt,desc` 와 같은 Pageable 쿼리 파라미터를 사용합니다.") + @Operation(summary = "상담 목록 조회", description = "`email`, `phone`, `status`, `type`, `currentWebsiteUrl`, `startDate`, `endDate` 검색 조건을 조합해 조회하며 `sort=createdAt,desc` 와 같은 Pageable 쿼리 파라미터를 사용합니다. `status` 파라미터를 생략하면 CANCELLED 상태 상담은 응답에서 제외됩니다.") @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = PageResponse.class))) ResponseEntity> getAllConsultations( diff --git a/src/main/java/redot/redot_server/domain/admin/dto/ConsultationSearchCondition.java b/src/main/java/redot/redot_server/domain/admin/dto/ConsultationSearchCondition.java index def5323..725d4e9 100644 --- a/src/main/java/redot/redot_server/domain/admin/dto/ConsultationSearchCondition.java +++ b/src/main/java/redot/redot_server/domain/admin/dto/ConsultationSearchCondition.java @@ -1,5 +1,7 @@ package redot.redot_server.domain.admin.dto; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; import redot.redot_server.domain.redot.consultation.entity.ConsultationStatus; import redot.redot_server.domain.redot.consultation.entity.ConsultationType; @@ -8,6 +10,11 @@ public record ConsultationSearchCondition( String phone, ConsultationStatus status, ConsultationType type, - String currentWebsiteUrl + String currentWebsiteUrl, + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate ) { + public static ConsultationSearchCondition empty() { + return new ConsultationSearchCondition(null, null, null, null, null, null, null); + } } diff --git a/src/main/java/redot/redot_server/domain/admin/dto/request/ConsultationUpdateRequest.java b/src/main/java/redot/redot_server/domain/admin/dto/request/ConsultationUpdateRequest.java index df0faf6..da26d04 100644 --- a/src/main/java/redot/redot_server/domain/admin/dto/request/ConsultationUpdateRequest.java +++ b/src/main/java/redot/redot_server/domain/admin/dto/request/ConsultationUpdateRequest.java @@ -17,6 +17,8 @@ public record ConsultationUpdateRequest( String content, String page, String currentWebsiteUrl, + @Size(max = 1000, message = "비고는 1000자를 초과할 수 없습니다") + String remark, @NotNull(message = "상담 상태를 선택해주세요.") ConsultationStatus status, @NotNull(message = "상담 타입을 선택해주세요.") diff --git a/src/main/java/redot/redot_server/domain/admin/service/AdminConsultationService.java b/src/main/java/redot/redot_server/domain/admin/service/AdminConsultationService.java index fe31102..ca6c823 100644 --- a/src/main/java/redot/redot_server/domain/admin/service/AdminConsultationService.java +++ b/src/main/java/redot/redot_server/domain/admin/service/AdminConsultationService.java @@ -9,6 +9,7 @@ import redot.redot_server.domain.admin.dto.request.ConsultationUpdateRequest; import redot.redot_server.domain.redot.consultation.dto.response.ConsultationResponse; import redot.redot_server.domain.redot.consultation.entity.Consultation; +import redot.redot_server.domain.redot.consultation.entity.ConsultationStatus; import redot.redot_server.domain.redot.consultation.exception.ConsultationErrorCode; import redot.redot_server.domain.redot.consultation.exception.ConsultationException; import redot.redot_server.domain.redot.consultation.repository.ConsultationRepository; @@ -22,16 +23,13 @@ public class AdminConsultationService { private final ConsultationRepository consultationRepository; public ConsultationResponse getConsultationInfo(Long consultationId) { - return consultationRepository.findById(consultationId) - .map(ConsultationResponse::fromEntity) - .orElseThrow(() -> new ConsultationException(ConsultationErrorCode.CONSULTATION_NOT_FOUND)); + Consultation consultation = getConsultation(consultationId); + return ConsultationResponse.fromEntity(consultation); } @Transactional public ConsultationResponse updateConsultationInfo(Long consultationId, ConsultationUpdateRequest request) { - Consultation consultation = consultationRepository.findById(consultationId) - .orElseThrow(() -> new ConsultationException(ConsultationErrorCode.CONSULTATION_NOT_FOUND)); - + Consultation consultation = getConsultation(consultationId); consultation.update(request); return ConsultationResponse.fromEntity(consultation); @@ -44,4 +42,10 @@ public PageResponse getAllConsultationsBySearchCondition( .map(ConsultationResponse::fromEntity); return PageResponse.from(page); } + + private Consultation getConsultation(Long consultationId) { + return consultationRepository.findById(consultationId) + .orElseThrow(() -> new ConsultationException(ConsultationErrorCode.CONSULTATION_NOT_FOUND)); + } + } diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/dto/response/ConsultationResponse.java b/src/main/java/redot/redot_server/domain/redot/consultation/dto/response/ConsultationResponse.java index a965811..11f5a59 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/dto/response/ConsultationResponse.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/dto/response/ConsultationResponse.java @@ -12,6 +12,7 @@ public record ConsultationResponse( String content, String page, String currentWebsiteUrl, + String remark, ConsultationStatus status, ConsultationType type, LocalDateTime createdAt @@ -24,6 +25,7 @@ public static ConsultationResponse fromEntity(Consultation consultation) { consultation.getContent(), consultation.getPage(), consultation.getCurrentWebsiteUrl(), + consultation.getRemark(), consultation.getStatus(), consultation.getType(), consultation.getCreatedAt() diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/entity/Consultation.java b/src/main/java/redot/redot_server/domain/redot/consultation/entity/Consultation.java index 443f865..02095b8 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/entity/Consultation.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/entity/Consultation.java @@ -40,6 +40,9 @@ public class Consultation extends BaseTimeEntity { private String page; + @Column(length = 1000) + private String remark; + @Column(nullable = false) @Enumerated(EnumType.STRING) private ConsultationStatus status; @@ -66,6 +69,7 @@ public void update(ConsultationUpdateRequest request) { this.content = request.content(); this.currentWebsiteUrl = request.currentWebsiteUrl(); this.page = request.page(); + this.remark = request.remark(); this.status = request.status(); this.type = request.type(); } diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/entity/ConsultationStatus.java b/src/main/java/redot/redot_server/domain/redot/consultation/entity/ConsultationStatus.java index 6260342..e703de7 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/entity/ConsultationStatus.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/entity/ConsultationStatus.java @@ -3,5 +3,6 @@ public enum ConsultationStatus { PENDING, IN_PROGRESS, - COMPLETED + COMPLETED, + CANCELLED } diff --git a/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepositoryImpl.java b/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepositoryImpl.java index a332e2d..04e9eb2 100644 --- a/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepositoryImpl.java +++ b/src/main/java/redot/redot_server/domain/redot/consultation/repository/ConsultationRepositoryImpl.java @@ -5,6 +5,7 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -28,9 +29,11 @@ public Page findAllBySearchCondition(ConsultationSearchCondition c BooleanExpression[] predicates = new BooleanExpression[]{ emailContains(condition.email()), phoneContains(condition.phone()), - statusEq(condition.status()), + statusCondition(condition.status()), typeEq(condition.type()), - currentWebsiteUrlContains(condition.currentWebsiteUrl()) + currentWebsiteUrlContains(condition.currentWebsiteUrl()), + createdAtGoe(condition.startDate()), + createdAtLt(condition.endDate()) }; List content = queryFactory.selectFrom(consultation) @@ -70,14 +73,25 @@ private BooleanExpression currentWebsiteUrlContains(String url) { return hasText(url) ? consultation.currentWebsiteUrl.containsIgnoreCase(url) : null; } - private BooleanExpression statusEq(ConsultationStatus status) { - return status == null ? null : consultation.status.eq(status); + private BooleanExpression statusCondition(ConsultationStatus status) { + if (status == null) { + return consultation.status.ne(ConsultationStatus.CANCELLED); + } + return consultation.status.eq(status); } private BooleanExpression typeEq(ConsultationType type) { return type == null ? null : consultation.type.eq(type); } + private BooleanExpression createdAtGoe(LocalDate startDate) { + return startDate == null ? null : consultation.createdAt.goe(startDate.atStartOfDay()); + } + + private BooleanExpression createdAtLt(LocalDate endDate) { + return endDate == null ? null : consultation.createdAt.lt(endDate.plusDays(1).atStartOfDay()); + } + private boolean hasText(String value) { return value != null && !value.isBlank(); } diff --git a/src/main/resources/db/migration/V4__add_consultation_remark_and_cancelled_status.sql b/src/main/resources/db/migration/V4__add_consultation_remark_and_cancelled_status.sql new file mode 100644 index 0000000..3c63bc3 --- /dev/null +++ b/src/main/resources/db/migration/V4__add_consultation_remark_and_cancelled_status.sql @@ -0,0 +1,9 @@ +ALTER TABLE consultations + ADD COLUMN IF NOT EXISTS remark VARCHAR(1000); + +ALTER TABLE consultations + DROP CONSTRAINT IF EXISTS ck_consultation_status; + +ALTER TABLE consultations + ADD CONSTRAINT ck_consultation_status + CHECK (status IN ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'));