Skip to content

Commit a561dbb

Browse files
authored
Merge pull request #85 from Move-Log/develop
[FEAT] 모든 사용자 데이터 기반 특정 단어의 통계 정보 조회 기능 구현
2 parents b5459d8 + d3c7c77 commit a561dbb

File tree

4 files changed

+141
-10
lines changed

4 files changed

+141
-10
lines changed

src/main/java/com/movelog/domain/record/application/KeywordService.java

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.movelog.domain.record.domain.Keyword;
44
import com.movelog.domain.record.domain.Record;
55
import com.movelog.domain.record.domain.repository.RecordRepository;
6+
import com.movelog.domain.record.dto.response.AllUserKeywordStatsRes;
67
import com.movelog.domain.record.dto.response.MyKeywordStatsRes;
78
import com.movelog.domain.record.dto.response.RecommendKeywordInStatsRes;
89
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
@@ -15,7 +16,6 @@
1516
import com.movelog.global.config.security.token.UserPrincipal;
1617
import lombok.RequiredArgsConstructor;
1718
import lombok.extern.slf4j.Slf4j;
18-
import org.springframework.data.jpa.repository.Query;
1919
import org.springframework.stereotype.Service;
2020
import org.springframework.transaction.annotation.Transactional;
2121

@@ -54,6 +54,7 @@ public List<SearchKeywordInStatsRes> searchKeywordInStats(UserPrincipal userPrin
5454

5555
}
5656

57+
// 사용자 개인의 특정 키워드 통계 조회
5758
public MyKeywordStatsRes getMyKeywordStatsRes(UserPrincipal userPrincipal, Long keywordId) {
5859
validUserById(userPrincipal);
5960
Keyword keyword = validKeywordById(keywordId);
@@ -64,35 +65,35 @@ public MyKeywordStatsRes getMyKeywordStatsRes(UserPrincipal userPrincipal, Long
6465

6566
return MyKeywordStatsRes.builder()
6667
.noun(keyword.getKeyword())
67-
.count(keywordRecordCount(keywordId))
68-
.lastRecordedAt(getLastRecordedAt(keywordId))
69-
.avgDailyRecord(calculateAverageDailyRecords(keywordId))
70-
.avgWeeklyRecord(getAvgWeeklyRecord(keywordId))
68+
.count(keywordRecordCountByKeywordId(keywordId))
69+
.lastRecordedAt(getLastRecordedAtByKeywordId(keywordId))
70+
.avgDailyRecord(calculateAverageDailyRecordsByKeywordId(keywordId))
71+
.avgWeeklyRecord(getAvgWeeklyRecordByKeywordId(keywordId))
7172
.build();
7273
}
7374

7475

7576
// 키워드 내 기록 개수를 반환
76-
private int keywordRecordCount(Long keywordId){
77+
private int keywordRecordCountByKeywordId(Long keywordId){
7778
Keyword keyword = validKeywordById(keywordId);
7879
return keyword.getRecords().size();
7980
}
8081

8182
// 키워드 내 기록이 많은 순서대로 정렬
8283
private List<Keyword> sortKeywordByRecordCount(List<Keyword> keywords) {
8384
return keywords.stream()
84-
.sorted((k1, k2) -> keywordRecordCount(k2.getKeywordId()) - keywordRecordCount(k1.getKeywordId()))
85+
.sorted((k1, k2) -> keywordRecordCountByKeywordId(k2.getKeywordId()) - keywordRecordCountByKeywordId(k1.getKeywordId()))
8586
.toList();
8687
}
8788

8889
// 키워드의 마지막 기록 시간을 반환
89-
private LocalDateTime getLastRecordedAt(Long keywordId) {
90+
private LocalDateTime getLastRecordedAtByKeywordId(Long keywordId) {
9091
Record record = recordRepository.findTopByKeywordKeywordIdOrderByActionTimeDesc(keywordId);
9192
return record.getActionTime();
9293
}
9394

9495
// 키워드의 일일 평균 기록 수를 반환
95-
public double calculateAverageDailyRecords(Long keywordId) {
96+
public double calculateAverageDailyRecordsByKeywordId(Long keywordId) {
9697
List<Object[]> results = recordRepository.findKeywordRecordCountsByDate(keywordId);
9798

9899
// 총 기록 수와 기록된 날짜 수 계산
@@ -109,7 +110,7 @@ public double calculateAverageDailyRecords(Long keywordId) {
109110
}
110111

111112
// 키워드의 최근 7일간 평균 기록 수를 반환
112-
public double getAvgWeeklyRecord(Long keywordId) {
113+
public double getAvgWeeklyRecordByKeywordId(Long keywordId) {
113114
Keyword keyword = validKeywordById(keywordId);
114115
List<Record> records = recordRepository.findTop5ByKeywordOrderByActionTimeDesc(keyword);
115116

@@ -141,6 +142,73 @@ public List<RecommendKeywordInStatsRes> getRecommendKeywords(UserPrincipal userP
141142
.toList();
142143
}
143144

145+
146+
// 전체 사용자의 특정 키워드 통계 조회
147+
public AllUserKeywordStatsRes getAllUserKeywordStats(UserPrincipal userPrincipal, String keyword) {
148+
validUserById(userPrincipal);
149+
// 해당 키워드에 대한 전체 사용자의 기록 목록
150+
List<Record> records = recordRepository.findAllByKeyword(keyword);
151+
152+
return AllUserKeywordStatsRes.builder()
153+
.noun(keyword)
154+
.count(records.size())
155+
.lastRecordedAt(getLastRecordedAtByRecords(records))
156+
.avgDailyRecord(calculateAverageDailyRecordsByRecords(keyword))
157+
.avgWeeklyRecord(getAvgWeeklyRecordByRecords(records))
158+
.build();
159+
}
160+
161+
// 키워드의 마지막 기록 시간을 반환
162+
private LocalDateTime getLastRecordedAtByRecords(List<Record> records) {
163+
return records.stream()
164+
.map(Record::getActionTime)
165+
.max(LocalDateTime::compareTo)
166+
.orElse(null);
167+
}
168+
169+
// 키워드의 일일 평균 기록 수를 반환
170+
private double calculateAverageDailyRecordsByRecords(String keyword) {
171+
/// 날짜별 기록 수 계산
172+
List<Object[]> results = recordRepository.findRecordCountsByKeywordGroupedByDate(keyword);
173+
174+
// 총 기록 수 계산
175+
long totalRecords = results.stream()
176+
.mapToLong(row -> ((Long) row[1])) // COUNT(r)
177+
.sum();
178+
179+
// 기록된 날짜 수 계산
180+
long days = results.stream()
181+
.map(row -> (java.sql.Date) row[0]) // DATE(r.actionTime)
182+
.distinct()
183+
.count();
184+
185+
// 일일 평균 계산
186+
double result = days == 0 ? 0 : (double) totalRecords / days;
187+
188+
// 소수점 둘째 자리에서 반올림하여 반환
189+
return roundToTwoDecimal(result);
190+
}
191+
192+
// 키워드의 최근 7일간 평균 기록 수를 반환
193+
private double getAvgWeeklyRecordByRecords(List<Record> records) {
194+
// 최근 7일간 기록 조회 (날짜 기준 오름차순 정렬 후 최근 7일 간의 기록만 추출) -> 하루에도 여러 개의 기록이 있을 수 있음
195+
LocalDateTime today = LocalDateTime.now();
196+
LocalDateTime weekAgo = today.minusDays(7);
197+
List<Record> recentRecords = records.stream()
198+
.filter(record -> record.getActionTime().isAfter(weekAgo))
199+
.toList();
200+
201+
// 최근 7일간 기록 수 계산
202+
long totalRecords = recentRecords.size();
203+
long days = 7;
204+
205+
// 일일 평균 계산
206+
double result = days == 0 ? 0 : (double) totalRecords / days;
207+
// 소수점 둘째 자리에서 반올림하여 반환
208+
return roundToTwoDecimal(result);
209+
}
210+
211+
144212
private User validUserById(UserPrincipal userPrincipal) {
145213
Optional<User> userOptional = userService.findById(userPrincipal.getId());
146214
// Optional<User> userOptional = userRepository.findById(5L);

src/main/java/com/movelog/domain/record/domain/repository/RecordRepository.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,18 @@ public interface RecordRepository extends JpaRepository<Record,Long> {
3939
List<Object[]> findKeywordRecordCountsByDate(Long keywordId);
4040

4141
Record findTopByKeywordKeywordIdOrderByActionTimeDesc(Long keywordId);
42+
43+
@Query("SELECT r FROM Record r " +
44+
"JOIN r.keyword k " +
45+
"WHERE k.keyword = :keyword " +
46+
"ORDER BY r.actionTime DESC")
47+
List<Record> findAllByKeyword(String keyword);
48+
49+
@Query("SELECT DATE(r.actionTime), COUNT(r) " +
50+
"FROM Record r " +
51+
"JOIN r.keyword k " +
52+
"WHERE k.keyword = :keyword " +
53+
"GROUP BY DATE(r.actionTime)")
54+
List<Object[]> findRecordCountsByKeywordGroupedByDate(String keyword);
55+
4256
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.movelog.domain.record.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.LocalDateTime;
10+
11+
@Builder
12+
@AllArgsConstructor
13+
@NoArgsConstructor
14+
@Getter
15+
public class AllUserKeywordStatsRes {
16+
@Schema( type = "String", example = "헬스", description = "통계 대상 명사(키워드)")
17+
private String noun;
18+
19+
@Schema( type = "int", example = "1", description = "전체 사용자가 해당 명사에 대해 기록한 횟수")
20+
private int count;
21+
22+
@Schema(type = "LocalDateTime", example = "2025-08-01T00:00:00", description = "마지막 기록 일시(가장 최근에 기록한 시간)")
23+
private LocalDateTime lastRecordedAt;
24+
25+
@Schema(type = "Double", example = "0.5", description = "평균 일간 기록")
26+
private double avgDailyRecord;
27+
28+
@Schema(type = "Double", example = "0.5", description = "최근 7일단 평균 기록")
29+
private double avgWeeklyRecord;
30+
}

src/main/java/com/movelog/domain/record/presentation/StatsController.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.movelog.domain.record.presentation;
22

33
import com.movelog.domain.record.application.KeywordService;
4+
import com.movelog.domain.record.dto.response.AllUserKeywordStatsRes;
45
import com.movelog.domain.record.dto.response.MyKeywordStatsRes;
56
import com.movelog.domain.record.dto.response.RecommendKeywordInStatsRes;
67
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
@@ -81,5 +82,23 @@ public ResponseEntity<?> getRecommendKeywords(
8182
return ResponseEntity.ok(response);
8283
}
8384

85+
@Operation(summary = "전체 사용자 대상 특정 단어 통계 조회 API", description = "전체 사용자 데이터를 대상으로 특정 단어 통계를 조회하는 API입니다.")
86+
@ApiResponses(value = {
87+
@ApiResponse(responseCode = "200", description = "전체 사용자 대상 특정 단어 통계 조회 성공",
88+
content = @Content(mediaType = "application/json",
89+
schema = @Schema(implementation = AllUserKeywordStatsRes.class))),
90+
@ApiResponse(responseCode = "400", description = "전체 사용자 대상 특정 단어 통계 조회 실패",
91+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
92+
})
93+
@GetMapping("/word/all")
94+
public ResponseEntity<?> getAllUserKeywordStats(
95+
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
96+
@Parameter(description = "검색할 명사를 입력해주세요.", required = true) @RequestParam String keyword
97+
) {
98+
AllUserKeywordStatsRes response = keywordService.getAllUserKeywordStats(userPrincipal, keyword);
99+
return ResponseEntity.ok(response);
100+
}
101+
102+
84103

85104
}

0 commit comments

Comments
 (0)