diff --git a/apps/mac/src/renderer/src/features/attendance/ui/AttendanceDialog.tsx b/apps/mac/src/renderer/src/features/attendance/ui/AttendanceDialog.tsx
index d8576e44..435166a3 100644
--- a/apps/mac/src/renderer/src/features/attendance/ui/AttendanceDialog.tsx
+++ b/apps/mac/src/renderer/src/features/attendance/ui/AttendanceDialog.tsx
@@ -1,4 +1,4 @@
-import { Button, Dialog } from "@/shared/ui";
+import { Button, Dialog, SkeletonPanel } from "@/shared/ui";
import { isAttended, type WeeklyAttendanceResponse } from "@/entities/attendance";
import * as S from "./AttendanceDialog.style";
@@ -48,29 +48,20 @@ export const AttendanceDialog = ({
return (
);
}
diff --git a/apps/mac/src/renderer/src/features/chapter-ranking/ui/ChapterRanking.style.ts b/apps/mac/src/renderer/src/features/chapter-ranking/ui/ChapterRanking.style.ts
index 2cbee1f7..670bad82 100644
--- a/apps/mac/src/renderer/src/features/chapter-ranking/ui/ChapterRanking.style.ts
+++ b/apps/mac/src/renderer/src/features/chapter-ranking/ui/ChapterRanking.style.ts
@@ -1,4 +1,4 @@
-import styled, { css, keyframes } from "styled-components";
+import styled, { css } from "styled-components";
import { font } from "@clash/design-tokens/font";
import { InlineNotice } from "@clash/ui";
import FirstFrameIcon from "../assets/first-frame.svg";
@@ -10,25 +10,8 @@ import { DefaultProfileIcon } from "@/shared/ui";
export type RankingPageEnum = "section" | "chapter";
export type RankingPositionEnum = "top" | "bottom";
-const skeletonWave = keyframes`
- 0% {
- background-position: 200% 0;
- }
-
- 100% {
- background-position: -200% 0;
- }
-`;
-
const skeletonBase = css`
- background: linear-gradient(
- 90deg,
- ${({ theme }) => theme.fill.normal} 0%,
- ${({ theme }) => theme.fill.alternative} 50%,
- ${({ theme }) => theme.fill.normal} 100%
- );
- background-size: 200% 100%;
- animation: ${skeletonWave} 1.6s ease-in-out infinite;
+ background-color: ${({ theme }) => theme.fill.neutral};
`;
export const RankingContainer = styled.div<{ $page: RankingPageEnum }>`
diff --git a/apps/mac/src/renderer/src/features/chapter/components/MissionContainer.tsx b/apps/mac/src/renderer/src/features/chapter/components/MissionContainer.tsx
index b3eb083f..271a2d66 100644
--- a/apps/mac/src/renderer/src/features/chapter/components/MissionContainer.tsx
+++ b/apps/mac/src/renderer/src/features/chapter/components/MissionContainer.tsx
@@ -1,6 +1,6 @@
import * as S from "./MissionContainer.style";
import { useEffect, useRef, useState } from "react";
-import { SidePanel } from "@/shared/ui";
+import { SidePanel, SkeletonPanel } from "@/shared/ui";
import { getErrorMessage } from "@/shared/lib";
import { MarkdownCodeContent } from "@/shared/ui/markdown-code-content/MarkdownCodeContent";
import type { Mission } from "@/features/chapter/model/chapter.types";
@@ -344,9 +344,7 @@ export const MissionContainer = ({
}
};
- const descriptionText = isLoading
- ? "챕터 정보를 불러오는 중입니다."
- : description?.trim() || "이 챕터에 대한 설명이 아직 준비되지 않았습니다.";
+ const descriptionText = description?.trim() || "이 챕터에 대한 설명이 아직 준비되지 않았습니다.";
const isCompletedStage =
isFinishedSection ||
(currentStage.totalMissions > 0 && currentStage.currentProgress >= currentStage.totalMissions);
@@ -392,9 +390,13 @@ export const MissionContainer = ({
) : (
<>
-
- {descriptionText}
-
+ {isLoading ? (
+
+ ) : (
+
+ {descriptionText}
+
+ )}
diff --git a/apps/mac/src/renderer/src/features/competition/ui/rival-competition/battle/Battle.tsx b/apps/mac/src/renderer/src/features/competition/ui/rival-competition/battle/Battle.tsx
index bd03025f..6c313657 100644
--- a/apps/mac/src/renderer/src/features/competition/ui/rival-competition/battle/Battle.tsx
+++ b/apps/mac/src/renderer/src/features/competition/ui/rival-competition/battle/Battle.tsx
@@ -1,6 +1,14 @@
import { useState } from "react";
import * as S from "./Battle.style";
-import { Button, DefaultProfileIcon, Dialog, Select, SlideSelector } from "@/shared/ui";
+import {
+ Button,
+ DefaultProfileIcon,
+ Dialog,
+ Select,
+ SkeletonPanel,
+ SkeletonRows,
+ SlideSelector,
+} from "@/shared/ui";
import type { AnalyzeCategory } from "@/entities/competition";
import { MATCH_VALUE } from "@/entities/competition";
import { useBattle } from "@/features/competition/model/useBattle";
@@ -69,11 +77,7 @@ export const Battle = () => {
)}
{battle.queries.info.isPending ? (
-
-
- 배틀 정보를 불러오는 중이에요.
-
-
+
) : battle.queries.info.isError && !battle.battleData ? (
@@ -139,13 +143,7 @@ export const Battle = () => {
{battle.isBattleSelected ? (
battle.queries.detail.isPending || battle.queries.detail.isPlaceholderData ? (
-
-
-
- 배틀 상세 정보를 불러오는 중이에요.
-
-
-
+
) : battle.queries.detail.isError && !battle.battleDetailData ? (
@@ -248,9 +246,7 @@ export const Battle = () => {
{battle.queries.analyze.isPending ||
battle.queries.analyze.isPlaceholderData ? (
-
- 세부 분석을 불러오는 중이에요.
-
+
) : battle.queries.analyze.isError &&
battle.queries.analyze.data === undefined ? (
@@ -379,9 +375,7 @@ export const Battle = () => {
{battle.queries.rivals.isPending ? (
-
- 신청 가능한 라이벌을 불러오는 중이에요.
-
+
) : battle.queries.rivals.isError && !battle.battleList ? (
신청 가능한 라이벌을 불러오지 못했어요.
@@ -475,9 +469,7 @@ export const Battle = () => {
aria-busy={battle.queries.applications.isFetching || undefined}
>
{battle.queries.applications.isPending ? (
-
- 배틀 신청 목록을 불러오는 중이에요.
-
+
) : battle.queries.applications.isError && !battle.battleApplyList ? (
배틀 신청 목록을 불러오지 못했어요.
diff --git a/apps/mac/src/renderer/src/features/group/ui/group-side-tab/panel/GroupFormPanel.tsx b/apps/mac/src/renderer/src/features/group/ui/group-side-tab/panel/GroupFormPanel.tsx
index 768b758b..103435c3 100644
--- a/apps/mac/src/renderer/src/features/group/ui/group-side-tab/panel/GroupFormPanel.tsx
+++ b/apps/mac/src/renderer/src/features/group/ui/group-side-tab/panel/GroupFormPanel.tsx
@@ -1,4 +1,4 @@
-import { Button, SlideSelector } from "@/shared/ui";
+import { Button, SkeletonCards, SlideSelector } from "@/shared/ui";
import { GROUP_CATEGORY_LABELS } from "@/entities/group";
import type { GroupFormPanelProps } from "../../../model/useGroup";
import { useGroupFormPanel } from "../../../model/useGroupFormPanel";
@@ -78,9 +78,11 @@ export const GroupFormPanel = ({
{isLoading ? (
-
- 로딩 중...
-
+
) : groups.length === 0 ? (
diff --git a/apps/mac/src/renderer/src/features/home/ui/ranking/Ranking.tsx b/apps/mac/src/renderer/src/features/home/ui/ranking/Ranking.tsx
index 9429db2b..67889da4 100644
--- a/apps/mac/src/renderer/src/features/home/ui/ranking/Ranking.tsx
+++ b/apps/mac/src/renderer/src/features/home/ui/ranking/Ranking.tsx
@@ -1,7 +1,7 @@
import * as S from "./Ranking.style";
import { rankingRewardTooltipContent } from "./Ranking.constants";
import { useRanking } from "@/features/home/model/useRanking";
-import { Button, Select, QuestionTooltip } from "@/shared/ui";
+import { Button, Select, QuestionTooltip, SkeletonRows } from "@/shared/ui";
import type { CategoryType, PeriodType, RankingItem } from "@/entities/ranking";
import { UserRanking } from "./user-ranking/UserRanking";
@@ -58,15 +58,14 @@ export const Ranking = () => {
{domain.isLoading || domain.isPlaceholderData ? (
-
-
-
- {domain.isPlaceholderData
- ? "선택한 조건의 랭킹을 불러오는 중이에요."
- : "랭킹을 불러오는 중이에요."}
-
-
-
+
) : domain.isError && !hasRankings ? (
diff --git a/apps/mac/src/renderer/src/features/major-choice/ui/test/Test.tsx b/apps/mac/src/renderer/src/features/major-choice/ui/test/Test.tsx
index 91364062..c488aeff 100644
--- a/apps/mac/src/renderer/src/features/major-choice/ui/test/Test.tsx
+++ b/apps/mac/src/renderer/src/features/major-choice/ui/test/Test.tsx
@@ -1,6 +1,7 @@
import * as S from "./Test.style";
import type { TestProps } from "@/features/major-choice/model/useMajorChoice";
import { Button } from "@/shared/ui/button";
+import { SkeletonRows } from "@/shared/ui/skeleton";
import { AGREEMENT_LABELS, LevelSlider } from "@/shared/ui/level-slider";
import type { LevelSliderValue } from "@/shared/ui/level-slider/types";
@@ -61,9 +62,13 @@ export const Test = ({
{isQuestionsLoading ? (
-
- 검사 문항을 불러오는 중이에요.
-
+
) : questionsError && questionData.length === 0 ? (
검사 문항을 불러오지 못했어요.
diff --git a/apps/mac/src/renderer/src/features/notice/ui/TopbarNotice.tsx b/apps/mac/src/renderer/src/features/notice/ui/TopbarNotice.tsx
index 84070faa..1fe7a3fd 100644
--- a/apps/mac/src/renderer/src/features/notice/ui/TopbarNotice.tsx
+++ b/apps/mac/src/renderer/src/features/notice/ui/TopbarNotice.tsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import type { KeyboardEvent } from "react";
import { formatNoticeDate, useTopbarNotice } from "@/features/notice/model/useTopbarNotice";
-import { Button, Popover } from "@/shared/ui";
+import { Button, Popover, SkeletonRows } from "@/shared/ui";
import * as S from "./TopbarNotice.style";
import { getErrorMessage } from "@/shared/lib";
@@ -157,9 +157,7 @@ export const TopbarNotice = () => {
)}
{isLoading ? (
-
- 알림을 불러오는 중입니다.
-
+
) : isError && !hasQueryData ? (
{getErrorMessage(error, "알림을 불러오지 못했어요.")}
diff --git a/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/github-activity-panel/GitHubActivityPanel.tsx b/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/github-activity-panel/GitHubActivityPanel.tsx
index 1e582675..df127fa9 100644
--- a/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/github-activity-panel/GitHubActivityPanel.tsx
+++ b/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/github-activity-panel/GitHubActivityPanel.tsx
@@ -3,6 +3,7 @@ import { GitHubInfo } from "@/features/profile/ui/profile-tabs/github-info/GitHu
import { GitHubStreak } from "@/features/profile/ui/profile-tabs/github-streak/GitHubStreak";
import { useProfileGitHubDetailQuery } from "@/entities/profile";
import { useProfileGitHubStreak } from "@/features/profile/model/useProfileTabs";
+import { SkeletonPanel } from "@/shared/ui";
import * as S from "./GitHubActivityPanel.style";
const formatKoreanDate = (ymd: string) => {
@@ -73,13 +74,17 @@ export const GitHubActivityPanel = () => {
-
+ {selectedDate && !displayDetail && (detailQuery.isFetching || detailQuery.isLoading) ? (
+
+ ) : (
+
+ )}
);
diff --git a/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/item-panel/ItemPanel.tsx b/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/item-panel/ItemPanel.tsx
index f7d9d412..7adec0fd 100644
--- a/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/item-panel/ItemPanel.tsx
+++ b/apps/mac/src/renderer/src/features/profile/ui/profile-tabs/item-panel/ItemPanel.tsx
@@ -12,7 +12,7 @@ import {
import { sortEquippedItemsFirst } from "@/features/profile/lib/sortEquippedItemsFirst";
import { useGetMyProfile, userQueryKeys } from "@/entities/user";
import { getErrorMessage } from "@/shared/lib";
-import { Button } from "@/shared/ui";
+import { Button, SkeletonCards } from "@/shared/ui";
const FILTER_OPTIONS = [
{ key: OwnedItemCategory.ALL, label: "전체" },
@@ -126,10 +126,7 @@ export const ItemPanel = () => {
) : isLoading ? (
-
- 아이템을 불러오는 중...
- 잠시만 기다려 주세요.
-
+
) : items.length === 0 ? (
보유한 아이템이 아직 없어요.
diff --git a/apps/mac/src/renderer/src/features/record/ui/task/Task.tsx b/apps/mac/src/renderer/src/features/record/ui/task/Task.tsx
index 756da309..d914d7b6 100644
--- a/apps/mac/src/renderer/src/features/record/ui/task/Task.tsx
+++ b/apps/mac/src/renderer/src/features/record/ui/task/Task.tsx
@@ -1,7 +1,7 @@
import * as S from "./Task.style";
import { formatTime, getErrorMessage } from "@/shared/lib";
import { useTaskList } from "../../model/useTaskList";
-import { Button, ConfirmDialog, Popover, Tooltip } from "@/shared/ui";
+import { Button, ConfirmDialog, Popover, SkeletonRows, Tooltip } from "@/shared/ui";
interface TaskProps {
selectedDate?: string;
@@ -74,7 +74,7 @@ export const Task = ({ selectedDate }: TaskProps) => {
{subjectsQuery.isPending ? (
- 과목을 불러오는 중이에요.
+
) : hasInitialLoadError ? (
{getErrorMessage(subjectsQuery.error, "과목을 불러오지 못했어요.")}
diff --git a/apps/mac/src/renderer/src/features/record/ui/timer/Timer.tsx b/apps/mac/src/renderer/src/features/record/ui/timer/Timer.tsx
index 71d2a961..1fabe7f6 100644
--- a/apps/mac/src/renderer/src/features/record/ui/timer/Timer.tsx
+++ b/apps/mac/src/renderer/src/features/record/ui/timer/Timer.tsx
@@ -2,6 +2,7 @@ import * as S from "./Timer.style";
import { formatTime } from "@/shared/lib";
import { useRecordStore } from "../../model/recordStore";
import { useLiveRecordStudyTime } from "../../model/useLiveRecordStudyTime";
+import { Skeleton } from "@/shared/ui";
interface TimerProps {
date: string;
@@ -103,7 +104,7 @@ export const Timer = ({
aria-label={isLoading ? "총 학습 시간을 불러오는 중" : `총 학습 시간 ${displayTime}`}
$loading={isLoading}
>
- {displayTime}
+ {isLoading ? : displayTime}
{stopButtonPosition === "RIGHT" && stopButton}
diff --git a/apps/mac/src/renderer/src/features/record/ui/todo/Todo.tsx b/apps/mac/src/renderer/src/features/record/ui/todo/Todo.tsx
index 836b8189..94eb6fb3 100644
--- a/apps/mac/src/renderer/src/features/record/ui/todo/Todo.tsx
+++ b/apps/mac/src/renderer/src/features/record/ui/todo/Todo.tsx
@@ -1,4 +1,4 @@
-import { Button, Popover, Select, Tooltip } from "@/shared/ui";
+import { Button, Popover, Select, SkeletonRows, Tooltip } from "@/shared/ui";
import * as S from "./Todo.style";
import { useTodoList } from "../../model/useTodoList";
import { getErrorMessage } from "@/shared/lib";
@@ -84,7 +84,7 @@ export const Todo = ({ selectedDate }: TodoProps) => {
)}
{tasksQuery.isPending ? (
- 할 일을 불러오는 중이에요.
+
) : hasInitialTasksError ? (
{getErrorMessage(tasksQuery.error, "할 일을 불러오지 못했어요.")}
diff --git a/apps/mac/src/renderer/src/features/rival-management/ui/rivals-management/RivalsManagement.tsx b/apps/mac/src/renderer/src/features/rival-management/ui/rivals-management/RivalsManagement.tsx
index 59270cba..6ce610ca 100644
--- a/apps/mac/src/renderer/src/features/rival-management/ui/rivals-management/RivalsManagement.tsx
+++ b/apps/mac/src/renderer/src/features/rival-management/ui/rivals-management/RivalsManagement.tsx
@@ -1,6 +1,6 @@
import { useState, type ChangeEvent } from "react";
import * as S from "./RivalsManagement.style";
-import { Button } from "@/shared/ui/button";
+import { Button, SkeletonRows } from "@/shared/ui";
import { Dialog } from "@/shared/ui/dialog";
import { SearchInput } from "@/shared/ui/search-input";
import { SlideSelector } from "@/shared/ui/slide-selector";
@@ -80,9 +80,7 @@ export const RivalsManagementDialog = ({ isOpen, onClose, rival }: AddRivalsDial
{rival.queries.myRivals.isPending ? (
-
- 현재 라이벌 정보를 확인하는 중이에요.
-
+
) : rival.queries.myRivals.isError ? (
현재 라이벌 정보를 확인하지 못했어요.
@@ -99,9 +97,7 @@ export const RivalsManagementDialog = ({ isOpen, onClose, rival }: AddRivalsDial
) : rival.queries.available.isPending ? (
-
- 추가할 수 있는 라이벌을 불러오는 중이에요.
-
+
) : rival.queries.available.isError ? (
라이벌 목록을 불러오지 못했어요.
@@ -204,9 +200,7 @@ export const RivalsManagementDialog = ({ isOpen, onClose, rival }: AddRivalsDial
{rival.queries.myRivals.isPending ? (
-
- 현재 라이벌을 불러오는 중이에요.
-
+
) : rival.queries.myRivals.isError && !rival.rivalsData ? (
현재 라이벌을 불러오지 못했어요.
@@ -268,9 +262,7 @@ export const RivalsManagementDialog = ({ isOpen, onClose, rival }: AddRivalsDial
{rival.queries.requests.isPending ? (
-
- 라이벌 신청 목록을 불러오는 중이에요.
-
+
) : rival.queries.requests.isError && !rival.rivalSignAll ? (
라이벌 신청 목록을 불러오지 못했어요.
diff --git a/apps/mac/src/renderer/src/features/section-progress/ui/SectionProgress.tsx b/apps/mac/src/renderer/src/features/section-progress/ui/SectionProgress.tsx
index 745a7d04..7c8be604 100644
--- a/apps/mac/src/renderer/src/features/section-progress/ui/SectionProgress.tsx
+++ b/apps/mac/src/renderer/src/features/section-progress/ui/SectionProgress.tsx
@@ -1,7 +1,7 @@
import * as S from "./SectionProgress.style";
import { useGetMyProfile } from "@/entities/user";
import { MajorEnum, useMajorSectionQuery } from "@/entities/roadmap";
-import { Button } from "@/shared/ui";
+import { Button, SkeletonPanel } from "@/shared/ui";
interface SectionProgressProps {
completed?: number;
@@ -25,10 +25,8 @@ export const SectionProgress = ({
if (needsRemoteCounts && (profileQuery.isLoading || sectionQuery.isLoading)) {
return (
-
-
- 진행률을 불러오는 중이에요.
-
+
+
);
}
diff --git a/apps/mac/src/renderer/src/features/section/components/PreviewModal.style.ts b/apps/mac/src/renderer/src/features/section/components/PreviewModal.style.ts
index e87a0e78..ba494b20 100644
--- a/apps/mac/src/renderer/src/features/section/components/PreviewModal.style.ts
+++ b/apps/mac/src/renderer/src/features/section/components/PreviewModal.style.ts
@@ -1,4 +1,4 @@
-import styled, { css, keyframes } from "styled-components";
+import styled, { css } from "styled-components";
import { font } from "@clash/design-tokens/font";
import { palette } from "@clash/design-tokens/theme";
import Check from "@/shared/ui/assets/check.svg";
@@ -7,25 +7,8 @@ import Flag from "../assets/flag.svg";
import Arrow from "../assets/arrow.svg";
import Star from "../assets/star.svg";
-const skeletonWave = keyframes`
- 0% {
- background-position: 200% 0;
- }
-
- 100% {
- background-position: -200% 0;
- }
-`;
-
const skeletonBase = css`
- background: linear-gradient(
- 90deg,
- ${({ theme }) => theme.fill.normal} 0%,
- ${({ theme }) => theme.fill.alternative} 50%,
- ${({ theme }) => theme.fill.normal} 100%
- );
- background-size: 200% 100%;
- animation: ${skeletonWave} 1.6s ease-in-out infinite;
+ background-color: ${({ theme }) => theme.fill.neutral};
`;
export const PreviewModalWrapper = styled.div`
diff --git a/apps/mac/src/renderer/src/pages/competition/rival/RivalCompetitionPage.tsx b/apps/mac/src/renderer/src/pages/competition/rival/RivalCompetitionPage.tsx
index ea8d8041..a2a87970 100644
--- a/apps/mac/src/renderer/src/pages/competition/rival/RivalCompetitionPage.tsx
+++ b/apps/mac/src/renderer/src/pages/competition/rival/RivalCompetitionPage.tsx
@@ -1,7 +1,7 @@
import * as S from "./RivalCompetitionPage.style";
import { RivalCompetition } from "@/features/competition";
import { RivalsManagementDialog, useRival } from "@/features/rival-management";
-import { Button } from "@/shared/ui";
+import { Button, SkeletonPanel } from "@/shared/ui";
import { getErrorMessage } from "@/shared/lib";
export const RivalCompetitionPage = () => {
@@ -24,9 +24,7 @@ export const RivalCompetitionPage = () => {
)}
{rival.queries.myRivals.isPending ? (
-
- 라이벌 경쟁 정보를 불러오는 중이에요.
-
+
) : rival.queries.myRivals.isError && !rival.rivalsData ? (
diff --git a/apps/mac/src/renderer/src/pages/group/GroupPage.style.ts b/apps/mac/src/renderer/src/pages/group/GroupPage.style.ts
index ee9cbe24..0e47dc44 100644
--- a/apps/mac/src/renderer/src/pages/group/GroupPage.style.ts
+++ b/apps/mac/src/renderer/src/pages/group/GroupPage.style.ts
@@ -19,6 +19,134 @@ export const Content = styled.div`
min-height: 0;
`;
+export const ActivitySkeleton = styled.div`
+ display: flex;
+ flex: 2;
+ min-width: 0;
+ min-height: 0;
+ flex-direction: column;
+ gap: 2.5rem;
+ padding: 2rem;
+ border-radius: 1rem;
+ background-color: ${({ theme }) => theme.background.normal};
+`;
+
+export const ActivityHeaderSkeleton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1.5rem;
+ padding: 0 1.5rem;
+`;
+
+export const TimerSkeleton = styled.div`
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ flex-direction: column;
+ gap: 0.75rem;
+`;
+
+export const StudySummarySkeleton = styled.div`
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.875rem;
+`;
+
+export const ActivityBodySkeleton = styled.div`
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ flex-direction: column;
+ gap: 2.5rem;
+`;
+
+export const GroupInfoSkeleton = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.875rem;
+`;
+
+export const GroupStatsSkeleton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+`;
+
+export const MemberGridSkeleton = styled.div`
+ display: grid;
+ flex: 1;
+ min-height: 0;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ align-content: start;
+ column-gap: 1rem;
+ row-gap: 1.25rem;
+ overflow: hidden;
+`;
+
+export const MemberSkeleton = styled.div`
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+`;
+
+export const GroupSideSkeleton = styled.div`
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ flex-direction: column;
+ padding: 1.5rem;
+ border-radius: 1rem;
+ background-color: ${({ theme }) => theme.background.normal};
+`;
+
+export const GroupSideHeaderSkeleton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+`;
+
+export const GroupListSkeleton = styled.div`
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ flex-direction: column;
+ margin-top: 1rem;
+ padding-bottom: 4rem;
+ overflow: hidden;
+`;
+
+export const GroupListItemSkeleton = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ width: 100%;
+ padding: 1.25rem 0.75rem;
+ border-bottom: 2px solid ${({ theme }) => theme.fill.alternative};
+`;
+
+export const GroupListItemLeftSkeleton = styled.div`
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 0.75rem;
+`;
+
+export const GroupListItemRightSkeleton = styled.div`
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 0.5rem;
+`;
+
export const PageState = styled.div`
display: flex;
flex: 1;
diff --git a/apps/mac/src/renderer/src/pages/group/GroupPage.tsx b/apps/mac/src/renderer/src/pages/group/GroupPage.tsx
index d6abe283..349ea0d7 100644
--- a/apps/mac/src/renderer/src/pages/group/GroupPage.tsx
+++ b/apps/mac/src/renderer/src/pages/group/GroupPage.tsx
@@ -6,6 +6,7 @@ import { shiftRecordDate, useTodayRecordDate } from "@/features/record";
import { GroupActivity } from "@/widgets/group-activity";
import { Button } from "@/shared/ui";
import { getErrorMessage } from "@/shared/lib";
+import { GroupPageSkeleton } from "./GroupPageSkeleton";
export const GroupPage = () => {
const [selectedGroupId, setSelectedGroupId] = useState(null);
@@ -38,10 +39,7 @@ export const GroupPage = () => {
if (isGroupsPending && !myGroupsResponse) {
return (
-
- 그룹을 불러오는 중이에요.
- 잠시만 기다려 주세요.
-
+
);
}
diff --git a/apps/mac/src/renderer/src/pages/group/GroupPageSkeleton.tsx b/apps/mac/src/renderer/src/pages/group/GroupPageSkeleton.tsx
new file mode 100644
index 00000000..777f9fad
--- /dev/null
+++ b/apps/mac/src/renderer/src/pages/group/GroupPageSkeleton.tsx
@@ -0,0 +1,59 @@
+import { Skeleton } from "@/shared/ui";
+import * as S from "./GroupPage.style";
+
+const memberSkeletons = Array.from({ length: 10 }, (_, index) => (
+
+
+
+
+
+));
+
+const groupSkeletons = Array.from({ length: 5 }, (_, index) => (
+
+
+
+
+
+
+
+
+
+
+));
+
+export const GroupPageSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {memberSkeletons}
+
+
+
+
+
+
+
+
+ {groupSkeletons}
+
+
+);
diff --git a/apps/mac/src/renderer/src/pages/home/ui/Home.tsx b/apps/mac/src/renderer/src/pages/home/ui/Home.tsx
index 6522d3eb..5c58866c 100644
--- a/apps/mac/src/renderer/src/pages/home/ui/Home.tsx
+++ b/apps/mac/src/renderer/src/pages/home/ui/Home.tsx
@@ -3,8 +3,7 @@ import { Active, Ranking, Transition } from "@/features/home";
import { Rival } from "@/widgets/home-rivals";
import { useQueryClient } from "@tanstack/react-query";
import axios from "axios";
-import { Dialog } from "@/shared/ui";
-import { Skeleton } from "@/shared/ui/skeleton";
+import { DashboardCardSkeleton, Dialog } from "@/shared/ui";
import * as S from "./Home.style";
import {
activeQueryKeys,
@@ -103,10 +102,10 @@ export const Home = () => {
if (isChecking) {
return (
<>
-
-
-
-
+
+
+
+
>
);
}
@@ -114,10 +113,10 @@ export const Home = () => {
if (isGitHubNotReady) {
return (
<>
-
-
-
-
+
+
+
+
{!isGitHubNotLinked && (