번개 모임 웹 어플리케이션 - 번개 작성 및 수정 페이지 마크업 (컴포넌트 및 로직 공유)

선정·2023년 5월 18일
0
post-custom-banner

파일구조

  • pages/CreateBungae.js
    : 번개 작성 페이지
  • pages/EditBungae.js
    : 번개 수정 페이지, 상세 페이지의 내용을 초기 값으로 가짐
  • components/CreateBungae/
    • CreateBungaeForm.js : <CreateBungaePage><EditBungaePage>가 공통으로 갖는 자식 컴포넌트로 공통된 데이터 및 마크업을 포함
    • MeetingLocation.js : <CreateBungaeForm> 내의 모임 장소 박스, css 코드량이 많아서 따로 분리


코드

폼과 다루는 데이터가 동일한 작성 페이지와 수정 페이지를 하나의 공통 컴포넌트를 사용함으로써 코드가 중복되지 않도록 코드를 만들었다. 또한 분리할 수 있는 상태 및 함수는 분리해서 @hooks 폴더 내에 커스텀 훅 또는 @util 폴더에서 함수로 만든 다음 컴포넌트에서 import 해서 사용함으로서 코드의 가독성 및 재사용성을 높인다.

<CreateBungaeForm> 폼 컴포넌트를 자식 컴포넌트로 사용할 때, 번개 상세 페이지의 객체 데이터를 받는 props(bungaeDetail)의 유무에 따라 작성/수정 페이지의 state 초기값이 다르게 들어오도록 구별한다.



pages/CreateBungae.js

import CreateBungaeForm from "../components/CreateBungae/CreateBungaeForm";
import HeadingPageContent from "../components/PageContent/HeadingPageContent";
import RootPageContent from "../components/PageContent/RootPageContent";

function CreateBungaePage() {
  return (
    <RootPageContent maxWidth="md">
      <HeadingPageContent heading="번개 모임 만들기">
        <CreateBungaeForm />
      </HeadingPageContent>
    </RootPageContent>
  );
}

export default CreateBungaePage;

pages/EditBungae.js

import { dummyBungaeDetail } from "../@constants/dummy";
import CreateBungaeForm from "../components/CreateBungae/CreateBungaeForm";
import HeadingPageContent from "../components/PageContent/HeadingPageContent";
import RootPageContent from "../components/PageContent/RootPageContent";

function EditBungaePage() {
  return (
    <RootPageContent maxWidth="md">
      <HeadingPageContent heading="번개 모임 만들기">
        <CreateBungaeForm bungaeDetail={dummyBungaeDetail} />
      </HeadingPageContent>
    </RootPageContent>
  );
}

export default EditBungaePage;

components/CreateBungae/CreateBungaeForm.js

import { useState } from "react";

import styled from "styled-components";

import MeetingLocation from "./MeetingLocation";
import { numberOptionList, timeOptionList } from "../../@constants/dropdown";
import useDropdown from "../../@hooks/useDropdown";
import useInput from "../../@hooks/useInput";
import { getInitialBungaeState } from "../../@utils/bungaeInfo";
import Button from "../UI/Button";
import Dropdown from "../UI/Dropdown";
import InputWithLabel from "../UI/InputWithLabel";
import Textarea from "../UI/Textarea";

const StyledMarginWrapper = styled.div`
  width: 100%;
  margin-bottom: 20px;
`;
const StyledDropdownContainer = styled(StyledMarginWrapper)`
  display: flex;
  gap: 20px;
`;
const StyledNarrowMarginWrapper = styled(StyledMarginWrapper)`
  margin-bottom: 14px;
`;

const StyledButtonContainer = styled.div`
  align-self: flex-end;
  display: flex;
  gap: 10px;
`;

function CreateBungaeForm({ bungaeDetail, onSubmit }) {
  // bungaeDetail에 따라 state 초기값으로 세팅될 값을 반환하는 함수 (getInitialBungaeState)
  const {
    initialNumberOfRecruits,
    initialMeetingTime,
    initialMeetingLocation,
    initialOpenChat,
    initialIntroduction
  } = getInitialBungaeState(bungaeDetail);
  
  // 드롭다운 상태 및 로직을 관리하는 커스텀 훅 (useDropdown)
  const {
    ref: numberDropdownRef,
    isOpen: numberDropdownIsOpen,
    selected: selectedNumberOption,
    onToggle: onToggleNumberDropdown,
    onSelect: onSelectNumberOption
  } = useDropdown(initialNumberOfRecruits);
  const {
    ref: timeDropdownRef,
    isOpen: timeDropdownIsOpen,
    selected: selectedTimeOption,
    onToggle: onToggleTimeDropdown,
    onSelect: onSelectTimeOption
  } = useDropdown(initialMeetingTime);
  
  const [meetingLocation] = useState(initialMeetingLocation);
  
  // input value 및 change 이벤트를 관리하는 커스텀 훅 (useInput)
  const { value: openChat, onChange: onChangeOpenChat } =
    useInput(initialOpenChat);
  const { value: introduction, onChange: onChangeIntroduction } =
    useInput(initialIntroduction);

  const bungaeSubmitHandler = () => {
    // 서버로 post(작성) 혹은 patch(수정) 요청 
    // onSubmit();
    // 후에 detailPage로 이동
  };

  return (
    <>
      <StyledDropdownContainer>
        <Dropdown
          label="모집 인원"
          ref={numberDropdownRef}
          isOpen={numberDropdownIsOpen}
          selected={selectedNumberOption}
          onToggle={onToggleNumberDropdown}
          onSelect={onSelectNumberOption}
          options={numberOptionList}
        />
        <Dropdown
          label="모임 시간"
          ref={timeDropdownRef}
          isOpen={timeDropdownIsOpen}
          selected={selectedTimeOption}
          onToggle={onToggleTimeDropdown}
          onSelect={onSelectTimeOption}
          options={timeOptionList}
        />
      </StyledDropdownContainer>
      <StyledMarginWrapper>
        <MeetingLocation meetingLocation={meetingLocation} />
      </StyledMarginWrapper>
      <StyledMarginWrapper>
        <InputWithLabel
          label="카카오톡 오픈채팅"
          id="kakao-link"
          placeholder="오픈 카톡방 링크"
          fontSize="base"
          height="46px"
          value={openChat}
          onChange={onChangeOpenChat}
        />
      </StyledMarginWrapper>
      <StyledNarrowMarginWrapper>
        <InputWithLabel
          label="제목"
          id="create-bungae-title"
          name="title"
          placeholder="제목을 입력해주세요"
          fontSize="base"
          height="46px"
          value={introduction.title}
          onChange={onChangeIntroduction}
        />
      </StyledNarrowMarginWrapper>
      <StyledNarrowMarginWrapper>
        <Textarea
          name="description"
          placeholder="번개 모임에 대해 소개해주세요"
          height="340px"
          value={introduction.description}
          onChange={onChangeIntroduction}
        />
      </StyledNarrowMarginWrapper>
      <StyledButtonContainer>
        <Button background="white" outline>
          취소
        </Button>
        <Button
          background="mainViolet"
          color="white"
          onClick={bungaeSubmitHandler}
        >
          번개 등록
        </Button>
      </StyledButtonContainer>
    </>
  );
}

export default CreateBungaeForm;

bungaeDetail 더미 데이터 구조

export const dummyBungaeDetail = {
  id: 1,
  owner: {
    id: 1,
    email: "test@test.com",
    emoji: "😶‍🌫️", // 필요
    nickname: "닉네임입니다"
  },
  title: "오늘 7시 성수역 클라이밍 하실 분 계신가요!!...",
  description: "설명",
  location: {
    country: "한국",
    city: "서울",
    state: "성동구",
    street: "성수일로",
    zipCode: "1234",
    detail: "303호"
  },
  openChat: "http://localhost:3000/", // 필요
  createdAt: "2023-05-17T00:00:00",
  meetingAt: "2023-05-17T18:00:00",
  numberOfParticipants: 2,
  numberOfRecruits: 4
};

@utils/bungaeInfo, getInitialBungaeState 함수

  • 함수에 전달되는 인수에 따라 다른 초깃값을 리턴한다.
export const getMeetingTime = (meetingAt) => {
  const meetingDate = new Date(meetingAt);
  const hours = String(meetingDate.getHours()).padStart(2, "0");
  const minutes = String(meetingDate.getMinutes()).padStart(2, "0");
  return `${hours}:${minutes}`;
};

export const getInitialBungaeState = (bungaeDetail) => {
  let initialNumberOfRecruits = { name: "1명 ~ 10명", value: null };
  let initialMeetingTime = { name: "00:30 ~ 23:30", value: null };
  let initialMeetingLocation = null;
  let initialOpenChat = "";
  let initialIntroduction = { title: "", description: "" };

  if (bungaeDetail) {
    initialNumberOfRecruits = numberOptionList.find(
      ({ value }) => value === bungaeDetail.numberOfRecruits
    );
    initialMeetingTime = timeOptionList.find(
      ({ value }) => value === getMeetingTime(bungaeDetail.meetingAt)
    );
    initialMeetingLocation = `${bungaeDetail.location.city} ${bungaeDetail.location.state} ${bungaeDetail.location.street} ${bungaeDetail.location.zipCode} ${bungaeDetail.location.detail}`;
    initialOpenChat = bungaeDetail.openChat;
    initialIntroduction = {
      title: bungaeDetail.title,
      description: bungaeDetail.description
    };
  }

  return {
    initialNumberOfRecruits,
    initialMeetingTime,
    initialMeetingLocation,
    initialOpenChat,
    initialIntroduction
  };
};

/@constants/dropdown, numberOptionList, timeOptionList

export const numberOptionList = [
  { name: "1명", value: 1 },
  { name: "2명", value: 2 },
  { name: "3명", value: 3 },
  { name: "4명", value: 4 },
  { name: "5명", value: 5 },
  { name: "6명", value: 6 },
  { name: "7명", value: 7 },
  { name: "8명", value: 8 },
  { name: "9명", value: 9 },
  { name: "10명", value: 10 }
];

export const timeOptionList = [
  { name: "00:30", value: "00:30" },
  { name: "01:00", value: "01:00" },
  { name: "01:30", value: "01:30" },
  { name: "02:00", value: "02:00" },
  { name: "02:30", value: "02:30" },
  { name: "03:00", value: "03:00" },
  { name: "03:30", value: "03:30" },
  { name: "04:00", value: "04:00" },
  { name: "04:30", value: "04:30" },
  { name: "05:00", value: "05:00" },
  { name: "05:30", value: "05:30" },
  { name: "06:00", value: "06:00" },
  { name: "06:30", value: "06:30" },
  { name: "07:00", value: "07:00" },
  { name: "07:30", value: "07:30" },
  { name: "08:00", value: "08:00" },
  { name: "08:30", value: "08:30" },
  { name: "09:00", value: "09:00" },
  { name: "09:30", value: "09:30" },
  { name: "10:00", value: "10:00" },
  { name: "10:30", value: "10:30" },
  { name: "11:00", value: "11:00" },
  { name: "11:30", value: "11:30" },
  { name: "12:00", value: "12:00" },
  { name: "12:30", value: "12:30" },
  { name: "13:00", value: "13:00" },
  { name: "13:30", value: "13:30" },
  { name: "14:00", value: "14:00" },
  { name: "14:30", value: "14:30" },
  { name: "15:00", value: "15:00" },
  { name: "15:30", value: "15:30" },
  { name: "16:00", value: "16:00" },
  { name: "16:30", value: "16:30" },
  { name: "17:00", value: "17:00" },
  { name: "17:30", value: "17:30" },
  { name: "18:00", value: "18:00" },
  { name: "18:30", value: "18:30" },
  { name: "19:00", value: "19:00" },
  { name: "19:30", value: "19:30" },
  { name: "20:00", value: "20:00" },
  { name: "20:30", value: "20:30" },
  { name: "21:00", value: "21:00" },
  { name: "21:30", value: "21:30" },
  { name: "22:00", value: "22:00" },
  { name: "22:30", value: "22:30" },
  { name: "23:00", value: "23:00" },
  { name: "23:30", value: "23:30" }
];
profile
starter
post-custom-banner

0개의 댓글