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
13 changes: 13 additions & 0 deletions src/main/java/com/DecodEat/domain/products/entity/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,17 @@ public class Product extends BaseEntity {
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@Builder.Default // 원재료 리스트
private List<ProductRawMaterial> ingredients = new ArrayList<>();

@OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private ProductNutrition productNutrition;

@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductInfoImage> infoImages = new ArrayList<>();

/**
* 상품의 대표 이미지를 새로운 URL로 업데이트
*/
public void updateProductImage(String newImageUrl) {
this.productImage = newImageUrl;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.DecodEat.domain.products.entity;

import com.DecodEat.domain.report.entity.NutritionReport;
import com.DecodEat.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
Expand Down Expand Up @@ -53,4 +54,22 @@ public class ProductNutrition extends BaseEntity {

@Column(name = "trans_fat")
private Double transFat;

/**
* 영양 정보 신고 내역을 바탕으로 업데이트
* @param report 업데이트할 정보가 담긴 NutritionReport 객체
*/
public void updateFromReport(NutritionReport report) {
this.calcium = report.getCalcium();
this.carbohydrate = report.getCarbohydrate();
this.cholesterol = report.getCholesterol();
this.dietaryFiber = report.getDietaryFiber();
this.energy = report.getEnergy();
this.fat = report.getFat();
this.protein = report.getProtein();
this.satFat = report.getSatFat();
this.sodium = report.getSodium();
this.sugar = report.getSugar();
this.transFat = report.getTransFat();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.DecodEat.domain.report.controller;

import com.DecodEat.domain.report.dto.request.ImageUpdateRequestDto;
import com.DecodEat.domain.report.dto.response.ReportResponseDto;
import com.DecodEat.domain.report.service.ReportService;
import com.DecodEat.global.apiPayload.ApiResponse;
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.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
Expand Down Expand Up @@ -43,4 +45,29 @@ public ApiResponse<ReportResponseDto.ReportListResponseDTO> getReports(
public ApiResponse<ReportResponseDto> rejectReport(@PathVariable Long reportId) {
return ApiResponse.onSuccess(reportService.rejectReport(reportId));
}

@Operation(
summary = "상품 수정 요청 수락 (관리자)",
description = """
관리자가 상품 정보 수정 요청을 수락합니다. 해당 신고 내역의 상태를 ACCEPTED로 변경합니다.
- **영양 정보 신고 수락 시:** 실제 상품의 영양 정보가 신고된 내용으로 업데이트됩니다.
- **부적절 이미지 신고 수락 시:** 관리자가 새로운 이미지를 넣으면 해당 이미지로 변경되고, 넣지 않으면 이미지가 삭제됩니다.""")
@Parameters({
@Parameter(name = "reportId", description = "수락할 신고의 ID", example = "1", required = true)
})
// @PreAuthorize("hasRole('ADMIN')") // Spring Security 사용 시 권한 설정
@PatchMapping("/{reportId}/accept")
public ApiResponse<ReportResponseDto> acceptReport(
@PathVariable Long reportId,
@Parameter(name = "imageUpdateRequestDto", description = "이미지 신고 시에만 사용. 새 이미지 URL을 제공하여 교체하거나, 본문을 비워 기존 이미지를 삭제합니다.")
@RequestBody(required = false) ImageUpdateRequestDto requestDto) {
return ApiResponse.onSuccess(reportService.acceptReport(reportId, requestDto));
}

@Operation(summary = "신고 상세 조회 API", description = "관리자가 특정 신고 내역의 상세 정보를 조회합니다.")
@GetMapping("/{reportId}")
public ApiResponse<ReportResponseDto.ReportListItemDTO> getReportDetetails(
@Parameter(description = "조회할 신고의 ID", example = "1") @PathVariable Long reportId) {
return ApiResponse.onSuccess(reportService.getReportDetails(reportId));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.DecodEat.domain.report.converter;

import com.DecodEat.domain.products.entity.Product;
import com.DecodEat.domain.products.entity.ProductInfoImage;
import com.DecodEat.domain.products.entity.ProductNutrition;
import com.DecodEat.domain.report.dto.request.ProductNutritionUpdateRequestDto;
import com.DecodEat.domain.report.dto.response.ReportResponseDto;
import com.DecodEat.domain.report.entity.ImageReport;
Expand All @@ -10,6 +12,7 @@
import com.DecodEat.domain.users.entity.User;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.stream.Collectors;

public class ReportConverter {
Expand Down Expand Up @@ -65,12 +68,46 @@ public static ReportResponseDto.ReportedNutritionInfo toReportedNutritionInfo(Nu
.build();
}

public static ReportResponseDto.SimpleProductInfoDTO toSimpleProductInfoDTO(Product product) {
List<String> infoImageUrls = product.getInfoImages().stream()
.map(ProductInfoImage::getImageUrl)
.collect(Collectors.toList());

return ReportResponseDto.SimpleProductInfoDTO.builder()
.productId(product.getProductId())
.productName(product.getProductName())
.manufacturer(product.getManufacturer())
.productImage(product.getProductImage())
.infoImageUrls(infoImageUrls)
.build();
}
public static ReportResponseDto.ProductNutritionInfoDTO toProductNutritionInfoDTO(ProductNutrition nutrition) {

return ReportResponseDto.ProductNutritionInfoDTO.builder()
.calcium(nutrition.getCalcium())
.carbohydrate(nutrition.getCarbohydrate())
.cholesterol(nutrition.getCholesterol())
.dietaryFiber(nutrition.getDietaryFiber())
.energy(nutrition.getEnergy())
.fat(nutrition.getFat())
.protein(nutrition.getProtein())
.satFat(nutrition.getSatFat())
.sodium(nutrition.getSodium())
.sugar(nutrition.getSugar())
.transFat(nutrition.getTransFat())
.build();
}

public static ReportResponseDto.ReportListItemDTO toReportListItemDTO(ReportRecord reportRecord){

Product product = reportRecord.getProduct();
ProductNutrition currentNutrition = product.getProductNutrition();

ReportResponseDto.ReportListItemDTO.ReportListItemDTOBuilder builder = ReportResponseDto.ReportListItemDTO.builder()
.reportId(reportRecord.getId())
.reporterId(reportRecord.getReporterId())
.productId(reportRecord.getProduct().getProductId())
.productName(reportRecord.getProduct().getProductName())
.productInfo(toSimpleProductInfoDTO(product))
.currentNutritionInfo(toProductNutritionInfoDTO(currentNutrition))
.reportStatus(reportRecord.getReportStatus())
.createdAt(reportRecord.getCreatedAt());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.DecodEat.domain.report.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@Schema(description = "이미지 신고 수락 시, 새 이미지로 교체할 경우 사용하는 DTO")
public class ImageUpdateRequestDto {

@Schema(description = "새로 등록할 상품의 대표 이미지 URL",
example = "http://example.image.jpg",
nullable = true)
private String newImageUrl;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.DecodEat.domain.report.dto.response;

import com.DecodEat.domain.report.dto.request.ProductNutritionUpdateRequestDto;
import com.DecodEat.domain.report.entity.ReportStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -44,6 +43,38 @@ public static class ReportedNutritionInfo {
private Double transFat;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "신고된 상품의 간략 정보")
public static class SimpleProductInfoDTO {
private Long productId;
private String productName;
private String manufacturer;
private String productImage;
private List<String> infoImageUrls;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "상품의 현재 영양 정보")
public static class ProductNutritionInfoDTO {
private Double calcium;
private Double carbohydrate;
private Double cholesterol;
private Double dietaryFiber;
private Double energy;
private Double fat;
private Double protein;
private Double satFat;
private Double sodium;
private Double sugar;
private Double transFat;
}


@Getter
@Builder
Expand All @@ -58,11 +89,8 @@ public static class ReportListItemDTO {
@Schema(description = "신고자 ID", example = "2")
private Long reporterId;

@Schema(description = "상품 ID", example = "13")
private Long productId;

@Schema(description = "상품명", example = "맛있는 사과")
private String productName;
@Schema(description = "신고된 상품 정보")
private SimpleProductInfoDTO productInfo;

@Schema(description = "신고 유형", example = "NUTRITION_UPDATE")
private String reportType;
Expand All @@ -79,6 +107,9 @@ public static class ReportListItemDTO {
@Schema(description = "신고된 영양정보 수정 요청 내용 (영양정보 신고인 경우)", nullable = true)
private ReportedNutritionInfo nutritionRequestInfo;

@Schema(description = "상품의 현재 DB에 저장된 영양 정보", nullable = true)
private ProductNutritionInfoDTO currentNutritionInfo;

}

@Getter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
package com.DecodEat.domain.report.repository;

import com.DecodEat.domain.report.entity.ReportRecord;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface ReportRecordRepository extends JpaRepository<ReportRecord, Long> {

// 영양정보 전체 조회
@Query(value = "SELECT r FROM ReportRecord r " +
"JOIN FETCH r.product p " +
"LEFT JOIN FETCH p.productNutrition",
countQuery = "SELECT count(r) FROM ReportRecord r")
Page<ReportRecord> findAllWithDetails(Pageable pageable);

// 단일 조회
@Query("SELECT r FROM ReportRecord r " +
"JOIN FETCH r.product p " +
"LEFT JOIN FETCH p.productNutrition " +
"WHERE r.id = :id")
Optional<ReportRecord> findByIdWithDetails(Long id);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.DecodEat.domain.report.service;

import com.DecodEat.domain.products.entity.Product;
import com.DecodEat.domain.products.entity.ProductNutrition;
import com.DecodEat.domain.products.repository.ProductRepository;
import com.DecodEat.domain.report.converter.ReportConverter;
import com.DecodEat.domain.report.dto.request.ImageUpdateRequestDto;
import com.DecodEat.domain.report.dto.request.ProductNutritionUpdateRequestDto;
import com.DecodEat.domain.report.dto.response.ReportResponseDto;
import com.DecodEat.domain.report.entity.ReportRecord;
import com.DecodEat.domain.report.entity.ReportStatus;
import com.DecodEat.domain.report.entity.*;
import com.DecodEat.domain.report.entity.ReportRecord;
import com.DecodEat.domain.report.repository.ImageReportRepository;
import com.DecodEat.domain.report.repository.NutritionReportRepository;
import com.DecodEat.domain.report.repository.ReportRecordRepository;
import com.DecodEat.domain.users.entity.User;
import com.DecodEat.global.apiPayload.code.status.ErrorStatus;
import com.DecodEat.global.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -50,13 +52,29 @@ public ReportResponseDto requestCheckImage(User user, Long productId, String ima
return ReportConverter.toReportResponseDto(productId,"상품 사진 확인 요청 완료");
}

// 신고 내역 전체 조회
@Transactional(readOnly = true)
public ReportResponseDto.ReportListResponseDTO getReports(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<ReportRecord> reportPage = reportRecordRepository.findAll(pageable);
Page<ReportRecord> reportPage = reportRecordRepository.findAllWithDetails(pageable);
return ReportConverter.toReportListResponseDTO(reportPage);
}

/**
* 단일 신고 내역 상세 조회
* @param reportId 조회할 신고의 ID
* @return 신고 상세 정보를 담은 DTO
*/
@Transactional(readOnly = true)
public ReportResponseDto.ReportListItemDTO getReportDetails(Long reportId) {

// 1. ID로 신고 내역 조회
ReportRecord reportRecord = reportRecordRepository.findByIdWithDetails(reportId)
.orElseThrow(() -> new GeneralException(ErrorStatus.REPORT_NOT_FOUND));

return ReportConverter.toReportListItemDTO(reportRecord);
}

/**
* 상품 수정 신고 요청 거절
* @param reportId 거절할 신고의 ID
Expand All @@ -67,10 +85,52 @@ public ReportResponseDto rejectReport(Long reportId){
ReportRecord reportRecord = reportRecordRepository.findById(reportId)
.orElseThrow(() -> new GeneralException(REPORT_NOT_FOUND));

// 2. reportstatus 상태를 rejected로 변경
// 2. 이미 처리된 내역인지 확인
if(reportRecord.getReportStatus() != ReportStatus.IN_PROGRESS) {
throw new GeneralException(ALREADY_PROCESSED_REPORT);
}

// 3. reportstatus 상태를 rejected로 변경
reportRecord.setReportStatus(ReportStatus.REJECTED);

// 3. DTO 반환
return ReportConverter.toReportResponseDto(reportRecord.getProduct().getProductId(), "신고 요청이 거절 처리되었습니다.");
}


/**
* 상품 수정 신고 요청 수락
* @param reportId 수락할 신고의 ID
* @return 처리 결과를 담은 DTO
*/
public ReportResponseDto acceptReport(Long reportId, ImageUpdateRequestDto requestDto){
// 1. ID로 신고 내역 조회
ReportRecord reportRecord = reportRecordRepository.findById(reportId)
.orElseThrow(() -> new GeneralException(REPORT_NOT_FOUND));

// 2. 이미 처리된 내역인지 확인
if(reportRecord.getReportStatus() != ReportStatus.IN_PROGRESS) {
throw new GeneralException(ALREADY_PROCESSED_REPORT);
}

Product product = reportRecord.getProduct();

// 3. 신고 유횽에 따른 로직 분기
if (reportRecord instanceof NutritionReport) {
ProductNutrition productNutrition = product.getProductNutrition();
productNutrition.updateFromReport((NutritionReport) reportRecord);

} else if (reportRecord instanceof ImageReport) {
// 새로운 이미지가 없는 경우 이미지 삭제 -> null로 처리
// 새로운 이미지가 있는 경우 해당 이미지로 변경
String newImageUrl = (requestDto != null) ? requestDto.getNewImageUrl() : null;
product.updateProductImage(newImageUrl);
}

// 4. reportstatus 상태를 accepted 변경
reportRecord.setReportStatus(ReportStatus.ACCEPTED);

// 4. DTO 반환
return ReportConverter.toReportResponseDto(reportRecord.getProduct().getProductId(), "신고 요청이 수락 처리되었습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public enum ErrorStatus implements BaseErrorCode {
FILE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE_001", "파일 업로드에 실패했습니다."),

// 신고
REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "REPONT_401", "존재하지 않는 신고 내역 입니다.");
REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "REPORT_401", "존재하지 않는 신고 내역 입니다."),
ALREADY_PROCESSED_REPORT(HttpStatus.CONFLICT, "REPORT_402", "이미 처리된 신고 입니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Loading