고래씌

[SpringBoot] 2-1. React에 스프링부트 연동(게시판, 채팅방 목록) 본문

Server/SpringBoot

[SpringBoot] 2-1. React에 스프링부트 연동(게시판, 채팅방 목록)

고래씌 2024. 2. 5. 09:50

1. React에 스프링부트 연동(게시판)

 
=> 메뉴앱 프로젝트 생성

 
 
=> 사용할 패키지 다운받기
cd .\menuapp\
 
① axios를 사용하기 위해 패키지 다운
npm install --save axios
 
② bootstrap 사용하기 위해 다운
npm install --save bootstrap
 
③ npm start
 
 
=> public폴더안에 css폴더와 images 폴더 그대로 복사하여 붙여놓기 해줌
=> css폴더안에 있는 style.css는 모두 App.css로 옮김
 
 
 
 
▶ src > components폴더 > Hedader.js 파일 생성

export default function Header(){
    
    return (
        <header>
			<div id="header-container">
				<h2>Menu</h2>
			</div>
			
			<nav className="navbar navbar-expand-lg navbar-light bg-light">
	            <a className="navbar-brand" href="#">
	                    <img src="/resources/images/logo-spring.png" alt="스프링로고" width="50px" />
	            </a>
	           
	            <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
	                <span classNameName="navbar-toggler-icon"></span>
	            </button>
	            <div classNameName="collapse navbar-collapse" id="navbarNav">
	            </div>
       		</nav>
		</header>
    )
}

 
 
▶ GetMenu.js

import {useState} from "react"
import axios from 'axios';


export default function GetMenu(){

    const [menus, setMenus] = useState([]);

    function getMenus(){
        /*
            CORS 정책 위반

            모든 브라우저는 보안상의 이유로 동일한 출처(Origin)가 아닌 경로에서 들어오는
            리소스 요청에 대해서는 전부 차단을 함

            * Origin ? 프로토콜 + ip주소 + 포트번호 => http://localhost:3000

            cors 정책은 값을 내려주는 서버측에서 허용을 해줘야지 요청/응답이 가능하다.
        */
        // 1차) 비동기요청 보내기
        axios
        .get("http://localhost:8083/springboot/menus")  // 전달할 데이터가 있다면 , {} 이런 형식으로 붙혀서 보내야하는데 전달할 데이터없기때문에 생략가능
        // 2차) 요청받은 데이터로 setMenus함수 호출
        .then(response => setMenus(response.data))
    }

    return(
        <>
            <div className='menu-test'>
                        <h4>전체메뉴조회기능(GET)</h4>
                        <input type='button' className='btn btn-block btn-outline-success btn-send'
                            id='btn-menus' value="전송" onClick={getMenus} />
            </div>
            <div id="menus-result" className='result'>
                <table className="table">
                    <thead>
                        <tr>
                            <th>번호</th>
                            <th>음식점</th>
                            <th>메뉴</th>
                            <th>가격</th>
                            <th>타입</th>
                            <th>맛</th>
                        </tr>
                    </thead>
                    <tbody>
                    {
                        menus.map((menu) => {
                            return (
                                <tr key={menu.id}>
                                    <td>{menu.id}</td>
                                    <td>{menu.restaurant}</td>
                                    <td>{menu.name}</td>
                                    <td>{menu.price}</td>
                                    <td>{menu.type}</td>
                                    <td>{menu.taste}</td>
                                </tr>
                            )
                        })
                    }
                    </tbody>
                </table>
            </div>
        </>
    )
}

 
 
=> springboot에서 MenuController.java에서 다음과 같이 수정해야 전체보기를 클릭하였을때 정상적으로 표시된다.
 
▶ MenuController.java

//	메뉴 목록 조회
	@GetMapping("/menus")
	public List<Menu> menus(HttpServletResponse res){
		
		// 응답헤더에 Accsess-Controll-Allow-Origin 추가
		System.out.println(menuService.selectMenus());
		List<Menu> list =  menuService.selectMenus();
//		return menuService.selectMenus();
		
		res.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://localhost:3000"); // origin이 접근하는 걸 허용하는 속성
		// 허용하고자하는 출처(ORIGIN)
		
		return list;
	}

 

 
 
 
=> 하지만 이렇게 하나하나 "http://localhost:8083" 지정하기 어렵다. 그러니 proxy 설정을 추가한다!
package.json 파일에 들어가서 맨 아래에 추가

 
 
 
▶ GetMenu.js 아래와 같이 수정

 
=> 프록시 설정 완료 후에는 localhost:3000으로 들어오는 모든 요청을 proxy가 localhost:8083 서버로 전달하여 대신 응답결과를 얻어와서, 현재서버에 전달을 해준다.
proxy : 중계자역할을 하는 객체
 
 
 
▶ PostMenu.js

import axios from "axios";
import { useState } from "react";

export default function PostMenu(){

    const [newMenu, setNewMenu] = useState({
        restaurant : '', name : '', price : '', type : '',
        taste : ''
    });

    const handleInputChange = (e) => {
        const {name, value} = e.target;
        setNewMenu({...newMenu, [name] : value});
    }

    console.log(newMenu);

    const insertMenu = (e) => {
        e.preventDefault();
        // 프록시 사용하지 않고 등록되는 기능 추가하기
        axios
        .post("http://localhost:3000/springboot/menu", newMenu) // 자동 매핑이 돼서 JSON.stringify 쓸필요 X
        .then(function(response){
            const {msg} = response.data;
            alert(msg);
        })
        .catch(console.log)
        .finally(
            function(){
                setNewMenu({
                    restaurant : '',
                    name : '',
                    price : '',
                    type : '',
                    taste : ''
                });
                    e.target.reset();
            }
        )
    }

    return (
        <>
        <div className="menu-test">
            <h4>메뉴 등록하기(POST)</h4>
            <form id="menuEnrollFrm" onSubmit={insertMenu} >
                <input type="text" name="restaurant" placeholder="음식점" className="form-control" onChange={handleInputChange} />
                <br />
                <input type="text" name="name" placeholder="메뉴" className="form-control" onChange={handleInputChange}/>
                <br />
                <input type="number" name="price" placeholder="가격" className="form-control" onChange={handleInputChange}/>
                <br />
                <div className="form-check form-check-inline">
                    <input type="radio" className="form-check-input" name="type" id="post-kr" value="kr" onChange={handleInputChange}/>
                    <label htmlFor="post-kr" className="form-check-label">한식</label>&nbsp;
                    <input type="radio" className="form-check-input" name="type" id="post-ch" value="ch" onChange={handleInputChange}/>
                    <label htmlFor="post-ch" className="form-check-label">중식</label>&nbsp;
                    <input type="radio" className="form-check-input" name="type" id="post-jp" value="jp" onChange={handleInputChange}/>
                    <label htmlFor="post-jp" className="form-check-label">일식</label>&nbsp;
                </div>
                <br />
                <div className="form-check form-check-inline">
                    <input type="radio" className="form-check-input" name="taste" id="post-hot" value="hot" onChange={handleInputChange}/>
                    <label htmlFor="post-hot" className="form-check-label">매운맛</label>&nbsp;
                    <input type="radio" className="form-check-input" name="taste" id="post-mild" value="mild" onChange={handleInputChange}/>
                    <label htmlFor="post-mild" className="form-check-label">순한맛</label>
                </div>
                <br />
                <input type="submit" className="btn btn-block btn-outline-success btn-send" value="등록"/>
            </form>
        </div>
        </>
    )
}

 
 
▶ MenuController.java
=> @CrossOrigin 어노테이션 추가

 
 
 
▶ App.js

import './App.css';
import 'bootstrap/dist/css/bootstrap.css'  // 부트스트랩 css 가져오기
import Header from './components/Header';
import GetMenu from './components/GetMenu';
import PostMenu from './components/PostMenu';




function App() {
  return (
    <div id='container'>
      <Header />
      <section id="content">
        <div id='menu-container' className='text-center'>
          <GetMenu />
          <PostMenu />
        </div>
      </section>
    </div>
  );
}

export default App;

 
 
 


2. React에 스프링부트 연동(채팅)

1) 시스템 관리자 계정에서 다음과 같이 설정

 
 
2) CHAT 에 SQL문을 열어서 아래와 같이 추가

DROP TABLE MEMBER;
DROP TABLE CHAT_MESSAGE;
DROP TABLE CHAT_ROOM;
DROP TABLE CHAT_ROOM_JOIN;


DROP SEQUENCE SEQ_UNO;
DROP SEQUENCE SEQ_RNO;


CREATE TABLE MEMBER (
  USER_NO NUMBER PRIMARY KEY,               
  EMAIL VARCHAR2(30) NOT NULL UNIQUE,   
  USER_PWD VARCHAR2(100) NOT NULL,  
  NICK_NAME VARCHAR2(15) NOT NULL,    
  ENROLL_DATE DATE DEFAULT SYSDATE,
  MODIFY_DATE DATE DEFAULT SYSDATE,
  STATUS VARCHAR2(1) DEFAULT 'Y' CHECK (STATUS IN('Y', 'N')),
  PROFILE VARCHAR2(200)
);

INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample1@naver.com',
  1234,
  '루피1',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user0.jpg'
);

INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample2@naver.com',
  1234,
  '루피2',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user1.jpg'
);
INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample3@naver.com',
  1234,
  '루피3',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user2.jpg'
);
INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample4@naver.com',
  1234,
  '루피4',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user3.jpg'
);
INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample5@naver.com',
  1234,
  '루피5',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user4.jpg'
);
INSERT INTO MEMBER 
VALUES(
  SEQ_UNO.NEXTVAL,
  'sample6@naver.com',
  1234,
  '루피6',
  DEFAULT,
  DEFAULT,
  DEFAULT,
  '/images/user5.jpg'
);

CREATE SEQUENCE SEQ_UNO NOCACHE;

CREATE TABLE CHAT_MESSAGE(
CM_NO NUMBER PRIMARY KEY,
MESSAGE VARCHAR2(4000),
CREATE_DATE DATE,
CHAT_ROOM_NO NUMBER,
USER_NO NUMBER
);

CREATE SEQUENCE SEQ_CM_NO NOCACHE;

CREATE TABLE CHAT_ROOM(
    CHAT_ROOM_NO NUMBER PRIMARY KEY,
    TITLE VARCHAR2(400),
    STATUS VARCHAR2(1) DEFAULT 'Y',
    USER_NO NUMBER
);

CREATE SEQUENCE SEQ_CRM_NO NOCACHE;

CREATE TABLE CHAT_ROOM_JOIN(
    USER_NO NUMBER,
    CHAT_ROOM_NO NUMBER,
    USER_STATUS NUMBER,
    PRIMARY KEY(USER_NO, CHAT_ROOM_NO)
);

COMMIT;

 
 
3) pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.kh</groupId>
	<artifactId>api</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-1</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>3.0.3</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
		    <groupId>com.oracle.database.jdbc</groupId>
		    <artifactId>ojdbc6</artifactId>
		    <version>11.2.0.4</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter-test</artifactId>
			<version>3.0.3</version>
			<scope>test</scope>
		</dependency>
		<!-- 웹소켓 의존성 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-websocket</artifactId>
			
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

 
 
4) application.properties

server.servlet.context-path=/api
server.port=8084
#datasource
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=CHAT
spring.datasource.password=CHAT
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
#mybatis
mybatis.mapper-locations=classpath*:/mapper/**/*.xml
mybatis.configuration.jdbc-type-for-null=NULL
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.kh.api
#mybatis.type-handlers-package=com.kh.springboot.menu.model.typeHandler

 
 
5) 다시 myapp으로 들어간다! 
cd .\myapp\
npm start
 
▶ App.js

 
=> 추가
 
 
=> pages > ChattingRoom.js, pages > ChattingRoomList.js 파일 생성
▶ ChattingRoomList.js   (채팅방 목록)

import { useEffect } from "react";
import { useState } from "react";
import axios from 'axios';

export default function ChattingRoomList(){

    let [채팅방목록, 채팅방목록변경] = useState([]);
    let [모달, 모달창오픈] = useState(false);

    useEffect(() => {
        axios
        .get("http://localhost:3000/api/chatRoomList")
        .then(
            // 응답데이터 함수
            (response) => {
                console.log(response);
                채팅방목록변경(response.data); // 채팅방 목록페이지 조회
            }
        )
        .catch( (err) => console.log(err))
    }, [])

    return (
        <>
        <section className="board-list">
            <h1 className="board-name">채팅방 목록</h1>
            <div className="list-wrapper">
                <table className="list-table">
                    <thead>
                        <tr>
                            <th>방번호</th>
                            <th>채팅방 주제(제목)</th>
                            <th>개설자</th>
                            <th>참여인원수</th>
                        </tr>
                    </thead>
                    <tbody>
                    {
                        채팅방목록.length == 0 ?
                        (
                            <tr>
                                <td colSpan={4}>존재하는 채팅방이 없습니다.</td>
                            </tr>
                        ) : (
                            채팅방목록.map((채팅방) => {
                                return(
                                    <tr key={채팅방.chatRoomNo}>
                                        <td>{채팅방.chatRoomNo}</td>
                                        <td>{채팅방.title}</td>
                                        <td>{채팅방.nickName}</td>
                                        <td>{채팅방.cnt}</td>
                                    </tr>
                                )
                            })
                        )
                    }
                    </tbody>
                </table>
                <div className="btn-area">
                    <button onClick={ () => 모달창오픈(true)}>채팅방 만들기</button>
                </div>
            </div>
        </section>

        {모달 && <채팅창 모달창오픈={모달창오픈} />}

        </>
    )
}

function 채팅창({모달창오픈}){
    return(
        <div className="modal">
            <div className="modal-content">
                <span className="close" onClick={() => 모달창오픈(false)}>&times;</span>
                <div className="login-form">
                    <h3>채팅방 만들기</h3>
                    <input type="text" name="title" className="form-control" placeholder="채팅방 제목"
                    /><button>만들기</button>
                </div>
            </div>
        </div>
    )
}

 
 
▶ package.json
=> 프록시 추가

 
 
 
 
=> spingboot로 가서 아래에 폴더및 class 파일 생성

 
 
 
▶ Member.java

package com.kh.api.model.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {
	
	private int userNo;
	private String eamil;
	private String nickName;
	private String userPwd;
	private String profile;
	private String status;
	private String enrollDate;
	private String modifyDate;

	private UserStatus userStatus;
	
}

 
 
▶ ChatRoom.js

package com.kh.api.model.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatRoom {
	
	private int chatRoomNo;
	private String title;
	private String status;
	private int userNo;
	
	private String nickName;
	private int cnt;
}

 
 
▶ ChatMessage.java

package com.kh.api.model.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
	
	private int cmNo;
	private String message;
	private String createDate;
	private int chatRoomNo;
	private int userNo;
	
	private String nickName;
	private String profile;

}

 
 
▶ ChatRoomJoin.java

package com.kh.api.model.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatRoomJoin {
	
	private int userNo;
	private int chatRoomNo;
	private int userStatus;  // 1 → 접속중, 2 → 나감

}

 
 
 
▶ UserStatus.java   (Enum 객체)   (com.kh.api.model.vo)

package com.kh.api.model.vo;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;

public enum UserStatus {
	
	@JsonProperty("1")
	online(1), 
	@JsonProperty("2")
	offline(2);
	
	private int userStatus;
	
	UserStatus(int userStatus){
		this.userStatus = userStatus;
	}
	
	
	// Json 형태로 변환시 문자열형태로 변환해야해서 String형으로 getter함수 사용
	@JsonValue
	public String getUserStatus() {
		return String.valueOf(userStatus);
	}
	
	public static UserStatus getValueOfUserStatus(int userStatus) {
		
		UserStatus [] list = UserStatus.values();
		
		for(UserStatus us : list) {
			if(us.userStatus == userStatus) {
				return us;
			}
		}
		return null;  // 못찾았다면 null값 반환
	}
}

 
 
 
▶ ChatController.java

package com.kh.api.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.kh.api.model.service.ChatService;
import com.kh.api.model.vo.ChatRoom;

// 모두 JSON으로 받아줄 예정

@RestController
public class ChatController {
	
	@Autowired
	private ChatService service;

//	채팅방 목록
	@GetMapping("/chatRoomList")
	public List<ChatRoom> selectChatRooms(){
		
		return service.selectChatRooms();
	}
}

 
 
▶ ChatService.java

package com.kh.api.model.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.kh.api.model.dao.ChatDao;
import com.kh.api.model.vo.ChatRoom;

@Service
public class ChatService {
	
	@Autowired
	private ChatDao dao;

//	채팅방 목록
	public List<ChatRoom> selectChatRooms() {
		return dao.selectChatRooms();
	}

}

 
 
▶ ChatDao.java

package com.kh.api.model.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.kh.api.model.vo.ChatRoom;

@Repository
public class ChatDao {
	
	@Autowired
	private SqlSessionTemplate session;

//	채팅방 목록
	public List<ChatRoom> selectChatRooms() {
		return session.selectList("chatMapper.selectChatRooms");
	}

}

 
 
▶ chatting-mapper.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="chatMapper">
	
	<select id="selectChatRooms" resultType="chatRoom">
		SELECT
			CHAT_ROOM_NO,
			TITLE,
			NICK_NAME,
			(SELECT COUNT(*) FROM CHAT_ROOM_JOIN CRJ WHERE CRJ.CHAT_ROOM_NO = CR.CHAT_ROOM_NO) CNT,
			USER_NO
		FROM CHAT_ROOM CR
		LEFT JOIN MEMBER USING (USER_NO)
	</select>
</mapper>

 
 
▶ 결과