Stack/React

[React] Lamp 쇼핑몰 구현하기 5 (이미지 업로드 / 상품등록 기능 구현)

7ingout 2022. 7. 4. 12:45

LAMP-SHOPPING-CLIENT

1. customHook 폴더 생성 후 useAsync.js 파일 생성

customHook/useASync.js

import { useReducer, useEffect, useCallback } from "react"
const initialState = {
    loading: false,
    data: null,
    error: null
}
// 로딩중? 데이터 받기 성공? 데이터 받기 실패
// LOADING , SUCCESS, ERROR
function reducer(state, action){
    switch(action.type) {
        case "LOADING":
            return {
                loading: true,
                data: null,
                error: null
            };
            case "SUCCESS":
            return {
                loading: false,
                data: action.data,
                error: null
            }
            case "ERROR":
            return {
                loading: false,
                data: null,
                error: action.error
            }
            default:
            return state;
    }
}
function useAsync(callback, deps=[]) {
    const [state, dispatch] = useReducer(reducer, initialState);
    const fetchDate = async () => {
        dispatch({type: "LOADING"});
        try {
            const data = await callback();
            dispatch({
                type: "SUCCESS",
                data: data
            })
        }
        catch(e){
            dispatch({
                type: "ERROR",
                error: e
            })
        }
    }
    useEffect(()=>{
        fetchDate();
    }, deps);
    return [state, fetchDate];
}
export default useAsync

 

main/index.js

import React from 'react';
import './index.scss';
import axios from 'axios';
// import useAsync from './useAsync';
import useAsync from '../customHook/useAsync';
import MainProduct from './MainProduct';

// 비동기
async function getProducts() {
    const response = await axios.get("http://localhost:3000/products");
    return response.data;
}

const MainPage = (props) => {
    // * 쌤 (비동기)
    const [state, refetch ] = useAsync(getProducts, [])
    const { loading, data:products, error} = state;
    if(loading) return <div>로딩중 ......</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!products) return <div>로딩중입니다.</div>

    // * 김효진 (비동기)
    // const [ state ] = useAsync(getProducts);
    // const { loading, data:products, error } = state;
    // if(loading) return <div>로딩중...</div>
    // if(error) return <div>에러가 발생했습니다.</div>
    // if(!products) return <div>상품이 없습니다.</div>;
        
    // 원래 받아오던 방법 (비동기 x)
    // const [ products, setProducts ] = useState([]);
    // useEffect(()=>{
    //     axios.get("http://localhost:3000/products")
    //     .then((result)=>{
    //         const products = result.data;
    //         setProducts(products);
    //     }).catch((e)=>{
    //         console.log(e);
    //     })
    // }, [])
    // if(products === [] ) return <div>로딩중입니다.</div>

    return (
        <div>
            <div id="main">
                <div id="banner">
                     {/* 바로 images 하면 public에 있는 images 들어감 */}
                    <img src="images/banners/banner1.png" alt="" />
                </div>
                <div id="product-list" className='inner'>
                    <h2>그린조명 최신상품</h2>
                    <div id="product-items">
                        {/* 나중에 map 이용해서 밑에꺼 8개 뿌려줄거임 */}
                        {products.map(product => <MainProduct key = {product.id} product={product} />)}   
                    </div>
                </div>
            </div>
        </div>
    );
};
export default MainPage;

 

2. product/index.js

import React from 'react';
import "./product.scss"
import axios from 'axios';
import { useParams } from 'react-router-dom';
import useAsync from '../customHook/useAsync'

async function getProduct(id) {
    const response = await axios.get(`http://localhost:3000/product/${id}`)
    return response.data;
}

const ProductPage = (props) => {

    // product /1
    const { id } = useParams();
    const [state] = useAsync(()=>getProduct(id), [id]);
    const { loading, data:product, error } = state;
    if(loading) return <div>로딩중입니다.....</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!product) return null;

    // useEffect(비동기 x)
    // const [ product, setProduct ] = useState(null);
    // // useParams() 실행되면 파라미터 값을 가지고 있는 객체를 반환
    // // product/1
    // const { id } = useParams();
    // // useEffect(function(){
    // //     axios.get(`http://localhost:3000/product/${id}`)
    // //     .then(result=> {
    // //         console.log(result);
    // //         const data = result.data;
    // //         setProduct(data);
    // //     })
    // //     .catch(e=> {
    // //         console.log(e);
    // //     })
    // // }, []) // 빈 배열 넣어줘야 마운트 될 때 한번만 시행
    // // if(!product) return <div>로딩중입니다...</div>

    return (
        <div className='inner'>
            <div id="image-box">
                <img src={product.imageUrl} alt =""/>
            </div>
            <div id="profile-box">
                <ul>
                    <li>
                        <div>
                            <img src="/images/icons/avatar.png" alt=""/>
                            <span>{product.seller}</span>
                        </div>
                    </li>
                    <li>
                    {product.name}
                    </li>
                    <li>
                        가격 {product.price}
                    </li>
                    <li>등록일 2022년 6월 2일</li>
                    <li>상세설명</li>
                </ul>
            </div>
        </div>
    );
};

export default ProductPage;

 

LAMP-SHOPPING-SERVER

3. server.js 에 상품등록 부분 추가하기

// 상품 등록
app.post("/products", (req, res)=> {
    // http body에 있는 데이터
    const body = req.body;
    // body객체에 있는 값을 각각 변수에 할당
    const { name, price, seller, imageUrl } = body;
    if(!name || !price || !seller) {
        res.send("모든 필드를 입력해주세요");
    }
    // Product테이블에 레코드를 삽입
    else {
        models.Product.create({
            name,
            price,
            seller,
            imageUrl
        }).then(result=>{
            console.log("상품 생성 결과 :", result);
            res.send({
                result
            })
        })
        .catch(e=>{
            console.error(e);
            res.send("상품 업로드에 문제가 생겼습니다.")
        })
    }
    
})

 

4. postMan으로 server 검사

잘 추가됨
NOT NULL 항목 NULL로 보낸 경우 모든 필드를 입력해주세요 뜸

 

5. server.js에 multer 선언

이미지 등록 부분 추가

// 업로드 이미지를 관리하는 스토리지 서버를 연결 -> multer를 사용하겠다
const multer = require("multer");
// 이미지 파일이 요청 오면 어디에 저장할건지 지정
const upload = multer({ 
    storage: multer.diskStorage({
        destination: function(req, file, cb) {
            // 어디에 저장할거냐? upload/
            cb(null, 'upload/')
        },
        filename: function(req, file, cb){
            // 어떤 이름으로 저장할거야?
            // file 객체의 오리지널 이름으로 저장하겠다.
            cb(null, file.originalname)
        }
    })
 })
// 이미지파일이 post로 요청이 왔을 때 upload라는 폴더에 이미지를 저장하기
// 이미지가 하나일 때 single
app.post('/image', upload.single('image'), (req, res)=>{
    const file = req.file;
    console.log(file);
    res.send({
        imageUrl: file.path
    })
})

postman으로 이미지 잘 업로드 되는거 확인!

 

LAMP-SHOPPING-CLIENT

6.upload/index.js

import React, { useState } from 'react';
import './upload.scss';
import 'antd/dist/antd.css';
import { Form, Divider, Input, InputNumber, Button, Upload } from 'antd';

const Uploadpage = (props) => {
    // 이미지 경로 상태관리 추가
    const [imageUrl, setImageUrl ] = useState(null);
    // 이미지 처리함수
    const onChangeImage = (info) => {
        // 파일이 업로드 중일 때
        console.log(info.file)
        if(info.file.status === "uploading"){
            return;
        }
        // 파일이 업로드 완료 되었을 때
        if(info.file.status === "done") {
            const response = info.file.response;
            const imageUrl = response.imageUrl;
            // 받은 이미지경로를 imageUrl에 넣어줌
            setImageUrl(imageUrl);
        }
    }
    return (
        <div id="upload-container" className='inner'>
            <Form name="productUpload">
                <Form.Item name="imgUpload"
                    label={<div className='upload-label'>상품사진</div>}>
                    <Upload name="image" action="http://localhost:3000/image"
                    listType="picture" showUploadList={false} onChange={onChangeImage}>

                        {/* 업로드 이미지가 있으면 이미지를 나타내고 업로드 이미지가 없으면
                        회색배경에 업로드 아이콘이 나타나도록 ... */}
                    { imageUrl ? <img src={imageUrl} 
                    alt="" width= "200px" height= "200px" /> : 
                            (<div id="upload-img-placeholder">
                            <img src="images/icons/camera.png" alt="" />
                            <span>이미지를 업로드 해주세요.</span>
                    </div>)}    
                    </Upload>
                </Form.Item>
                <Divider/>
                <Form.Item name="seller" 
                    label={<div className='upload-label'>판매자명</div>}>
                    <Input className="nameUpload" size='large'
                    placeholder='판매자 이름을 입력하세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="name"
                label={<div className='upload-label'>상품이름</div>}>
                    <Input
                        className='upload-name'
                        size='large'
                        placeholder='상품 이름을 입력해주세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="price"
                label={<div className='upload-label'>상품가격</div>}>
                    <InputNumber defaultValue={0} size="large"/>
                </Form.Item>
                <Divider/>
                <Form.Item name="description"
                label={<div className='upload-label'>상품소개</div>}>
                <Input.TextArea
                    size='large'
                    id = "product-description"
                    maxLength={300}
                    placeholder="상품 소개를 적어주세요"
                />
                </Form.Item>
                <Form.Item>
                    <Button id="submit-button" size="large" htmlType='submit'>
                        상품등록하기
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
};

export default Uploadpage;

여기서 사진 업로드하면
server 리액트에 추가된다

 

7.upload/index.js

서버로 데이터 전송하는 부분 / 리다이랙션 구현

import React, { useState } from 'react';
import './upload.scss';
import 'antd/dist/antd.css';
import { Form, Divider, Input, InputNumber, Button, Upload } from 'antd';
// import useAsync from '../customHook/useAsync'
import axios from "axios";
import { useNavigate } from 'react-router-dom';

// async function postProduct(values){
//     const response = await axios.post(`http://localhost:3000/products`, {
//         name: values.name,
//         seller: values.seller,
//         price: values.price,
//         imageUrl: imageUrl // 상태관리 되고 있는 imageurl
//     });
//     return response.data;
// }
const Uploadpage = (props) => {
    const navigate = useNavigate();

    // 이미지 경로 상태관리 추가
    const [imageUrl, setImageUrl ] = useState(null);
    // 이미지 처리함수
    const onChangeImage = (info) => {
        // 파일이 업로드 중일 때
        console.log(info.file)
        if(info.file.status === "uploading"){
            return;
        }
        // 파일이 업로드 완료 되었을 때
        if(info.file.status === "done") {
            const response = info.file.response;
            const imageUrl = response.imageUrl;
            // 받은 이미지경로를 imageUrl에 넣어줌
            setImageUrl(imageUrl);
        }
    }
    const onSubmit = (values) => {
        // 서버로 데이터 전송하기
        axios.post("http://localhost:3000/products", {
            name: values.name,
            seller: values.seller,
            price: values.price,
            imageUrl: imageUrl
        }).then((result)=>{
            console.log(result)
            navigate("/");
        })
        .catch(e=>{
            console.log(e);
        })
    }
    return (
        <div id="upload-container" className='inner'>
            <Form name="productUpload" onFinish={onSubmit}>
                <Form.Item name="imgUpload"
                    label={<div className='upload-label'>상품사진</div>}>
                    <Upload name="image" action="http://localhost:3000/image"
                    listType="picture" showUploadList={false} onChange={onChangeImage}>

                        {/* 업로드 이미지가 있으면 이미지를 나타내고 업로드 이미지가 없으면
                        회색배경에 업로드 아이콘이 나타나도록 ... */}
                    { imageUrl ? <img src={imageUrl} 
                    alt="" width= "200px" height= "200px" /> : 
                            (<div id="upload-img-placeholder">
                            <img src="images/icons/camera.png" alt="" />
                            <span>이미지를 업로드 해주세요.</span>
                    </div>)}    
                    </Upload>
                </Form.Item>
                <Divider/>
                <Form.Item name="seller" 
                    label={<div className='upload-label'>판매자명</div>}>
                    <Input className="nameUpload" size='large'
                    placeholder='판매자 이름을 입력하세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="name"
                label={<div className='upload-label'>상품이름</div>}>
                    <Input
                        className='upload-name'
                        size='large'
                        placeholder='상품 이름을 입력해주세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="price"
                label={<div className='upload-label'>상품가격</div>}>
                    <InputNumber defaultValue={0} size="large"/>
                </Form.Item>
                <Divider/>
                <Form.Item name="description"
                label={<div className='upload-label'>상품소개</div>}>
                <Input.TextArea
                    size='large'
                    id = "product-description"
                    maxLength={300}
                    placeholder="상품 소개를 적어주세요"
                />
                </Form.Item>
                <Form.Item>
                    <Button id="submit-button" size="large" htmlType='submit'>
                        상품등록하기
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
};

export default Uploadpage;

 

8. 데이터베이스 삭제 후 description 컬럼 추가

LAMP-SHOPPING-CLIENT

models/products.js

// Common.js 구문 내보내기
// module.exprors
// 테이블을 모델링하는 파일
module.exports = function (sequelize, DataTypes) {
    // 컬럼 name, price, imageUrl, seller
    // 제약조건 allowNull: 컬럼의 값이 없어도 되는지 여부 (default: true)
    const product = sequelize.define('Product', {
        name: {
            type: DataTypes.STRING(20),
            allowNull: false
        },
        price: {
            type: DataTypes.INTEGER(20),
            allowNull: false
        },
        imageUrl: {
            type: DataTypes.STRING(500),
        }, 
        seller: {
            type: DataTypes.STRING(200),
            allowNull: false
        },
        description: {
            type: DataTypes.STRING(500),
            allowNull: false
        }
    });
    return product;
}

 

upload/index.js

import React, { useState } from 'react';
import './upload.scss';
import 'antd/dist/antd.css';
import { Form, Divider, Input, InputNumber, Button, Upload } from 'antd';
// import useAsync from '../customHook/useAsync'
import axios from "axios";
import { useNavigate } from 'react-router-dom';

// async function postProduct(values){
//     const response = await axios.post(`http://localhost:3000/products`, {
//         name: values.name,
//         seller: values.seller,
//         price: values.price,
//         imageUrl: imageUrl // 상태관리 되고 있는 imageurl
//     });
//     return response.data;
// }
const Uploadpage = (props) => {
    const navigate = useNavigate();

    // 이미지 경로 상태관리 추가
    const [imageUrl, setImageUrl ] = useState(null);
    // 이미지 처리함수
    const onChangeImage = (info) => {
        // 파일이 업로드 중일 때
        console.log(info.file)
        if(info.file.status === "uploading"){
            return;
        }
        // 파일이 업로드 완료 되었을 때
        if(info.file.status === "done") {
            const response = info.file.response;
            const imageUrl = response.imageUrl;
            // 받은 이미지경로를 imageUrl에 넣어줌
            setImageUrl(imageUrl);
        }
    }
    const onSubmit = (values) => {
        // 서버로 데이터 전송하기
        axios.post("http://localhost:3000/products", {
            name: values.name,
            seller: values.seller,
            price: values.price,
            imageUrl: imageUrl,
            description: values.description
        }).then((result)=>{
            console.log(result)
            navigate("/");
        })
        .catch(e=>{
            console.log(e);
        })
    }
    return (
        <div id="upload-container" className='inner'>
            <Form name="productUpload" onFinish={onSubmit}>
                <Form.Item name="imgUpload"
                    label={<div className='upload-label'>상품사진</div>}>
                    <Upload name="image" action="http://localhost:3000/image"
                    listType="picture" showUploadList={false} onChange={onChangeImage}>

                        {/* 업로드 이미지가 있으면 이미지를 나타내고 업로드 이미지가 없으면
                        회색배경에 업로드 아이콘이 나타나도록 ... */}
                    { imageUrl ? <img src={imageUrl} 
                    alt="" width= "200px" height= "200px" /> : 
                            (<div id="upload-img-placeholder">
                            <img src="images/icons/camera.png" alt="" />
                            <span>이미지를 업로드 해주세요.</span>
                    </div>)}    
                    </Upload>
                </Form.Item>
                <Divider/>
                <Form.Item name="seller" 
                    label={<div className='upload-label'>판매자명</div>}>
                    <Input className="nameUpload" size='large'
                    placeholder='판매자 이름을 입력하세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="name"
                label={<div className='upload-label'>상품이름</div>}>
                    <Input
                        className='upload-name'
                        size='large'
                        placeholder='상품 이름을 입력해주세요'/>
                </Form.Item>
                <Divider/>
                <Form.Item name="price"
                label={<div className='upload-label'>상품가격</div>}>
                    <InputNumber defaultValue={0} size="large"/>
                </Form.Item>
                <Divider/>
                <Form.Item name="description"
                label={<div className='upload-label'>상품소개</div>}>
                <Input.TextArea
                    size='large'
                    id = "product-description"
                    maxLength={300}
                    placeholder="상품 소개를 적어주세요"
                />
                </Form.Item>
                <Form.Item>
                    <Button id="submit-button" size="large" htmlType='submit'>
                        상품등록하기
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
};

export default Uploadpage;

 

product/index.js

import React from 'react';
import "./product.scss"
import axios from 'axios';
import { useParams } from 'react-router-dom';
import useAsync from '../customHook/useAsync'

async function getProduct(id) {
    const response = await axios.get(`http://localhost:3000/product/${id}`)
    return response.data;
}
const ProductPage = (props) => {
    // product /1
    const { id } = useParams();
    const [state] = useAsync(()=>getProduct(id), [id]);
    const { loading, data:product, error } = state;
    if(loading) return <div>로딩중입니다.....</div>
    if(error) return <div>에러가 발생했습니다.</div>
    if(!product) return null;

    // useEffect(비동기 x)
    // const [ product, setProduct ] = useState(null);
    // // useParams() 실행되면 파라미터 값을 가지고 있는 객체를 반환
    // // product/1
    // const { id } = useParams();
    // // useEffect(function(){
    // //     axios.get(`http://localhost:3000/product/${id}`)
    // //     .then(result=> {
    // //         console.log(result);
    // //         const data = result.data;
    // //         setProduct(data);
    // //     })
    // //     .catch(e=> {
    // //         console.log(e);
    // //     })
    // // }, []) // 빈 배열 넣어줘야 마운트 될 때 한번만 시행
    // // if(!product) return <div>로딩중입니다...</div>

    return (
        <div className='inner'>
            <div id="image-box">
                <img src={product.imageUrl} alt =""/>
            </div>
            <div id="profile-box">
                <ul>
                    <li>
                        <div>
                            <img src="/images/icons/avatar.png" alt=""/>
                            <span>{product.seller}</span>
                        </div>
                    </li>
                    <li>
                    {product.name}
                    </li>
                    <li>
                        가격 {product.price}
                    </li>
                    <li>등록일</li>
                    <li>상세설명</li>
                    <li>{product.description}</li>
                </ul>
            </div>
        </div>
    );
};

export default ProductPage;

 

LAMP-SHOPPING-SERVER

server.js

const express = require("express");
const cors = require("cors");
const app = express();
const port = 3000;
const models = require('./models');

// 업로드 이미지를 관리하는 스토리지 서버를 연결 -> multer를 사용하겠다
const multer = require("multer");
// 이미지 파일이 요청 오면 어디에 저장할건지 지정
const upload = multer({ 
    storage: multer.diskStorage({
        destination: function(req, file, cb) {
            // 어디에 저장할거냐? upload/
            cb(null, 'upload/')
        },
        filename: function(req, file, cb){
            // 어떤 이름으로 저장할거야?
            // file 객체의 오리지널 이름으로 저장하겠다.
            cb(null, file.originalname)
        }
    })
 })

// json형식의 데이터를 처리할 수 있게 설정
app.use(express.json());
// 브라우저 cors 이슈를 막기 위해 사용(모든 브라우저의 요청을 일정하게 받겠다)
app.use(cors());

// upload 폴더에 있는 파일에 접근할 수 있도록 설정
app.use("/upload", express.static("upload"));
// 요청처리
// app.메서드(url, 함수)
// 이미지파일이 post로 요청이 왔을 때 upload라는 폴더에 이미지를 저장하기
// 이미지가 하나일 때 single
app.post('/image', upload.single('image'), (req, res)=>{
    const file = req.file;
    console.log(file);
    res.send({
        imageUrl: "http://localhost:3000/"+file.destination+file.filename
    })
})


app.get('/products',async(req,res)=>{
    // 데이터베이스 조회하기
    models.Product.findAll()
    .then(result=> {
        // console.log("제품전체조회", result);
        res.send(result);
    })
    .catch(e=>{
        console.error(e)
        res.send("파일 조회에 문제가 있습니다.")
    })
})
// method는 get이고 오고 url은 /product/2 로 요청이 온 경우
app.get('/product/:id', async (req, res) => {
    const params = req.params;
    // const { id } = params;
    // 하나만 조회할때는 findDone -> select문
    models.Product.findOne({
        // 조건절
        where: {
            id: params.id
        }
    })
    .then(result=>{
        res.send(result);
    })
    .catch(e=>{
        console.log(e)
        res.send("상품 조회에 문제가 생겼습니다.")
    })
    // const product = {
    //     id: id,
    //     name: "서버에서 보내는 이름",
    //     price: 50000,
    //     imgsrc:"images/products/product4.jpg",
    //     seller: "green",
    // }
    // res.send(product);
});
// app.post('/green', async (req, res)=>{
//     console.log(req);
//     res.send('그린 게시판에 게시글이 등록되었습니다.');
// });

// 상품 등록
app.post("/products", (req, res)=> {
    // http body에 있는 데이터
    const body = req.body;
    // body객체에 있는 값을 각각 변수에 할당
    const { name, price, seller, imageUrl, description } = body;
    if(!name || !price || !seller) {
        res.send("모든 필드를 입력해주세요");
    }
    // Product테이블에 레코드를 삽입
    else {
        models.Product.create({
            name,
            price,
            seller,
            imageUrl,
            description
        }).then(result=>{
            console.log("상품 생성 결과 :", result);
            res.send({
                result
            })
        })
        .catch(e=>{
            console.error(e);
            res.send("상품 업로드에 문제가 생겼습니다.")
        })
    }
    
})

// 실행
app.listen(port, ()=>{
    console.log('쇼핑몰 서버가 동작중입니다.');
    // sequelize와 데이터베이스 연결작업
    // 데이터베이스 동기화
    models.sequelize
    .sync()
    .then(()=> {
        console.log('DB연결 성공');
    })
    .catch(e=>{
        console.error(e);
        console.log('DB연결 에러');
        // 서버실행이 안되면 프로세서를 종료
        process.exit();
    })
})

description 추가됨