diff --git a/.gitignore b/.gitignore index def2dcf..866521c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out/ ### VS Code ### .vscode/ +/src/main/resources/application.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index b0e27ce..8419c4f 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'com.fasterxml.jackson.core:jackson-databind' + } tasks.named('test') { diff --git a/src/main/java/umc/moviein/config/SecurityConfig.java b/src/main/java/umc/moviein/config/SecurityConfig.java index cfad909..532f3af 100644 --- a/src/main/java/umc/moviein/config/SecurityConfig.java +++ b/src/main/java/umc/moviein/config/SecurityConfig.java @@ -51,7 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 로그인, 회원가입 API 는 토큰이 없는 상태에서 요청이 들어오기 때문에 permitAll 설정 .authorizeHttpRequests(authorizeRequests -> { - authorizeRequests.requestMatchers("/api/sign/**","/swagger-ui/**","/v3/api-docs/**").permitAll(); + authorizeRequests.requestMatchers("/api/sign/**","/swagger-ui/**","/v3/api-docs/**", "/api/review/**").permitAll(); authorizeRequests.anyRequest().authenticated(); // 나머지 API 는 전부 인증 필요 }) // JwtFilter 를 addFilterBefore 로 등록했던 JwtSecurityConfig 클래스를 적용 diff --git a/src/main/java/umc/moviein/domain/Review.java b/src/main/java/umc/moviein/domain/Review.java new file mode 100644 index 0000000..7789aea --- /dev/null +++ b/src/main/java/umc/moviein/domain/Review.java @@ -0,0 +1,41 @@ +package umc.moviein.domain; + +import jakarta.persistence.*; +import lombok.*; +import umc.moviein.domain.common.BaseEntity; +import umc.moviein.domain.mapping.TagReview; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "reveiw") +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long reviewId; + + @Column(nullable = false, columnDefinition = "int default 10") + private Integer rating; + @Column(nullable = false, length = 50) + private String content; + @Column(nullable = false) + private boolean isSpoiled = false; + + @OneToMany(mappedBy = "review") + List tagReviewList = new ArrayList<>(); + + private Long movieId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + +} diff --git a/src/main/java/umc/moviein/domain/Tag.java b/src/main/java/umc/moviein/domain/Tag.java new file mode 100644 index 0000000..f81395f --- /dev/null +++ b/src/main/java/umc/moviein/domain/Tag.java @@ -0,0 +1,29 @@ +package umc.moviein.domain; + + +import jakarta.persistence.*; +import lombok.*; +import umc.moviein.domain.mapping.TagReview; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Setter +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "tag") +public class Tag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long tagId; + + @Column(nullable = true) + private String name; + + @OneToMany(mappedBy = "tag") + List tagReviewList = new ArrayList<>(); + +} diff --git a/src/main/java/umc/moviein/domain/User.java b/src/main/java/umc/moviein/domain/User.java index 6495f81..a60dd24 100644 --- a/src/main/java/umc/moviein/domain/User.java +++ b/src/main/java/umc/moviein/domain/User.java @@ -4,6 +4,8 @@ import lombok.*; import umc.moviein.domain.common.BaseEntity; +import java.util.*; + @Entity @Getter @@ -28,4 +30,7 @@ public class User extends BaseEntity { @Column(nullable = false, length = 50) private String nickname; + @OneToMany(mappedBy = "user") + private List reviewList = new ArrayList<>(); + } diff --git a/src/main/java/umc/moviein/domain/mapping/TagReview.java b/src/main/java/umc/moviein/domain/mapping/TagReview.java new file mode 100644 index 0000000..3871587 --- /dev/null +++ b/src/main/java/umc/moviein/domain/mapping/TagReview.java @@ -0,0 +1,26 @@ +package umc.moviein.domain.mapping; + +import jakarta.persistence.*; +import lombok.*; +import umc.moviein.domain.Review; +import umc.moviein.domain.Tag; + +@Entity +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "tag-review") +public class TagReview { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long tagReviewId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; +} diff --git a/src/main/java/umc/moviein/repository/ReviewRepository.java b/src/main/java/umc/moviein/repository/ReviewRepository.java new file mode 100644 index 0000000..8c33326 --- /dev/null +++ b/src/main/java/umc/moviein/repository/ReviewRepository.java @@ -0,0 +1,14 @@ +package umc.moviein.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import umc.moviein.domain.Review; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReviewRepository extends JpaRepository { + + Optional> findAllByMovieId(Long movieId); +} diff --git a/src/main/java/umc/moviein/repository/TagRespsitory.java b/src/main/java/umc/moviein/repository/TagRespsitory.java new file mode 100644 index 0000000..808495a --- /dev/null +++ b/src/main/java/umc/moviein/repository/TagRespsitory.java @@ -0,0 +1,8 @@ +package umc.moviein.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import umc.moviein.domain.Tag; +@Repository +public interface TagRespsitory extends JpaRepository { +} diff --git a/src/main/java/umc/moviein/repository/TagReviewRepository.java b/src/main/java/umc/moviein/repository/TagReviewRepository.java new file mode 100644 index 0000000..6652079 --- /dev/null +++ b/src/main/java/umc/moviein/repository/TagReviewRepository.java @@ -0,0 +1,17 @@ +package umc.moviein.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import umc.moviein.domain.Tag; +import umc.moviein.domain.mapping.TagReview; + +import java.util.List; + +@Repository +public interface TagReviewRepository extends JpaRepository { + + @Query("SELECT t FROM TagReview tr JOIN tr.tag t WHERE tr.review.reviewId = :reviewId") + List findTagsByReviewId(@Param("reviewId") Long reviewId); +} diff --git a/src/main/java/umc/moviein/service/ReviewService.java b/src/main/java/umc/moviein/service/ReviewService.java new file mode 100644 index 0000000..94da59a --- /dev/null +++ b/src/main/java/umc/moviein/service/ReviewService.java @@ -0,0 +1,88 @@ +package umc.moviein.service; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import umc.moviein.repository.UserRepository; +import umc.moviein.web.dto.MovieInfoResponseDto; +import umc.moviein.web.dto.ReviewRequestDto; +import umc.moviein.web.dto.ReviewResponseDto; +import umc.moviein.web.dto.TagDto; +import umc.moviein.domain.Review; +import umc.moviein.domain.Tag; +import umc.moviein.domain.mapping.TagReview; +import umc.moviein.repository.ReviewRepository; +import umc.moviein.repository.TagRespsitory; +import umc.moviein.repository.TagReviewRepository; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static umc.moviein.jwt.SecurityUtil.getCurrentMemberId; + +@Service +@RequiredArgsConstructor +@Builder +public class ReviewService { + final ReviewRepository reviewRepository; + final TagRespsitory tagRespsitory; + final TagReviewRepository tagReviewRepository; + final UserRepository userRepository; + + public Review registerReview(ReviewRequestDto dto) { + Review review = ReviewRequestDto.convertDtoToEntity(dto); + // Todo 예외처리 + review.setUser(userRepository.findById(getCurrentMemberId()).orElseThrow(() -> new IllegalArgumentException("not valid user in review"))); + reviewRepository.save(review); + tagReviewRepository.saveAll(createOrUpdateTags(dto.getTags(), review)); + + + return review; + } + + public List createOrUpdateTags(List tags, Review review) { + List tagReviews = new ArrayList<>(); + for (Integer tagId : tags) { + // 태그 조회 + Tag tag = tagRespsitory.findById(tagId.longValue()) + .orElseGet(() -> { + // 태그가 없으면 새로 생성 + Tag newTag = new Tag(); + newTag.setTagId(tagId.longValue()); // ID 설정 (필요한 경우) + newTag.setName("New Tag " + tagId); // 태그 이름 설정 (적절히 변경) + return tagRespsitory.save(newTag); + }); + + // 태그 리뷰 생성 + TagReview tagReview = TagReview.builder() + .review(review) + .tag(tag) + .build(); + tagReviews.add(tagReview); + } + return tagReviews; + + } + public MovieInfoResponseDto findReviewsByMovieId(Long movieId) { + // 영화 id로 리뷰 필터링 + List reviewList = reviewRepository.findAllByMovieId(movieId) + .orElseThrow(() -> new IllegalArgumentException("No reviews found for movieId: " + movieId)); + + List reviews = reviewList.stream() + .map(review -> { + // 태그 정보 + List tagDtos = TagDto.convertTagDtoTOList( + tagReviewRepository.findTagsByReviewId(review.getReviewId())); + + return ReviewResponseDto.convertEntityToDto(review, tagDtos); + }) + .collect(Collectors.toList()); + + return MovieInfoResponseDto.builder() + .movieId(movieId) + .reviews(reviews) + .build(); + } +} + diff --git a/src/main/java/umc/moviein/web/controller/ReviewController.java b/src/main/java/umc/moviein/web/controller/ReviewController.java new file mode 100644 index 0000000..5b21d2e --- /dev/null +++ b/src/main/java/umc/moviein/web/controller/ReviewController.java @@ -0,0 +1,38 @@ +package umc.moviein.web.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import umc.moviein.apiPayload.ApiResponse; +import umc.moviein.web.dto.MovieInfoResponseDto; +import umc.moviein.web.dto.ReviewRequestDto; +import umc.moviein.domain.Review; +import umc.moviein.service.ReviewService; + +import static org.springframework.http.ResponseEntity.ok; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ReviewController { + final ReviewService reviewService; + + @PostMapping("/review") + public ResponseEntity createReview(@RequestBody ReviewRequestDto reviewDto) { + try { + Review review = reviewService.registerReview(reviewDto); + return ResponseEntity.ok("Review created successfully"); + } catch (Exception e) { + // 예외 처리 (선택적으로 로깅 추가 가능) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to create review: " + e.getMessage()); + } + + } + @GetMapping("/reviews/{movieId}") + public ResponseEntity> findReviews(@PathVariable(name = "movieId") Long movieId) { + MovieInfoResponseDto reviews = reviewService.findReviewsByMovieId(movieId); + return ok(ApiResponse.onSuccess(reviews)); + } +} diff --git a/src/main/java/umc/moviein/web/dto/MovieInfoResponseDto.java b/src/main/java/umc/moviein/web/dto/MovieInfoResponseDto.java new file mode 100644 index 0000000..5f7ec99 --- /dev/null +++ b/src/main/java/umc/moviein/web/dto/MovieInfoResponseDto.java @@ -0,0 +1,17 @@ +package umc.moviein.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MovieInfoResponseDto { + private Long movieId; + private List reviews; +} diff --git a/src/main/java/umc/moviein/web/dto/ReviewRequestDto.java b/src/main/java/umc/moviein/web/dto/ReviewRequestDto.java new file mode 100644 index 0000000..dfb9d67 --- /dev/null +++ b/src/main/java/umc/moviein/web/dto/ReviewRequestDto.java @@ -0,0 +1,33 @@ +package umc.moviein.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import umc.moviein.domain.Review; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReviewRequestDto { + + Long movieId; + Integer rating; + List tags = new ArrayList<>(); + String content; + boolean isSpoiled; + + public static Review convertDtoToEntity(ReviewRequestDto dto) { + return Review.builder() + .movieId(dto.movieId) + .rating(dto.rating) + .content(dto.content) + .isSpoiled(dto.isSpoiled) + .build(); + } + +} diff --git a/src/main/java/umc/moviein/web/dto/ReviewResponseDto.java b/src/main/java/umc/moviein/web/dto/ReviewResponseDto.java new file mode 100644 index 0000000..c26974a --- /dev/null +++ b/src/main/java/umc/moviein/web/dto/ReviewResponseDto.java @@ -0,0 +1,45 @@ +package umc.moviein.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import umc.moviein.domain.Review; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ReviewResponseDto { + + private Long reviewId; + private Integer rating; + private String content; + private Boolean isSpoiled; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private List tagReviewList; // 태그 정보 포함 + +// private String userImage; + + + public static ReviewResponseDto convertEntityToDto(Review review, List tags) { + return ReviewResponseDto.builder() + .reviewId(review.getReviewId()) + .rating(review.getRating()) + .content(review.getContent()) + .isSpoiled(review.isSpoiled()) + .tagReviewList(tags) + .createdAt(review.getCreatedAt()) + .updatedAt(review.getUpdatedAt()) + .build(); + } + + + + + +} diff --git a/src/main/java/umc/moviein/web/dto/TagDto.java b/src/main/java/umc/moviein/web/dto/TagDto.java new file mode 100644 index 0000000..afcff4d --- /dev/null +++ b/src/main/java/umc/moviein/web/dto/TagDto.java @@ -0,0 +1,34 @@ +package umc.moviein.web.dto; + + +import lombok.*; +import umc.moviein.domain.Tag; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TagDto { + + private Long tagId; + private String tagName; + + public static TagDto convertTagReveiwToDto(Tag tag) { + return TagDto.builder() + .tagId(tag.getTagId()) + .tagName(tag.getName()) + .build(); + } + public static List convertTagDtoTOList(List tags) { + List dtoList = new ArrayList<>(); + for(Tag tag : tags) { + dtoList.add(TagDto.convertTagReveiwToDto(tag)); + } + return dtoList; + + } +} \ No newline at end of file