반응형
여승철
INTP 개발자
여승철
  • 분류 전체보기 (376)
    • CS (16)
      • 면접 준비 (7)
      • 운영체제 (0)
      • 네트워크 (2)
      • HTTP (6)
      • 스프링(Spring) IoC 컨테이너 (0)
      • 알고리즘 (1)
    • Web (13)
    • AWS (6)
    • Java (43)
    • JSP & Servlet (65)
      • 개념 (42)
      • 실습 (23)
    • Spring Framework (33)
    • Spring Boot (10)
    • Spring Data (22)
      • JPA (14)
      • Query DSL (7)
      • Redis (1)
    • Spring Security (9)
    • Spring Batch (4)
    • MyBatis (10)
    • Front-End (51)
      • JS (27)
      • Vue.js (17)
      • React (5)
      • JQuery (0)
      • d3.js (2)
    • DBMS (24)
      • SQL, RDBMS (16)
      • MongoDB (5)
      • Redis (3)
    • Kafka (3)
    • 리눅스 (Linux) (4)
    • 디자인 패턴 (3)
    • VCS (8)
    • API (0)
    • TOOL (3)
    • Reading Book (28)
      • 이펙티브 자바 (11)
      • Clean Code (10)
      • 1분 설명력 (4)
      • HOW TO 맥킨지 문제해결의 기술 (3)
    • C# (4)
    • NSIS (6)
    • ETC (11)

블로그 메뉴

  • 홈
  • 태그

인기 글

태그

  • servlet
  • EC2
  • controller
  • mybatis
  • 게시판
  • HTTP
  • 이펙티브 자바
  • jsp
  • JDBC
  • 회원 관리
  • 로그인
  • querydsl
  • Dao
  • 환경 세팅
  • 맥킨지
  • JSTL
  • 디자인 패턴
  • Spring Batch
  • ubuntu
  • 스트림

최근 댓글

최근 글

hELLO· Designed By 정상우.
여승철

INTP 개발자

ETC

Spring + Ajax 파일 업로드

2022. 9. 25. 13:05
반응형

서버에서 첨부파일 처리 방식

  • cos.jar
    • 2002년에 개발이 종료되어, 더 이상 잘 사용되지 않음

  • commons-fileupload
    • 가장 많이 사용되는 방식

  • 자체적인 파일 업로드 처리
    • 서블릿 3.0 이상부터 자체적인 파일 업로드 처리가 API 상에서 지원

첨부파일시 고려해야할 것들

해당 포스팅은 아래 고려사항들을 적용하여 구현했습니다. 각 링크타고 들어가 포스팅을 읽어보는 것이 도움됩니다.
  • 동일한 이름으로 파일 업로드 시 기존 파일이 사라지는 문제
  • 이미지 파일의 경우 원본 파일의 용량이 큰 경우 섬네일 이미지를 생성해야 하는 문제
  • 이미지 파일과 일반 파일을 구분해서 다운로드 혹은 페이지에서 조회하도록 처리하는 문제
  • 첨부파일 공격에 대비하기 위한 업로드 파일의 확장자 제한

스프링 설정

서블릿 3.0 이상부터 지원되는 자체적인 파일 업로드 방식을 해보기 위해서 서블릿 버전을 바꿔준다.

 

SpringMVC 프로젝트를 실행하면 기본적으로 서블릿 버전이 2.5 버전이므로 web.xml에서 수정해준다.

 

추가로 파일 설정을 위해 <multipart-config> 태그도 추가해 준다.

 

 

▶web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	id="WebApp_ID" version="3.1">
    
    
    ...
    
    
    	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
		
		<!-- 저장 공간, 파일 크기, 한 번에 올리는 최대 크기, 특정 사이즈의 메모리 사용 크기 설정 -->
		<multipart-config>
			<location>D:\\upload\\temp</location>
			<max-file-size>20971520</max-file-size> <!--1MB * 20 -->
			<max-request-size>41943040</max-request-size><!-- 40MB -->
			<file-size-threshold>20971520</file-size-threshold> <!-- 20MB -->
		</multipart-config>
	</servlet>
    
    ...

 

 

 

▶servlet-context.xml

id는 'multipartResolver'라는 이름으로 지정된 이름을 사용
	<!-- file upload -->
	<context:component-scan base-package="com.spring.boardapp"></context:component-scan>
	
	<beans:bean id="multipartResolver" 
		class="org.springframework.web.multipart.support.StandardServletMultipartResolver">

	</beans:bean>

 

 

 

※ 자바 기반 설정할 경우

더보기

▶pom.xml

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<configuration>
                	<failOnMissingWebXml>false</failOnMissingWebXml>
				</configuration>
			</plugin>

pom.xml에서 web.xml이 없어도 문제가 없도록 위 plugin을 추가해준다.

 

 

 

▶WebConfig.java

web.xml 대신 존재하는 WebConfig.java
public class WebConfig extends AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] { RootConfig.class };
	}

	@Override
	protected Class<?>[] getServletConfigClasses() {
		return new Class[] { ServletConfig.class };
	}

	@Override
	protected String[] getServletMappings() {
		return new String[] { "/" };
	}

	@Override
	protected Filter[] getServletFilters() {
		CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
		characterEncodingFilter.setEncoding("UTF-8");
		characterEncodingFilter.setForceEncoding(true);

		return new Filter[] { characterEncodingFilter };
	}

	@Override
	protected void customizeRegistration(ServletRegistration.Dynamic registration) {

		registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");

		MultipartConfigElement multipartConfig = 
				new MultipartConfigElement("D:\\upload\\temp", 
						20971520, 
						41943040,
						20971520);
		
		registration.setMultipartConfig(multipartConfig);

	}
}

 

 

▶ServletConfig.java

servlet-context.xml 대신 존재하는 ServletConfig.java
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.XXX" })
public class ServletConfig implements WebMvcConfigurer {

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {

    InternalResourceViewResolver bean = new InternalResourceViewResolver();
    bean.setViewClass(JstlView.class);
    bean.setPrefix("/WEB-INF/views/");
    bean.setSuffix(".jsp");
    registry.viewResolver(bean);
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {

    registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
  }
  
  @Bean
  public MultipartResolver multipartResolver() {
    StandardServletMultipartResolver resolver 
              = new StandardServletMultipartResolver();
    return resolver;
  }
}

 


<form> 방식의 파일 업로드

▶UploadController.java

package com.spring.boardapp.controller;

import java.io.File;

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

@Controller
public class UploadController {

	@GetMapping("/uploadForm")
	public void uploadForm() {
		
		
	}
	
	@PostMapping("/uploadFormAction")
	public void uploaoFormPost(MultipartFile[] uploadFile, Model model) {
		
		String uploadFolder = "D:\\upload";
		
		for(MultipartFile multipartFile : uploadFile) {
			
			File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
			
			try {
				multipartFile.transferTo(saveFile);
			}catch(Exception e) {
				System.out.println(e.getMessage());
			}
			
		}
	}
}
  • MultipartFile
    • getName() : 파라미터의 이름 <input> 태그의 이름
    • getOriginalFileName()
    • isEmpty()
    • getSize()
    • getBytes()
    • getInputStream() : 파일데이터와 연결된 InputStream 반환
    • trasferTo(File file) : 파일의 저장

 

 

 

 

 

▶uploadForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>File Upload</title>
</head>
<body>


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

	<input type='file' name='uploadFile' multiple>

	<button>Submit</button>

</form>

</body>
</html>
  • enctype의 속성값을 'multipart/form-data'
  • <input type="file"> 태그의 multiple 속성은 하나의 <input> 태그로 한꺼번에 여러 개의 파일 업로드가 가능하다.

Ajax 이용 파일 업로드

▶UploadController.java

package com.spring.boardapp.controller;

import java.io.File;

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

@Controller
public class UploadController {

	@GetMapping("/uploadAjax")
	public void uploadAjax() {

	}

	@PostMapping("/uploadAjaxAction")
	public void uploadAjaxPost(MultipartFile[] uploadFile) {


		String uploadFolder = "D:\\upload";

		for (MultipartFile multipartFile : uploadFile) {

			String uploadFileName = multipartFile.getOriginalFilename();

			// IE의 경우 전체 파일 경로가 전송되므로, 마지막 \를 기준으로 잘라낸 문자열이 실제 파일 이름이 된다.
			uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);

			File saveFile = new File(uploadFolder, uploadFileName);

			try {

				multipartFile.transferTo(saveFile);
			} catch (Exception e) {
			} // end catch

		} // end for

	}
}

 

 

 

▶UploadAjax.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

	<div class='uploadDiv'>
		<input type='file' name='uploadFile' multiple>
	</div>
	
	<button id='uploadBtn'>Upload</button>
	
	<script src="https://code.jquery.com/jquery-3.3.1.min.js"
		integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="crossorigin="anonymous">
	</script>


	<script>
	
	$(document).ready(function(){
		
		 $("#uploadBtn").on("click", function(e){

		 var formData = new FormData();
		
		 var inputFile = $("input[name='uploadFile']");
		
		 var files = inputFile[0].files;
		
		 console.log(files);
		
		 //add filedate to formdata
		 for(var i = 0; i < files.length; i++){
		 	formData.append("uploadFile", files[i]);
		 }
		 
		 	$.ajax({
				url : '/uploadAjaxAction',
				processData : false,
				contentType : false,
				data : formData,
				type : 'POST',
				success : function(result) {

					alert("Uploaded")

				}
			}); //$.ajax
		 
		});
	});
	
	</script>
	
</body>
</html>

 

 

 

 

 

 

 

💡파일 데이터 반환

※ Ajax로 파일 업로드 하는 경우 브라우저에서는 아무 데이터도 전달 받지 못하였기 때문에 브라우저에도 피드백을 주어야 한다. 피드백을 json객체로 브라우저에게 반환하도록 코드를 작성해보겠다.

 

그럼 브라우저에 피드백 줄 내용은 무엇이 있을까?

  1. 업로드된 파일 이름 & 원본 파일 이름
  2. 파일이 저장된 경로
  3. 업로드된 파일이 이미지 파일인가 아닌가

이에 대한 정보를 처리하기 위해 별도의 객체를 생성해서 처리하는 방법을 이용하도록 하겠다.

 

 

 

▶pom.xml

jackson-databind 관련 라이브러리를 추가 시킨다.
		<!-- Jackson -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>${jackson.version}</version>
		</dependency>
        
        	<!-- jackson-databind 관련 라이브러리 추가 -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>${jackson.version}</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-xml</artifactId>
			<version>${jackson.version}</version>
		</dependency>
        	<!-- jackson-databind -->
        
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
			<version>${jackson.version}</version>
		</dependency>

 

 

▶DTO 클래스

AttachFile.java

package com.spring.boardapp.domain;

public class AttachFile {

	private String fileName;
	private String uploadPath;
	private String uuid;
	private boolean image;
	
	
	public String getFileName() {
		return fileName;
	}
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	public String getUploadPath() {
		return uploadPath;
	}
	public void setUploadPath(String uploadPath) {
		this.uploadPath = uploadPath;
	}
	public String getUuid() {
		return uuid;
	}
	public void setUuid(String uuid) {
		this.uuid = uuid;
	}
	public boolean isImage() {
		return image;
	}
	public void setImage(boolean image) {
		this.image = image;
	}
}
  • uuid
    이는 파일의 중복된 이름을 처리하기 위해 넣는 UUID 값으로 "중복된 파일 이름 해결"에 설명을 해놓았다.

 

▶Controller 클래스

package com.spring.boardapp.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.spring.boardapp.domain.AttachFile;

import net.coobird.thumbnailator.Thumbnailator;

@Controller
public class UploadController {

	@GetMapping("/uploadForm")
	public void uploadForm() {
			System.out.println("upload form");
	}

	@PostMapping("/uploadFormAction")
	public void uploaoFormPost(MultipartFile[] uploadFile, Model model) {

		String uploadFolder = "D:\\upload";

		for (MultipartFile multipartFile : uploadFile) {

			File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());

			try {
				multipartFile.transferTo(saveFile);
			} catch (Exception e) {
				System.out.println(e.getMessage());
			}

		}
	}
	
	@GetMapping("/uploadAjax")
	public void uploadAjax() {
		System.out.println("upload ajax");
	}
	
	// 날짜 폴더 생성
	private String getFolder() {

		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

		Date date = new Date();

		String str = sdf.format(date);

		return str.replace("-", File.separator);
	}
	
	// 이미지 파일인지 검사
	private boolean checkImageType(File file) {

		try {
			String contentType = Files.probeContentType(file.toPath());

			return contentType.startsWith("image");

		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return false;
	}

	
	@PostMapping(value = "/uploadAjaxAction", 
				produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	@ResponseBody
	public ResponseEntity<List<AttachFile>> uploadAjaxPost(MultipartFile[] uploadFile) {

		List<AttachFile> list = new ArrayList<AttachFile>();
		String uploadFolder = "D:\\upload";

		String uploadFolderPath = getFolder();
		// make folder --------
		File uploadPath = new File(uploadFolder, uploadFolderPath);

		if (uploadPath.exists() == false) {
			uploadPath.mkdirs();
		}
		// make yyyy/MM/dd folder

		for (MultipartFile multipartFile : uploadFile) {

			AttachFile attachDTO = new AttachFile();

			String uploadFileName = multipartFile.getOriginalFilename();

			// IE의 경우 전체 파일 경로가 전송되므로, 마지막 \를 기준으로 잘라낸 문자열이 실제 파일 이름이 된다.
			uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1);
			attachDTO.setFileName(uploadFileName);

			UUID uuid = UUID.randomUUID();

			uploadFileName = uuid.toString() + "_" + uploadFileName;

			try {
				File saveFile = new File(uploadPath, uploadFileName);
				multipartFile.transferTo(saveFile);

				attachDTO.setUuid(uuid.toString());
				attachDTO.setUploadPath(uploadFolderPath);

				// check image type file
				if (checkImageType(saveFile)) {

					attachDTO.setImage(true);

					FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));

					Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);

					thumbnail.close();
				}

				// add to List
				list.add(attachDTO);

			} catch (Exception e) {
				e.printStackTrace();
			}

		} // end for
		return new ResponseEntity<List<AttachFile>>(list, HttpStatus.OK);
	}
}

여기엔 위에서 설명한 것 외에 여러가지 요소들을 추가하였다. 그 요소들은 아래 첨부파일시 고려해야할 것들에 기재해놨으므로 링크를 클릭하여 추가해주면 된다.

 

 

 

▶UploadAjax.jsp

			$.ajax({
				url : '/uploadAjaxAction',
				processData : false,
				contentType : false,
				data : formData,
				type : 'POST',
				dataType : 'json',
				success : function(result) {
					
					alert("Uploaded")

				}
			}); //$.ajax

Ajax를 호출했을 때 결과 타입(dataType)은 'json' 타입으로 변경한다.

 


파일 여러 번 업로드하기

업로드는 <input type="file">을 통해서 이루어진다.
이 때 한 번 업로드가 끝난 후에 다시 올리기 위해서는 이를 초기화 시켜주는 작업이 필요하다.

▶UploadAjax.jsp

		var cloneObj = $(".uploadDiv").clone();
		
		 $("#uploadBtn").on("click", function(e){

		 var formData = new FormData();
         
         ...
         
         
         		$.ajax({
				url : '/uploadAjaxAction',
				processData : false,
				contentType : false,
				data : formData,
				type : 'POST',
				dataType : 'json',
				success : function(result) {

					console.log(result);

					$(".uploadDiv").html(cloneObj.html());

				}
			}); //$.ajax
  • <input type="file">은 자체적으로 readonly 속성을 갖고 있다.
    • 따라서 별도의 방법으로 초기화 시켜 다른 첨부파일을 또 업로드할 수 있도록 만들어주어야 한다.
    • 첨부파일 업로드 전 <input type="file"> 객체가 포함된 <div>를 복사(clone) 하는 방법이다.
      첨부파일을 업로드 한 뒤 복사된 객체를 <div> 내에 다시 추가해서 첨부파일 부분을 초기화시켜준다.

 

 

올린 파일 목록 출력 + 이미지 같이 출력

 

▶UploadAjax.jsp

<style>
.uploadResult {
	width: 100%;
	background-color: gray;
}

.uploadResult ul {
	display: flex;
	flex-flow: row;
	justify-content: center;
	align-items: center;
}

.uploadResult ul li {
	list-style: none;
	padding: 10px;
}

.uploadResult ul li img {
	width: 100px;
}
</style>


	...
        
	<script>
	
	$(document).ready(function(){
		
		var regex = new RegExp("(.*?)\.(exe|sh|zip)$");
		var maxSize = 10485760; // 10MB
		
		// 사이즈, 확장자 체크
		function checkExtension(fileName, fileSize) {

			if (fileSize >= maxSize) {
				alert("파일 사이즈 초과");
				return false;
			}

			if (regex.test(fileName)) {
				alert("해당 종류의 파일은 업로드할 수 없습니다.");
				return false;
			}
			return true;
		}
		
		// 파일 추가로 올리기 위한 조치
		var cloneObj = $(".uploadDiv").clone();
		
		// 클릭시 
		$("#uploadBtn").on("click", function(e){

			var formData = new FormData();
		
		 	var inputFile = $("input[name='uploadFile']");
		
		 	var files = inputFile[0].files;
		
		
		 	//add filedate to formdata
		 	for(var i = 0; i < files.length; i++){
		 		
		 		if (!checkExtension(files[i].name, files[i].size)) {
					return false;
				}
		 		formData.append("uploadFile", files[i]);
		 	}
		 
		 	
		 	$.ajax({
				url : '/uploadAjaxAction',
				processData : false,
				contentType : false,
				data : formData,
				type : 'POST',
				dataType : 'json',
				success : function(result) {

					console.log(result);
					
					showUploadedFile(result);

					$(".uploadDiv").html(cloneObj.html());

				}
			}); //$.ajax
		});
		
		var uploadResult = $(".uploadResult ul");
		 
		
		function showUploadedFile(uploadResultArr) {

			var str = "";

			$(uploadResultArr).each(function(i, obj) {

				if(!obj.image){
					str += "<li><img src='/resources/img/attach.png'>"+obj.fileName+"</li>";
				}
				else{
		        //str += "<li>"+ obj.fileName+"</li>";
		        var fileCallPath =  encodeURIComponent( obj.uploadPath+ "/s_"+obj.uuid+"_"+obj.fileName);
		        str += "<li><img src='/display?fileName="+fileCallPath+"'><li>";
		   		}
			});
			uploadResult.append(str);
		} 	
	});
	
	</script>
  • encodeUIRComponent
    • 파일 경로 및 이름에 공백이나 한글이 포함될 경우 문제가 발생할 수 있기 때문에 JavaScript#encodeURIComponent() 를 이용해서 URI에 문제가 없는 문자열을 생성해 처리한다.

 

 

 

  • 일반 파일인 경우
    • /resources/img/attach.png 파일 이미지를 보이게 하였다.
  • 이미지 파일인 경우
    • 섬네일 파일을 보여주기 위해서 서버를 통해 특정 URI 를 호출하면 보여주도록 처리하였다. 이를 위해 아래와 같이 Controller 컨트롤러에 섬네일 데이터 전송하는 코드 추가

 

▶UploadController.java

특정한 파일 이름을 받아서 이미지 데이터를 전송하는 코드
	@GetMapping("/display")	
	@ResponseBody
	public ResponseEntity<byte[]> getFile(String fileName) {

		File file = new File("D:\\upload\\" + fileName);

		ResponseEntity<byte[]> result = null;

		try {
			HttpHeaders header = new HttpHeaders();

			header.add("Content-Type", Files.probeContentType(file.toPath()));
			result = new ResponseEntity<byte[]>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return result;
	}
  • probeContentType()
    byte[]로 이미지 파일로 전송할 때 파일의 종류에 따라 MIME 타입이 달라진다.
    이 부분을 해결하기 위해 probeContentType()을 이용해서 적절한 MIME 타입 데이터를 Http의 헤더 메시지에 포함할 수 있도록 처리한다.
  • 호출은 '/display?fileName='년/월/일/파일이름' 형태

 

반응형

'ETC' 카테고리의 다른 글

Spring + Ajax 중복된 파일 이름 해결  (0) 2022.09.25
Spring + Ajax 파일 확장자, 크기 제한 (Ajax)  (0) 2022.09.25
[SpringMVC + MyBatis + MySql] 게시판 댓글 수 게시판 List에 출력  (0) 2022.09.25
[SpringMVC + MyBatis + Ajax] 게시판 댓글 추가/삭제/List (Ajax 이용)  (0) 2022.09.24
[SpringMVC + MyBatis + MySql] 게시판 CRUD + 페이징처리 + 검색조건 + 조회수  (0) 2022.09.15
    여승철
    여승철

    티스토리툴바