Repository: SOPT-all/36-COLLABORATION-SERVER-WATCHAPEDIA
Pull request #23: [FEAT] 영화 상세정보 조회 API 구현
Author: login: bykbyk0401
Labels: name: :sparkles: feature
description: 기능 구현
Created: 2025-05-19T10:08:34Z | Merged: 2025-05-19T15:35:40Z

## ✨ Issue

- #19 

<br>

## 📕 Summary

- 영화 ID를 받아 해당 영화의 상세 정보를 조회하는 API를 구현하였습니다.
- 홈에서 영화를 눌렀을 때 영화에 대한 자세한 정보를 모두 조회하여 응답합니다.
- Integer 타입으로 저장되어 있던 실행시간(runningTime)을 "시간 분" 형태의 문자열로 변환하여 제공합니다.

<br>

## 🖥️ Describe your code
- **영화 상세 정보 조회 API 구현**
    - `GET /posts/{postId}` 엔드포인트를 통해 영화 상세 정보를 조회할 수 있습니다.
    - 영화 기본 정보(제목, 개봉일, 연령 제한 등)와 함께 장르, 출연진, 이미지 정보를 포함합니다.
    - 존재하지 않는 영화 ID를 요청할 경우 적절한 예외 처리를 제공합니다.

- **시간 포맷팅 유틸리티 분리**
    - 실행시간(분)을 "시간 분" 형식으로 변환하는 클래스를 구현하였습니다. `TimeFormatUtil`
    - 예: 90분 → "1시간 30분"

- **국가 정보 Enum 처리**
    - 우선은 enum을 한글 값으로 정의하여 직관적인 데이터 관리가 가능하도록 구현하였습니다. `FilmCountry`
    - KOR, USA 등의 코드값으로 변경하고 표시 이름을 별도 관리하는 방식이 나을지에 대해 고민중입니다.
      - 재연님의 생각이 궁금합니다!

<br>

## ✏️ What I Learned
- DTO에서의 비즈니스 로직 처리
    - 현재 응답 데이터 구성의 책임을 DTO에 부여하여 `MovieDetailResponseDTO`에서 여러 엔티티의 데이터를 조합하고 변환하는 작업을 수행하고 있습니다. 
    - 복잡도가 증가할 경우에 대해 고민이 되긴 하는데, 추후 다시 생각해보겠습니다.

- Enum 설계에 대한 고민
    - 표시 이름은 별도로 관리하는 것이 더 유연한 설계가 될 것으로 판단됩니다.





Diff:
diff --git a/src/main/java/or/sopt/soptwatcha/controller/MovieController.java b/src/main/java/or/sopt/soptwatcha/controller/MovieController.java
index 3e39edc..e9455d6 100644
--- a/src/main/java/or/sopt/soptwatcha/controller/MovieController.java
+++ b/src/main/java/or/sopt/soptwatcha/controller/MovieController.java
@@ -7,20 +7,28 @@
 import or.sopt.soptwatcha.dto.response.GetMovieSoonResponseDTO;
 import or.sopt.soptwatcha.dto.response.GetMovieTopRankingResponseDTO;
 import or.sopt.soptwatcha.dto.response.GetPreferenceMoviesListResponse;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
 import or.sopt.soptwatcha.service.MovieService;
+import or.sopt.soptwatcha.service.MovieServiceImpl;
 import org.springframework.web.bind.annotation.*;
 
 @RestController
-@RequestMapping("")
+@RequestMapping("/posts")
 @RequiredArgsConstructor
 @Tag(name = "영화 관련 API")
 public class MovieController {
 
-
     private final MovieService movieService;
 
+    @GetMapping("/{postId}")
+    @Operation(summary = "영화 상세 정보 조회 API")
+    public BaseResponse<MovieDetailResponseDTO> getMovieDetail(@PathVariable Long postId) {
+        MovieDetailResponseDTO response = movieService.getMovieDetail(postId);
+        return BaseResponse.ok(response);
+    }
+
 
-    @GetMapping("posts/preference/{commentId}")
+    @GetMapping("/preference/{commentId}")
     @Operation(summary = "코멘트를 남긴 영화 기반 추천 API")
     public BaseResponse<GetPreferenceMoviesListResponse> getPreferenceMovies(@PathVariable Long commentId) {
 
@@ -30,7 +38,7 @@ public BaseResponse<GetPreferenceMoviesListResponse> getPreferenceMovies(@PathVa
     }
 
 
-    @GetMapping("posts/ranking")
+    @GetMapping("/ranking")
     @Operation(summary = "내가 좋아할 만한 작품 랭킹 API",
     description = "작품에 따른 예상 별점으로 다섯개의 작품 리스트를 반환합니다")
     public BaseResponse<GetMovieTopRankingResponseDTO.GetMovieTopRankingResponseListDTO> getRankingMovies() {
@@ -41,7 +49,7 @@ public BaseResponse<GetMovieTopRankingResponseDTO.GetMovieTopRankingResponseList
     }
 
 
-    @GetMapping("posts/soon")
+    @GetMapping("/soon")
     @Operation(summary = "개봉 예정작 조회 API",
     description = "개봉예정일과 가장 가까운 작품 다섯개를 리스트로 반환합니다 <br><br> **movieType**에는 영화라면 MOVIE,시리즈라면 SERIES 를 넣어주세요")
     public BaseResponse<GetMovieSoonResponseDTO.GetMovieSoonResponseListDTO> getSoonMovies(@RequestParam String movieType) {
diff --git a/src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java b/src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java
index 3cf62b2..68b42b9 100644
--- a/src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java
+++ b/src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java
@@ -1,5 +1,5 @@
 package or.sopt.soptwatcha.domain.common.enums;
 
 public enum FilmCountry {
-    A
+    한국, 미국, 영국, 프랑스, 일본, 중국
 }
diff --git a/src/main/java/or/sopt/soptwatcha/domain/common/enums/Role.java b/src/main/java/or/sopt/soptwatcha/domain/common/enums/Role.java
index d1fb0cb..755a8d4 100644
--- a/src/main/java/or/sopt/soptwatcha/domain/common/enums/Role.java
+++ b/src/main/java/or/sopt/soptwatcha/domain/common/enums/Role.java
@@ -1,5 +1,5 @@
 package or.sopt.soptwatcha.domain.common.enums;
 
 public enum Role {
-    A
+    DIRECTOR, ACTOR
 }
diff --git a/src/main/java/or/sopt/soptwatcha/dto/response/ArtistResponseDTO.java b/src/main/java/or/sopt/soptwatcha/dto/response/ArtistResponseDTO.java
new file mode 100644
index 0000000..4ff5490
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/dto/response/ArtistResponseDTO.java
@@ -0,0 +1,26 @@
+package or.sopt.soptwatcha.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import or.sopt.soptwatcha.domain.common.enums.Role;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ArtistResponseDTO {
+    private Role role;
+    private String name;
+    private String character;
+
+    public static ArtistResponseDTO from(MovieArtist movieArtist) {
+        return ArtistResponseDTO.builder()
+                .role(movieArtist.getRole())
+                .name(movieArtist.getArtist().getName())
+                .character(movieArtist.getCharacterName())
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/or/sopt/soptwatcha/dto/response/MovieDetailResponseDTO.java b/src/main/java/or/sopt/soptwatcha/dto/response/MovieDetailResponseDTO.java
new file mode 100644
index 0000000..652a1a2
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/dto/response/MovieDetailResponseDTO.java
@@ -0,0 +1,76 @@
+package or.sopt.soptwatcha.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import or.sopt.soptwatcha.domain.MovieGenre;
+import or.sopt.soptwatcha.domain.MovieImage;
+import or.sopt.soptwatcha.domain.common.enums.MovieImageType;
+import or.sopt.soptwatcha.util.TimeFormatUtil;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MovieDetailResponseDTO {
+    private Long id;
+    private String title;
+    private String engTitle;
+    private List<String> genres;
+    private Integer releaseYear;
+    private String runningTime;
+    private Integer ageLimit;
+    private String detail;
+    private String posterImage;
+    private String detailImage;
+    private String country;
+    private List<ArtistResponseDTO> artists;
+
+    public static MovieDetailResponseDTO from(Movie movie, 
+                                             List<MovieGenre> movieGenres, 
+                                             List<MovieArtist> movieArtists, 
+                                             List<MovieImage> movieImages) {
+        
+        List<String> genreList = movieGenres.stream()
+                .map(movieGenre -> movieGenre.getGenre().getValue())
+                .collect(Collectors.toList());
+
+        List<ArtistResponseDTO> artistList = movieArtists.stream()
+                .map(ArtistResponseDTO::from)
+                .collect(Collectors.toList());
+
+        String posterImageUrl = movieImages.stream()
+                .filter(image -> image.getMovieImageType() == MovieImageType.POSTER)
+                .findFirst()
+                .map(MovieImage::getImageLink)
+                .orElse(null);
+
+        String detailImageUrl = movieImages.stream()
+                .filter(image -> image.getMovieImageType() == MovieImageType.DESCRIPTION)
+                .findFirst()
+                .map(MovieImage::getImageLink)
+                .orElse(null);
+
+        return MovieDetailResponseDTO.builder()
+                .id(movie.getId())
+                .title(movie.getTitle())
+                .engTitle(movie.getEngTitle())
+                .genres(genreList)
+                .releaseYear(movie.getReleaseYear().getYear())
+                .runningTime(TimeFormatUtil.formatMinutesToHoursAndMinutes(movie.getRunningTime()))
+                .ageLimit(movie.getAgeLimit())
+                .detail(movie.getDetails())
+                .posterImage(posterImageUrl)
+                .detailImage(detailImageUrl)
+                .country(movie.getFilmCountry().name())
+                .artists(artistList)
+                .build();
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/or/sopt/soptwatcha/repository/GenreRepository.java b/src/main/java/or/sopt/soptwatcha/repository/GenreRepository.java
new file mode 100644
index 0000000..0517716
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/repository/GenreRepository.java
@@ -0,0 +1,7 @@
+package or.sopt.soptwatcha.repository;
+
+import or.sopt.soptwatcha.domain.Genre;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface GenreRepository extends JpaRepository<Genre, Long> {
+}
\ No newline at end of file
diff --git a/src/main/java/or/sopt/soptwatcha/repository/MovieArtistRepository.java b/src/main/java/or/sopt/soptwatcha/repository/MovieArtistRepository.java
new file mode 100644
index 0000000..db5260d
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/repository/MovieArtistRepository.java
@@ -0,0 +1,11 @@
+package or.sopt.soptwatcha.repository;
+
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface MovieArtistRepository extends JpaRepository<MovieArtist, Long> {
+    List<MovieArtist> findAllByMovie(Movie movie);
+}
\ No newline at end of file
diff --git a/src/main/java/or/sopt/soptwatcha/repository/MovieGenreRepository.java b/src/main/java/or/sopt/soptwatcha/repository/MovieGenreRepository.java
new file mode 100644
index 0000000..7ce35d1
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/repository/MovieGenreRepository.java
@@ -0,0 +1,11 @@
+package or.sopt.soptwatcha.repository;
+
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieGenre;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface MovieGenreRepository extends JpaRepository<MovieGenre, Long> {
+    List<MovieGenre> findAllByMovie(Movie movie);
+}
\ No newline at end of file
diff --git a/src/main/java/or/sopt/soptwatcha/repository/MovieImageRepository.java b/src/main/java/or/sopt/soptwatcha/repository/MovieImageRepository.java
index 09e15bc..dde4bdd 100644
--- a/src/main/java/or/sopt/soptwatcha/repository/MovieImageRepository.java
+++ b/src/main/java/or/sopt/soptwatcha/repository/MovieImageRepository.java
@@ -1,11 +1,14 @@
 package or.sopt.soptwatcha.repository;
 
+import or.sopt.soptwatcha.domain.Movie;
 import or.sopt.soptwatcha.domain.MovieImage;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
 import java.util.Optional;
 
 public interface MovieImageRepository extends JpaRepository<MovieImage, Long> {
 
     Optional<MovieImage> findByFileName(String filename);
+    List<MovieImage> findAllByMovie(Movie movie);
 }
diff --git a/src/main/java/or/sopt/soptwatcha/service/MovieService.java b/src/main/java/or/sopt/soptwatcha/service/MovieService.java
index da22727..bc3a841 100644
--- a/src/main/java/or/sopt/soptwatcha/service/MovieService.java
+++ b/src/main/java/or/sopt/soptwatcha/service/MovieService.java
@@ -3,7 +3,7 @@
 import or.sopt.soptwatcha.dto.response.GetMovieSoonResponseDTO;
 import or.sopt.soptwatcha.dto.response.GetMovieTopRankingResponseDTO;
 import or.sopt.soptwatcha.dto.response.GetPreferenceMoviesListResponse;
-import org.springframework.stereotype.Service;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
 import org.springframework.transaction.annotation.Transactional;
 
 public interface MovieService {
@@ -16,4 +16,7 @@ public interface MovieService {
 
     @Transactional(readOnly = true)
     GetMovieSoonResponseDTO.GetMovieSoonResponseListDTO getSoon(String movieType);
+
+    @Transactional(readOnly = true)
+    MovieDetailResponseDTO getMovieDetail(Long postId);
 }
diff --git a/src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java b/src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java
index 94dc920..897b701 100644
--- a/src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java
+++ b/src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java
@@ -25,7 +25,23 @@ public class MovieServiceImpl implements MovieService {
     private final CommentRepository commentRepository;
     private final CommentKeywordRepository commentKeywordRepository;
     private final MovieRepository movieRepository;
+    private final MovieGenreRepository movieGenreRepository;
+    private final MovieArtistRepository movieArtistRepository;
+    private final MovieImageRepository movieImageRepository;
 
+    
+    @Override
+    @Transactional(readOnly = true)
+    public MovieDetailResponseDTO getMovieDetail(Long movieId) {
+        Movie movie = movieRepository.findById(movieId)
+                .orElseThrow(() -> new CustomException(ErrorCode.INVALID_REQUEST, "해당 영화를 찾을 수 없습니다."));
+
+        List<MovieGenre> movieGenres = movieGenreRepository.findAllByMovie(movie);
+        List<MovieArtist> movieArtists = movieArtistRepository.findAllByMovie(movie);
+        List<MovieImage> movieImages = movieImageRepository.findAllByMovie(movie);
+
+        return MovieDetailResponseDTO.from(movie, movieGenres, movieArtists, movieImages);
+    }
 
     @Override
     @Transactional(readOnly = true)
diff --git a/src/main/java/or/sopt/soptwatcha/util/TimeFormatUtil.java b/src/main/java/or/sopt/soptwatcha/util/TimeFormatUtil.java
new file mode 100644
index 0000000..8c970a6
--- /dev/null
+++ b/src/main/java/or/sopt/soptwatcha/util/TimeFormatUtil.java
@@ -0,0 +1,17 @@
+package or.sopt.soptwatcha.util;
+
+public class TimeFormatUtil {
+    
+    public static String formatMinutesToHoursAndMinutes(int minutes) {
+        int hours = minutes / 60;
+        int remainingMinutes = minutes % 60;
+        
+        if (hours > 0 && remainingMinutes > 0) {
+            return hours + "시간 " + remainingMinutes + "분";
+        } else if (hours > 0) {
+            return hours + "시간";
+        } else {
+            return remainingMinutes + "분";
+        }
+    }
+}
\ No newline at end of file


Reviews:
user: login: gdbs1107
body: 충돌이 발생했네요...! 제 생각에 `@RequestMapping("/posts")`은 영경님이 해주신대로 가져가고 `@Transactional(readOnly = true)` 는 메서드 단으로 내리는게 좋지 않을까 싶습니다. 그거 말고는 그대로 붙이면 될 것 같아요.

병합 충돌 해결하기 어려운 부분 생기면 말씀해주세요!! 제가 도와드릴 수 있는 부분까지 최선을 다해볼게요! 고생많으셨습니다 ㅎㅎ
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T10:54:08Z
user: login: bykbyk0401
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T15:25:36Z
user: login: bykbyk0401
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T15:26:12Z
user: login: bykbyk0401
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T15:33:57Z

Review comments:
diff_hunk: @@ -0,0 +1,28 @@
+package or.sopt.soptwatcha.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import or.sopt.soptwatcha.common.response.BaseResponse;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
+import or.sopt.soptwatcha.service.MovieServiceImpl;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/posts")
+@RequiredArgsConstructor
+@Tag(name = "영화 관련 API")
path: src/main/java/or/sopt/soptwatcha/controller/MovieController.java
user: login: gdbs1107
body: `@RequestMapping("/posts")` 이렇게 엔드포인트를 관리하면 중복이 줄어들어 편리하지만 post 단건 조회나 저장 시에도 엔드포인트가 `posts`로 시작하게 됩니다. 자원을 명시해야하는 것이 restful한 설계라고 생각하여 일부러 넣지 않았지만 !

다시 찾아보니 명사를 복수형으로 관리하는게 일반적인 관례라고 하네요...! 제가 수정하겠습니다...!
created_at: 2025-05-19T10:34:08Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,5 @@
 package or.sopt.soptwatcha.domain.common.enums;
 
 public enum FilmCountry {
-    A
+    한국, 미국, 영국, 프랑스, 일본, 중국
path: src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java
user: login: gdbs1107
body: 개인적인 생각으로는 KOR,USA로 관리하는게 좋지 않을까 싶습니다! 이러면 한글로 파라미터를 받거나, 전환하는 로직이 있어야하는데 그렇게 하는 것보단 영어로 두는게 보다 관례적인 구현이지 않을까 싶습니다.

그리고 실제 국적은 description으로 관리를 하면 좋을 것 같습니다!
created_at: 2025-05-19T10:35:57Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,76 @@
+package or.sopt.soptwatcha.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import or.sopt.soptwatcha.domain.MovieGenre;
+import or.sopt.soptwatcha.domain.MovieImage;
+import or.sopt.soptwatcha.domain.common.enums.MovieImageType;
+import or.sopt.soptwatcha.util.TimeFormatUtil;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MovieDetailResponseDTO {
+    private Long id;
+    private String title;
+    private String engTitle;
+    private List<String> genres;
+    private Integer releaseYear;
+    private String runningTime;
+    private Integer ageLimit;
+    private String detail;
+    private String posterImage;
+    private String detailImage;
+    private String country;
+    private List<ArtistResponseDTO> artists;
+
+    public static MovieDetailResponseDTO from(Movie movie, 
+                                             List<MovieGenre> movieGenres, 
+                                             List<MovieArtist> movieArtists, 
+                                             List<MovieImage> movieImages) {
+        
+        List<String> genreList = movieGenres.stream()
+                .map(movieGenre -> movieGenre.getGenre().getValue())
+                .collect(Collectors.toList());
+
+        List<ArtistResponseDTO> artistList = movieArtists.stream()
+                .map(ArtistResponseDTO::from)
+                .collect(Collectors.toList());
+
+        String posterImageUrl = movieImages.stream()
+                .filter(image -> image.getMovieImageType() == MovieImageType.POSTER)
+                .findFirst()
+                .map(MovieImage::getImageLink)
+                .orElse(null);
+
+        String detailImageUrl = movieImages.stream()
+                .filter(image -> image.getMovieImageType() == MovieImageType.DESCRIPTION)
+                .findFirst()
+                .map(MovieImage::getImageLink)
+                .orElse(null);
path: src/main/java/or/sopt/soptwatcha/dto/response/MovieDetailResponseDTO.java
user: login: gdbs1107
body: 그러게요 고민이 되는 문제인 것 같아요! 저는 다 파싱해서 변환 메서드에 던지는 편인데, 이렇게 해야 오히려 일관성있으려나 싶기도 하구요.
created_at: 2025-05-19T10:47:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+package or.sopt.soptwatcha.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import or.sopt.soptwatcha.common.exception.CustomException;
+import or.sopt.soptwatcha.common.exception.ErrorCode;
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import or.sopt.soptwatcha.domain.MovieGenre;
+import or.sopt.soptwatcha.domain.MovieImage;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
+import or.sopt.soptwatcha.repository.MovieArtistRepository;
+import or.sopt.soptwatcha.repository.MovieGenreRepository;
+import or.sopt.soptwatcha.repository.MovieImageRepository;
+import or.sopt.soptwatcha.repository.MovieRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
path: src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java
user: login: gdbs1107
body: 헛 여기에 readOnly를 달면 여기 밑의 모든 메서드가 readOnly가 될 것 같아요! 이번 기획에는 post 메서드가 코멘트에 밖에 없어서 괜찮을 수 있지만 그래도 readOnly 메서드단에 다는게 확장성있는 설계가 될 것 같아요
created_at: 2025-05-19T10:51:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+package or.sopt.soptwatcha.util;
+
+public class TimeFormatUtil {
+    
+    public static String formatMinutesToHoursAndMinutes(int minutes) {
+        int hours = minutes / 60;
+        int remainingMinutes = minutes % 60;
+        
+        if (hours > 0 && remainingMinutes > 0) {
+            return hours + "시간 " + remainingMinutes + "분";
+        } else if (hours > 0) {
+            return hours + "시간";
+        } else {
+            return remainingMinutes + "분";
+        }
+    }
+}
path: src/main/java/or/sopt/soptwatcha/util/TimeFormatUtil.java
user: login: gdbs1107
body: 이렇게 유틸 클래스 분리한거 진짜 좋네요! 책임 분할이 확실하게 되는 것 같아요!!
created_at: 2025-05-19T10:52:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,28 @@
+package or.sopt.soptwatcha.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import or.sopt.soptwatcha.common.response.BaseResponse;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
+import or.sopt.soptwatcha.service.MovieServiceImpl;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/posts")
+@RequiredArgsConstructor
+@Tag(name = "영화 관련 API")
path: src/main/java/or/sopt/soptwatcha/controller/MovieController.java
user: login: bykbyk0401
body: ㅎㅎ 좋숩니닷
created_at: 2025-05-19T15:25:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+package or.sopt.soptwatcha.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import or.sopt.soptwatcha.common.exception.CustomException;
+import or.sopt.soptwatcha.common.exception.ErrorCode;
+import or.sopt.soptwatcha.domain.Movie;
+import or.sopt.soptwatcha.domain.MovieArtist;
+import or.sopt.soptwatcha.domain.MovieGenre;
+import or.sopt.soptwatcha.domain.MovieImage;
+import or.sopt.soptwatcha.dto.response.MovieDetailResponseDTO;
+import or.sopt.soptwatcha.repository.MovieArtistRepository;
+import or.sopt.soptwatcha.repository.MovieGenreRepository;
+import or.sopt.soptwatcha.repository.MovieImageRepository;
+import or.sopt.soptwatcha.repository.MovieRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
path: src/main/java/or/sopt/soptwatcha/service/MovieServiceImpl.java
user: login: bykbyk0401
body: 넵 체크 감사합니당~
created_at: 2025-05-19T15:26:12Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,5 @@
 package or.sopt.soptwatcha.domain.common.enums;
 
 public enum FilmCountry {
-    A
+    한국, 미국, 영국, 프랑스, 일본, 중국
path: src/main/java/or/sopt/soptwatcha/domain/common/enums/FilmCountry.java
user: login: bykbyk0401
body: 저도 고민했던부분이.. 국가는 실제 반환문구로 사용이 되는데, 여기서는 한글로 반환을 해줘야해서 오히려 영어로 두면 불필요한 것 아닌가 하는 고민이 되었는데요!
우선은 다른 기능 먼저 개발 후 추후 다시 고려해보겠습니다!!

의견 감사합니다~
created_at: 2025-05-19T15:33:57Z
author_association: COLLABORATOR

PR comments:
user: login: bykbyk0401
created_at: 2025-05-19T15:28:31Z
body: 충돌 해결 완료했습니다!!
현재는 테스트코드에서 에러가 나는게 있는것같은데, 다른 부분과 관련된 부분인 것 같아서 문제는 안될 것 같습니다.
추가적으로 생각나는 부분 없다면 우선은 머지해도 될 것 같아보여요!
author_association: COLLABORATOR
user: login: gdbs1107
created_at: 2025-05-19T15:30:29Z
body: > 충돌 해결 완료했습니다!!
> 
> 현재는 테스트코드에서 에러가 나는게 있는것같은데, 다른 부분과 관련된 부분인 것 같아서 문제는 안될 것 같습니다.
> 
> 추가적으로 생각나는 부분 없다면 우선은 머지해도 될 것 같아보여요!

넵 바로 머지해주세요! 고생하셨습니다~!
author_association: COLLABORATOR