Project/AWS-React

게시판 프로젝트(1) - AWS 풀스택 과정 44일차

awspspgh 2024. 9. 23. 18:17

※ 실행 준비 과정

- 파일을 하나 만든후 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