📃 요약
Toast UI Editor 샘플입니다.
Toast UI Editor 외부 라이브러리를 이용해서 진행해 보도록 하겠습니다.
Toast UI Editor 는 네이버에서 개발된 위지윅 에디터로 활용이 간편합니다.
단, 위지윅 에디터는 XSS 공격 등 해킹 위험이 있으므로 제공되는 fix 등 이력을 살펴보시고 적용하시기 바랍니다.
Toast UI Editor 를 활용해서 CRUD 중 상세 조회 / 수정 기능만 샘플을 구성합니다.
간단 예제를 위해 기본키는 DB 저장된 값으로 상세조회를 합니다.
Toast UI Editor 공식홈페이지는 아래 URL 을 참고하시기 바랍니다.
( https://ui.toast.com/ )
프론트는 Vue 를 사용하고, 벡엔드는 spring boot / oracle docker 를 사용합니다.
요소 기술 :
– 프론트엔드 : Vue
- 프론트 패키지 : toast ui , axios
– 벡엔드 : Spring Boot & JPA & Oracle
- 벡엔드 패키지 : 오라클 , Logback 라이브러리
결과 화면 :
- 상세조회 #1

- 상세조회 #2

프로젝트 탐색기 : Vue

프로젝트 탐색기 : String Boot

Backend : URL 정보
방식 | URL | 설명 |
---|---|---|
GET | /dept/{dno} | 상세 조회 |
UPDATE | /dept/{dno} | 수정 – @RequestBody 로 Dept 객체 전달받음 |
📃 기술 구현
스펙 :
데이터베이스 테이블 / 부서 샘플 데이터 만들기
- schema.sql
-- Table , 시퀀스 등 구조 정의 DROP SEQUENCE SQ_DEPT; CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY 10; DROP TABLE TB_DEPT CASCADE CONSTRAINT; CREATE TABLE TB_DEPT ( DNO NUMBER NOT NULL PRIMARY KEY, DNAME VARCHAR2(255), LOC VARCHAR2(255), INSERT_TIME VARCHAR2(255), UPDATE_TIME VARCHAR2(255) );
- data.sql
-- 샘플 데이터 입력 INSERT INTO TB_DEPT VALUES (SQ_DEPT.nextval, 'ACCOUNTING', 'NEW YORK',TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL); INSERT INTO TB_DEPT VALUES (SQ_DEPT.nextval, 'RESEARCH', 'DALLAS', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL); INSERT INTO TB_DEPT VALUES (SQ_DEPT.nextval, 'SALES', 'CHICAGO', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL); INSERT INTO TB_DEPT VALUES (SQ_DEPT.nextval, 'OPERATIONS', 'BOSTON', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL); COMMIT;
Spring build.gradle : dependencies 블럭 내 추가
dependencies { // sql 출력 결과를 보기위한 라이브러리 추가 implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' // 오라클 19버전용 라이브러리 추가 설치 implementation 'com.oracle.database.jdbc:ucp:19.14.0.0' implementation 'com.oracle.database.security:oraclepki:19.14.0.0' implementation 'com.oracle.database.security:osdt_cert:19.14.0.0' implementation 'com.oracle.database.security:osdt_core:19.14.0.0' ... 생략 }
Cors 설정
– WebConfig.java
package org.example.simpledept.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @fileName : WebConfig * @author : GGG * @since : 2024-04-02 * description : CORS 보안 설정 */ @Configuration public class WebConfig implements WebMvcConfigurer { @Value("${simpleDms.app.front-url}") private String frontUrl; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // spring 모든 경로(접근) .allowedOrigins(frontUrl) // vue 의 주소 .allowedMethods( // 허용 방식 : get/post/put/delete HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name() ); } }
공통 Exception 클래스 : 컨트롤러에서 어떤 예외가 발생하더라도 이 클래스의 함수가 실행됨
-CommonExceptionAdvice.java
package org.example.simpledept.exceptions; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * @author : kangtaegyung * @fileName : CommonExceptionAdvice * @since : 24. 5. 27. * description : 공통된 예외 처리 함수 * @ResponseStatus(HttpStatus.상태코드) : 상태코드가 발생하면 * @ExceptionHandler(Exception.class) : Exception 예외 클래스에 대해 처리한다. */ @Slf4j @RestControllerAdvice public class CommonExceptionAdvice { // 컨트롤러에서 어떤 에러가 발생하더라도 이 함수가 실행됨 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ResponseEntity<?> internalServerErrorException(Exception e) { log.debug("벡엔드 에러: " + e.getMessage()); return new ResponseEntity<>("벡엔드 에러: " + e.getMessage() , HttpStatus.INTERNAL_SERVER_ERROR); } }
-DeptController.java
package org.example.simpledept.controller; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.simpledept.model.entity.basic.Dept; import org.example.simpledept.service.DeptService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.Optional; /** * fileName : DeptController * author : GGG * date : 2024-04-02 * description : 부서 컨트롤러 */ @Slf4j @RestController @RequestMapping("/api/basic") @RequiredArgsConstructor public class DeptController { private final DeptService deptService; // 상세 조회 함수 @GetMapping("/dept/{dno}") public ResponseEntity<?> findById( @PathVariable int dno ) { Optional<Dept> optionalDept = deptService.findById(dno); if (optionalDept.isEmpty()) { return new ResponseEntity<>(HttpStatus.NO_CONTENT); } return new ResponseEntity<>(optionalDept.get() , HttpStatus.OK); } // 수정 함수 @PutMapping("/dept/{dno}") public ResponseEntity<?> update( @PathVariable int dno, @RequestBody Dept dept ) { deptService.save(dept); return new ResponseEntity<>(HttpStatus.OK); } }
– DeptService.java
상세조회 , 수정함수만 구현함
package org.example.simpledept.service; import lombok.RequiredArgsConstructor; import org.example.simpledept.model.entity.basic.Dept; import org.example.simpledept.repository.basic.DeptRepository; import org.springframework.stereotype.Service; import java.util.Optional; /** * fileName : DeptService * author : GGG * date : 2024-04-02 * description : 부서 서비스 클래스 */ @Service @RequiredArgsConstructor public class DeptService { private final DeptRepository deptRepository; // 상세조회 public Optional<Dept> findById(int dno) { Optional<Dept> optionalDept = deptRepository.findById(dno); return optionalDept; } // 저장/수정 함수 public void save(Dept dept) { deptRepository.save(dept); } }
– DeptRepository.java
package org.example.simpledept.repository.basic; import org.example.simpledept.model.entity.basic.Dept; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** * fileName : DeptRepository * author : GGG * date : 2024-04-02 * description : 부서 SQL 작성하는 인터페이스 */ @Repository public interface DeptRepository extends JpaRepository<Dept,Integer> { }
– Dept.java
package org.example.simpledept.model.entity.basic; import jakarta.persistence.*; import lombok.*; import org.example.simpledept.model.common.BaseTimeEntity; /** * fileName : Dept * author : GGG * date : 2024-04-02 * description : 부서 엔티티 : 테이블과 연결되는 자바클래스, 테이블과 동일하게 정의함 */ @Entity @Table(name = "TB_DEPT") @SequenceGenerator( name = "SQ_DEPT_GENERATOR" , sequenceName = "SQ_DEPT" , initialValue = 1 , allocationSize = 1 ) @Setter @Getter @NoArgsConstructor @AllArgsConstructor @ToString public class Dept extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE , generator = "SQ_DEPT_GENERATOR" ) private Integer dno; // 부서번호(기본키) private String dname; // 부서명 private String loc; // 부서위치 }
Vue 페이지 : 간단한 채팅 화면입니다.
– Vue 패키지 추가 :
"dependencies": { "@toast-ui/editor": "^3.2.2", "axios": "^1.7.2", ...생략 },
1) 공통 js
– utils/constraints.js : axios 기본 설정 파일
// axios 기본 설정 : spring ip 주소 설정 import axios from "axios"; export default axios.create({ baseURL: "http://localhost:8000/api", headers: { "Content-Type": "application/json" } })
– router/index.js
import { createRouter, createWebHistory } from "vue-router"; const routes = [ // 홈 { path: "/", component: () => import("../views/HomeView.vue"), }, // 부서상세조회 / 수정 { path: "/dept/:dno", component: () => import("../views/basic/DeptDetail.vue"), }, ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); export default router;
Vue 페이지
결과 화면 :


– HeaderCom.vue
<template> <div> <!-- 부트스트랩 메뉴 넣기 --> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <a class="navbar-brand" href="#">Simple</a> <!-- 단축메뉴(햄버거 메뉴) --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" > <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- 메뉴(왼쪽) --> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <!-- 1st : Home --> <li class="nav-item"> <router-link class="nav-link active" aria-current="page" to="/">Home</router-link> </li> <!-- 기초예제 메뉴 #1 --> <li class="nav-item dropdown"> <!-- 대메뉴 --> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" > 기초 예제 </a> <!-- 소메뉴 --> <ul class="dropdown-menu"> <!-- TODO: 50번 부서상세조회 : 간단 예시를 위해 강제 하드코딩 --> <li><router-link class="dropdown-item" to="/dept/50">부서상세</router-link></li> </ul> </li> </ul> </div> </div> </nav> </div> </template> <script> export default {}; </script> <style></style>
App.vue
<template> <!-- 머리말 --> <HeaderCom /> <div class="container mt-5"> <router-view /> </div> </template> <script> import HeaderCom from "@/components/common/HeaderCom.vue"; export default { components : { HeaderCom // 컴포넌트 등록 } } </script>
HomeView.vue
<template> <div> 홈화면입니다. </div> </template>
DeptDetail.vue
<template> <div v-if="dept"> <div> <label for="loc" class="col-form-label"> 부서위치 </label> </div> <!-- TOAST UI 에디터 --> <div id="editor"></div> <div class="row g-3 mt-3 mb-3"> <button type="submit" class="btn btn-success ms-2 col" @click="updateDept" > 수정 </button> </div> </div> </template> <script> import Editor from "@toast-ui/editor"; import "@toast-ui/editor/dist/toastui-editor.css"; // Editor's Style import DeptService from "@/services/basic/DeptService"; export default { data() { return { dept: null, }; }, methods: { // 상세조회요청 함수 async getDept(dno) { try { let response = await DeptService.get(dno); this.dept = response.data; console.log(response.data); } catch (e) { console.log(e); } }, // toast ui 에디터 생성 createEditer(loc) { new Editor({ el: document.querySelector("#editor"), initialEditType: "wysiwyg", initialValue: loc, // TODO: 중요 : 여기 loc 넣기 , 에디터에 loc 보임 }); }, // 수정요청 함수 async updateDept() { try { let temp = { dno: this.dept.dno, dname: this.dept.dname, loc: this.toastEditor.getHTML(), // TODO: 중요 : 에디터의 글 loc 가져오기 }; console.log(temp); let response = await DeptService.update(this.dept.dno, temp); console.log(response.data); alert("수정이 성공했습니다."); // 다시 상세조회 this.getDept(this.dept.dno); } catch (e) { console.log(e); } }, }, async mounted() { await this.getDept(this.$route.params.dno); // 상세조회 함수 실행 this.createEditer(this.dept.loc); // TODO: 에디터에 loc 넣기 }, }; </script>
- 로직 설명
- 1) 상세 화면이 표시될때 상세조회 및 Toast UI 에디터가 랜더링됩니다.
- 2) 부서위치( loc ) 내용을 DB 에서 가져와서 Toast UI 에디터에 표시됩니다.
- 3) 부서위치( loc ) 내용을 수정하고 수정버튼을 클릭하면 DB 수정됩니다. ( update 문 실행 )
- initialValue : Toast UI 에디터에 글을 전달하는 값으로 DB 의 내용을 화면에 표시할 때 사용하면 됩니다.
- getHTML() : Toast UI 에디터의 현재 화면에 표시된 내용을 가져옵니다. 이것을 이용해 부서위치 속성( loc ) 에 저장하고 벡엔드로 전송 및 DB 저장하시면 됩니다.
📃 결론
Toast UI Editor 를 이용해서 상세조회 및 수정을 진행하는 예제를 살펴보았습니다.
상세조회는 화면이 로딩될때 표시됩니다
Toast UI Editor 에서 글을 수정하고 수정 버튼을 클릭하면 DB 에 저장됩니다.
Toast UI Editor 에서 DB 의 글을 가져와서 표시하고, 에디터의 내용을 DB 로 저장하는 방법을 알아보았습니다.
Toast UI Editor 를 이용해서 DB 저장하기에 관심 있으시다면 Source 는 아래에서 찾을 수 있습니다.
- Github 주소 : https://github.com/KangTaeGyung/Toast-UI-Sample
감사합니다.
“Toast UI Editor sample” 에 하나의 답글
You have mentioned very interesting details! ps decent website.Raise range