Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions frontend/src/features/discussion/ui/DiscussionRank/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState } from 'react';

import { Tab } from '@/components/Tab';

const DiscussionRank = () => {
const [tab, setTab] = useState('eventsRankedDefault');
const handleChange = (value: string) => {
setTab(value);
};

return (
<div>
<Tab onChange={handleChange} selectedValue={tab}>
<Tab.List>
<Tab.Item value='eventsRankedDefault'>참가자 많은 순</Tab.Item>
<Tab.Item value='eventsRankedOfTime'>빠른 시간 순</Tab.Item>
</Tab.List>
<Tab.Content value='eventsRankedDefault'>캘린더</Tab.Content>
<Tab.Content value='eventsRankedOfTime'>순위</Tab.Content>
</Tab>
</div>
);
};

export default DiscussionRank;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useState } from 'react';
import { Tab } from '@/components/Tab';

import DiscussionCalendar from '../DiscussionCalendar';
import DiscussionRank from '../DiscussionRank';
import { tabContainerStyle } from './index.css';

const DiscussionTab = () => {
Expand All @@ -25,7 +26,7 @@ const DiscussionTab = () => {
<DiscussionCalendar />
</Tab.Content>
<Tab.Content value='rank'>
순위
<DiscussionRank />
</Tab.Content>
</Tab>
);
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/features/my-calendar/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { request } from '@/utils/fetch';

import type { PersonalEventDTO, PersonalEventRequest, PersonalEventResponse } from '../model';

export const personalEventApi = {
getPersonalEvent: async (
{ startDateTime, endDateTime }: Pick<PersonalEventDTO, 'startDateTime' | 'endDateTime'>,
): Promise<PersonalEventResponse[]> => {
const response = await request.get('/api/v1/personal-event', {
params: {
startDateTime: `${startDateTime}T00:00:00`,
endDateTime: `${endDateTime}T00:00:00`,
},
});
return response.data;
},
postPersonalEvent: async (body: PersonalEventRequest): Promise<PersonalEventResponse> => {
const response = await request.post('/api/v1/personal-event', { body });
return response;
},
};
7 changes: 7 additions & 0 deletions frontend/src/features/my-calendar/api/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PersonalEventDTO } from '../model';

export const personalEventKeys = {
all: ['personalEvents'],
detail: (data: Pick<PersonalEventDTO, 'startDateTime' | 'endDateTime'>) =>
[...personalEventKeys.all, data],
};
20 changes: 20 additions & 0 deletions frontend/src/features/my-calendar/api/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import type { PersonalEventRequest } from '../model';
import { personalEventApi } from '.';
import { personalEventKeys } from './keys';

export const usePersonalEventMutation = () => {
const queryClient = useQueryClient();

const { mutate } = useMutation({
mutationFn: (body: PersonalEventRequest) => personalEventApi.postPersonalEvent(body),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: personalEventKeys.all,
});
},
});

return { mutate };
};
16 changes: 16 additions & 0 deletions frontend/src/features/my-calendar/api/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery } from '@tanstack/react-query';

import type { PersonalEventDTO, PersonalEventResponse } from '../model';
import { personalEventApi } from '.';
import { personalEventKeys } from './keys';

export const usePersonalEventsQuery = (
data: Pick<PersonalEventDTO, 'startDateTime' | 'endDateTime'>,
) => {
const { data: personalEvents, isLoading } = useQuery<PersonalEventResponse[]>({
queryKey: personalEventKeys.detail(data),
queryFn: () => personalEventApi.getPersonalEvent(data),
});

return { personalEvents, isLoading };
};
22 changes: 22 additions & 0 deletions frontend/src/features/my-calendar/model/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
import { z } from 'zod';

const PersonalEventDTO = z.object({
id: z.number(),
title: z.string(),
startDateTime: z.string().datetime(),
endDateTime: z.string().datetime(),
Comment on lines +6 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3;
저희 나중에 DTO 쓰기 편하도록, 서버에서 string 형태로 들어오는 날짜 혹은 시간 문자열을 Date type으로 쓸 수 있게끔, zod에서 전처리를 하면 좋을 것 같은데 어떻게 생각하시나요!

isAdjustable: z.boolean(),
syncWithGoogleCalendar: z.boolean(),
googleEventId: z.string(),
calendarId: z.string(),
});

const PersonalEventResponse = PersonalEventDTO.omit({ syncWithGoogleCalendar: true });
const PersonalEventRequest = PersonalEventDTO.omit(
{ id: true, googleEventId: true, calendarId: true },
);

export type PersonalEventDTO = z.infer<typeof PersonalEventDTO>;
export type PersonalEventResponse = z.infer<typeof PersonalEventResponse>;
export type PersonalEventRequest = z.infer<typeof PersonalEventRequest>;

export type PopoverType = 'add' | 'edit';
28 changes: 14 additions & 14 deletions frontend/src/features/my-calendar/ui/CalendarCardList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import { TIME_HEIGHT } from '@/constants/date';
import { calcPositionByDate } from '@/utils/date/position';

import type { PersonalEventDTO } from '../../model';
import { CalendarCard } from '../CalendarCard';

interface DateRange {
startDate: Date | null;
endDate: Date | null;
}

const calcSize = (height: number) => {
if (height < TIME_HEIGHT) return 'sm';
if (height < TIME_HEIGHT * 2.5) return 'md';
return 'lg';
};

export const CalendarCardList = ({ cards }: { cards: DateRange[] }) => (
export const CalendarCardList = (
{ cards }: { cards: Omit<PersonalEventDTO, 'syncWithGoogleCalendar'>[] },
) => (
<>
{cards.map((card, idx) => {
const { x: sx, y: sy } = calcPositionByDate(card.startDate);
const { y: ey } = calcPositionByDate(card.endDate);
{cards.map((card) => {
const start = new Date(card.startDateTime);
const end = new Date(card.endDateTime);
const { x: sx, y: sy } = calcPositionByDate(start);
const { y: ey } = calcPositionByDate(end);
const height = ey - sy;

return (
<CalendarCard
endTime={card.endDate}
key={idx}
endTime={end}
key={card.id}
size={calcSize(height)}
startTime={card.startDate}
status='adjustable'
startTime={start}
status={card.isAdjustable ? 'adjustable' : 'fixed'}
style={{
width: 'calc((100% - 72px) / 7)',
height,
position: 'absolute',
left: `calc(((100% - 72px) / 7 * ${sx}) + 72px)`,
top: 16 + sy,
}}
title='새 일정 제목이 개길면 어떻게 될까?'
title={card.title}
/>
);
})}
Expand Down
36 changes: 20 additions & 16 deletions frontend/src/features/my-calendar/ui/MyCalendar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,35 @@ import { useState } from 'react';
import { Calendar } from '@/components/Calendar';
import { useSharedCalendarContext } from '@/components/Calendar/context/SharedCalendarContext';
import { useSelectTime } from '@/hooks/useSelectTime';
import { formatDateToWeekRange } from '@/utils/date';
import { formatDateToBarString, formatDateToDateTimeString } from '@/utils/date/format';

import { usePersonalEventsQuery } from '../../api/queries';
import type { PersonalEventResponse } from '../../model';
import { CalendarCardList } from '../CalendarCardList';
import { SchedulePopover } from '../SchedulePopover';
import { calendarStyle, containerStyle } from './index.css';

// TODO: 자주 쓰이는 타입 정의는 따로 빼기
interface DateRange {
startDate: Date | null;
endDate: Date | null;
}

const CalendarTable = () => {
const CalendarTable = (
{ personalEvents = [] }: { personalEvents?: PersonalEventResponse[] },
) => {
const { handleMouseUp, ...time } = useSelectTime();
const [open, setOpen] = useState(false);
const [cards, setCards] = useState<DateRange[]>([]);

const handleMouseUpAddSchedule = () => {
setOpen(true);
};

return (
<div className={containerStyle}>
{open &&
<SchedulePopover
cards={cards}
endDate={time.doneEndTime}
isOpen={open}
setCards={setCards}
endDateTime={formatDateToDateTimeString(time.doneEndTime)}
setIsOpen={setOpen}
startDate={time.doneStartTime}
startDateTime={formatDateToDateTimeString(time.doneStartTime)}
type='add'
/>
<CalendarCardList cards={cards} />
/>}
<CalendarCardList cards={personalEvents} />
<Calendar.Table
context={{
handleMouseUp: () => handleMouseUp(handleMouseUpAddSchedule),
Expand All @@ -47,11 +44,18 @@ const CalendarTable = () => {

export const MyCalendar = () => {
const calendar = useSharedCalendarContext();
const { startDate, endDate } = formatDateToWeekRange(calendar.selectedDate);

const { personalEvents, isLoading } = usePersonalEventsQuery({
startDateTime: formatDateToBarString(startDate),
endDateTime: formatDateToBarString(endDate),
});

return (
<Calendar {...calendar} className={calendarStyle}>
<Calendar.Core />
<Calendar.Header />
<CalendarTable />
{isLoading ? <div>로딩중...</div> : <CalendarTable personalEvents={personalEvents} />}
</Calendar>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,71 @@ import { Flex } from '@/components/Flex';
import Input from '@/components/Input';
import { Text } from '@/components/Text';
import { Toggle } from '@/components/Toggle';
import type { FormRef } from '@/hooks/useFormRef';
import { vars } from '@/theme/index.css';
import { formatDateToTimeString } from '@/utils/date';

import type { PersonalEventRequest } from '../../model';
import { cardStyle, inputStyle } from './index.css';

interface PopoverFormProps {
startDate: Date | null;
endDate: Date | null;
}
// TODO: Form Context 관리
const AdjustableCheckbox = ({ handleChange, valuesRef }: FormRef<PersonalEventRequest>) => (
<Checkbox
inputProps={{
name: 'isAdjustable',
checked: valuesRef.current.isAdjustable,
onChange: handleChange,
}}
size='sm'
>
시간 조정 가능
</Checkbox>
);

export const PopoverForm = ({ startDate, endDate }: PopoverFormProps) =>
// form 관리
(
<>
<Flex
align='flex-end'
className={cardStyle}
direction='column'
gap={400}
const GoogleCalendarToggle = (
{ handleChange, valuesRef }: FormRef<PersonalEventRequest>,
) =>
<Toggle
inputProps={{
name: 'syncWithGoogleCalendar',
checked: valuesRef.current.syncWithGoogleCalendar,
onChange: handleChange,
}}
/>;

export const PopoverForm = ({ valuesRef, handleChange }: FormRef<PersonalEventRequest>) =>
<>
<Flex
align='flex-end'
className={cardStyle}
direction='column'
gap={400}
>
<input
className={inputStyle}
name='title'
onChange={handleChange}
placeholder='새 일정'
/>
<Input.Multi
borderPlacement='container'
label='시간 설정'
separator='~'
type='text'
>
<input className={inputStyle} placeholder='새 일정' />
<Input.Multi
borderPlacement='container'
label='시간 설정'
separator='~'
type='text'
>
<Input.Multi.InputField readOnly value={formatDateToTimeString(startDate)} />
<Input.Multi.InputField readOnly value={formatDateToTimeString(endDate)} />
</Input.Multi>
<Checkbox size='sm'>시간 조정 가능</Checkbox>
</Flex>
<Flex className={cardStyle} justify='space-between'>
<Text color={vars.color.Ref.Netural[600]} typo='caption'>구글 캘린더 연동</Text>
<Toggle />
</Flex>
</>
)
;
<Input.Multi.InputField
readOnly
value={formatDateToTimeString(new Date(valuesRef.current.startDateTime))}
/>
<Input.Multi.InputField
readOnly
value={formatDateToTimeString(new Date(valuesRef.current.endDateTime))}
/>
</Input.Multi>
<AdjustableCheckbox handleChange={handleChange} valuesRef={valuesRef} />
</Flex>
<Flex className={cardStyle} justify='space-between'>
<Text color={vars.color.Ref.Netural[600]} typo='caption'>구글 캘린더 연동</Text>
<GoogleCalendarToggle handleChange={handleChange} valuesRef={valuesRef} />
</Flex>
</>;
Loading