jnk1m
Foliage IT
jnk1m
전체 방문자
오늘
어제
  • 분류 전체보기 (209)
    • Today I Learned (34)
    • Java (47)
    • Database (15)
    • [NHN Academy] (27)
    • Spring (47)
    • HTML + CSS + JavaScript (11)
    • JSP (3)
    • Node.js (10)
    • React Native (2)
    • 기타 (8)
    • 스크랩 (5)

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
글쓰기 / 관리자
jnk1m

Foliage IT

Spring

[SpringBoot] / 2022/05/31 ManyToMany

2022. 6. 4. 00:47

YAML(Yaml Ain't Markup Language - 야믈)
=>문자열을 표현하는 방법 중의 하나로 인간이 알아보기 쉬워서 최근에 많이 사용되는 포맷입니다.
=>Spring Boot에서는 properties 파일 대신에 yml 파일을 만들어서 사용해도 됩니다.

=>properties 파일
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/adam?useUnicode=yes&characterEncoding=UTF-8&serverTimezon=UTC
spring.datasource.username=adam
spring.datasource.password=wnddkd

=>yml 파일
spring:
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:3306/adam?useUnicode=yes&characterEncoding=UTF-8&serverTimezon=UTC
username:adam
password:wnddkd

=>Micro Service 도입
Spring MVC, 전자 정부 프레임워크 -> Spring Boot
Maven -> gradle
xml 이나 properties -> yaml


**다 대 다 관계
한꺼번에 (하나의 테이블) 저장되어야 할 내용이지만 기본키를 설정하고 난 후 하나의 기본키에 여러 개의 데이터가 매칭되는 경우라면 이 정보는 별도의 테이블로 분리시켜야 하는데 이 때 분리된 테이블은 자신만의 기본키를 가져오야 하고 분리되기 전 테이블의 기본키를 외래키로 가져야 합니다.


영화 정보

 영화 일련번호 - 기본키
 영화 제목 - 일련번호 1개 당 1개
 주연 배우 - 0개 이상
 영화 이미지 - 0 개 이상
 영화 줄거리 - 1개


 영화 일련번호
 영화 제목
 영화 줄거리

 배우 번호 - 기본키
 영화 일련번호 - 영화 테이블에 대한 외래키
 주연 배우

 영화 이미지 번호 - 기본키
 영화 일련번호 - 영화 테이블에 대한 외래키
 영화 이미지 - 파일을 저장할 때는 내용을 저장하고자 하면 BLOB 경로를 저장하고자 하면 경로를 저장하는데 경로는 유일 무이하게 만들어야 합니다.

1.프로젝트 생성
1)프로젝트 생성 - movie

2)application.properties 파일에 기본 설정을 추가
#server 의 port 설정
server.port = 80

#연결할 데이터베이스 설정 - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/adam?useUnicode=yes&characterEncoding=UTF-8&serverTimezon=UTC
spring.datasource.username=adam
spring.datasource.password=wnddkd

#연결할 데이터베이스 설정 - Oracle
#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#spring.datasource.url=jdbc:oracle:thin:@192.168.10.4:1521:xe
#spring.datasource.username=system
#spring.datasource.password=oracle

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
logging.level.org.hibernate.type.descriptor.sql=trace

spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false

3)EntryPoint 클래스에 데이터베이스의 변경 사항을 감시하는 어노테이션을 추가
@SpringBootApplication
@EnableJpaAuditing
public class MovieApplication {

public static void main(String[] args) {
SpringApplication.run(MovieApplication.class, args);
}

}

4)기본패키지.model 패키지에 생성 시간 과 수정 시간을 소유한 기본 Entity 클래스 생성 - BaseEntity
@MappedSuperclass
@EntityListeners(value= {AuditingEntityListener.class})
@Getter
public class BaseEntity {
@CreatedDate
@Column(name = "regdate", updatable=false)
private LocalDateTime regDate;

@LastModifiedDate
@Column(name = "moddate")
private LocalDateTime modDate;
}

2.Entity 설계
1)영화 정보 - Movie
=>영화 번호 와 제목
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Movie extends BaseEntity {

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long mno;

private String title;
}


2)영화 이미지 - MovieImage
=>이미지 번호, uuid, 파일이름, 파일 저장 경로(날짜), Movie 로의 외래키

@Entity
@Embeddable
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString(exclude ="movie")
public class MovieImage extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long inum;
private String uuid;
private String imgName;
private String path;

@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
}

3)회원 - Member
=>회원 구분 번호, email, pw, nickname

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Table(name = "m_member")
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long mid;
private String email;
private String pw;
private String nickname;
}


4)리뷰 - Review
=>리뷰 번호, 영화에 대한 외래키, 회원에 대한 외래키, 평점, 내용

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie", "member"})
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long reviewnum;

@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;

@ManyToOne(fetch = FetchType.LAZY)
private Member member;

private int grade;
private String text;
}


3.Repository 작업
1)Movie 엔티티 작업을 위한 Repository 인터페이스를 생성 - MovieRepository

2)MovieImage 엔티티 작업을 위한 Repository 인터페이스를 생성 - MovieImageRepository

3)Test 클래스를 만들어서 샘플 데이터를 삽입
@SpringBootTest
public class RepositoryTest {

@Autowired
private MovieRepository movieRepository;

@Autowired
private MovieImageRepository movieImageRepository;

@Test
//여러 개의 데이터를 삽입하므로 모두 성공하거나 실패하도록 하기 위해서 추가
@Transactional
@Commit
public void insertMovie() {
IntStream.rangeClosed(1, 100).forEach(i -> {
Movie movie = Movie.builder().title("Movie..." + i).build();
movieRepository.save(movie);

int count = (int)(Math.random() * 5) + 1;
for(int j = 0; j<count; j++) {
MovieImage movieImage = MovieImage.builder()
.uuid(UUID.randomUUID().toString())
.movie(movie)
.imgName("test" + j + ".jpg")
.build();
movieImageRepository.save(movieImage);
}
});
}
}

4)Member 엔티티를 위한 Repository 인터페이스를 생성 - MemberRepository

5)Test 클래스에 Member Entity에 데이터를 삽입하는 테스트
@Autowired
private MemberRepository memberRepository;

@Test
//여러 개의 데이터를 삽입하므로 모두 성공하거나 실패하도록 하기 위해서 추가
@Transactional
@Commit
public void insertMember() {
IntStream.rangeClosed(1, 100).forEach(i -> {
Member member = Member.builder()
.email("r" + i + "@gmail.com")
.pw("1111").build();
memberRepository.save(member);

});
}

6)Review 엔티티를 위한 Repository 인터페이스를 생성 - ReviewRepository

7)Test 클래스에 Review Entity에 데이터를 삽입하는 테스트
@Autowired
private ReviewRepository reviewRepository;

@Test
@Transactional
@Commit
public void insertReview() {

IntStream.range(1,  200).forEach(i -> {
//존재하는 영화번호
Long mno = (long)(Math.random() * 100) + 1;
//회원 번호
Long mid = (long)(Math.random() * 100) + 1;

Movie movie = Movie.builder().mno(mno).build();
Member member = Member.builder().mid(mid).build();

Review movieReview = Review.builder().member(member).movie(movie)
.grade((int)(Math.random() * 5) + 1)
.text("영화 리뷰..." + i)
.build();
reviewRepository.save(movieReview);
});
}

8)영화 목록을 출력하기 위한 메서드를 MovieRepository 인터페이스에 추가
@Query("select m, max(mi), avg(coalesce(r.grade, 0)), count(distinct r) "
+ "from Movie m left outer join MovieImage mi on mi.movie = m "
+ "left outer join Review r on r.movie = m group by m")
Page<Object []> getListPage(Pageable pageable);

9)영화 목록을 가져오는 메서드 테스트
@Test
public void testListPage() {
PageRequest pageRequest = PageRequest.of(
0,  10, Sort.by(Sort.Direction.DESC, "mno"));
Page<Object [] > result = movieRepository.getListPage(pageRequest);
for(Object [] objects : result.getContent()) {
System.out.println(Arrays.toString(objects));
}
}

9)영화 1개의 모든 이미지 와 평균 평점 및 리뷰 개수 가져오기 - MovieRepository
@Query("select m, mi, avg(coalesce(r.grade, 0)), count(r) "
+ "from Movie m left outer join MovieImage mi on mi.movie = m "
+ "left outer join Review r on r.movie = m "
+ "where m.mno = :mno group by mi")
List<Object []> getMovieAll(@Param("mno") Long mno);

10)영화 1개의 모든 이미지 와 평균 평점 및 리뷰 개수 가져오는 메서드 테스트
@Test
public void testGetMovieWithAll() {
List<Object []> result = movieRepository.getMovieAll(92L);
for(Object [] ar : result) {
System.out.println(Arrays.toString(ar));
}
}


11)특정 영화에 대한 모든 리뷰를 가져오는 메서드를 ReviewRepository 인터페이스에 생성 
//영화 정보를 가지고 리뷰 목록을 가져오는 메서드
//영화 정보를 자세히 출력할 때 필요
List<Review> findByMovie(Movie movie);

12)리뷰 목록을 가져오는 메서드 테스트
@Test
public void testGetMovieReviews() {
Movie movie = Movie.builder().mno(100L).build();

List<Review> list = reviewRepository.findByMovie(movie);
for(Review review : list){
System.out.println(review);
}
}


13)특정 영화에 대한 모든 리뷰를 가져오는 메서드를 수정
//영화 정보를 가지고 리뷰 목록을 가져오는 메서드
//영화 정보를 자세히 출력할 때 필요
//속성에 해당하는 데이터는 EAGER로 처리해서 바로 가져옵니다.
@EntityGraph(attributePaths= {"member"}, type=EntityGraph.EntityGraphType.FETCH)
List<Review> findByMovie(Movie movie);

14)테스트
@Test
public void testGetMovieReviews() {
Movie movie = Movie.builder().mno(100L).build();

List<Review> list = reviewRepository.findByMovie(movie);
for(Review review : list){
System.out.println(review.getMember().getEmail());
}
}

15)회원이 삭제될 때 회원이 작성한 댓글도 삭제하기 위해서 ReviewRepository 인터페이스에 회원 정보를 가지고 댓글을 삭제하는 메서드를 선언
//회원 정보를 가지고 삭제하는 메서드
void deleteByMember(Member member);


16)회원 정보를 이용해서 삭제하는 메서드 테스트
//2개의 삭제 구문을 사용하므로 @Transactinal 과 @Commit을 이용해서 동시에 수행되던가 아니면 하나도 되지 않도록 처리를 해주어야 합니다.
@Test
@Transactional
@Commit
public void testDeleteMember() {
Long mid = 6L;
Member member = Member.builder().mid(mid).build();
reviewRepository.deleteByMember(member);
memberRepository.deleteById(mid);
}


17)회원 정보를 가지고 삭제하는 메서드를 수정 - delete 구문이 1번만 수행됩니다.

@Modifying
@Query("delete from Review mr where mr.member = :member")
void deleteByMember(@Param("member") Member member);



4.파일 업로드 처리
1)파일 업로드 처리 방법
=>Servlet 3.0에서 부터 제공하는 자체적인 파일 업로드 라이브러리를 이용
=>별도의 외부 라이브러리 이용(commons-fileupload 등)
=>WAS(Web Application Server - Tomcat이 대표적인 WAS) 가 아닌 환경에서 Spring boot를 실행하거나 실행하는 WAS 의 버전이 낮을 때는 별도의 라이브러리를 이용해서 파일 업로드를 처리

2)이미지 파일 미리보기
=>자바스크립트를 이용해서 업로드 하기 전에 미리보기
=>서버에 업로드 한 후 미리보기

3)spring boot에서의 업로드 처리
=>application.properties 에 설정만 하면 됨
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\Users\\tjoeun304\\Documents\\data
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB

4)View를 출력하기 위한 Controller를 생성하고 요청을 처리하는 메서드를 작성 - UploadTestController
@Controller
public class UploadTestController {
@GetMapping("/uploadex")
public void uploadex() {

}
}

5)REST API를 위한 Controller를 생성하고 파일 업로드 요청을 처리하는 메서드를 작성 - UploadController
@RestController
@Log4j2
public class UploadController {
@PostMapping(value="/uploadajax")
public void uploadFile(MultipartFile[] uploadFiles) {
for(MultipartFile uploadFile : uploadFiles) {
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("fileName:" + fileName);
}
}
}

6)templates 디렉토리에 uploadex.html 파일을 추가하고 작성
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>파일 업로드</title>
</head>
<body>
<input name="uploadFiles" id="uploadFiles" type="file" accept="image/*"
multiple />
<button class="uploadBtn">업로드</button>
<img id="img" width="200" height="200" border="1" />
</body>

<!-- integrity 는 소스 코드가 조작되었는지 확인하기 위한 해시값이고 
crossorigin 은 동일한 도메인이 아닐 때 코드를 공유할 수 있도록 해주는 속성 -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>

<script>
$('.uploadBtn').click(function(){
//파일 전송을 위해서 FormData 생성
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;

if(files.length < 1){
alert("업로드 할 파일이 선택되지 않았습니다.");
return;
}
for(var i = 0; i<files.length; i++){
console.log(files[i]);
formData.append("uploadFiles", files[i])
}

$.ajax({
url:"/uploadajax",
processData:false,
contentType:false,
data:formData,
type:"POST",
dataType:"json",
success:function(result){
console.log(result);
},
error:function(jqXHR, textstatus, errorThrown){
console.log(textstatus);
}
})
});

document.getElementById("uploadFiles").addEventListener("change", function(){
//이벤트를 처리할 때 this는 이벤트가 발생한 객체입니다.
//여기서는 document.getElementById("uploadFiles") 입니다.
readURL(this);
});

function readURL(input){
if(input.files && input.files[0]){
var filename = input.files[0].name;

var reader = new FileReader();

reader.readAsDataURL(input.files[0]);

reader.addEventListener("load", function(e){
document.getElementById("img").src = e.target.result;
})
}
}
</script>
</html>

7)실행하고 파일을 업로드 했을 때 콘솔에 파일 이름이 출력되는지 확인

8)application.properties 파일에 업로드를 위한 디렉토리를 변수로 추가
com.adamsoft.upload.path = C:\\Users\\tjoeun304\\Documents\\data

9)UploadController 클래스에 위의 변수를 읽는 부분을 생성
@Value("${com.adamsoft.upload.path}")
private String uploadPath;

10)UploadController에 디렉토리를 생성해주는 사용자 정의 메서드를 추가
private String makeFolder() {
//오늘 날짜를 문자열로 가져옴
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
// /문자열을 파일의 경로 구분자로 변경
String realUploadPath = str.replace("//", File.separator);
//uploadPath 와  realUploadPath를 합쳐서 파일 객체를 생성
File uploadPathDir = new File(uploadPath, realUploadPath);
//이 파일이 존재하지 않는다면 디렉토리를 생성
if(uploadPathDir.exists() == false) {
uploadPathDir.mkdirs();
}
//디렉토리 이름을 리턴
return realUploadPath;
}

11)UploadController 클래스의 파일 업로드 처리 메서드를 수정
@PostMapping(value="/uploadajax")
public void uploadFile(MultipartFile[] uploadFiles) {
//log.info("uploadPath:" + uploadPath);
for(MultipartFile uploadFile : uploadFiles) {
//이미지 파일이 아닌 파일이 있으면 업로드 중지
if(uploadFile.getContentType().startsWith("image") == false) {
log.warn("이미지 파일만 업로드 하세요");
return;
}

String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("fileName:" + fileName);

//파일을 저장할 디렉토리 생성
String realUploadPath = makeFolder();

//UUID 생성
String uuid = UUID.randomUUID().toString();
//실제 저장할 파일 경로를 생성
String saveName = uploadPath + File.separator + realUploadPath + 
File.separator + uuid + fileName;
Path savePath = Paths.get(saveName);
try {
//파일 업로드
uploadFile.transferTo(savePath);
}catch(Exception e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
}

12)브라우저에서 파일을 업로드 하고 컴퓨터에 오늘 날짜로 디렉토리가 생성되고 그 안에 파일이 업로드 되었는지 확인
=>파일 이름은 원본 이름 앞에 UUID가 추가되어 있습니다.


13)업로드 결과를 반환하기 위한 UploadResultDTO 클래스를 생성
=>파일 이름, uuid, 업로드 경로를 소유

@Data
@AllArgsConstructor
public class UploadResultDTO {

private String fileName;
private String uuid;
private String uploadPath;

//이미지 경로를 리턴해주는 메서드
public String getImageURL() {
try {
//파일에 한글이 있을 경우를 대비해서 UTF-8로 인코딩
return URLEncoder.encode(uploadPath + "/" + uuid + fileName, "UTF-8");
}catch(Exception e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
return "";
}
}


14)UploadController 클래스의 파일 업로드를 처리하기 위한 메서드를 수정
@PostMapping(value="/uploadajax")
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) {
//리턴할 List 생성
List<UploadResultDTO> resultDTOList = new ArrayList<>();

//log.info("uploadPath:" + uploadPath);
for(MultipartFile uploadFile : uploadFiles) {
//이미지 파일이 아닌 파일이 있으면 업로드 중지
if(uploadFile.getContentType().startsWith("image") == false) {
log.warn("이미지 파일만 업로드 하세요");
//실패 상태를 전송합니다.
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}

String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("fileName:" + fileName);

//파일을 저장할 디렉토리 생성
String realUploadPath = makeFolder();

//UUID 생성
String uuid = UUID.randomUUID().toString();
//실제 저장할 파일 경로를 생성
String saveName = uploadPath + File.separator + realUploadPath + 
File.separator + uuid + fileName;
Path savePath = Paths.get(saveName);
try {
//파일 업로드
uploadFile.transferTo(savePath);

resultDTOList.add(new UploadResultDTO(fileName, uuid, realUploadPath));
}catch(Exception e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
}

15)브라우저에서 파일을 업로드 하고 확인 - 콘솔에서 에러가 없어집니다.

16)업로드 한 이미지를 화면에서 출력하기 위해서 UploadController 에 이미지를 다운로드하는 요청을 처리하는 메서드를 생성
//파일의 내용을 전송하는 요청을 처리해주는 메서드
@GetMapping("/display")
public ResponseEntity<byte []> getFile(String filename){
ResponseEntity <byte []> result = null;
try {
//파일 이름을 가지고 파일 경로를 생성
File file = new File(uploadPath + File.separator + 
URLDecoder.decode(filename, "UTF-8"));
//Header 는 데이터의 종류가 어떤 것인지 알려주기 위한 용도
HttpHeaders  header = new HttpHeaders();
//Content-Type에 이 파일의 종류가 무엇인지 설정을 해줍니다.
header.add("Content-Type", Files.probeContentType(file.toPath()));
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, 
HttpStatus.OK);

}catch(Exception e) {
log.error(e.getLocalizedMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}


17)uploadex.html 파일에 이미지 출력 영역을 생성
<div class="uploadResult"></div>

18)uploadex.html 파일에 업로드된 이미지를 호출하는 함수를 스크립트에 작성
function showUploadedImages(ar){
var divArea = $(".uploadResult");
for(var i=0; i<ar.length; i++){
divArea.append("<img src='/display?filename=" + ar[i].imageURL + "'>")
}
}

19)uploadex.html 파일의 파일 업로드 요청 함수 안에 업로드된 이미지를 호출하는 함수를 호출하는 구문을 추가
$('.uploadBtn').click(function(){
//파일 전송을 위해서 FormData 생성
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;

if(files.length < 1){
alert("업로드 할 파일이 선택되지 않았습니다.");
return;
}
for(var i = 0; i<files.length; i++){
console.log(files[i]);
formData.append("uploadFiles", files[i])
}

$.ajax({
url:"/uploadajax",
processData:false,
contentType:false,
data:formData,
type:"POST",
dataType:"json",
success:function(result){
console.log(result);
showUploadedImages(result);
},
error:function(jqXHR, textstatus, errorThrown){
console.log(textstatus);
}
})
});









    'Spring' 카테고리의 다른 글
    • Request Method 반환 값 & score 3일차 과제 메모
    • [SpringBoot] Git 연동하기
    • [SpringBoot] 2022/05/30 OneToMany
    • [SpringBoot] 2022/05/24 ToDo (REST API)

    티스토리툴바