※ 실행 준비 과정
- 파일을 하나 만든후 VSCode에서 만든 파일을 오픈 (파일 이름은 한글을 사용하면 안 됨)
그 후 터미널에서 npx create-react-app '내 파일이름'을 입력하면 여러 react 파일이 생김 (ex : npx create-react-app project-app)
터미널에서 cd project-app으로 파일 이동 후 다음과 같이 실행
- npm i express ⇒ 설치 : 서버
- npm i mysql ⇒ 설치 : DB
- npm i axios ⇒ 설치 : 비동기
- npm i cors ⇒ 설치 : 서버와 클라이언트 간의 자원공유 관리
- npm i json ⇒ 설치 : json
- npm i nodemon ⇒ 설치 : 자동감지 서버 재시작 도구 (소스코드의 변경이 발생하면 자동으로 서버 재시작)
- npm i react-router-dom ⇒ react 표준 라우팅 라이브러리 (컴포넌트간의 전환이 일어날때 화면 전환)
▣ project-app
- App.js
import './App.css';
import ProjectHome from './component/ProjectHome';
function App() {
return (
<div className="App">
<ProjectHome/>
</div>
);
}
export default App;
- server.js
// 설치한 라이브러리 변수로 받아오기
const express = require('express');
const bodyParser = require('body-parser');
const mysql = require('mysql');
const cors = require('cors');
//express 사용하기 위한 app 생성
const app = express();
//express 사용할 서버포트 설정
const PORT = 5000;
app.use(cors());
app.use(bodyParser.json());
//DB 접속
const db = mysql.createConnection({
host : 'localhost',
user: 'react', // 생성
password: 'mysql',
port:'3306',
database:'db_react' // 생성
});
// express 접속
app.listen(PORT, ()=>{
console.log(`server connecting on : http://localhost:${PORT}`);
});
//db 연결
db.connect((err)=>{
if(!err){
console.log("seccuss");
}else{
console.log("fail");
}
});
// DB에서 값을 가져오기
// / => root 연결시 보여지는 기본화면 설정
app.get('/',(req, res) => {
res.send("React Server Connect Success!!")
})
// 게시글 목록 가져오기
app.get('/list', (req, res) => {
// console.log('/list');
const sql = 'select * from project order by id desc';
db.query(sql, (err, data)=>{
if(!err){
res.send(data);
}else{
console.log(err);
res.send('전송오류');
}
});
});
// 게시글 하나 가져오기 : id
// 화면에서 서버로 요청하는 값 : request (req)
// 서버에서 화면으로 보내주는 값 : response (res)
// 화면에서 가져온 파라미터 추출 : req.params.id
app.get('/view/:id', (req, res) => {
// 파라미터 가져오기
const id = req.params.id;
console.log(`/view/${id}`);
const sql = `select * from project where id = ${id}`;
db.query(sql, (err, data) => {
if(!err){
res.send(data);
}else{
console.log(err);
res.send("전송오류");
}
})
});
// board 등록
app.post('/insert', (req, res) => {
// 파라미터 가져오기 requset.body
// const board = req.body;
// board.title
const { title, writer, contents } = req.body;
const sql = 'insert into project(title, writer, contents) value (?,?,?)';
db.query(sql, [title, writer, contents], (err, data) => {
if(!err){
// res.send("OK");
res.sendStatus(200); // 전송 잘됨
}else{
console.log(err);
res.send("전송오류");
}
})
});
// 수정-불러오기
app.get('/modify/:id', (req, res) => {
// 파라미터 가져오기
const id = req.params.id;
console.log(`/modify/${id}`);
const sql = `select * from project where id = ${id}`;
db.query(sql, (err, data) => {
if(!err){
res.send(data);
}else{
console.log(err);
res.send("전송오류");
}
})
});
// 수정-저장
app.post('/modify/:id', (req, res) => {
// 파라미터 가져오기 requset.body
// const board = req.body;
// board.title
const id = req.params.id;
const { title, writer, contents } = req.body;
const sql = `update project set title=?, writer=?, contents=? where id=?`;
db.query(sql, [title, writer, contents, id], (err, data) => {
if(!err){
// res.send("OK");
res.sendStatus(200); // 전송 잘됨
}else{
console.log(err);
res.send("전송오류");
}
})
});
// 삭제
app.post('/delete/:id', (req, res) => {
const id = req.params.id;
const sql = `delete from project where id=${id}`;
db.query(sql, (err, data) => {
if(!err){
// res.send("OK");
res.sendStatus(200); // 전송 잘됨
}else{
console.log(err);
res.send("전송오류");
}
})
});
// answer 등록 및 수정-불러오기
app.get('/answer/:id', (req, res) => {
// 파라미터 가져오기
const id = req.params.id;
console.log(`/answer/${id}`);
const sql = `select * from project where id = ${id}`;
db.query(sql, (err, data) => {
if(!err){
res.send(data);
}else{
console.log(err);
res.send("전송오류");
}
})
});
// answer - 저장
app.post('/answer/:id', (req, res) => {
// 파라미터 가져오기 requset.body
// const board = req.body;
// board.title
const id = req.params.id;
const { answer } = req.body;
const sql = `update project set answer=? where id = ?`;
db.query(sql, [answer, id], (err, data) => {
if(!err){
// res.send("OK");
res.sendStatus(200); // 전송 잘됨
}else{
console.log(err);
res.send("전송오류");
}
})
});
- ProjectHome.jsx
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ProjectList from './ProjectList';
import '../component/project-style.css';
import ProjectDetail from './ProjectDetail';
import ProjectRegister from './ProjectRegister';
import ProjectModify from './ProjectModify';
import ProjectAnswer from './ProjectAnswer';
import pink from '../img/pink.jpg';
const ProjectHome = () => {
return (
<div className='projectHome'>
<div className='header'>
<img src={pink} width = '50px'/>
<span className='title'>이젠 답을 찾는 Q&A</span>
</div>
<BrowserRouter>
<Routes>
<Route path='/' element={<ProjectList/>}/>
<Route path='/list' element={<ProjectList/>}/>
<Route path="/detail/:id" element={<ProjectDetail/>} />
<Route path='/register' element={<ProjectRegister/>}/>
<Route path='/modify/:id' element={<ProjectModify/>}/>
<Route path='/answer/:id' element={<ProjectAnswer/>}/>
</Routes>
</BrowserRouter>
</div>
);
};
export default ProjectHome;
- ProjectList.jsx
import React, { useState } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
import { useEffect } from 'react';
const ProjectList = () => {
const [ projectList, setProjectList ] = useState({});
const getProjectData = async () => {
try{
const projects = await axios('/list');
console.log(projects);
setProjectList(projects.data);
} catch (error) {
console.log(error);
}
}
useEffect(() => {
getProjectData();
},[])
if(projectList.length > 0){
return (
<div className='projectList'>
<h2>List Page</h2>
<table>
<thead>
<tr className='subTitle'>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
</tr>
</thead>
<tbody>
{projectList.map(b => (
<tr key={b.id}>
<td className='center'>{b.id}</td>
<td><Link to={`/detail/${b.id}`}>{b.title}</Link><span className='lock'>({b.lock_type})</span></td>
<td>{b.writer}</td>
<td>{b.reg_date.substring(0, b.reg_date.indexOf("T"))}</td>
</tr>
))}
</tbody>
</table>
<Link to={`/register`}><button className='textButton'>글쓰기</button></Link>
</div>
);
}
};
export default ProjectList;
- ProjectDetail.jsx
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import axios from 'axios';
const ProjectDetail = () => {
const { id } = useParams();
const [ project, setProject ] = useState(null);
const getProject = async () => {
try{
const res = await axios(`/view/${id}`);
setProject(res.data[0]);
console.log(res);
}catch(error){
console.log(error);
}
};
useEffect(()=>{
getProject();
},[]);
const onDelete = async () => {
if(window.confirm('삭제하시겠습니까?')){
try{
await axios.post(`/delete/${id}`);
window.location.href = `/list`;
}catch(error){
console.log(error);
}
}
};
if(project != null){
return (
<div className='projectDetailProject'>
<h2>No.{project.id} / Detail Page</h2>
<div className='QA'>Q :</div>
<div className='projectDetailContainer'>
<span>{project.title}</span>
<span className='bold'>{project.writer} [{project.reg_date.substring(0, project.reg_date.indexOf("T"))}]</span>
<div>{project.contents}</div>
</div>
<div>
<Link to = {`/modify/${project.id}`}><button>수정</button></Link>
<button onClick = {onDelete}>삭제</button>
</div>
<div className='QA'>A :</div>
<div className='projectDetailContainer answerSpan'>
<span>[{project.answer}]</span>
</div>
<div>
<Link to = {`/answer/${project.id}`}><button>답변</button></Link>
<Link to={'/list'}><button>돌아가기</button></Link>
</div>
</div>
);
}
};
export default ProjectDetail;
- ProjectAnswer.jsx
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import axios from 'axios';
const ProjectAnswer = () => {
const { id } = useParams();
const [ mod, setMod ] = useState({});
const getProject = async () => {
try{
const res = await axios(`/answer/${id}`);
setMod(res.data[0]);
console.log(res);
}catch(error){
console.log(error);
}
};
useEffect(()=>{
getProject();
},[]);
const onChange = (e) => {
const { name, value } = e.target;
setMod({
...mod,
[name]:value
});
console.log(mod);
};
const onSubmit = async () => {
if(window.confirm('등록하시겠습니까?')){
try{
const res = await axios.post(`/answer/${id}`, mod);
console.log(res);
window.location.href = `/detail/${id}`;
}catch(error){
console.log(error);
}
}
};
const onReset = () => {
setMod({
...mod,
answer: ''
})
}
if(mod != null){
return (
<div className='projectDetailProject'>
<div className='projectDetailContainer'>
<textarea type="text" className='contentContainer' value={mod.contents}/>
</div>
<div className='projectDetailContainer2'>
<textarea type="text" className='contentContainer2' name='answer' onChange={onChange}/>
</div>
<div>
<button onClick={onSubmit}>등록</button>
<button onClick={onReset}>초기화</button>
<Link to={`/detail/${id}`}><button>돌아가기</button></Link>
</div>
</div>
);
}
};
export default ProjectAnswer;
- ProjectModify.jsx
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import axios from 'axios';
const ProjectModify = () => {
const { id } = useParams();
const [ mod, setMod ] = useState({
title: '',
writer: '',
contents: '',
reg_date: ''
});
const { title, writer, contents, reg_date } = mod;
const getProject = async () => {
try{
const res = await axios(`/modify/${id}`);
setMod(res.data[0]);
console.log(res);
}catch(error){
console.log(error);
}
};
useEffect(()=>{
getProject();
},[]);
const onChange = (e) => {
const { name, value } = e.target;
setMod({
...mod,
[name]:value
});
};
const onSubmit = async () => {
if(title === ''){
alert('title is null');
return;
}
if(writer === ''){
alert('wirter is null');
return;
}
if(contents === ''){
alert('contents is null');
return;
}
if(window.confirm('수정하시겠습니까?')){
try{
const res = await axios.post(`/modify/${id}`, mod);
console.log(res);
window.location.href = `/detail/${id}`;
}catch(error){
console.log(error);
}
}
};
if(mod != null){
return (
<div className='projectRegister'>
<h2>Modify Page</h2>
<div className='content'>
<input type="text" className='content-box' name='reg_date' value={reg_date.substring(0, reg_date.indexOf("T"))}/>
<input type="text" className='content-box' name='title' placeholder='Title' value={title} onChange={onChange}/>
<input type="text" className='content-box' name='writer' placeholder='Writer' value={writer} onChange={onChange}/>
<div className='contentContainer'>
<textarea type="text" className='content-box' name='contents' placeholder='Contents' value={contents} onChange={onChange}/>
</div>
</div>
<div className='lock-radio'>
<label for="default">
<label>
공개
<input name="default" id="default" type="radio" checked/>
</label>
<label>
비공개
<input name="default" type="radio"/>
</label>
</label>
</div>
<button onClick={onSubmit}>수정</button>
<Link to={'/list'}><button>취소</button></Link>
</div>
);
}
};
export default ProjectModify;
- ProjectRegister.jsx
import React, { useState } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
const ProjectRegister = () => {
const [ project, setProject ] = useState({});
const onChange = (e) => {
const { name, value } = e.target;
setProject({
...project,
[name]:value
});
}
const onReset = () => {
setProject({
...project,
title: '',
writer: '',
contents: ''
})
}
const onCreate = async () => {
if(project.title === ''){
alert('title is null');
return;
}
if(project.writer === ''){
alert('wirter is null');
return;
}
if(project.contents === ''){
alert('contents is null');
return;
}
if(window.confirm('등록하시겠습니까?')){
try{
const res = await axios.post('/insert', project);
console.log(res);
window.location.href = "/list";
}catch(error){
console.log(error);
}
}
}
return (
<div className='projectRegister'>
<h2>Register Page</h2>
<div className='content'>
<input type="text" className='content-box' name='title' value={project.title} placeholder='Title' onChange={onChange}/>
<input type="text" className='content-box' name='writer' value={project.writer} placeholder='Writer' onChange={onChange}/>
<div className='contentContainer'>
<textarea type="text" className='content-box' name='contents' value={project.contents} placeholder='Contents' onChange={onChange}/>
</div>
</div>
<div className='lock-radio'>
<label for="default">
<label>
공개
<input name="default" id="default" type="radio" checked/>
</label>
<label>
비공개
<input name="default" type="radio"/>
</label>
</label>
</div>
<button onClick={onCreate}>등록</button>
<button onClick={onReset}>초기화</button>
<Link to={'/list'}><button>취소</button></Link>
</div>
);
};
export default ProjectRegister;
- project-style.css
@font-face {
font-family: '런드리고딕 Regular';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2403-2@1.0/TTLaundryGothicB.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
.projectList {
font-family: '런드리고딕 Regular';
margin: 20px;
}
.header{
position: relative;
padding-top: 30px;
}
.title{
font-family: '런드리고딕 Regular';
font-size: 40px;
/* text 그라데이션 */
background: linear-gradient(to top ,#FEAC5E, #C779D0, #4BC0C8);
color: transparent;
-webkit-background-clip: text;
margin: 10px 50px;
}
img{
position: absolute;
}
h2 {
text-align: center;
color: #667eea;
}
table {
width: 1024px;
border-collapse: collapse;
margin: 20px auto;
}
.lock{
color: gray;
}
.subTitle{
background: linear-gradient(to right top, #12c2e975, #c471ed7a, #f64f5a8c);
}
.subTitle>th{
color: #764ba2;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.center{
text-align: center;
}
th {
color: #333;
}
td>a:link{
color: #667eea;
}
td>a:visited{
color: #667eea;
}
tr:nth-child(even) {
background-color: #F9F9F9;
}
tr:hover {
background: linear-gradient(to right, rgba(135, 207, 235, 0.452), rgba(255, 192, 203, 0.479));
color: #667eea;
font-weight: 700;
}
table {
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}
.textButton{
width: 150px;
height: 60px;
color: white;
font-size: 20px;
font-weight: 700;
font-family: '런드리고딕 Regular';
/* border 그라데이션 - button */
border: 1px solid transparent;
border-radius: 20px;
background-image: linear-gradient(to right top, #12c2e975, #c471ed7a, #f64f5a8c);
background-origin: border-box;
background-clip: content-box, border-box;
}
.textButton:hover{
cursor: pointer;
transform: scale(1.05);
}
.textButton:active {
transform: scale(0.95);
}
/* ProjectDetail */
a{
text-decoration-line: none;
}
.projectDetailContainer{
font-family: '런드리고딕 Regular';
margin: 40px;
}
.projectDetailProject button{
width: 150px;
height: 60px;
color: white;
font-size: 20px;
font-weight: 700;
/* border 그라데이션 - button */
border: 1px solid transparent;
border-radius: 20px;
background-image: linear-gradient(to right top, #12c2e975, #c471ed7a, #f64f5a8c);
background-origin: border-box;
background-clip: content-box, border-box;
}
button>a{
color: white;
}
.projectDetailProject button:hover{
cursor: pointer;
transform: scale(1.05);
}
.projectDetailProject button:active {
transform: scale(0.95);
}
.projectDetailContainer{
margin: 0 auto;
padding: 30px 20px;
width: 1000px;
border: 2px solid pink;
border-radius: 15px;
color: #333;
}
.answerSpan{
border: 2px solid skyblue;
}
.projectDetailContainer>span{
padding: 20px 20px 10px 5px;
}
.projectDetailContainer>div{
margin: 20px 0 20px 0;
}
.bold{
font-size: 15px;
font-weight: bold;
}
.QA{
width: 1000px;
margin: 30px 0 5px 160px;
font-family: '런드리고딕 Regular';
font-size: 40px;
font-weight: 450;
text-align: left;
/* text 그라데이션 */
background: linear-gradient(to top ,#FEAC5E, #C779D0, #4BC0C8);
color: transparent;
-webkit-background-clip: text;
}
/* ProjectRegister */
.content{
width: 1024px;
margin: 0 auto;
/* border 그라데이션 */
border: 2px solid transparent;
border-radius: 15px;
background-image: linear-gradient(#fff, #fff), linear-gradient(to right top, #12c2e975, #c471ed7a, #f64f5a8c);
background-origin: border-box;
background-clip: content-box, border-box;
}
.content>.content-box{
font-family: '런드리고딕 Regular';
width: 150px;
height: 45px;
padding: 10px;
border: 3px solid pink;
border-radius: 20px;
outline: none;
font-size: 15px;
font-weight: 500;
margin: 10px 20px 10px 33px;
}
.content>.content-box:first-child{
margin-left: 34px;
}
.content>.content-box:nth-child(2){
width: 250px;
border: 3px solid skyblue;
}
.content>div>.content-box{
font-family: '런드리고딕 Regular';
width: 800px;
height: 300px;
padding: 10px;
border: 1px solid pink;
outline: none;
font-size: 15px;
font-weight: 500;
margin: 10px;
}
.contentContainer{
background-color: rgba(255, 192, 203, 0.322)
}
.projectRegister button{
font-family: '런드리고딕 Regular';
width: 150px;
height: 60px;
color: white;
margin-top: 20px;
font-size: 20px;
font-weight: 700;
/* border 그라데이션 - button */
border: 1px solid transparent;
border-radius: 20px;
background-image: linear-gradient(to right top, #12c2e975, #c471ed7a, #f64f5a8c);
background-origin: border-box;
background-clip: content-box, border-box;
}
.projectRegister button:hover{
cursor: pointer;
transform: scale(1.05);
}
.projectRegister button:active {
transform: scale(0.95);
}
/* ProjectAnswer */
.projectDetailProject{
width: 1280px;
margin: 50px auto;
}
.projectDetailProject>.projectDetailContainer>.contentContainer{
width: 800px;
height: 150px;
padding: 10px;
color: #333;
border: 1px solid pink;
outline: none;
font-size: 15px;
font-weight: 500;
margin: 10px;
}
.projectDetailContainer>.contentContainer{
background-color: rgba(255, 192, 203, 0.322)
}
.projectDetailProject>.projectDetailContainer2>.contentContainer2{
width: 800px;
height: 100px;
padding: 10px;
color: #333;
border: 1px solid skyblue;
outline: none;
font-size: 15px;
font-weight: 500;
margin: 10px;
}
.projectDetailContainer2>.contentContainer2{
background-color: rgb(135, 206, 235, 0.322)
}
.projectDetailContainer2{
margin: 20px auto;
padding: 30px 20px;
width: 1000px;
border: 2px solid skyblue;
border-radius: 15px;
color: #333;
}
/* 여러 가지 */
.lock-radio{
width: 135px;
margin: 0 auto;
font-family: '런드리고딕 Regular';
display: flex;
justify-content: space-between;
align-items: center;
}
.lock-radio input{
accent-color: skyblue;
margin-top: 10px;
}
input[type=radio] {
zoom: 1.5;
}
- 런드리고딕 Regular.ttf
: ttf 파일을 다운로드한 후, 삽입
https://noonnu.cc/font_page/1359
=>
- package.json
{
"name": "project-app",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:5000/",
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.7.7",
"cors": "^2.8.5",
"express": "^4.21.0",
"json": "^11.0.0",
"mysql": "^2.18.1",
"nodemon": "^3.1.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
- cmd
mysql -ureact -p비밀번호
use db_react
drop table project;
create table project (
id bigint auto_increment primary key,
title varchar(30),
contents varchar(500),
writer varchar(20),
answer varchar(500) DEFAULT '아직 답변이 입력되어있지 않습니다',
lock_type varchar(10) DEFAULT '공개',
reg_date timestamp DEFAULT now()
);
insert into project(title, contents, writer) values(내용들);
...
insert into project(title, contents, writer) values(내용들);
▷ 출력
'Project > AWS-React' 카테고리의 다른 글
게시판 프로젝트(2) - AWS 풀스택 과정 45일차 (0) | 2024.09.24 |
---|