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