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: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/calendar/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 캘린더 데이터를 조회하는 동안 월간 그리드의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="calendar" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/chat/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 채팅 초기 메시지와 참여자 데이터를 조회하는 동안 대화 패널을 표시합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="chat" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 대시보드 레이아웃과 위젯을 조회하는 동안 카드 배치를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="dashboard" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/files/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 자료실 목록을 조회하는 동안 자료 카드 목록의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="list" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/meeting-notes/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 회의록 목록을 조회하는 동안 목록 카드의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="list" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/notices/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 공지 목록을 조회하는 동안 공지 카드와 상세 영역의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="list" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/notifications/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 전체 알림 목록을 조회하는 동안 알림 카드 목록의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="list" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/progress-chart/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 진행률 데이터를 계산하는 동안 차트 카드의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="dashboard" />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 프로젝트 업무 보드를 조회하는 동안 칸반 열과 업무 카드의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="kanban" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/settings/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 워크스페이스 설정과 멤버 정보를 조회하는 동안 설정 폼의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="settings" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/sprint-board/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 스프린트와 업무를 조회하는 동안 스프린트 보드의 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="sprint" />;
}
5 changes: 5 additions & 0 deletions src/app/workspaces/[workspaceId]/work-schedule/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 주간 근무유형과 스케줄을 조회하는 동안 근무표의 행과 셀 자리를 유지합니다.
import { WorkspacePageSkeleton } from '@/widgets/workspace-page-skeleton';
export default function Loading() {
return <WorkspacePageSkeleton variant="schedule" />;
}
12 changes: 12 additions & 0 deletions src/shared/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 데이터 로딩 중 실제 콘텐츠의 자리와 크기를 유지하는 공통 스켈레톤 블록입니다.
import { cn } from '@/shared/lib/utils';

interface SkeletonProps {
className?: string;
}

export function Skeleton({ className = '' }: SkeletonProps) {
return (
<div aria-hidden="true" className={cn('animate-pulse rounded-lg bg-slate-200/80', className)} />
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions src/widgets/workspace-page-skeleton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// 워크스페이스 페이지 로딩 스켈레톤의 공개 진입점입니다.
export { WorkspacePageSkeleton } from './ui/WorkspacePageSkeleton';
export type { WorkspacePageSkeletonVariant } from './ui/WorkspacePageSkeleton';
202 changes: 202 additions & 0 deletions src/widgets/workspace-page-skeleton/ui/WorkspacePageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// 워크스페이스 모듈 전환 중 실제 화면 구조를 유지하는 페이지별 로딩 스켈레톤입니다.
import { Skeleton } from '@/shared/ui/skeleton';

export type WorkspacePageSkeletonVariant =
'calendar' | 'chat' | 'dashboard' | 'kanban' | 'list' | 'schedule' | 'settings' | 'sprint';

interface WorkspacePageSkeletonProps {
/** 대상 모듈의 정보 밀도에 맞춰 선택하는 스켈레톤 형태입니다. */
variant: WorkspacePageSkeletonVariant;
}

const LIST_ROWS = Array.from({ length: 5 });
const TABLE_ROWS = Array.from({ length: 5 });
const TABLE_COLUMNS = Array.from({ length: 7 });

function PageHeading({ withAction = true }: { withAction?: boolean }) {
return (
<div className="mb-6 flex items-center justify-between gap-4">
<div className="space-y-2">
<Skeleton className="h-8 w-28" />
<Skeleton className="h-4 w-56" />
</div>
{withAction ? <Skeleton className="h-11 w-28" /> : null}
</div>
);
}

function ListSkeleton() {
return (
<>
<PageHeading />
<div className="max-w-4xl space-y-4">
{LIST_ROWS.map((_, index) => (
<div
key={index}
className="rounded-2xl border border-slate-200 bg-white px-6 py-5 shadow-sm"
>
<Skeleton className="h-5 w-2/5" />
<Skeleton className="mt-3 h-4 w-4/5" />
<Skeleton className="mt-2 h-4 w-1/4" />
</div>
))}
</div>
</>
);
}

function ScheduleSkeleton() {
return (
<>
<PageHeading />
<div className="mb-6 flex gap-3">
<Skeleton className="h-14 w-28" />
<Skeleton className="h-14 w-28" />
<Skeleton className="h-14 w-28" />
</div>
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<div className="grid grid-cols-[160px_repeat(7,minmax(88px,1fr))] border-b border-slate-200 p-5">
{Array.from({ length: 8 }).map((_, index) => (
<Skeleton key={index} className="mx-auto h-5 w-10" />
))}
</div>
{TABLE_ROWS.map((_, rowIndex) => (
<div
key={rowIndex}
className="grid grid-cols-[160px_repeat(7,minmax(88px,1fr))] items-center border-b border-slate-100 p-5 last:border-b-0"
>
<Skeleton className="h-9 w-32" />
{TABLE_COLUMNS.map((_, columnIndex) => (
<Skeleton key={columnIndex} className="mx-auto h-10 w-16 rounded-full" />
))}
</div>
))}
</div>
</>
);
}

function KanbanSkeleton() {
return (
<>
<PageHeading />
<div className="mb-5 flex flex-wrap gap-3">
<Skeleton className="h-11 w-48" />
<Skeleton className="h-11 w-28" />
</div>
<div className="grid min-w-[900px] grid-cols-3 gap-5">
{Array.from({ length: 3 }).map((_, columnIndex) => (
<div key={columnIndex} className="rounded-2xl bg-slate-100/80 p-4">
<Skeleton className="mb-4 h-5 w-24" />
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, cardIndex) => (
<div key={cardIndex} className="rounded-xl border border-slate-200 bg-white p-4">
<Skeleton className="h-4 w-4/5" />
<Skeleton className="mt-4 h-3 w-2/5" />
</div>
))}
</div>
</div>
))}
</div>
</>
);
}

function DashboardSkeleton() {
return (
<div className="grid grid-cols-1 gap-5 xl:grid-cols-12">
<Skeleton className="h-52 xl:col-span-7" />
<Skeleton className="h-52 xl:col-span-5" />
<Skeleton className="h-64 xl:col-span-4" />
<Skeleton className="h-64 xl:col-span-8" />
<Skeleton className="h-56 xl:col-span-6" />
<Skeleton className="h-56 xl:col-span-6" />
</div>
);
}

function CalendarSkeleton() {
return (
<>
<PageHeading withAction={false} />
<div className="mb-5 flex items-center justify-between">
<Skeleton className="h-10 w-32" />
<Skeleton className="h-10 w-28" />
</div>
<div className="grid grid-cols-7 gap-px overflow-hidden rounded-2xl border border-slate-200 bg-slate-200">
{Array.from({ length: 42 }).map((_, index) => (
<div key={index} className="min-h-28 bg-white p-3">
<Skeleton className="h-4 w-6" />
{index % 3 === 0 ? <Skeleton className="mt-4 h-5 w-full" /> : null}
</div>
))}
</div>
</>
);
}

function ChatSkeleton() {
return (
<>
<PageHeading withAction={false} />
<div className="grid min-h-[calc(100vh_-_230px)] gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.32fr)]">
<div className="flex h-[min(720px,calc(100dvh_-_230px))] min-h-[480px] flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white">
<div className="flex items-center justify-between border-b border-slate-100 px-6 py-5">
<Skeleton className="h-10 w-36" />
<Skeleton className="h-7 w-20 rounded-full" />
</div>
<div className="flex-1 space-y-6 p-6">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className={index % 2 ? 'ml-auto w-2/5' : 'w-3/5'}>
<Skeleton className="mb-2 h-3 w-20" />
<Skeleton className="h-12 w-full rounded-2xl" />
</div>
))}
</div>
<div className="border-t border-slate-100 p-5">
<Skeleton className="h-12 w-full rounded-xl" />
</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-5">
<Skeleton className="h-6 w-28" />
<div className="mt-5 space-y-4">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton key={index} className="h-9 w-full" />
))}
</div>
</div>
</div>
</>
);
}

function SettingsSkeleton() {
return (
<div className="mx-auto max-w-3xl">
<PageHeading withAction={false} />
<div className="flex gap-3 border-b border-slate-200 pb-3">
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24" />
</div>
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<Skeleton className="h-5 w-28" />
<Skeleton className="mt-5 h-11 w-full" />
<Skeleton className="mt-5 h-5 w-28" />
<Skeleton className="mt-3 h-28 w-full" />
<Skeleton className="mt-6 h-11 w-28" />
</div>
</div>
);
}

export function WorkspacePageSkeleton({ variant }: WorkspacePageSkeletonProps) {
if (variant === 'dashboard') return <DashboardSkeleton />;
if (variant === 'calendar') return <CalendarSkeleton />;
if (variant === 'chat') return <ChatSkeleton />;
if (variant === 'schedule') return <ScheduleSkeleton />;
if (variant === 'kanban' || variant === 'sprint') return <KanbanSkeleton />;
if (variant === 'settings') return <SettingsSkeleton />;
return <ListSkeleton />;
}