Skip to content

Commit bf36d36

Browse files
authored
Merge pull request #83 from YAPP-Github/feat/T3-210
Feat/t3 210
2 parents ac70273 + 1b912cc commit bf36d36

15 files changed

Lines changed: 593 additions & 4 deletions

File tree

src/main/java/bitnagil/bitnagil_backend/BitnagilBackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.cloud.openfeign.EnableFeignClients;
66
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7+
import org.springframework.scheduling.annotation.EnableScheduling;
78

89
@SpringBootApplication
910
@EnableFeignClients
1011
@EnableJpaAuditing
12+
@EnableScheduling
1113
public class BitnagilBackendApplication {
1214

1315
public static void main(String[] args) {
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
package bitnagil.bitnagil_backend.emotionMarble.repository;
22

33
import bitnagil.bitnagil_backend.emotionMarble.domain.EmotionMarble;
4-
import bitnagil.bitnagil_backend.global.entity.HistoryPk;
4+
import bitnagil.bitnagil_backend.emotionMarble.domain.enums.EmotionMarbleType;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
68
import org.springframework.stereotype.Repository;
79

810
import java.time.LocalDate;
9-
import java.util.UUID;
11+
import java.util.List;
1012

1113
@Repository
12-
public interface EmotionMarbleRepository extends JpaRepository<EmotionMarble, HistoryPk> {
14+
public interface EmotionMarbleRepository extends JpaRepository<EmotionMarble, Long> {
1315
EmotionMarble findByUserId(Long userId);
1416

1517
EmotionMarble findByUserIdAndDateIs(Long userId, LocalDate now);
1618

1719
boolean existsByUserIdAndDate(Long userId, LocalDate nowDate);
20+
21+
@Query("SELECT COUNT(e) FROM EmotionMarble e WHERE e.date BETWEEN :start AND :end")
22+
long countByDateBetween(@Param("start") LocalDate start, @Param("end") LocalDate end);
23+
24+
@Query("SELECT COUNT(e) FROM EmotionMarble e WHERE e.date BETWEEN :start AND :end AND e.emotionMarbleType IN :types")
25+
long countByDateBetweenAndEmotionMarbleTypeIn(
26+
@Param("start") LocalDate start,
27+
@Param("end") LocalDate end,
28+
@Param("types") List<EmotionMarbleType> types);
1829
}

src/main/java/bitnagil/bitnagil_backend/global/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public class SecurityConfig {
3737
"/swagger-ui/**",
3838
"/v3/api-docs/**",
3939
"/api/v1/health-check",
40-
"/api/v1/version/**"
40+
"/api/v1/version/**",
41+
"/api/v1/kpi/**"
4142
};
4243

4344
private final JwtAuthenticationFilter jwtAuthenticationFilter;

src/main/java/bitnagil/bitnagil_backend/global/swagger/ApiTags.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ public class ApiTags {
1515
public static final String APP_VERSION = "앱 버전 API";
1616
public static final String REPORT = "제보 API";
1717
public static final String FILE = "파일 API";
18+
public static final String KPI = "KPI API";
1819
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package bitnagil.bitnagil_backend.kpi.controller;
2+
3+
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
4+
import bitnagil.bitnagil_backend.kpi.controller.spec.KpiSpec;
5+
import bitnagil.bitnagil_backend.kpi.service.KpiService;
6+
import java.time.YearMonth;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
@RestController
12+
@RequiredArgsConstructor
13+
@RequestMapping("/api/v1/kpi")
14+
public class KpiController implements KpiSpec {
15+
16+
private final KpiService kpiService;
17+
18+
@Override
19+
public CustomResponseDto<String> calculateAndSave(int year, int month) {
20+
YearMonth targetMonth = YearMonth.of(year, month);
21+
kpiService.calculateAndSave(targetMonth);
22+
return CustomResponseDto.from(targetMonth + " KPI 저장 완료");
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package bitnagil.bitnagil_backend.kpi.controller.spec;
2+
3+
import bitnagil.bitnagil_backend.global.response.CustomResponseDto;
4+
import bitnagil.bitnagil_backend.global.swagger.ApiTags;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.Parameter;
7+
import io.swagger.v3.oas.annotations.Parameters;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestParam;
11+
12+
@Tag(name = ApiTags.KPI)
13+
public interface KpiSpec {
14+
15+
@Operation(summary = "월별 KPI 계산 및 저장",
16+
description = "지정한 연·월의 KPI를 계산하여 DB에 저장합니다. 이미 해당 월 데이터가 있으면 저장하지 않습니다. (로컬/스웨거 테스트용)")
17+
@Parameters({
18+
@Parameter(name = "year", description = "연도", required = true, example = "2025"),
19+
@Parameter(name = "month", description = "월 (1~12)", required = true, example = "2")
20+
})
21+
@PostMapping("/calculate")
22+
CustomResponseDto<String> calculateAndSave(
23+
@RequestParam int year,
24+
@RequestParam int month
25+
);
26+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package bitnagil.bitnagil_backend.kpi.domain;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.GeneratedValue;
6+
import jakarta.persistence.GenerationType;
7+
import jakarta.persistence.Id;
8+
import jakarta.validation.constraints.NotNull;
9+
import java.math.BigDecimal;
10+
import java.time.LocalDate;
11+
import lombok.AccessLevel;
12+
import lombok.Builder;
13+
import lombok.Getter;
14+
import lombok.NoArgsConstructor;
15+
16+
/**
17+
* 월별 성과 KPI 집계 결과를 저장하는 엔티티입니다.
18+
* 매월 1일 스케줄러가 전월 데이터를 집계하여 한 건 저장합니다.
19+
*/
20+
@Getter
21+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
22+
@Entity
23+
public class MonthlyKpi {
24+
25+
@Id
26+
@GeneratedValue(strategy = GenerationType.IDENTITY)
27+
private Long id;
28+
29+
/**
30+
* 집계 대상 월 (해당 월 1일)
31+
*/
32+
@NotNull
33+
@Column(name = "target_month", unique = true, nullable = false)
34+
private LocalDate targetMonth;
35+
36+
/**
37+
* 지표 1: 가입 후 7일 이내 루틴 1회 이상 완료 비율 (%)
38+
* (7일 내 루틴 1회 이상 완료 유저 수 / 신규 가입자 수) × 100
39+
*/
40+
@Column(precision = 5, scale = 2)
41+
private BigDecimal routineCompletionWithinSevenDaysRate;
42+
43+
/**
44+
* 지표 2: 월간 1회 이상 루틴 완료 사용자 비율 (%)
45+
* (월 1회 이상 루틴 완료 유저 수 / 전체 가입자 수) × 100
46+
*/
47+
@Column(precision = 5, scale = 2)
48+
private BigDecimal monthlyRoutineActiveUserRate;
49+
50+
/**
51+
* 지표 3: 앱 접속 후 루틴 1개 이상 등록 비율 (%)
52+
* (루틴 1개 이상 등록 유저 수 / 월간 활성 사용자 수) × 100
53+
*/
54+
@Column(precision = 5, scale = 2)
55+
private BigDecimal routineRegistrationRate;
56+
57+
/**
58+
* 지표 4: '나가봐요' 카테고리 루틴 1회 이상 완료 비율 (%)
59+
* (외출 루틴 1회 이상 완료 유저 수 / 월간 활성 사용자 수) × 100
60+
*/
61+
@Column(precision = 5, scale = 2)
62+
private BigDecimal outingRoutineCompletionRate;
63+
64+
/**
65+
* 지표 5: 긍정 감정 비율 (%)
66+
* (긍정 감정 기록 수 / 전체 감정 기록 수) × 100 — 활기, 만족, 평온
67+
*/
68+
@Column(precision = 5, scale = 2)
69+
private BigDecimal positiveEmotionRate;
70+
71+
@Builder
72+
public MonthlyKpi(LocalDate targetMonth,
73+
BigDecimal routineCompletionWithinSevenDaysRate,
74+
BigDecimal monthlyRoutineActiveUserRate,
75+
BigDecimal routineRegistrationRate,
76+
BigDecimal outingRoutineCompletionRate,
77+
BigDecimal positiveEmotionRate) {
78+
this.targetMonth = targetMonth;
79+
this.routineCompletionWithinSevenDaysRate = routineCompletionWithinSevenDaysRate;
80+
this.monthlyRoutineActiveUserRate = monthlyRoutineActiveUserRate;
81+
this.routineRegistrationRate = routineRegistrationRate;
82+
this.outingRoutineCompletionRate = outingRoutineCompletionRate;
83+
this.positiveEmotionRate = positiveEmotionRate;
84+
}
85+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package bitnagil.bitnagil_backend.kpi.repository;
2+
3+
import java.time.LocalDate;
4+
import java.time.LocalDateTime;
5+
import java.util.List;
6+
7+
/**
8+
* KPI 집계 전용 조회 쿼리를 모아둔 리포지토리 인터페이스입니다.
9+
* 복합 집계(루틴 완료 유저 수, 7일 내 완료 유저 수 등)를 제공합니다.
10+
*/
11+
public interface KpiQueryRepository {
12+
13+
long countDistinctUsersWithRoutineCompletionV1InPeriod(LocalDate start, LocalDate end);
14+
15+
long countDistinctUsersWithRoutineCompletionV2InPeriod(LocalDate start, LocalDate end);
16+
17+
/**
18+
* 전월 기간 내 V1 또는 V2 루틴 1회 이상 완료한 distinct user_id 수 (중복 제거)
19+
*/
20+
long countDistinctUsersWithRoutineCompletionInPeriod(LocalDate start, LocalDate end);
21+
22+
23+
long countDistinctUsersWithRoutineRegistrationInPeriod(LocalDateTime monthStart, LocalDateTime monthEnd);
24+
25+
long countDistinctUsersWithOutingCompletionInPeriod(LocalDate start, LocalDate end);
26+
27+
long countDistinctUsersWithEmotionInPeriod(LocalDate start, LocalDate end);
28+
29+
List<Long> findDistinctActiveUserIdsInPeriod(
30+
LocalDate start,
31+
LocalDate end,
32+
LocalDateTime monthStart,
33+
LocalDateTime monthEnd);
34+
35+
/**
36+
* 전월 신규 가입자 중 가입 후 7일 이내 V1 루틴 1회 이상 완료한 user_id 목록 (중복 제거는 서비스에서 V2와 합쳐서 수행)
37+
*/
38+
List<Long> findUserIdsWithRoutineCompletionWithin7DaysV1(LocalDateTime monthStart, LocalDateTime monthEnd);
39+
40+
/**
41+
* 전월 신규 가입자 중 가입 후 7일 이내 V2 루틴 1회 이상 완료한 user_id 목록
42+
*/
43+
List<Long> findUserIdsWithRoutineCompletionWithin7DaysV2(LocalDateTime monthStart, LocalDateTime monthEnd);
44+
}

0 commit comments

Comments
 (0)