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
2 changes: 1 addition & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if (process.env.GOOGLE_SERVICES_JSON) {
export default {
name: "네모네모",
slug: "frontend",
version: "1.0.1",
version: "1.0.2",
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "frontend",
Expand Down
9 changes: 2 additions & 7 deletions app/[teamId]/calendar/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import GroupIcon from "@/assets/icons/group";
import SquaredPlusIcon from "@/assets/icons/squaredPlus";
import TeamSetting from "@/assets/icons/teamSetting";
import BellIcon from "@/assets/icons/bell";
import {
useLatestNotice,
useNoticeMutations,
Expand All @@ -25,7 +26,6 @@ import NemoText from "@/shared/ui/atoms/NemoText";
import Tabs, { TabsText } from "@/shared/ui/molecules/Tabs";
import ModalEditableField from "@/shared/ui/organisms/ModalEditableField";
import SideModal from "@/shared/ui/templates/SideModal";
import { Feather } from "@expo/vector-icons";
import { Image as ExpoImage } from "expo-image";
import {
Slot,
Expand Down Expand Up @@ -296,12 +296,7 @@ export default function CalendarTodosScreen() {
</Pressable>
<View style={styles.spacer} />
<Pressable onPress={handlePressAlarm}>
<Feather
name="bell"
size={20}
color={globalGray700}
style={{ marginRight: 12 }}
/>
<BellIcon size={20} color={globalGray700} style={{ marginRight: 12 }} />
</Pressable>
<Pressable onPress={handlePressTeamSettings}>
<TeamSetting size={24} color={globalGray700} />
Expand Down
30 changes: 18 additions & 12 deletions assets/icons/bell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,30 @@ interface MainIconProps extends SvgProps {
}

const BellIcon = ({
size = 24,
size = 20,
color = globalGray700,
...props
}: MainIconProps) => (
<Svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<Svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
{...props}
>
<Path
d="M10.5566 19.5C10.7029 19.7532 10.9133 19.9634 11.1667 20.1095C11.42 20.2557 11.7074 20.3326 12 20.3326C12.2925 20.3326 12.5799 20.2557 12.8333 20.1095C13.0866 19.9634 13.297 19.7532 13.4433 19.5"
stroke="#5F5F5F"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
d="M8.55615 17.5C8.70244 17.7532 8.91283 17.9634 9.16619 18.1095C9.41955 18.2557 9.70694 18.3326 9.99949 18.3326C10.292 18.3326 10.5794 18.2557 10.8328 18.1095C11.0861 17.9634 11.2965 17.7532 11.4428 17.5"
stroke={color}
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Path
d="M4.71772 14.7715C4.60886 14.8908 4.53702 15.0392 4.51094 15.1986C4.48486 15.358 4.50566 15.5215 4.57081 15.6693C4.63597 15.8171 4.74267 15.9428 4.87794 16.0311C5.0132 16.1193 5.17121 16.1664 5.33272 16.1665H18.6661C18.8276 16.1666 18.9856 16.1197 19.1209 16.0316C19.2563 15.9435 19.3631 15.818 19.4285 15.6703C19.4938 15.5226 19.5148 15.3591 19.4889 15.1997C19.4631 15.0402 19.3914 14.8918 19.2827 14.7723C18.1744 13.6298 16.9994 12.4157 16.9994 8.6665C16.9994 7.34042 16.4726 6.06865 15.5349 5.13097C14.5972 4.19329 13.3255 3.6665 11.9994 3.6665C10.6733 3.6665 9.40154 4.19329 8.46386 5.13097C7.52618 6.06865 6.99939 7.34042 6.99939 8.6665C6.99939 12.4157 5.82356 13.6298 4.71772 14.7715Z"
stroke="#5F5F5F"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
d="M2.71772 12.7717C2.60886 12.891 2.53702 13.0394 2.51094 13.1988C2.48486 13.3582 2.50566 13.5217 2.57081 13.6695C2.63597 13.8173 2.74267 13.943 2.87794 14.0312C3.0132 14.1195 3.17121 14.1666 3.33272 14.1667H16.6661C16.8276 14.1667 16.9856 14.1199 17.1209 14.0318C17.2563 13.9437 17.3631 13.8181 17.4285 13.6704C17.4938 13.5228 17.5148 13.3593 17.4889 13.1998C17.4631 13.0404 17.3914 12.892 17.2827 12.7725C16.1744 11.63 14.9994 10.4159 14.9994 6.66669C14.9994 5.3406 14.4726 4.06883 13.5349 3.13115C12.5972 2.19347 11.3255 1.66669 9.99939 1.66669C8.67331 1.66669 7.40154 2.19347 6.46386 3.13115C5.52618 4.06883 4.99939 5.3406 4.99939 6.66669C4.99939 10.4159 3.82356 11.63 2.71772 12.7717Z"
stroke={color}
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
Expand Down
176 changes: 173 additions & 3 deletions features/calendar/hooks/useSchedules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
QueryClient,
QueryKey,
useMutation,
useQuery,
useQueryClient,
Expand Down Expand Up @@ -97,6 +98,16 @@ const shouldIncludeOccurrence = (
candidate <= paramsEnd &&
candidate <= repeatEnd;

const isScheduleOverlappingRange = (
schedule: SchedulesResponse,
paramsStart: Date,
paramsEnd: Date
) => {
const scheduleStart = new Date(schedule.startAt);
const scheduleEnd = new Date(schedule.endAt);
return scheduleStart <= paramsEnd && scheduleEnd >= paramsStart;
};

const generateRepeatOccurrences = (
schedule: SchedulesResponse,
params: ScheduleQueryParams
Expand Down Expand Up @@ -192,8 +203,21 @@ const expandSchedulesWithRepeats = (
schedules: SchedulesResponse[],
params: ScheduleQueryParams
) => {
const queryStart = new Date(params.start);
const queryEnd = new Date(params.end);

return schedules
.flatMap((schedule) => [schedule, ...generateRepeatOccurrences(schedule, params)])
.flatMap((schedule) => {
const normalizedType = (schedule.repeatType ?? "NONE").toUpperCase();
if (normalizedType === "NONE" || !schedule.repeatEndDate) {
return [schedule];
}

return generateRepeatOccurrences(schedule, params);
})
.filter((schedule) =>
isScheduleOverlappingRange(schedule, queryStart, queryEnd)
)
.sort(
(a, b) =>
new Date(a.startAt).getTime() - new Date(b.startAt).getTime()
Expand Down Expand Up @@ -243,6 +267,112 @@ type DeleteSchedulePayload = {
teamId?: number | null;
};

type ScheduleCacheSnapshot = Array<[QueryKey, SchedulesResponse[] | undefined]>;

const snapshotScheduleCaches = (queryClient: QueryClient) => ({
team: queryClient.getQueriesData<SchedulesResponse[]>({
queryKey: ["teams"],
exact: false,
predicate: (query) => query.queryKey.includes("schedules"),
}),
me: queryClient.getQueriesData<SchedulesResponse[]>({
queryKey: ["me", "schedules"],
exact: false,
}),
});

const restoreScheduleCaches = (
queryClient: QueryClient,
snapshot?: { team: ScheduleCacheSnapshot; me: ScheduleCacheSnapshot }
) => {
if (!snapshot) return;

[...snapshot.team, ...snapshot.me].forEach(([key, data]) => {
queryClient.setQueryData(key, data);
});
};

const patchScheduleInCache = (
data: SchedulesResponse[] | undefined,
scheduleId: number,
body: ScheduleRequest
) => {
if (!data) return data;

return data.map((schedule) =>
schedule.id === scheduleId || schedule.parentScheduleId === scheduleId
? {
...schedule,
title: body.title,
description: body.description,
startAt: body.startAt,
endAt: body.endAt,
isAllDay: body.isAllDay,
place: body.place,
url: body.url,
repeatType: body.repeatType,
repeatInterval: body.repeatInterval,
repeatWeekDays: body.repeatWeekDays,
repeatUseDate: body.repeatUseDate,
repeatEndDate: body.repeatEndDate,
positionIds: body.positionIds,
attendeeMemberIds: body.attendeeMemberIds,
notificationMinutes: body.notificationMinutes,
}
: schedule
);
};

const removeScheduleFromCache = (
data: SchedulesResponse[] | undefined,
scheduleId: number
) => {
if (!data) return data;
return data.filter(
(schedule) =>
schedule.id !== scheduleId && schedule.parentScheduleId !== scheduleId
);
};

const optimisticallyUpdateScheduleCaches = (
queryClient: QueryClient,
scheduleId: number,
body: ScheduleRequest
) => {
queryClient.setQueriesData<SchedulesResponse[]>(
{
queryKey: ["teams"],
exact: false,
predicate: (query) => query.queryKey.includes("schedules"),
},
(old) => patchScheduleInCache(old, scheduleId, body)
);

queryClient.setQueriesData<SchedulesResponse[]>(
{ queryKey: ["me", "schedules"], exact: false },
(old) => patchScheduleInCache(old, scheduleId, body)
);
};

const optimisticallyDeleteScheduleCaches = (
queryClient: QueryClient,
scheduleId: number
) => {
queryClient.setQueriesData<SchedulesResponse[]>(
{
queryKey: ["teams"],
exact: false,
predicate: (query) => query.queryKey.includes("schedules"),
},
(old) => removeScheduleFromCache(old, scheduleId)
);

queryClient.setQueriesData<SchedulesResponse[]>(
{ queryKey: ["me", "schedules"], exact: false },
(old) => removeScheduleFromCache(old, scheduleId)
);
};

function invalidateScheduleQueries(
queryClient: QueryClient,
teamId?: number | null
Expand Down Expand Up @@ -278,15 +408,55 @@ export function useScheduleMutations() {
const updateSchedule = useMutation({
mutationFn: (payload: UpdateSchedulePayload) =>
changeSchedule(payload.scheduleId, payload.body),
onSuccess: (_, variables) => {
onMutate: async (variables) => {
await queryClient.cancelQueries({
queryKey: ["teams"],
exact: false,
predicate: (query) => query.queryKey.includes("schedules"),
});
await queryClient.cancelQueries({
queryKey: ["me", "schedules"],
exact: false,
});

const snapshot = snapshotScheduleCaches(queryClient);
optimisticallyUpdateScheduleCaches(
queryClient,
variables.scheduleId,
variables.body
);
return { snapshot };
},
onError: (_, __, context) => {
restoreScheduleCaches(queryClient, context?.snapshot);
},
onSettled: (_, __, variables) => {
invalidateScheduleQueries(queryClient, variables.body.teamId);
},
});

const deleteScheduleMutation = useMutation({
mutationFn: (payload: DeleteSchedulePayload) =>
deleteScheduleRequest(payload.scheduleId),
onSuccess: (_, variables) => {
onMutate: async (variables) => {
await queryClient.cancelQueries({
queryKey: ["teams"],
exact: false,
predicate: (query) => query.queryKey.includes("schedules"),
});
await queryClient.cancelQueries({
queryKey: ["me", "schedules"],
exact: false,
});

const snapshot = snapshotScheduleCaches(queryClient);
optimisticallyDeleteScheduleCaches(queryClient, variables.scheduleId);
return { snapshot };
},
onError: (_, __, context) => {
restoreScheduleCaches(queryClient, context?.snapshot);
},
onSettled: (_, __, variables) => {
invalidateScheduleQueries(queryClient, variables.teamId);
},
});
Expand Down
Loading