이번 프로젝트에서 Jsp 프로젝트를 Spring으로 이관하는 과정에서 비동기 Servlet으로 사용한

CKEditor API를 POJO 형식인 일반 Java 로 변환 하여 흐름을 다시 정리 하려고 합니다.

 

HttpServletRequest 내장 객체인 getPart로 받아오는 것이 아닌

Spring에서 지원하는 MultipartFile 을 사용하려고 합니다.

 

전체 흐름

  1. Was(Web Application Server) 설정 및 실행 흐름
  2. 게시글 등록 페이지 이동 및 CKEditor API 호출
  3. CKEditor 이미지 업로드
  4. 게시글 등록

 

 

1.  Was(Web Application Server) 설정 및 실행 흐름

프로젝트 Was 는 톰캣을 선정했습니다.

이때 Multipart 를 사용할때 form-data 규칙을 완화해 주기위해

content.xml 에 allowCasualMultipartParsing="true" 설정을 진행해줍니다.

<Context allowCasualMultipartParsing="true">
<!--  Default set of monitored resources. If one of these changes, the     -->
<!--  web application will be reloaded.                                    -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<!--  Uncomment this to enable session persistence across Tomcat restarts  -->
<!-- 
    <Manager pathname="SESSIONS.ser" />
     -->
</Context>

 

등록 후 톰캣을 실행하게 되면 web.xml에 등록되어 있는 설정을 먼저 읽어오고

더보기
 <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
  	<servlet>
		<servlet-name>ds</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>ds</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

이때 백단 DB(DAO, Service 등)의 클래스들을 먼저 Bean 등록하기 위해 applicationContext.xml 를 호출합니다.

더보기
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                  http://www.springframework.org/schema/beans/spring-beans.xsd
                  http://www.springframework.org/schema/context
                  http://www.springframework.org/schema/context/spring-context-4.2.xsd
                  http://www.springframework.org/schema/aop
                  http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
                  http://www.springframework.org/schema/tx
                  http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

    <context:component-scan base-package="
        com.coma.app.biz.battle,
        com.coma.app.biz.battle_record,
        com.coma.app.biz.board,
        com.coma.app.biz.common,
        com.coma.app.biz.crew,
        com.coma.app.biz.crew_board,
        com.coma.app.biz.favorite,
        com.coma.app.biz.grade,
        com.coma.app.biz.gym,
        com.coma.app.biz.member,
        com.coma.app.biz.product,
        com.coma.app.biz.reply,
        com.coma.app.biz.reservation"/>

    <aop:aspectj-autoproxy/>

    <bean class="org.apache.commons.dbcp.BasicDataSource" id="bds" destroy-method="close">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://root@localhost:3306/스키마"/>
        <property name="username" value="DB user ID"/>
        <property name="password" value="DB user password"/>
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <property name="dataSource" ref="bds"/>
    </bean>

    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="txManager">
        <property name="dataSource" ref="bds" />
    </bean>

    <!-- 설정해둔 메서드들에 모든 R(select) 는 읽기 만 실행 -->
    <!-- 설정해둔 메서드들에 모든 CUD(insert, update, delete) 에 관해서 실행 -->
    <tx:advice transaction-manager="txManager" id="txAdvice">
        <tx:attributes>
            <tx:method name="select*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 클래스 명에 Impl 가 들어간 모든 메서드들에 적용합니다. -->
    <aop:config>
        <aop:pointcut expression="execution(* com.coma.app.biz..TestTransaction.*(..))" id="txPointcut"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

</beans>

DB 백단의 클래스 Bean 등록이 완료되고 ds-servlet 을 호출하여 Controller 클래스, ViewResolver, multipartResolver 등을 Bean 으로 등록합니다.

더보기
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
						http://www.springframework.org/schema/beans/spring-beans.xsd
						http://www.springframework.org/schema/context
						http://www.springframework.org/schema/context/spring-context-4.2.xsd">

    <context:component-scan base-package="
        com.coma.app.view.asycnServlet,
        com.coma.app.view.common,
        com.coma.app.view.community,
        com.coma.app.view.crew.battle,
        com.coma.app.view.crew.community,
        com.coma.app.view.crew.join,
        com.coma.app.view.function,
        com.coma.app.view.gym,
        com.coma.app.view.main,
        com.coma.app.view.member,
        com.coma.app.view.mypage,
        com.coma.app.view.ranking,
        com.coma.app.view.store"/>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
            </list>
        </property>
    </bean>
    <bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver" />

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

 

2. 게시글 등록 페이지 이동 및 CKEditor API 호출

클라이언트에서 화면 호출 요청합니다.

https://localhost:8080/boardInsert.do

 

호출을 받은 Controller에서 페이지 이동 로직을 실행합니다.

더보기
	@GetMapping("/boardInsert.do")
	public String boardInsert() {

		String path = "editing";//글작성페이지

		String[] login = this.loginCheck.success();

		//로그인 정보가 있는지 확인해주고
		String member_id = login[0];
		
		System.out.println("InsertBoard 로그인 정보 로그 : "+member_id);
		//만약 로그인 정보가 없다면
		if(member_id == null) {
			//로그인 페이지로 넘어간다
			path = "redirect:login.do";
		}
		
		//로그인이 되어 있다면
		//글 작성 페이지로 넘어간다
		return path;
	}

 

호출 받은 View 페이지에서는 CKEditor API 를 사용하기 위해 CDN 을 불러와 사용자에게 보여줍니다.

 

3. CKEditor 이미지 업로드

클라이언트에서 CKEditor API에 이미지를 업로드합니다.

View에서는 Js를 실행하여 이미지 저장 비동기 클래스를 호출합니다.

CKEditor.js

더보기
//editorConfig.js에서 CKEditor 설정 정보 불러오기
import { editorConfig, setContentsLength, setTextLength, setImgLength } from 'editorConfig';
import { ClassicEditor } from 'CKEditor';

//CKEditor 생성
console.log('CKEditor 생성');
ClassicEditor.create(document.querySelector('#content'), editorConfig)
.then( editor => {
	window.editor = editor;
	console.log('CKEditor 전송 정보 : ' + editor);	
	
	
	// editor가 변경되면 현재 글자수와 이미지 개수를 출력해줍니다.
	editor.model.document.on('change:data', function () {
		//CKEditor에 작성된 정보를 가져옵니다.
		var str = editor.getData();
	    setTextLength(str);
		setImgLength(str);
	});
	
	//editor 의 id 값을 넣어줍니다.
	var editorStatus = false;
	// 폼을 제출할 때 이벤트 리스너 추가
	const form = document.querySelector('form'); // 폼 선택
	form.addEventListener('submit', function(event) {
		//CKEditor에 작성된 정보를 가져옵니다.
		var str = editor.getData();
		//이미지 개수 / 글자수 제한
		editorStatus = setContentsLength(str, 2, 255);
		//현재 상태 확인용 로그
		console.log("form.addEventListener 실행 : " + editorStatus);
		if (!editorStatus) {
			//현재 상태 확인용 로그
			console.log("if (!editorStatus)  실행 : " + editorStatus);
			//글자수가 넘어가면 form 제출을 방지합니다.
			event.preventDefault(); // 폼 제출 방지
		}
	});
})
.catch(err => {
	console.log('발생 오류 : '+err);
});

CKEditorConfig.js

더보기
//...다른 Config 설정 생략
// 파일 업로드 설정
simpleUpload: {
	//파일을 저장할 서버의 주소를 입력
	uploadUrl: '/ckupload.do',
},
//...다른 Config 설정 생략

 

js에서 호출한 JAVA 클래스

비동기클래스에서 파일을 저장 후 CKEditor API로 저장된 이미지에 주소를 전달합니다.

더보기
@PostMapping("/ckupload.do")
    public @ResponseBody String returnImage(HttpSession session, @RequestBody MultipartFile upload) throws IOException {
        //현재 들어온 파일 타입 확인용
        //upload.getContentType();

        //--------------------------------------------------------------------------
        //FIXME 사용된 변수명
        //로그인 정보가 있는지 확인해주고
        String[] login = loginCheck2.success();
        log.error(login[0]);
        String member_id = login[0];//세션에 있는 사용자의 아이디
        MultipartFile file = upload;
        // simpleUpload 의 기본 파라미터는 "upload" 파라미터로 전송된다.
//        MultipartFile file = testDTO.getUpload();
        //넘어오는 파일의 명칭과 형식을 가져와서 변수에 저장합니다.
        String fileName = file.getOriginalFilename();
        //받아온 파일의 형식만 불러와줍니다.
        String fileform = fileName.substring(fileName.lastIndexOf("."));

        //이미지이름을 보안코드로 변환하여 서버에 저장해줍니다.
        fileName = CKEditor_Upload_Test.img_security()+fileform;

        //저장할 폴더 명칭
        String folerName = "/board_img/"+member_id+"/";
        //가져온 파일을 저장할 위치를 지정해줍니다.
        String uploadPath = context.getRealPath(folerName);
        //저장될 위치를 확인하기 위한 로그
        System.out.println("파일 주소 : "+uploadPath);
        System.out.println("FOLDER_NUM : "+session.getAttribute("FOLDER_NUM"));
        //세션에 저장되어 있는 폴더 개수를 가져옵니다. 삼항연산자로 만약 세션값이 null이 아니라면 정수형으로 변경하여 가져오도록 수정
        int folder_session = (session.getAttribute("FOLDER_NUM") == null) ? 0:(Integer)session.getAttribute("FOLDER_NUM");
        System.out.println("UPDATE_FOLDER_NUM : "+session.getAttribute("UPDATE_FOLDER_NUM"));
        //세션에 저장되어 있는 폴더 개수를 가져옵니다. 삼항연산자로 만약 세션값이 null이 아니라면 정수형으로 변경하여 가져오도록 수정
        int update_folder_session = (session.getAttribute("UPDATE_FOLDER_NUM") == null) ? 0:(Integer)session.getAttribute("UPDATE_FOLDER_NUM");

        //--------------------------------------------------------------------------
        //FIXME 저장될 폴더를 생성 로직
        int folder_num = 0;
        //폴더가 없을 수 있기 때문에 만들어둔 폴더생성 함수를 활용
        Mkdir_File.create(uploadPath);
        if(update_folder_session > 0) {
            System.out.println("CKEditor_Upload.java folder_session 로그 : "+update_folder_session);
            folder_num = update_folder_session;
        }
        else {
            //현재 폴더 개수를 불러와줍니다.
            //(처음 불러올때는 서버/사용자폴더/ 로 불러옵니다)
            folder_num = CKEditor_Upload_Test.member_folder_num(uploadPath);
            //만약 저장되어 있는 개수가 없다면
            if(folder_session <= 0 && update_folder_session <= 0) {
                //사용자의 게시판 이미지 폴더 개수 확인용 로그
                System.out.println("사용자 게시판 이미지 폴더 확인용 로그 :"+folder_num);
                //번호를 불러와서 +1을 해준다음
                folder_num = CKEditor_Upload_Test.member_folder_num(uploadPath)+1;
                //폴더를 추가해줍니다.
                Mkdir_File.create(uploadPath+folder_num);
                //추가된 폴더의 번호를 세션에 저장해줍니다.
                session.setAttribute("FOLDER_NUM", folder_num);
            }
        }
        //--------------------------------------------------------------------------
        //FIXME 서버에 이미지 파일 저장 로직
        //파일 주소가 변경되었으니 변경된 주소를 추가해줍니다.
        uploadPath = uploadPath + "/" + folder_num;

        //받아온 주소로 파일을 저장합니다.
        //운영체제마다 파일 구분자가 다르기 때문에 File.separator를 추가해줍니다.
        //이런 File.separator 클래스가 있다는 걸 처음보아 사용해보았습니다.
        file.transferTo(new File(uploadPath + File.separator + fileName));

        //--------------------------------------------------------------------------
        //FIXME View로 서버에 저장된 주소를 전달하는 로직
        //view로 보내줄 파일 주소를 저장해서 전달해줍니다.
        String fileUrl = context.getContextPath() + folerName + folder_num + "/" + fileName;
        System.out.println("파일 주소 : "+fileUrl);
        return "{ \"url\": \"" + fileUrl + "\" }";
        //--------------------------------------------------------------------------
    }

 

전달 받은 주소로 클라이언트에게 저장된 이미지를 출력하여 보여줍니다.

 

4. 게시글 등록

클라이언트에서 게시글 작성합니다.

Controller 에서 작성된 제목 + 게시글을 등록하기 위해 BoardService를 호출합니다.

더보기
@PostMapping("/boardInsert.do")
public String boardInsert(HttpSession session, Model model, BoardDTO boardDTO) {
//...생략
		if (!boardService.insert(boardDTO)) {
			log.error("게시글 저장 실패");
//...생략
		}
        return path;
	}

BoardServiceImpl.java

더보기
@Service("boardService")
public class BoardServiceImpl implements BoardService{
//...생략
	@Override
	public boolean insert(BoardDTO boardDTO) {
		boolean flag = this.boardDAO.insert(boardDTO);
		if (!flag){
            throw new RuntimeException("Insert Error");
		}
		return flag;
	}
//...생략
}

 

글 작성 성공 여부를 View로 전달하고 전체 게시글 페이지로 이동으로 게시글 등록이 마무리됩니다.

728x90
개발자가 되고 싶은 곰