Spring

[Spring] 웹페이지에 파일 업로드하도록 개발하기(CRUD) - 1편

Journey Jeong 2023. 5. 8. 13:10

파일 업로드의 경우 파일이 여러개 이고 보여지는화면이 하나이기에 1:N 으로 구성되어있음

따라서 , 테이블 생성 시 foreign key 지정이 필요함 

 

* 사전에 html 메뉴 생성해놓기 

 

 

 

1. 데이터 생성하기 

: 두개의 데이터 테이블 생성

create table goods( 
	gno bigint primary key auto_increment,
	name varchar(255) not null,
	content text not null,
	price int not null,
	created_date timestamp default current_timestamp(),
	updated_date timestamp default current_timestamp() on update current_timestamp()
);

create table goods_file( 
	fno bigint primary key auto_increment,
	url varchar(255) not null,
	name varchar(255) not null,
	size bigint not null,
	created_date timestamp default current_timestamp(),
	updated_date timestamp default current_timestamp() on update current_timestamp(),
	gno bigint not null,
	constraint fk_goods_file_goods foreign key(gno) references goods(gno)
);

 

 

 

2. 맵핑할 클래스 생성하기 

 

- main에  NoArgsConstructor 와 AllArgsConstructor 를 두개 쓰는 이유 : https://cantcoding.tistory.com/61

 

<롬복 코드 참고: https://dingue.tistory.com/14

@Builder
성자에 인자가 많을 때, 불필요한 생성자를 제거하고, 가독성+객체불변성+일관성 등 유연함을 부여함
- 빌더패턴의 표현, 파라미터 있는 생성자에만 쓸 수 있다.
즉 NoArgsConstructor 은 단독으로 사용하지 못함
@Getter  단순히 필드를 리턴하는 것 
@Setter 단순히 필드에 값을 설정해 주는 것 
@NoArgsConstructor  파라미터가 없는 생성자를 생성함 
- 필드가 final 로 생성되어 있는 경우 오류가 발생함 
@AllArgsConstructor  클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성해줌 
- NonNull 마크가 되어있다면 생성자 내에서 null-check 로직으로 자동 생성 
@RequiredConstructor 초기화되지 않은 모든 final 필드, @NonNull 로 마크되어있는 모든 필드들의 생성자를 자동 생성 
package com.green.nowon.domain.entry;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

//@RequiredArgsConstructor //final 이나 @nonnull인 필드값만 파라미터로 받는 생성자 
@Builder //생성자에 인자가 많을 때, 불필요한 생성자를 제거하고, 가독성+객체불변성+일관성 등 유연함을 부여함 
@AllArgsConstructor //모든 필드값을 파라미터로 받는 생성자로 만듬(필드 6개 모두를 초기화하는 역할을하는 파라미터가 정의된 생성자) 
@NoArgsConstructor //파라미터가 없는 기본생성자를 생성(디폴트 초기화, 인자없이 생성할 수 있는 생성자) 
@Getter
public class GoodsEntity {

	private long gno; // 상품관리번호
	private String name; //상품명
	private String content; //내용
	private int price; //가격
	private LocalDateTime createdDate;
	private LocalDateTime updatedDate;
}
package com.green.nowon.domain.entry;

import java.time.LocalDateTime;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GoodsFileEntity {
	
	private long fno;
	private String url; //파일 경로
	private String name; // 파일이름
	private long size; //파일사이즈 byte 
	private LocalDateTime createdDate;
	private LocalDateTime updatedDate;
	//FK 
	private long gno; //FK Goods 에 있는 pk  
}

 

 

 

 

3. 컨트롤러 생성

 

package com.green.nowon.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class GoodsController {

	@GetMapping("/goods")
	public String list() {
		return "goods/list" ;
	}
}

 

 

 

4. HTML 생성 

 

- Thymeleaf 에서 this 는 생략 가능 

<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" 
		th:replace="layout/layout1 :: layout(~{this::head},~{this::main})" > 
	<!-- 호출 th:replace 또는 th:insert |여기 테플릿 head와 main 에 넣을게요-->
<head>
</head>
<main>
	<h1>메인영역</h1>
	<div class="wrap view-size">
		<section id="goods-list">
			<h1>상품목록 페이지</h1>
			<div class="wrap">
				<p class="tit"> 상품목록 페이지 </p>
				<div class= "flex end">
					<a href="/goods/new"> 상품등록 </a>
				</div>
			</div>
		</section>
	</div>
</main>
</html>

 

 

5. write page생성 

 

- controller 수정

package com.green.nowon.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class GoodsController {

	@GetMapping("/goods")
	public String list() {
		return "goods/list" ;
	}
	
	@GetMapping("/goods/new")
	public String write() {
		return "goods/write" ;
	}
}

 

- writer page 생성 

 

*파일은 기존 text 불러오는것과는 별도로 구성을 해주어야 함 

-> 서버주소 정하기 : post = 저장, put = 수정, delete =삭제 등등
-> 파일업로드 할때: enctype="multipart/form-data" : default 파일 업로드 크기 제한은 1mb

-> name 을 넣어 데이터랑 맵핑 해주기 

 

- 중요 태그 

<form action="/goods" method="post" enctype="multipart/form-data">

<input type="file" name="img" accept="image/*" multiple="multiple"> // multiple 은 사진 여러개 올리는 것 

 
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" 
		th:replace="layout/layout1 :: layout(~{this::head},~{this::main})" > 
	<!-- 호출 th:replace 또는 th:insert |여기 테플릿 head와 main 에 넣을게요-->
<head>
	<!-- include libraries(jQuery, bootstrap) -->
	<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
	
	<!-- include summernote css/js -->
	<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
	<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>	
	
	<script type="text/javascript">
		$(document).ready(function() {
		  $('#summernote').summernote();
		});
	</script>
</head>
<main>
	<h1>메인영역</h1>
	<div class="wrap view-size">
		<section id="goods-list">
			<h1>상품목록 페이지</h1>
			<div class="wrap">
				<p class="tit"> 상품목록 페이지 </p>
				<!-- 파일 업로드 처리시  enctype="multipart/form-data -->
				<form action="/goods" method="post" enctype="multipart/form-data"> <!-- 서버주소 정하기  -->
				
					<p><input type="text" name="name" placeholder="상품명"></p>
					<p><input type="text" name="price" placeholder="가격">	</p>
					<!--  이미지는 file 로 type, accept 는 형식 -->
					<p>
						<label>파일은 2MB 이내의 이미지 파일만 허용합니다. </label>
						<input type="file" name="img" accept="image/*" ><!-- multiple="multiple" -->
					</p>
					<p><textarea id="summernote" name="content"></textarea></p>
					<p><button type="submit">등록</button>
				</form>
			</div>
		</section>
	</div>
</main>
</html>

 

 


 


6. 데이터 맵핑

 

- controller save 맵핑 

파일 - 맵핑 MultipartFile 이름 

- multiple="multiple" 할 시 맵핑은 배열로 표기해야함 

- name MultipartFile 객체 변수 이름과 일치하도록 설정 img 

- private final 로 서비스 인터페이스 객체 생성 후 final 이니까 @RequiredArgsConstructor 

- 저장할 메서드 만들기 

package com.green.nowon.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;
import com.green.nowon.service.GoodsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class GoodsController {

	//Autowired
	private final GoodsService service; 
	
	@GetMapping("/goods")
	public String list() {
		return "goods/list" ;
	}
	
	@GetMapping("/goods/new")
	public String write() {
		return "goods/write" ;
	}
	
	@PostMapping("/goods")
	public String save(MultipartFile img, GoodsSaveDTO dto) { //MultipartFile 이름 //dto 에 나머지 맵핑하기  
		/* 여러개 사진 올릴 시 배열로 처리해야함: (MultipartFile img[], GoodsSaveDTO dto)*/
		service.saveProcess(img,dto);
		return "goods/write" ;
	}
}

 

-> 나머지 정보 -  맵핑할 클래스 생성하기 

package com.green.nowon.domain.dto;

import com.green.nowon.domain.entry.GoodsEntity;

import lombok.Setter;
import lombok.ToString;

@ToString
@Setter
public class GoodsSaveDTO {

	private String name;
	private int price;
	private String content;
	
	//편의 메서드를 만들게요
	public GoodsEntity toEntity() {
		
		//builder.빌더클래스(필드값(위에랑 똑같이)).빌더클래스(필더값)... 
		return GoodsEntity.builder() //innerClass인 GoodsEntityBuilder 객체 생성 
				.name(name).price(price).content(content)// set name, price, content 한  GoodsEntityBuilder 객체  
				.build(); //
		//= return new GoodsEntity()l;
	}
}

- 용량제한 풀어주기

: application.yml 

  webflux:
    multipart:
      max-file-size: 2MB #default 1MB

 

 

 

7. 서비스 만들기 - interface 로 ! 

 

- controller 에서 적은대로 GoodsService.Interface 생성하기

package com.green.nowon.service;

public interface GoodsService {

}

 

- Interface 상속받을 클래스 만들기 
- > 상속받은 클래스에 @Service 

 

package com.green.nowon.service.impl;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;

import com.green.nowon.service.GoodsService;

@Service
public class GoodsServiceProcess implements GoodsService {


}

 

- 상품 정보저장할 method 생성 : service.saveProcess(img,dto); 

package com.green.nowon.service;

import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;

public interface GoodsService {

	void saveProcess(MultipartFile img, GoodsSaveDTO dto);

}

 

- 클래스에서 Override 하기 

- mapper.save(dto.toEntity()); 저장 메서드 만들기 

package com.green.nowon.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;
import com.green.nowon.mybatis.mapper.GoodsMapper;
import com.green.nowon.service.GoodsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class GoodsServiceProcess implements GoodsService {

	//DB 에 접근할 수 있는 DAO : mapper, repository -> interface 
	private final GoodsMapper mapper; //DI 종속객체를 주입한다(외부에서 주입): 내부 코드에서 결정 x (IoC)  
	
	@Override
	public void saveProcess(MultipartFile img, GoodsSaveDTO dto) {
		//상품정보를 DB - goods 테이블에 저장 
		mapper.save(dto.toEntity());
		
		//파일정보 저장 DB- goods-file 테이블에 저장 
		//서버에 업로드 
	}


}

 

- mapper Interface 도 생성 

 

package com.green.nowon.mybatis.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.green.nowon.domain.entry.GoodsEntity;

@Mapper
public interface GoodsMapper {

	void save(GoodsEntity entity);

}

 

 

8. xml  만들기

 

- goods 테이블의 변수로 값 셋팅 하기 

- goodsSaveDTO 와 동일하게 필드값 넣기 

 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.green.nowon.mybatis.mapper.GoodsMapper"> 
	
	<!-- 추상메서드에 있는 이름과id 가 일치해야함  -->
	<insert id="save"> 
		insert into goods(name, price, content)
		values(#{name}, #{price}, #{content})
	</insert>
</mapper>

 

 

9. Goods-file 도 mapper 하기 

 

- private final GoodsFileMapper fileMapper 와 같이 인터페이스 생성 

- 저장 메서드 작성하기 : 상품정보 저장( goods)  -> 서버에 업로드 -> 파일 정보 저장 (goods-file) 

package com.green.nowon.service.impl;

import java.io.File;
import java.io.IOException;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;
import com.green.nowon.domain.entry.GoodsFileEntity;
import com.green.nowon.mybatis.mapper.GoodsFileMapper;
import com.green.nowon.mybatis.mapper.GoodsMapper;
import com.green.nowon.service.GoodsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class GoodsServiceProcess implements GoodsService {
	
	//DB 에 접근할 수 있는 DAO : mapper, repository -> interface 
	private final GoodsMapper mapper; //DI 종속객체를 주입한다(외부에서 주입): 내부 코드에서 결정 x (IoC)  
	private final GoodsFileMapper fileMapper;
	
	@Override
	public void saveProcess(MultipartFile img, GoodsSaveDTO dto) throws Exception, IOException {
		////1. 상품정보 저장 : 상품정보를 DB - goods 테이블에 저장 
		//mapper.save(dto.toEntity());
		
		//  2.  서버에 업로드 
		//파일정보 저장 DB- goods-file 테이블에 저장 
		long size=img.getSize();
		String url="/images/upload/goods/"; //http 경로 
		String name=img.getOriginalFilename();
		String fileFolder="E:/ncs2023/spring/spring-config-client/src/main/resources/static/images/upload/goods/"; //업로드되는 서버의 주소 
		
		File dest= new File(fileFolder+name);
		img.transferTo(dest);
		System.out.println(">>>> : 파일 업로드 완료!");
		
		// 3. 파일 정보 저장 DB goods_file 테이블에 저장 
		fileMapper.save(GoodsFileEntity.builder()
				.size(size).url(url).name(name)
				.build());
	}


}

 

 

- 인터페이스 

package com.green.nowon.mybatis.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.green.nowon.domain.entry.GoodsFileEntity;

@Mapper
public interface GoodsFileMapper {

	void save(GoodsFileEntity build);

}

 

 

여기까지 하면, forengn key로 인한 오류가 남 

 

 

 

10. foreign key 설정하기 

따라서 설정한 forengn key 는 bno 로, 조회 후 값을 넣도록 설정해야함 

 

 

 

- goods-mapper 로 가서 아래와 같이 수정 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.green.nowon.mybatis.mapper.GoodsMapper"> 
	
	<!-- 추상메서드에 있는 이름과id 가 일치해야함  -->
	<insert id="save"> 
		<!-- 아래에 있는 insert 쿼리르 실행 후 auto_increment 로 처리되는 pk 값을 조회하여 gno에 결과로 맵핑 -->
		<selectKey keyProperty="gno" resultType="long" order="AFTER"> <!-- 아래 insert 후에 실행 --> 
			select LAST_INSERT_ID()
		</selectKey>
		insert into goods(name, price, content)
		values(#{name}, #{price}, #{content})
	</insert>
</mapper>

 

 

-serviceprocess 에 파일 정보 쿼리에 gno(goods.getGno()) 매핑 

package com.green.nowon.service.impl;

import java.io.File;
import java.io.IOException;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;
import com.green.nowon.domain.entry.GoodsEntity;
import com.green.nowon.domain.entry.GoodsFileEntity;
import com.green.nowon.mybatis.mapper.GoodsFileMapper;
import com.green.nowon.mybatis.mapper.GoodsMapper;
import com.green.nowon.service.GoodsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class GoodsServiceProcess implements GoodsService {
	
	//DB 에 접근할 수 있는 DAO : mapper, repository -> interface 
	private final GoodsMapper mapper; //DI 종속객체를 주입한다(외부에서 주입): 내부 코드에서 결정 x (IoC)  
	private final GoodsFileMapper fileMapper;
	
	@Override
	public void saveProcess(MultipartFile img, GoodsSaveDTO dto) throws Exception, IOException {
		////1. 상품정보 저장 : 상품정보를 DB - goods 테이블에 저장 
		GoodsEntity goods=dto.toEntity();
		System.out.println(" insert 실행 전 "+goods);
		mapper.save(goods);
		System.out.println(" insert 실행 후 "+goods); // pk 컬럼값이 매핑된 결과를 볼 수 있다. 
		
		//  2.  서버에 업로드 
		//파일정보 저장 DB- goods-file 테이블에 저장 
		long size=img.getSize();
		String url="/images/upload/goods/"; //http 경로 
		String name=img.getOriginalFilename();
		String fileFolder="E:/ncs2023/spring/spring-config-client/src/main/resources/static/images/upload/goods/"; //업로드되는 서버의 주소 
		
		File dest= new File(fileFolder+name);
		img.transferTo(dest);
		System.out.println(">>>> : 파일 업로드 완료!");
		
		// 3. 파일 정보 저장 DB goods_file 테이블에 저장 
		
		fileMapper.save(GoodsFileEntity.builder()
				.size(size).url(url).name(name).gno(goods.getGno())
				.build()); 
	}


}

 

- goods_files 에서 gno 잘 들어갔는지 확인 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.green.nowon.mybatis.mapper.GoodsFileMapper"> 
	
	<insert id="save"> 
		insert into goods_file(size, url, name, gno)
		values(#{size}, #{url}, #{name}, #{gno})
	</insert>
	
</mapper>

 

 

 

>>>>>>>>>> 완료 ! 

파일 업로드해서 데이터 잘 들어가는지 확인하기 ! 

 

 

----> 사진은 업로드 후 프로젝트 refresh 후 http://localhost:8080/images/upload/goods/파일명.파일형식으로 확인

 

 

 


 

파일 저장 다른방법 1

 

 

서버에 올리기

: 단. 3가지 서비스(DB올리기, 서버올리기, 저장하기) 가 있어서 효율이 좋지 않음

package com.green.nowon.service.impl;

import java.io.File;
import java.io.IOException;

import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.green.nowon.domain.dto.GoodsSaveDTO;
import com.green.nowon.domain.entry.GoodsEntity;
import com.green.nowon.domain.entry.GoodsFileEntity;
import com.green.nowon.mybatis.mapper.GoodsFileMapper;
import com.green.nowon.mybatis.mapper.GoodsMapper;
import com.green.nowon.service.GoodsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class GoodsServiceProcess implements GoodsService {
	
	//DB 에 접근할 수 있는 DAO : mapper, repository -> interface 
	private final GoodsMapper mapper; //DI 종속객체를 주입한다(외부에서 주입): 내부 코드에서 결정 x (IoC)  
	private final GoodsFileMapper fileMapper;
	
	@Override
	public void saveProcess(MultipartFile img, GoodsSaveDTO dto) throws Exception, IOException {
		////1. 상품정보 저장 : 상품정보를 DB - goods 테이블에 저장 
		GoodsEntity goods=dto.toEntity();
		System.out.println(" insert 실행 전 "+goods);
		mapper.save(goods);
		System.out.println(" insert 실행 후 "+goods); // pk 컬럼값이 매핑된 결과를 볼 수 있다. 
		
		//  2.  서버에 업로드 
		//파일정보 저장 DB- goods-file 테이블에 저장 
		long size=img.getSize();
		String url="/images/upload/goods/"; //http 경로 
		String name=img.getOriginalFilename();
		//String fileFolder="E:/ncs2023/spring/spring-config-client/src/main/resources/static/images/upload/goods/"; //업로드되는 서버의 주소 				
		//File dest= new File(fileFolder+name);
		
		ClassPathResource cpr = new ClassPathResource("/static"+url);	
		System.out.println("업로드 폴더정보: "+ cpr.getPath().toString());
		File dest=new File(cpr.getFile(), name);
		img.transferTo(dest);
		System.out.println(">>>> : 파일 업로드 완료!");
		
		// 3. 파일 정보 저장 DB goods_file 테이블에 저장 
		
		fileMapper.save(GoodsFileEntity.builder()
				.size(size).url(url).name(name).gno(goods.getGno())
				.build()); 
	}


}