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,18 +1,19 @@
package com.whereyouad.WhereYouAd.domains.advertisement.application.mapper;

import com.whereyouad.WhereYouAd.domains.advertisement.application.dto.response.AdvertisementResponse;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.BudgetFieldType;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Grain;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdCampaign;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdContent;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdGroup;

import java.util.List;
import java.time.LocalDateTime;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Grain;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.BudgetHistory;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.MetricFact;
import com.whereyouad.WhereYouAd.domains.project.persistence.entity.Project;

import java.time.LocalDateTime;
import java.util.List;

public class AdvertisementConverter {

public static AdvertisementResponse.AdContentInfoResponse toAdContentInfo(AdContent adContent,
Expand Down Expand Up @@ -64,4 +65,37 @@ public static MetricFact createMetricFact(AdContent adContent, LocalDateTime tim
.revenue(null)
.build();
}

public static BudgetHistory toCampaignBudgetHistory(AdCampaign campaign, Long previousValue, Long newValue, Long changedBy, Provider provider) {
return BudgetHistory.builder()
.adCampaign(campaign)
.fieldType(BudgetFieldType.CAMPAIGN_BUDGET)
.previousValue(previousValue)
.newValue(newValue)
.changedBy(changedBy)
.provider(provider)
.build();
}

public static BudgetHistory toAdGroupBudgetHistory(AdGroup adGroup, Long previousValue, Long newValue, Long changedBy, Provider provider) {
return BudgetHistory.builder()
.adGroup(adGroup)
.fieldType(BudgetFieldType.AD_GROUP_BUDGET)
.previousValue(previousValue)
.newValue(newValue)
.changedBy(changedBy)
.provider(provider)
.build();
}

public static BudgetHistory toBidAmountHistory(AdGroup adGroup, Long previousValue, Long newValue, Long changedBy, Provider provider) {
return BudgetHistory.builder()
.adGroup(adGroup)
.fieldType(BudgetFieldType.BID_AMOUNT)
.previousValue(previousValue)
.newValue(newValue)
.changedBy(changedBy)
.provider(provider)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.constant;

public enum BudgetFieldType {
CAMPAIGN_BUDGET,
AD_GROUP_BUDGET,
BID_AMOUNT
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.whereyouad.WhereYouAd.domains.advertisement.domain.service;

import com.whereyouad.WhereYouAd.domains.advertisement.application.mapper.AdvertisementConverter;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdCampaign;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.AdGroup;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.AdCampaignRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.AdGroupRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.BudgetHistoryRepository;
import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;
import com.whereyouad.WhereYouAd.domains.organization.exception.handler.OrgHandler;
import com.whereyouad.WhereYouAd.domains.organization.exception.code.OrgErrorCode;
Expand All @@ -28,6 +31,7 @@
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
Expand All @@ -40,6 +44,7 @@ public class NaverAdApiService {
private final NaverClient naverClient;
private final AdGroupRepository adGroupRepository;
private final AdCampaignRepository adCampaignRepository;
private final BudgetHistoryRepository budgetHistoryRepository;

// 캠페인 목록 조회
@Transactional(readOnly = true)
Expand Down Expand Up @@ -191,16 +196,26 @@ public NaverDTO.CampaignResponse updateCampaignBudget(Long userId, Long connecti
throw new AdvertisementHandler(NaverAdErrorCode.NAVER_INVALID_BUDGET_RANGE);
}
}
Optional<AdCampaign> campaignOpt = adCampaignRepository
.findByPlatformAccountAndExternalCampaignId(connection.getPlatformAccount(), campaignId);
campaignOpt.ifPresent(campaign -> {
if (request.dailyBudget() != null && request.dailyBudget().equals(campaign.getBudget())) {
throw new AdvertisementHandler(NaverAdErrorCode.NAVER_SAME_BUDGET_VALUE);
}
});
try {
Map<String, String> headers = adApiAuthUtil.generateAuthHeaders(
connectionId, AdAuthRequest.forMethodAndPath("PUT", "/ncc/campaigns/" + campaignId));
NaverDTO.UpdateCampaignBudgetBody body =
new NaverDTO.UpdateCampaignBudgetBody(campaignId, request.useDailyBudget(), request.dailyBudget());
NaverDTO.CampaignResponse result = naverClient.updateCampaignBudget(headers, campaignId, "budget", body);

adCampaignRepository
.findByPlatformAccountAndExternalCampaignId(connection.getPlatformAccount(), campaignId)
.ifPresent(campaign -> campaign.updateBudget(request.dailyBudget()));
campaignOpt.ifPresent(campaign -> {
Long previousBudget = campaign.getBudget();
campaign.updateBudget(request.dailyBudget());
budgetHistoryRepository.save(AdvertisementConverter.toCampaignBudgetHistory(
campaign, previousBudget, request.dailyBudget(), userId, Provider.NAVER));
});

return result;
} catch (Exception e) {
Expand Down Expand Up @@ -229,6 +244,17 @@ public NaverDTO.AdGroupResponse updateAdGroupBudget(Long userId, Long connection
throw new AdvertisementHandler(NaverAdErrorCode.NAVER_INVALID_BID_AMOUNT_RANGE);
}
}
Optional<AdGroup> adGroupOpt = adGroupRepository
.findByAdCampaign_PlatformAccountAndExternalGroupId(connection.getPlatformAccount(), adgroupId);
adGroupOpt.ifPresent(adGroup -> {
boolean budgetUnchanged = request.dailyBudget() == null
|| request.dailyBudget().equals(adGroup.getBudget());
boolean bidAmtUnchanged = request.bidAmt() == null
|| request.bidAmt().equals(adGroup.getBidAmount());
if (budgetUnchanged && bidAmtUnchanged) {
throw new AdvertisementHandler(NaverAdErrorCode.NAVER_SAME_BUDGET_VALUE);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 여기서 입찰가나 예산 중 하나라도 이전과 같은 값으로 수정하게 되면 예외가 발생하는데, 사용자가 둘 중 하나만 값을 변경하는 경우를 위해 값이 아무것도 변경 되지 않은 경우에만 예외를 던지는 건 어떨까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 감사합니다! &&로 묶어서 수정했습니다!

try {
Map<String, String> headers = adApiAuthUtil.generateAuthHeaders(
connectionId, AdAuthRequest.forMethodAndPath("PUT", "/ncc/adgroups/" + adgroupId));
Expand All @@ -243,9 +269,19 @@ public NaverDTO.AdGroupResponse updateAdGroupBudget(Long userId, Long connection
new NaverDTO.UpdateAdGroupBudgetBody(adgroupId, null, null, request.bidAmt()));
}

adGroupRepository
.findByAdCampaign_PlatformAccountAndExternalGroupId(connection.getPlatformAccount(), adgroupId)
.ifPresent(adGroup -> adGroup.updateBudget(request.dailyBudget(), request.bidAmt()));
adGroupOpt.ifPresent(adGroup -> {
if (request.dailyBudget() != null) {
Long previousBudget = adGroup.getBudget();
budgetHistoryRepository.save(AdvertisementConverter.toAdGroupBudgetHistory(
adGroup, previousBudget, request.dailyBudget(), userId, Provider.NAVER));
}
if (request.bidAmt() != null) {
Long previousBidAmount = adGroup.getBidAmount();
budgetHistoryRepository.save(AdvertisementConverter.toBidAmountHistory(
adGroup, previousBidAmount, request.bidAmt(), userId, Provider.NAVER));
}
adGroup.updateBudget(request.dailyBudget(), request.bidAmt());
});

return result;
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum NaverAdErrorCode implements BaseErrorCode {
NAVER_INVALID_BUDGET_VALUE(HttpStatus.BAD_REQUEST, "NAVER_400_2", "예산 및 입찰가는 10원 단위로 입력해야 합니다."),
NAVER_INVALID_BUDGET_RANGE(HttpStatus.BAD_REQUEST, "NAVER_400_3", "예산은 50원 이상 1,000,000,000원 이하로 입력해야 합니다."),
NAVER_INVALID_BID_AMOUNT_RANGE(HttpStatus.BAD_REQUEST, "NAVER_400_4", "입찰가는 70원 이상 100,000원 이하로 입력해야 합니다."),
NAVER_SAME_BUDGET_VALUE(HttpStatus.BAD_REQUEST, "NAVER_400_5", "이전 예산과 동일한 값으로 수정할 수 없습니다."),

// 404
NAVER_CONNECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "NAVER_404_1", "플랫폼 연결 정보를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.BudgetFieldType;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

@Entity
@Table(name = "budget_history")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class BudgetHistory extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "budget_history_id")
private Long id;

@Enumerated(EnumType.STRING)
@Column(name = "field_type", nullable = false)
private BudgetFieldType fieldType;

@Column(name = "previous_value")
private Long previousValue;

@Column(name = "new_value")
private Long newValue;

@Column(name = "changed_by", nullable = false)
private Long changedBy;

@Enumerated(EnumType.STRING)
@Column(name = "provider", nullable = false)
private Provider provider;

// 연관 관계 (nullable로 2개 중에 1개만 연결 가능)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ad_campaign_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private AdCampaign adCampaign;
Comment thread
ojy0903 marked this conversation as resolved.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ad_group_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private AdGroup adGroup;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository;

import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.BudgetHistory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;

public interface BudgetHistoryRepository extends JpaRepository<BudgetHistory, Long> {

@Query("""
SELECT bh FROM BudgetHistory bh
LEFT JOIN bh.adCampaign ac
LEFT JOIN bh.adGroup ag
LEFT JOIN ag.adCampaign agc
WHERE (ac.organization.id = :orgId OR agc.organization.id = :orgId)
AND bh.createdAt BETWEEN :start AND :end
ORDER BY bh.createdAt DESC
""")
List<BudgetHistory> findByOrgAndPeriod(
@Param("orgId") Long orgId,
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.whereyouad.WhereYouAd.domains.dashboard.application.dto.response;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.BudgetFieldType;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.click.application.dto.response.ClickResponse;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

public class DashboardResponse {
Expand Down Expand Up @@ -97,4 +99,19 @@ public record PlatformMetricFactSummaryResponse(
DailyMetricFactResponse total, // 기간 전체 합계 지표
List<DailyMetricFactResponse> dailyMetrics // 일자별 지표 리스트
) {}

public record BudgetHistoryItem(
BudgetFieldType fieldType,
String targetName,
Long previousValue,
Long newValue,
LocalDateTime changedAt,
Provider provider
) {}

public record BudgetHistoryListResponse(
LocalDate startDate,
LocalDate endDate,
List<BudgetHistoryItem> histories
) {}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.whereyouad.WhereYouAd.domains.dashboard.application.mapper;

import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.BudgetHistory;
import com.whereyouad.WhereYouAd.domains.click.application.dto.response.ClickResponse;
import com.whereyouad.WhereYouAd.domains.dashboard.application.dto.response.DashboardResponse;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

import java.math.BigDecimal;

public class DashboardConverter {

public static DashboardResponse.BudgetSummaryResponse toBudgetSummary(
Expand Down Expand Up @@ -53,6 +53,22 @@ public static DashboardResponse.OngoingPlatformAdCountResponse toOngoingPlatform
return new DashboardResponse.OngoingPlatformAdCountResponse(startDate, endDate, totalCount, providerCount);
}

public static List<DashboardResponse.BudgetHistoryItem> toBudgetHistoryItems(List<BudgetHistory> histories) {
return histories.stream().map(bh -> {
String targetName = bh.getAdCampaign() != null
? bh.getAdCampaign().getName()
: bh.getAdGroup().getName();
return new DashboardResponse.BudgetHistoryItem(
bh.getFieldType(),
targetName,
bh.getPreviousValue(),
bh.getNewValue(),
bh.getCreatedAt(),
bh.getProvider()
);
}).toList();
}

//Data -> DTO
public static DashboardResponse.RealTimeGraphResponse toRealTimeGraphResponse(
List<ClickResponse.RealtimeClickCount> timeSeriesData, String mode, Boolean hasSuspect, DashboardResponse.SuspectDetail suspectDetail)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ DashboardResponse.OngoingPlatformAdCountResponse getOngoingAdCountByProvider(
// 플랫폼 대시보드의 일자별 지표(MetricFact) 조회
DashboardResponse.PlatformMetricFactSummaryResponse getPlatformMetricFacts(
Long userId, Long orgId, String providerType, Integer days);

// 기간별 예산 변경 이력 조회
DashboardResponse.BudgetHistoryListResponse getBudgetHistory(
Long userId, Long orgId, LocalDate startDate, LocalDate endDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Status;
import com.whereyouad.WhereYouAd.domains.advertisement.exception.AdvertisementHandler;
import com.whereyouad.WhereYouAd.domains.advertisement.exception.code.AdvertisementErrorCode;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.BudgetFieldType;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.BudgetHistory;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.AdCampaignRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.BudgetHistoryRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.MetricFactRepository;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.projection.MetricSumProjection;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.projection.RoasProjection;
Expand Down Expand Up @@ -41,6 +44,7 @@ public class DashboardServiceImpl implements DashboardService {

private final AdCampaignRepository adCampaignRepository;
private final MetricFactRepository metricFactRepository;
private final BudgetHistoryRepository budgetHistoryRepository;
private final OrgMemberRepository orgMemberRepository;
private final OrgRepository orgRepository;
private final BudgetCalculator budgetCalculator;
Expand Down Expand Up @@ -404,4 +408,25 @@ public DashboardResponse.PlatformMetricFactSummaryResponse getPlatformMetricFact
dailyMetrics
);
}

@Override
@Transactional(readOnly = true)
public DashboardResponse.BudgetHistoryListResponse getBudgetHistory(
Long userId, Long orgId, LocalDate startDate, LocalDate endDate) {

orgRepository.findById(orgId)
.orElseThrow(() -> new DashboardException(OrgErrorCode.ORG_NOT_FOUND));
orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
.orElseThrow(() -> new DashboardException(DashboardErrorCode.ACCESS_FORBIDDEN));

List<BudgetHistory> histories = budgetHistoryRepository.findByOrgAndPeriod(
orgId,
startDate.atStartOfDay(),
endDate.plusDays(1).atStartOfDay()
);

List<DashboardResponse.BudgetHistoryItem> items = DashboardConverter.toBudgetHistoryItems(histories);

return new DashboardResponse.BudgetHistoryListResponse(startDate, endDate, items);
}
}
Loading