Skip to content

Commit 497e85f

Browse files
committed
[FEAT] 사용자 데이터 기반 특정 단어의 통계 정보 조회 기능 구현
1 parent 7b9e186 commit 497e85f

File tree

6 files changed

+132
-8
lines changed

6 files changed

+132
-8
lines changed

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package com.movelog.domain.record.application;
22

33
import com.movelog.domain.record.domain.Keyword;
4+
import com.movelog.domain.record.domain.Record;
5+
import com.movelog.domain.record.domain.repository.RecordRepository;
6+
import com.movelog.domain.record.dto.response.MyKeywordStatsRes;
47
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
58
import com.movelog.domain.record.exception.KeywordNotFoundException;
69
import com.movelog.domain.record.domain.repository.KeywordRepository;
710
import com.movelog.domain.user.application.UserService;
811
import com.movelog.domain.user.domain.User;
12+
import com.movelog.domain.user.domain.repository.UserRepository;
913
import com.movelog.domain.user.exception.UserNotFoundException;
1014
import com.movelog.global.config.security.token.UserPrincipal;
1115
import lombok.RequiredArgsConstructor;
1216
import lombok.extern.slf4j.Slf4j;
1317
import org.springframework.stereotype.Service;
1418
import org.springframework.transaction.annotation.Transactional;
1519

20+
import java.time.LocalDateTime;
1621
import java.util.List;
1722
import java.util.Optional;
1823

@@ -23,10 +28,14 @@
2328
public class KeywordService {
2429

2530
private final UserService userService;
31+
private final UserRepository userRepository;
2632
private final KeywordRepository keywordRepository;
33+
private final RecordRepository recordRepository;
2734

2835
public List<SearchKeywordInStatsRes> searchKeywordInStats(UserPrincipal userPrincipal, String keyword) {
36+
2937
User user = validUserById(userPrincipal);
38+
3039
// 검색어를 포함한 키워드 리스트 조회
3140
List<Keyword> keywords = keywordRepository.findAllByUserAndKeywordContaining(user, keyword);
3241

@@ -42,6 +51,19 @@ public List<SearchKeywordInStatsRes> searchKeywordInStats(UserPrincipal userPrin
4251

4352
}
4453

54+
public MyKeywordStatsRes getMyKeywordStatsRes(UserPrincipal userPrincipal, Long keywordId) {
55+
validUserById(userPrincipal);
56+
Keyword keyword = validKeywordById(keywordId);
57+
58+
return MyKeywordStatsRes.builder()
59+
.noun(keyword.getKeyword())
60+
.count(keywordRecordCount(keywordId))
61+
.lastRecordedAt(getLastRecordedAt(keywordId))
62+
.avgDailyRecord(calculateAverageDailyRecords(keywordId))
63+
.avgWeeklyRecord(getAvgWeeklyRecord(keywordId))
64+
.build();
65+
}
66+
4567

4668
// 키워드 내 기록 개수를 반환
4769
private int keywordRecordCount(Long keywordId){
@@ -56,8 +78,53 @@ private List<Keyword> sortKeywordByRecordCount(List<Keyword> keywords) {
5678
.toList();
5779
}
5880

81+
// 키워드의 마지막 기록 시간을 반환
82+
private LocalDateTime getLastRecordedAt(Long keywordId) {
83+
Record record = recordRepository.findTopByKeywordKeywordIdOrderByActionTimeDesc(keywordId);
84+
return record.getActionTime();
85+
}
86+
87+
// 키워드의 일일 평균 기록 수를 반환
88+
public double calculateAverageDailyRecords(Long keywordId) {
89+
List<Object[]> results = recordRepository.findKeywordRecordCountsByDate(keywordId);
90+
91+
// 총 기록 수와 기록된 날짜 수 계산
92+
long totalRecords = results.stream()
93+
.mapToLong(row -> (Long) row[0]) // recordCount
94+
.sum();
95+
96+
long days = results.size(); // 날짜 수
97+
98+
// 일일 평균 계산
99+
double result = days == 0 ? 0 : (double) totalRecords / days;
100+
// 소수점 둘째 자리에서 반올림하여 반환
101+
return roundToTwoDecimal(result);
102+
}
103+
104+
// 키워드의 최근 7일간 평균 기록 수를 반환
105+
public double getAvgWeeklyRecord(Long keywordId) {
106+
Keyword keyword = validKeywordById(keywordId);
107+
List<Record> records = recordRepository.findTop5ByKeywordOrderByActionTimeDesc(keyword);
108+
109+
// 최근 7일간 기록 수 계산
110+
long totalRecords = records.size();
111+
long days = 7;
112+
113+
// 일일 평균 계산
114+
double result = days == 0 ? 0 : (double) totalRecords / days;
115+
// 소수점 둘째 자리에서 반올림하여 반환
116+
return roundToTwoDecimal(result);
117+
118+
}
119+
120+
// 소수점 둘째 자리에서 반올림하여 반환
121+
private double roundToTwoDecimal(double value) {
122+
return Math.round(value * 100) / 100.0;
123+
}
124+
59125
private User validUserById(UserPrincipal userPrincipal) {
60126
Optional<User> userOptional = userService.findById(userPrincipal.getId());
127+
// Optional<User> userOptional = userRepository.findById(5L);
61128
if (userOptional.isEmpty()) { throw new UserNotFoundException(); }
62129
return userOptional.get();
63130
}

src/main/java/com/movelog/domain/record/domain/Keyword.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ public Keyword(User user, String keyword, VerbType verbType) {
4444
this.keyword = keyword;
4545
this.verbType = verbType;
4646
}
47+
4748
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public interface KeywordRepository extends JpaRepository<Keyword,Long> {
2020
Keyword findByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType);
2121

2222
List<Keyword> findAllByUserAndKeywordContaining(User user, String keyword);
23+
2324
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.data.domain.Pageable;
88
import org.springframework.data.jpa.repository.JpaRepository;
99
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
1011
import org.springframework.stereotype.Repository;
1112

1213
import java.time.LocalDateTime;
@@ -31,4 +32,11 @@ public interface RecordRepository extends JpaRepository<Record,Long> {
3132
// 5개의 기록만 조회
3233
List<Record> findTop5ByKeywordUserAndRecordImageNotNullOrderByActionTimeDesc(User user);
3334

35+
@Query("SELECT COUNT(r) AS recordCount, DATE(r.actionTime) AS recordDate " +
36+
"FROM Record r " +
37+
"WHERE r.keyword.keywordId = :keywordId " +
38+
"GROUP BY DATE(r.actionTime)")
39+
List<Object[]> findKeywordRecordCountsByDate(Long keywordId);
40+
41+
Record findTopByKeywordKeywordIdOrderByActionTimeDesc(Long keywordId);
3442
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 MyKeywordStatsRes {
16+
17+
@Schema( type = "String", example = "헬스", description = "통계 대상 명사(키워드)")
18+
private String noun;
19+
20+
@Schema( type = "int", example = "1", description = "사용자가 해당 명사에 대해 기록한 횟수")
21+
private int count;
22+
23+
@Schema(type = "LocalDateTime", example = "2025-08-01T00:00:00", description = "마지막 기록 일시(가장 최근에 기록한 시간)")
24+
private LocalDateTime lastRecordedAt;
25+
26+
@Schema(type = "Double", example = "0.5", description = "평균 일간 기록")
27+
private double avgDailyRecord;
28+
29+
@Schema(type = "Double", example = "0.5", description = "최근 7일단 평균 기록")
30+
private double avgWeeklyRecord;
31+
32+
}

src/main/java/com/movelog/domain/record/presentation/KeywordController.java renamed to src/main/java/com/movelog/domain/record/presentation/StatsController.java

Lines changed: 23 additions & 8 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.MyKeywordStatsRes;
45
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
56
import com.movelog.global.config.security.token.UserPrincipal;
67
import com.movelog.global.payload.ErrorResponse;
@@ -15,19 +16,16 @@
1516
import lombok.extern.slf4j.Slf4j;
1617
import org.springframework.http.ResponseEntity;
1718
import org.springframework.security.core.annotation.AuthenticationPrincipal;
18-
import org.springframework.web.bind.annotation.GetMapping;
19-
import org.springframework.web.bind.annotation.RequestMapping;
20-
import org.springframework.web.bind.annotation.RequestParam;
21-
import org.springframework.web.bind.annotation.RestController;
19+
import org.springframework.web.bind.annotation.*;
2220

2321
import java.util.List;
2422

2523
@Slf4j
2624
@RestController
2725
@RequiredArgsConstructor
28-
@RequestMapping("/api/v1/keyword")
29-
@Tag(name = "Keyword", description = "단어 관련 API입니다.")
30-
public class KeywordController {
26+
@RequestMapping("/api/v1/stats")
27+
@Tag(name = "Stats", description = "통계 관련 API입니다.")
28+
public class StatsController {
3129

3230
private final KeywordService keywordService;
3331

@@ -39,7 +37,7 @@ public class KeywordController {
3937
@ApiResponse(responseCode = "400", description = "단어 검색 결과 조회 실패",
4038
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
4139
})
42-
@GetMapping("/stats/search")
40+
@GetMapping("/word/search")
4341
public ResponseEntity<?> searchKeywordInStats(
4442
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
4543
@Parameter(description = "검색할 명사를 입력해주세요.", required = true) @RequestParam String keyword
@@ -49,6 +47,23 @@ public ResponseEntity<?> searchKeywordInStats(
4947
}
5048

5149

50+
@Operation(summary = "나의 특정 단어 통계 정보 조회 API", description = "나의 특정 단어 통계 정보를 조회하는 API입니다.")
51+
@ApiResponses(value = {
52+
@ApiResponse(responseCode = "200", description = "나의 특정 단어 통계 정보 조회 성공",
53+
content = @Content(mediaType = "application/json",
54+
schema = @Schema(implementation = MyKeywordStatsRes.class))),
55+
@ApiResponse(responseCode = "400", description = "나의 특정 단어 통계 정보 조회 실패",
56+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
57+
})
58+
@GetMapping("/word/my/{keywordId}")
59+
public ResponseEntity<?> getMyKeywordStats(
60+
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
61+
@Parameter(description = "검색할 명사의 id를 입력해주세요.", required = true) @PathVariable Long keywordId
62+
) {
63+
MyKeywordStatsRes response = keywordService.getMyKeywordStatsRes(userPrincipal, keywordId);
64+
return ResponseEntity.ok(response);
65+
}
66+
5267

5368

5469
}

0 commit comments

Comments
 (0)