diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java new file mode 100644 index 0000000..61aa1d7 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java @@ -0,0 +1,87 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api; + +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeResponse; +import com.devkor.ifive.nadab.domain.dailyreport.application.HomeQueryService; +import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; +import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; +import com.devkor.ifive.nadab.global.security.principal.UserPrincipal; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +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.RestController; + +@Tag(name = "홈화면 API", description = "홈화면 관련 API") +@RestController +@RequestMapping("${api_prefix}/home") +@RequiredArgsConstructor +public class HomeController { + + private final HomeQueryService homeQueryService; + + @GetMapping + @PreAuthorize("isAuthenticated()") + @Operation( + summary = "홈화면 정보 조회 API", + description = """ + 홈화면에 표시할 답변 관련 정보를 조회합니다. + + ### 제공 정보 + 1. 주간 답변 상태: 이번 주(월~일) 답변한 날짜 목록 + 2. 연속 기록(Streak): 현재 연속 답변 일수 + 3. 총 기록 일수: 첫 답변 이후 경과 일수 (N일째 기록 중) + + ### 계산 기준 + - 주 시작: 월요일, 주 종료: 일요일 + - Streak: 현재까지 매일 연속 답변한 총 일수 + * 오늘 답변 있음 → 오늘까지 포함한 연속 일수 + * 오늘 답변 없음 → 어제까지의 연속 일수 + * 어제도 답변 없음 → 0 + - 총 기록 일수: (오늘 - 첫 답변 날짜) + 1 + + ### 예시 + - 첫 답변: 2025-12-27 + - 1월 1일~15일 매일 연속 답변 + - 오늘: 2026-01-15 + + 응답: + - answeredDates: ["2026-01-13", "2026-01-14", "2026-01-15"] + - streakCount: 15 (1월 1일~15일 연속) + - totalRecordDays: 20 (12월 27일부터 오늘까지 경과) + """, + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse( + responseCode = "200", + description = "홈화면 정보 조회 성공", + content = @Content(schema = @Schema(implementation = HomeResponse.class), mediaType = "application/json") + ), + @ApiResponse( + responseCode = "401", + description = "인증 실패", + content = @Content + ), + @ApiResponse( + responseCode = "404", + description = """ + - ErrorCode: USER_NOT_FOUND - 사용자를 찾을 수 없음 + """, + content = @Content + ) + } + ) + public ResponseEntity> getHomeData( + @AuthenticationPrincipal UserPrincipal principal + ) { + HomeResponse response = homeQueryService.getHomeData(principal.getId()); + return ApiResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java new file mode 100644 index 0000000..34e46e0 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java @@ -0,0 +1,18 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "홈화면 요약 정보 응답") +public record HomeResponse( + @Schema(description = "이번 주(월~일) 답변한 날짜 목록 (오름차순)", example = "[\"2026-01-13\", \"2026-01-14\", \"2026-01-15\"]") + List answeredDates, + + @Schema(description = "현재 연속 답변 일수 (오늘 답변 있으면 오늘까지, 없으면 어제까지)", example = "15") + long streakCount, + + @Schema(description = "첫 답변 이후 경과 일수 (N일째 기록 중)", example = "20") + long totalRecordDays +) { +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java new file mode 100644 index 0000000..60335d3 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java @@ -0,0 +1,94 @@ +package com.devkor.ifive.nadab.domain.dailyreport.application; + +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeResponse; +import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryQueryRepository; +import com.devkor.ifive.nadab.global.shared.util.TodayDateTimeProvider; +import com.devkor.ifive.nadab.global.shared.util.WeekRangeCalculator; +import com.devkor.ifive.nadab.global.shared.util.dto.WeekRangeDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HomeQueryService { + + private final AnswerEntryQueryRepository answerEntryQueryRepository; + + public HomeResponse getHomeData(Long userId) { + LocalDate today = TodayDateTimeProvider.getTodayDate(); + + // 1. 이번 주 범위 계산 + WeekRangeDto weekRange = WeekRangeCalculator.weekRangeOf(today); + + // 2. 첫 답변 날짜 조회 + LocalDate firstAnswerDate = answerEntryQueryRepository + .findFirstAnswerDateByUserId(userId) + .orElse(null); + + // 3. 첫 답변 날짜 ~ 오늘까지 전체 답변 날짜 조회 + List allAnswerDates; + if (firstAnswerDate == null) { + allAnswerDates = List.of(); // 신규 사용자 + } else { + allAnswerDates = answerEntryQueryRepository + .findAnswerDatesByUserIdAndDateBetween(userId, firstAnswerDate, today); + } + + // 4. 주간 답변 필터링 + List weeklyAnsweredDates = allAnswerDates.stream() + .filter(date -> !date.isBefore(weekRange.weekStartDate()) + && !date.isAfter(weekRange.weekEndDate())) + .sorted() + .toList(); + + // 5. Streak 계산 + long currentStreak = calculateCurrentStreak(today, allAnswerDates); + + // 6. 총 기록 일수 계산 + long totalDaysSinceStart = calculateTotalDaysSinceStart(today, firstAnswerDate); + + // 7. 응답 생성 + return new HomeResponse( + weeklyAnsweredDates, + currentStreak, + totalDaysSinceStart + ); + } + + private long calculateCurrentStreak(LocalDate today, List answerDates) { + if (answerDates == null || answerDates.isEmpty()) { + return 0; + } + + Set dateSet = new HashSet<>(answerDates); + + // 시작 날짜 결정: 오늘 답변 있으면 오늘부터, 없으면 어제부터 + LocalDate checkDate = dateSet.contains(today) ? today : today.minusDays(1); + + // 역순 연속 계산 + long streak = 0; + + while (dateSet.contains(checkDate)) { + streak++; + checkDate = checkDate.minusDays(1); + } + + return streak; + } + + private long calculateTotalDaysSinceStart(LocalDate today, LocalDate firstAnswerDate) { + if (firstAnswerDate == null || firstAnswerDate.isAfter(today)) { + return 0; + } + + return ChronoUnit.DAYS.between(firstAnswerDate, today) + 1; + } +} \ No newline at end of file diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java index b7a4742..75f1e79 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/core/repository/AnswerEntryQueryRepository.java @@ -117,4 +117,31 @@ Optional findByUserAndDate( @Param("userId") Long userId, @Param("date") LocalDate date ); + + /** + * 특정 기간 내 사용자가 답변한 날짜 목록 조회 (오름차순) + */ + @Query(""" + SELECT ae.date + FROM AnswerEntry ae + WHERE ae.user.id = :userId + AND ae.date >= :startDate + AND ae.date <= :endDate + ORDER BY ae.date ASC + """) + List findAnswerDatesByUserIdAndDateBetween( + @Param("userId") Long userId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + + /** + * 사용자의 첫 번째 답변 날짜 조회 + */ + @Query(""" + SELECT MIN(ae.date) + FROM AnswerEntry ae + WHERE ae.user.id = :userId + """) + Optional findFirstAnswerDateByUserId(@Param("userId") Long userId); } \ No newline at end of file