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}`;
};
// ...