국비 지원 개발자 코스_Day79

  • by

MVC의 한계

양방향 데이터 바인딩 → 복잡도 증가 → Redox와 같은 단방향 방식

상태 관리 → 쿠키 및 세션 → 상태 값이 변경될 때 화면 반영 → 브라우저가 Render Tree를 그릴 필요가 있다(기존 DOM Tree + css) → Render Engine에 의한 속도, 성능 차이

새로운 데이터 관련 → useState, useEffect → 재 렌더링 (return 내의 것이 재묘화 된다)


createStore →예에서 작성한 스토어

구성 요소 외부에 있는 리포지토리

상태를 관리하고 줌 → 모든 상태 변경이 결정되었습니다.

let state가 있다

매개변수로 작업자 수신

increase를 받으면 카운트 값을 올립니다.

worker(state, action) → 예제에서 만든 Dispatcher

액션

변경되는 정보(유형) 포함 → increase 키워드를 Store에 전달

디스패처

액션을 상점에 전달하는 허브 역할

State와 Action이 매개변수로 전달됨

switch 문을 사용하여 필터링

보기

컴퍼넌트에 increase 한 결과가 출력된다

다른 입력(요청) 발생 → Action 발생하고 다시 Dispatcher로 이동


게시 – 구독 모델

비동기 메시징 패러다임

게시 – 구독 모델에서 보낸 사람의 메시지는 특별한 수신자에 의해 결정되지 않습니다.

대신, 발행된 메시지는 지정된 카테고리에 따라 각 카테고리의 구독을 신청한 수신자에게 전달됩니다.

받는 사람은 게시자에 대한 지식이 없어도 원하는 메시지만 받을 수 있습니다.

리덕스 플로우

컴퍼넌트의 스토어 구독 subscribe (listener)

구성 요소가 상점을 구독합니다(구독 중에 특정 함수가 상점에 전달됨)

→ 스토어 상태 값에 변동이 발생했을 경우에 건네받은 함수를 호출해 줌

상점에 상태 변경 알림 dispatch(action)

어떤 컴퍼넌트에 이벤트가 발생해 상태를 갱신할 필요가 있는 경우는, 디스패치 함수를 개입시켜 액션을 스토어에 건네줍니다 (액션은 상태가 변화했을 때에 참조할 수 있는 오브젝트).

→ 전달된 데이터(Action)에는 유형(type)이 포함되어야 합니다(ex.{type: ‘increase’})

리듀서에 의한 상태 변화

action 객체를 받으면 받은 액션 유형에 따라 상태를 업데이트하는 방법을 정의합니다.

→ 리듀서는 업데이트 로직을 정의하는 함수 (store에 들어가는 state와 state를 바꾸는 함수를 정의하는 곳)

→ 해당 로직을 구현해야합니다 (불변성을 유지하는 중요!
)

상태 변화가 발생하면 구독하고 있던 컴퍼넌트에 통지 listener()

상태에 변화가 발생하면, 이전에 컴퍼넌트가 스토어에 구독했을 때에 건네준 함수(listener)가 불려 갑니다

→ 컴포넌트가 새로운 상태를 생성함에 따라 다시 렌더링 실행

react-redux 사용법

프로젝트의 루트 레벨 – index.js


worker

dispatcher 함수 사용 – switch 문


index.js (app이 들어 있기 때문에)

호출 할 때 useSelector 후크를 사용하십시오.


dispatch 상태를 변경하는 함수


// 상태는 createStore() 안에 있다
// 상태를 담을 변수 선언
// 콜백함수를 담을 배열 선언
// send함수 구현 - 파라미터로 action을 받음
// 구독발행모델 - subscribe(handler-콜백함수)
// subscribe를 통해서 들어온 콜백함수는 handlers배열에 담는다
// getState함수를 통해서 state값을 반환받음
// return { send, subscribe, getState }
const createStore = () => { // 배치위치는 index.js -> 이곳에서 store생성
  let state; // 상태를 담아두는 저장소
  let handlers = () // 함수를 담아두는 배열 선언

  // 상태를 바꾸는 일을 send()에서 한다 - useSelector훅
  const send = (action) => {
    console.log('send 호출')
    // 새로운 객체가 만들어진다 -> 깊은복사
    // 아래 코드와 같은 원리
    // Map m = new HashMap()
    // m = new HashMap()
    state = worker(state, action)
    // 나에게 구독신청한 사람들에게 모두 알림
    handlers.forEach(handler => handler()) // 전달받은 함수를 호출해줘
  }

  const subscribe = ((handler) => { // useDispatch훅
    // 콜백함수
    handlers.push(handler)
  })

  const getState = () => {
    return state;
  }

  // 함수 안에서 함수를 리턴하도록 처리해야 바깥쪽에서 해당 함수를 요청할 수 있다
  return {
    // state -> 이런식으로 직접적으로 상태값을 주지 않는다
    // 밖에서 상태값을 변경해야하기에 send를 넘겨준다
    send, // 함수(객체), 파라미터로 들어온 상태를 받아서 가공 후 새로운 객체로 내보냄
    // 직접 상태값을 주지 못하지만 getState호출해 상태값 알 수 있게 함
    getState, // 함수, 상태정보를 담은 state를 반환해줌
    subscribe
  }
} // end of createStore

// react-redux에서는 worker가 dispatcher가 됨
// reducer, dispatch함수
const worker = (state = {count: 0}, action) => { // state가 undefined되는 것 방지하기위해 객체 선언
  // worker안에서는 무엇을 하는가?
  // 상태를 바꾸면 createStore 안 state의 참조 무결성이 깨짐
  // 리덕스에서는 상태를 바꾸는 함수는 반드시 새로운 상태를 반환해야한다
  // 새로운 상태 -> 화면의 입력(Action)으로 상태의 객체를 줄테니 이 객체를 Deeo copy해서
  // 기존의 참조링크를 끊어내라 - 그래야 side effect 방지 가능함
  switch(action.type){
    case 'increase':
      return {...state, count: state.count + 1}
      case 'decrease':
      return {...state, count: state.count - 1}
    default:
      return {...state}
  }
}

// 자바스크립트에서는 함수도 파라미터로 넘길 수 있다
const store = createStore(worker) // index.js에서 생성할 것임 - props대신 중앙에서 한번에 가져다 사용

store.subscribe(function () {
  console.log(store.getState())
})

// action의 내용은 send에서 만듦
// 사용자가 버튼을 클릭했을 때 시그널 발생 - type을 정해서 value를 store에 전달한다
// store가 받아서 전역변수로 관리됨 - 다른 컴포넌트에서 즉시 사용 가능함
store.send({type: 'increase'}) // 시그널을 주기(어떤 타입을 바꿀건지) - action
store.send({type: 'increase'})
store.send({type: 'decrease'})

/*
send 호출
{ count: 1 }
send 호출
{ count: 2 }
send 호출
{ count: 1 }
*/

/*
  JS에서 함수는 객체이다
  소문자로 선언하면 함수이고
  대문자로 선언하면 화면을 렌더링하는 컴포넌트이다

  1. UI한테는 직접적인 상태를 주지 않는다
    return에서는 상태값을 직접 넘겨주지 않는다
    상태는 createStore함수에 있지만,
    변경하거나 읽거나 하는 코드들은 UI의 Component들이다(컴포넌트가 데이터를 사용한다)
    이 컴포넌트들은 createStore함수의 바깥쪽에 위치한다
    -> 밖에서 createStore함수 안의 state를 변경하는 로직을 작성해야한다
    -> worker를 createStore에 넘겨준다(createStore에서 변경할 값과 변경 내용을 알아야하기에)
    -> 언제 무엇을 변경할 것인가 시그널은 store 밖에서 준다(ex. 사용자가 버튼을 누름)

  문제제기
    컴포넌트(HomePage.jsx, LoginPage.jsx 등)가 여러개 있는 상황에서 어떤 컴포넌트가
    데이터가 변경되었는지 어떻게 알고서 getState함수를 호출할 것인가?
    -> 구독발행 모델 사용 - Pub and Subscribe
      일종의 패턴, 어떤 함수를 주고 데이터가 변경되면 그 함수를 호출해줌(이벤트처리)
*/

메모화

기존에 한 연산의 결값을 저장해 두고 같은 입력이 들어오면 재활용하는 프로그래밍 기법

중복 연산을 피할 수 있으며 애플리케이션 성능 최적화

useMemo와 useCallback의 차이

useMemo

메모된 값을 반환합니다.

아래 코드에서 ex로 지정된 값이 변경되면 () => fn 함수를 실행하고 함수의 반환 값을 반환합니다.

ex 값이 변경된 경우에만 작업을 수행 할 수 있도록 useMemo를 사용하여 ex라는 변수에 의존하도록 등록합니다.

→ 다시 렌더링이 발생하면 특정 변수가 변경된 경우에만 useMemo에 등록한 함수를 실행하도록 처리하면 불필요한 연산을 피할 수 있다

useMemo(() => fn, (ex))

useCallback

메모된 함수 반환

함수를 반환하려면 함수를 가진 변수로 초기화하는 것이 일반적입니다.

자식 컴포넌트에 함수를 props에 전달하거나(자식 컴포넌트의 다시 렌더링을 방지) 또는 외부에서 값을 얻는 api를 호출하는 경우(함수 참조 값과 동일) 사용

useCallback(fn, (ex))

스프링 주석

@Configuration

구성 파일을 작성하기 위한 (Bean 등록을 위한) 주석

Java 클래스에서 코스 문제를 해결하는 데 사용

클래스 선언 앞에 위치

@Bean

개발자가 직접 제어할 수 없는 외부 라이브러리 등을 Bean으로 할 때 사용

spring-service, data, servlet-context에서 사용

메소드 선언 앞에 위치

@ComponentScan

@Component 및 @Service, @Repository, @Controller 어노테이션이 부여된 클래스를 자동으로 스캔하여 Bean으로 등록하는 역할

클래스 선언 앞에 위치

@Controller

Model 객체를 만들고 데이터를 저장하고 View를 반환합니다.

클래스 선언 앞에 위치

@RestController

@Controller + @ResponseBody → 마임 타입 text/plain

화면이 아닌 문자열 출력 단순히 객체 만 반환, 데이터는 JSON 또는 XML 형식으로 HTTP 응답에 넣어 전송

클래스 선언 앞에 위치

@Autowired

종속성 주입에 사용되는 주석에서 종속 객체 유형에 해당하는 병을 찾아 주입하는 역할

setter 객체 주입 방법 대체

클래스 선언 앞에 위치

@RequestParam

사용자가 전달하는 값을 1:1에 매핑하는 주석

매개 변수로 값을 전달할 때 자주 사용

하나의 HTTP 요청 매개변수를 수신하는 데 사용 →

Map pMap

메소드 파라미터의 위치

@RequestBody

JSON 형식의 HTTP Body를 Java 객체로 변환하는 역할

메소드 파라미터의 위치

@Service

모델 계층에서 사용

내부에서 Java 로직 처리

클래스 선언 앞에 위치

@Repository

MyBatis 레이어를 객체 주입할 때 사용

DB 및 파일과 같은 외부 I/O 작업 처리

클래스 선언 앞에 위치

@PathVariable

해시 값 받기

요청 URI의 매핑 템플릿 변수를 처리하는 데 사용

메소드 파라미터의 위치


리액트 후크

useState

state 관리(동적 상태 관리)

가변적인 상태를 가질 수 있도록

함수에 파라미터를 넣어 호출하면(자), 건네받은 파라미터에 값이 변경되어 컴퍼넌트가 리렌더링 된다 (Dom Tree 와 Render Tree)

useEffect

렌더링될 때마다 특정 작업을 수행하도록 설정

종속 배열에 아무것도 넣지 않으면 렌더링 할 때마다 빈 배열은 처음 한 번만 배열에 값을 넣으면 값이 변경 될 때마다 실행됩니다.

useRef

사용자 입력 구성 요소를 기억하고 최근에는 useCallback을 많이 사용

저장 공간, DOM 요소에 액세스하는 데 사용

useNavigate

화면 전환용

양식이 제출되거나 특정 이벤트가 발생할 때 URL을 조작할 수 있는 인터페이스 제공

useCallback

메모가 있고 전체 함수를 기억합니다 (특정 함수 재사용)

함수 재사용시 렌더링 성능 최적화

useMemo

메모가 있고 값 저장(특정 값 재사용)

연관 값 재사용시 구성 요소 내부의 연산 최적화 가능

렌더링 중에 특정 값이 변경된 경우에만 연산 수행

import { Route, Routes } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';

function App({imageUploader}) {
  return (
    <>
      <Routes>
        <Route path="/" exact={true} element={<LoginPage />} />
        <Route path="/home" exact={true} element={<HomePage />} />
        <Route path="/dept/:gubun" element={<DeptPage imageUploader={imageUploader} />} />
        <Route path="/auth/kakao/callback" exact={true} element={<KakaoRedirectHandler />} />
        <Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
        <Route path="/profile" exact={true} element={<Profile />} />
      </Routes>
    </>
  );
}

export default App;

import React from 'react'
import { Container, Nav, Navbar } from 'react-bootstrap'
import { Link } from 'react-router-dom'

const BlogHeader = () => {
  return (
    <>
      <Navbar bg="light" variant="light">
        <Container>
          <Link to="/" className="nav-link">TerrGYM</Link>
          <Nav className="me-auto">
            <Link to="/home" className="nav-link">Home</Link>
            <Link to="/dept/0" className="nav-link">부서관리</Link>
            <Link to="/board" className="nav-link">게시판</Link>
          </Nav>
        </Container>
      </Navbar>
    </>
  )
}

export default BlogHeader

import React, { useCallback, useEffect, useState } from 'react'
import { Button, Form, Modal, Table } from 'react-bootstrap'
import '../css/style.css'
import BlogHeader from '../include/BlogHeader'
import DeptRow from '../dept/DeptRow'
import { deptInsertDB, deptListDB } from '../../service/dbLogic'
import { useNavigate, useParams } from 'react-router-dom'
import { MyInput, MyLabel, MyLabelAb } from '../styles/FormStyle'
import { validateDname } from '../../service/validateLogic'

const DeptPage = ({imageUploader}) => {
  // 화면 전환시나 가급적 전체 페이지 리로딩을 하지 않음 -> Navigate훅을 사용하면 됨
  const navigate = useNavigate()
  // path = "/dept/:gubun" -> 이 부분을 useParams가 가져옴
  // 디폴트 없고 부서등록이 성공하면 1을 돌려줌
  const gubun = useParams()
  const (deptList, setDeptList) = useState(())
  const (show, setShow) = useState(false)
  const handleClose = () => setShow(false)
  const handleShow = () => setShow(true)
  const (deptno, setDeptno) = useState(0)
  const (dname, setDname) = useState('')
  const (loc, setLoc) = useState('')
  // filename과 fileurl, 두 개를 담아야하니 객체로 선언할 것
  const (files, setFiles) = useState({filename: null, fileurl: null})
  const (comment, setComment) = useState({
    deptno: "",
    dname: "",
    loc: ""
  })
  const (star, setStar) = useState({
    deptno: "*",
    dname: "*",
    loc: "*"
  })
  const validate = (key, e) => {
    console.log('validate: ' + key)
    let result;
    if(key === 'dname'){
      result = validateDname(e);
    }
    setComment({...comment, (key): result})
    if(result) {
      if(result === ''){
        setStar({...star, (key): ''})
      } else {
        setStar({...star, (key): '*'})
      }
    } else {
      setStar({...star, (key): ''})
    }
  }

  // deptno 값 가져와서 저장
  const handleDeptno = useCallback((value) => {
    console.log(value)
    setDeptno(value)
  }, ())
  // dname 값 가져와서 저장
  const handleDname = useCallback((value) => {
    console.log(value)
    setDname(value)
  }, ())
   // loc 값 가져와서 저장
  const handleLoc = useCallback((value) => {
    console.log(value)
    setLoc(value)
  }, ())

  // 조건 검색 구현
  const reactSearch = () => {
    // select 콤보에서 선택한 값 담기
    const gubun = document.querySelector('#gubun').value
    // 조건검색에 필요한 문자열 담기
    const keyword = document.querySelector('#keyword').value
    console.log(gubun + ", " + keyword)
    const asyncDB = async () => {
       // 키와 값이 같을경우 생략가능 gubun:gubun -> gubun
      const res = await deptListDB({gubun, keyword, deptno: 0})
      console.log(res.data)
      if(res.data) {
        setDeptList(res.data)
      }
    }
    asyncDB()
  }

  // 부서목록 JSON포맷 가져오기
  const jsonDeptList = async() => {
    const res = await deptListDB({deptno: 0})
    console.log(res.data)
    if(res.data) {
      setDeptList(res.data)
    } else {
      console.log('부서목록 조회 실패')
    }
  }

  // 이미지 파일 첨부 구현
  const imgChange = async(event) => {
    const uploaded = await imageUploader.upload(event.target.files(0))
    console.log(uploaded)
    setFiles({
      filename: uploaded.public_id + "." + uploaded.format,
      fileurl: uploaded.url
    })
    // input의 이미지 객체 얻어오기
    const upload = document.querySelector("#dimg")
    // 이미지를 집어넣을 곳의 부모태그
    const holder = document.querySelector("#uploadImg")
    const file = upload.files(0)
    const reader = new FileReader()
    reader.onload = (event) => {
      const img = new Image()
      img.src = event.target.result
      if(img.width > 150) {
        img.width = 150
      }
      holder.innerHTML = "";
      holder.appendChild(img)
    }
    reader.readAsDataURL(file)
    return false
  }

  // 부서 등록 구현
  // 스프링 부트와 리액트 연동하기 - @RequestBody사용해서 JSON포맷으로 넘김
  const deptInsert = async() => {
    const dept = {
      deptno,
      dname,
      loc,
      filename: files.filename,
      fileurlL: files.fileurl
    }
    const res = await deptInsertDB(dept)
    console.log(res + ", " + res.data)
    if(!
res.data) { console.log('부서등록에 실패하였습니다.

') } else { console.log('부서등록 성공!
') // 성공시 부서목록 새로고침 처리 - window.location.reload()쓰지 말것!
- SPA컨벤션 // useEffect - 의존성 배열 연습할 수 있음 handleClose() // 모달창을 닫기 // 부서목록 새로고침 처리 navigate("/dept/1") } } useEffect (() => { jsonDeptList() }, (gubun)) // 의존성배열이 빈 배열이면 최초 한번만 // 의존성 배열에 올 수 있는 변수는 전역변수!
return ( <React.Fragment> <BlogHeader /> <div className="container"> <div className="page-header"> <h2>부서관리&nbsp;<i className="fa-solid fa-angles-right"></i>&nbsp;<small>부서목록</small></h2> <hr /> </div> <div className="row"> <div className="col-3"> <select id="gubun" className="form-select" aria-label="분류선택"> <option defaultValue>분류선택</option> <option value="deptno">부서번호</option> <option value="dname">부서명</option> <option value="loc">지역</option> </select> </div> <div className="col-6"> <input type="text" id="keyword" className="form-control" placeholder="검색어를 입력하세요" aria-label="검색어를 입력하세요" aria-describedby="btn_search" /> </div> <div className="col-3"> <Button variant="danger" id="btn_search" onClick={reactSearch}>검색</Button> </div> </div> <div className="book-list"> <Table striped bordered hover> <thead> <tr> <th>#</th> <th>부서번호</th> <th>부서명</th> <th>지역</th> </tr> </thead> <tbody> {deptList.map(dept => ( <DeptRow key={dept.DEPTNO} dept={dept} /> ))} </tbody> </Table> <hr /> <div className="booklist-footer"> <Button variant="warning" onClick={jsonDeptList}> 전체조회 </Button>&nbsp; <Button variant="success" onClick={handleShow}> 부서등록 </Button> </div> </div> </div> {/* ========================== (( 도서등록 Modal )) ========================== */} <Modal show={show} onHide={handleClose} animation={false}> <Modal.Header closeButton> <Modal.Title>부서등록</Modal.Title> </Modal.Header> <Modal.Body> <div style={{display: 'flex', flexWrap: 'wrap', justifyContent: 'center'}}> <div style={{display: 'flex'}}> <MyLabel>부서번호<span style={{color: 'red'}}>{star.deptno}</span> <MyInput type="text" id="deptno" placeholder="Enter 부서번호" onChange={(e) => {handleDeptno(e.target.value)}} /> <MyLabelAb>{comment.deptno}</MyLabelAb> </MyLabel> </div> <div style={{display: 'flex'}}> <MyLabel>부서명<span style={{color: 'red'}}>{star.dname}</span> <MyInput type="text" id="dname" placeholder="Enter 부서명" onChange={(e) => {handleDname(e.target.value); validate('dname', e);}} /> <MyLabelAb>{comment.dname}</MyLabelAb> </MyLabel> </div> <div style={{display: 'flex'}}> <MyLabel>지역<span style={{color: 'red'}}>{star.loc}</span> <MyInput type="text" id="loc" placeholder="Enter 지역" onChange={(e) => {handleLoc(e.target.value)}} /> <MyLabelAb>{comment.loc}</MyLabelAb> </MyLabel> </div> <Form.Group className="mb-3" controlId="formBasicOffice"> <Form.Label>건물이미지</Form.Label> <input className="form-control" type="file" accept="image/*" id="dimg" name="dimg" onChange={imgChange}/> </Form.Group> <div id="uploadImg"> <img className="thumbNail" src="http://via.placeholder.com/200X250" alt="미리보기" /> </div> </div> </Modal.Body> <Modal.Footer> <Button variant="secondary" onClick={handleClose}> 닫기 </Button> <Button variant="primary" onClick={deptInsert}> 저장 </Button> </Modal.Footer> </Modal> {/* ========================== (( 부서등록 Modal )) ========================== */} </React.Fragment> ) } export default DeptPage

import React from 'react'
import { Link } from 'react-router-dom'

const DeptRow = ({dept}) => {
  return (
    <>
      <tr>
        <td>{dept.DEPTNO}</td>
        <td>
          <Link to={"/deptdetail" + dept.DEPTNO} className="btn btn-primary">{dept.DEPTNO}</Link>
        </td>
        <td>{dept.DNAME}</td>
        <td>{dept.LOC}</td>
      </tr>
    </>
  )
}

export default DeptRow

import axios from "axios";

export const deptInsertDB = (dept) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post", // @RequestBody
        url: process.env.REACT_APP_SPRING_IP + "dept/deptInsert",
        data: dept, // post방식 data
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const deptListDB = (dept) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "dept/deptList",
        params: dept, // 쿼리스트링은 header에 담김 - get방식 params
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const jsonMemberListDB = (member) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "member/jsonMemberList",
        params: member,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const validateDname = (e) => {
  // 사용자가 입력한 부서명
  const name = e.target.value // input onChange
  const kor = /^(ㄱ-ㅎ가-힣)+$/;
  const eng = /^(a-zA-Z)+$/;
  if(name.length === 0) {
    return " ";
  } else if (kor.test(name) || eng.test(name)) { // test의 리턴타입은 boolean
    return "";
  } else {
    return "부서명은 영어 또는 한글만 가능합니다.

"; } }

package com.example.demo.vo;

import lombok.Builder;
import lombok.Data;

@Data // getter와 setter 역할 대신하는 어노테이션
@Builder // 생성자 생성 역할 어노테이션
public class DeptVO {
	private int deptno;
	private String dname;
	private String loc;
	private String filename;
	private String fileurl;
}

package com.example.demo.controller;

import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.logic.DeptLogic;
import com.example.demo.vo.DeptVO;
import com.google.gson.Gson;

@RestController
@RequestMapping("/dept/*")
public class RestDeptController {
	Logger logger = LoggerFactory.getLogger(RestDeptController.class);
	
	@Autowired
	private DeptLogic deptLogic = null;
	
	/**************************************************
	 * 부서 조회
	 * @param pMap
	 * @return
	 **************************************************/
	@GetMapping("deptList")
	public String deptList(@RequestParam Map<String, Object> pMap) {
		List<Map<String, Object>> dList = null;
		dList = deptLogic.deptList(pMap);
		Gson g = new Gson();
		String temp = g.toJson(dList);
		return temp;
	}
	
	/**************************************************
	 * 부서등록 구현
	 * @param pdVO
	 * @return insert문 성공 여부 : 1이면 성공, 0이면 실패
	 **************************************************/
	// 단위 테스트 URL은
	// <http://localhost:8000/dept/deptInsert이나> 테스트는 불가함(포스트맨 사용)
	@PostMapping("deptInsert")
	public String deptInsert(@RequestBody DeptVO pdVO) {
		logger.info(pdVO.toString());
		int result = 0;
		result = deptLogic.deptInsert(pdVO);
		return String.valueOf(result);
	}

	@PutMapping("deptUpdate")
	public String deptUpdate(@RequestBody DeptVO pdVO) {
		logger.info(pdVO.toString());
		int result = 0;
		result = deptLogic.deptUpdate(pdVO);
		return String.valueOf(result);
	}
	
	@DeleteMapping("deptDelete")
	public String deptDelete(int deptno) {
		// 연습용으로 파라미터 원시형타입 deptno
		logger.info("사용자가 선택한 부서번호(단, 자손이 없어야 함)=> " + deptno);
		int result = 0;
		result = deptLogic.deptDelete(deptno);
		return String.valueOf(result);
	}
}

/*
@Controller는 리턴값이 화면 출력으로 사용됨
@RestController는 리턴값에 대한 마임타입이 text/plain으로 사용됨

리액트 연동시 조회된 결과나 백엔드에서 전달되는 값은 text이거나 json포맷만 가능하므로
@RestController를 사용함

@RequestBody는 post방식으로 사용자가 입력한 값이 보안상 중요한 정보인 경우 사용가능
패킷 헤더가 아닌 바디에 저장되므로 노출 위험이 없음

@RequestParam는 get방식
*/

package com.example.demo.logic;

import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.dao.DeptDao;
import com.example.demo.vo.DeptVO;

@Service
public class DeptLogic {
	Logger logger = LoggerFactory.getLogger(DeptLogic.class);
	
	@Autowired
	private DeptDao deptDao = null;
	
	public List<Map<String, Object>> deptList(Map<String, Object> pMap) {
		List<Map<String, Object>> dList = null;
		dList = deptDao.deptList(pMap);
		return dList;
	}
	
	public int deptInsert(DeptVO pVO) {
		int result = 0;
		result = deptDao.deptInsert(pVO);
		return result;
	}

	public int deptUpdate(DeptVO pVO) {
		int result = 0;
		result = deptDao.deptUpdate(pVO);
		return result;
	}

	public int deptDelete(int deptno) {
		int result = 0;
		result = deptDao.deptDelete(deptno);
		return result;
	}
}

package com.example.demo.dao;

import java.util.List;
import java.util.Map;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.example.demo.vo.DeptVO;

@Repository
public class DeptDao {
	Logger logger = LoggerFactory.getLogger(DeptDao.class);
	
	// DatabaseConfigutation에서 @Configuration으로 빈둥록된 객체 주입받기 코드임
	// application.properties에서 물리적으로 떨어져있는 오라클 서버 정보 받음
	// DML문을 가진 xml문은 src/main/resources아래 있음
	@Autowired
	private SqlSessionTemplate sqlSessionTemplate = null;
	
	public List<Map<String, Object>> deptList(Map<String, Object> pMap) {
		List<Map<String, Object>> dList = null;
		dList = sqlSessionTemplate.selectList("deptList", pMap);
		return dList;
	}
	
	public int deptInsert(DeptVO pVO) {
		int result = 0;
		result = sqlSessionTemplate.update("deptInsert", pVO);		
		return result;
	}

	public int deptUpdate(DeptVO pVO) {
		int result = 0;
		result = sqlSessionTemplate.update("deptUpdate", pVO);
		logger.info("" + result);
		return result;
	}

	public int deptDelete(int deptno) {
		int result = 0;
		result = sqlSessionTemplate.delete("deptDelete", deptno);
		logger.info("" + result);
		return result;
	}
}

<?xml version="1.0" encoding="UTF-8"?>
<!
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo"> <!
-- 부서조회 --> <select id="deptList" parameterType="map" resultType="map"> SELECT deptno, dname, loc, filename, fileurl from dept <where> <if test="deptno!
=null and deptno > 0"> AND deptno = #{deptno} </if> <if test="gubun!
=null and gubun.equals("deptno")"> AND deptno LIKE '%'||#{keyword}||'%' </if> <if test="gubun!
=null and gubun.equals("dname")"> AND dname LIKE '%'||#{keyword}||'%' </if> <if test="gubun!
=null and gubun.equals("loc")"> AND loc LIKE '%'||#{keyword}||'%' </if> </where> order by deptno desc </select> <!
-- 부서입력 --> <insert id="deptInsert" parameterType="com.example.demo.vo.DeptVO"> INSERT INTO dept(deptno, dname, loc <if test="filename !
= null"> ,filename </if> <if test="fileurl !
= null"> ,fileurl </if> ) VALUES (#{deptno}, #{dname}, #{loc} <if test="filename !
= null"> ,#{filename} </if> <if test="fileurl !
= null"> ,#{fileurl} </if> ) </insert> <!
-- 부서 정보 수정 --> <update id="deptUpdate" parameterType="map"> UPDATE dept SET dname = #{dname} ,loc = #{loc} <where> <if test="deptno!
=null and deptno > 0"> AND deptno = #{deptno} </if> </where> </update> <!
-- 부서 정보 삭제 --> <delete id="deptDelete" parameterType="int"> DELETE from dept <where> <if test="value!
=null and value > 0"> AND deptno = #{value} </if> </where> </delete> </mapper>