외부에서 업로드하는 파일의 크기와 개수, 종류를 제한하지 않고, 외부에서 접근 가능한 경로에 파일을 저장했을 때 발생
~~~~~~~~~~~~~ ~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
| | 서버에서 실행 가능한 파일을 외부에서 실행
| +-- 서버에서 실행 가능한 파일 또는 악성 행위를 수행하는 파일을
| 업로드 가능
| ⇒ 서버의 제어권을 탈취 (웹쉘)
| ⇒ 해당 서버가 악성 코드 유포지로 악용
+-- 서버의 연결 자원과 디스크(저장소) 자원을 고갈시켜 정상적인 서비스를 방해
방어 기법
1. 외부에서 업로드하는 파일의 크기, 개수, 종류를 제한
~~~~~~~~~~~
확장자 검증, Content-Type 검증, Magic Code(Number)
2. 업로드 파일을 외부에서 접근할 수 없는 경로에 저장 ⇒ 저장된 파일을 제공하려면
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 다운로드 기능이 필요
Web Document Root 밖의 디렉터리를 의미
3. 업로드 파일명, 확장자, 저장경로 등을 외부에서 알 수 없도록 변경해서 사용
4. 파일의 실행 권한을 제거하고 저장
@Override
public void insertBoard(BoardDto boardDto, MultipartHttpServletRequest request) {
// 로그인한 사용자를 글쓴이로 설정
// TODO. 로그인한 사용자의 ID로 변경
boardDto.setCreatedId("hong");
boardMapper.insertBoard(boardDto);
try {
// 첨부 파일을 디스크에 저장하고, 첨부 파일 정보를 반환
List<BoardFileDto> fileInfoList = fileUtils.parseFileInfo(boardDto.getBoardIdx(), request);
// 첨부 파일 정보를 DB에 저장
if (!CollectionUtils.isEmpty(fileInfoList)) {
boardMapper.insertBoardFileList(fileInfoList);
}
} catch(Exception e) {
log.error(e.getMessage());
}
}
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
BoardDto selectBoardDetail(int boardIdx);
void updateBoard(BoardDto boardDto);
void deleteBoard(BoardDto boardDto);
void updateHitCnt(int boardIdx);
void insertBoardFileList(List<BoardFileDto> fileInfoList);
}
<!--
외부 입력값을 쿼리에 반영할 경우, #{ }을 이용해야 SQL 인젝션 공격을 방어할 수 있음
useGeneratedKeys : DBMS가 자동 생성한 키를 사용
keyProperty : 반환하는 키
-->
<insert id="insertBoard" parameterType="board.dto.BoardDto"
useGeneratedKeys="true" keyProperty="boardIdx">
insert into t_board(title, contents, created_dt, created_id)
values (#{title}, #{contents}, now(), #{createdId})
</insert>
<insert id="insertBoardFileList" parameterType="board.dto.BoardFileDto">
insert into t_file
(board_idx, original_file_name, stored_file_path, file_size, created_id, created_dt)
values
<foreach collection="list" item="item" separator=",">
(#{item.boardIdx}, #{item.originalFileName}, #{item.storedFilePath}, #{item.fileSize}, 'admin', now())
</foreach>
</insert>
<select id="selectBoardFileList" parameterType="int" resultType="board.dto.BoardFileDto">
select idx, board_idx, original_file_name, format(round(file_size/1024), 0) as file_size
from t_file
where board_idx = #{boardIdx} and deleted_yn = 'N'
</select>
@Data
public class BoardDto {
private int boardIdx;
private String title;
private String contents;
private int hitCnt;
private String createdDt;
private String createdId;
private String updatorDt;
private String updatorId;
// 첨부 파일 정보를 저장할 필드를 추가
private List<BoardFileDto> fileInfoList;
}
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
BoardDto selectBoardDetail(int boardIdx);
void updateBoard(BoardDto boardDto);
void deleteBoard(BoardDto boardDto);
void updateHitCnt(int boardIdx);
void insertBoardFileList(List<BoardFileDto> fileInfoList);
List<BoardFileDto> selectBoardFileList(int boardIdx);
}
게시판 상세 조회 시 첨부 파일을 조회해서 상세 조회 결과에 추가
@Override
public BoardDto selectBoardDetail(int boardIdx) {
boardMapper.updateHitCnt(boardIdx);
BoardDto boardDto = boardMapper.selectBoardDetail(boardIdx);
List<BoardFileDto> boardFileInfoList = boardMapper.selectBoardFileList(boardIdx);
boardDto.setFileInfoList(boardFileInfoList);
return boardDto;
}
</form>
<!-- 첨부파일 목록 -->
<div class="file_list">
<a th:each="file : ${board.fileInfoList}" th:text="|${file.originalFileName} (${file.fileSize}kb)|"></a>
</div>
<input type="button" id="list" class="btn" value="목록으로" />
<input type="button" id="update" class="btn" value="수정하기" />
<input type="button" id="delete" class="btn" value="삭제하기" />
<!-- 첨부파일 목록 -->
<div class="file_list">
<a th:each="file : ${board.fileInfoList}"
th:text="|${file.originalFileName} (${file.fileSize}kb)|"
th:href="@{/board/downloadBoardFile.do(idx=${file.idx}, boardIdx=${file.boardIdx})}"></a>
</div>
<!-- 첨부파일 목록 -->
<div class="file_list">
<a href="/board/downloadBoardFile.do?idx=3&boardIdx=7">또강아지.png (45kb)</a>
<a href="/board/downloadBoardFile.do?idx=4&boardIdx=7">강아지.jpg (7kb)</a>
</div>
<select id="selectBoardFileInfo" parameterType="map" resultType="board.dto.BoardFileDto">
select original_file_name, stored_file_path, file_size
from t_file
where idx = #{idx} and board_idx = #{boardIdx} and deleted_yn = 'N'
</select>
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto);
BoardDto selectBoardDetail(int boardIdx);
void updateBoard(BoardDto boardDto);
void deleteBoard(BoardDto boardDto);
void updateHitCnt(int boardIdx);
void insertBoardFileList(List<BoardFileDto> fileInfoList);
List<BoardFileDto> selectBoardFileList(int boardIdx);
BoardFileDto selectBoardFileInfo(@Param("idx") int idx, @Param("boardIdx") int boardIdx);
}
public interface BoardService {
List<BoardDto> selectBoardList();
void insertBoard(BoardDto boardDto, MultipartHttpServletRequest request);
BoardDto selectBoardDetail(int boardIdx);
void updateBoard(BoardDto boardDto);
void deleteBoard(int boardIdx);
BoardFileDto selectBoardFileInfo(int idx, int boardIdx);
}
@Override
public BoardFileDto selectBoardFileInfo(int idx, int boardIdx) {
return boardMapper.selectBoardFileInfo(idx, boardIdx);
}
// 파일 다운로드 요청을 처리하는 메서드
@GetMapping("/board/downloadBoardFile.do")
public void downloadBoardFile(@RequestParam("idx") int idx, @RequestParam("boardIdx") int boardIdx, HttpServletResponse response) throws Exception {
// idx와 boardIdx가 일치하는 파일 정보를 조회
BoardFileDto boardFileDto = boardService.selectBoardFileInfo(idx, boardIdx);
if (ObjectUtils.isEmpty(boardFileDto)) {
return;
}
// 원본 파일 저장 위치에서 파일을 읽어서 호출(요청)한 곳으로 파일을 응답으로 전달
Path path = Paths.get(boardFileDto.getStoredFilePath());
byte[] file = Files.readAllBytes(path);
response.setContentType("application/octet-stream"); ⇐ 브라우저가 해당 콘텐츠를 바이너리 파일로 처리하도록 지정
response.setContentLength(file.length); ⇐ 다운로드할 파일의 크기를 명시
response.setHeader("Content-Disposition", ⇐ 브라우저가 파일을 다운로드하도록 지시
"attachment; fileName=\"" + URLEncoder.encode(boardFileDto.getOriginalFileName(), "UTF-8") + "\";");
response.setHeader("Content-Transfer-Encoding", "binary"); ⇐ 응답 본문을 바이너리로 전송하도록 지정
response.getOutputStream().write(file);
response.getOutputStream().flush();
response.getOutputStream().close();
}
참고 => https://myanjini.tistory.com/entry/test
공통 예외 페이지를 만들어서 어플리케이션에서 발생하는 예외를 처리 => @ControllerAdvice
어노테이션을 이용
package board.common;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice
@Slf4j
public class ErrorHandler {
@ExceptionHandler(Exception.class)
public ModelAndView defaultExceptionHandler(HttpServletRequest request, Exception exception) {
ModelAndView mv = new ModelAndView("/error/default");
mv.addObject("exception", exception);
mv.addObject("request", request);
log.error("exception >>> ", exception);
return mv;
}
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>공통 에러 페이지</title>
<style>
body {
font-family: Arial, sans-serif; line-height: 1.6; background-color: #f9f9f9; color: #333;
}
h1 {
background-color: #f44336; color: white; padding: 20px; text-align: center;
}
.error-details {
margin: 20px auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px; background-color: white;
max-width: 800px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.error-details p {
margin: 10px 0;
}
.toggle-button {
display: inline-block; margin-top: 10px; padding: 10px 20px; font-size: 14px; color: white;
background-color: #007bff; border: none; border-radius: 5px; cursor: pointer;
}
.toggle-button:hover {
background-color: #0056b3;
}
.hidden {
display: none;
}
ul {
list-style: none;
padding-left: 20px;
}
ul li {
font-family: monospace; font-size: 14px;
}
</style>
</head>
<body>
<h1>공통 에러 페이지</h1>
<div class="error-details">
<p><strong>요청 URL:</strong> <span th:text="${request.getRequestURI()}"></span></p>
<p><strong>오류 메시지:</strong> <span th:text="${exception.message}"></span></p>
<h2>오류 상세 내용</h2>
<button class="toggle-button" onclick="toggleDetails()">보기</button>
<ul id="error-details-list" class="hidden" th:each="line : ${exception.stackTrace}" th:text="${line.toString()}"></ul>
</div>
<script>
function toggleDetails() {
const detailsList = document.getElementById("error-details-list");
const button = document.querySelector(".toggle-button");
if (detailsList.classList.contains("hidden")) {
detailsList.classList.remove("hidden");
button.textContent = "숨기기";
} else {
detailsList.classList.add("hidden");
button.textContent = "보기";
}
}
</script>
</body>
</html>
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfuration
@EnableAutoConfiguration(exclude = { ErrorMvcAutoConfiguration.class })
어노테이션 추가
@Slf4j
@SpringBootApplication
@EnableAutoConfiguration(exclude = { ErrorMvcAutoConfiguration.class })
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}
설 연휴 주까지 !!!!!! 꺄아가가각가꺄아꺄아꺄아꺙까아꺄아꺄아야야까
아래의 샘플 테이블과 데이터 또는 자바 미니 프로젝트에서 사용했던 데이터 구조를 이용해서 CRUD 기능을 구현해 보세요
프로젝트 생성해서 CRUD 기능 (등록, 목록 조회, 상세 조회, 수정, 삭제, 파일 업로드/다운로드)
작성한 프로젝트는 git 주소 또는 압축파일로 제출
CREATE TABLE Books (
book_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 고유 식별자
title VARCHAR(255) NOT NULL, -- 도서 제목
author VARCHAR(255) NOT NULL, -- 저자
publisher VARCHAR(255), -- 출판사
published_date DATE, -- 출판일
isbn VARCHAR(20) UNIQUE, -- ISBN 번호
description TEXT, -- 도서 설명
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 생성 시각
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 수정 시각
);
CREATE TABLE BookImages (
image_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 고유 식별자
book_id BIGINT NOT NULL, -- 도서와의 외래 키 관계
image_url VARCHAR(500) NOT NULL, -- 이미지 URL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 생성 시각
FOREIGN KEY (book_id) REFERENCES Books(book_id) ON DELETE CASCADE -- 도서 삭제 시 이미지도 삭제
);
INSERT INTO Books (title, author, publisher, published_date, isbn, description)
VALUES
('Clean Code 클린 코드', '로버트 C. 마틴', '인사이트(insight)', '2013-12-24', '9788966260959', '애자일 소프트웨어 장인 정신'),
('스프링 부트 3 백엔드 개발자 되기 : 자바 편 (2판)', '신선영', '골든래빗', '2024-04-05', '9791191905717', 'JPA+OAuth2+JWT+AWS와 배우는 스프링 부트 3 Java 백엔드 입문자를 위한 풀 패키지'),
('GitOps Cookbook', '나탈리 빈토, 알렉스 소토 부에노', '인사이트(insight)', '2024-11-26', '9788966264537', '커스터마이즈, 헬름, 텍톤, Argo CD를 활용한 쿠버네티스 CI/CD 구축하기');
INSERT INTO BookImages (book_id, image_url)
VALUES
(1, 'https://image.yes24.com/goods/11681152/XL'),
(2, 'https://image.yes24.com/goods/125668284/XL'),
(3, 'https://image.yes24.com/goods/138929285/XL');
CREATE TABLE Movies (
movie_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 영화 고유 식별자
title VARCHAR(255) NOT NULL, -- 영화 제목
director VARCHAR(255) NOT NULL, -- 감독 이름
release_date DATE, -- 개봉일
genre VARCHAR(100), -- 장르
rating DECIMAL(3, 1), -- 평점 (예: 8.5)
description TEXT, -- 영화 설명
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 생성 시각
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 수정 시각
);
CREATE TABLE MoviePosters (
poster_id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 포스터 고유 식별자
movie_id BIGINT NOT NULL, -- 영화 ID (외래 키)
poster_url VARCHAR(500) NOT NULL, -- 포스터 URL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 생성 시각
FOREIGN KEY (movie_id) REFERENCES Movies(movie_id) ON DELETE CASCADE -- 영화 삭제 시 포스터도 삭제
);
INSERT INTO Movies (title, director, release_date, genre, rating, description)
VALUES
('검은 수녀들', '권혁재', '2025-01-24', '미스터리,드라마', 9.0, '금지된 곳으로 갈 준비가 되었습니다.'),
('하얼빈', '우민호', '2024-12.24', '드라마,액션,스릴러,느와르,첩보,시대극', 9.3, '안중근 의사 하얼빈 의거를 다룬 우민호 감독의 여섯 번째 장편 영화. 제49회 토론토 국제 영화제 갈라 프레젠테이션 부문 공식 초청작이다.');
INSERT INTO MoviePosters (movie_id, poster_url)
VALUES
(1, 'https://img.cgv.co.kr/Movie/Thumbnail/Poster/000089/89398/89398_320.jpg'),
(1, 'https://img.cgv.co.kr/Movie/Thumbnail/StillCut/000089/89398/89398233560_727.jpg'),
(2, 'https://img.cgv.co.kr/Movie/Thumbnail/Poster/000088/88797/88797_320.jpg');
상욱슨의 티칭과 함께 해보기 !!!!!
spring.io 새 프로젝트 생성
DB 스키마 생성
빌드 도구 설정 IntelliJ IDEA로 변경
application.yml로 변경
spring:
datasource:
url: jdbc:mysql://localhost:3306/[만든 db 스키마 이름]?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: [username]
password: [비밀번호]
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: create
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(name = "created_at", columnDefinition = "TIMESTAMP(6)")
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", columnDefinition = "TIMESTAMP(6)")
private LocalDateTime updatedAt;
}
Movie : MoviePoster = 1 : n
package com.practice.demo.domain;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Movie extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String director;
@Column(name = "release_date")
private LocalDate releaseDate;
private String genre;
private Float rating;
private String description;
}
package com.practice.demo.domain;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import static jakarta.persistence.FetchType.*;
import static jakarta.persistence.GenerationType.*;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "movie_poster")
public class MoviePoster extends BaseEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@Column(name = "poster_url")
private String posterUrl;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "movie_id")
private Movie movie;
}
여기서
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "movie_id")
조건 중 영화 삭제 시 포스터도 삭제된다는 조건이 있다.
CASCADE를 걸어줘야 한다.
=> 그러려면 양방향 설정을 해줘야한다. Movie에도 @OneToMany 걸어주기 !!
@OneToMany(mappedBy = "movie", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<MoviePoster> moviePosterList = new ArrayList<>();
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
public interface MoviePosterRepository extends JpaRepository<MoviePoster, Long> {
}
public interface MovieService {
}
@Service
@RequiredArgsConstructor
public class MovieServiceImpl implements MovieService{
private final MovieRepository movieRepository;
}
@RequiredArgsConstructor
역할은 서비스에서 repository를 사용할건데 @Autowired 해서 생성자까지 만들어서 의존관계를 주입을 했는데 @RequiredArgsConstructor 이 어노테이션을 쓰면 @Autowired 해서 생성자까지 만들 필요가 없어진다.
MoviePosterService와 Impl도 똑같이 만들어준다.
서비스를 가져다 뷰에 쓰는 것 !
서비스를 델꼬오자.
@Controller
@RequiredArgsConstructor
public class MovieController {
private final MovieService movieService;
}
@Controller
@RequiredArgsConstructor
public class MoviePosterController {
private final MoviePosterService moviePosterService;
}
service에서 return 하는 애를 dto로 필요한 애들만 쏙쏙 골라내기
@Builder
public record MovieRequest() {
}
@Builder
public record MoviePosterDto() {
}
CRUD를 service에서 짜보자 !!
1. 등록
dto - MovieRequest
package com.practice.demo.dto.movie;
import lombok.Builder;
import java.time.LocalDate;
@Builder
public record MovieRequest(
String title,
String director,
LocalDate releaseDate,
String genre,
Float rating,
String description) {
}
#### domain/Movie
- static 붙이면 클래스만 있으면 할당됨. 객체를 생성해서 메서드를 쓸 수 있는 게 아니라 클래스.메서드로 메서드 바로 사용할 수 있도록 static을 붙였다.
- builder 패턴으로 Movie 라는 객체를 생성한다.
```java
public static Movie create(String title, String director, LocalDate releaseDate, String genre, Float rating, String description) {
return Movie.builder()
.title(title)
.director(director)
.releaseDate(releaseDate)
.genre(genre)
.rating(rating)
.description(description)
.build();
}
함수 선언
public interface MovieService {
void saveMovie(MovieDto movieDto);
}
함수 구현
@Service
@RequiredArgsConstructor
@Transactional
public class MovieServiceImpl implements MovieService{
private final MovieRepository movieRepository;
private final MoviePosterRepository moviePosterRepository;
@Override
public void saveMovie(MovieDto movieDto) {
Movie movie = Movie.create(
movieDto.title(),
movieDto.director(),
movieDto.releaseDate(),
movieDto.genre(),
movieDto.rating(),
movieDto.description()
);
movieRepository.save(movie);
}
}
2. 목록 조회 (전체 조회)
보여줄 애들만 쏙쏙 !!
movieId 추가, description 삭제
package com.practice.demo.dto.movie.response;
import lombok.Builder;
import java.time.LocalDate;
@Builder
public record MovieResponse(
Long movieId,
String title,
String director,
LocalDate releaseDate,
String genre,
Float rating
) {
public static MovieResponse of(Long movieId, String title, String director, LocalDate releaseDate, String genre, Float rating) {
return MovieResponse.builder()
.movieId(movieId)
.title(title)
.director(director)
.releaseDate(releaseDate)
.genre(genre)
.rating(rating)
.build();
}
}
List<MovieResponse> showMovieList();
// Read - 목록 조회
@Override
public List<MovieResponse> showMovieList() {
List<Movie> movies = movieRepository.findAll();
List<MovieResponse> movieResponses = movies.stream()
.map(m -> MovieResponse.of(
m.getId(),
m.getTitle(),
m.getDirector(),
m.getReleaseDate(),
m.getGenre(),
m.getRating()
))
.toList();
return movieResponses;
// TODO: 영화 포스터 썸네일 포함시키기
}
3. 상세 조회
package com.practice.demo.dto.movie.response;
import lombok.Builder;
import java.time.LocalDate;
@Builder
public record MovieDetailResponse(
Long movieId,
String title,
String director,
LocalDate releaseDate,
String genre,
Float rating,
String description
) {
public static MovieDetailResponse of(Long movieId, String title, String director, LocalDate releaseDate, String genre, Float rating, String description) {
return MovieDetailResponse.builder()
.movieId(movieId)
.title(title)
.director(director)
.releaseDate(releaseDate)
.genre(genre)
.rating(rating)
.description(description)
.build();
}
}
MovieDetailResponse showMovie(Long movieId);
// Read - 상세 조회
@Override
public MovieDetailResponse showMovie(Long movieId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new RuntimeException());
MovieDetailResponse movieDetailResponse = MovieDetailResponse.of(
movie.getId(),
movie.getTitle(),
movie.getDirector(),
movie.getReleaseDate(),
movie.getGenre(),
movie.getRating(),
movie.getDescription()
);
return movieDetailResponse;
// TODO: 영화 포스터 포함시키기
// TODO: 예외 처리 구현하기
}
findById가 Optional 객체를 리턴하므로 Optional 안 쓰려면 예외처리 해줘야한다.
4. 수정
void updateMovie(Long movieId, MovieRequest movieRequest); // Update - 수정
public void update(String title, String director, LocalDate releaseDate, String genre, Float rating, String description) {
if (title != null) {
this.title = title;
}
if (director != null) {
this.director = director;
}
if (releaseDate != null) {
this.releaseDate = releaseDate;
}
if (genre != null) {
this.genre = genre;
}
if (rating != null) {
this.rating = rating;
}
if (description != null) {
this.description = description;
}
}
// Update - 수정
@Override
public void updateMovie(Long movieId, MovieRequest movieRequest) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new RuntimeException());
movie.update(
movieRequest.title(),
movieRequest.director(),
movieRequest.releaseDate(),
movieRequest.genre(),
movieRequest.rating(),
movieRequest.description()
);
findById가 Optional 객체를 리턴하므로 Optional 안 쓰려면 예외처리 해줘야한다.
5. 삭제
void deleteMovie(Long movieId); // Delete - 삭제
// Delete - 삭제
@Override
public void deleteMovie(Long movieId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new RuntimeException());
movieRepository.delete(movie);
}
package com.practice.demo.controller;
import com.practice.demo.dto.movie.request.MovieRequest;
import com.practice.demo.dto.movie.response.MovieDetailResponse;
import com.practice.demo.dto.movie.response.MovieResponse;
import com.practice.demo.service.movie.MovieService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/movie")
public class MovieTestController {
private final MovieService movieService;
@PostMapping
public ResponseEntity<?> create(@RequestBody MovieRequest movieRequest) {
movieService.saveMovie(movieRequest);
return ResponseEntity.ok("영화 저장 성공");
}
@GetMapping
public ResponseEntity<List<MovieResponse>> readAll() {
return ResponseEntity.ok(movieService.showMovieList());
}
@GetMapping("/{movieId}")
public ResponseEntity<MovieDetailResponse> read(@PathVariable("movieId") Long movieId) {
return ResponseEntity.ok(movieService.showMovie(movieId));
}
@PatchMapping("/{movieId}")
public ResponseEntity<?> update(@PathVariable("movieId") Long movieId, @RequestBody MovieRequest movieRequest) {
movieService.updateMovie(movieId, movieRequest);
return ResponseEntity.ok("영화 수정 성공");
}
@DeleteMapping("/{movieId}")
public ResponseEntity<?> delete(@PathVariable("movieId") Long movieId) {
movieService.deleteMovie(movieId);
return ResponseEntity.ok("영화 삭제 성공");
}
}