// 루프 전체 리스트 조회
@Override
@Transactional(readOnly = true)
public PageResponse<LoopWithCheckListResponse> getAllLoop(Pageable pageable, CurrentUserDto currentUser) {
checkPageSize(pageable.getPageSize());
// 1) 루프 페이지 조회 (DB에서 NULLS LAST 처리)
Page<Loop> mainPage = loopRepository.findByMemberIdWithOrder(currentUser.getId(), pageable);
// 2) 해당 페이지에 포함된 루프 ID 추출
List<Long> mainIds = mainPage.getContent().stream().map(Loop::getId).toList();
// 3) 체크리스트 벌크 조회 (DB에서 loop.id ASC, deadline ASC NULLS LAST 처리)
List<LoopCheckList> loopCheckLists = mainIds.isEmpty()
? List.of()
: loopCheckListRepository.findByLoopIdIn(mainIds);
// 4) 체크리스트를 loopId 기준으로 그룹핑
Map<Long, List<LoopCheckList>> subByMainId = new LinkedHashMap<>();
for (LoopCheckList sg : loopCheckLists) {
Long mid = sg.getLoop().getId();
subByMainId.computeIfAbsent(mid, k -> new ArrayList<>()).add(sg);
}
// 5) DTO 변환
List<LoopWithCheckListResponse> content = new ArrayList<>(mainPage.getNumberOfElements());
for (Loop mg : mainPage.getContent()) {
LoopResponse mainDto = convertToLoopResponse(mg);
List<LoopCheckListResponse> subDtos = subByMainId.getOrDefault(mg.getId(), List.of())
.stream()
.map(this::convertToCheckListResponse)
.toList();
content.add(LoopWithCheckListResponse.builder()
.loop(mainDto)
.build());
}
// 6) Page로 감싸서 반환
return PageResponse.of(new PageImpl<>(content, mainPage.getPageable(), mainPage.getTotalElements()));
}
// 루프 전체 리스트 조회
@Override
@Transactional(readOnly = true)
public PageResponse<LoopSimpleResponse> getAllLoop(Pageable pageable, CurrentUserDto currentUser) {
checkPageSize(pageable.getPageSize());
// 1. Loop 엔티티 페이지를 DB에서 조회
Page<Loop> loopPage = loopRepository.findByMemberIdWithOrder(currentUser.getId(), pageable);
List<Long> loopIds = loopPage.stream().map(Loop::getId).toList();
// 2. 모든 체크리스트를 한 번에 조회해서 Map으로 그룹핑
Map<Long, List<LoopCheckList>> checklistsMap = loopCheckListRepository.findByLoopIdIn(loopIds)
.stream()
.collect(Collectors.groupingBy(cl -> cl.getLoop().getId())); // Stream의 groupingBy를 사용해 한 줄로 그룹핑
// 3. 엔티티 페이지를 DTO 페이지로 직접 변환
Page<LoopSimpleResponse> simpleDtoPage = loopPage.map(loop ->
convertToSimpleResponse(loop, checklistsMap.getOrDefault(loop.getId(), List.of()))
);
return PageResponse.of(simpleDtoPage);
}
→ 모든 체크리스트를 한번에 조회하여 N+1 문제 해결 (loopCheckListRepository.findByLoopIdIn(loopIds))
→ 기존의 “5) DTO 변환”은 for문을 돌면서 수동으로 list를 만들었으나, Page.map() 함수를 사용하여 간편하게 엔티티가 담긴 페이지를 DTO가 담긴 페이지로 변환했다.
<aside> 💡
groupingBy
현재 모든 체크리스트를 한번에 조회했기에 각 루프의 id를 기준으로 그룹핑해줌.
Collectors.groupingBy(cl -> cl.getLoop().getId()) ← 분류 기준을 루프의 id로 설정
ex) 1번 루프의 체크리스트끼리 그룹핑해서 map으로 저장
</aside>
그룹 ID 생성하여 그룹화
→ String (UUID 사용) vs long (별도 테이블 생성)
선택: String(UUID)
→ 별도 테이블이 없기에 추가적인 INSERT가 없어서 더 빠름
→ 루프 식별 만을 위해 사용하기에 테이블로 관리하는 것은 과한 설계
switch-case 문으로 반복 규칙에 따른 비즈니스 로직 메서드 분리
LocalDate 클래스 사용
윤년 또는 다음 달의 마지막 날이 존재하지 않아 유효한 날짜가 아니게 될 경우
자바의 LocalDate.plusMonths()가 자동으로 유효한 날짜로 보정해줌.
→ 하지만, 여기서 문제 발생. 보정된 날짜로 plusMonths(1)을 했을 때, 해당 일자로 한달을 추가하게 됨.
ex) 3월31일->4월30일(보정)->5월30일
→ 해결 방법 2가지
보정을 원복해주는 조건문 추가
int dayOfMonth = start.getDayOfMonth(); //기준으로 둘 일 수를 저장 ex) 31일
if (date.getDayOfMonth() != dayOfMonth) { //보정으로 인하여 일 수가 달라진 경우 date = date.withDayOfMonth(date.lengthOfMonth()); //말일로 일 수 교체 }
시작일을 기준으로 plusMonths의 값을 1씩 더해가는 구조
monthsToAdd++; //매 반복문마다 1씩 더하기 (1달 후, 2달 후 ….) date = start.plusMonths(monthsToAdd); //매번 시작일을 기준으로 더해주기에 보정이 누적되지 않음.
<aside> 💡
LocalDate 클래스
→ java.time 패키지에 속한 날짜 클래스 (오직 ‘연-월-일’ 정보만 표현하는 불변 객체)
간단한 날짜 계산에 적합
LocalDate.now(): 당일 날짜
LocalDate.of(2025, 10, 31): 해당 날짜
date.isAfter(특정날짜): 오늘이 특정날짜 이후인가?
date.plusDays(n): n일 뒤 날짜
date.getDayOfWeek(): 요일 반환
date.with~ : 날짜 변경
반복 규칙 설정 - Loop Schedule
[옵션별 동작 규칙]
없음: 반복 없이 한 번만 실행되는 단일 루프, 캘린더에 한 번만 표시됨. 선택 시, 특정날짜 입력칸이 생김. 시작일/종료일 입력칸이 비활성화됨.
매주: 선택한 요일이 반복되는 루프. 선택 시, 요일 드롭다운이 생겨 선택 가능 (월~일)
매월: 선택한 날짜와 매월 동일한 날짜로 반복 생성 예: 매월 10일에 루프 자동 등록
매년: 선택한 날짜와 매년 동일한 날짜로 반복 생성
(추후에 수정으로 반복 옵션 변경 시 기존 일정은 새 규칙에 맞춰 갱신되어야 함.)
루프 시작일, 종료일 설정 - Loop Until