1. 관리자페이지에서 상영시간을 등록하기
12일을 첫날로 잡고
페이지를 호출하면 12일에 관한 상영관을 업로드 시켰다.
controller
@GetMapping("/admin/showtime")
public String showtime(HttpServletRequest request, Model model){
Admin sessionAdmin = (Admin) session.getAttribute("sessionAdmin");
int cinemaId = 1;
LocalDate startDate = LocalDate.of(2024, 9, 12);
LocalDate today = startDate; // 12일을 "오늘"로 취급
List<Map<String, Object>> dateList = new ArrayList<>();
for (int i = 0; i < 7; i++) {
LocalDate currentDate = startDate.plusDays(i);
Map<String, Object> dateMap = new HashMap<>();
dateMap.put("formattedDate", currentDate.format(DateTimeFormatter.ofPattern("dd")));
dateMap.put("formattedDay", currentDate.format(DateTimeFormatter.ofPattern("E", Locale.KOREAN)));
dateMap.put("isToday", currentDate.equals(today)); // 12일을 기준으로 "오늘" 처리
dateList.add(dateMap);
}
AdminResponse.CinemaWithScreensDTO cinemaSchedule = adminService.상영관별상영스케줄(sessionAdmin, startDate);
model.addAttribute("dates", dateList); // 생성된 날짜 리스트를 모델로 추가
request.setAttribute("model",cinemaSchedule);
return "admin/showtime";
}mutach 파일에서 날짜에 관한 내용을 사용하기위하여 Map을 사용하여 formattedDate, formattedDay, isToday를 추가하였다.
service
public AdminResponse.CinemaWithScreensDTO 상영관별상영스케줄(Admin sessionAdmin, LocalDate selectedDate) {
Long cinemaId= 1L;
Cinema cinema = cinemaRepository.findById(cinemaId)
.orElseThrow(() -> new IllegalArgumentException("Cinema not found"));
// DTO 생성 (Cinema 정보만 포함)
AdminResponse.CinemaWithScreensDTO cinemaWithScreensDTO = new AdminResponse.CinemaWithScreensDTO(cinema);
// 2. Screen 정보 조회
List<Long> screenIds = new ArrayList<>();
for (Screen screen : cinema.getScreens()) {
screenIds.add(screen.getId());
System.out.println("----------------");
System.out.println(screen.getId());
}
// 3. 상영관과 상영시간을 fetch join으로 한 번에 조회 + 시간을 가져오기
List<Screen> screensWithShowtimesAndDate = screenRepository.findScreensWithShowtimesByIdsAndDate(screenIds, selectedDate);
//TODO 정리하기 시간을 가지고와서 밑에거는 안씀
// 3. 상영관과 상영시간을 fetch join으로 한 번에 조회
List<Screen> screensWithShowtimes = screenRepository.findScreensWithShowtimesByIds(screenIds);
// 4. 상영관과 상영시간을 DTO에 추가
for (Screen screen : screensWithShowtimesAndDate) {
System.out.println(screen.getId());
AdminResponse.ScreenDTO screenDTO = new AdminResponse.ScreenDTO(screen);
cinemaWithScreensDTO.addScreen(screen); // Screen 추가
}
return cinemaWithScreensDTO;
}우선 영화관을 조회하는 로직인데 우선 1번 영화관을 고정하여 가지고왔다.
영화관 1번에 관한 내용을 db에서 받아와고 이 영화관에 ontwomany로 등록되어있는 screen의 id를 list로 가지고와 in절을 사용하여 상영시간에 관한 db를 가지고왔다.
이때 12일이면 12일에 관한 정보만을 가지고와야하니 파라미터로 LocalDate selectedDate를 받아 넘겨주었다.
repository
public interface ScreenRepository extends JpaRepository<Screen, Long> {
//TODO MYSQL로 바꾸면 바꿔야함
/*
* MySQL은 DATE() 함수를 지원하지만, H2나 다른 데이터베이스에서는 이를 지원하지 않을 수 있습니다.
대안으로 CAST나 TRUNC 함수를 사용하여 시간 부분을 제거하고 날짜만 비교할 수 있습니다.
* 여기서는 H2 데이터베이스에서 사용할 수 있는 CAST를 이용한 방법*/
@Query("SELECT s FROM Screen s LEFT JOIN FETCH s.showtimes st LEFT JOIN FETCH st.movie "
+ "WHERE s.id IN :screenIds AND (st.startedAt IS NULL OR CAST(st.startedAt AS date) = :selectedDate)")
List<Screen> findScreensWithShowtimesByIdsAndDate(@Param("screenIds") List<Long> screenIds,
@Param("selectedDate") LocalDate selectedDate);파라미터로 가져온 상영관id와 참고하는 날짜를 사용하여 where문을 만들었고
상영시간이 없어도 상영관이 등록이 되어있다면 나와야하는 페이지이므로 LEFT JOIN FETCH를 사용하여 상영시간이 없어도 상영관을 가지고 오게하였다.
ResponseDTO
package shop.mtcoding.filmtalk.admin;
import lombok.Data;
import shop.mtcoding.filmtalk.cinema.Cinema;
import shop.mtcoding.filmtalk.movie.Movie;
import shop.mtcoding.filmtalk.screen.Screen;
import shop.mtcoding.filmtalk.showtime.Showtime;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
public class AdminResponse {
@Data
public static class movieDTO{
private Long movieId;
private String movieName;
public movieDTO(Movie movie) {
this.movieId = movie.getId();
this.movieName = movie.getMovieNm();
}
}
@Data
public static class CinemaWithScreensDTO{
private Long cinemaid;
private String cinemaName;
private List<ScreenDTO> screens = new ArrayList<>();;
public CinemaWithScreensDTO(Cinema cinemaPG) {
this.cinemaid = cinemaPG.getId();
this.cinemaName = cinemaPG.getName();
// this.screens = cinema.getScreens();
}
// Screen 정보를 DTO에 추가하는 메서드
public void addScreen(Screen screen) {
ScreenDTO screenDTO = new ScreenDTO(screen);
this.screens.add(screenDTO);
}
}
@Data
public static class ScreenDTO{
private Long screenId;
private String screenName;
private List<ShowtimeDTO> showtimes;
public ScreenDTO(Screen screenPG) {
this.screenId = screenPG.getId();
this.screenName = screenPG.getName();
showtimes = new ArrayList<>();
for (Showtime showtime : screenPG.getShowtimes()){
showtimes.add(new ShowtimeDTO(showtime));
}
}
}
@Data
public static class ShowtimeDTO{
private Long showtimeId;
private String movieName;
private Integer runtime;
private String startedAt;
public ShowtimeDTO(Showtime showtimePG) {
this.showtimeId = showtimePG.getId();
this.movieName = showtimePG.getMovie().getMovieNm();
this.runtime = showtimePG.getMovie().getRuntime();
this.startedAt = new SimpleDateFormat("HH:mm").format(showtimePG.getStartedAt());
}
}
}
영화관을 우선으로 받고 lazy로딩으로 중복되는 데이터가 들어오는걸 확인하여 영화관따로 상영관이랑 상영시간을 따로 dto로 받기로 결정했다.

집고 넘어가기
@Query("SELECT s FROM Screen s LEFT JOIN FETCH s.showtimes st LEFT JOIN FETCH st.movie "
+ "WHERE s.id IN :screenIds AND (st.startedAt IS NULL OR CAST(st.startedAt AS date) = :selectedDate)")
List<Screen> findScreensWithShowtimesByIdsAndDate(@Param("screenIds") List<Long> screenIds,
@Param("selectedDate") LocalDate selectedDate);1.H2와 Mysql의 차이로 인한 cast사용 - 추후 변경필요
- H2 데이터베이스에서는
CAST구문을 사용하여Timestamp값을Date로 변환하는 방식으로 필터링이 가능합니다.
- MySQL에서는
DATE()함수를 사용하여Timestamp에서 날짜 부분만 추출하여 비교하는 것이 일반적입니다.
- H2로 테스트할 때는
CAST(st.startedAt AS date)을 사용하고, MySQL에서는DATE(st.startedAt)로 변경해야 한다.
2. JPQL의 제한 사항:
- JPQL에서는
JOIN에 대한ON절을 활용할 수 있는 기능이 제한적입니다. 따라서,ON절에서의 조건 필터링을 수행하지 못하는 경우,WHERE절에서 필터링해야한다.
- 따라서, 상영시간이 없는 상영관도 포함하려면
LEFT JOIN을 사용하고, 상영시간이 있는 경우에만 추가 필터링을 해야 합니다.
3. LEFT JOIN의 한계:
LEFT JOIN FETCH는 상영시간이 없는 상영관도 가져옵니다. 하지만,WHERE절에서st.startedAt조건으로 필터링할 경우, 상영시간이 없는 상영관들이 제외됩니다. 이는 WHERE 절에서 필터링이 JOIN 후에 적용되기 때문입니다.
- JPQL의 한계로 인해
ON절에서 필터링하는 방식 대신WHERE절에서 필터링을 수행해야 했습니다. 이를 해결하기 위해서는 상영시간이 없는 상영관도 포함시키면서도st.startedAt이NULL이거나, 선택된 날짜와 일치하는 데이터를 가져오도록 조건을 설정해야 합니다.
4. 쿼리 작성 방식:
- 최종 쿼리에서 상영시간이 없는 상영관도 가져오고, 상영시간이 있는 경우 해당 날짜와 일치하는 상영관만 가져오도록 다음과 같은 조건을 사용했습니다:
@Query("SELECT s FROM Screen s LEFT JOIN FETCH s.showtimes st LEFT JOIN FETCH st.movie "
+ "WHERE s.id IN :screenIds AND (st.startedAt IS NULL OR CAST(st.startedAt AS date) = :selectedDate)")
List<Screen> findScreensWithShowtimesByIdsAndDate(@Param("screenIds") List<Long> screenIds,
@Param("selectedDate") LocalDate selectedDate);st.startedAt IS NULL: 상영시간이 없는 경우를 처리하여 상영관이 제외되지 않도록 합니다.
CAST(st.startedAt AS date) = :selectedDate: 상영시간이 있는 경우, 해당 날짜와 일치하는 상영시간만 가져옵니다.
5. 상영시간이 없는 상영관도 가져오는 이유:
LEFT JOIN FETCH를 사용하여 상영시간이 없는 상영관들도 포함합니다.
- 그러나
WHERE절에서 날짜 조건을 걸면, 상영시간이 없는 상영관들이 필터링되어 제외될 수 있으므로, 이를 방지하기 위해 상영시간이 없는 경우에도 가져오기 위한 조건st.startedAt IS NULL을 추가했습니다.
6. List로 처리하는 이유:
- JPQL에서
LEFT JOIN FETCH를 사용하면 여러 개의 상영시간을 가진 데이터를 가져올 수 있기 때문에, 리스트로 반환한 후 각 상영관에서 필요한 마지막 상영시간을 처리할 수 있습니다.
- 상영시간이 여러 개인 경우, 결과를 리스트로 받아서 별도로 필터링을 수행해야 합니다.
이 내용을 기반으로 최종 쿼리는 MySQL로 이전할 때
CAST(st.startedAt AS date)를 DATE(st.startedAt)로 변경해야 하며, 상영시간이 없는 상영관도 포함시키면서 필터링이 필요한 경우에는 WHERE 절에서 적절히 처리하도록 설계되었다.Share article