ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 43일차 - 파일 업로드 다운로드
    백엔드(웹 서버, WAS)/Spring 2024. 3. 22. 17:49

    회원, 게시글, 게시글에 첨부된 사진 테이블 구조

    create table member (
    	id VARCHAR(50)
    	,pw VARCHAR(100)
    	,name VARCHAR(50)
    	,age INT(4)
    	,gender VARCHAR(4)
    	,email VARCHAR(100)
    );
    
    create table bbs (  
    	idx int(8) primary key auto_increment
    	,subject varchar(100)
    	,user_name varchar(50)
    	,content varchar(500)
    	,bHit int(8) default 0
    	,reg_date date default current_date
    );	
    
    create table photo (
    	file_idx int(8) primary key auto_increment
    	,ori_filename varchar(200)
    	,new_filename varchar(200)
    	,reg_date date default current_date
    	,idx int(8)
    	,constraint foreign key(idx) references bbs(idx) on delete cascade
    );

    commons-io, fileupload 라이브러리 pom.xml 에 등록하기

    https://central.sonatype.com/artifact/commons-io/commons-io

     

    Maven Central: commons-io:commons-io

    Discover commons-io in the commons-io namespace. Explore metadata, contributors, the Maven POM file, and more.

    central.sonatype.com

    https://central.sonatype.com/artifact/commons-fileupload/commons-fileupload

     

    Maven Central: commons-fileupload:commons-fileupload

    Discover commons-fileupload in the commons-fileupload namespace. Explore metadata, contributors, the Maven POM file, and more.

    central.sonatype.com

    		<!-- commons-io -->
    		<!-- https://central.sonatype.com/artifact/commons-io/commons-io -->
    		<dependency>
    		    <groupId>commons-io</groupId>
    		    <artifactId>commons-io</artifactId>
    		    <version>2.7</version>
    		</dependency>
    		<!-- commons-fileupload -->
    		<!-- https://central.sonatype.com/artifact/commons-fileupload/commons-fileupload -->
    		<dependency>
    		    <groupId>commons-fileupload</groupId>
    		    <artifactId>commons-fileupload</artifactId>
    		    <version>1.3.3</version>
    		</dependency>

     

    commons-io 는 2.15.1 에서 2.7 로 commons-fileupload 는 1.3.3 에서 1.5 로 바꾸어줘야 한다

    root-context.xml 에서 파일 첨부 관련 빈을 등록하기

    일반적인 리퀘스트 객체는 문자열만 왔다갔다 

    멀티파트는 리퀘스트 객체에 담긴 정보가 문자열만 오지 않을 경우 어떻게 처리할지 

    	<!-- 서버가 켜졌을때 읽는 설정 -->
    	<!-- 파일 첨부 설정 -->
    	<!-- defaultEncoding : 혹시라도 파일 이름이 한글이면 깨져버리니깐 UTF-8로 인코딩 -->
    	<!-- maxUploadSize : 최대 업로드 가능한 크기
    	우리 서버가 1기가 밖에 저장 못하는데 누군가가 1기가를 저장하라고 하면 금지 시키야한다 -->
    	<!-- maxInMemorySize : 데이터 전송에서 주고 받을때 사용하는 버퍼의 크기이다
    	애는 MB 단위가 아니고 B(바이트)이다 직접 개발자가 버퍼 크기를 계산해서 설정해줘야한다 -->
       <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
          <property name="defaultEncoding" value="UTF-8"/>
          <property name="maxUploadSize" value="10485760"/>
          <property name="maxInMemorySize" value="10485760"/>
       </bean>

    writeForm.jsp 에서 서버로 파일 전송 기능 만들기

     

    		<tr>
    			<th>사진</th>
    			<td><input type="file" name="photos"/></td>
    		</tr>

     


    BoardController 에서는 클라이언트가 보낸 파일을 파라미터로 전달받는다

    MultipartFile 파라미터는 다른 파라미터보다 앞에 있어야 인식된다

    	<!-- 파일을 주고받을 때에는 POST 방식으로 보내야한다 -->
    	<!-- 멀티파트로 구성된 form-data 가 여러 파트로 구성되어 있다고 알릴것 -->
    	<form action="write" method = "post" enctype="multipart/form-data">

    	// multipart 는 다른 파라미터보다 앞에 있어야한다
    	@RequestMapping(value="/write", method = RequestMethod.POST)
    	public String write(MultipartFile photos, HttpSession session
    			,@RequestParam Map<String,String>param) {
    		logger.info("글작성 요청");
    		String page = "redirect:/list";
    		if(session.getAttribute("loginId")!=null) {
    			int row = service.write(photos, param);
    			if(row < 1) {
    				page ="writeForm";
    			}
    		}
    		
    		return page;
    	}

    전달 받은 파일의 이름을 로그에 출력해 정상적으로 파일을 전달받았는지 호가인한다

    	public int write(MultipartFile photos,
    			Map<String, String> param) {
    		int row = -1;
    		row = dao.write(param);
    		
    		fileSave(photos);
    		
    		return row;
    	}
    
    	public void fileSave(MultipartFile photos) {
    		// 1. 업로드할 파일이름이 있는가?
    		// 1-1. 업로드할 파일이 있다면 이름이 나타나고 아니라면 없을 것이다
    		String fileNames = photos.getOriginalFilename();
    		logger.info(fileNames);
    	}

    최대로 업로드가 가능한 크기는 10MB 이하이다 그 크기보다 작은 파일을 선택하자

    서버 로그에서 정상적으로 서버에 파일의 정보가 전송되었는지 확인한다


    파일을 저장할려면 서버에서도 파일을 저장할 장소(경로)가 필요하다

    C드라이브에 아래 upload 폴더를 만든다

    파일을 전달받으면 생기는 문제, 동일한 이름을 가진 파일은 덮어씌위기 된다

    파일 이름의 중복 때문에 덮어씌워지는 문제 해결방법

    1. 암호화 해쉬(실무에서 사용된다)

    2. 현재 시간은 밀리세컨드 파일이 한꺼번에 밀려들지 않는 이상은 이름이 바뀐다 (실무에서는 좋은 방식이 아니다)

     

    파일을 전달받으면 이름을 현재 시간의 밀리세컨드로 바꾸어버리고 upload 폴더에 저장한다


    	public String file_root = "C:/upload/";

    	public void fileSave(MultipartFile photos) {
    		// 1. 업로드할 파일이름이 있는가?
    		// 1-1. 업로드할 파일이 있다면 이름이 나타나고 아니라면 없을 것이다
    		String fileName = photos.getOriginalFilename();
    		logger.info("upload file name : " + fileName);
    		// 2. 파일을 저장하라면 서버에서 파일을 저장할 장소가 필요하다
    		if (!fileName.equals("")) { // 파일 이름이 있다면 == 업로드 파일이 있다면
    			// 1. 기존 파일명에서 확장자 추출(high.gif)
    			// 1-1. "." 은 특수문자로 인식하기 때문에 \\(역슬래시) 두번 넣어줘야한다
    			/* String[] arr = fileName.split("\\.");
    			String ext = arr[arr.length-1]; */
    			// 1-2. high.gif 
    			// fileName.lastIndexOf(".") : 파일의 이름에서 뒷 문자(f)부터 . 이 있는지 찾고 그 위치(인덱스)를 반환한다
    			// fileName.length() : 파일 이름의 끝까지
    			String ext = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
    			// 파일의 이름에서 마지막 . 부터 시작해서 끝 문자열을 가져온다 즉 확장자명을 가져온다 
    			
    			// 2. 새 파일의 이름 생성
    			String newFileName = System.currentTimeMillis() + "." + ext;
    			logger.info(fileName + "->" + newFileName);
    			
    			// 3. 파일 저장
    			try {
    				byte[] bytes = photos.getBytes(); // multipartFile 로 부터 바이너리 추출한다
    				// 1. fileoutstream, buffered
    				// 2. nio (java 1.6 버젼 이상에서 사용가능
    				Path path = Paths.get(file_root + newFileName); // 서버에서 파일의 저장 경로 지정한다
    				Files.write(path, bytes); // 저장할 파일의 데이터가 담긴 bytes 를 path(저장 경로) 에 저장한다
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    	}

    게시글에 해당되는 파일이 어디에 저장됬는지 알 수 없기 때문에 데이터베이스에 파일과 관련된 정보를 저장해야한다

    1. 파일의 고유 번호

    2. 이전 파일의 이름

    3. 바뀐 파일의 이름

    4. 파일이 업로드된 날짜

    5. 파일이 업로드한 게시글의 번호(FK, 왜리키)

    파일의 정보를 저장할 photo 테이블을 만든다

    create table photo (
    	file_idx int(8) primary key auto_increment
    	,ori_filename varchar(200)
    	,new_filename varchar(200)
    	,reg_date date default current_date
    	,idx int(8)
    	,constraint foreign key(idx) references bbs(idx) on delete cascade
    );
    
    -- 연계 참조 무결성 제약조건
    -- 부모가 지워질려고 하면 먼저 자식을 지워야하는데, 자동으로 자식들을 다 지우고
    -- 부모를 지워주는 제약조건이 on delete cascade 이다

    연계 참조 무결성

    부모(게시글) 이 지울려면 자식(파일)을 먼저 지우고, 나중에 부모를 지워야한다


    글쓰기 작성 후 저장 버튼을 눌렀을때 photo 테이블에 어떻게 파일정보를 insert 할지 쿼리문을 작성하시오

     

    insert into photo(ori_filename, new_filename, idx)
    values("apple.jpg", "134950345.jpg", idx);

    어떻게 해야 bbs 테이블에 방금 넣은 idx 값을 가져올 수 있을까?

    1) bbs 테이블에서 가장 마지막 게시글 정보를 가져온다
    x, 왜냐하면 조회하는 사이에 또 게시글이 추가될 수 있다

    	public int write(MultipartFile photos,
    			Map<String, String> param) {
    		int row = -1;
    		// insert 후 생성된 idx 가져오는 방법
    		// 조건 1. 파라미터는 DTO 로 넣을 것
    		BoardDTO dto = new BoardDTO();
    		dto.setUser_name(param.get("user_name"));
    		dto.setSubject(param.get("subject"));
    		dto.setContent(param.get("content"));
    		row = dao.write(dto); // 글쓰기 완료 후 
    		// 조건 3. 이후 DTO 에서 저장된 키 값을 받아온다
    		int idx = dto.getIdx();
    		logger.info("idx={}", idx);
    		if (row > 0) {
    			fileSave(idx, photos);
    		}
    		return row;
    	}

    	<!-- 조건 2. 추가 설정 -->
    <!-- 		
    		useGeneratedKeys="true" : insert 후 생성된 키 가져오기 설정하기 
    		keyColumn="idx" : 가져올 키(PK)의 이름을 지정하기
    		keyProperty="idx" : 키를 저장할 DTO 속성(필드)의 이름
    		방금 INSERT 한 내용에 대해서 idx(PK) 를 가져올 수 있고 이것을 DTO(JAVA 영역) 에 넣어 줄 수 있다
    -->
    	<insert id="write"
    		useGeneratedKeys="true"
    		keyColumn="idx"
    		keyProperty="idx"
    		parameterType="kr.co.photo.board.dto.BoardDTO">
    		INSERT INTO bbs(
    			subject, user_name,content
    			)values(
    			#{subject},#{user_name},#{content}
    			)
    	</insert>

    	public void fileSave(int idx, MultipartFile photos) {
    		// 1. 업로드할 파일이름이 있는가?
    		// 1-1. 업로드할 파일이 있다면 이름이 나타나고 아니라면 없을 것이다
    		String fileName = photos.getOriginalFilename();
    		logger.info("upload file name : " + fileName);
    		// 2. 파일을 저장하라면 서버에서 파일을 저장할 장소가 필요하다
    		if (!fileName.equals("")) { // 파일 이름이 있다면 == 업로드 파일이 있다면
    			// 1. 기존 파일명에서 확장자 추출(high.gif)
    			// 1-1. "." 은 특수문자로 인식하기 때문에 \\(역슬래시) 두번 넣어줘야한다
    			/* String[] arr = fileName.split("\\.");
    			String ext = arr[arr.length-1]; */
    			// 1-2. high.gif 
    			// fileName.lastIndexOf(".") : 파일의 이름에서 뒷 문자(f)부터 . 이 있는지 찾고 그 위치(인덱스)를 반환한다
    			// fileName.length() : 파일 이름의 끝까지
    			String ext = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
    			// 파일의 이름에서 마지막 . 부터 시작해서 끝 문자열을 가져온다 즉 확장자명을 가져온다 
    			
    			// 2. 새 파일의 이름 생성
    			String newFileName = System.currentTimeMillis() + "." + ext;
    			logger.info(fileName + "->" + newFileName);
    			
    			// 3. 파일 저장
    			try {
    				byte[] bytes = photos.getBytes(); // multipartFile 로 부터 바이너리 추출한다
    				// 1. fileoutstream, buffered
    				// 2. nio (java 1.6 버젼 이상에서 사용가능
    				Path path = Paths.get(file_root + newFileName); // 서버에서 파일의 저장 경로 지정한다
    				Files.write(path, bytes); // 저장할 파일의 데이터가 담긴 bytes 를 path(저장 경로) 에 저장한다
    				dao.fileWrite(fileName, newFileName, idx);
    			} catch (IOException e) {
    				e.printStackTrace();
    			}
    		}
    	}

    public interface BoardDAO {
    
    	List<BoardDTO> list();
    
    	void del(String idx);
    
    	int write(BoardDTO dto);
    
    	BoardDTO detail(String idx);
    
    	void upHit(String idx);
    
    	void update(Map<String, String> params);
    
    	void fileWrite(String fileName, String newFileName, int idx);
    
    }

    <insert id="fileWrite">
        INSERT INTO photo (ori_filename,new_filename,idx)
            VALUES (#{param1},#{param2},#{param3})
    </insert>

    사진 읽어오는 기능 구현하기 

    1. LEFT 아우터 RIGHT 아우터 유니온해서 조인하기

    해당 게시글과 첨부된 파일의 정보들을 모두 가져와 사용자에게 보여준다

    컨트롤러

    컨트롤러에서는 게시글을 상세하게 보고 싶다는 요청이 들어오면 서비스에게 게시글과 파일의 정보을 요구한다

    	@RequestMapping(value="/detail")
    	public String detail(String idx, HttpSession session, Model model) {
    		String page="redirect:/list";
    		logger.info("detail idx="+idx);
    		
    		if(session.getAttribute("loginId")!= null) {
    //			BoardDTO bbs = service.detail(idx);
    //			model.addAttribute("bbs", bbs);
    			// model 줄태니 여기에 bbs 와 photos 담아와라
    			// 하지만 model 은 인터페이스이므로 객체화가 안된다
    			// 그래서 서비스한테 두가지를 전달한다
    			service.detail(idx, model);
    			
    			page = "detail";
    		}
    		return page;
    	}

    서비스

    dao 로 부터 게시글의 정보를 dto(BoardDTO) 에 저장한다 해당 게시글에 첨부된 파일의 데이터를 가져오기 위해 

    	public void detail(String idx, Model model) {
    		dao.upHit(idx);
    		// 컨트롤러로부터 전달받은 model 에다가 게시글 정보를 담는다
    		BoardDTO dto = dao.detail(idx);
    		model.addAttribute("bbs", dto);
    		// 여기서 게시글에 대한 dto 는 있지만 사진 파일에 대한 정보가 담긴 dto 클래스는 없어서 생성해준다
    		List<PhotoDTO> photos = dao.photos(idx);
    		logger.info("photos : {}", photos);
    		model.addAttribute("photos", photos);
    	}

     

    Board_mapper.xml

    해당 게시글의 첨부된 파일의 예전 이름(사용자가 업로드 당시)과 서버에 저장된 이름(업로드 당시의 밀리세컨드 시간), 파일의 고유번호를 PhotoDTO 형태로 다시 반환한다(dao.photos)

    	<select id="photos" resultType="kr.co.photo.board.dto.PhotoDTO">
    		SELECT 
    			ori_FileName
    			,new_FileName
    			,file_idx
    		FROM photo WHERE idx=#{param1}
    	</select>

    View, detail.jsp

    모델(서버)로부터 전달받은 데이터는 bbs(BoardDTO)와 photos(List<PhotoDTO>) 이다 bbs 는 게시글의 조회수, 제목, 작성자, 내용 정보를 보여주고, photos 는 첨부된 사진(파일) 을 리스트 형태로 가지고 있다 

     

    이미지 태그는 src 에 입력된 경로로 사진(파일)을 찾아가 사용자에게 사진을 보여준다 이때 경로에 /photo 는 서버에 미리 저장된 C:/upload/ 와 매핑되어있어 자동적으로 C:/upload/<photo.new_filename> 로 경로로 찾을 수 있다

     


    server.xml 설정

     

    <!-- 
    	서버는 /main 이라는 요청이 들어오면 프로젝트 경로로 다시 돌려주어 
    	스프링 컨테이너가 이를 처리한다
    -->
    <Context docBase="13_photoboard_jm" path="/main" reloadable="true" 
    	source="org.eclipse.jst.jee.server:13_photoboard_jm"/>

     

    <!-- /photo 라는 요청이 서버에 들어오면 C:/upload/ 경로로 다시 돌려준다 -->
    <Context docBase="C:/upload/" path="/photo"/>

     


    파일을 여러개 첨부할 수 있는 기능을 만들기

    	public void fileSave(int idx, MultipartFile[] photos) {
    		for (MultipartFile photo : photos) {
    			// 1. 업로드할 파일이름이 있는가?
    			// 1-1. 업로드할 파일이 있다면 이름이 나타나고 아니라면 없을 것이다
    			String fileName = photo.getOriginalFilename();
    			logger.info("upload file name : " + fileName);
    			// 2. 파일을 저장하라면 서버에서 파일을 저장할 장소가 필요하다
    			if (!fileName.equals("")) { // 파일 이름이 있다면 == 업로드 파일이 있다면
    				// 1. 기존 파일명에서 확장자 추출(high.gif)
    				// 1-1. "." 은 특수문자로 인식하기 때문에 \\(역슬래시) 두번 넣어줘야한다
    				/* String[] arr = fileName.split("\\.");
    				String ext = arr[arr.length-1]; */
    				// 1-2. high.gif 
    				// fileName.lastIndexOf(".") : 파일의 이름에서 뒷 문자(f)부터 . 이 있는지 찾고 그 위치(인덱스)를 반환한다
    				// fileName.length() : 파일 이름의 끝까지
    				String ext = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
    				// 파일의 이름에서 마지막 . 부터 시작해서 끝 문자열을 가져온다 즉 확장자명을 가져온다 
    				
    				// 2. 새 파일의 이름 생성
    				String newFileName = System.currentTimeMillis() + "." + ext;
    				logger.info(fileName + "->" + newFileName);
    				
    				// 3. 파일 저장
    				try {
    					byte[] bytes = photo.getBytes(); // multipartFile 로 부터 바이너리 추출한다
    					// 1. fileoutstream, buffered
    					// 2. nio (java 1.6 버젼 이상에서 사용가능
    					Path path = Paths.get(file_root + newFileName); // 서버에서 파일의 저장 경로 지정한다
    					Files.write(path, bytes); // 저장할 파일의 데이터가 담긴 bytes 를 path(저장 경로) 에 저장한다
    					dao.fileWrite(fileName, newFileName, idx);
    					Thread.sleep(1); // 파일명을 위해 강제 휴식 부여하기 
    				} catch (Exception e) {
    					e.printStackTrace();
    				}
    			}		
    		}
    	}

    첨부된 파일 추가하기

    View, updateForm.jsp

    		<tr>
    			<th>사진</th>
    			<td><input type="file" name="photos" multiple="multiple"/></td>
    		</tr>
    		<c:if test="${photos.size()>0}">
    		<tr>
    			<th>사진</th>
    			<td>
    				<!-- 이런 요청이 왔을때 컨트롤러에 연결해서 그 요청을 처리해 -->
    				<!-- 톰캣 서버에서 어떤 요청이 오면 어디로 이동해! -->
    				<c:forEach items="${photos}" var="photo">
    					<img src="/photo/${photo.new_filename}"/>
    				</c:forEach>
    			</td>
    		</tr>
    		</c:if>

     

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

     

     


    게시글 삭제시 파일까지 삭제

     

Designed by Tistory.