이번 프로젝트에서 Jsp 프로젝트를 Spring으로 이관하는 과정에서 비동기 Servlet으로 사용한
CKEditor API를 POJO 형식인 일반 Java 로 변환 하여 흐름을 다시 정리 하려고 합니다.
HttpServletRequest 내장 객체인 getPart로 받아오는 것이 아닌
Spring에서 지원하는 MultipartFile 을 사용하려고 합니다.
전체 흐름
- Was(Web Application Server) 설정 및 실행 흐름
- 게시글 등록 페이지 이동 및 CKEditor API 호출
- CKEditor 이미지 업로드
- 게시글 등록
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로 전달하고 전체 게시글 페이지로 이동으로 게시글 등록이 마무리됩니다.
'팀 프로젝트 > Web' 카테고리의 다른 글
[Spring] 트랜잭션 정리 (0) | 2024.10.20 |
---|---|
최종 프로젝트 와이어 프레임 (0) | 2024.10.07 |
코마-중간 프로젝트 발표 영상 (0) | 2024.09.29 |
팀 프로젝트 전체 로직 (0) | 2024.09.23 |
게시판 페이지 CKEditor 글 수정 파트 (Controller) (1) | 2024.09.22 |