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,23 @@
package com.movelog.domain.record.application;

import com.movelog.domain.record.domain.Keyword;
import com.movelog.domain.record.domain.Record;
import com.movelog.domain.record.domain.repository.RecordRepository;
import com.movelog.domain.record.dto.response.MyKeywordStatsRes;
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
import com.movelog.domain.record.exception.KeywordNotFoundException;
import com.movelog.domain.record.domain.repository.KeywordRepository;
import com.movelog.domain.user.application.UserService;
import com.movelog.domain.user.domain.User;
import com.movelog.domain.user.domain.repository.UserRepository;
import com.movelog.domain.user.exception.UserNotFoundException;
import com.movelog.global.config.security.token.UserPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

Expand All @@ -23,10 +28,14 @@
public class KeywordService {

private final UserService userService;
private final UserRepository userRepository;
private final KeywordRepository keywordRepository;
private final RecordRepository recordRepository;

public List<SearchKeywordInStatsRes> searchKeywordInStats(UserPrincipal userPrincipal, String keyword) {

User user = validUserById(userPrincipal);

// 검색어를 포함한 키워드 리스트 조회
List<Keyword> keywords = keywordRepository.findAllByUserAndKeywordContaining(user, keyword);

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

}

public MyKeywordStatsRes getMyKeywordStatsRes(UserPrincipal userPrincipal, Long keywordId) {
validUserById(userPrincipal);
Keyword keyword = validKeywordById(keywordId);

return MyKeywordStatsRes.builder()
.noun(keyword.getKeyword())
.count(keywordRecordCount(keywordId))
.lastRecordedAt(getLastRecordedAt(keywordId))
.avgDailyRecord(calculateAverageDailyRecords(keywordId))
.avgWeeklyRecord(getAvgWeeklyRecord(keywordId))
.build();
}


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

// 키워드의 마지막 기록 시간을 반환
private LocalDateTime getLastRecordedAt(Long keywordId) {
Record record = recordRepository.findTopByKeywordKeywordIdOrderByActionTimeDesc(keywordId);
return record.getActionTime();
}

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

// 총 기록 수와 기록된 날짜 수 계산
long totalRecords = results.stream()
.mapToLong(row -> (Long) row[0]) // recordCount
.sum();

long days = results.size(); // 날짜 수

// 일일 평균 계산
double result = days == 0 ? 0 : (double) totalRecords / days;
// 소수점 둘째 자리에서 반올림하여 반환
return roundToTwoDecimal(result);
}

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

// 최근 7일간 기록 수 계산
long totalRecords = records.size();
long days = 7;

// 일일 평균 계산
double result = days == 0 ? 0 : (double) totalRecords / days;
// 소수점 둘째 자리에서 반올림하여 반환
return roundToTwoDecimal(result);

}

// 소수점 둘째 자리에서 반올림하여 반환
private double roundToTwoDecimal(double value) {
return Math.round(value * 100) / 100.0;
}

private User validUserById(UserPrincipal userPrincipal) {
Optional<User> userOptional = userService.findById(userPrincipal.getId());
// Optional<User> userOptional = userRepository.findById(5L);
if (userOptional.isEmpty()) { throw new UserNotFoundException(); }
return userOptional.get();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ public Keyword(User user, String keyword, VerbType verbType) {
this.keyword = keyword;
this.verbType = verbType;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ public interface KeywordRepository extends JpaRepository<Keyword,Long> {
Keyword findByUserAndKeywordAndVerbType(User user, String noun, VerbType verbType);

List<Keyword> findAllByUserAndKeywordContaining(User user, String keyword);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

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

@Query("SELECT COUNT(r) AS recordCount, DATE(r.actionTime) AS recordDate " +
"FROM Record r " +
"WHERE r.keyword.keywordId = :keywordId " +
"GROUP BY DATE(r.actionTime)")
List<Object[]> findKeywordRecordCountsByDate(Long keywordId);

Record findTopByKeywordKeywordIdOrderByActionTimeDesc(Long keywordId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.movelog.domain.record.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class MyKeywordStatsRes {

@Schema( type = "String", example = "헬스", description = "통계 대상 명사(키워드)")
private String noun;

@Schema( type = "int", example = "1", description = "사용자가 해당 명사에 대해 기록한 횟수")
private int count;

@Schema(type = "LocalDateTime", example = "2025-08-01T00:00:00", description = "마지막 기록 일시(가장 최근에 기록한 시간)")
private LocalDateTime lastRecordedAt;

@Schema(type = "Double", example = "0.5", description = "평균 일간 기록")
private double avgDailyRecord;

@Schema(type = "Double", example = "0.5", description = "최근 7일단 평균 기록")
private double avgWeeklyRecord;

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.movelog.domain.record.presentation;

import com.movelog.domain.record.application.KeywordService;
import com.movelog.domain.record.dto.response.MyKeywordStatsRes;
import com.movelog.domain.record.dto.response.SearchKeywordInStatsRes;
import com.movelog.global.config.security.token.UserPrincipal;
import com.movelog.global.payload.ErrorResponse;
Expand All @@ -15,36 +16,54 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/keyword")
@Tag(name = "Keyword", description = "단어 관련 API입니다.")
public class KeywordController {
@RequestMapping("/api/v1/stats")
@Tag(name = "Stats", description = "통계 관련 API입니다.")
public class StatsController {

private final KeywordService keywordService;

@Operation(summary = "단어 통계 조회 API", description = "서비스 내에서 생성된 단어를 검색하는 API입니다.")
@Operation(summary = "통계 조회 시 단어 검색 API", description = "통계 조회 시 서비스 내에서 생성된 단어를 검색하는 API입니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "단어 통계 조회 성공",
@ApiResponse(responseCode = "200", description = "단어 검색 결과 조회 성공",
content = @Content(mediaType = "application/json",
schema = @Schema(type = "array", implementation = SearchKeywordInStatsRes.class))),
@ApiResponse(responseCode = "400", description = "단어 통계 조회 실패",
@ApiResponse(responseCode = "400", description = "단어 검색 결과 조회 실패",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/search")
@GetMapping("/word/search")
public ResponseEntity<?> searchKeywordInStats(
@Parameter(description = "Access Token을 입력해주세요.", required = true) @AuthenticationPrincipal UserPrincipal userPrincipal,
@Parameter(description = "검색할 명사를 입력해주세요.", required = true) @RequestParam String keyword
) {
List<SearchKeywordInStatsRes> response = keywordService.searchKeywordInStats(userPrincipal, keyword);
return ResponseEntity.ok(response);
}


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



}