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 @@ -60,6 +60,26 @@ MetricSumProjection findMetricsSumByOrgIdAndDateRange(
@Param("status") Status status
);

// orgId에 속한 모든 프로젝트의 지표중 해당 MetricFact 가 속한 AdCampaign 지표를 지정된 기간 범위 합산 (status 고려 x)
@Query("SELECT " +
"COALESCE(SUM(m.impressions), 0) AS totalImpressions, " +
"COALESCE(SUM(m.clicks), 0) AS totalClicks, " +
"COALESCE(SUM(m.conversions), 0) AS totalConversions, " +
"COALESCE(SUM(m.spend), 0) AS totalSpend, " +
"COALESCE(SUM(m.revenue), 0) AS totalRevenue " +
"FROM MetricFact m " +
"JOIN m.project p " +
"WHERE p.organization.id = :orgId " +
"AND p.organization.status = :orgStatus " +
"AND m.timeBucket >= :startDate AND m.timeBucket < :endDate "
)
MetricSumProjection findMetricsSumByOrgIdAndDateRange(
@Param("orgId") Long orgId,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
@Param("orgStatus") OrgStatus orgStatus
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

//orgId 와 provider 가 일치하고 해당 MetricFact 가 속한 AdCampaign 의 status 가 ON_GOING 인 지표에 대해 지정된 기간 범위 합산
@Query("SELECT " +
"COALESCE(SUM(m.impressions), 0) AS totalImpressions, " +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.whereyouad.WhereYouAd.domains.timeline.domain.service;

import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.projection.MetricSumProjection;
import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;
import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgStatus;
import com.whereyouad.WhereYouAd.domains.organization.exception.code.OrgErrorCode;
import com.whereyouad.WhereYouAd.domains.organization.exception.handler.OrgHandler;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember;
Expand All @@ -18,6 +20,7 @@
import com.whereyouad.WhereYouAd.domains.timeline.exception.code.TimelineErrorCode;
import com.whereyouad.WhereYouAd.domains.timeline.persistence.entity.Timeline;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Grain;
import com.whereyouad.WhereYouAd.domains.advertisement.domain.constant.Status;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.entity.MetricFact;
import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.MetricFactRepository;
import com.whereyouad.WhereYouAd.domains.timeline.persistence.repository.TimelineRepository;
Expand Down Expand Up @@ -66,21 +69,33 @@ public TimelineResponse.CreateResponseDTO createTimeline(Long userId, Long orgId
// 비교 기준 날짜(지난 주, 지난 달, 지난 년도와 비교)
ComparisonDateRange comparisonDates = calculateComparisonDates(dto.startDate(), dto.endDate(), dto.comparisonPeriodType());

// 비교 기간에 성과 데이터가 없으면 타임라인 생성 불가
boolean hasComparisonData = metricFactRepository.existsByTimeBucketBetweenAndOrg(
// 비교 기간 성과 합계 조회 (Projection 사용)
MetricSumProjection pastFacts = metricFactRepository.findMetricsSumByOrgIdAndDateRange(
orgId,
comparisonDates.start().atStartOfDay(),
comparisonDates.end().atTime(LocalTime.MAX),
orgId
comparisonDates.end().plusDays(1).atStartOfDay(),
OrgStatus.ACTIVE
);
if (!hasComparisonData) {

// 비교 기간에 성과 데이터가 없거나 모두 0이면 타임라인 생성 불가
if (isProjectionEmpty(pastFacts)) {
throw new TimelineException(TimelineErrorCode.TIMELINE_NO_COMPARISON_DATA);
}

// 현재 기간 성과 합계 조회 (Projection 사용)
MetricSumProjection currentFacts = metricFactRepository.findMetricsSumByOrgIdAndDateRange(
orgId,
dto.startDate().atStartOfDay(),
dto.endDate().plusDays(1).atStartOfDay(),
OrgStatus.ACTIVE,
Status.ON_GOING
);

// 입력받은 DTO를 타임라인 엔티티로 변환
Timeline timeline = TimelineConverter.toTimeline(dto, organization, userId, comparisonDates.start(), comparisonDates.end());

// PerformanceStatus 계산 및 판별 로직 호출 및 저장
PerformanceStatus status = timelineUtil.calculatePerformanceStatus(timeline);
// 성과 상태 - PerformanceStatus 계산 및 판별 로직 호출 및 저장 (초안)
PerformanceStatus status = timelineUtil.calculatePerformanceStatus(timeline, currentFacts, pastFacts);
timeline.updatePerformanceStatus(status);

// 엔티티 저장 및 반환
Expand Down Expand Up @@ -117,16 +132,28 @@ public TimelineResponse.CreateResponseDTO updateTimeline(Long userId, Long orgId
// 6. 비교 기준 날짜 재계산
ComparisonDateRange comparisonDates = calculateComparisonDates(dto.startDate(), dto.endDate(), dto.comparisonPeriodType());

// 7. 비교 기간 성과 데이터 존재 검증
boolean hasComparisonData = metricFactRepository.existsByTimeBucketBetweenAndOrg(
// 7. 비교 기간 성과 합계 조회 (Projection 사용)
MetricSumProjection pastFacts = metricFactRepository.findMetricsSumByOrgIdAndDateRange(
orgId,
comparisonDates.start().atStartOfDay(),
comparisonDates.end().atTime(LocalTime.MAX),
orgId
comparisonDates.end().plusDays(1).atStartOfDay(),
OrgStatus.ACTIVE
);
if (!hasComparisonData) {

// 비교 기간에 성과 데이터가 없거나 모두 0이면 예외 처리
if (isProjectionEmpty(pastFacts)) {
throw new TimelineException(TimelineErrorCode.TIMELINE_NO_COMPARISON_DATA);
}

// 현재 기간 성과 합계 조회
MetricSumProjection currentFacts = metricFactRepository.findMetricsSumByOrgIdAndDateRange(
orgId,
dto.startDate().atStartOfDay(),
dto.endDate().plusDays(1).atStartOfDay(),
OrgStatus.ACTIVE,
Status.ON_GOING
);

// 8. 성과 리스트 -> boolean 플래그 변환
boolean useClick = dto.metrics().contains(MetricType.CLICK);
boolean useConversion = dto.metrics().contains(MetricType.CONVERSION);
Expand All @@ -141,7 +168,7 @@ public TimelineResponse.CreateResponseDTO updateTimeline(Long userId, Long orgId
timeline.updateSummary(null);

// 10. PerformanceStatus 재계산
PerformanceStatus status = timelineUtil.calculatePerformanceStatus(timeline);
PerformanceStatus status = timelineUtil.calculatePerformanceStatus(timeline, currentFacts, pastFacts);
timeline.updatePerformanceStatus(status);

// 11. 변환 후 반환
Expand Down Expand Up @@ -351,4 +378,14 @@ private ComparisonDateRange calculateComparisonDates(LocalDate startDate, LocalD
case LAST_YEAR -> new ComparisonDateRange(startDate.minusYears(1), endDate.minusYears(1));
};
}

// Projection 결과가 비어있는지(또는 모든 수치가 0인지) 확인하는 헬퍼 메서드
private boolean isProjectionEmpty(MetricSumProjection projection) {
if (projection == null) return true;
return (projection.getTotalImpressions() == null || projection.getTotalImpressions() == 0L) &&
(projection.getTotalClicks() == null || projection.getTotalClicks() == 0L) &&
(projection.getTotalConversions() == null || projection.getTotalConversions() == 0L) &&
(projection.getTotalSpend() == null || projection.getTotalSpend().compareTo(BigDecimal.ZERO) == 0) &&
(projection.getTotalRevenue() == null || projection.getTotalRevenue().compareTo(BigDecimal.ZERO) == 0);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,90 @@
package com.whereyouad.WhereYouAd.domains.timeline.domain.util;

import com.whereyouad.WhereYouAd.domains.advertisement.persistence.repository.projection.MetricSumProjection;
import com.whereyouad.WhereYouAd.domains.timeline.domain.constant.PerformanceStatus;
import com.whereyouad.WhereYouAd.domains.timeline.persistence.entity.Timeline;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.math.RoundingMode;

@Component
public class TimelineUtil {
// TODO: PerformanceStatus 판별 로직 추가
public PerformanceStatus calculatePerformanceStatus(Timeline timeline) {
return null;

// 성과 상태 판별 로직 초안
public PerformanceStatus calculatePerformanceStatus(Timeline timeline, MetricSumProjection currentFacts, MetricSumProjection pastFacts) {

// 데이터가 아예 없는 경우 방어 로직 (Projection은 SUM이 없으면 0을 반환하도록 COALESCE 되어있음)
if (currentFacts == null || pastFacts == null) {
return PerformanceStatus.ON_TRACK;
}

double totalRate = 0.0;
int activeMetricCount = 0;

// 1. 클릭수 비교
if (timeline.isUseClick()) {
long currentClicks = currentFacts.getTotalClicks() != null ? currentFacts.getTotalClicks() : 0L;
long pastClicks = pastFacts.getTotalClicks() != null ? pastFacts.getTotalClicks() : 0L;

if (pastClicks > 0) {
totalRate += (double) currentClicks / pastClicks;
activeMetricCount++;
}
}

// 2. 전환수 비교
if (timeline.isUseConversion()) {
long currentConv = currentFacts.getTotalConversions() != null ? currentFacts.getTotalConversions() : 0L;
long pastConv = pastFacts.getTotalConversions() != null ? pastFacts.getTotalConversions() : 0L;

if (pastConv > 0) {
totalRate += (double) currentConv / pastConv;
activeMetricCount++;
}
}

// 3. 노출수 비교
if (timeline.isUseImpression()) {
long currentImp = currentFacts.getTotalImpressions() != null ? currentFacts.getTotalImpressions() : 0L;
long pastImp = pastFacts.getTotalImpressions() != null ? pastFacts.getTotalImpressions() : 0L;

if (pastImp > 0) {
totalRate += (double) currentImp / pastImp;
activeMetricCount++;
}
}

// 4. ROAS 비교
if (timeline.isUseRoas()) {
BigDecimal currentSpend = currentFacts.getTotalSpend() != null ? currentFacts.getTotalSpend() : BigDecimal.ZERO;
BigDecimal currentRev = currentFacts.getTotalRevenue() != null ? currentFacts.getTotalRevenue() : BigDecimal.ZERO;
BigDecimal currentRoas = currentSpend.compareTo(BigDecimal.ZERO) > 0 ? currentRev.divide(currentSpend, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;

BigDecimal pastSpend = pastFacts.getTotalSpend() != null ? pastFacts.getTotalSpend() : BigDecimal.ZERO;
BigDecimal pastRev = pastFacts.getTotalRevenue() != null ? pastFacts.getTotalRevenue() : BigDecimal.ZERO;
BigDecimal pastRoas = pastSpend.compareTo(BigDecimal.ZERO) > 0 ? pastRev.divide(pastSpend, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;

if (pastRoas.compareTo(BigDecimal.ZERO) > 0) {
totalRate += currentRoas.divide(pastRoas, 4, RoundingMode.HALF_UP).doubleValue();
activeMetricCount++;
}
}

// 비교할 유효 지표가 없는 경우
if (activeMetricCount == 0) {
return PerformanceStatus.ON_TRACK;
}

double avgRate = totalRate / activeMetricCount;

// 초안: 10% 이상 상승하면 ABOVE_AVG, 10% 이상 하락하면 UNDERPERFORM, 그 외 ON_TRACK
if (avgRate >= 1.1) {
return PerformanceStatus.ABOVE_AVG;
} else if (avgRate <= 0.9) {
return PerformanceStatus.UNDERPERFORM;
} else {
return PerformanceStatus.ON_TRACK;
}
}
}
Loading