본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day80

by 루팽 2023. 3. 23.

<부서관리 게시판 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "@fortawesome/fontawesome-free/js/all.js";
import ImageUploader from "./service/imageUploader";
// 이미지 업로더 객체 생성
const imageUploader = new ImageUploader();
const root = ReactDOM.createRoot(document.getElementById("root"));
// 리덕스 추가 - store 생성
// createStore 호출
root.render(
  <>
    <BrowserRouter>
      <App imageUploader={imageUploader} />
    </BrowserRouter>
  </>
);

 

<부서관리 게시판 - App.jsx>

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';
import DeptDetail from './components/dept/DeptDetail';

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} />} />
        {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
        <Route path='/deptdetail/:deptno' element={<DeptDetail 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;

 

<부서관리 게시판 - DeptPage.jsx>

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'
import styled from 'styled-components'
import BlogFooter from '../include/BlogFooter'

const DivUploadImg = styled.div`
  display: flex;
  width: 200px;
  height: 250px;
  align-items: center;
  overflow: hidden;
  margin: 10px auto;
`;

const Img = styled.img`
  width: 100%;
  height: 100%;
  object-fit: cover;
`;

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,
      fileurl: 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>
            <DivUploadImg id="uploadImg">
              <Img src="http://via.placeholder.com/200X250" alt="미리보기" />
            </DivUploadImg>
          </div>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              닫기
            </Button>
            <Button variant="primary" onClick={deptInsert}>
              저장
            </Button>
          </Modal.Footer>
        </Modal>     
      {/* ========================== [[ 부서등록 Modal ]] ========================== */}
      <BlogFooter />
    </React.Fragment>
  )
}

export default DeptPage

 

<부서관리 게시판 - DeptDetail.jsx>

import React, { useCallback, useEffect, useState } from 'react'
import { Button, Card, Form, Modal } from 'react-bootstrap'
import BlogFooter from '../include/BlogFooter'
import BlogHeader from '../include/BlogHeader'
import styled from 'styled-components'
import { useNavigate, useParams } from 'react-router-dom'
import { deptUpdateDB, deptListDB, deptDeleteDB } from '../../service/dbLogic'
import { MyInput, MyLabel, MyLabelAb } from '../styles/FormStyle'
import { validateDname } from '../../service/validateLogic'

const DivUploadImg = styled.div`
display: flex;
width: 200px;
height: 250px;
align-items: center;
overflow: hidden;
margin: 10px auto;
`;

const Img = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
`;

const DivDeptBody = styled.div`
  display: flex;
  flex-direction: column;
  margin: 0px 20px;
`;

const DeptDetail = ({ imageUploader }) => {
  const navigate = useNavigate()
  // 부서번호를 클릭했을 때 해시값으로 전달된 부서번호를 담기
  // 사용자가 부서번호를 선택할때마다 변경됨 - useEffect에서 의존배열인자로 사용
  const {deptno} = useParams() // App.jsx의 Route path에서 해시값으로 넘어옴 - 바뀐다
  // 수정화면 모달 마운트(화면에 나타남) 여부 결정 - false 안보임, true 보임 
  const [show, setShow] = useState(false)
  const handleClose = () => setShow(false)
  const handleShow = () => setShow(true)
  const [dname, setDname] = useState('')
  const [loc, setLoc] = useState('')
  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]: ''})
    }
  }
  // 오라클 서버에서 파라미터로 넘어온 부서번호를 가지고 한 건을 조회한 후 담기
  const [dept, setDept] = useState({
    DEPTNO: 0,
    DNAME: '',
    LOC: '',
    FILENAME: '',
    FILEURL: '',
  })
  
  useEffect(() => {
    // 파라미터로 넘어오는 deptno가 바뀌면 다시 실행됨
    const asyncDB = async() => {
      const res = await deptListDB({deptno: deptno})
      console.log(res.data)
      const result = JSON.stringify(res.data)
      const jsonDoc = JSON.parse(result)
      setDept({DEPTNO: jsonDoc[0].DEPTNO, DNAME: jsonDoc[0].DNAME, LOC: jsonDoc[0].LOC, FILENAME: jsonDoc[0].FILENAME, FILEURL: jsonDoc[0].FILEURL})
      console.log(jsonDoc[0].FILEURL)
    }
    asyncDB()
    return () => {
      // 언마운트될 때 처리할 일이 있으면 여기에 코딩할 것
    }
  }, [deptno]) // deptno가 변결될때마다 함수가 실행됨
  if(!dept.FILEURL){
    dept.FILEURL = "http://via.placeholder.com/200X250"
  }
  
  // 삭제
  const deptDelete = () => {
    console.log('삭제')
    const asyncDB = async() =>{
      const res = deptDeleteDB({deptno: deptno})
      console.log(res.data)
      navigate("/dept/0")
    }
    asyncDB()
  };

  // 부서 목록 페이지 이동하기
  const deptList = () => {
    navigate("/dept/0")
  }

  /*
    리액트에서는 메모이제이션 컨벤션이 있음
    useMemo와 useCallback - 첫번째 파라미터에는 함수, 두번째 파라미터에는 의존성 배열
    차이점: useMemo는 값을 반환, useCallback은 함수를 반환

    리렌더링은 언제 일어나는가?
      1. state가 변경됨   2. props가 변경됨   3. 부모 컴포넌트가 변경됨
  */
 // dname 값 가져와서 저장
  const handleDname = useCallback((value) => {
    console.log(value)
    setDname(value)
  }, [])
  
  /*
    아래와 같이 함수를 선언하면 DeptDetail컴포넌트가 마운트될 때마다 주소번지가 바뀐다(비효율적)
    const handleLoc = (value) => {
      setLoc(value)
    }
    함수의 구현내용 변화가 없는 경우라면, 한 번 생성된 주소번지를 계속 가지고있어도 되지 않나?
    -> 그러면 이걸 기억해줘(cache에) - 필요할 때 새로 생성하지 말고 캐시에있는 함수를 부름
    -> 이렇게 처리할 때 useCallback 사용
  */
  // loc 값 가져와서 저장
  const handleLoc = useCallback((value) => {
    console.log(value)
    setLoc(value)
  }, [])

  // 이미지 파일 첨부 구현
  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 deptUpdate = async() => {
    const dept = {
      deptno,
      dname,
      loc,
      filename: files.filename,
      fileurl: files.fileurl
    }
    const res = await deptUpdateDB(dept)
    console.log(res + ", " + res.data)
    if(!res.data) {
      console.log('부서수정에 실패하였습니다.')
    } else {
      console.log('부서수정 성공!')
      // 모달창을 닫기
      handleClose()
      // 부서목록 새로고침 처리
      navigate("/dept/1")
    }
  }

  return (
    <>
      <BlogHeader />
      <div className='container'>
        <div className="page-header">
          <h2>부서관리&nbsp;<i className="fa-solid fa-angles-right"></i>&nbsp;<small>상세보기</small></h2>
          <hr />
        </div> 
        <Card style={{width: '58rem'}}>
          <Card.Body>
            <Card.Img style={{width:'250px'}} src={`${dept.FILEURL}`} alt="Card image" />
            <DivDeptBody>
              <Card.Title>{dept.DNAME}</Card.Title>
              <Card.Text>{dept.LOC}</Card.Text>
              <Card.Text>{dept.DEPTNO}</Card.Text>
            </DivDeptBody>
          </Card.Body>
          <div>
            <Button onClick={handleShow}>수정</Button>
            &nbsp;
            <Button onClick={deptDelete}>삭제</Button>
            &nbsp;
            <Button onClick={deptList}>부서목록</Button>
          </div>
        </Card>
      </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 부서번호" value={deptno} />
                <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>
            <DivUploadImg id="uploadImg">
              <Img src="http://via.placeholder.com/200X250" alt="미리보기" />
            </DivUploadImg>
          </div>
          </Modal.Body>
          <Modal.Footer>
            <Button variant="secondary" onClick={handleClose}>
              닫기
            </Button>
            <Button variant="primary" onClick={deptUpdate}>
              저장
            </Button>
          </Modal.Footer>
        </Modal>     
      {/* ========================== [[ 부서정보 수정 Modal ]] ========================== */}
      <BlogFooter />
    </>
  )
}

export default DeptDetail

 

<부서관리 게시판 - dbLogic.js>

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 deptUpdateDB = (dept) => {
  return new Promise((resolve, reject) => {
    console.log(dept)
    try {
      const response = axios({
        method: "post", // @RequestBody
        url: process.env.REACT_APP_SPRING_IP + "dept/deptUpdate",
        data: dept, // post방식 data
      });
      resolve(response); // 요청 처리 성공했을 때
    } catch (error) {
      reject(error); // 요청 처리 실패했을 때
    }
  });
};

export const deptDeleteDB = (dept) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "dept/deptDelete",
        params: dept,
      });
      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);
    }
  });
};

 

<부서관리 게시판 - RestDeptController.java>

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.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);
	}

	@PostMapping("deptUpdate")
	public String deptUpdate(@RequestBody DeptVO pdVO) {
		logger.info(pdVO.toString());
		int result = 0;
		result = deptLogic.deptUpdate(pdVO);
		return String.valueOf(result);
	}
	
	@GetMapping("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방식
*/

 

<부서관리 게시판 - dept.xml>

<?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}
		 		<if test="filename != null">
					,filename = #{filename}
				</if>
				<if test="fileurl != null">
					,fileurl = #{fileurl}
				</if>
	 	<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>

 

<Flux 연습 - index.html>

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Flux Architecture</title>
  <script src="app.js" type="module"></script>
</head>
<body>
  Flux
</body>
</html>

 

<Flux 연습 - app.js>

import { createStore } from "./redux.js"
import { reducer } from "./reducer.js" // worker함수
import { decrease, increase } from "./actions.js"

// 사용 - 함수 호출 -> store 생성하기 - 리액트에서는 index.js에서 할 일!
// -> index.js에서 모든 전역 state를 관리하기위해
// app.js에 있는 코드가 리액트 컴포넌트에 써야하는 코드임(store부분은 index.js)
// 문제제기 - app.js 하나에 모두 있을때는 파라미터에 reducer(구:worker)를 파라미터로 넘겨야 함
const store = createStore(reducer) // index.js

store.subscribe(function () { // 구독발행모델 - 함수호출
  // getState는 리액트에서 useSelector(state => state.userAUth) - 상태값을 store에서 읽어올 때 사용
  console.log(store.getState()) // 변경된 상태값 찍기 - 리액트 컴포넌트가 마운트될 때 찍기
})

 // dispatch가 리액트에선 const dispatch = useDispatch() -> dispatch(type, payload)
 // -> 액션을 스토어에 전달
store.dispatch(increase())
store.dispatch(increase())
store.dispatch(decrease())

 

<Flux 연습 - redux.js>

// 커링함수
// 파라미터 2개 -> 액션의 타입과 값
export const actionCreator = (type) => (payload) => ({
  type,
  payload,
})

export const createStore = (reducer) => {
  let state;
  let handlers = []

  const dispatch = (action) => {
    console.log('dispatch 호출')
    state = reducer(state, action)
    handlers.forEach(handler => handler())
  }

  const subscribe = ((handler) => {
    handlers.push(handler)
  })

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

  return {
    dispatch,
    getState,
    subscribe
  }
}

 

<Flux 연습 - reducer.js>

import * as ActionType from './action-type.js'

// store에서 관리해야하는 상태값의 종류가 점점 늘어남 - 객체 리터럴 - 열거형 연산자 - n개 관리, 초기화 처리
const initializeState = { count: 0 } // 처음엔 let state로 처리함

export const reducer = (state = initializeState, action) => { // 처음예제에선 worker
  switch(action.type){
    case ActionType.INCREASE:
      return {...state, count: state.count + 2}
      case ActionType.DECREASE:
      return {...state, count: state.count - 1}
      case ActionType.RESET:
      return { ...state, count: 0 };
    default:
      return {...state}
  }
}

 

<Flux 연습 - actions.js>

import { DECREASE, INCREASE, RESET } from "./action-type.js";
import { actionCreator } from "./redux.js";

// store.dispatch(increase()) - dispatch는 action을 store에 전달하는 허브
// store에 들어있는 상태값을 꺼내는 것이 getState - 리액트 useSelector

export const increase = actionCreator(INCREASE)
export const decrease = actionCreator(DECREASE)
export const reset = actionCreator(RESET)

 

<Flux 연습 - action-type.js>

export const INCREASE = 'increase'
export const DECREASE = 'decrease'
export const RESET = 'reset'

 

커링(currying)

  하나 이상의 파라미터를 갖는 함수를 부분적으로 나누어 각각 단일 파라미터를 갖는 함수로 설정하는 기법

  함수의 재사용성을 높이기 위해 함수 자체를 리턴하는 함수

  함수를 하나만 사용할 때는 필요한 파라미터를 한 번에 모두 넣어야 하지만, 커링을 사용하면 함수를 분리할 수 있으므로 파라미터도 나눠서 전달할 수 있음

// uncurring
function plusFunc(a, b, c){
  console.log(a + b + c);
}

plusFunc(1, 2, 3); // 6

// curring
function plusFunc(a){
    return function(b){
       return function(c){
          console.log(a + b + c);
       }
    }
}

plusFunc(1)(2)(4); // 7

 

<커링함수 - currying.js>

const repeatWidth = (character, count) => character.repeat(count)
console.log(repeatWidth('*', 5))

const repeatWidth2 = (character) => (count) => character.repeat(count)

const repeatStar = repeatWidth2('#')
console.log(repeatStar(3))

 

<댓글형 게시판 - App.jsx>

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';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';

function App({imageUploader}) {
  return (
    <>
      <Routes>
        <Route path='/' exact={true} element={<LoginPage />} />
        <Route path='/home' exact={true} element={<HomePage />} />
        <Route path='/repleboard' element={<RepleBoardPage />} />
        <Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
        {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
        <Route path='/deptdetail/:deptno' element={<DeptDetail 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;

 

<댓글형 게시판 - RepleBoardPage.jsx>

import React from 'react'
import { Button, Table } from 'react-bootstrap'
import BlogFooter from '../include/BlogFooter'
import BlogHeader from '../include/BlogHeader'

const RepleBoardPage = () => {
  const boardSearch = () => {

  }
  
  return (
    <>
      <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="b_no">글번호</option>
              <option value="b_title">글제목</option>
              <option value="b_content">내용</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={boardSearch}>검색</Button>
          </div>
        </div> 
        <div className='book-list'>
          <Table striped bordered hover>
            <thead>
              <tr>
                <th>번호</th>
                <th>제목</th>
                <th>작성자</th>
                <th>작성일</th>
                <th>조회수</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>1</td>
                <td>댓글형 게시판 구현</td>
                <td>관리자</td>
                <td>2023-03-23</td>
                <td>17</td>
              </tr>
            </tbody>
          </Table> 
          <hr />    
          <div className='booklist-footer'>
            <Button variant="warning" >
              전체조회
            </Button>&nbsp; 
            <Button variant="success">
              글쓰기
            </Button> 
          </div>
        </div>
      </div>
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      <BlogFooter />
    </>
  )
}

export default RepleBoardPage

 

<댓글형 게시판 - BlogHeader.jsx>

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="/repleboard" className='nav-link'>게시판</Link>
          </Nav>
        </Container>
      </Navbar>
    </>
  )
}

export default BlogHeader

댓글