<부서관리 게시판 - 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>부서관리 <i className="fa-solid fa-angles-right"></i> <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>
<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>부서관리 <i className="fa-solid fa-angles-right"></i> <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>
<Button onClick={deptDelete}>삭제</Button>
<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>댓글형 게시판 <i className="fa-solid fa-angles-right"></i> <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>
<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
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day82 (1) | 2023.03.27 |
---|---|
국비 지원 개발자 과정_Day81 (0) | 2023.03.25 |
국비 지원 개발자 과정_Day79 (0) | 2023.03.22 |
국비 지원 개발자 과정_Day78 (0) | 2023.03.21 |
국비 지원 개발자 과정_Day77 (0) | 2023.03.20 |
댓글