Skip to content

Conversation

@dpwls8984
Copy link
Collaborator

🔀 Pull Request

Schedule 1차 개발 구현하였습니다.

🏷 PR 타입(Type)

아래에서 이번 PR의 종류를 선택해주세요.

  • Feature (새로운 기능 추가)
  • Fix (버그 수정)
  • Refactor (기능 변화 없는 구조 개선)
  • Chore (환경 설정 / 빌드 / 기타 작업)
  • Docs (문서 작업)

🍗 관련 이슈

📝 개요(Summary)

이번에 구현한 기능 리스트는 다음과 같습니다.

  1. 월별 전체 조회: 팔로우한 모든 아티스트의 월별 일정 통합 조회
  2. 월별 개별 조회: 특정 아티스트 필터링 후 월별 일정 조회 (팔로우 여부 검증 포함)
  3. 일별 상세 조회: 특정 날짜의 상세 일정 팝업 조회 (전체/개별 필터링 지원)
  4. 다가오는 일정(Widget): 현재 시각 이후의 일정을 D-Day와 함께 시간순 조회 (Limit 지원)

🔧 코드 설명 & 변경 이유(Code Description)

1. DTO Projection을 통한 조회 성능 최적화 (N+1 방지)

JPA의 findAll 후 엔티티를 DTO로 변환하는 방식 대신, JPQL의 SELECT new ... 구문(DTO Projection)을 사용하여 필요한 데이터만 즉시 조회하도록 구현했습니다. Artist 정보를 가져오기 위한 불필요한 추가 쿼리(N+1 문제)를 원천 차단하고, Fetch Join보다 가벼운 방식으로 데이터를 조회하기 위함입니다.

예시: ScheduleRepository

@Query("""
    SELECT new back.kalender.domain.schedule.dto.response.MonthlyScheduleItem(
        s.id, s.artistId, a.name, s.title, ...
    )
    FROM Schedule s
    JOIN ArtistTmp a ON s.artistId = a.id
    WHERE s.artistId IN :artistIds ...
""")

2. 쿼리 성능을 위해 Index를 추가

일정 조회 시 artist_id로 필터링하고 schedule_time으로 범위 검색 및 정렬을 수행하는 패턴이 빈번하여 복합 인덱스를 적용했습니다.

@Table(name = "schedules", indexes = @Index(name = "idx_schedule_artist_time", columnList = "artistId, scheduleTime"))

3. getFollowedArtistIds 메서드 추출

getFollowingSchedules, getUpcomingEvents 등에서 팔로우 목록을 가져와서 -> 비어있는지 체크하고 -> ID 리스트로 변환하는 로직이 계속 반복되어 getFollowedArtistIds 메서드로 추출하여 반복을 줄였습니다.

4. date필드를 다시 삭제

schedule_time 필드 활용해서 날짜 데이터 받아오는 것이 혼선을 더 줄일 것이라고 판단하였습니다.

5. D-Day 계산 로직 추가

다가오는 일정 조회 시 DB 부하를 줄이고 비즈니스 로직의 유연성을 확보하기 위해, D-Day(daysUntilEvent) 필드는 DB 함수에 의존하지 않고, Service 계층에서 Java Time API를 사용하여 계산하도록 구현했습니다. Repository에서는 NULL을 반환받고 Service에서 값을 주입합니다.

6. 일별 조회 & 다가오는 일정 API의 단일화

초기 설계 시에는 [특정 날짜 조회]와 [다가오는 일정 조회] 기능에 전체 아티스트 조회 API와 특정 아티스트 조회 API로 각각 분리하는 방안을 고려했으나, 최종적으로 하나의 엔드포인트(GET /schedule/daily, /upcoming)에서 Optional 파라미터(artistId)로 분기 처리하는 방식을 채택했습니다.
두 경우 모두 반환되는 데이터 구조(Response DTO)가 동일하며, 비즈니스 로직의 차이는 오직 '조회 대상을 누구로 설정하느냐' 의 차이라서 파라미터로 구분하였습니다.

🧪 테스트 절차(Test Plan)

  • 다가오는 일정 조회 - 성공 (전체 아티스트 & D-Day 계산 검증)

  • 다가오는 일정 조회 - 성공 (특정 아티스트 필터링)

  • 다가오는 일정 조회 - 실패 (팔로우하지 않은 아티스트)

  • 다가오는 일정 조회 - 실패 (Limit 값 오류)

  • 월별 전체 조회 - 성공 (전체 아티스트)

  • 월별 전체 조회 - 실패 (유효하지 않은 월 입력)

  • 월별 개별 조회 - 실패 (팔로우하지 않은 아티스트 요청)

  • 월별 개별 조회 - 실패 (유효하지 않은 월 입력)

  • 여러 아티스트 중 특정 아티스트(BTS)만 조회 시, 정확히 해당 ID로만 필터링하여 요청한다.

  • 하루 상세 조회 - 실패 (날짜 포맷 오류)

  • 하루 상세 조회 - 성공 (전체 아티스트)

  • 하루 상세 조회 - 성공 (특정 아티스트 필터링)

  • 하루 상세 조회 - 실패 (팔로우하지 않은 아티스트 요청)

🔄 API 변경 / 흐름 영향(API & Flow Impact)

[다가오는 일정 조회] API가 추가되었습니다. (노션에 업데이트 완료)

GET /api/v1/schedule/upcoming

응답데이터 예시

{
  "upcomingEvents": [
    {
      "scheduleId": 205,
      "artistName": "aespa",
      "title": "팬사인회",
      "scheduleCategory": "FAN_SIGN",
      "scheduleTime": "2025-12-20T14:00:00",
      "performanceId": null,
      "link": "https://example.com",
      "daysUntilEvent": 5,
      "location": "코엑스"
    }
  ]
}

주요 기능 흐름

- 월별 전체 일정 조회 (GET /schedule/following)

  1. 유저 ID 기반 팔로우 목록 조회(Helper)
  2. 팔로우한 아티스트가 없으면 빈 리스트 반환(Early Return)
  3. 아티스트 ID 추출
  4. 해당 월의 시작/끝 날짜 계산
  5. DTO Projection 쿼리 실행 (한 번의 쿼리로 아티스트 이름까지 조회)
  6. 응답 반환

- 월별 개별 아티스트 일정 조회 (GET /schedule/artist/{artistId})

  1. 월 입력값 검증
  2. existsBy... 메서드로 팔로우 권한 단건 조회 (최적화)
  3. 권한 없으면 403 예외 발생
  4. 단일 아티스트 ID로 월별 일정 쿼리 재사용
  5. 응답 반환

- 일별 상세 일정 조회 (GET /schedule/daily)

  1. String 날짜 파싱 및 범위 계산(00:00 ~ 23:59)
  2. artistId 파라미터 존재 여부에 따라 [전체 팔로우 목록] vs [단일 아티스트] 타겟 설정
  3. 해당 날짜 범위의 일정 조회 (상세 정보 포함)
  4. 응답 반환

- 다가오는 일정 조회 (GET /schedule/upcoming)

  1. artistId 파라미터에 따라 타겟 설정
  2. 현재 시간(LocalDateTime.now()) 이후의 일정을 Pageable을 사용해 limit 개수만큼 조회
  3. Service 계층에서 Java Time API를 사용하여 D-Day 계산 및 데이터 주입
  4. 응답 반환

👀 리뷰 포인트(Notes for Reviewer)

  1. ServiceImpl, Repo, EntityDTO의 관계 위주로 봐주시면 감사하겠습니다.
  2. Batch size 방식이 아니라 DTO Projection 으로 사용했는데, Batch size로 통일 원하시면 편하게 말씀해주시길 바랍니다.
  3. 부족한 테스트 코드가 있다면 말씀해주세요.
  4. 혹시 DB가 계속 추적되고 있다면 말씀해주세요.

@dpwls8984 dpwls8984 self-assigned this Dec 10, 2025
@dpwls8984 dpwls8984 added the feat label Dec 10, 2025
@dpwls8984 dpwls8984 removed the feat label Dec 10, 2025
@BackSeungBeom
Copy link
Collaborator

반복되는 부분들은 따로 피드백 안 달았습니다! 동일한 이슈들 찾아서 전체에 반영하면 좋을 것 같습니다.

@Table(name = "artist_follows")
public class ArtistFollowTmp extends BaseEntityTmp {

//나중에 꼭 제거해야함
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO로 작성하면 좋을 것 같습니다.
예시)
// TODO: SecurityContext에서 가져오기

이렇게 써놓으면 intelliJ에서 찾기도 쉽고 따로 표시되서 보기도 좋습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 완료했습니다!
(해당 파일은 다른 도메인 파일 임시로 가져온 거라 나중에 삭제 예정입니다)

new ScheduleItem(108L, 2L, "BTS", "지민 생일", ScheduleCategory.BIRTHDAY, Optional.empty(), LocalDateTime.of(year, month, 18, 0, 0), LocalDate.of(year, month, 18))
);
MonthlySchedulesResponse response = new MonthlySchedulesResponse(dummyList);
Long userId = 1L; // 임시 userId
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO 사용

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 완료했습니다!

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

public record DailyScheduleItem(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dto 명들 보니 request를 item으로 통일 하신 것 같은데 request와 response 구분 쉽도록 request를 끝에 붙여서 통일하면 좋을 것 같습니다. 다른 도메인들도 이렇게 되어있어서 맞추는 게 좋을 것 같아요!
ex) DailyScheduleItemRequest

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Item -> Response
Response -> ListReponse
로 변경 및 통일 완료하였습니다!

);

@Query("""
SELECT new back.kalender.domain.schedule.dto.response.DailyScheduleItem(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금은 경로가 다 써있는데 import하고 DailyScheduleItem으로 사용할 수 있을 것 같습니다. 코드가 깔끔해질 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 이 부분은 저도 코드가 너무 지저분하다고 생각해서 알아봤는데, JPQL에서는 import문이 작동되지 않는다고 하더라구요.. (자바 입장에서는 그냥 String 문이라서) 그래서 우선 지금 상태를 유지하려합니다. 혹시 더 좋은 방법이 있다면 추천해주시면 감사하겠습니다!

Copy link
Collaborator

@junseokPP junseokPP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다

log.info("[Schedule] [GetPerArtist] 아티스트별 월별 일정 조회 시작 - userId={}, artistId={}, year={}, month={}",
userId, artistId, year, month);

if (month < 1 || month > 12) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복되는 메서드는 공통 메서드로 빼면 좋을 듯 합니다.

private void validateMonth(int month) { if (month < 1 || month > 12) { throw new ServiceException(ErrorCode.INVALID_INPUT_VALUE); } }


log.debug("[Schedule] [GetPerArtist] 팔로우 관계 확인 완료 - userId={}, artistId={}", userId, artistId);

try {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

값검증을 try,catch로 구분하고 있던데 dto에서 @Vaild검증으로 앞단에서 검증하는 것도 좋아보입니다. try-catch는 네트워크나 외부연동일때 사용하는 걸로 알고있어서 말해봅니다.

Long id = artistId.get();

boolean isFollowing = artistFollowRepository.existsByUserIdAndArtistId(userId, id);
if (!isFollowing) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 if문도 반복사용이던데 공통 메서드로 빼는건 어떤지 물어봅니다.

private void validateFollowing(Long userId, Long artistId) { if (!artistFollowRepository.existsByUserIdAndArtistId(userId, artistId)) { throw new ServiceException(ErrorCode.ARTIST_NOT_FOLLOWED); } }

private List<Long> getFollowedArtistIds(Long userId) {
List<ArtistFollowTmp> follows = artistFollowRepository.findAllByUserId(userId);

if (follows.isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stream이 애초에 값이 없으면 빈리스트를 넣는걸로 알고있는데 if문 제거해도 될 듯 합니다.

Copy link
Collaborator

@BackSeungBeom BackSeungBeom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다

@dpwls8984 dpwls8984 merged commit 5c4d60c into dev Dec 11, 2025
1 check passed
@dpwls8984 dpwls8984 deleted the feature/#13/schedule/v1 branch December 11, 2025 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants