diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/community/controller/CommunityController.java b/src/main/java/com/cokerthon/Team3_Backend/domain/community/controller/CommunityController.java index 0f369d0..7ce1ed6 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/community/controller/CommunityController.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/community/controller/CommunityController.java @@ -18,11 +18,13 @@ import com.cokerthon.Team3_Backend.global.security.CurrentUser; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @RestController @RequestMapping("/api/community") @RequiredArgsConstructor +@Tag(name = "Community API") public class CommunityController { private final CommunityService communityService; diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java index 453a385..06534ec 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/controller/EmotionController.java @@ -1,11 +1,17 @@ package com.cokerthon.Team3_Backend.domain.emotion.controller; +import java.time.LocalDate; + import com.cokerthon.Team3_Backend.domain.emotion.converter.EmotionConverter; import com.cokerthon.Team3_Backend.domain.emotion.dto.request.EmotionRequestDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionDailyResDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionMonthlyResDto; import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionResponseDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionWeeklyResDto; import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; import com.cokerthon.Team3_Backend.domain.emotion.exception.code.EmotionSuccessCode; import com.cokerthon.Team3_Backend.domain.emotion.service.command.EmotionCommandService; +import com.cokerthon.Team3_Backend.domain.emotion.service.query.EmotionQueryService; import com.cokerthon.Team3_Backend.domain.user.entity.User; import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository; import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse; @@ -15,6 +21,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; + +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -28,6 +36,7 @@ public class EmotionController { private final EmotionCommandService emotionCommandService; + private final EmotionQueryService emotionQueryService; private final UserRepository userRepository; /** @@ -176,4 +185,33 @@ public ResponseEntity> deleteEmotion( ) ); } + + @GetMapping("/daily") + @Operation(summary = "감정 일별 조회", description = "특정 날짜의 감정 기록 리스트와 평균 점수를 조회합니다.") + public ApiResponse getDailyEmotions( + @CurrentUser User user, + @RequestParam(name = "date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date + ) { + EmotionDailyResDto response = emotionQueryService.getDailyEmotions(user, date); + return ApiResponse.onSuccess(EmotionSuccessCode.EMOTION_FOUND, response); + } + + @GetMapping("/weekly") + @Operation(summary = "이번 주 감정 조회", description = "이번 주 월~일요일의 요일별 평균 점수와 주간 전체 평균을 조회합니다.") + public ApiResponse getWeeklyEmotions(@CurrentUser User user) { + EmotionWeeklyResDto response = emotionQueryService.getWeeklyEmotions(user); + return ApiResponse.onSuccess(EmotionSuccessCode.EMOTION_FOUND, response); + } + + @GetMapping("/monthly") + @Operation(summary = "감정 월별 조회", description = "특정 월의 일별 평균 점수 목록을 조회합니다.") + public ApiResponse getMonthlyEmotions( + @CurrentUser User user, + @RequestParam(name = "year") int year, + @RequestParam(name = "month") int month + ) { + EmotionMonthlyResDto response = emotionQueryService.getMonthlyEmotions(user, year, month); + return ApiResponse.onSuccess(EmotionSuccessCode.EMOTION_FOUND, response); + } + } \ No newline at end of file diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java index 7800d5f..0a1407c 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/converter/EmotionConverter.java @@ -55,15 +55,15 @@ public static EmotionResponseDto.EmotionUpdateResponseDto toUpdateResponse(Emoti ); } - public static EmotionResponseDto.EmotionDetailResponseDto toDetailResponse(EmotionRecord emotion) { - return new EmotionResponseDto.EmotionDetailResponseDto( - emotion.getId(), - emotion.getEmotionScore(), - emotion.getContent(), - emotion.getVisibility(), - emotion.getStatus(), - emotion.getCreatedAt(), - emotion.getUpdatedAt() - ); + public static EmotionResponseDto.EmotionDetailResponseDto toDetailDto(EmotionRecord record) { + return EmotionResponseDto.EmotionDetailResponseDto.builder() + .emotionId(record.getId()) + .emotionScore(record.getEmotionScore()) + .content(record.getContent()) + .visibility(record.getVisibility()) + .status(record.getStatus()) + .createdAt(record.getCreatedAt()) + .updatedAt(record.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionDailyResDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionDailyResDto.java new file mode 100644 index 0000000..583840e --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionDailyResDto.java @@ -0,0 +1,21 @@ +package com.cokerthon.Team3_Backend.domain.emotion.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(description = "감정 일별 조회 응답 DTO") +public record EmotionDailyResDto( + @Schema(description = "해당 날짜") + LocalDate date, + + @Schema(description = "하루 평균 감정 점수") + double averageScore, + + @Schema(description = "해당 일자 감정 일기 목록") + List emotions +) { +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionMonthlyResDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionMonthlyResDto.java new file mode 100644 index 0000000..fcb4e46 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionMonthlyResDto.java @@ -0,0 +1,22 @@ +package com.cokerthon.Team3_Backend.domain.emotion.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(description = "감정 월별 조회 응답 DTO") +public record EmotionMonthlyResDto( + @Schema(description = "조회한 연도 및 월 (YYYY-MM)") + String yearMonth, + + @Schema(description = "일별 평균 점수 목록") + List dayAverages +) { + public record DayAverageDto( + LocalDate date, + double dailyAverage // 데이터가 없으면 0.0 + ) {} +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java index 8cf4e03..c4b5a96 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionResponseDto.java @@ -5,6 +5,8 @@ import java.time.LocalDateTime; +import lombok.Builder; + public class EmotionResponseDto { public record EmotionCreateResponseDto( @@ -24,6 +26,7 @@ public record EmotionUpdateResponseDto( ) { } + @Builder public record EmotionDetailResponseDto( Long emotionId, int emotionScore, diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionWeeklyResDto.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionWeeklyResDto.java new file mode 100644 index 0000000..fae63c4 --- /dev/null +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/dto/response/EmotionWeeklyResDto.java @@ -0,0 +1,23 @@ +package com.cokerthon.Team3_Backend.domain.emotion.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(description = "이번 주 감정 조회 응답 DTO") +public record EmotionWeeklyResDto( + @Schema(description = "이번 주 전체 평균 점수 (예: 3.2도)") + double weeklyAverage, + + @Schema(description = "요일별 요약 정보 목록") + List weeklyList +) { + public record DaySummaryDto( + String dayOfWeek, + LocalDate date, + double dailyAverage // 해당 날짜의 평균 점수 + ) {} +} diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java index 9bfd11a..3281127 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/exception/code/EmotionSuccessCode.java @@ -14,6 +14,8 @@ public enum EmotionSuccessCode implements BaseSuccessCode { EMOTION_UPDATED(HttpStatus.OK, "EMOTION_200_2", "감정 기록 수정 성공"), EMOTION_DELETED(HttpStatus.OK, "EMOTION_200_3", "감정 기록 삭제 성공"), + + EMOTION_FOUND(HttpStatus.OK, "EMOTION_200_4", "감정 기록 조회 성공"), ; private final HttpStatus status; diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRecordRepository.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRecordRepository.java index 05d7e43..23e91bb 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRecordRepository.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/repository/EmotionRecordRepository.java @@ -1,5 +1,6 @@ package com.cokerthon.Team3_Backend.domain.emotion.repository; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -7,6 +8,7 @@ import com.cokerthon.Team3_Backend.domain.community.dto.response.CommunityResDto; import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import com.cokerthon.Team3_Backend.domain.user.entity.User; public interface EmotionRecordRepository extends JpaRepository { @@ -19,4 +21,6 @@ public interface EmotionRecordRepository extends JpaRepository findAllCommunityPosts(); + + List findByUserAndCreatedAtBetween(User user, LocalDateTime startOfDay, LocalDateTime endOfDay); } diff --git a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java index 0c12fb4..72f7f76 100644 --- a/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java +++ b/src/main/java/com/cokerthon/Team3_Backend/domain/emotion/service/query/EmotionQueryService.java @@ -1,4 +1,129 @@ package com.cokerthon.Team3_Backend.domain.emotion.service.query; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.cokerthon.Team3_Backend.domain.emotion.converter.EmotionConverter; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionDailyResDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionMonthlyResDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionResponseDto; +import com.cokerthon.Team3_Backend.domain.emotion.dto.response.EmotionWeeklyResDto; +import com.cokerthon.Team3_Backend.domain.emotion.entity.EmotionRecord; +import com.cokerthon.Team3_Backend.domain.emotion.repository.EmotionRecordRepository; +import com.cokerthon.Team3_Backend.domain.user.entity.User; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional public class EmotionQueryService { + + private final EmotionRecordRepository emotionRecordRepository; + + @Transactional(readOnly = true) + public EmotionDailyResDto getDailyEmotions(User user, LocalDate date) { + LocalDateTime startOfDay = date.atStartOfDay(); + LocalDateTime endOfDay = date.atTime(LocalTime.MAX); + + List records = emotionRecordRepository.findByUserAndCreatedAtBetween(user, startOfDay, endOfDay); + + double averageScore = records.stream() + .mapToInt(EmotionRecord::getEmotionScore) + .average() + .orElse(0.0); + + averageScore = Math.round(averageScore * 10) / 10.0; + + List emotionDtos = records.stream() + .map(EmotionConverter::toDetailDto) + .toList(); + + return EmotionDailyResDto.builder() + .date(date) + .averageScore(averageScore) + .emotions(emotionDtos) + .build(); + } + + @Transactional(readOnly = true) + public EmotionWeeklyResDto getWeeklyEmotions(User user) { + LocalDate today = LocalDate.now(); + // 이번 주 월요일 찾기 + LocalDate monday = today.with(java.time.temporal.TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)); + + List weeklyList = new ArrayList<>(); + double totalScoreSum = 0; + int daysWithData = 0; + + // 월요일부터 일요일까지 7일간 반복 + for (int i = 0; i < 7; i++) { + LocalDate targetDate = monday.plusDays(i); + LocalDateTime start = targetDate.atStartOfDay(); + LocalDateTime end = targetDate.atTime(LocalTime.MAX); + + List dayRecords = emotionRecordRepository.findByUserAndCreatedAtBetween(user, start, end); + + double dayAvg = dayRecords.stream() + .mapToInt(EmotionRecord::getEmotionScore) + .average().orElse(0.0); + dayAvg = Math.round(dayAvg * 10) / 10.0; + + weeklyList.add(new EmotionWeeklyResDto.DaySummaryDto( + targetDate.getDayOfWeek().getDisplayName(java.time.format.TextStyle.SHORT, java.util.Locale.KOREAN), + targetDate, + dayAvg + )); + + if (dayAvg > 0) { + totalScoreSum += dayAvg; + daysWithData++; + } + } + + // 주간 전체 평균 계산 + double weeklyAvg = (daysWithData > 0) ? Math.round((totalScoreSum / daysWithData) * 10) / 10.0 : 0.0; + + return EmotionWeeklyResDto.builder() + .weeklyAverage(weeklyAvg) + .weeklyList(weeklyList) + .build(); + } + + @Transactional(readOnly = true) + public EmotionMonthlyResDto getMonthlyEmotions(User user, int year, int month) { + YearMonth yearMonth = YearMonth.of(year, month); + LocalDate startOfMonth = yearMonth.atDay(1); + LocalDate endOfMonth = yearMonth.atEndOfMonth(); + + List dayAverages = new ArrayList<>(); + + // 1일부터 말일까지 반복 + for (LocalDate date = startOfMonth; !date.isAfter(endOfMonth); date = date.plusDays(1)) { + LocalDateTime start = date.atStartOfDay(); + LocalDateTime end = date.atTime(LocalTime.MAX); + + List dayRecords = emotionRecordRepository.findByUserAndCreatedAtBetween(user, start, end); + + double avg = dayRecords.stream() + .mapToInt(EmotionRecord::getEmotionScore) + .average().orElse(0.0); + + avg = Math.round(avg * 10) / 10.0; + + dayAverages.add(new EmotionMonthlyResDto.DayAverageDto(date, avg)); + } + + return EmotionMonthlyResDto.builder() + .yearMonth(yearMonth.toString()) + .dayAverages(dayAverages) + .build(); + } }