📃 요약
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