번개 모임 웹 어플리케이션 - 번개 상세 페이지 마크업

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

파일구조

  • pages/BungaeDetail.js
    : 번개 카드를 클릭했을 때 해당하는 글 상세 내용이 보이는 페이지

  • components/BungaeDetail/

    • BungaeDetail.js : <BungaeDetailPage> 컴포넌트의 자식 컴포넌트로, 대부분의 마크업 코드를 포함한다.


코드

모임 장소, 모임 시간, 작성 시간은 서버에서 받은 데이터를 그대로 사용하는 것이 아니라 원하는 형식으로 변경해야 하므로 함수를 사용하는데, 해당 로직을 담은 함수들은 @utils 폴더로 분리해서 가독성을 높였다.

그리고 수정 페이지로 이동할 때는 useNavigate로 처리하는데 이를 이용해 우선 기존 글 내용을 받을 때, state를 전달하는 방법을 택했다. 이렇게 되면 수정 버튼을 눌렀을 때만 state가 전달되므로 상세 페이지에서 수정 버튼을 눌렀을 때만 정상적으로 기존 글 내용을 받아오게 된다. url을 통해 직접 접근할 때는 useLocation으로 받아온 state가 null 값으로 뜬다. 아마 추후 서버에서 받아오는 방법으로 변경해야 할 것 같다.



pages/BungaeDetail.js

import { useEffect, useState } from "react";

import { dummyBungaeDetail } from "../@constants/dummy";
import BungaeDetail from "../components/BungaeDetail/BungaeDetail";
import RootPageContent from "../components/PageContent/RootPageContent";

function BungaeDetailPage() {
  const [bungaeDetail, setBungaeDetail] = useState({});
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setBungaeDetail(dummyBungaeDetail);
    setIsLoading(false);
  }, []);

  if (isLoading) return;

  return (
    <RootPageContent maxWidth="md">
      <BungaeDetail isLoading={isLoading} bungaeDetail={bungaeDetail} />
    </RootPageContent>
  );
}

export default BungaeDetailPage;

components/BungaeDetail/BungaeDetail.js

import { useNavigate } from "react-router-dom";

import styled from "styled-components";

import * as bungaeInfoUtil from "../../@utils/bungaeInfo";
import Button from "../UI/Button";

const StyledBungaeHeader = styled.section`
  width: 100%;
  margin-bottom: 30px;

  > .bungae-title {
    font-size: ${({ theme }) => theme.fontSize["2xl"]};
    font-weight: ${({ theme }) => theme.fontWeight.bold};
    margin-bottom: 24px;
    line-height: 1.3;
  }
`;

const StyledUnderscoreContainer = styled.div`
  display: flex;
  justify-content: space-between;
  padding-bottom: 24px;
  border-bottom: 2px solid ${({ theme }) => theme.palette.gray2};

  > .bungae-owner-and-ago {
    display: flex;
    align-items: center;
    gap: 16px;
    color: ${({ theme }) => theme.palette.gray5};

    > .bungae-owner {
      padding-right: 16px;
      border-right: 2px solid ${({ theme }) => theme.palette.gray2};
      color: ${({ theme }) => theme.palette.black};
    }
  }

  > .bungae-edit-and-delete {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: ${({ theme }) => theme.fontWeight.semiBold};
    color: ${({ theme }) => theme.palette.mainNavy};
  }
`;

const StyledBungaeInfoContainer = styled.ul`
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 20px;
  font-size: ${({ theme }) => theme.fontSize.md};
`;

const StyledBungaeInfoContentWrapper = styled.li`
  display: flex;
  gap: 20px;
  font-weight: ${({ theme }) => theme.fontWeight.semiBold};

  > .bungae-content-title {
    color: ${({ theme }) => theme.palette.gray5};
  }

  > a {
    color: ${({ theme }) => theme.palette.mainNavy};
    text-decoration: underline;
  }
`;

const StyledButtonWrapper = styled.div`
  align-self: flex-start;
  margin-top: 20px;
`;

const StyledIntroductionContent = styled.section`
  width: 100%;
  margin-top: 40px;

  > .bungae-introduction-title {
    font-size: ${({ theme }) => theme.fontSize.lg};
    font-weight: ${({ theme }) => theme.fontWeight.bold};
    padding-bottom: 24px;
    border-bottom: 2px solid ${({ theme }) => theme.palette.gray2};
    margin-bottom: 30px;
  }
  > .bungae-introduction-description {
    line-height: 1.3;
  }
`;

function BungaeDetail({ bungaeDetail }) {
  const navigate = useNavigate();

  const {
    id,
    title,
    owner,
    location,
    createdAt,
    meetingAt,
    openChat,
    numberOfParticipants,
    numberOfRecruits,
    description
  } = bungaeDetail;

  const meetingLoacation = bungaeInfoUtil.getMeetingLocation(location);
  const meetingTime = bungaeInfoUtil.getMeetingTime(meetingAt);
  const createdDate = bungaeInfoUtil.getCreatedDate(createdAt);

  const handleClickEdit = () => {
    navigate(`/bungae/${id}/edit`, { state: { ...bungaeDetail } });
  };

  return (
    <>
      <StyledBungaeHeader>
        <div className="bungae-title">{title}</div>
        <StyledUnderscoreContainer>
          <div className="bungae-owner-and-ago">
            <div className="bungae-owner">
              {owner.emoji} {owner.nickname}
            </div>
            <div>{createdDate}</div>
          </div>
          <div className="bungae-edit-and-delete">
            <Button color="mainNavy" basic onClick={handleClickEdit}>
              수정
            </Button>
            <Button color="mainNavy" basic>
              삭제
            </Button>
          </div>
        </StyledUnderscoreContainer>
      </StyledBungaeHeader>
      <StyledBungaeInfoContainer>
        <StyledBungaeInfoContentWrapper>
          <div className="bungae-content-title">모임 장소</div>
          <div>{meetingLoacation}</div>
        </StyledBungaeInfoContentWrapper>
        <StyledBungaeInfoContentWrapper>
          <div className="bungae-content-title">모임 시간</div>
          <div>{meetingTime}</div>
        </StyledBungaeInfoContentWrapper>
        <StyledBungaeInfoContentWrapper>
          <div className="bungae-content-title">연락 방법</div>
          <a href={openChat} target="_blank" rel="noopener noreferrer">
            카카오톡 오픈채팅
          </a>
        </StyledBungaeInfoContentWrapper>
        <StyledBungaeInfoContentWrapper>
          <div className="bungae-content-title">모집 현황</div>
          <div>{`${numberOfParticipants} / ${numberOfRecruits}`}</div>
        </StyledBungaeInfoContentWrapper>
      </StyledBungaeInfoContainer>
      <StyledButtonWrapper>
        <Button background="mainViolet" color="white" size="md">
          번개 참가하기
        </Button>
      </StyledButtonWrapper>
      <StyledIntroductionContent>
        <div className="bungae-introduction-title">[ 번개 소개 ]</div>
        <div className="bungae-introduction-description">{description}</div>
      </StyledIntroductionContent>
    </>
  );
}

export default BungaeDetail;

@utils/bungaeInfo.js

// ...

const padStartWithZero = (number) => {
  return String(number).padStart(2, "0");
};

export const getMeetingLocation = (location) => {
  return `${location.city} ${location.state} ${location.street} ${location.zipCode} ${location.detail}`;
};

export const getMeetingTime = (meetingAt) => {
  const meetingDate = new Date(meetingAt);
  const hours = padStartWithZero(meetingDate.getHours());
  const minutes = padStartWithZero(meetingDate.getMinutes());
  return `${hours}:${minutes}`;
};

export const getCreatedDate = (createdAt) => {
  const createdDate = new Date(createdAt);
  const year = createdDate.getFullYear();
  const month = padStartWithZero(createdDate.getMonth() + 1);
  const date = padStartWithZero(createdDate.getDate());
  return `${year}.${month}.${date}`;
};

// ...
profile
starter
post-custom-banner

0개의 댓글