import React, { useState } from 'react';
import styled from 'styled-components';
import { useParams } from 'react-router-dom';
import { ActiveSettingIcon, MemberIcon, MemberAddIcon, AddIcon } from '@assets';
import {
MemberTable,
TitleTab,
EventTab,
ActionButton,
EventFile,
MemberInviteModal,
MemberInfoModal,
MeetCreateModal,
MeetJoinModal,
When2meet,
} from '@components';
import { FlexCol, FlexRow, ItemsCenterRow, ItemsCenterStartRow } from '@styles';
import Layout from '../Layout';
import ProjectCalendar from '../components/calendar/ProjectCalendar';
// -- 인터페이스 --
interface MeetingData {
meetingId: string;
meetingName: string;
startDate: string;
createdAt: string;
}
interface ScheduleData {
scheduleId: string;
scheduleName: string;
startDate: string;
endDate: string;
createdAt: string;
isEnded: boolean;
}
interface MemberData {
organizationId: string;
username: string;
email: string;
type: string | null;
role: string;
}
// -- 스타일 컴포넌트 --
const Container = styled(FlexRow)`
width: 100%;
max-width: 1100px;
`;
const ContentArea = styled(FlexCol)`
width: 50%;
overflow-y: auto;
height: calc(100vh - 50px);
padding: 40px;
gap: 30px;
::-webkit-scrollbar {
width: 0;
background: transparent;
}
-ms-overflow-style: none;
scrollbar-width: none;
`;
const IconImage = styled.img<{ $width: number; $height: number }>`
width: ${(props) => props.$width}px;
height: ${(props) => props.$height}px;
cursor: pointer;
`;
const ButtonContainer = styled.div`
text-align: right;
`;
const LeftContentArea = styled(ContentArea)``;
const MemberArea = styled(FlexCol)``;
const MemberTabArea = styled(ItemsCenterRow)`
gap: 6px;
padding-left: 3px;
margin-bottom: 15px;
font-size: 20px;
color: var(--color-gray-600);
`;
const MemberAddArea = styled(ItemsCenterStartRow)`
padding: 5px;
border-bottom: 0.5px solid var(--color-gray-300);
`;
const MemberAddButton = styled(ItemsCenterRow)`
background-color: var(--color-gray-50);
border-radius: 3px;
padding: 2px;
gap: 1px;
font-size: 7px;
color: var(--color-gray-400);
cursor: pointer;
`;
const ContentTabArea = styled(FlexCol)``;
const ContentFileArea = styled(FlexCol)`
gap: 4px;
`;
const RightContentArea = styled(ContentArea)``;
const ProjectDetailPage: React.FC = () => {
const { projectId } = useParams<{ projectId: string }>();
const [activeTab, setActiveTab] = useState<'meeting' | 'schedule'>(
'meeting',
);
const [meetingData, setMeetingData] = useState<MeetingData[]>([
{
meetingId: '1',
meetingName: '프로젝트 킥오프 미팅',
startDate: '2024-11-12',
createdAt: '2024-11-10T15:30:00',
},
{
meetingId: '1',
meetingName: '프로젝트 킥오프 미팅',
startDate: '2024-11-12',
createdAt: '2024-11-10T15:30:00',
},
{
meetingId: '1',
meetingName: '프로젝트 킥오프 미팅',
startDate: '2024-11-12',
createdAt: '2024-11-10T15:30:00',
},
{
meetingId: '1',
meetingName: '프로젝트 킥오프 미팅',
startDate: '2024-11-12',
createdAt: '2024-11-10T15:30:00',
},
]);
const [scheduleData, setScheduleData] = useState<ScheduleData[]>([]);
const [modalType, setModalType] = useState<
'memberAdd' | 'memberInfo' | 'meetCreate' | 'meetJoin' | null
>(null);
const [selectedMeeting, setSelectedMeeting] = useState<MeetingData | null>(
null,
);
const [scheduleClicked, setScheduleClicked] = useState(false);
const members: MemberData[] = [
{
organizationId: '1',
username: '이정욱',
email: '[email protected]',
type: 'BE',
role: 'owner',
},
{
organizationId: '2',
username: '신진욱',
email: '[email protected]',
type: 'FE',
role: 'member',
},
];
const handleOpenModal = (
type: 'memberAdd' | 'memberInfo' | 'meetCreate' | 'meetJoin',
meeting?: MeetingData,
) => {
setModalType(type);
if (meeting) {
setSelectedMeeting(meeting);
}
};
const handleCloseModal = () => {
setModalType(null);
setSelectedMeeting(null);
};
const onClickEventFile = (event: MeetingData | ScheduleData) => {
if (activeTab === 'meeting') {
handleOpenModal('meetJoin', event as MeetingData);
} else if (activeTab === 'schedule') {
setScheduleClicked(true);
}
};
const addSchedule = (newSchedule: ScheduleData) => {
setScheduleData((prev) => [...prev, newSchedule]);
};
const eventData = activeTab === 'meeting' ? meetingData : scheduleData;
return (
<Layout>
<Container>
<LeftContentArea>
<TitleTab type="project" title={`Project ${projectId}`} />
<MemberArea>
<MemberTabArea>
<IconImage
src={MemberIcon}
$width={28}
$height={20}
/>
Member
<IconImage
src={ActiveSettingIcon}
$width={16}
$height={16}
onClick={() => handleOpenModal('memberInfo')}
/>
</MemberTabArea>
<MemberTable data={members} />
<MemberAddArea>
<MemberAddButton
onClick={() => handleOpenModal('memberAdd')}
>
<IconImage
src={MemberAddIcon}
$width={7}
$height={7}
/>
추가하기
</MemberAddButton>
</MemberAddArea>
</MemberArea>
<ContentTabArea>
<EventTab
activeTab={activeTab}
onClickTab={setActiveTab}
/>
<ContentFileArea>
{activeTab === 'meeting' && (
<ButtonContainer>
<ActionButton
icon={AddIcon}
label="회의 생성"
onClick={() =>
handleOpenModal('meetCreate')
}
/>
</ButtonContainer>
)}
{eventData.map((event) => (
<EventFile
key={
'meetingId' in event
? event.meetingId
: event.scheduleId
}
meetingName={
'meetingName' in event
? event.meetingName
: event.scheduleName
}
dateTime={event.createdAt}
onClick={() => onClickEventFile(event)}
/>
))}
</ContentFileArea>
</ContentTabArea>
</LeftContentArea>
<RightContentArea>
{scheduleClicked ? (
<When2meet onCancel={() => setScheduleClicked(false)} />
) : (
<ProjectCalendar
projectId={projectId || ''}
addSchedule={addSchedule}
/>
)}
</RightContentArea>
</Container>
{modalType === 'memberAdd' && (
<MemberInviteModal
projectId={projectId || ''}
onCancel={handleCloseModal}
/>
)}
{modalType === 'memberInfo' && (
<MemberInfoModal
projectId={projectId || ''}
onCancel={handleCloseModal}
/>
)}
{modalType === 'meetCreate' && (
<MeetCreateModal
projectId={projectId || ''}
onCancel={handleCloseModal}
/>
)}
{modalType === 'meetJoin' && selectedMeeting && (
<MeetJoinModal
meetingId={selectedMeeting.meetingId}
onCancel={handleCloseModal}
/>
)}
</Layout>
);
};
export default ProjectDetailPage;
이번 프로젝트에서 작성한 코드를 통해 React의 상태 관리와 컴포넌트 구조에 대해 많이 배웠습니다. useState
를 사용하여 동적인 상태를 관리하고, 이를 기반으로 탭과 모달을 처리하는 방법을 익혔습니다. 특히, activeTab
상태에 따라 다른 데이터를 보여주는 조건부 렌더링을 구현하면서, 어떻게 상태에 따라 UI를 효과적으로 업데이트할 수 있는지 이해할 수 있었습니다.
또한, styled-components
를 사용하여 CSS와 컴포넌트를 분리하고, 각 UI 요소를 재사용 가능한 형태로 구조화하는 것의 중요성을 느꼈습니다. 모달을 동적으로 렌더링하는 방식은 실제 프로젝트에서 필요한 기능을 구현하는 데 매우 유용한 접근법임을 확인했습니다.
이 코드를 작성하면서 React의 핵심 개념인 컴포넌트와 상태 관리의 상호작용에 대해 더 깊이 이해하게 되었고, 이를 통해 더 복잡한 기능을 다룰 때 유용한 패턴을 익혔습니다. 전체적으로 사용자의 편의성을 고려한 UI 흐름 설계와, 상태에 따라 적절히 UI를 변화시키는 방법을 익힐 수 있었던 값진 경험이었습니다.