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
@@ -1,6 +1,7 @@
package com.back.domain.product.product.entity;

import com.back.domain.product.category.entity.Category;
import com.back.domain.product.qna.entity.ProductQna;
import com.back.domain.user.entity.User;
import com.back.global.jpa.entity.BaseEntity;
import jakarta.persistence.*;
Expand Down Expand Up @@ -126,6 +127,10 @@ public class Product extends BaseEntity {
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)// 상품이 저장/삭제되면 매핑 중간테이블의 해당 데이터도 저장/삭제됨, 상품에서 특정 태그를 제거하면 DB에서 해당 매핑 데이터도 삭제됨.
private Set<ProductTagMapping> productTags= new HashSet<>(); // 상품과 태그(스타일)의 중간 테이블. 하나의 상품에 동일한 태그를 중복으로 붙이는 걸 허용하지 않으므로 List말고 Set 사용

@Builder.Default
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductQna> productQnas = new ArrayList<>(); // 상품 Q&A

// 할인된 가격 계산
public int getDiscountPrice() {
return price - (price * discountRate / 100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ private void checkProductOwner(Product product, User user) {
}
}
// 존재하지 않는 상품 검증
private Product getProductOrThrow(UUID productUuid) {
public Product getProductOrThrow(UUID productUuid) {
return productRepository.findByProductUuid(productUuid)
.orElseThrow(() -> new ServiceException("404", "존재하지 않는 상품입니다. UUID: " + productUuid));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.back.domain.product.qna.controller;

import com.back.domain.product.qna.dto.request.ProductQnaRequestDto;
import com.back.domain.product.qna.dto.response.ProductQnaListResponseDto;
import com.back.domain.product.qna.dto.response.ProductQnaResponseDto;
import com.back.domain.product.qna.service.ProductQnaService;
import com.back.global.rsData.RsData;
import com.back.global.security.auth.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/api/products/qna/{productUuid}")
@RequiredArgsConstructor
@Tag(name = "상품 Q&A", description = "상품 Q&A 관련 API")
public class ProductQnaController {

private final ProductQnaService productQnaService;

/** 상품 Q&A 등록 */
@PostMapping
@Operation(
summary = "상품 Q&A 등록",
responses = {
@ApiResponse(
responseCode = "200",
description = "상품 Q&A 등록 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(
example = """
{
"resultCode": "200",
"msg": "상품 Q&A가 성공적으로 등록되었습니다.",
"data": "550e8400-e29b-41d4-a716-446655440000"
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 (필수값 누락, 존재하지 않는 상품 등)",
content = @Content(
mediaType = "application/json",
schema = @Schema(
example = """
{
"resultCode": "400",
"msg": "Q&A 카테고리는 필수입니다.",
"data": null
}
"""
)
)
),
@ApiResponse(
responseCode = "403",
description = "권한 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(
example = """
{
"resultCode": "403",
"msg": "상품 Q&A 등록 권한이 없습니다.",
"data": null
}
"""
)
)
)
}
)
public ResponseEntity<RsData<UUID>> createProductQna(
@PathVariable UUID productUuid,
@Valid @RequestBody ProductQnaRequestDto request,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
UUID createdProductUuid = productQnaService.createProductQna(productUuid, request, customUserDetails);
return ResponseEntity.ok(RsData.of("200", "상품 Q&A가 성공적으로 등록되었습니다.", createdProductUuid));
}

/** 상품 Q&A 상세 조회 */
@GetMapping("/{productQnaId}")
@Operation(
summary = "상품 Q&A 상세 조회",
responses = {
@ApiResponse(
responseCode = "200",
description = "상품 Q&A 상세 조회 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ProductQnaResponseDto.class)
)
),
@ApiResponse(
responseCode = "404",
description = "해당 상품 Q&A를 찾을 수 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(
example = """
{
"resultCode": "404",
"msg": "해당 상품 Q&A를 찾을 수 없습니다.",
"data": null
}
"""
)
)
)
}
)
public ResponseEntity<RsData<ProductQnaResponseDto>> getProductQnaDetail(
@PathVariable Long productQnaId) {
ProductQnaResponseDto responseDto = productQnaService.getProductQnaDetail(productQnaId);
return ResponseEntity.ok(RsData.of("200", "상품 Q&A 상세 조회 성공", responseDto));
}

/** 상품 Q&A 목록 조회 (페이지네이션) */
@GetMapping("/list")
@Operation(
summary = "상품 Q&A 목록 조회 (페이지네이션)",
parameters = {
@Parameter(name = "qnaCategory", description = "Q&A 카테고리 (예: 배송, 상품, 교환/환불, 기타. '전체' 또는 미지정 시 전체 카테고리 조회)", example = "배송"),
@Parameter(name = "page", description = "페이지 번호", example = "1"),
@Parameter(name = "size", description = "페이지당 항목 수", example = "10")
},
responses = {
@ApiResponse(
responseCode = "200",
description = "상품 Q&A 목록 조회 성공",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ProductQnaListResponseDto.class)
)
)
}
)
public ResponseEntity<RsData<ProductQnaListResponseDto>> getProductQnaList(
@PathVariable UUID productUuid,
@RequestParam(value = "qnaCategory", required = false) String qnaCategory,
@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "size", defaultValue = "10") int size) {
ProductQnaListResponseDto responseDto = productQnaService.getProductQnaList(productUuid, qnaCategory, page, size);
return ResponseEntity.ok(RsData.of("200", "상품 Q&A 목록 조회 성공", responseDto));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.back.domain.product.qna.dto.request;

import com.back.global.s3.S3FileRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

import java.util.List;

/**
* 상품 Q&A 작성 요청을 받는 DTO
*/
public record ProductQnaRequestDto(
@Schema(description = "Q&A 카테고리", example = "배송")
@NotBlank(message = "Q&A 카테고리는 필수입니다.")
String qnaCategory,

@Schema(description = "Q&A 제목", example = "배송 문의합니다.")
@NotBlank(message = "Q&A 제목은 필수입니다.")
String qnaTitle,

@Schema(description = "Q&A 내용", example = "상품 배송이 언제쯤 시작될까요?")
@NotBlank(message = "Q&A 내용은 필수입니다.")
String qnaDescription,

@Schema(
description = "첨부 이미지 파일 목록 (null 허용)",
example = "["
+ "{\"url\":\"https://example.com/image1.jpg\",\"type\":\"ADDITIONAL\",\"s3Key\":\"s3-key-1\",\"originalFileName\":\"image1.jpg\"},"
+ "{\"url\":\"https://example.com/image2.jpg\",\"type\":\"ADDITIONAL\",\"s3Key\":\"s3-key-2\",\"originalFileName\":\"image2.jpg\"}"
+ "]"
)
@Valid
List<S3FileRequest> qnaImages
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.product.qna.dto.response;

import com.back.domain.product.qna.entity.ProductQna;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.domain.Page;

import java.util.List;

/**
* 상품 Q&A 목록 조회 응답 DTO
*/
public record ProductQnaListResponseDto(
@Schema(description = "현재 페이지 번호", example = "1")
int currentPage,
@Schema(description = "총 페이지 수", example = "5")
int totalPages,
@Schema(description = "한 페이지당 항목 수", example = "10")
int pageSize,
@Schema(description = "총 항목 수", example = "45")
long totalElements,
@Schema(description = "상품 Q&A 목록")
List<ProductQnaResponseDto> qnaList
) {
public static ProductQnaListResponseDto fromPage(Page<ProductQna> page, List<ProductQnaResponseDto> qnaList) {
return new ProductQnaListResponseDto(
page.getNumber() + 1,
page.getTotalPages(),
page.getSize(),
page.getTotalElements(),
qnaList
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.back.domain.product.qna.dto.response;

import com.back.global.s3.UploadResultResponse;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

/**
* 상품 Q&A 상세 조회 응답 DTO
*/
@Schema(name = "ProductQnaResponseDto", description = "상품 Q&A 조회 응답 DTO")
public record ProductQnaResponseDto(
@Schema(description = "상품 Q&A 글 번호", example = "1")
Long id,
@Schema(description = "Q&A 카테고리", example = "배송")
String qnaCategory,
@Schema(description = "Q&A 제목", example = "배송 문의합니다.")
String qnaTitle,
@Schema(description = "Q&A 내용", example = "상품 배송이 언제쯤 시작될까요?")
String qnaDescription,
@Schema(description = "작성자 이름", example = "홍길동")
String authorName,
@Schema(description = "작성일", example = "23.10.26")
String createDate,
@Schema(description = "첨부 이미지 파일 목록", example = "[" + "{\"url\":\"https://example.com/image1.jpg\",\"type\":\"ADDITIONAL\",\"s3Key\":\"product-images/uuid1.png\",\"originalFileName\":\"example.png\"}," + "{\"url\":\"https://example.com/image2.jpg\",\"fileType\":\"ADDITIONAL\",\"s3Key\":\"product-images/uuid2.png\",\"originalFileName\":\"example2.png\"}," + "]" )
List<UploadResultResponse> qnaImages
) {
}
45 changes: 45 additions & 0 deletions src/main/java/com/back/domain/product/qna/entity/ProductQna.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.back.domain.product.qna.entity;

import com.back.domain.product.product.entity.Product;
import com.back.domain.user.entity.User;
import com.back.global.jpa.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "product_qna")
public class ProductQna extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product; // 상품 FK

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user; // Q&A 작성자 FK

@Column(nullable = false)
private String qnaCategory; // 카테고리

@Column(nullable = false)
private String qnaTitle; // 제목

@Column(columnDefinition = "TEXT", nullable = false)
private String qnaDescription; // 내용

@Builder.Default
@OneToMany(mappedBy = "productQna", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductQnaImage> productQnaImages = new ArrayList<>();

public void addProductQnaImage(ProductQnaImage productQnaImage) {
productQnaImages.add(productQnaImage);
productQnaImage.setProductQna(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.product.qna.entity;

import com.back.global.jpa.entity.BaseEntity;
import com.back.global.s3.FileType;
import jakarta.persistence.*;
import lombok.*;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "product_qna_image")
public class ProductQnaImage extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_qna_id", nullable = false)
private ProductQna productQna;

@Column(nullable = false)
private String fileUrl;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private FileType fileType;

@Column(nullable = false)
private String s3Key;

@Column(nullable = false)
private String originalFileName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.back.domain.product.qna.repository;

import com.back.domain.product.qna.entity.ProductQnaImage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductQnaImageRepository extends JpaRepository<ProductQnaImage, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.product.qna.repository;

import com.back.domain.product.product.entity.Product;
import com.back.domain.product.qna.entity.ProductQna;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductQnaRepository extends JpaRepository<ProductQna, Long> {
Page<ProductQna> findByProduct(Product product, Pageable pageable);
Page<ProductQna> findByProductAndQnaCategory(Product product, String qnaCategory, Pageable pageable);
}

Loading