본문 바로가기

실습

게시판 만들기.04

실무용 폴더구조(container / presenter)

프로젝트를 만들때 폴더 구조는 굉장히 중요합니다.

실무에서 사용하는 폴더 구조는 여러가지 방법이 존재하는데, 이러한 방법들을 패턴이라고 부릅니다.

리액트에서 사용하는 유명한 패턴으로는 container / presentational 패턴과 atomic 패턴이 있습니다.

 

container / presentational 패턴을 사용 하였습니다.

 

container

import { useState } from "react";
import { useMutation } from '@apollo/client'
import { useRouter } from 'next/router'
import BoardWriteUI from './BoardWrite.presenter'
import { CREATE_BOARD } from './BoardWrite.queries'

export default function BoardWrite(){
    const router = useRouter()

  const [writer, setWriter] = useState("");
  const [password, setPassword] = useState("");
  const [title, setTitle] = useState("");
  const [contents, setContents] = useState("");

  const [writerError, setWriterError] = useState("");
  const [passwordError, setPasswordError] = useState("");
  const [titleError, setTitleError] = useState("");
  const [contentsError, setContentsError] = useState("");

  const [createBoard] = useMutation(CREATE_BOARD)

  const onChangeWriter = (event) => {
    setWriter(event.target.value);
    if(event.target.value !== ""){
      setWriterError("")
    }
  };

  const onChangePassword = (event) => {
    setPassword(event.target.value);
    if(event.target.value !== ""){
      setPasswordError("")
    }
  };

  const onChangeTitle = (event) => {
    setTitle(event.target.value);
    if(event.target.value !== ""){
      setTitleError("")
    }
  };

  const onChangeContents = (event) => {
    setContents(event.target.value);
    if(event.target.value !== ""){
      setContentsError("")
    }
  };

  const onClickSubmit = async () => {
    if (!writer) {
      setWriterError("작성자를 입력해주세요.");
    }
    if (!password) {
      setPasswordError("비밀번호를 입력해주세요.");
    }
    if (!title) {
      setTitleError("제목을 입력해주세요.");
    }
    if (!contents) {
      setContentsError("내용을 입력해주세요.");
    }
    if (writer && password && title && contents) {
      try {
        const result = await createBoard({
          variables: {
            createBoardInput: {
              writer: writer,
              password: password,
              title: title,
              contents: contents
            }
          }
        })
        console.log(result.data.createBoard._id)
        router.push(`/boards/${result.data.createBoard._id}`)
      } catch(error) {
        alert(error.message)
      }
    }
  };

  return (
    <BoardWriteUI
        writerError={writerError}
        passwordError={passwordError}
        titleError={titleError}
        contentsError={contentsError}
        onChangeWriter={onChangeWriter}
        onChangePassword={onChangePassword}
        onChangeTitle={onChangeTitle}
        onChangeContents={onChangeContents}
        onClickSubmit={onClickSubmit}
    />
  )
}

presenter

import * as S from "./BoardWrite.styles";

export default function BoardWriteUI(props){
    return (
        <S.Wrapper>
            <S.Title>게시글 등록</S.Title>
            <S.WriterWrapper>
                <S.InputWrapper>
                <S.Label>작성자</S.Label>
                <S.Writer type="text" placeholder="이름을 적어주세요." onChange={props.onChangeWriter} />
                <S.Error>{props.writerError}</S.Error>
                </S.InputWrapper>
                <S.InputWrapper>
                <S.Label>비밀번호</S.Label>
                <S.Password type="password" placeholder="비밀번호를 작성해주세요." onChange={props.onChangePassword} />
                <S.Error>{props.passwordError}</S.Error>
                </S.InputWrapper>
            </S.WriterWrapper>
            <S.InputWrapper>
                <S.Label>제목</S.Label>
                <S.Subject type="text" placeholder="제목을 작성해주세요." onChange={props.onChangeTitle} />
                <S.Error>{props.titleError}</S.Error>
            </S.InputWrapper>
            <S.InputWrapper>
                <S.Label>내용</S.Label>
                <S.Contents placeholder="내용을 작성해주세요." onChange={props.onChangeContents} />
                <S.Error>{props.contentsError}</S.Error>
            </S.InputWrapper>
            <S.InputWrapper>
                <S.Label>주소</S.Label>
                <S.ZipcodeWrapper>
                <S.Zipcode placeholder="07250" />
                <S.SearchButton>우편번호 검색</S.SearchButton>
                </S.ZipcodeWrapper>
                <S.Address />
                <S.Address />
            </S.InputWrapper>
            <S.InputWrapper>
                <S.Label>유튜브</S.Label>
                <S.Youtube placeholder="링크를 복사해주세요." />
            </S.InputWrapper>
            <S.ImageWrapper>
                <S.Label>사진첨부</S.Label>
                <S.UploadButton>+</S.UploadButton>
                <S.UploadButton>+</S.UploadButton>
                <S.UploadButton>+</S.UploadButton>
            </S.ImageWrapper>
            <S.OptionWrapper>
                <S.Label>메인설정</S.Label>
                <S.RadioButton type="radio" id="youtube" name="radio-button" />
                <S.RadioLabel htmlFor="youtube">유튜브</S.RadioLabel>
                <S.RadioButton type="radio" id="image" name="radio-button" />
                <S.RadioLabel htmlFor="image">사진</S.RadioLabel>
            </S.OptionWrapper>
            <S.ButtonWrapper>
                <S.SubmitButton onClick={props.onClickSubmit}>등록하기</S.SubmitButton>
            </S.ButtonWrapper>
        </S.Wrapper>
    )
}

queries

import { gql } from '@apollo/client'

export const CREATE_BOARD = gql`
  mutation createBoard($createBoardInput: CreateBoardInput!){
    createBoard(createBoardInput: $createBoardInput){
      _id
    }
  }
`

styles(CSS)

import styled from "@emotion/styled";

export const Wrapper = styled.div`
  width: 1200px;
  /* height: 1847px; */
  border: 1px solid black;
  margin: 100px;
  padding-top: 80px;
  padding-bottom: 100px;
  padding-left: 102px;
  padding-right: 102px;
  display: flex;
  flex-direction: column;
  align-items: center;
  border: none;
  box-shadow: 0px 0px 10px gray;
`;

export const Title = styled.div`
  font-family: Arial, Helvetica, sans-serif;
  font-size: 36px;
  font-weight: bold;
`;

export const WriterWrapper = styled.div`
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding-top: 40px;
`;

export const Writer = styled.input`
  width: 486px;
  height: 52px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const Password = styled.input`
  width: 486px;
  height: 52px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const Label = styled.div`
  padding-bottom: 16px;
  font-size: 16px;
  font-weight: 500;
`;

export const InputWrapper = styled.div`
  padding-top: 40px;
`;

export const Subject = styled.input`
  width: 996px;
  height: 52px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const Contents = styled.textarea`
  width: 996px;
  height: 480px;
  padding-left: 16px;
  padding: 14px;
  border: 1px solid #bdbdbd;
`;

export const ZipcodeWrapper = styled.div`
  display: flex;
  flex-direction: row;
`;

export const Zipcode = styled.input`
  width: 77px;
  height: 52px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const SearchButton = styled.button`
  width: 124px;
  height: 52px;
  margin-left: 16px;
  background-color: black;
  cursor: pointer;
  color: white;
`;

export const Address = styled.input`
  width: 996px;
  height: 52px;
  margin-top: 16px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const Youtube = styled.input`
  width: 996px;
  height: 52px;
  padding-left: 16px;
  border: 1px solid #bdbdbd;
`;

export const ImageWrapper = styled.div`
  width: 996px;
  padding-top: 40px;
`;

export const UploadButton = styled.button`
  width: 78px;
  height: 78px;
  background-color: #bdbdbd;
  margin-right: 24px;
  outline: none;
  border: none;
  cursor: pointer;
`;

export const OptionWrapper = styled.div`
  width: 996px;
  padding-top: 40px;
`;

export const RadioButton = styled.input`
  cursor: pointer;
`;

export const RadioLabel = styled.label`
  margin-left: 8px;
  margin-right: 20px;
  font-weight: 500;
  cursor: pointer;
`;

export const ButtonWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: center;
  padding-top: 80px;
`;

export const CancelButton = styled.button`
  width: 179px;
  height: 52px;
  background-color: #bdbdbd;
  border: none;
  font-size: 16px;
  font-weight: 500;
  margin-left: 12px;
  margin-right: 12px;
  cursor: pointer;
`;

export const SubmitButton = styled.button`
  width: 179px;
  height: 52px;
  border: none;
  font-size: 16px;
  font-weight: 500;
  margin-left: 12px;
  margin-right: 12px;
  cursor: pointer;

  background-color: yellow;
`;

export const Error = styled.div`
  padding-top: 10px;
  font-size: 14px;
  color: red;
`;

기존의 part들을 실무용 폴더구조로 나누었습니다.

part별로 보기 매우 편해졌습니다.

import BoardWrite from "../../../src/components/units/board/write/BoardWrite.container";

export default function BoardWritePage() {
  return <BoardWrite />
}

 

구조 

        main - container- presenter(HTML) - styles(CSS) 

                                  - queries

 

'실습' 카테고리의 다른 글

게시판 만들기.03  (0) 2023.01.31
게시판 만들기.02  (0) 2023.01.30
게시판 만들기.01  (0) 2023.01.30