본문 바로가기
Spring Boot

230221 Spring Boot 이미지 등록하기, @Transactional, selectKey

클라우드에 저장하면, DB에 찾을 수 있게 내용을 저장해줘야 한다.
일반파일과 달리 이미지파일은 db에 저장하고 화면을 출력해 주는 내용까지 있어야 함.

 

 

style.js에 파일업로드 함수가 만들어져 있다.

 

jquery는 다중 태그이벤트도 한번에 처리 가능. $('선택자1 선택자2')

 

productReg.html에

이미지파일 업로드가 가능한 태그가 3개 있다.

이 태그들은

 

<label class="upload-display" for="main_file">
     <span class="upload-thumb-wrap">
        <img class="upload-thumb" src="../img/plus_btn.png" >
    </span>
</label>
<input class="upload-name" value="파일선택" disabled="disabled">
<input type="file" name="file" id="main_file" class="upload-hidden"

로 되어 있고, form으로 감싸져 있다.

 

이미지 미리보기

label태그의 for="정한이름" 속성과 input의 id="정한이름"가 같으면 서로 연결된다.
파일은 input에 hidden으로 넣어서 form으로 넘긴다.

이 input은 모두 name이 file이다.
한번에 연결! 

 

 


ProductController.java

//등록
@PostMapping("/registForm")
public String registForm(@Valid ProductVO vo, Errors error, Model model, RedirectAttributes ra, @RequestParam("file")List<MultipartFile> lists) {
	if(error.hasErrors()) {
		List<FieldError> list=error.getFieldErrors();
		ArrayList<String> msglist=new ArrayList<>();
		for(FieldError err : list) {
			if(err.isBindingFailure()) {
				model.addAttribute("msg", "형식이 잘못되었습니다.");
			}else {
				msglist.add(err.getDefaultMessage());
			}
		}
		model.addAttribute("msglist", msglist);
		model.addAttribute("vo", vo);
		return "product/productReg";
	}
	
	//파일업로드 작업 -> 
	//리스트에서 빈값은 제거
	lists=lists.stream().filter((x)->x.isEmpty()==false).collect(Collectors.toList());
	
	//확장자가 image가 아니라면 경고문
	for(MultipartFile file:lists) {
		if(file.getContentType().contains("image") ==false ) {
			ra.addFlashAttribute("msg","이미지는 png, jpg, jpeg형식만 등록가능합니다.");
			return "redirect:/product/productReg";
		}
	}
	//파일업로드 작업을 ->service영역으로 위임
	
	
	//글 등록 작업
	int result=productService.regist(vo,lists);
	String msg=result==1?"정상 등록되었습니다":"등록에 실패했습니다";
	ra.addFlashAttribute("msg", msg);
	
	return "redirect:/product/productList"; //목록으로
}

@RequestParam("file")List<MultipartFile> list는 VO로도 받을 수도 있다.

확장자가 image가 아니라면 경고
이미지가 올라오면 file.getContentType이 image로 나온다.
-text/image
-image/png
-image/jpg
-image/jpeg
등등.

 

contains를 써서 이미지파일이 아닐 경우 경고메시지와 함께 리다이렉트.

 


ProductService.java

//글등록 (파일업로드)
public int regist(ProductVO vo,List<MultipartFile> list);

 

ProductServiceImpl.java

@Value("${project.uploadpath}")
private String uploadpath;

//날짜별로 폴더생성
public String makeDir() {
	Date date=new Date();
	SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
	String now=sdf.format(date);
	String path=uploadpath + "\\" +now; //경로
	File file = new File(path);
	if(file.exists()==false) {//파일이 존재하면 true
		file.mkdir(); //폴더생성
	}
	return now; //년월일 폴더위치
}
//글등록
//한 프로세스 안에서 예외가 발생하면, 기존에 진행했던 CRUD작업을 Rollback시킨다.
//조건-catch를 통해서 예외처리가 진행되면 트랜잭션 처리가 되지 않는다.
@Transactional(rollbackFor = Exception.class)
@Override
public int regist(ProductVO vo, List<MultipartFile> list) {
	
	//1. 글등록 처리->
	int result=productMapper.regist(vo);
	
	//2. 파일인서트
	for(MultipartFile file:list) {
		//파일명
		String origin = file.getOriginalFilename(); 
		//브라우저별로 경로가 포함되서 올라오는 경우가 있기에 간단한 처리.
		String filename=origin.substring(origin.lastIndexOf("\\")+1); 
		//폴더생성
		String filepath=makeDir();
		//중복파일의 처리
		String uuid=UUID.randomUUID().toString();
		//최종저장경로
		String savename=uploadpath+"\\"+filepath+"\\"+uuid+"_"+filename;
		try {
			File save = new File(savename); //세이브경로
			file.transferTo(save);//업로드 진행
		} catch (Exception e) {
			e.printStackTrace();
			return 0; //실패의 의미로 0
		}
		
		
		
		//인서트 - insert이전에 prod_id가 필요한데, selectKey방식으로 처리
		ProductUploadVO prodVO=ProductUploadVO.builder().filename(filename).filepath(filepath).uuid(uuid).prod_writer(vo.getProd_writer()).build();
		productMapper.registFile(prodVO);
		
	}//end for
	
	return result; //성공시 1, 실패시 0
}

※이건 미완성된 구문임. mapper와 연결하는게 두개가 있다. 둘 다 검사해야한다.

makeDir의 return과 savename부분이 이전과 다른데, db에 저장할 때 간단하게 저장해서 나중에 편하게 사용하기 위함.

 

service는 매개변수로 list를 하나 받는데, 이 list는 업로드한 이미지들이 들어있다.

 

ProductUploadVO를 생성하고 build를 사용해 값을 넣는다. 그리고 mapper로 연결.

 


@Transactional(rollbackFor = Exception.class)
한 프로세스 안에서 예외가 발생하면, 기존에 진행했던 CRUD작업을 Rollback시킨다.
조건-catch를 통해서 예외처리가 진행되면 트랜잭션 처리가 되지 않는다.
-throws Exception을 하면 Transactional은 동작하지 않는다.
-try-catch를 해도 Transactional은 동작하지 않는다.

spring에서 사용하려면 최소한의 설정을 해줘야 하는데, 스프링부트는 바로 사용 가능
메서드에서 어떠한 에러가 발생하면 sql을 전부 롤백해줌.
위에서 insert가 2번, select가 1번 발생하는데 그 중 하나가 에러를 발생해도 다른 건 실행될 수 있다.
이걸 막기 위해 롤백이 실행됨.

 

try-catch 바깥에서 insert를 실행한다. Transactional은 이때 에러를 찾으면 동작.



날짜별로 폴더생성하는 makeDir메서드의 return을 now로 바꾼다(년월일 폴더위치)
나중에 찾기 편하게 하기 위해.
makeDir의 return이 바뀌었으니 최종저장경로를 지정하는 구문도 수정.


테이블생성

############################파일업로드 테이블############################
CREATE TABLE PRODUCT_UPLOAD (
	UPLOAD_NO INT PRIMARY KEY auto_increment,
	FILENAME varchar(100) not null, ##실제파일명
	FILEPATH varchar(100) not null, ##폴더명
	UUID varchar(50) not null, ##UUID명
	REGDATE TIMESTAMP default now(),
	PROD_ID INT, ##FK
	PROD_WRITER VARCHAR(20) ##FK
);

1:N관계

PRODUCT가 1, PRODUCT_UPLOAD가 N

 

N쪽에 fk가 들어간다.
prod_id와 prod_writer를 fk로.
upload_no는 pk.


DB에 파일의 경로를 넣어 놓고, 나중에 찾아 쓴다.

filepath는 개발환경이나 클라우드저장 등에 따라 바뀔 수 있다.

 

이 테이블은 ProductUploadVO와 연결됨. 동일한 변수를 가지고 있다.




select max(prod_id) as prod_id from product where prod_writer = 'admin';

max는 위험할 수도 있다. 
db는 Acid를 제공. but 약간의 확률이 있을 수도.
지금은 where로 특정조건을 주므로 문제되지 않을 것.

 

 

 

 

 

 

ProductMapper.java

public int regist(ProductVO vo);
public int registFile(ProductUploadVO vo);

mapper에서
파일업로드 테이블과 연결하는 메서드의 매개변수는 vo나 맵으로.

 

 

 

ProductMapper.xml

<!-- 
	1. insert전에 product테이블의 키값을 selectKey태그를 이용해서 얻습니다.
	2. resultType은 조회된 결과 타입, 
	keyProperty는 sql에 전달되는 vo에 저장할 key값,
	order는 BEFORE, AFTER - insert 이전에 실행 or insert 이후에 실행
	 -->
	<insert id="registFile" parameterType="ProductUploadVO">
		<selectKey resultType="int" keyProperty="prod_id" order="BEFORE">
			select max(prod_id) as prod_id from PRODUCT where prod_writer=#{prod_writer}			
		</selectKey>
		insert into PRODUCT_UPLOAD(filename,
									filepath,
									uuid,
									prod_id,
									prod_writer)
		 values(#{filename},
		 		#{filepath},
		 		#{uuid},
		 		#{prod_id},
		 		#{prod_writer})
	</insert>

insert이전에 prod_id가 필요한데, 값을 구할 방법이 없다!
서비스의 regist메서드에서 prod_writer는 화면에서 넘어오는데 prod_id는 넘어오지 않는다.
따라서, 시퀀스를 사용하거나 해야 하는데, mysql은 시퀀스가 없다.

 

=>
mybatis의
<selectKey>태그
-order는 순서. insert 이전인가 이후인가.

-resultType은 조회된 결과 타입.
-keyProperty는 sql에 전달되는 vo에 저장할 key값. vo의 멤버변수에 저장하겠다는 선언.

 

이 태그는 insert태그 안에 있다. 따라서 select ~ as 이름으로 얻은 값을 insert의 sql구문에서 #{}로 사용할 수 있다.

 

 

 

동작이 끝나면 db에도 저장되고, 업로드되는 폴더에도 저장된다.

db에 저장되었으니 list화면에서도 찾아볼 수 있다. 

 

select *from product where prod_writer = 'admin' order by prod_id desc;
select * from product_upload;

위 select 결과의 일부분
아래 select 결과