+ 00 00 0000

Have any Questions?

Toast UI Editor sample

Toast UI Editor sample

📃 요약

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 객체 전달받음

📃 기술 구현

스펙 :

Vue 3.x
jdk 17
spring boot 3.x
intellij IDEA & gradle
logging tool : logback
oracle docker

데이터베이스 테이블 / 부서 샘플 데이터 만들기

  • 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 는 아래에서 찾을 수 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다