반응형

https://devjunyeok.tistory.com/186#3)%20%EB%B6%80%EC%84%9C(Department)%20%EA%B2%8C%EC%8B%9C%ED%8C%90%20%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0-1

 

front + backend 게시판 CRUD 구현 (1)

안녕하세요 ! :) front + backend 를 통합한 게시판을 생성하는 예제를 만들어보겠습니다. 📖 frontend작업 프론트 작업은 VSCode를 이용하여 프로젝트를 생성하였습니다. frontend 프로젝트는 React + typescri

devjunyeok.tistory.com

지난시간에 이어 이번에는 부서 게시판 기능 중 부서 추가 페이지를 생성해보겠습니다 :)

 

먼저 프론트 프로젝트 작업부터 시작하겠습니다.

 

📖 프론트 작업

1) pages > dept-nop > AddDeptNop.tsx 파일 생성

이 페이지는 부서를 추가하는 기능을 하는 페이지 입니다. 그래서 번저 함수 안에 변수를 정의합니다.

// todo ) 새 부서(객체 1개) 저장 페이지
function AddDeptNop() {
    // 변수정의
    // todo : 초기화 객체
    const initialDept = {
        dno: null,
        dname: "",
        loc: "",
    }

그리고 부서를 담을 변수와 저장하면 바뀌는 변수 (true/false)를 선언합니다.

// todo 바인딩 변수
    // todo 새부서 객체 변수
    const [dept, setDept] = useState<IDept>(initialDept);
    // todo 저장하면 true, 아니면 false 인 변수(값에따라 화면바뀜)
    const [submitted, setSubmitted] = useState<boolean>(false);

그리고 아래에 함수를 정의합니다.

함수는 새로운 입력 폼을 보여주는 함수와 그리고 각 입력창에 수동 바인딩 공통함수와 저장함수를 작성합니다.

 

  // 함수정의
    // todo 새로운 폼(form)을 보여주는 함수
    const newDept = () => { 
        // 새 폼 == 객체 초기화, submitted 변수 초기화(false)
        setDept(initialDept);   // 객체 초기화
        setSubmitted(false);   
     }
     // todo 각각의 입력창 수동 바인딩 공통함수
     const handleInputChange = (e:React.ChangeEvent<HTMLInputElement>) => { 
        const { name, value } = e.target;   // 화면 값[이름]
        // 화면 값을 Dept 객체의 속성에 저장
        setDept({...dept, [name]: value});

      }
      // todo 저장 함수
      const saveDept = () => { 
        // 임시 부서 변수(저장될 객체)
        var data = {
            dname: dept.dname,
            loc: dept.loc
        }
        // 저장 함수 호출
        DeptService.create(data)   // 벡엔드로 저장 요청
        .then((response:any)=>{
            // 저장 성공유무 -> submitted 변수에 값을 true로 변경
            setSubmitted(true); // 화면 변경
            console.log(response.date);
        })
        .catch((e:Error)=>{
            console.log(e);
        })
       }

그리고 리턴 값 아래의 HTML 작성은 다음과 같이 하였습니다.

return (
    <div className="row">
      {submitted ? (
        // 저장버튼 클릭하면 아래 화면이 보임
        <div className="col-6 mx-auto">
          <h4>You submitted successfully!</h4>
          {/* Add 버튼 클릭하면 다시 새로운 부서 저장 페이지로 이동(새폼 보이기) */}
          <button className="btn btn-success" onClick={newDept}>
            Add
          </button>
        </div>
      ) : (
        <>
          {/* 제목 start */}
          <TitleCom title="Add Dept No Page" />
          {/* 제목 end */}

          <div className="col-6 mx-auto">
            {/* 부서명 입력창 */}
            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="dname" className="col-form-label">
                  Dname
                </label>
              </div>

              <div className="col-9">
                <input
                  type="text"
                  id="dname"
                  required
                  className="form-control"
                  value={dept.dname}
                  onChange={handleInputChange}
                  placeholder="dname"
                  name="dname"
                />
              </div>
            </div>
            {/* 부서명 입력창 끝*/}
            {/* 부서위치 입력창 시작 */}
            <div className="row g-3 align-items-center mb-3">
              <div className="col-3">
                <label htmlFor="loc" className="col-form-label">
                  Loc
                </label>
              </div>
              <div className="col-9">
                <input
                  type="text"
                  id="loc"
                  required
                  className="form-control"
                  value={dept.loc}
                  onChange={handleInputChange}
                  placeholder="loc"
                  name="loc"
                />
              </div>
            </div>
            {/* 부서위치 입력창 끝 */}

            {/* 저장버튼 시작 */}
            <div className="row g-3 mt-3 mb-3">
              <button
                onClick={saveDept}
                className="btn btn-outline-primary ms-2 col"
              >
                Submit
              </button>
            </div>
            {/* 저장버튼 끝 */}
          </div>
          
        </>
      )}
    </div>
  );
}

export default AddDeptNop;

App.tsx 파일의 AddDeptNop 페이지 라우트 처리

 {/* dept */}
          <Route path="/dept-nop" element={<DeptListNop />} />
          <Route path="/add-dept-nop" element={<AddDeptNop />} />

📖 벡엔드 작업

1) service > DeptService.java 파일에 전체조회, 검색어 조회 함수를 작성합니다.

@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;  // DI

    /** 전체조회 */
    public List<Dept> findAll(){
        List<Dept> list = deptRepository.findAll();
        return list;
    }

    /** 검색어(dname like) 조회 함수 */
    public List<Dept> findAllByDnameContaining(String dname){
        List<Dept> list = deptRepository.findAllByDnameContaining(dname);
        return list;
    }

2) controller > DeptController.java 파일에 전체 조회 함수를 작성합니다.

@Slf4j
@RestController
@RequestMapping("/api")
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /**
     * 전체 조회 + like 검색
     */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll(
            @RequestParam(defaultValue = "") String dname) {
        try {
            // 전체 조회 + like 검색
            List<Dept> list = deptService.findAllByDnameContaining(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

3) API 테스트 - 아래 화면과 같이 GET 메서드로 테스트 합니다.

결과

더보기
GET http://localhost:8000/api/dept

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 20 Oct 2023 07:42:35 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 50,
    "dname": "ACCOUNTING",
    "loc": "NEW YORK"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 60,
    "dname": "RESEARCH",
    "loc": "DALLAS"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 70,
    "dname": "SALES",
    "loc": "CHICAGO"
  },
  {
    "insertTime": "2023-10-20 07:42:32",
    "updateTime": null,
    "deleteYn": "N",
    "deleteTime": null,
    "dno": 80,
    "dname": "OPERATIONS",
    "loc": "BOSTON"
  }
]
응답 파일이 저장되었습니다.
> 2023-10-20T164235.200.json

Response code: 200; Time: 234ms (234 ms); Content length: 529 bytes (529 B)

4) 프론트엔드 + 벡엔드 연동 확인

프론트 엔드 서버와 벡엔드 서버를 재시작하여 전체 조회 기능을 확인합니다.

반응형

'Spring Boot > 스프링부트 예제' 카테고리의 다른 글

QnA 다양한 검색 게시판 CRUD (1)  (1) 2023.10.24
게시판 페이징 처리  (1) 2023.10.23
JPA - 연관관계 매핑  (0) 2023.10.19
JPA 페이징 처리  (1) 2023.10.18
JPA - JPQL(Java Persistence Query Language)  (1) 2023.10.17
반응형

안녕하세요 ! :)

front + backend 를 통합한 게시판을 생성하는 예제를 만들어보겠습니다.

 

📖 frontend작업

프론트 작업은 VSCode를 이용하여 프로젝트를 생성하였습니다.

frontend 프로젝트는 React + typescript를 이용하였습니다.

프로젝트 형태는 다음과 같습니다.

src 폴더 아래에는 다음과 같은 파일들이 들어갑니다.

  • assets(css파일, 이미지파일, javascript 파일)
  • compoents(common(헤더, 푸터, 타이틀))
  • page(auth(로그인관련), 각종 메뉴 페이지들)
  • common(공통으로 사용할 페이지 관련)
  • service(벡엔드에 요청할 서비스 함수작성 파일)
  • types(자바의 모델과 같은 파일)
  • utils(백앤드 서버와 연동) 파일을 생성하였습니다.

1) 패키지 설치

# 설치 패키지 
# 1) 메뉴 라이브러리 설치
npm i react-router-dom

# 2) 벡엔드 연동 라이브러리 설치 : ajax 업그레이드
npm i axios

# 3) pre css 컴파일러 : node-sass -> 더이상 안씀 : sass 설치할것
<!-- npm i node-sass -->
npm install sass
# 4) Material Page component 업그레이드 
# 과거 v4 -> v5 변경 설치
npm i @mui/material @emotion/react @emotion/styled

# 4-1) 소스에서 임포트 사용법 : <Pagination />
import Pagination from '@mui/material/Pagination';

# 5) typescript jquery, jqueryui type 넣기
# 5-1) typescript jquery 사용
npm i --save-dev @types/jquery

2) axios 설정

📂 utils > http-commons.ts

이곳에서는 axios를 이용하여 벡엔드와 연동하는 작업을 하는 곳입니다.

import axios from "axios";

// todo : baseURL: "http://스프링ip주소:스프링포트번호/공통url"
export default axios.create({
  baseURL: "http://localhost:8000/api",
  headers: {
    "Content-Type": "application/json"
  }
});

base url에는 스프링부트주소를 입력합니다. 주소뒤에는 공통url인 api로 지정하였습니다.

 

3) 부서(Department) 게시판 생성하기

1) 부서 인터페이스 생성

📂 types > IDept.ts 생성

// 자바의 모델 클래스와 같음
// 인터페이스 : 속성의 자료형을 미리 지정하는 것
export default interface IDept {
    dno?: any | null,
    dname: string,
    loc: string
}

2) 부서 서비스 생성

📂 service > DeptService.ts 생성

// DeptService.ts
// axios 통신을 위한 import 작업
import http from "../utils/http-common";
// type 인터페이스 import
import IDept from "../types/IDept";

// 화살표 함수 단축키 : nfn
/** 전체 조회 요청 함수 */
const getAll = () => {
  // 조회요청 : .get("/url")
  // 사용법 : http.get<리턴타입>("스프링부트의 컨트롤러url");
  return http.get<Array<IDept>>("/dept");
};

/** 상세 조회(1건조회) 요청 함수 : 기본키 */
//  매개변수로는 부서번호(dno) type는 any
const get = (dno: any) => {
  return http.get<IDept>(`/dept/${dno}`);
};

/** 저장 요청 함수 */
const create = (data: IDept) => {
  return http.post<IDept>("/dept", data);
};

/** 수정 요청 함수 : 기본키와 객체 필요 */
const update = (dno: any, data: IDept) => {
  return http.put<any>(`/dept/${dno}`, data);
};

/** 삭제 요청 함수 : 기본키(dno) */
const remove = (dno: any) => {
  return http.delete<any>(`/dept/deletion/${dno}`);
};

/** 부서명 검색 요청 함수 */
const findByDname = (dname: string) => {
  return http.get<Array<IDept>>(`/dept?dname=${dname}`);
};


// 함수 기능 내보내기
const DeptService = {
    getAll,
    get,
    create,
    update,
    remove,
    findByDname
}

export default DeptService;

3) 부서페이지 생성

📂 page> DeptListNop.tsx 생성

// DeptListNop.tsx

import React from 'react'

function DeptListNop() {
  return (
    <div>DeptListNop</div>
  )
}

export default DeptListNop

3-1) App.ts 페이지 라우터 처리

import React from "react";
// app css import
import "./assets/css/app.css";

import HeaderCom from "./components/common/HeaderCom";
import { Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/auth/Login";
import Register from "./pages/auth/Register";
import ForgotPassword from "./pages/auth/ForgotPassword";
import NotFound from "./pages/common/NotFound";
import DeptListNop from "./pages/dept-nop/DeptListNop";
import EmpListNop from "./pages/emp-nop/EmpListNop";

function App() {
  return (
    <div className="App">
      <HeaderCom />

      {/* <!-- 구분 막대 시작 --> */}
      <div className="gutter text-center text-muted fade-in-box">
        <div>클론 코딩 예제 사이트에 오신 것을 환영합니다.</div>
      </div>
      {/* <!-- 구분 막대 끝 --> */}

      <div id="content-wrapper">
        {/* 라우터 정의 시작 */}
        <Routes>
          {/* login */}
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          <Route path="/forgot-password" element={<ForgotPassword />} />

          {/* dept */}
          <Route path="/dept-nop" element={<DeptListNop />} />

          {/* emp */}
          <Route path="/emp-nop" element={<EmpListNop />} />

          {/* NotFound */}
          <Route path="*" element={<NotFound />} />
        </Routes>
        {/* 라우터 정의 끝 */}
      </div>
    </div>
  );
}

export default App;

4) 서버 시작

5) 리스트 페이지 변수 및 함수 정의

// DeptListNop.tsx

import React, { useEffect, useState } from "react";
import TitleCom from "../../components/common/TitleCom";
import { Link } from "react-router-dom";
import IDept from "../../types/IDept";
import DeptService from "./../../services/DeptService";

function DeptListNop() {
  // 변수 정의
  // todo: 부서 배열 변수
  const [dept, setDept] = useState<Array<IDept>>([]);
  // todo: 검색어 변수
  const [searchDname, setSearchDname] = useState<string>("");

  // 함수 정의
  // todo: useEffect : 화면이 뜨자마자 실행되는 이벤트 함수(1번)
  // 사용법 : useEffect(()=>{실행문},[])
  useEffect(() => {
    // 전체 조회 실행
    retrieveDept();
  }, []);

  // todo) 검색어 수동 바인딩 함수
  const onChangeSearchDname = (e: any) => {
    // todo) e.target : input 태그에 현재 걸린 이벤트
    // => e.target.value : 현재 조작하는 태그의 value 값
    setSearchDname(e.target.value);
  };

  // todo) 전체 조회 함수
  const retrieveDept = () => {
    DeptService.getAll() // backend에 전체조회요청
      .then((response: any) => {
        // todo 성공했을때 처리
        setDept(response.data);
        // todo 로그 찍기
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // todo 실패했을때 처리
        console.log(e);
      });
  };

  // todo) 검색어 조회 함수
  const findByDname = () => {
    DeptService.findByDname(searchDname) // backend 요청
      .then((response: any) => {
        // todo 성공했을때 처리
        setDept(response.data);
        // todo 로그 찍기
        console.log("response", response.data);
      })
      .catch((e: Error) => {
        // todo 실패했을때 처리
        console.log(e);
      });
  };

  return (
    <>
      {/* 제목 보여주는 공통 컴포넌트 */}
      <TitleCom title="Dept List No Page" />
      {/* 제목 end */}

      {/* dname start(검색창) */}
      <div className="row mb-5 justify-content-center">
        {/* w-50 : 크기 조정, mx-auto : 중앙정렬(margin: 0 auto), justify-content-center */}
        <div className="col-12 w-50 input-group mb-3">
          {/* 입력창 시작 */}
          <input
            type="text"
            className="form-control"
            placeholder="Search by dname"
            value={searchDname}
            onChange={onChangeSearchDname}
          />
          {/* 입력창 끝 */}

          {/* 검색버튼 시작 */}
          <div className="input-group-append">
            <button
              className="btn btn-outline-secondary"
              type="button"
              onClick={findByDname}
            >
              Search
            </button>
          </div>
          {/* 검색버튼 끝 */}
        </div>
      </div>
      {/* dname end */}

      {/* table start(본문) */}
      <div className="col-md-12">
        {/* table start */}
        <table className="table">
          {/* 테이블 제목 시작 */}
          <thead className="table-light">
            <tr>
              <th scope="col">Dname</th>
              <th scope="col">Loc</th>
              <th scope="col">Actions</th>
            </tr>
          </thead>
          {/* 테이블 제목 끝 */}
          {/* TODO: 테이블 데이터 시작 */}
          <tbody>
            {dept &&
              dept.map((data) => (
                <tr key={data.dno}>
                  <td>{data.dname}</td>
                  <td>{data.loc}</td>
                  <td>
                    <Link to={"/dept-nop/" + data.dno}>
                      <span className="badge bg-success">Edit</span>
                    </Link>
                  </td>
                </tr>
              ))}
          </tbody>
          {/* 테이블 데이터 끝 */}
        </table>
        {/* table end */}
      </div>
      {/* table end */}
    </>
  );
}

export default DeptListNop;

6) 서버 재시작 및 결과 확인

📖 backend 작업

frontend 작업 후 backend 작업을 해봅시다 :)

backend 작업은 인텔리제이에서 프로젝트를 생성하였습니다.

 

1) backend 프로젝트 생성

프로젝트 이름은 SimpleDMS로 정하고 위치는 프론트엔드 작업물과 같은 폴더 위치로 지정합니다.

JDK는 azul-11 버전, Java 버전은 11버전으로 하고 패키지 생성은 War 파일로 선택하였습니다.

 

스프링부트 버전은 2.7.16 으로 하고 종속성 추가는

  1. Spring Boot DevTools
  2. Lombok
  3. Spring Web
  4. Spring Data JPA
  5. Oracle Driver

를 선택후 프로젝트를 생성합니다.

 

2) backend 프로젝트 환경설정

2-1) build.gradle 파일에 dependencies 추가 

//    오라클 라이브러리( 19c )
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'
//    logback , log4jdbc 설정
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.slf4j:jcl-over-slf4j:1.7.36'

2-2) 프로퍼티 파일 한글 깨짐 방지를 위한 설정

설정 > 에디터 > 파일 인코딩 > 프로퍼티 파일에 대한 디폴트 인코딩을 UTF-8로 저장 후 아래 체크박스를 체크한후 확인

2-3) 프로퍼티 파일 설정

main > resources > application.properties 파일에 설정을 추가합니다.

# 서버 포트
server.port=8000

# 오라클 설정 : log4j 적용
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:오라클서버포트/db명
spring.datasource.username=서버id
spring.datasource.password=서버패스워드

# jpa 설정
spring.jpa.hibernate.ddl-auto=none
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
spring.jpa.show-sql=true
# sql log 찍기
spring.jpa.properties.hibernate.format_sql=true
#Logging Setting , hibernate info 레벨 로깅 설정 : debug, trace 등
logging.level.org.hibernate=info
# batch size 설정 : 연관관계 설정 시 N+1 문제 최소화
#  여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줌
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
# 1) resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.jpa.defer-datasource-initialization=true
# 2)  resource/data.sql 자동 실행 ( DML 실행 )
#  -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true

# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true

# HikariCP settings
spring.datasource.hikari.minimumIdle=1
spring.datasource.hikari.maximumPoolSize=1
spring.datasource.hikari.poolName=HikariPoolBooks

 

2-4) 실습용 스크립트 logback 추가

실습용 스크립트 파일 및 logback 파일을 resources 파일 아래에 추가합니다.

1. data.sql

더보기
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'ACCOUNTING', 'NEW YORK','N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'RESEARCH', 'DALLAS', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'SALES', 'CHICAGO', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_DEPT
VALUES (SQ_DEPT.nextval, 'OPERATIONS', 'BOSTON', 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);

INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'SMITH', 'CLERK', 7902, TO_CHAR(to_date('17-12-1980', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 800, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'ALLEN', 'SALESMAN', 7698, TO_CHAR(to_date('20-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1600,
        300, 30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'WARD', 'SALESMAN', 7698, TO_CHAR(to_date('22-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1250, 500,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'JONES', 'MANAGER', 7839, TO_CHAR(to_date('2-4-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2975, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'MARTIN', 'SALESMAN', 7698, TO_CHAR(to_date('28-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1250,
        1400, 30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'BLAKE', 'MANAGER', 7839, TO_CHAR(to_date('1-5-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2850, NULL,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'CLARK', 'MANAGER', 7839, TO_CHAR(to_date('9-6-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 2450, NULL,
        10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'SCOTT', 'ANALYST', 7566, TO_CHAR(to_date('13-07-1987', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 3000,
        NULL, 20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'KING', 'PRESIDENT', NULL, TO_CHAR(to_date('17-11-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 5000,
        NULL, 10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'TURNER', 'SALESMAN', 7698, TO_CHAR(to_date('8-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1500, 0,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'ADAMS', 'CLERK', 7788, TO_CHAR(to_date('13-07-1987', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1100, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'JAMES', 'CLERK', 7698, TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 950, NULL,
        30, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'FORD', 'ANALYST', 7566, TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 3000, NULL,
        20, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);
INSERT INTO TB_EMP
VALUES (SQ_EMP.nextval, 'MILLER', 'CLERK', 7782, TO_CHAR(to_date('23-1-1982', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'), 1300, NULL,
        10, 'N', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'),NULL, NULL);

COMMIT;

 

2. log4j 프로퍼티 파일

더보기
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

3. logback xml 파일

더보기
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

	<!-- Appenders -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p [%c] %m%n</Pattern>
		</encoder>
	</appender>

	<appender name="console-infolog" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<Pattern>%d %5p %m%n</Pattern>
		</encoder>
	</appender>

	<!-- Logger -->
	<logger name="com.example.simpledms" level="DEBUG" appender-ref="console" />
	<logger name="jdbc.sqlonly" level="INFO" appender-ref="console-infolog" />
	<logger name="jdbc.resultsettable" level="INFO" appender-ref="console-infolog" />

	<!-- Root Logger -->
	<root level="off">
		<appender-ref ref="console" />
	</root>
</configuration>

4. schema.sql 파일

더보기
DROP SEQUENCE SQ_DEPT;
CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY 10;

DROP SEQUENCE SQ_EMP;
CREATE SEQUENCE SQ_EMP START WITH 8000 INCREMENT BY 1;

DROP TABLE TB_EMP CASCADE CONSTRAINT;
DROP TABLE TB_DEPT CASCADE CONSTRAINT;

CREATE TABLE TB_DEPT
(
    DNO         NUMBER NOT NULL PRIMARY KEY,
    DNAME       VARCHAR2(255),
    LOC         VARCHAR2(255),
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

CREATE TABLE TB_EMP
(
    ENO         NUMBER NOT NULL PRIMARY KEY,
    ENAME       VARCHAR2(255),
    JOB         VARCHAR2(255),
    MANAGER     NUMBER,
    HIREDATE    VARCHAR2(255),
    SALARY      NUMBER,
    COMMISSION  NUMBER,
    DNO         NUMBER,
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

2-5) com.example.simpledms > 패키지 폴더 생성

아래와 같이 설정, 컨트롤러, 모델, 레포지토리, 서비스 폴더를 생성합니다.

 

3) 파일 생성

3-1) model > BaseTimeEntity.java 생성

📂 model > BaseTimeEntity.java

더보기
package com.example.simpledms.model;

import lombok.Getter;
import org.hibernate.annotations.SQLDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : kangtaegyung
 * date : 2022/10/16
 * description : JPA 에서 자동으로 생성일자/수정일자 만들어주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/16         kangtaegyung          최초 생성
 */
@Getter
// @MappedSuperclass : JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우
// 필드들(createdDate, modifiedDate)도 칼럼으로 인식하도록 한다.
@MappedSuperclass
// @EntityListeners(AuditingEntityListener.class) : BaseTimeEntity 클래스에
// Auditing 기능을(자동 생성일, 수정일) 포함시킨다.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    private String insertTime;

    private String updateTime;

    private String deleteYn;        // 삭제여부

    private String deleteTime;      // 삭제시간

    @PrePersist
        //해당 엔티티 저장하기 전
    void onPrePersist(){
        this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.deleteYn = "N";
    }

    @PreUpdate
        //해당 엔티티 수정 하기 전
    void onPreUpdate(){
        this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime;
        this.deleteYn = "N";
    }
}

3-2) model > Dept/Emp.java 생성

📂 Dept.java

더보기
package com.example.simpledms.model;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

/**
 * packageName : com.example.modelexam.model
 * fileName : Dept
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 모델 클래스
 * 요약 :
 * Hard Delete : 실제 데이터를 삭제하는 행위
 * Soft Delete는 물리적인 데이터 삭제로 발생할 수 있는 문제를 방지하고 쉽게 복원할 필요가 있거나 삭제된 데이터들을 보관하여 데이터로써 활용할 필요나 가치가 있는 경우에 사용
 * 실무에서는 법적으로 개인자료일 경우 3년 또는 1년이상 데이터를 보관할 의무가 있고 어길수 법적 문제가 생길 수 있음 -> 그래서 soft delete 방식을 대부분 구현하고 있음
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Entity
@Table(name="TB_DEPT")
@SequenceGenerator(
        name = "SQ_DEPT_GENERATOR"
        , sequenceName = "SQ_DEPT"
        , initialValue = 1
        , allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
// 사용법 : 1) @Where(clause = "DELETE_YN = 'N'") : select 될 때 조건("DELETE_YN = 'N'")을 강제로 붙여줌
//         2) @SQLDelete(sql = "대체 sql문") : delete 될때 대체해서 실행될 쿼리문
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_DEPT SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE DNO = ?")
public class Dept extends BaseTimeEntity {
    //    부서넘버
//    @Id : Primary Key 에 해당
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_DEPT_GENERATOR"
    )
    @Column
    private Integer dno;
    //    부서이름
    @Column
    private String dname;
    //    부서위치
    @Column
    private String loc;
}

📂 Emp.java

더보기
package com.example.simpledms.model;

import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.*;

@Entity
@Table(name = "TB_EMP")
@SequenceGenerator(
        name = "SQ_EMP_GENERATOR"
        , sequenceName = "SQ_EMP"
        , initialValue = 1
        , allocationSize = 1
)
@Setter
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_EMP SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE ENO = ?")
public class Emp extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE
            , generator = "SQ_EMP_GENERATOR"
    )
    @Column
    private Integer eno;

    @Column
    private String ename;

    @Column
    private String job;

    @Column
    private Integer manager;

    @Column
    private String hiredate;

    @Column
    private Integer salary;

    @Column
    private Integer commission;

    @Column
    private Integer dno;
}

3-3) DeptRepository.java 생성

📂 repository > DeptRepository.java

package com.example.simpledms.repository;

import com.example.simpledms.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;


@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
    // dname like 검색 - 쿼리메소드
    List<Dept> findAllByDnameContaining(String dname);
}

3-4) DeptService.java 생성

📂 service > DeptService.java

package com.example.simpledms.service;

import com.example.simpledms.model.Dept;
import com.example.simpledms.repository.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class DeptService {
    @Autowired
    DeptRepository deptRepository;  // DI
    
    /** 전체조회 */
    public List<Dept> findAll(){
        List<Dept> list = deptRepository.findAll();
        return list;
    }
    
    /** 검색어(dname like) 조회 함수 */
    public List<Dept> findAllByDnameContaining(String dname){
        List<Dept> list = deptRepository.findAllByDnameContaining(dname);
        return list;
    }
    
    
}

3-5) 프론트<->벡엔드 통신을 위해 CORS 보안 허용 설정 파일 생성

📂 config > WebConfig.java

package com.example.simpledms.config;

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;

/**
 * packageName : com.example.dongsungsi.controller
 * fileName : WebConfig
 * author : kangtaegyung
 * date : 2022/06/14
 * description :
 * @Configuration : 자바클래스에 설정 기능을 부여하는 어노테이션 (application.properties 비슷)
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/06/14         kangtaegyung          최초 생성
 */

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
//                아래 url 허용
//                사용법 : .allowedOrigins("http://허용할 ip:허용할Port")
                .allowedOrigins("http://localhost:3000")

//                Todo: 아래 추가해야 update, delete, insert, select 가 cors 문제가 안생김
                .allowedMethods(
                        HttpMethod.GET.name(),
                        HttpMethod.POST.name(),
                        HttpMethod.PUT.name(),
                        HttpMethod.DELETE.name(),
                        HttpMethod.PATCH.name()
                );
    }
}

3-6) DeptController.java 생성

📂 controller > DeptController.java

package com.example.simpledms.controller;

import com.example.simpledms.model.Dept;
import com.example.simpledms.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * packageName : com.example.simpledms.controller
 * fileName : DeptController
 * author : GGG
 * date : 2023-10-19
 * description : 부서 컨트롤러 (@RestController - react용)
 * 요약 :
 * react(3000) <-> springboot(8000) 연동 : axios
 * 인터넷 기본 보안 : ip, port 최초에 지정된 것과 달라지면
 * => 해킹으로 기본 인정 (블러킹 : 단절) : CORS 보안
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-19         GGG          최초 생성
 */
@Slf4j
@RestController
@RequestMapping("/api")
public class DeptController {
    @Autowired
    DeptService deptService;    // DI

    /**
     * 전체 조회 + like 검색
     */
    @GetMapping("/dept")
    public ResponseEntity<Object> getDeptAll(
            @RequestParam(defaultValue = "") String dname) {
        try {
            // 전체 조회 + like 검색
            List<Dept> list = deptService.findAllByDnameContaining(dname);
            if (list.isEmpty() == false) {
//                성공
                return new ResponseEntity<>(list, HttpStatus.OK);
            } else {
//                데이터 없음
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

}

 

3-7) REST API 테스트

3-8) 프론트 연동 테스트

반응형

'Spring Boot' 카테고리의 다른 글

JWT  (0) 2024.01.26
스프링 시큐리티  (0) 2024.01.26

+ Recent posts