앞선 <nodemailer를 통해 이메일 인증 구현> 게시글에서 언급했지만 노드 리액트 기초 강의 이후부터는 John Ahn님이 제공해주시는 보일러 플레이트 틀 바탕으로 코드를 수정한 뒤 진행된다. 

server>routes폴더 안에 파일들을 보일러 플레이트에서 가져오고 index.js 파일과 프론트에서 백으로 api 요청하는 부분을 알맞게 수정했다.

그 외 나머지는 대부분 노드 리액트 기초 강의 그대로 사용했다. (but 부분적으로 코드 수정 필요)

↓참고 사이트

https://github.com/jaewonhimnae/react-shop-app

 

 

게시글 작성 · 정렬 · 검색 · 상세보기 등과 같이 게시글과 관련된 기초적인 부분은 John Ahn님의 Online Shopping Mall Clone 강의를 참고하여 진행했다. 

↓참고 사이트

https://www.youtube.com/playlist?list=PL9a7QRYt5fqlWKC_wejtfUHYb9uyS8Cxw 

 

Online Shop Clone

 

www.youtube.com

 

 

UploadPage

 

게시글 작성 페이지

 

가장 먼저 할 일은 위 이미지와 같은 게시글 작성 페이지를 만드는 것이다.

client>src>components>views>UploadTemplatePage 폴더를 만들고 그 안에 UploadTemplatePage.js 파일을 생성한다.

그리고나서 rfce를 입력해서 functional component를 생성한다.

(내가 진행한 프로젝트가 태블릿 템플릿 공유 웹사이트이기 때문에 강의에서 Product라고 나오는 부분을 모두 Template으로 치환해서 작성했다. )

 

 

게시글 작성 페이지에 접근할 수 있게 App.js에 Route path를 작성한다. 

// App.js


import UploadTemplatePage from './views/UploadTemplatePage/UploadTemplatePage'


function App() {
	return (
    	<Suspense fallback={(<div>Loading...</div>)}>
        	<NavBar />
            <div style={{ paddingTop: '75px', minHeight: 'calc(100vh - 80px)' }}>
            	<Switch>
                	...
                	<Route exact path="/template/upload" component={ Auth(UploadTemplatePage, true) } />
                	...
                </Switch>
            </div>
            <Footer />
        </Suspense>
    );
}

export default App;

 

 

그런 다음 상단 바(NavBar)에 게시글 작성 페이지로 이동할 수 있는 메뉴를 만든다. 

참고로 VSC에서 빠르게 해당 파일로 이동하려면 Ctrl + P를 누르고 파일 이름을 입력하면 된다.

// client>src>components>views>NavBar>Sections>RightMenu.js

...

return (
    <Menu mode={props.mode}>
    	<Menu.Item key="upload">
        	<a href="/product/upload">Upload</a>
        </Menu.Item>
		<Menu.Item key="logout">
        	<a onClick={logoutHandler}>Logout</a>
        </Menu.Item>
    </Menu>
)

...

 

 

이제 아까 생성했던 UploadTemplatePage.js에 코드를 작성해야 하는데, John Ahn님의 강의에서 알려주는 코드를 바탕으로 현재 진행중인 프로젝트에 맞게 코드를 수정 및 추가했다. 

디자인적인 부분은 노드 리액트 기초 강의에서 언급되었던 ant design을 사용하여 적용했다. 

// UploadTemplatePage.js

import React, { useState } from 'react';
import { Typography, Button, Form, Input, message } from 'antd';
import axios from 'axios';
import { 
    Categories, Detail_1, Detail_2, Detail_3, Detail_4, Detail_5, Detail_6 
} from './Sections/Datas';


const { Title } = Typography;
const { TextArea } = Input;

var Detail = Detail_1;

function UploadTemplatePage() {
    const [TitleValue, setTitleValue] = useState("")
    const [DescriptionValue, setDescriptionValue] = useState("")
    const [CategoryValue, setCategoryValue] = useState(1)
    const [DetailValue, setDetailValue] = useState(1)
    
    const onTitleChange = (event) => {
        setTitleValue(event.currentTarget.value)
    }
    const onDescriptionChange = (event) => {
        setDescriptionValue(event.currentTarget.value)
    }
    
    const onCategorySelectChange = (event) => {
        setCategoryValue(event.currentTarget.value)
        switch (event.currentTarget.value) {
            case '1': 
                Detail = Detail_1.slice();
                setDetailValue(1);
                break;
            case '2': 
                Detail = Detail_2.slice();
                setDetailValue(1);
                break;
            case '3': 
                Detail = Detail_3.slice();
                setDetailValue(1);
                break;
            case '4': 
                Detail = Detail_4.slice();
                setDetailValue(1);
                break; 
            case '5': 
                Detail = Detail_5.slice();
                setDetailValue(1);
                break; 
            case '6': 
                Detail = Detail_6.slice();
                setDetailValue(1);
                break; 
            default:
                break;
        }
    }
    const onDetailSelectChange = (event) => {
        setDetailValue(event.currentTarget.value)
    }

   
    return (
        <div style={{ maxWidth: '700px', margin: '2rem auto' }}> 
            <div style={{ textAlign:'center', marginBottom:'2rem' }}>
                <Title level={2}>Upload Template</Title>
            </div>
            <Form onSubmit >
                {/*DropZone*/}
                
                <br/>
                <br/>
                
                <label>Title</label>
                <Input
                    onChange={onTitleChange}
                    value={TitleValue}
                    style={{marginTop:'8px'}}
                />
                <br/>
                <br/>
                
                <label>Description</label>
                <TextArea
                    onChange={onDescriptionChange}
                    value={DescriptionValue}
                    style={{marginTop:'8px'}}
                />
                
                <br/>
                <br/>
                
                <label>Category</label>
                <select onChange={onCategorySelectChange}>
                	{Categories.map(item => (
                		<option key={item.key} value={item.key}>{item.value}</option>
                  	))}
                </select>
                
                <label>Detail</label>
                <select onChange={onDetailSelectChange}>
                	{Detail.map(item => (
                    	<option key={item.key} value={item.key}>{item.value}</option>
                   	))}
                </select>
                <br/>
                <br/>
              
                <Button>
                    Submit
                </Button>
            </Form>
        </div>
    )
}

export default UploadTemplatePage

 

 

그리고 UploadTemplatePage 폴더 안에 Sections 폴더를 하나 만들고 Datas.js 파일을 생성한다. 

이 파일에서는 카테고리와 세부 카테고리 정보를 관리한다. 

// client>src>components>views>UploadTemplatePage>Sections>Datas.js

const Categories = [
    { key:1, value: "다이어리" },
    { key:2, value: "플래너" },
    { key:3, value: "노트" },
    { key:4, value: "라이프" },
    { key:5, value: "스티커" },
    { key:6, value: "기타" },
]
const Detail_1 = [
    { key:1, value: "날짜형" },
    { key:2, value: "만년형" },
    { key:3, value: "일기장" },
]
const Detail_2 = [
    { key:1, value: "먼슬리" },
    ...
]
const Detail_3 = [
    { key:1, value: "줄" },
    ...
]
const Detail_4 = [
    { key:1, value: "가계부" },
    ...
]
const Detail_5 = [
    { key:1, value: "메모지" },
    ...
]
const Detail_6 = [
    { key:1, value: "트래커" },
    ...
]


export {
    Categories, 
    Detail_1,
    Detail_2,
    Detail_3,
    Detail_4,
    Detail_5,
    Detail_6
}

 

 

코드를 저장하고 터미널 창에 npm run dev를 입력해서 실행시키면 다음과 같이 간단하게 제목, 설명을 입력하고 카테고리와 세부 카테고리를 선택할 수 있는 게시글 작성 페이지가 뜨는 것을 확인할 수 있다. 

UploadTemplatePage

 

 

FileUpload

파일 업로드 기능 구현을 위해 먼저 백엔드 부분을 작성해보겠다. 

server폴더 안에 있는 index.js에 다음 코드를 추가하고 routes폴더에 template.js 파일을 생성한다.

// server > index.js 

app.use('/api/template', require('./routes/template'));

 

 

클라이언트에서 이미지를 업로드하면 백엔드에서 이 이미지를 받아 노드 서버에 저장해야 하는데 이때 Multer 라이브러리가 필요하기 때문에 npm install multer --save를 통해 다운받는다. 

 

그런 다음, template.js에 다음 코드를 추가하고 이미지 파일을 저장할 uploads라는 폴더를 root directory에 생성한다. 

// template.js

const express = require('express');
const router = express.Router();
const multer = require('multer');
const { auth } = require("../middleware/auth");


var storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'uploads/')
    }, //파일 저장 위치
    filename: (req, file, cb) => {
        cb(null, `${Date.now()}_${file.originalname}`)
    }, //파일 이름 ex)1126201_hello.png
    fileFilter: (req, file, cb) => {
        const ext = path.extname(file.originalname)
        if (ext !== '.jpg' || ext !== '.png') { //jpg와 png 파일만 허용, 다른 파일 확장자 추가 가능
            return cb(res.status(400).end('only jpg, png are allowed'), false)
        }
        cb(null, true)
    }
})

var upload = multer({ storage: storage }).single("file")


router.post("/uploadImage", auth, (req, res) => {
    upload(req, res, err => {
        if(err) return res.json({ success: false, err })
        return res.json({ success: true, image: res.req.file.path, fileName: res.req.file.filename })
    	//저장에 성공하면 이미지 경로와 파일 이름이 클라이언트로 전달됨
    })
});

module.exports = router;

 

 

 

이제 클라이언트에서 파일 업로드 페이지를 만들기에 앞서 client>src>components>utils 폴더를 하나 생성한다. 

이 폴더에는 자주 사용되는 기능들을 구현한 스크립트 파일들을 모아두어 다른 js파일에서 쉽게 import하여 사용할 수 있게 한다. 

FileUpload.js도 다른 곳에서 사용할 수 있기 때문에 utils 폴더에 생성하여 코드를 작성할 것이다. 

 

 

FileUpload.js를 생성하고나면 rfce를 입력하여 functional component를 생성한다. 

그리고나서 터미널 창에 cd client 입력후 npm install react-dropzone --save를 통해 react-dropzone을 설치한다. 

이제 FileUpload.js에 다음 코드를 작성한다. 

// FileUpload.js


import React, { useState } from 'react';
import DropZone from 'react-dropzone';
import { PlusOutlined } from '@ant-design/icons';
import axios from 'axios';

function FileUpload(props) {
    const [Images, setImages] = useState([]) //배열을 사용하는 이유는 여러 이미지를 저장할 것이기 때문

    const onDrop=(files) => {
        let formData = new FormData();
        const config = {
            header: {'content-type': 'multipart/form-data'}
        }
        formData.append("file", files[0])
        
        //선택한 이미지를 노드 서버에 저장 
        axios.post('/api/template/uploadImage', formData, config)
        .then(response => {
            if(response.data.success) {
               setImages([...Images, response.data.image]) // ...Images는 기존 이미지 배열, response.data.image는 새 이미지
               props.refreshFunction([...Images, response.data.image]) //UploadTemplatePage.js에 있는 updateImages() 실행시킴
            } else {
                alert('이미지 저장에 실패했습니다.')
            }
        })
        
    }

    const onDelete = (image) => {
        const currentIndex = Images.indexOf(image) //선택한 이미지의 index

        let newImages = [...Images] //현재 이미지 배열
        newImages.splice(currentIndex, 1) //splice를 통해 선택한 이미지를 배열에서 잘라내기

        setImages(newImages)
        props.refreshFunction(newImages)
    }


    return (
        <div style={{ display: 'flex', justify-content: 'space-between' }}>
            <DropZone
                onDrop={onDrop}
                multiple={false} //이미지를 한 번에 하나씩 등록
                maxSize={80000000}
            >
                {({getRootProps, getInputProps}) => (
                    <div style={{ width:'300px', height:'240px', border:'1px solid lightgray', 
                    display:'flex', alignItems:'center', justifyContent:'center' }}
                        {...getRootProps()}
                    >
                        <input {...getInputProps()}/>
                        <PlusOutlined style={{ fontSize:'3rem' }} />
                    </div>
                )}
            </DropZone>
            
            <div style={{ display:'flex', width:'350px', height:'240px', overflowX:'scroll' }}>
            	{Images.map((image, index) => (
                    <div onClick={()=>onDelete(image)} key={index}>
                    	<img style={{ minWidth:'300px', width:'300px', height:'240px' }}
                            src={`http://localhost:5000/${image}`} alt={`templateImg-${index}`} />
                    </div>
                ))} 
            </div>
        </div>
    )
}

export default FileUpload

 

splice 함수에 대한 정리는 아래 '[Javascript] splice' 함수 글에서 확인할 수 있다. 

 

[Javascript] splice 함수

초기화 let arr = ['a', 'b', 'c', 'd'] 삽입 arr.splice(0, 0, 'f')​ arr.splice(5, 0, 'e') → 5 이상의 수를 넣어도 결과는 같음 제거 arr.splice(0, 2)​

sungeun97.tistory.com

 

 

UpdateTemplatePage.js에는 다음 코드를 추가한다. 

// UploadTemplatePage.js

import React, { useState } from 'react';
import axios from 'axios';
...
import FileUpload from '../../utils/FileUpload';

...

function UploadTemplatePage() {
    const [Images, setImages] = useState([])
    
    ...
    
    
    const updateImages = (newImages) =>{
        setImages(newImages)
    }
    

    return (
        <div style={{ maxWidth: '700px', margin: '2rem auto' }}> 
            <div style={{ textAlign:'center', marginBottom:'2rem' }}>
                <h2>Upload Template</h2>
            </div>
            <Form onSubmit >
                {/*DropZone*/}
                <FileUpload refreshFunction={updateImages}/>
                <br/>
                <br/>
                
                ...
                
                <div style={{textAlign:'center', marginTop:'2%', marginBottom:'30px'}}>
                <Button>
                    Submit
                </Button>
                </div>
            </Form>
        </div>
    )
}

export default UploadTemplatePage

 

refreshFunction

fileUpload.js의

props.refreshFunction([...Images, response.data.image])

을 예로 설명하자면, 기존 이미지들의 배열인 ...Images에 새로 업로드한 이미지인 response.data.image를 추가한 배열을

 

refreshFunction을 통해 UploadTemplatePage.js에 있는 updateImages()에 인수로 넘겨주고,

<FileUpload refreshFunction={updateImages}/>

 

이 updateImages()는 넘겨 받은 배열을 UpdateTemplatePage.js에 있는 Images배열에 저장한다. 

const updateImages = (newImages) =>{
    setImages(newImages)
}

 

참고로 refreshFunction은 다른 이름으로 바꿔서 사용해도 무관한다. 

 

 

 

MongoDB에 저장하기

작성한 게시글을 MongoDB에 저장하기 위해서 먼저 백엔드에 User 모델처럼 Template 모델을 만들어야 한다. 

Template 모델은 server폴더에 있는 models 폴더 안에 Template.js파일을 하나 생성해서 만든다. 

// server > models > Template.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const templateSchema = mongoose.Schema({
    writer: {
        type: Schema.Types.ObjectId,
        ref: 'User' //User model에서 user에 대한 모든 정보를 가져올 수 있음
    },
    title: {
        type: String, 
        maxlength: 50
    },
    description: {
        type: String,
    },
    category: {
        type: Number,
        default: 1
    },
    detail: {
        type: Number,
        default: 1
    },
    images: {
        type: Array,
        default: []
    },
    downloads: {
        type: Number,
        maxlength: 1000,
        default: 0
    },
    views: {
        type: Number,
        default: 0
    }
}, {timestamps: true}) //timestamps를 통해서 생성 및 갱신 시간을 알 수 있음


const Template = mongoose.model('Template', templateSchema)

module.exports = { Template }

 

 

그리고나서 server>routes>template.js에 다음 코드를 추가한다. 

// server > routes > template.js

const { Template } = require("../models/Template");

...

router.post("/uploadTemplate", auth, (req, res) => {
    //클라이언트에서 받은 모든 정보를 DB에 저장
    const template = new Template(req.body)
    template.save((err) => {
        if(err) return res.status(400).json({ success: false, err })
        return res.status(200).json({ success: true })
    })
});

...

 

 

클라이언트 부분으로 넘어와 UploadTemplatePage.js에 onSubmit 함수를 작성한다. 

// UploadTemplatePage.js


...

function UploadTemplatePage(props) {
    
    ...
    
    
    const onSubmit = (event) => {
        event.preventDefault()

        if(!TitleValue || !DescriptionValue || Images.length == 0) { // 입력하지 않은 영역이 있는 경우
            return alert('Fill all the fields first!')
        }

        const variables = {
            //Redux를 통해 가져온 로그인된 유저의 정보를 props.user.userData로 접근하여 사용할 수 있다.
            writer: props.user.userData._id,  
            title: TitleValue,
            description: DescriptionValue,
            images: Images,
            category: CategoryValue,
            detail: DetailValue,
        }

        axios.post('/api/template/uploadTemplate', variables)
            .then(response => {
                if(response.data.success) {
                    alert('Template Successfully Uploaded')
                    props.history.push('/')
                } else {
                    alert('Failed to upload Template')
                }
            })
    }

    return (
        <div style={{ maxWidth: '700px', margin: '2rem auto' }}> 
            
            ...
            
            <Form onSubmit={onSubmit} >
                
                ...
                
                <div style={{textAlign:'center', marginTop:'2%', marginBottom:'30px'}}>
                <Button
                    onClick={onSubmit}
                >
                    Submit
                </Button>
                </div>
            </Form>
        </div>
    )
}

export default UploadTemplatePage

 

 

 

이제 실행시켜서 게시글 작성을 완성한 뒤 Submit 버튼을 누르면 다음과 같이 'Template Successfully Uploaded' 알림창이 뜨고, 확인 버튼을 누르면 Landing Page로 이동한다. 

게시글 작성 완료

 

 

MongoDB로 넘어와 templates를 확인해보니 성공적으로 게시글 정보가 저장된 것을 확인할 수 있었다. 

MongoDB - templates

 

 

+ Recent posts