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
@@ -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<ApiResponseDto<HomeResponse>> getHomeData(
@AuthenticationPrincipal UserPrincipal principal
) {
HomeResponse response = homeQueryService.getHomeData(principal.getId());
return ApiResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<LocalDate> answeredDates,

@Schema(description = "현재 연속 답변 일수 (오늘 답변 있으면 오늘까지, 없으면 어제까지)", example = "15")
long streakCount,

@Schema(description = "첫 답변 이후 경과 일수 (N일째 기록 중)", example = "20")
long totalRecordDays
) {
}
Original file line number Diff line number Diff line change
@@ -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<LocalDate> allAnswerDates;
if (firstAnswerDate == null) {
allAnswerDates = List.of(); // 신규 사용자
} else {
allAnswerDates = answerEntryQueryRepository
.findAnswerDatesByUserIdAndDateBetween(userId, firstAnswerDate, today);
}

// 4. 주간 답변 필터링
List<LocalDate> 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<LocalDate> answerDates) {
if (answerDates == null || answerDates.isEmpty()) {
return 0;
}

Set<LocalDate> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,31 @@ Optional<SearchAnswerEntryDto> 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<LocalDate> 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<LocalDate> findFirstAnswerDateByUserId(@Param("userId") Long userId);
}