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
5 changes: 4 additions & 1 deletion apps/mac/src/main/ipc/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ipcMain, screen, session, shell } from "electron";
import { app, ipcMain, screen, session, shell } from "electron";
import type { AppMonitor } from "../services";

// AppMonitor 및 외부 URL 열기 관련 IPC를 등록합니다.
Expand Down Expand Up @@ -50,6 +50,9 @@ export const registerIpcHandlers = (getAppMonitor: () => AppMonitor | null) => {
ipcMain.handle("open-external-url", async (_, url: string) => {
await shell.openExternal(url);
});
ipcMain.handle("app:set-badge-count", async (_, count: number): Promise<boolean> => {
return app.setBadgeCount(Math.max(0, count));
});

// 인증 세션(쿠키) 정리
ipcMain.handle("auth:clear-session", async () => {
Expand Down
1 change: 1 addition & 0 deletions apps/mac/src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface AppMonitorAPI {
contentType: string
) => Promise<void>;
openExternalUrl: (url: string) => Promise<void>;
setBadgeCount: (count: number) => Promise<boolean>;
clearAuthSession: () => Promise<void>;
checkForUpdates: () => Promise<void>;
getStartupUpdateStateSync: () => StartupUpdateState;
Expand Down
1 change: 1 addition & 0 deletions apps/mac/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const api = {
}),

openExternalUrl: (url: string) => ipcRenderer.invoke("open-external-url", url),
setBadgeCount: (count: number) => ipcRenderer.invoke("app:set-badge-count", count),
clearAuthSession: () => ipcRenderer.invoke("auth:clear-session"),
checkForUpdates: () => ipcRenderer.invoke("app:check-for-updates"),
getStartupUpdateStateSync: () => ipcRenderer.sendSync("app:get-startup-update-state-sync"),
Expand Down
18 changes: 17 additions & 1 deletion apps/mac/src/renderer/src/app/layouts/main/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useRef, useEffect, useState } from "react";
import { Outlet, useLocation } from "react-router-dom";
import { AttendanceDialog, useAttendanceDialog } from "@/features/attendance";
import { useDailyRefresh, useRealtimeSync } from "@/features/app-monitor";
import { GlobalAnnouncementDialog } from "@/features/announcement";
import { GitHubGuard } from "@/features/github";
import { useNoticePushNotification } from "@/features/notice";
import { Topbar } from "@/widgets/topbar";
import { Sidebar } from "@/widgets/sidebar";
import * as S from "./MainLayout.style";
Expand All @@ -11,8 +13,10 @@ export const MainLayout = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const mainContentRef = useRef<HTMLElement>(null);
const { pathname } = useLocation();
const attendanceDialog = useAttendanceDialog();
useRealtimeSync();
useDailyRefresh();
useNoticePushNotification();

useEffect(() => {
mainContentRef.current?.scrollTo(0, 0);
Expand All @@ -24,13 +28,25 @@ export const MainLayout = () => {

return (
<S.LayoutContainer>
<Topbar onToggleSidebar={toggleSidebar} />
<Topbar
onToggleSidebar={toggleSidebar}
onOpenAttendance={attendanceDialog.open}
isAttendancePending={attendanceDialog.isAttendancePending}
/>
<S.ContentWrapper>
<Sidebar isOpen={isSidebarOpen} />
<S.MainContent ref={mainContentRef}>
<Outlet />
</S.MainContent>
</S.ContentWrapper>
<AttendanceDialog
weeklyAttendance={attendanceDialog.weeklyAttendance}
isOpen={attendanceDialog.isOpen}
isSubmitting={attendanceDialog.isSubmitting}
errorMessage={attendanceDialog.errorMessage}
onClose={attendanceDialog.close}
onConfirm={attendanceDialog.confirm}
/>
<GlobalAnnouncementDialog />
<GitHubGuard />
</S.LayoutContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ declare module "*.svg?url" {
const src: string;
export default src;
}

declare module "*.mp3" {
const src: string;
export default src;
}
66 changes: 66 additions & 0 deletions apps/mac/src/renderer/src/entities/attendance/api/attendanceApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { api, type ApiResponse } from "@/shared/api";
import {
ATTENDANCE_STATUS,
type AttendanceStatus,
MarkAttendanceResponse,
WeeklyAttendanceResponse,
} from "@/entities/attendance/model/attendance.types";

type AttendanceStatusApiValue = AttendanceStatus | "attendanced" | "not-attendanced";

interface AttendanceDayItemApiResponse {
date: string;
dayOfWeek: string;
attendanceStatus?: AttendanceStatusApiValue;
isAttended?: boolean;
status?: AttendanceStatusApiValue;
}

interface WeeklyAttendanceApiResponse extends Omit<WeeklyAttendanceResponse, "days"> {
days: AttendanceDayItemApiResponse[];
}

const normalizeAttendanceStatus = ({
attendanceStatus,
isAttended,
status,
}: AttendanceDayItemApiResponse): AttendanceStatus => {
const rawStatus = attendanceStatus ?? status;

if (rawStatus === "attendanced" || rawStatus === ATTENDANCE_STATUS.ATTENDED) {
return ATTENDANCE_STATUS.ATTENDED;
}

if (rawStatus === "not-attendanced" || rawStatus === ATTENDANCE_STATUS.NOT_ATTENDED) {
return ATTENDANCE_STATUS.NOT_ATTENDED;
}

return isAttended ? ATTENDANCE_STATUS.ATTENDED : ATTENDANCE_STATUS.NOT_ATTENDED;
};

const normalizeWeeklyAttendance = (
weeklyAttendance: WeeklyAttendanceApiResponse
): WeeklyAttendanceResponse => ({
...weeklyAttendance,
days: weeklyAttendance.days.map(day => ({
date: day.date,
dayOfWeek: day.dayOfWeek,
attendanceStatus: normalizeAttendanceStatus(day),
})),
});

export const attendanceApi = {
getWeeklyAttendance: async () => {
const result = await api.get<ApiResponse<WeeklyAttendanceApiResponse>>("/users/me/attendance/weekly");

return {
...result.data,
data: result.data.data ? normalizeWeeklyAttendance(result.data.data) : null,
};
},

markAttendance: async () => {
const result = await api.post<ApiResponse<MarkAttendanceResponse>>("/users/me/attendance");
return result.data;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { attendanceApi } from "@/entities/attendance/api/attendanceApi";
import { attendanceQueryKeys } from "@/entities/attendance/api/query/useAttendance.query";

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

return useMutation({
mutationFn: async () => {
const response = await attendanceApi.markAttendance();
return response.data;
},
onSettled: async (_data, error) => {
await queryClient.invalidateQueries({ queryKey: attendanceQueryKeys.all });

if (!error) {
await queryClient.invalidateQueries({ queryKey: ["user"] });
}
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { attendanceApi } from "@/entities/attendance/api/attendanceApi";

export const attendanceQueryKeys = {
all: ["attendance"] as const,
weekly: ["attendance", "weekly"] as const,
};

export const useWeeklyAttendanceQuery = (enabled = true) => {
return useQuery({
queryKey: attendanceQueryKeys.weekly,
queryFn: async () => {
const response = await attendanceApi.getWeeklyAttendance();
return response.data;
},
enabled,
staleTime: 0,
refetchOnMount: "always",
retry: false,
});
};
10 changes: 10 additions & 0 deletions apps/mac/src/renderer/src/entities/attendance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export { attendanceApi } from "./api/attendanceApi";
export { useMarkAttendanceMutation } from "./api/mutation/useAttendance.mutation";
export { attendanceQueryKeys, useWeeklyAttendanceQuery } from "./api/query/useAttendance.query";
export { ATTENDANCE_STATUS, isAttended } from "./model/attendance.types";
export type {
AttendanceDayItem,
MarkAttendanceResponse,
AttendanceStatus,
WeeklyAttendanceResponse,
} from "./model/attendance.types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const ATTENDANCE_STATUS = {
ATTENDED: "attended",
NOT_ATTENDED: "not-attended",
} as const;

export type AttendanceStatus = (typeof ATTENDANCE_STATUS)[keyof typeof ATTENDANCE_STATUS];

export const isAttended = (status: AttendanceStatus) => status === ATTENDANCE_STATUS.ATTENDED;

export interface AttendanceDayItem {
date: string;
dayOfWeek: string;
attendanceStatus: AttendanceStatus;
}

export interface WeeklyAttendanceResponse {
weekNumber: number;
weekStart: string;
weekEnd: string;
days: AttendanceDayItem[];
currentStreak: number;
}

export interface MarkAttendanceResponse {
attendanceStreak: number;
earnedCookies: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,36 +39,6 @@ export const hideAnnouncementForThreeDays = (announcementId: number) => {
window.localStorage.setItem(getHideStorageKey(announcementId), String(hiddenUntil));
};

export const formatAnnouncementPeriod = (startedAt: string | null, endedAt: string | null) => {
if (!startedAt && !endedAt) {
return null;
}

const formatDate = (value: string | null) => {
if (!value) return null;

const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}

return new Intl.DateTimeFormat("ko-KR", {
year: "numeric",
month: "short",
day: "numeric",
}).format(date);
};

const startText = formatDate(startedAt);
const endText = formatDate(endedAt);

if (startText && endText) {
return `${startText} - ${endText}`;
}

return startText ?? endText;
};

export const sortAnnouncements = (announcements: AnnouncementItem[]) =>
[...announcements].sort((left, right) => {
const leftTime = left.startedAt ? new Date(left.startedAt).getTime() : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useMemo, useState } from "react";
import { useActiveAnnouncementsQuery } from "@/entities/announcement";
import { useGetMyProfile } from "@/entities/user";
import {
formatAnnouncementPeriod,
hideAnnouncementForThreeDays,
isAnnouncementHidden,
sortAnnouncements,
Expand All @@ -12,23 +11,22 @@ export const useGlobalAnnouncement = () => {
const { data: user, isLoading: isUserLoading } = useGetMyProfile();
const isAnnouncementEnabled = Boolean(user?.githubLinked) && !isUserLoading;
const { data: announcements = [] } = useActiveAnnouncementsQuery(isAnnouncementEnabled);
const [dismissedAnnouncementId, setDismissedAnnouncementId] = useState<number | null>(null);
const [dismissedAnnouncementIds, setDismissedAnnouncementIds] = useState<number[]>([]);
const [hideForThreeDaysChecked, setHideForThreeDaysChecked] = useState(false);
const [hideForThreeDaysAnnouncementId, setHideForThreeDaysAnnouncementId] = useState<
number | null
>(null);

const announcement = useMemo(() => {
const [latestAnnouncement] = sortAnnouncements(announcements);
return latestAnnouncement ?? null;
}, [announcements]);
return (
sortAnnouncements(announcements).find(
announcement =>
!dismissedAnnouncementIds.includes(announcement.id) && !isAnnouncementHidden(announcement.id)
) ?? null
);
}, [announcements, dismissedAnnouncementIds]);

const isOpen = Boolean(
isAnnouncementEnabled &&
announcement &&
announcement.id !== dismissedAnnouncementId &&
!isAnnouncementHidden(announcement.id)
);
const isOpen = Boolean(isAnnouncementEnabled && announcement);

const hideForThreeDays =
announcement?.id === hideForThreeDaysAnnouncementId ? hideForThreeDaysChecked : false;
Expand All @@ -47,7 +45,9 @@ export const useGlobalAnnouncement = () => {
hideAnnouncementForThreeDays(announcement.id);
}

setDismissedAnnouncementId(announcement.id);
setDismissedAnnouncementIds(previous =>
previous.includes(announcement.id) ? previous : [...previous, announcement.id]
);
setHideForThreeDaysChecked(false);
setHideForThreeDaysAnnouncementId(announcement.id);
};
Expand All @@ -58,8 +58,5 @@ export const useGlobalAnnouncement = () => {
hideForThreeDays,
setHideForThreeDays,
handleClose,
period: announcement
? formatAnnouncementPeriod(announcement.startedAt, announcement.endedAt)
: null,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,20 @@ export const Meta = styled.div`
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem 0.875rem;
gap: 0.5rem;
color: ${({ theme }) => theme.label.assistive};
`;

export const Author = styled.span`
${font.label.medium}
`;

export const Period = styled.span`
${font.caption.medium}
${font.label.medium}
`;

export const ContentBox = styled.div`
display: flex;
flex: 1 1 auto;
min-height: 0;
margin-top: 1rem;
padding: 1rem 1.125rem;
padding: 1rem 1.125rem 1.75rem;
border-radius: 1rem;
background: ${({ theme }) => theme.background.alternative};
overflow-y: auto;
Expand All @@ -57,6 +53,12 @@ export const Content = styled.div`
margin-top: 0.875rem;
}

&::after {
content: "";
display: block;
height: 0.875rem;
}

p,
li {
word-break: keep-all;
Expand Down
Loading
Loading