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

국비 지원 개발자 과정_Day86

by 루팽 2023. 3. 31.

RepleBoardPage.jsx

  글쓰기버튼 → 라우트 /reple/boardwrite → RepleBoardWriteForm에 연결됨

 

RepleBoardWriteForm - files[]

  QuillEditor → 이벤트 여기서 일어나는데 WriteForm에 있는 files[]에 담음(버블링) - 핵심 키워드 props(얕은 복사, 원본이 넘어오는 것이기에 거기에 담아준다)

  RepleBoardFileInsert - props 가져야 함 - files[] → map으로 꺼내서 처리해 줌

 

버블링

자식으로부터 부모로 올라가는 것

한 요소에 이벤트가 발생하면 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작하고 최상단의 부모 요소를 만날 때까지 반복되면서 핸들러가 동작하는 현상

 

캡쳐링

부모로부터 자식으로 내려오는 것

버블링과 반대 방향으로 진행되는 이벤트 전파 방식

 

<Quill 이미지, 파일 첨부 - App.jsx>

import { Route, Routes, useNavigate } 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';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';
import KhLoginPage from './components/auth/KhLoginPage';
import { onAuthChange } from './service/authLogic';
import { memberListDB } from './service/dbLogic';
import EmailVerifiedPage from './components/auth/EmailVerifiedPage';
import FindEmailPage from './components/auth/FindEmailPage';
import ResetPwdPage from './components/auth/ResetPwdPage';
import RepleBoardWriteForm from './components/repleboard/RepleBoardWriteForm';
import RepleBoardDetail from './components/repleboard/RepleBoardDetail';

function App({authLogic, imageUploader}) {
  const navigate = useNavigate()
  const dispatch = useDispatch()
  const ssg = sessionStorage;
  const toastStatus = useSelector(state => state.toastStatus)
  useEffect(() => {
    const asyncDB = async() => {
      const auth = authLogic.getUserAuth()
      // 현재 인증된 사용자 정보를 가져온다
      const user = await onAuthChange(auth)
      // 사용자가 있을 경우(userId가 존재) - 구글 로그인으로 사용자 정보를 가지고 있을 때
      // user정보가 있으면 sessionStorage에 담는다 - email
      if(user) {
        console.log('user정보가 있을 때')
        ssg.setItem('email', user.email)
        const res = await memberListDB({mem_uid: user.uid, type: 'auth'})
        console.log(res.data)
        //오라클 서버의 회원집합에 uid가 존재할 경우 - 세션스토리지에 값을 담는다
        if(res.data !== 0) { // 스프링 부트 - RestMemberController - memberList() -> 0 혹은 [{}]
          const temp = JSON.stringify(res.data)
          const jsonDoc = JSON.parse(temp)
          ssg.setItem('nickname', jsonDoc[0].MEM_NICKNAME)
          ssg.setItem('status', jsonDoc[0].MEM_STATUS)
          ssg.setItem('auth', jsonDoc[0].MEM_AUTH)
          ssg.setItem('no', jsonDoc[0].MEM_NO)
          //navigate("/")
          return // 렌더링이 종료됨
        }
        // 구글 계정이 아닌 다른 계정으로 로그인 시도를 했을 땐 user.emailVerified가 없다
        // -> undefined
        if(!user.emailVerified) {
          navigate('./auth/emailVerified')
        }
        //오라클 서버의 회원집합에 uid가 존재하지 않을 경우 
        else {
          console.log('가입되지 않은 구글 계정입니다.')
          //navigate('/auth/signup')
        }
      }
      // 사용자 정보가 없을 경우
      else {
        console.log('user정보가 없을 때')
        if(ssg.getItem('email')){
          // 세션 스토리지에 있는 값 모두 삭제하기
          ssg.clear()
          window.location.reload()
        } // end of inner if
      } // end of else
    }
    asyncDB()
  }, [dispatch])

  return (
    <>
      <div style={{height: '100vh'}}>
        {toastStatus.status && <Toast />}
        <Routes>
          <Route path='/login' exact={true} element={<KhLoginPage authLogic={authLogic} />} />
          <Route path='/' exact={true} element={<HomePage />} />
          <Route path='/home' exact={true} element={<HomePage />} />
          <Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
          <Route path='/auth/emailVerified' exact={true} element={<EmailVerifiedPage authLogic={authLogic} />} />
          <Route path='/auth/findEmail' exact={true} element={<FindEmailPage />} />
          <Route path='/auth/resetPwd' exact={true} element={<ResetPwdPage authLogic={authLogic} />} />
          <Route path='/reple/board' exact={true} element={<RepleBoardPage />} />
          <Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
          <Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
          <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>
      </div>
    </>
  );
}

export default App;

 

<Quill 이미지, 파일 첨부 - RepleBoardPage.jsx>

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

const RepleBoardPage = () => {
  const navigate = useNavigate()

  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" onClick={() => navigate('/reple/boardwrite')}>
              글쓰기
            </Button> 
          </div>
        </div>
      </div>
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      <BlogFooter />
    </>
  )
}

export default RepleBoardPage

 

<Quill 이미지, 파일 첨부 - RepleBoardWriteForm.jsx>

import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { boardInsertDB, uploadFileDB, uploadImageDB } from '../../service/dbLogic'
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle'
import QuillEditor from './QuillEditor'
import RepleBoardFileInsert from './RepleBoardFileInsert'

const RepleBoardWriteForm = () => {
  const navigate = useNavigate()
  const[title, setTitle] = useState("")
  const[writer, setWriter] = useState("")
  const[pw, setPw] = useState("")
  const[content, setcontent] = useState("")
  const[bsfile, setBsFile] = useState("")
  const[bssize, setBsSize] = useState("")
  const[fileName, setFileName] = useState("")
  // QuillEditor이미지 선택하면 imageUploadDB타고 스프링 플젝 pds에 해당 이미지 업로드
  // pds에 업로드된 파일을 읽어서 Editor안에 보여줌 imageGet?imageName=XXX.png
  const[files, setFiles] = useState([])

  const quillRef = useRef()
  const fileRef = useRef()
  
  const handleTitle = useCallback((e) => {
    setTitle(e)
  }, [])
  const handleWriter = useCallback((e) => {
    setWriter(e)
  }, [])
  const handlePW = useCallback((e) => {
    setPw(e)
  }, [])
  const handleContent = useCallback((e) => {
    setcontent(e)
  }, [])

  const boardInsert = async() => {
    const board = {
      bm_title: title,
      bm_content: content,
      bm_writer: writer,
      bm_pw: pw,
      bs_file: bsfile,
      bs_size: bssize,
    }
    const res = await boardInsertDB(board)
    console.log(res)
    navigate("/board")
  }

  const handleClick = (event) => {
    event.preventDefault()
    document.querySelector("#file-input").click((event)=>{
      console.log(event.currentTarget.value);
    })
  }

  const handleChange = async (event) => {
    console.log('첨부파일 선택'+event.target.value);
    //console.log(fileRef.current.value);
    //fileRef에서 가져온 값중 파일명만 담기
    const str = fileRef.current.value.split('/').pop().split('\\\\').pop()
    setFileName(str)
    console.log(str);
    //선택한 파일을 url로 바꾸기 위해 서버로 전달할 폼데이터 만들기
    const formData = new FormData()
    const file = document.querySelector("#file-input").files[0]
    formData.append("file_name", file)// 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함!
    const res = await uploadFileDB(formData)
    console.log(res.data)
    const fileinfo = res.data.split(',')
    console.log(fileinfo)
    setBsFile(fileinfo[0])
    setBsSize(fileinfo[1])
  }

  const handleFiles = () => {
  }

  return (
    <>
    <ContainerDiv>
    <HeaderDiv>
      <h3 style={{marginLeft:"10px"}}>공지사항 글작성</h3>
    </HeaderDiv>
    <FormDiv>
      <div style={{width:"100%", maxWidth:"2000px"}}>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>제목</h3> 
          <BButton onClick={()=>{boardInsert()}}>글쓰기</BButton>
        </div>
        <input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
          style={{width:"100%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>작성자</h3> 
        </div>              
        <input id="dataset-writer" type="text" maxLength="50" placeholder="작성자를 입력하세요."
          style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleWriter(e.target.value)}}/>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>비밀번호</h3> 
        </div>              
        <input id="dataset-pw" type="text" maxLength="50" placeholder="비밀번호를 입력하세요."
          style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handlePW(e.target.value)}}/>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>첨부파일</h3> 
        </div>                      
        <input id="file-input" name='file_name' ref={fileRef} type="file" maxLength="50" className="visuallyhidden" onChange={handleChange}/>            
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
        </div>
        <h3>상세내용</h3>
        <hr style={{margin:'10px 0px 10px 0px'}}/>
        <QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
        <RepleBoardFileInsert files={files}/>
      </div>
    </FormDiv>
  </ContainerDiv>
</>
  )
}

export default RepleBoardWriteForm

 

<Quill 이미지, 파일 첨부 - RepleBoardFileInsert.jsx>

import React from 'react';

const RepleBoardFileInsert = ({files}) => {
  console.log(files);
  return (
    <div style={{display:'block', border:'1px solid lightGray', borderRadius:'10px', minHeight:'60px', padding:'5px'}}>
      <div style={{textAlign:"left", padding: "2px 5px 2px 5px"}}>첨부사진</div>
      {
        files.map((item, index)=>(
          <div type='text' id='fileUpload' style={{padding:"5px"}} key={index}>{item}</div>
        ))
      }
    </div>
  );
};

export default RepleBoardFileInsert;

 

<Quill 이미지, 파일 첨부 - QuillEditor.jsx>

import React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import ReactQuill, { Quill } from 'react-quill'; 
import { uploadImageDB } from '../../service/dbLogic';

const QuillEditor = ({ value, handleContent, quillRef, files, handleFiles}) => {
    console.log(files);
    console.log(Array.isArray(files)); // array

    //const dispatch = useDispatch();
    const imageHandler = useCallback(() => {
        console.log(files);
        if(files.length > 2){
            return "이미지는 3장까지 업로드 가능합니다.";
        }
        const formData = new FormData(); // 이미지를 url로 바꾸기위해 서버로 전달할 폼데이터 만들기
        
        const input = document.createElement("input"); // input 태그를 동적으로 생성하기
        input.setAttribute("type", "file");
        input.setAttribute("accept", "image/*"); // 이미지 파일만 선택가능하도록 제한
        input.setAttribute("name", "image");
        input.click(); 
        
        // 파일 선택창에서 이미지를 선택하면 실행될 콜백 함수 등록
        input.onchange = async () => {
            const file = input.files[0];
            const fileType = file.name.split('.');
            console.log(fileType);
            if(!fileType[fileType.length-1].toUpperCase().match('JPG')&&
            !fileType[fileType.length-1].toUpperCase().match('PNG')&&
            !fileType[fileType.length-1].toUpperCase().match('JPEG')) {
                console.log("jpg png jpeg형식만 지원합니다.");
            }
            // 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함! - 415(미디어타입 주의!)
            formData.append("image", file); // 위에서 만든 폼데이터에 이미지 추가
            for (let pair of formData.entries()) {
                console.log(pair[0], pair[1]); 
            }
            // 폼데이터를 서버에 넘겨 multer로 이미지 URL 받아오기
            const res = await uploadImageDB(formData);
            files.push(res.date)
            console.log(res.data); // 리턴받는 파일명
            if (!res.data) {
                console.log("이미지 업로드에 실패하였습니다.");
            }
            const url = process.env.REACT_APP_SPRING_IP+`reple/imageGet?imageName=${res.data}`;
            const quill = quillRef.current.getEditor();
            /* ReactQuill 노드에 대한 Ref가 있어야 메서드들을 호출할 수 있으므로
            useRef()로 ReactQuill에 ref를 걸어주자.
            getEditor() : 편집기를 지원하는 Quill 인스턴스를 반환함
            여기서 만든 인스턴스로 getText()와 같은 메서드를 사용할 수 있다.*/
            
            const range = quill.getSelection().index; 
            //getSelection()은 현재 선택된 범위를 리턴한다. 에디터가 포커싱되지 않았다면 null을 반환한다.
            
            if (typeof (range) !== "number") return;
            /*range는 0이 될 수도 있으므로 null만 생각하고 !range로 체크하면 잘못 작동할 수 있다.
            따라서 타입이 숫자이지 않을 경우를 체크해 리턴해주었다.*/
            
            quill.setSelection(range, 1);
            /* 사용자 선택을 지정된 범위로 설정하여 에디터에 포커싱할 수 있다. 
            위치 인덱스와 길이를 넣어주면 된다.*/
            
            quill.clipboard.dangerouslyPasteHTML(
                range,
                `<img src=${url} style="width: 100%; height: auto;" alt="image" />`);
                
                //handleFiles(res.data, `${Math.floor(file.size/(1024*1024)*10)/10}MB`);
            }   //주어진 인덱스에 HTML로 작성된 내용물을 에디터에 삽입한다.
            
            
        }, [quillRef, files]);
        
    const modules = useMemo(
        () => ({
        toolbar: { // 툴바에 넣을 기능들을 순서대로 나열하면 된다.
            container: [
                [{ 'header': [1, 2, 3, 4, 5, 6, false] }, { color: [] }, { 'align': [] }, { 'background': [] }],
                ["bold", "italic", "underline", "strike", "blockquote"],
                [
                    { list: "ordered" },
                    { list: "bullet" },
                    { indent: "-1" },
                    { indent: "+1" },
                ],
                ['clean'],
                ['link', "image"],
            ],
            handlers: { // 위에서 만든 이미지 핸들러 사용하도록 설정
                image: imageHandler,
            },
        },
/*         ImageResize: {
            modules: ['Resize']
        } */
    }), [imageHandler])
    useEffect(()=>{
        console.log('QuillEditor useEffect');
    },[])
    const formats = [
        //'font',
        'header',
        'bold', 'italic', 'underline', 'strike', 'blockquote',
        'list', 'bullet', 'indent',
        'link', 'image',
        'align', 'color', 'background',        
    ]

    return (
        <div style={{height: "550px", display: "flex", justifyContent: "center", padding:"0px"}}>
            <ReactQuill 
                ref={quillRef}
                style={{height: "470px", width: "100%"}} 
                theme="snow" 
                placeholder= "본문 입력"
                modules={modules} 
                formats={formats}
                value={value} 
                onChange={(content, delta, source, editor) => {handleContent(editor.getHTML());}} />
        </div>
    )
}

export default QuillEditor

 

<Quill 이미지, 파일 첨부 - dbLogic.js>

import axios from "axios";

export const uploadImageDB = (file) => {
  console.log(file);
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/imageUpload",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        processData: false,
        contentType: false,
        data: file,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const uploadFileDB = (file) => {
  console.log(file);
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/fileUpload",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        processData: false,
        contentType: false,
        data: file,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const boardInsertDB = (board) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/boardInsert",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        data: board,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

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

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

export const memberUpdateDB = (member) => {
  return new Promise((resolve, reject) => {
    console.log(member)
    try {
      const response = axios({
        method: "post", // @RequestBody
        url: process.env.REACT_APP_SPRING_IP + "member/memberUpdate",
        data: member, // post방식 data
      });
      resolve(response); // 요청 처리 성공했을 때
    } catch (error) {
      reject(error); // 요청 처리 실패했을 때
    }
  });
};

export const memberDeleteDB = (member) => {
  return new Promise((resolve, reject) => {
    console.log(member)
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "member/memberDelete",
        params: member,
      });
      resolve(response); // 요청 처리 성공했을 때
    } catch (error) {
      reject(error); // 요청 처리 실패했을 때
    }
  });
};

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

 

<Quill 이미지, 파일 첨부 - RepleBoardController.java>

package com.example.demo.controller;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.example.demo.logic.RepleBoardLogic;
import com.google.gson.Gson;

@RestController
@RequestMapping("/reple/*")
public class RepleBoardController {
	Logger logger = LogManager.getLogger(RepleBoardController.class);
	
	@Autowired
	private RepleBoardLogic repleBoardLogic = null;
	
	@GetMapping("boardList")
	public String boardList(@RequestParam Map<String, Object> pMap) {
		logger.info("boardList 호출");
		List<Map<String, Object>> bList = null;
		bList = repleBoardLogic.boardList(pMap);
		Gson g = new Gson();
		String temp = g.toJson(bList);
		return temp;
	}
	
	@PostMapping("boardInsert")
	public String boardInsert(@RequestBody Map<String, Object> pMap) {
		logger.info("boardInsert 호출");
		int result = 0;
		result = repleBoardLogic.boardInsert(pMap);
		return String.valueOf(result);
	}
	
	@PostMapping("imageUpload")
	public Object imageUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="image", required=false) MultipartFile image) {
		logger.info("imageUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!image.isEmpty()) {
			filename = image.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = image.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@PostMapping("fileUpload")
	public Object fileUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="file_name", required=false) MultipartFile file_name) {
		logger.info("fileUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!file_name.isEmpty()) {
			filename = file_name.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = file_name.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@GetMapping("imageGet")
	public Object imageGet(HttpServletRequest req, HttpServletResponse res) {
		// imageName 정보는 공통코드로 제공된 QuillEditor.jsx에서 파라미터로 넘어오는 값임
		// imageUpload 메소드에서는 업로드된 파일 정보(파일명, 파일크기)가 리턴됨
		String b_file = req.getParameter("imageName"); // get방식으로 넘어옴
		logger.info("imageGet 호출 성공===>" + b_file); // XXX.png
		// 톰캣 서버의 물리적인 위지 정보
		String filePath = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
		String fname = b_file;
		logger.info("b_file: 8->euc" + b_file);
		// File은 내용까지 복제되는 것은 아니고 파일명만 객체화해주는 클래스이다
		File file = new File(filePath, b_file.trim());
		// 실제 업로드된 파일에 대한 마임타입을 출력해줌
		String mimeType = req.getServletContext().getMimeType(file.toString());
		logger.info(mimeType); // image, video, text
		if (mimeType == null) { // 마임타입이 널이면 아래의 속성값으로 마임타입을 설정
			// -> 브라우저는 해석이 가능한 마임타입은 페이지 로딩 처리,
			// 		해석이 불가능한 마임타입은 다운로드함
			// 강제로 다운로드 처리를 위한 마임타입 변경
			// -> 브라우저에서 해석가능한 마임타입의 경우 화면에 그대로 출력되니까 그걸 방지하기위해
			res.setContentType("application/octet-stream");
		}
		// 다운로드되는 파일 이름 담기
		String downName = null;
		// 위 File 객체에서 생성된 객체에 내용을 읽기위한 클래스 선언
		FileInputStream fis = null;
		// 응답으로 나갈 정보가 웹 서비스에 처리되어야 하기에 사용한 객체
		ServletOutputStream sos = null;
		try {
			if (req.getHeader("user-agent").indexOf("MSIE") == -1) {
				downName = new String(b_file.getBytes("UTF-8"), "8859_1");
			} else {
				downName = new String(b_file.getBytes("EUC-KR"), "8859_1");
			}
			// 응답 헤더에 다운로드 될 파읿명을 매핑하기
			res.setHeader("Content-Disposition", "attachment;filename=" + downName);
			// 위에서 생성된 파일 문자열 객체를 가지고 파일생성에 필요한 객체의 파라미터 넘김
			fis = new FileInputStream(file);
			sos = res.getOutputStream();
			// 파일 내용을 담을 byte배열을 생성
			byte b[] = new byte[1024 * 10];
			int data = 0;
			while ((data = (fis.read(b, 0, b.length))) != -1) {
				// 파일에서 읽은 내용을 가지고 실제 파일에 쓰기 처리함
				// 여기서 처리된 값은 브라우저를 통해서 내보내진다
				sos.write(b, 0, data);
			}
			// 처리한 내용이 버퍼에 있는데 이것을 모두 처리요청하기
			// 내보내고 버퍼를 비운다 - 버퍼는 크기가 작음(휘발성)
			sos.flush();
		} catch (Exception e) {
			logger.info(e.toString());
		} finally {
			try {
				if (sos != null)
					sos.close();
				if (fis != null)
					fis.close();
			} catch (Exception e2) {
				// TODO: handle exception
			}
		}
		// byte[] fileArray = boardLogic.imageDownload(imageName);
		// logger.info(fileArray.length);
		return null;
	}// end of imageGet
}

 

Promise

실패 reject → callback

이행 fulfilled → 결괏값 resolve()

실패했을 경우 catch(e)

비동기메소드에서 동기메소드처럼 값을 반환할 수 있음 → 다만 미래의 어떤 시점에 결과를 제공하겠다는 약속을 반환함

 

<qna리스트 출력 - App.jsx>

import { Route, Routes, useNavigate } 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';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';
import SignupPage from './components/auth/SignupPage';
import KhLoginPage from './components/auth/KhLoginPage';
import { onAuthChange } from './service/authLogic';
import { memberListDB } from './service/dbLogic';
import EmailVerifiedPage from './components/auth/EmailVerifiedPage';
import FindEmailPage from './components/auth/FindEmailPage';
import ResetPwdPage from './components/auth/ResetPwdPage';
import RepleBoardWriteForm from './components/repleboard/RepleBoardWriteForm';
import RepleBoardDetail from './components/repleboard/RepleBoardDetail';

function App({authLogic, imageUploader}) {
  // 화면 전환시킬때
  // window.location.href - 새로고침 요청 발생 - 가상돔 사용하지 않음
  // useNavigate - 가상돔 사용됨
  const navigate = useNavigate()
  const dispatch = useDispatch() // 허브 - action.type(switch-선택), action.payload(내용)
  const ssg = sessionStorage;
  const toastStatus = useSelector(state => state.toastStatus) // store에 값을 접근할때
  useEffect(() => { // 의존성배열 - 의존성 배열에 있는 변수, 함수, 훅이 변할때마다 다시 호출 가능함
    const asyncDB = async() => { // 함수선언 - memberListDB호출
      console.log('asyncDB 호출')
      const auth = authLogic.getUserAuth()
      // 현재 인증된 사용자 정보를 가져온다
      const user = await onAuthChange(auth)
      // 사용자가 있을 경우(userId가 존재) - 구글 로그인으로 사용자 정보를 가지고 있을 때
      // user정보가 있으면 sessionStorage에 담는다 - email
      if(user) {
        console.log('user정보가 있을 때')
        // 세션스토리지에 이메일 주소가 등록됨 - 단, 구글 로그인이 되어있는 상태일때만
        ssg.setItem('email', user.email)
        const res = await memberListDB({mem_uid: user.uid, type: 'auth'})
        console.log(res.data)
        //오라클 서버의 회원집합에 uid가 존재할 경우 - 세션스토리지에 값을 담는다
        if(res.data !== 0) { // 스프링 부트 - RestMemberController - memberList() -> 0 혹은 [{}]
          const temp = JSON.stringify(res.data)
          const jsonDoc = JSON.parse(temp)
          ssg.setItem('nickname', jsonDoc[0].MEM_NICKNAME)
          ssg.setItem('status', jsonDoc[0].MEM_STATUS)
          ssg.setItem('auth', jsonDoc[0].MEM_AUTH)
          ssg.setItem('no', jsonDoc[0].MEM_NO)
          //navigate("/")
          return // 렌더링이 종료됨
        }
        // 구글 계정이 아닌 다른 계정으로 로그인 시도를 했을 땐 user.emailVerified가 없다
        // -> undefined
        if(!user.emailVerified) {
          navigate('./auth/emailVerified')
        }
        //오라클 서버의 회원집합에 uid가 존재하지 않을 경우 
        else {
          console.log('가입되지 않은 구글 계정입니다.')
          //navigate('/auth/signup')
        }
      }
      // 사용자 정보가 없을 경우
      else {
        console.log('user정보가 없을 때')
        if(ssg.getItem('email')){
          // 세션 스토리지에 있는 값 모두 삭제하기
          ssg.clear()
          window.location.reload()
        } // end of inner if
      } // end of else
    }
    asyncDB() // 함수호출
  }, [dispatch]) // dispatch가 바뀔때마다 호출

  return (
    <>
      <div style={{height: '100vh'}}>
        {toastStatus.status && <Toast />}
        <Routes>
          <Route path='/login' exact={true} element={<KhLoginPage authLogic={authLogic} />} />
          <Route path='/' exact={true} element={<HomePage />} />
          <Route path='/home' exact={true} element={<HomePage />} />
          <Route path='/auth/signup' exact={true} element={<SignupPage authLogic={authLogic} />} />
          <Route path='/auth/emailVerified' exact={true} element={<EmailVerifiedPage authLogic={authLogic} />} />
          <Route path='/auth/findEmail' exact={true} element={<FindEmailPage />} />
          <Route path='/auth/resetPwd' exact={true} element={<ResetPwdPage authLogic={authLogic} />} />
          <Route path='/reple/board' exact={true} element={<RepleBoardPage />} />
          <Route path='/reple/boarddetail/*' element={<RepleBoardDetail />} />
          <Route path='/reple/boardwrite' exact={true} element={<RepleBoardWriteForm />} />
          <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>
      </div>
    </>
  );
}

export default App;

 

<qna리스트 출력 - RepleBoardWriteForm.jsx>

import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { boardInsertDB, qnaInsertDB, uploadFileDB, uploadImageDB } from '../../service/dbLogic'
import { BButton, ContainerDiv, FormDiv, HeaderDiv } from '../styles/FormStyle'
import QuillEditor from './QuillEditor'
import RepleBoardFileInsert from './RepleBoardFileInsert'

const RepleBoardWriteForm = () => {
  const navigate = useNavigate()
  const[title, setTitle] = useState("") // 사용자가 입력한 제목 담기
  const[secret, setSecret] = useState("") // 사용자가 입력한 비번 담기
  const[content, setContent] = useState("") // 사용자가 입력한 내용 담기(태그포함된 값들)
  const[file_name, setFilename] = useState("") // 첨부파일 이름 담기
  const[file_size, setFileSize] = useState("") // 첨부파일 크기 담기
  // QuillEditor이미지 선택하면 imageUploadDB타고 스프링 플젝 pds에 해당 이미지 업로드
  // pds에 업로드된 파일을 읽어서 Editor안에 보여줌 imageGet?imageName=XXX.png
  const[files, setFiles] = useState([])

  const quillRef = useRef()
  const fileRef = useRef()
  
  const handleTitle = useCallback((e) => {
    setTitle(e)
  }, [])
  const handleSecret = useCallback((e) => {
    setSecret(e)
  }, [])
  const handleContent = useCallback((e) => { // QuillEditor에서 담김 - 태그포함 정보
    setContent(e)
  }, [])

  const boardInsert = async() => {
    const no = sessionStorage.getItem(no)
    const board = {
      qna_bno: 0, // 오라클 자동채번하는 시퀀스 사용
      qna_title: title,
      qna_content: content,
      qna_type: '양도',
      qna_secret: secret,
      qna_hit: 0,
      mem_no: no,
    }
    const res = await qnaInsertDB(board)
    console.log(res)
    navigate("/reple/board")
  }

  const handleChange = async (event) => {
    console.log('첨부파일 선택'+event.target.value);
    //console.log(fileRef.current.value);
    //fileRef에서 가져온 값중 파일명만 담기
    const str = fileRef.current.value.split('/').pop().split('\\\\').pop()
    setFilename(str)
    console.log(str);
    //선택한 파일을 url로 바꾸기 위해 서버로 전달할 폼데이터 만들기
    const formData = new FormData()
    const file = document.querySelector("#file-input").files[0]
    formData.append("file_name", file)// 아래 "image"와 스프링 컨트롤러 메소드의 value 맞춰야함!
    const res = await uploadFileDB(formData)
    console.log(res.data)
    const fileinfo = res.data.split(',')
    console.log(fileinfo)
    setFilename(fileinfo[0])
    setFileSize(fileinfo[1])
  }

  const handleFiles = () => {
  }

  return (
    <>
    <ContainerDiv>
    <HeaderDiv>
      <h3 style={{marginLeft:"10px"}}>공지사항 글작성</h3>
    </HeaderDiv>
    <FormDiv>
      <div style={{width:"100%", maxWidth:"2000px"}}>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>제목</h3> 
          <BButton onClick={()=>{boardInsert()}}>글쓰기</BButton>
        </div>
        <input id="dataset-title" type="text" maxLength="50" placeholder="제목을 입력하세요."
          style={{width:"100%",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleTitle(e.target.value)}}/>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>비밀번호</h3> 
        </div>              
        <input id="dataset-pw" type="text" maxLength="50" placeholder="비밀번호를 입력하세요."
          style={{width:"200px",height:'40px' , border:'1px solid lightGray', marginBottom:'5px'}} onChange={(e)=>{handleSecret(e.target.value)}}/>
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
          <h3>첨부파일</h3> 
        </div>                      
        <input id="file-input" name='file_name' ref={fileRef} type="file" maxLength="50" className="visuallyhidden" onChange={handleChange}/>            
        <div style={{display: 'flex', justifyContent: 'space-between', marginBottom:'5px'}}>
        </div>
        <h3>상세내용</h3>
        <hr style={{margin:'10px 0px 10px 0px'}}/>
        <QuillEditor value={content} handleContent={handleContent} quillRef={quillRef} files={files} handleFiles={handleFiles}/>
        <RepleBoardFileInsert files={files}/>
      </div>
    </FormDiv>
  </ContainerDiv>
</>
  )
}

export default RepleBoardWriteForm

 

<qna리스트 출력 - RepleBoardPage.jsx>

import React from 'react'
import { Button, Table } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom'
import BlogFooter from '../include/BlogFooter'
import BlogHeader from '../include/BlogHeader'
import RepleBoardList from '../repleboard/RepleBoardList'

const RepleBoardPage = () => {
  const navigate = useNavigate()

  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>
              <RepleBoardList />
            </tbody>
          </Table> 
          <hr />    
          <div className='booklist-footer'>
            <Button variant="warning" >
              전체조회
            </Button>&nbsp; 
            <Button variant="success" onClick={() => navigate('/reple/boardwrite')}>
              글쓰기
            </Button> 
          </div>
        </div>
      </div>
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      
      {/* ========================== [[ 글쓰기 Modal ]] ========================== */}
      <BlogFooter />
    </>
  )
}

export default RepleBoardPage

 

<qna리스트 출력 - RepleBoardList.jsx>

import React, { useEffect, useState } from 'react'
import { qnaListDB } from '../../service/dbLogic'
import RepleBoardRow from './RepleBoardRow'

// 고려사항 - 상위 컴포넌트에서 하위 컴포넌트로만 props 전달이 가능한 점
// 초급 - 가급적 상위 컴포넌트에 두는 것을 추천
// 중급 - 하위 컴포넌트에서 일어난 상태 변화를 상위 컴포넌트에 반영
// 리렌더링되는 3가지 - 부모컴포넌트, state, props
// useEffect, useMemo(값 메모이제이션), useCallback(함수 메모이제이션) -> 의존성배열을 가짐
// 의존성배열 [] - 맨 처음 딱 한 번 호출
// 의존성배열 없음 - 코딩할때마다 호출, 무한루프
// 의존성배열 [값] - 의존성배열에 입력한 변수가 바뀔때마다 호출 - 다중처리(주의: 전역변수만 가능)
const RepleBoardList = () => {
  const [boards, setBoards] = useState([{}])
  const [board, setBoard] = useState({
    cb_gubun: "qna_title",
    keyword: "PT10회권 양도",
  })

  // useEffect의 의존성 배열을 통해서 렌더링 시점을 제어할 수 있다
  useEffect(()=>{
    const qnaList = async() => { // 비동기처리
      const res = await qnaListDB(board) // async가 있을때 await 사용 가능
      console.log(res.data)
      setBoards(res.data)
    }
    qnaList() // 호출
  },[board])

  return (
    <>
      {boards.map((board) => (
        <RepleBoardRow board={board} />
      ))}
    </>
  ) // 리렌더링 조건을 수렴할때마다 return()그룹이 재호출됨
}

export default RepleBoardList

 

<qna리스트 출력 - RepleBoardRow.jsx>

import React from 'react'

const RepleBoardRow = ({board}) => {
  return (
    <>
      <tr key={board.QNA_BNO}>
        <td>{board.QNA_BNO}</td>
        <td>{board.QNA_TITLE}</td>
        <td>{board.MEM_NAME}</td>
        <td>{board.QNA_DATE}</td>
        <td>{board.QNA_HIT}</td>
      </tr>
    </>
  )
}

export default RepleBoardRow

 

<qna리스트 출력 - dbLogic.js>

import axios from "axios";

export const qnaListDB = (board) => {
  return new Promise((resolve, reject) => {
    try {
      console.log(board)
      // axios 비동기요청 처리(ajax - fetch[브라우저, 클라이언트사이드] - axios[NodeJS-오라클 서버연동, 서버사이드])
      const response = axios({ // 3000번 서버에서 8000서버로 요청함 - 네트워크(다른서버-CORS이슈, 지연발생)
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "reple/qnaList",
        params: board,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const qnaInsertDB = (board) => {
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/qnaInsert",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        data: board,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const uploadImageDB = (file) => {
  console.log(file);
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/imageUpload",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        processData: false,
        contentType: false,
        data: file,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

export const uploadFileDB = (file) => {
  console.log(file);
  return new Promise((resolve, reject) => {
    try {
      const response = axios({
        method: "post",
        url: process.env.REACT_APP_SPRING_IP + "reple/fileUpload",
        headers: {
          "Content-Type": "multipart/form-data",
        },
        processData: false,
        contentType: false,
        data: file,
      });
      resolve(response);
    } catch (error) {
      reject(error);
    }
  });
};

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

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

export const memberUpdateDB = (member) => {
  return new Promise((resolve, reject) => {
    console.log(member)
    try {
      const response = axios({
        method: "post", // @RequestBody
        url: process.env.REACT_APP_SPRING_IP + "member/memberUpdate",
        data: member, // post방식 data
      });
      resolve(response); // 요청 처리 성공했을 때
    } catch (error) {
      reject(error); // 요청 처리 실패했을 때
    }
  });
};

export const memberDeleteDB = (member) => {
  return new Promise((resolve, reject) => {
    console.log(member)
    try {
      const response = axios({
        method: "get",
        url: process.env.REACT_APP_SPRING_IP + "member/memberDelete",
        params: member,
      });
      resolve(response); // 요청 처리 성공했을 때
    } catch (error) {
      reject(error); // 요청 처리 실패했을 때
    }
  });
};

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

 

<qna리스트 출력 - RepleBoardController.java>

package com.example.demo.controller;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import com.example.demo.logic.RepleBoardLogic;
import com.google.gson.Gson;

@RestController
@RequestMapping("/reple/*")
public class RepleBoardController {
	Logger logger = LogManager.getLogger(RepleBoardController.class);
	
	@Autowired
	private RepleBoardLogic repleBoardLogic = null;
	
	@GetMapping("qnaList")
	public String qnaList(@RequestParam Map<String, Object> pMap) {
		logger.info("qnaList 호출");
		List<Map<String, Object>> bList = null;
		bList = repleBoardLogic.qnaList(pMap);
		Gson g = new Gson();
		String temp = g.toJson(bList);
		return temp;
	}
	
	@PostMapping("qnaInsert")
	public String qnaInsert(@RequestBody Map<String, Object> pMap) {
		logger.info("qnaInsert 호출"); // 해당 메소드 호출 여부 찍어보기
		logger.info(pMap); // 넘어온 파라미터값 찍어보기
		int result = 0;
		result = repleBoardLogic.qnaInsert(pMap);
		return String.valueOf(result);
	}
	
	@PostMapping("imageUpload")
	public Object imageUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="image", required=false) MultipartFile image) {
		logger.info("imageUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!image.isEmpty()) {
			filename = image.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = image.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@PostMapping("fileUpload")
	public Object fileUpload(MultipartHttpServletRequest mRequest, @RequestParam(value="file_name", required=false) MultipartFile file_name) {
		logger.info("fileUpload 호출");
		// 사용자가 선택한 파일 이름 담기
		String filename = null;
		if(!file_name.isEmpty()) {
			filename = file_name.getOriginalFilename();
			String saveFolder = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
			String fullPath = saveFolder + "\\\\" + filename;
			try {
				// File객체는 파일명을 객체화해주는 클래스 - 생성되었다고해서 실제 파일까지 생성되는 것이 아님
				File file = new File(fullPath);
				byte[] bytes = file_name.getBytes();
				// outputStream을 반드시 생성해서 파일 정보를 읽은 후 쓰기 처리해줌 -> 완전한 파일이 생성됨
				// BufferedOutputStream은 필터 클래스이지 실제 파일을 쓸 수 없는 객체이고
				// 실제 파일쓰기가 가능한 클래스는 FileOutputStream클래스이다 - 생성자 파라미터에 파일정보를 담는다
				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
				bos.write(bytes);
				// 파일쓰기와 관련된 위변조 방지위해서 사용 후 반드시 닫을 것!
				bos.close();
			} catch (Exception e) {
				
			}
		}
		// 리턴값으로 선택한 이미지 파일명을 넘겨서 사용자 화면에 첨부된 파일명을 열거해주는데 사용
		String temp = filename;
		return temp;
	}
	
	@GetMapping("imageGet")
	public Object imageGet(HttpServletRequest req, HttpServletResponse res) {
		// imageName 정보는 공통코드로 제공된 QuillEditor.jsx에서 파라미터로 넘어오는 값임
		// imageUpload 메소드에서는 업로드된 파일 정보(파일명, 파일크기)가 리턴됨
		String b_file = req.getParameter("imageName"); // get방식으로 넘어옴
		logger.info("imageGet 호출 성공===>" + b_file); // XXX.png
		// 톰캣 서버의 물리적인 위지 정보
		String filePath = "D:\\\\workspace_sts\\\\mblog-1\\\\src\\\\main\\\\webapp\\\\pds";
		String fname = b_file;
		logger.info("b_file: 8->euc" + b_file);
		// File은 내용까지 복제되는 것은 아니고 파일명만 객체화해주는 클래스이다
		File file = new File(filePath, b_file.trim());
		// 실제 업로드된 파일에 대한 마임타입을 출력해줌
		String mimeType = req.getServletContext().getMimeType(file.toString());
		logger.info(mimeType); // image, video, text
		if (mimeType == null) { // 마임타입이 널이면 아래의 속성값으로 마임타입을 설정
			// -> 브라우저는 해석이 가능한 마임타입은 페이지 로딩 처리,
			// 		해석이 불가능한 마임타입은 다운로드함
			// 강제로 다운로드 처리를 위한 마임타입 변경
			// -> 브라우저에서 해석가능한 마임타입의 경우 화면에 그대로 출력되니까 그걸 방지하기위해
			res.setContentType("application/octet-stream");
		}
		// 다운로드되는 파일 이름 담기
		String downName = null;
		// 위 File 객체에서 생성된 객체에 내용을 읽기위한 클래스 선언
		FileInputStream fis = null;
		// 응답으로 나갈 정보가 웹 서비스에 처리되어야 하기에 사용한 객체
		ServletOutputStream sos = null;
		try {
			if (req.getHeader("user-agent").indexOf("MSIE") == -1) {
				downName = new String(b_file.getBytes("UTF-8"), "8859_1");
			} else {
				downName = new String(b_file.getBytes("EUC-KR"), "8859_1");
			}
			// 응답 헤더에 다운로드 될 파읿명을 매핑하기
			res.setHeader("Content-Disposition", "attachment;filename=" + downName);
			// 위에서 생성된 파일 문자열 객체를 가지고 파일생성에 필요한 객체의 파라미터 넘김
			fis = new FileInputStream(file);
			sos = res.getOutputStream();
			// 파일 내용을 담을 byte배열을 생성
			byte b[] = new byte[1024 * 10];
			int data = 0;
			while ((data = (fis.read(b, 0, b.length))) != -1) {
				// 파일에서 읽은 내용을 가지고 실제 파일에 쓰기 처리함
				// 여기서 처리된 값은 브라우저를 통해서 내보내진다
				sos.write(b, 0, data);
			}
			// 처리한 내용이 버퍼에 있는데 이것을 모두 처리요청하기
			// 내보내고 버퍼를 비운다 - 버퍼는 크기가 작음(휘발성)
			sos.flush();
		} catch (Exception e) {
			logger.info(e.toString());
		} finally {
			try {
				if (sos != null)
					sos.close();
				if (fis != null)
					fis.close();
			} catch (Exception e2) {
				// TODO: handle exception
			}
		}
		// byte[] fileArray = boardLogic.imageDownload(imageName);
		// logger.info(fileArray.length);
		return null;
	}// end of imageGet
}

 

<qna리스트 출력 - RepleBoardLogic.java>

package com.example.demo.logic;

import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.dao.RepleBoardDao;

@Service
public class RepleBoardLogic {
	Logger logger = LogManager.getLogger(RepleBoardLogic.class);

	@Autowired
	private RepleBoardDao repleBoardDao = null;
	
	public List<Map<String, Object>> qnaList(Map<String, Object> pMap) {
		logger.info("qnaList 호출");
		List<Map<String, Object>> bList = null;
		bList = repleBoardDao.qnaList(pMap);
		return bList;
	}

	public int qnaInsert(Map<String, Object> pMap) {
		logger.info("qnaInsert 호출");
		int result = 0;
		result = repleBoardDao.qnaInsert(pMap);
		return result;
	}
}

 

<qna리스트 출력 - RepleBoardDao.java>

package com.example.demo.dao;

import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class RepleBoardDao {
	Logger logger = LogManager.getLogger(RepleBoardDao.class);
	
	@Autowired
	private SqlSessionTemplate sqlSessionTemplate = null;

	public List<Map<String, Object>> qnaList(Map<String, Object> pMap) {
		logger.info("qnaList 호출");
		List<Map<String, Object>> bList = null;
		bList = sqlSessionTemplate.selectList("qnaList", pMap);
		return bList;
	}

	public int qnaInsert(Map<String, Object> pMap) {
		logger.info("qnaInsert 호출");
		int result = 0;
		// insert는 반환값이 object여서 update를 사용함
		result = sqlSessionTemplate.update("qnaInsert", pMap);
		return result;
	}
}

 

<qna리스트 출력 - repleboard.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="qnaList" parameterType="map" resultType="map">
			SELECT q.qna_bno
					 , q.qna_title
					 , q.qna_content
			     , q.qna_type
			     , q.qna_hit
			     , q.qna_date
			     , m.mem_name
			FROM qna q, member230324 m
			WHERE q.mem_no = m.mem_no
	</select>
	
	<!--
		@RequestParam - Map타입이 올 수 있다 - GET방식 요청 - 요청 header에 담긴다 - 인터셉트 가능성(캐시에 있는 정보가 다시 출력될 가능성)
			문제점: URL에 노출됨 - 보안에 취약 - 조회할때 사용
		@RequestBody - POST방식 요청 - 단위테스트 불가함 - postman사용해 테스트 가능 - 요청 body에 담긴다 - 인터셉트 불가(무조건 서버로 전달)
			VO Map 원시형타입 모두 가능
			
		qna_type 질문 타입은 상수로 양도를 줌
		qna_secret은 비밀번호를 입력받음
		비밀번호가 null이면 공개, null이 아니면 비공개처리
		
		생각해볼 문제
			mem_no는 어디서 가져오고 인증인 어디서 하는가? - App.jsx의 useEffect가
			화면에서 가져올 컬럼의 종류는 몇가지인가?
			세션이나 쿠키 또는 세션스토리지에서 가져와야하는 컬럼이 있는가?
			상수로 넣을 수 있는 (또는 넣어야하는) 컬럼이 존재하는가?
			만약 존재한다면 어떤 컬럼인가?
			작성자는 입력받도록 화면을 그려야 하는가 혹은 자동으로 결정될 수 있는가?
	 -->
	<!-- 게시글 입력 -->
	<insert id="qnaInsert" parameterType="map">
		INSERT INTO QNA(
		      qna_bno
		    , mem_no
		    , qna_title
		    , qna_content
		    , qna_type
		    , qna_secret
		    , qna_hit
		    , qna_date
		    )
		    VALUES(
		      qna_seq.nextval
		    , #{mem_no}
		    , #{qna_title}
		    , #{qna_content}
		    , '양도'
		    , #{qna_secret}
		    , #{qna_hit}
		    , to_char(sysdate, 'YYYY-MM-DD')
		    )
	</insert>
	
	<!-- 게시글 수정 -->
	<update id="qnaUpdate" 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="qnaDelete" parameterType="int">
		DELETE from dept
	 	<where>
	 		<if test='value!=null and value > 0'>
				AND deptno = #{value}
			</if>
	 	</where>
	</delete>
</mapper>

댓글