반응형

사이트 바로가기 : http://playhost.shop

 

이번 23년 11월 15일부터 12월 8일 대략 3주간 팀 프로젝트를 진행하였습니다.

 

팀원은 총 6명이 진행하였으며 팀당 2명씩 역할 분담을 나누어 사이트를 제작하였습니다.

 


📝 프로젝트 선정배경

프로젝트 선정배경은 프로젝트 주제 선정과정에서 강의 시간에 배웠던 API를 활용한 사이트을 제작하기로 회의 때 이야기를 하였고, 그 중 게임 소프트웨어 유통망 사이트인 Steam(스팀)의 API를 활용한 게임 판매 사이트를 제작하기로 결정하였습니다.

 


📝 프로젝트 목표

저희 playhost 사이트의 목표는 다음과 같습니다.

  • 직관적이고 한눈에 보기 쉬운 사이트 일 것
  • 스팀 API를 활용한 다양한 게임 조회 기능 구현
  • 사이트 관리자가 직접 판매할 게임을 추가/삭제할 수 있는 기능 구현
  • 포인트 구매 기능 구현

제작 기간은 23년 11월 15일 ~ 12월 08일 약 3주간 진행되었습니다.


📝 담당 역할

저는 사이트 프론트 + 백엔드 부분을 담당하였고 상세 기능은 아래와 같이 기능을 구현하였습니다.

  • 구매한 게임을 담을 수 있는 장바구니 기능 구현
  • 토스페이먼츠 API를 활용한 결제 시스템 구현
  • 게임 뉴스를 게시할 수 있는 News 페이지 기능 구현
  • 팀원 소개 및 사이트 소개 페이지 구현

📝 프로젝트에 사용한 기술

프로젝트에서 사용한 기술은 다음과 같습니다.

🔶 개발 툴

1) intellij IDEA - 백앤드 부분

2) VSCode - 프론트앤드 부분

3) Github - 형상관리

4) Oracle DB - DB

5) 배포 - 아마존 AWS


📝 시스템 구성도


📝 ERD Table

 


📝 로그인 기능

 

먼저 사이트 로그인 기능입니다.

로그인은 이메일, 비밀번호를 입력하여 로그인을 할 수 있습니다.

회원가입의 경우 이름, 이메일, 비밀번호, 비밀번호 재입력을 통해 회원가입을 합니다.

 

회원가입을 진행하면 회원가입시 입력한 이메일로 인증번호가 전송되며 전송된 인증번호를 입력하여 회원가입을 진행합니다.

 

비밀번호 찾기 기능은 가입시 입력한 이메일을 입력하면 이메일 주소로 인증번호가 전송됩니다.

그 인증번호를 입력하여 일치하면 비밀번호 변경 부분으로 넘어가게 되고, 새로운 비밀번호로 변경이 가능합니다.

 

소셜로그인 부분입니다.

 

소셜로그인은 구글, 카카오로 로그인이 가능하며 별도 회원가입 없이 구글과 카카오에 로그인이 되어있으면 다이렉트로 로그인이 진행됩니다.

 


📝 메인페이지

메인페이지는 사이트 처음 접속시 보이는 페이지입니다.

  • 추천게임 목록 부분은 관리자 페이지에서 해당 상품에 이미지를 업로드 한 경우 메인 화면에 표시가 됩니다.
  • 금주의 할인 게임은 관리자 페이지에서 판매 제품에 할인율을 적용할 경우 할인 게임 탭 부분에 표시가 됩니다.


📝 상세 조회

상세 조회 페이지부분은 게임 태그별(액션, 어드벤처 등...) 게임 목록들을 보여주는 페이지입니다.

쇼핑몰로 치면 카테고리별로 상품을 보여주는 부분이라 생각하시면 됩니다.


📝 게임 상세조회

게임 목록에서 해당 게임을 선택하면 보이는 페이지 입니다.

화면 상단에는 썸네일 이미지 + 타이틀이름 + 리뷰 상태를 표시하며

화면 중간 부분에는 게임 스크린샷, 게임 트레일러 영상 등이 표시됩니다.

스크롤을 내리면 게임 설명부분과 사이드바 부분으로 나누어져 있으며 장바구니 추가 버튼이 존재합니다.

 

해당 게임에 대한 리뷰도 작성이 가능합니다. 단, 게임을 구매한 경우에만 리뷰 작성이 가능하도록 설정되어 있습니다.


📝 장바구니 기능

 

장바구니 부분은 게임 상세조회에서 장바구니 탭 부분에 금액이 적힌 버튼을 누르게 되면 장바구니에 추가가 됩니다.

 

장바구니 페이지로 넘어가게 되면 해당 장바구니에 담긴 개수 표시와 장바구니에 담긴 제품 상세 표시, 주문하기 버튼으로 구성되어 있습니다.

 


📝 결제 페이지

위의 장바구니 페이지에서 결제하기 버튼을 누르면 결제 페이지로 넘어가게 됩니다.

 

결제 페이지부분은

유저의 주문내역 부분과 포인트 영역, 토스페이먼츠 API를 이용한 결제 선택창이 표시됩니다.

토스페이먼츠 API를 이용하여 실제 결제는 되지 않고 테스트 결제가 되어 결제가 진행됩니다.

 

구매완료시 해당 계정 이메일로 구매내역을 전송합니다.

 

 

 

다음시간에는 그 외 기능을 포스팅하겠습니다~🙋‍♂️

반응형
반응형

안녕하세요 ! :)

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) 프론트 연동 테스트

반응형

+ Recent posts