Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public class InquiryService {

@Transactional
public void createInquiry(UserJpaEntity user, InquiryCreateRequest request) {
InquiryJpaEntity inquiryEntity = inquiryJpaRepository.save(request.toEntity(user));
InquiryJpaEntity inquiry = inquiryJpaRepository.save(request.toEntity(user));

hasPermission(inquiryEntity.getUserId(), user);
inquiryAttachmentService.createAttachment(request.attachments(), inquiryEntity);
hasPermission(inquiry.getUserId(), user);
inquiryAttachmentService.createAttachment(request.attachments(), inquiry);
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
Expand All @@ -51,12 +51,18 @@ public Page<InquiryResponse> getInquiries(
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public InquiryDetailResponse getInquiryDetail(Long postId) {
public InquiryDetailResponse getInquiryDetail(UserJpaEntity user, Long postId) {
InquiryJpaEntity inquiry = getInquiry(postId);
hasPermission(inquiry.getUserId(), user);

return toInquiryDetailResponse(inquiry);
}

@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public Page<InquiryResponse> getMyInquiry(Long userId, Pageable pageable) {
return inquiryJpaRepository.searchMyInquiry(userId, pageable);
}

@Transactional
public void deleteInquiry(UserJpaEntity user, Long postId) {
InquiryJpaEntity inquiry = getInquiry(postId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface InquiryQueryRepository {

Page<InquiryResponse> searchInquiries(InquiryStatus status, String sortField, boolean asc,
Pageable pageable);

Page<InquiryResponse> searchMyInquiry(Long userId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import static life.mosu.mosuserver.domain.base.BaseTimeEntity.formatDate;

import com.querydsl.core.Tuple;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.ComparableExpressionBase;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
Expand All @@ -25,6 +27,7 @@
public class InquiryJpaRepositoryImpl implements InquiryQueryRepository {

private final JPAQueryFactory queryFactory;
private final EntityManager entityManager;
private final QInquiryJpaEntity inquiry = QInquiryJpaEntity.inquiryJpaEntity;

@Override
Expand All @@ -41,16 +44,32 @@ public Page<InquiryResponse> searchInquiries(
.offset(pageable.getOffset())
.limit(pageable.getPageSize());

long total = getTotalCount(inquiry, status);
long total = getTotalCount(query, inquiry.count());

// 결과 매핑
List<InquiryResponse> content = query.fetch().stream()
.map(this::mapToResponse)
.toList();

return new PageImpl<>(content, pageable, total);
}

@Override
public Page<InquiryResponse> searchMyInquiry(Long userId, Pageable pageable) {
JPAQuery<Tuple> query = baseQuery(inquiry)
.where(inquiry.userId.eq(userId));

long total = getTotalCount(query, inquiry.count());

List<InquiryResponse> content = query
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch().stream()
.map(this::mapToResponse)
.toList();

return new PageImpl<>(content, pageable, total);
}


private JPAQuery<Tuple> baseQuery(QInquiryJpaEntity inquiry) {
return queryFactory
Expand Down Expand Up @@ -81,14 +100,10 @@ private OrderSpecifier<?> buildOrderByCondition(String sortField, boolean asc) {
return asc ? expression.asc() : expression.desc();
}

private long getTotalCount(QInquiryJpaEntity inquiry, InquiryStatus status) {
private <T> long getTotalCount(JPAQuery<T> query, Expression<Long> countExpression) {
return Optional.ofNullable(
queryFactory
.select(inquiry.count())
.from(inquiry)
.where(
buildStatusCondition(inquiry, status)
)
query.clone(entityManager)
.select(countExpression)
.fetchOne()
).orElse(0L);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package life.mosu.mosuserver.presentation.admin;

import life.mosu.mosuserver.application.inquiry.InquiryAnswerService;
import life.mosu.mosuserver.application.inquiry.InquiryService;
import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.admin.docs.AdminInquiryControllerDocs;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/inquiry")
public class AdminInquiryController implements AdminInquiryControllerDocs {
private final InquiryService inquiryService;
private final InquiryAnswerService inquiryAnswerService;

@GetMapping("/list")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Page<InquiryResponse>>> getInquiryList(
@RequestParam(required = false) InquiryStatus status,
@RequestParam(required = false, defaultValue = "id") String sort,
@RequestParam(required = false, defaultValue = "true") boolean asc,
@PageableDefault(size = 10) Pageable pageable
) {
Page<InquiryResponse> inquiries = inquiryService.getInquiries(status, sort, asc,
pageable);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "질문 목록 조회 성공", inquiries));
}


@PostMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> inquiryAnswer(
@PathVariable Long postId,
@RequestBody InquiryAnswerRequest request
) {
inquiryAnswerService.createInquiryAnswer(postId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 등록 성공"));
}

@PutMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> updateInquiryAnswer(
@PathVariable Long postId,
@RequestBody InquiryAnswerUpdateRequest request
) {
inquiryAnswerService.updateInquiryAnswer(postId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 수정 성공"));
}

@DeleteMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> deleteInquiryAnswer(
@PathVariable Long postId) {
inquiryAnswerService.deleteInquiryAnswer(postId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 삭제 성공"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package life.mosu.mosuserver.presentation.admin.docs;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
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 life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "Admin Inquiry API", description = "1:1 문의 관련 API 명세")
public interface AdminInquiryControllerDocs {
@Operation(summary = "문의 목록 조회", description = "조건에 맞는 문의 목록을 페이징하여 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "질문 목록 조회 성공",
content = @Content(schema = @Schema(implementation = Page.class)))
})
ResponseEntity<ApiResponseWrapper<Page<InquiryResponse>>> getInquiryList(
@Parameter(name = "status", description = "문의 상태 (PENDING, COMPLETED)", in = ParameterIn.QUERY)
@RequestParam(required = false) InquiryStatus status,
@Parameter(name = "sort", description = "정렬 기준 필드", in = ParameterIn.QUERY)
@RequestParam(required = false, defaultValue = "id") String sort,
@Parameter(name = "asc", description = "오름차순 정렬 여부", in = ParameterIn.QUERY)
@RequestParam(required = false, defaultValue = "true") boolean asc,
@Parameter(hidden = true) Pageable pageable
);

@Operation(summary = "문의 답변 등록 (관리자용)", description = "특정 문의에 대한 답변을 등록합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "답변 등록 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> inquiryAnswer(
@Parameter(name = "postId", description = "답변을 등록할 문의의 ID", in = ParameterIn.PATH)
@PathVariable Long postId,
@Parameter(description = "답변 내용")
@RequestBody InquiryAnswerRequest request
);

@Operation(summary = "문의 답변 수정 (관리자용)", description = "특정 문의에 대한 답변을 수정합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "답변 수정 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> updateInquiryAnswer(
@Parameter(name = "postId", description = "답변을 수정할 문의의 ID", in = ParameterIn.PATH)
@PathVariable Long postId,
@Parameter(description = "수정할 답변 내용")
@RequestBody InquiryAnswerUpdateRequest request
);

@Operation(summary = "문의 답변 삭제 (관리자용)", description = "특정 문의에 대한 답변을 삭제합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "답변 삭제 성공")
})
ResponseEntity<ApiResponseWrapper<Void>> deleteInquiryAnswer(
@Parameter(name = "postId", description = "답변을 삭제할 문의의 ID", in = ParameterIn.PATH)
@PathVariable Long postId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@

import jakarta.validation.Valid;
import life.mosu.mosuserver.application.auth.PrincipalDetails;
import life.mosu.mosuserver.application.inquiry.InquiryAnswerService;
import life.mosu.mosuserver.application.inquiry.InquiryService;
import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus;
import life.mosu.mosuserver.global.annotation.UserId;
import life.mosu.mosuserver.global.util.ApiResponseWrapper;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryAnswerUpdateRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryCreateRequest;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryDetailResponse;
import life.mosu.mosuserver.presentation.inquiry.dto.InquiryResponse;
Expand All @@ -23,10 +20,8 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -35,7 +30,6 @@
public class InquiryController implements InquiryControllerDocs {

private final InquiryService inquiryService;
private final InquiryAnswerService inquiryAnswerService;

@PostMapping
@PreAuthorize("isAuthenticated() and hasRole('USER')")
Expand All @@ -46,26 +40,23 @@ public ResponseEntity<ApiResponseWrapper<Void>> create(
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.CREATED, "질문 등록 성공"));
}

@GetMapping("/list")
@GetMapping("/my")
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ApiResponseWrapper<Page<InquiryResponse>>> getInquiryList(
@RequestParam(required = false) InquiryStatus status,
@RequestParam(required = false, defaultValue = "id") String sort,
@RequestParam(required = false, defaultValue = "true") boolean asc,
public ResponseEntity<ApiResponseWrapper<Page<InquiryResponse>>> getMyInquiries(
@UserId Long userId,
@PageableDefault(size = 10) Pageable pageable
) {
Page<InquiryResponse> inquiries = inquiryService.getInquiries(status, sort, asc,
pageable);
return ResponseEntity.ok(
ApiResponseWrapper.success(HttpStatus.OK, "질문 목록 조회 성공", inquiries));
Page<InquiryResponse> inquiries = inquiryService.getMyInquiry(userId, pageable);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "내 질문 목록 조회 성공", inquiries));
}

@GetMapping("/{postId}")
@PreAuthorize("isAuthenticated() and hasRole('USER')")
public ResponseEntity<ApiResponseWrapper<InquiryDetailResponse>> getInquiryDetail(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@PathVariable Long postId) {

InquiryDetailResponse inquiry = inquiryService.getInquiryDetail(postId);
InquiryDetailResponse inquiry = inquiryService.getInquiryDetail(principalDetails.user(), postId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "질문 상세 조회 성공",
inquiry));
}
Expand All @@ -80,31 +71,4 @@ public ResponseEntity<ApiResponseWrapper<Void>> deleteInquiry(
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "질문 삭제 성공"));
}

@PostMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> inquiryAnswer(
@PathVariable Long postId,
@RequestBody InquiryAnswerRequest request
) {
inquiryAnswerService.createInquiryAnswer(postId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 등록 성공"));
}

@PutMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> updateInquiryAnswer(
@PathVariable Long postId,
@RequestBody InquiryAnswerUpdateRequest request
) {
inquiryAnswerService.updateInquiryAnswer(postId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 수정 성공"));
}

@DeleteMapping("/{postId}/answer")
@PreAuthorize("isAuthenticated() and hasRole('ADMIN')")
public ResponseEntity<ApiResponseWrapper<Void>> deleteInquiryAnswer(
@PathVariable Long postId) {
inquiryAnswerService.deleteInquiryAnswer(postId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "답변 삭제 성공"));
}
}
Loading