diff --git a/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalQueryService.java b/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalQueryService.java new file mode 100644 index 00000000..c7391491 --- /dev/null +++ b/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalQueryService.java @@ -0,0 +1,32 @@ +package com.example.RealMatch.business.application.service; + +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.RealMatch.business.domain.entity.CampaignProposal; +import com.example.RealMatch.business.domain.repository.CampaignProposalRepository; +import com.example.RealMatch.business.exception.BusinessErrorCode; +import com.example.RealMatch.business.presentation.dto.response.CampaignProposalDetailResponse; +import com.example.RealMatch.global.exception.CustomException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CampaignProposalQueryService { + + private final CampaignProposalRepository campaignProposalRepository; + + public CampaignProposalDetailResponse getProposalDetail( + Long userId, + UUID proposalId + ) { + CampaignProposal proposal = campaignProposalRepository.findByIdWithTags(proposalId) + .orElseThrow(() -> new CustomException(BusinessErrorCode.CAMPAIGN_PROPOSAL_NOT_FOUND)); + + return CampaignProposalDetailResponse.from(proposal); + } +} diff --git a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java index d8f257cc..c3693b70 100644 --- a/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java +++ b/src/main/java/com/example/RealMatch/business/domain/repository/CampaignProposalRepository.java @@ -1,6 +1,7 @@ package com.example.RealMatch.business.domain.repository; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -49,4 +50,13 @@ List findReceivedProposalIds( @Param("status") ProposalStatus status ); + @Query(""" + select distinct cp + from CampaignProposal cp + left join fetch cp.tags t + left join fetch t.tagContent + where cp.id = :proposalId + """) + Optional findByIdWithTags(UUID proposalId); + } diff --git a/src/main/java/com/example/RealMatch/business/exception/BusinessErrorCode.java b/src/main/java/com/example/RealMatch/business/exception/BusinessErrorCode.java index 69f0e1e9..1d2c2d7c 100644 --- a/src/main/java/com/example/RealMatch/business/exception/BusinessErrorCode.java +++ b/src/main/java/com/example/RealMatch/business/exception/BusinessErrorCode.java @@ -13,7 +13,9 @@ public enum BusinessErrorCode implements BaseErrorCode { // ===== 조회 ===== - CAMPAIGN_APPLY_ALREADY_APPLIED(HttpStatus.BAD_REQUEST, "CAMPAIGN_400_1", "이미 지원한 캠페인입니다."); + CAMPAIGN_APPLY_ALREADY_APPLIED(HttpStatus.BAD_REQUEST, "CAMPAIGN_400_1", "이미 지원한 캠페인입니다."), + + CAMPAIGN_PROPOSAL_NOT_FOUND(HttpStatus.NOT_FOUND, "CAMPAIGN_404_1", "캠페인 지원 내역이 없습니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/RealMatch/business/presentation/controller/CampaignProposalController.java b/src/main/java/com/example/RealMatch/business/presentation/controller/CampaignProposalController.java index 2c2330a1..5d88c76f 100644 --- a/src/main/java/com/example/RealMatch/business/presentation/controller/CampaignProposalController.java +++ b/src/main/java/com/example/RealMatch/business/presentation/controller/CampaignProposalController.java @@ -1,14 +1,20 @@ package com.example.RealMatch.business.presentation.controller; +import java.util.UUID; + import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.example.RealMatch.business.application.service.CampaignProposalQueryService; import com.example.RealMatch.business.application.service.CampaignProposalService; import com.example.RealMatch.business.presentation.docs.CampaignProposalSwagger; import com.example.RealMatch.business.presentation.dto.request.CampaignProposalRequestDto; +import com.example.RealMatch.business.presentation.dto.response.CampaignProposalDetailResponse; import com.example.RealMatch.global.config.jwt.CustomUserDetails; import com.example.RealMatch.global.presentation.CustomResponse; import com.example.RealMatch.user.domain.entity.enums.Role; @@ -20,10 +26,11 @@ @Tag(name = "Business", description = "비즈니스 API") @RestController -@RequestMapping("/api/v1/campaigns") +@RequestMapping("/api/v1/campaigns/proposal") @RequiredArgsConstructor public class CampaignProposalController implements CampaignProposalSwagger { private final CampaignProposalService campaignProposalService; + private final CampaignProposalQueryService campaignProposalQueryService; @Override @PostMapping("/request") @@ -49,5 +56,30 @@ public CustomResponse requestCampaignProposal( campaignProposalService.requestCampaign(1L, Role.from(userDetails.getRole()), request); return CustomResponse.ok("캠페인 제안에 성공했습니다."); } + + @GetMapping("/{campaignProposalId}") + @Operation( + summary = "캠페인 제안 상세 조회 API", + description = """ + 캠페인 제안 단건의 상세 정보를 조회합니다. + + 제안에 포함된 콘텐츠 태그 정보와 + 제안 상태(REVIEWING / MATCHED / REJECTED)를 함께 반환합니다. + """ + ) + public CustomResponse getProposalDetail( + @AuthenticationPrincipal CustomUserDetails principal, + @PathVariable UUID campaignProposalId + ) { + CampaignProposalDetailResponse response = + campaignProposalQueryService.getProposalDetail( + principal.getUserId(), + campaignProposalId + ); + + return CustomResponse.ok(response); + } + + } diff --git a/src/main/java/com/example/RealMatch/business/presentation/dto/response/CampaignProposalDetailResponse.java b/src/main/java/com/example/RealMatch/business/presentation/dto/response/CampaignProposalDetailResponse.java index 0b2a56eb..ec06113b 100644 --- a/src/main/java/com/example/RealMatch/business/presentation/dto/response/CampaignProposalDetailResponse.java +++ b/src/main/java/com/example/RealMatch/business/presentation/dto/response/CampaignProposalDetailResponse.java @@ -9,7 +9,7 @@ import java.util.UUID; import com.example.RealMatch.business.domain.entity.CampaignProposal; -import com.example.RealMatch.campaign.domain.entity.CampaignContentTag; +import com.example.RealMatch.business.domain.entity.CampaignProposalContentTag; import com.example.RealMatch.tag.domain.enums.ContentTagType; import com.example.RealMatch.tag.presentation.dto.response.ContentTagResponse; @@ -41,10 +41,7 @@ public class CampaignProposalDetailResponse { private ContentTagResponse contentTags; - public static CampaignProposalDetailResponse from( - CampaignProposal proposal, - List tags - ) { + public static CampaignProposalDetailResponse from(CampaignProposal proposal) { return CampaignProposalDetailResponse.builder() .proposalId(proposal.getId()) .brandId(proposal.getBrand().getId()) @@ -58,17 +55,17 @@ public static CampaignProposalDetailResponse from( .status(proposal.getStatus().name()) .refusalReason(proposal.getRefusalReason()) .createdAt(proposal.getCreatedAt()) - .contentTags(toContentTagResponse(tags)) + .contentTags(toContentTagResponse(proposal.getTags())) .build(); } private static ContentTagResponse toContentTagResponse( - List tags + List tags ) { Map> map = new EnumMap<>(ContentTagType.class); - for (CampaignContentTag tag : tags) { + for (CampaignProposalContentTag tag : tags) { ContentTagType type = tag.getTagContent().getTagType(); map.computeIfAbsent(type, k -> new ArrayList<>()) @@ -85,19 +82,16 @@ private static ContentTagResponse toContentTagResponse( } private static ContentTagResponse.TagItemResponse toTagItem( - CampaignContentTag tag + CampaignProposalContentTag tag ) { + String baseName = tag.getTagContent().getKorName(); + String name = tag.getCustomTagValue() != null && !tag.getCustomTagValue().isBlank() + ? baseName + " (" + tag.getCustomTagValue() + ")" + : baseName; + return new ContentTagResponse.TagItemResponse( tag.getTagContent().getId(), - resolveTagName(tag) + name ); } - - private static String resolveTagName(CampaignContentTag tag) { - String baseName = tag.getTagContent().getKorName(); - if (tag.getCustomTagValue() != null && !tag.getCustomTagValue().isBlank()) { - return baseName + " (" + tag.getCustomTagValue() + ")"; - } - return baseName; - } }