React

[MERN stack] 여행 사이트

낮에만코딩함 2022. 8. 22. 21:27

몽고 DB와 Express 그리고 Node.js를 활용해보기 위해서 여행 사이트를 제작해보았다.

 

내가 제작한 사이트의 완성본은 다음과 같다.

 

메인페이지
업로드 페이지

 

 

메인 페이지와 업로드 페이지만을 제작하였다. 그럼 먼저 업로드 페이지 제작 과정을 살펴보자.

 

제일 먼저 Route를 사용해준다. Route를 통해 페이지 간의 이동을 손 쉽게 제어해준다.

 

function App() {
  return (
    <Suspense fallback={(<div>Loading...</div>)}>
      <NavBar />
      <div style={{ paddingTop: '69px', minHeight: 'calc(100vh - 80px)' }}>
        <Switch>
          <Route exact path="/" component={Auth(LandingPage, null)} />
          <Route exact path="/login" component={Auth(LoginPage, false)} />
          <Route exact path="/register" component={Auth(RegisterPage, false)} />
          <Route exact path="/product/upload" component={Auth(UpLoadProductPage, true)} />
        </Switch>
      </div>
      <Footer />
    </Suspense>
  );
}

 

login과 register는 boiler-plate에서 제공하는 로그인과 가입 기능이다.

 

내가 추가한 부분은 product/upload부분으로 UploadProductPage.js에 접근이 가능하게 해준다.

 


이제 본격적으로 업로드 페이지 구조에 대해 알아보자.

 return (
        <div style={{ maxWidth: '700px', margin: '2rem auto' }}>
            <div style={{ textAlign: 'center', marginBottom: '2rem' }}>
                <h2> 여행 상품 업로드</h2>
            </div>

            <Form onSubmit={submitHandler}>
                {/* DropZone */}
                <FileUpload refreshFunction={updateImages} />

                <br />
                <br />
                <label>이름</label>
                <Input onChange={titleChangeHandler} value={Title} />
                <br />
                <br />
                <label>설명</label>
                <TextArea onChange={descriptionChangeHandler} value={Description} />
                <br />
                <br />
                <label>가격($)</label>
                <Input type="number" onChange={priceChangeHandler} value={Price} />
                <br />
                <br />
                <select onChange={continentChangeHandler} value={Continent}>
                    {Continents.map(item => (
                        <option key={item.key} value={item.key}> {item.value}</option>
                    ))}
                </select>
                <br />
                <br />
                <button type="submit">
                    확인
                </button>
            </Form>
        </div>
    )

 

return부분을 먼저 살펴보면 drpozone, title, 설명, 가격, 대륙 등을 설정하도록 하였다.

 

Dropzone부분은 FileUpload라는 컴포넌트를 따로 만들었다. 이때 updateImage라는 함수를 함께 보내 자식 컴포넌트에서 이미지가 업로드 되어도 부모 컴포넌트에서 저장가능하도록 하였다.

 

 const [Title, setTitle] = useState("")
    const [Description, setDescription] = useState("")
    const [Price, setPrice] = useState(0)
    const [Continent, setContinent] = useState(1)
    const [Images, setImages] = useState([])

    const titleChangeHandler = (event) => {
        setTitle(event.currentTarget.value)
    }

    const descriptionChangeHandler = (event) => {
        setDescription(event.currentTarget.value)
    }

    const priceChangeHandler = (event) => {
        setPrice(event.currentTarget.value)
    }

    const continentChangeHandler = (event) => {
        setContinent(event.currentTarget.value)
    }

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

 

이러한 부분들을 리엑트를 사용하여 제어해주기 위해 usestate를 모두 걸어주었다.

 

이를 통해 사용자가 입력할때마다 onChange함수가 changehandler를 호출하고 그 함수는 useState의 set으로 제어해준다.

 

const submitHandler = (event) => {
        event.preventDefault();

        if (!Title || !Description || !Price || !Continent || Images.length === 0) {
            return alert(" 모든 값을 넣어주셔야 합니다.")
        }

        //서버에 채운 값들을 request로 보낸다.

        const body = {
            //로그인 된 사람의 ID 
            writer: props.user.userData._id,
            title: Title,
            description: Description,
            price: Price,
            images: Images,
            continents: Continent
        }

        Axios.post('/api/product', body)
            .then(response => {
                if (response.data.success) {
                    alert('상품 업로드에 성공 했습니다.')
                    props.history.push('/')
                } else {
                    alert('상품 업로드에 실패 했습니다.')
                }
            })
    }

 

이 부분은 return 부분에서 Form onsubmit에 해당하는 함수이다.

 

사용자가 입력을 완료한 후에 submit을 하면 입력값들이 비어있는지 확인하고 비어있지 않다면 이를 body라는 객체에 담아서 axios를 통해 post해준다.

 

현재까지의 구조


uploadProductPage에서 post한 객체를 받기 위해서 같은 형식의 model을 만들어준다.

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

const productSchema = mongoose.Schema({
    writer: {
        type: Schema.Types.ObjectId,
        ref: 'User'
    },
    title: {
        type: String,
        maxlength: 50
    },
    description: {
        type: String,
    },
    price: {
        type: Number,
        default: 0
    },
    images: {
        type: Array,
        default: []
    },
    sold: {
        type: Number,
        maxlength: 100,
        default: 0
    },

    continents: {
        type: Number,
        default: 1
    },

    views: {
        type: Number,
        default: 0
    }
}, { timestamps: true })

productSchema.index({
    title: 'text',
    description: 'text'
}, {
    weights: {
        title: 5,
        description: 1
    }
})


const Product = mongoose.model('Product', productSchema);

module.exports = { Product }
const {Product} = require('../models/Product');

router.post('/', (req, res) => {

    //받아온 정보들을 DB에 넣어 준다.
    const product = new Product(req.body)

    product.save((err) => {
        if (err) return res.status(400).json({ success: false, err })
        return res.status(200).json({ success: true })
    })

})

받아온 객체를 product에 저장해주고 이를 DB에 모두 저장해준다.

 


이제 메인페이지의 제작 과정에 대해 알아보자.

 

메인 페이지는 uploadpage에서 DB에 저장한 데이터들을 한번에 모두 보여주는 역할을 한다.

 

먼저 메인페이지인 LadnigPage의 구조를 살펴보자.

 

  return (
        <>
            <div style={{width: "75%", margin: '3rem auto'}}>
                <div style={{ textAlign:'center'}}>
                    <h2>Let's Travel Anywhere <Icon type="rocket"/></h2>
                </div>

                <Row gutter={[16, 16]}>
                <Col lg={12} xs={24}>
                    {/* CheckBox */}
                    <Checkbox list={continents} handleFilters={filters => handleFilters(filters, "continents")} />
                </Col>
                <Col lg={12} xs={24}>
                    {/* RadioBox */}
                    <Radiobox list={price} handleFilters={filters => handleFilters(filters, "price")} />
                </Col>
                </Row>
                {}
                {}
                <Row gutter={[16,16]}>
                     {renderCards}
                </Row>

                <br />

                {PostSize >= Limit &&
                <div style={{justifyContent:'center'}}>
                    <button onClick={loadMoreHandler}>더보기</button>
                </div>
                }
            </div>
        </>
    )
}

 

function LandingPage() {

    const [Products, setProducts]= useState([])
    const [Skip, setSkip] = useState(0)
    const [Limit,setLimit] = useState(8)
    const [PostSize,setPostSize]=useState(0)
    const [Filters, setFilters] = useState({
        continents: [],
        price: []
    })
    
    useEffect(()=>{

        let body ={
            skip: Skip,
            limit:Limit
        }
        getProducts(body)
    },[])

    const getProducts = (body) =>{
        axios.post('/api/product/products',body)
        .then(response=>{
            if(response.data.success){
                if(body.loadMore){
                    setProducts([...Products, ...response.data.productInfo])
                }else{
                    setProducts(response.data.productInfo)
                }
                setPostSize(response.data.PostSize)
            }else{
                alert("상품들을 가져오는데 실패했습니다.")
            }
            }
        )
    }

    const loadMoreHandler = ()=>{

        let skip = Skip + Limit 

        let body ={
            skip: Skip,
            limit:Limit,
            loadMore:true
        }

        getProducts(body)
        setSkip(skip)
    }


    const renderCards = Products.map((product, index) => {

        return <Col lg={6} md={8} xs={24} key={index}>
            <Card
                cover={<ImageSlider images={product.images}/>}
            >
                <Meta
                    title={product.title}
                    description={`$${product.price}`}
                />
            </Card>
        </Col>
    })

 

useEffect는 나타내는 목록들의 개수를 body에 저장하여 getProducts라는 함수를 호출한다.

 

이 getProducts는 그 body를 객체로 받아서 products로 post를 보낸다.

 

서버에서 카드 목록들이 응답되면 이를 setProducts를 통해 Products에 저장해주고 renderCards를 통해 Products.map으로 화면에 나타나게 된다.

 

 

 

(boiler-plate와 자료는 johnAhn님 오픈소스와 강의자료를 사용하였다.)