diff --git a/apps/web/src/app/home/_apis/postCreateTournament.ts b/apps/web/src/apis/postCreateTournament.ts
similarity index 93%
rename from apps/web/src/app/home/_apis/postCreateTournament.ts
rename to apps/web/src/apis/postCreateTournament.ts
index 81b48e57..4503a030 100644
--- a/apps/web/src/app/home/_apis/postCreateTournament.ts
+++ b/apps/web/src/apis/postCreateTournament.ts
@@ -1,11 +1,10 @@
 import { clientApi } from '@/apis/client';
 import { ENDPOINTS } from '@/consts/api';
 import type { ApiResponseT } from '@/types/api';
-
 import type {
   PostCreateTournamentRequestT,
   PostCreateTournamentResponseT,
-} from '../_types/tournament';
+} from '@/types/tournament';
 
 export const postCreateTournament = async (body: PostCreateTournamentRequestT) => {
   const { data } = await clientApi.post<ApiResponseT<PostCreateTournamentResponseT>>(
diff --git a/apps/web/src/app/archive/_common/_components/ArchivePageLayout.tsx b/apps/web/src/app/archive/_common/_components/ArchivePageLayout.tsx
deleted file mode 100644
index 4885c43e..00000000
--- a/apps/web/src/app/archive/_common/_components/ArchivePageLayout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Header, HeaderIcon } from '@/components/header';
-
-type Props = {
-  title: string;
-  children: React.ReactNode;
-};
-
-function ArchivePageLayout({ title, children }: Props) {
-  return (
-    <div className="flex min-h-dvh flex-col bg-bg-layer-basement">
-      <div className="sticky top-0 z-20 inline-flex w-full flex-col items-start gap-5 bg-bg-layer-basement px-5 pt-padding-top pb-6">
-        <div className="flex w-full flex-col gap-[5px]">
-          <Header right={<HeaderIcon name="ALARM" />} />
-          <h1 className="text-[28px] leading-[137.5%] font-bold tracking-[-0.708px] text-text-neutral-primary">
-            {title}
-          </h1>
-        </div>
-      </div>
-      {children}
-    </div>
-  );
-}
-
-export default ArchivePageLayout;
diff --git a/apps/web/src/app/archive/tournament/_components/ArchiveTournamentClient.tsx b/apps/web/src/app/archive/tournament/_components/ArchiveTournamentClient.tsx
deleted file mode 100644
index 36ae85a6..00000000
--- a/apps/web/src/app/archive/tournament/_components/ArchiveTournamentClient.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import { Suspense, useState } from 'react';
-
-import type { TournamentStatusT } from '@/types/tournament';
-
-import ArchivePageLayout from '../../_common/_components/ArchivePageLayout';
-import TournamentHistoryList from './TournamentHistoryList';
-import TournamentStatusTab, { type TournamentStatusTabT } from './TournamentStatusTab';
-
-const STATUS_BY_TAB: Record<TournamentStatusTabT, TournamentStatusT[]> = {
-  'in-progress': ['PENDING', 'IN_PROGRESS'],
-  completed: ['COMPLETED'],
-};
-
-function ArchiveTournamentClient() {
-  const [activeTab, setActiveTab] = useState<TournamentStatusTabT>('in-progress');
-
-  return (
-    <ArchivePageLayout title="내 토너먼트">
-      <TournamentStatusTab activeTab={activeTab} onTabChange={setActiveTab} />
-      <Suspense
-        fallback={
-          <main className="flex flex-1 flex-col items-center justify-center pb-24">
-            <p className="body-1-semibold text-text-neutral-tertiary">
-              토너먼트를 불러오는 중이에요
-            </p>
-          </main>
-        }
-      >
-        <TournamentHistoryList key={activeTab} statuses={STATUS_BY_TAB[activeTab]} />
-      </Suspense>
-    </ArchivePageLayout>
-  );
-}
-
-export default ArchiveTournamentClient;
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentFab.tsx b/apps/web/src/app/archive/tournament/_components/TournamentFab.tsx
new file mode 100644
index 00000000..f06758e6
--- /dev/null
+++ b/apps/web/src/app/archive/tournament/_components/TournamentFab.tsx
@@ -0,0 +1,25 @@
+import { AddIconFill } from '@/assets/icons';
+import Button from '@/components/button';
+import CreateTournamentDialogContent from '@/components/common/create-tournament-dialog';
+import { Dialog, DialogTrigger } from '@/components/dialog';
+
+function TournamentFab() {
+  return (
+    <Dialog>
+      <DialogTrigger asChild>
+        <Button
+          variant="primary"
+          size="xl"
+          icon="only"
+          aria-label="토너먼트 만들기"
+          className="fixed right-[max(20px,calc(50%-480px/2+20px))] bottom-[103px] z-30"
+        >
+          <AddIconFill width={30} height={30} />
+        </Button>
+      </DialogTrigger>
+      <CreateTournamentDialogContent />
+    </Dialog>
+  );
+}
+
+export default TournamentFab;
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentHistoryContent.tsx b/apps/web/src/app/archive/tournament/_components/TournamentHistoryContent.tsx
deleted file mode 100644
index ec562773..00000000
--- a/apps/web/src/app/archive/tournament/_components/TournamentHistoryContent.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
-
-import { getTournamentList } from '@/apis/getTournamentList';
-import type { TournamentStatusT } from '@/types/tournament';
-import { getQueryClient } from '@/utils/queryClient';
-
-import ArchiveTournamentClient from './ArchiveTournamentClient';
-
-const DEFAULT_STATUSES: TournamentStatusT[] = ['PENDING', 'IN_PROGRESS'];
-
-async function TournamentHistoryContent() {
-  const queryClient = getQueryClient();
-
-  await queryClient.prefetchQuery({
-    queryKey: ['tournamentList', DEFAULT_STATUSES],
-    queryFn: () => getTournamentList(DEFAULT_STATUSES),
-  });
-
-  return (
-    <HydrationBoundary state={dehydrate(queryClient)}>
-      <ArchiveTournamentClient />
-    </HydrationBoundary>
-  );
-}
-
-export default TournamentHistoryContent;
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentHistoryList.tsx b/apps/web/src/app/archive/tournament/_components/TournamentHistoryList.tsx
index 4246edbc..4e2de0d0 100644
--- a/apps/web/src/app/archive/tournament/_components/TournamentHistoryList.tsx
+++ b/apps/web/src/app/archive/tournament/_components/TournamentHistoryList.tsx
@@ -20,7 +20,7 @@ function TournamentHistoryList({ statuses }: Props) {
     );
 
   return (
-    <main className="hide-scrollbar flex flex-1 flex-col gap-3 overflow-y-auto pb-24">
+    <>
       {tournamentListData.map(tournament => (
         <TournamentCard
           key={tournament.tournamentId}
@@ -32,7 +32,7 @@ function TournamentHistoryList({ statuses }: Props) {
           participantCount={tournament.participantProfileImages.length}
         />
       ))}
-    </main>
+    </>
   );
 }
 
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentHistorySection.tsx b/apps/web/src/app/archive/tournament/_components/TournamentHistorySection.tsx
new file mode 100644
index 00000000..e0459ad8
--- /dev/null
+++ b/apps/web/src/app/archive/tournament/_components/TournamentHistorySection.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import { Suspense, useState } from 'react';
+
+import Spacing from '@/components/spacing';
+
+import { STATUS_BY_TAB, type TournamentStatusTabT } from '../_consts/tournamentTab';
+import TournamentHistoryList from './TournamentHistoryList';
+import TournamentHistorySkeleton from './TournamentHistorySkeleton';
+import TournamentStatusTab from './TournamentStatusTab';
+
+type Props = {
+  initialTab: TournamentStatusTabT;
+};
+
+function TournamentHistorySection({ initialTab }: Props) {
+  const [activeTab, setActiveTab] = useState<TournamentStatusTabT>(initialTab);
+
+  /** 서버 왕복 없이 주소만 동기화 */
+  const handleTabChange = (tab: TournamentStatusTabT) => {
+    setActiveTab(tab);
+    window.history.replaceState(null, '', `?tab=${tab}`);
+  };
+
+  return (
+    <>
+      <div className="sticky top-0 z-20 flex w-full flex-col bg-bg-layer-basement pt-padding-top">
+        <h1 className="heading-1-bold text-text-neutral-primary">내 토너먼트</h1>
+        <Spacing size={16} />
+        <TournamentStatusTab activeTab={activeTab} onTabChange={handleTabChange} />
+      </div>
+      <main className="hide-scrollbar flex flex-1 flex-col gap-4 overflow-y-auto pt-6 pb-24">
+        <Suspense key={activeTab} fallback={<TournamentHistorySkeleton />}>
+          <TournamentHistoryList statuses={STATUS_BY_TAB[activeTab]} />
+        </Suspense>
+      </main>
+    </>
+  );
+}
+
+export default TournamentHistorySection;
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentHistorySkeleton.tsx b/apps/web/src/app/archive/tournament/_components/TournamentHistorySkeleton.tsx
new file mode 100644
index 00000000..69c88875
--- /dev/null
+++ b/apps/web/src/app/archive/tournament/_components/TournamentHistorySkeleton.tsx
@@ -0,0 +1,15 @@
+import TournamentCardSkeleton from '@/components/tournament-card/TournamentCardSkeleton';
+
+const SKELETON_COUNT = 4;
+
+function TournamentHistorySkeleton() {
+  return (
+    <>
+      {Array.from({ length: SKELETON_COUNT }).map((_, index) => (
+        <TournamentCardSkeleton key={index} />
+      ))}
+    </>
+  );
+}
+
+export default TournamentHistorySkeleton;
diff --git a/apps/web/src/app/archive/tournament/_components/TournamentStatusTab.tsx b/apps/web/src/app/archive/tournament/_components/TournamentStatusTab.tsx
index 7577c49f..2304c9c2 100644
--- a/apps/web/src/app/archive/tournament/_components/TournamentStatusTab.tsx
+++ b/apps/web/src/app/archive/tournament/_components/TournamentStatusTab.tsx
@@ -1,34 +1,42 @@
 'use client';
 
+import type { ComponentType, SVGProps } from 'react';
+
+import { BasketIconOutline, ReciptIconOutline } from '@/assets/icons';
 import { cn } from '@/utils/cn';
 
-export type TournamentStatusTabT = 'in-progress' | 'completed';
+import type { TournamentStatusTabT } from '../_consts/tournamentTab';
 
 type Props = {
   activeTab: TournamentStatusTabT;
   onTabChange: (tab: TournamentStatusTabT) => void;
 };
 
-const TABS: { value: TournamentStatusTabT; label: string }[] = [
-  { value: 'in-progress', label: '진행 중' },
-  { value: 'completed', label: '완료' },
+const TABS: {
+  value: TournamentStatusTabT;
+  label: string;
+  Icon: ComponentType<SVGProps<SVGSVGElement>>;
+}[] = [
+  { value: 'ongoing', label: '진행 중', Icon: BasketIconOutline },
+  { value: 'completed', label: '완료', Icon: ReciptIconOutline },
 ];
 
 function TournamentStatusTab({ activeTab, onTabChange }: Props) {
   return (
     <div className="flex w-full border-b border-border-neutral-muted">
-      {TABS.map(tab => (
+      {TABS.map(({ value, label, Icon }) => (
         <button
-          key={tab.value}
+          key={value}
           type="button"
-          onClick={() => onTabChange(tab.value)}
+          onClick={() => onTabChange(value)}
           className={cn(
-            'relative flex h-11 flex-1 cursor-pointer items-center justify-center body-1-semibold transition-colors',
-            activeTab === tab.value ? 'text-text-neutral-primary' : 'text-text-neutral-tertiary'
+            'relative flex h-11 flex-1 cursor-pointer items-center justify-center gap-1 body-1-semibold transition-colors',
+            activeTab === value ? 'text-text-neutral-primary' : 'text-text-neutral-tertiary'
           )}
         >
-          {tab.label}
-          {activeTab === tab.value && (
+          <Icon className="size-5" />
+          {label}
+          {activeTab === value && (
             <span className="absolute inset-x-0 bottom-0 h-0.5 bg-text-neutral-primary" />
           )}
         </button>
diff --git a/apps/web/src/app/archive/tournament/_consts/tournamentTab.ts b/apps/web/src/app/archive/tournament/_consts/tournamentTab.ts
new file mode 100644
index 00000000..802c0db7
--- /dev/null
+++ b/apps/web/src/app/archive/tournament/_consts/tournamentTab.ts
@@ -0,0 +1,13 @@
+import type { TournamentStatusT } from '@/types/tournament';
+
+export type TournamentStatusTabT = 'ongoing' | 'completed';
+
+/**
+ * 탭 → 조회할 토너먼트 상태 매핑
+ * - 진행 중(ongoing) 탭: 미완료(담는 중 + 플레이 중) 토너먼트
+ * - 완료(completed) 탭: 완료된 토너먼트
+ */
+export const STATUS_BY_TAB: Record<TournamentStatusTabT, TournamentStatusT[]> = {
+  ongoing: ['PENDING', 'IN_PROGRESS'],
+  completed: ['COMPLETED'],
+};
diff --git a/apps/web/src/app/archive/tournament/page.tsx b/apps/web/src/app/archive/tournament/page.tsx
index 27d4cdc6..dde48825 100644
--- a/apps/web/src/app/archive/tournament/page.tsx
+++ b/apps/web/src/app/archive/tournament/page.tsx
@@ -1,13 +1,36 @@
+import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
+
+import { getTournamentList } from '@/apis/getTournamentList';
 import BottomTabBar from '@/components/bottom-tab-bar';
+import { getQueryClient } from '@/utils/queryClient';
+
+import TournamentFab from './_components/TournamentFab';
+import TournamentHistorySection from './_components/TournamentHistorySection';
+import { STATUS_BY_TAB, type TournamentStatusTabT } from './_consts/tournamentTab';
+
+type Props = {
+  searchParams: Promise<{ tab?: string }>;
+};
+
+async function ArchiveTournamentPage({ searchParams }: Props) {
+  const { tab } = await searchParams;
+  const initialTab: TournamentStatusTabT = tab === 'completed' ? 'completed' : 'ongoing';
 
-import TournamentHistoryContent from './_components/TournamentHistoryContent';
+  const queryClient = getQueryClient();
+  const statuses = STATUS_BY_TAB[initialTab];
+  await queryClient.prefetchQuery({
+    queryKey: ['tournamentList', statuses],
+    queryFn: () => getTournamentList(statuses),
+  });
 
-async function ArchiveTournamentPage() {
   return (
-    <>
-      <TournamentHistoryContent />
+    <div className="flex min-h-dvh flex-col bg-bg-layer-basement px-5">
+      <HydrationBoundary state={dehydrate(queryClient)}>
+        <TournamentHistorySection initialTab={initialTab} />
+      </HydrationBoundary>
+      <TournamentFab />
       <BottomTabBar />
-    </>
+    </div>
   );
 }
 
diff --git a/apps/web/src/app/home/_components/CreateTournamentDialog.tsx b/apps/web/src/app/home/_components/CreateTournamentDialog.tsx
deleted file mode 100644
index be74490c..00000000
--- a/apps/web/src/app/home/_components/CreateTournamentDialog.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import { useState } from 'react';
-
-import { BasketIconFill, EditIconFill } from '@/assets/icons';
-import Button from '@/components/button';
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogTitle,
-  DialogTrigger,
-} from '@/components/dialog';
-import Input from '@/components/input';
-
-import { usePostCreateTournament } from '../_hooks/usePostCreateTournament';
-
-/** 토너먼트 생성 시 기본 초대 마감 시각 — 현재 + 30분. */
-const DEFAULT_INVITE_DURATION_MINUTES = 30;
-
-function CreateTournamentDialog() {
-  const [open, setOpen] = useState(false);
-  const [name, setName] = useState('');
-  const { postCreateTournamentMutation, isPostCreateTournamentPending } = usePostCreateTournament();
-
-  const trimmedName = name.trim();
-  const isDisabled = trimmedName.length === 0 || isPostCreateTournamentPending;
-
-  const handleCreate = () => {
-    if (isDisabled) return;
-    postCreateTournamentMutation(
-      { name: trimmedName, inviteDurationMinutes: DEFAULT_INVITE_DURATION_MINUTES },
-      {
-        onSuccess: () => {
-          setOpen(false);
-          setName('');
-        },
-      }
-    );
-  };
-
-  return (
-    <Dialog open={open} onOpenChange={setOpen}>
-      <DialogTrigger asChild>
-        <button
-          type="button"
-          aria-label="새 토너먼트 만들기"
-          className="flex h-[104px] cursor-pointer flex-col rounded-2xl bg-gray-900 p-4"
-        >
-          <span className="text-left body-1-semibold whitespace-pre-line text-base-50">
-            {'새 토너먼트\n만들기'}
-          </span>
-          <BasketIconFill className="size-7.5 self-end text-white" />
-        </button>
-      </DialogTrigger>
-      <DialogContent showCloseButton={false} className="flex flex-col gap-5">
-        <DialogDescription className="sr-only">새 토너먼트 생성 다이얼로그</DialogDescription>
-        <DialogTitle className="text-center heading-1-bold text-text-neutral-primary">
-          새 토너먼트
-        </DialogTitle>
-        <div className="flex flex-col gap-[15px]">
-          <Input
-            label="토너먼트 이름"
-            placeholder="ex.이번주 신발 고르기"
-            value={name}
-            onChange={e => setName(e.target.value)}
-            autoFocus
-            maxLength={30}
-            right={isDisabled ? <EditIconFill className="size-5" /> : null}
-          />
-          <Button size="lg" variant="primary" disabled={isDisabled} onClick={handleCreate}>
-            생성하기
-          </Button>
-        </div>
-      </DialogContent>
-    </Dialog>
-  );
-}
-
-export default CreateTournamentDialog;
diff --git a/apps/web/src/app/home/_components/tournament-list/TournamentListSkeleton.tsx b/apps/web/src/app/home/_components/tournament-list/TournamentListSkeleton.tsx
deleted file mode 100644
index 28f76c3f..00000000
--- a/apps/web/src/app/home/_components/tournament-list/TournamentListSkeleton.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import Skeleton from '@/components/skeleton';
-
-/** prefetch 되는 동안 노출할 카드 개수. 리스트 limit(3)과 맞춤. */
-const SKELETON_COUNT = 3;
-
-function TournamentCardSkeleton() {
-  return (
-    <div className="flex w-full items-start gap-2.5 rounded-xl bg-base-50 px-5 py-3">
-      {/* 썸네일 */}
-      <div className="relative h-[72px] w-[85px] shrink-0">
-        <Skeleton className="absolute top-1 left-0 size-[63px] rounded-[16px] border-[3px] border-white" />
-      </div>
-
-      {/* 텍스트 영역 */}
-      <div className="flex flex-1 flex-col gap-2 self-center">
-        <div className="flex items-center gap-2">
-          <Skeleton className="h-6 w-14 rounded-full" />
-          <Skeleton className="h-5 w-32" />
-        </div>
-        <div className="flex items-center -space-x-1.5">
-          <Skeleton shape="circle" className="size-6 border-2 border-base-50" />
-          <Skeleton shape="circle" className="size-6 border-2 border-base-50" />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function TournamentListSkeleton() {
-  return (
-    <>
-      {Array.from({ length: SKELETON_COUNT }).map((_, index) => (
-        <TournamentCardSkeleton key={index} />
-      ))}
-    </>
-  );
-}
-
-export default TournamentListSkeleton;
diff --git a/apps/web/src/app/home/_components/tournament-list/index.tsx b/apps/web/src/app/home/_components/tournament-list/index.tsx
index 53968c4b..f05728df 100644
--- a/apps/web/src/app/home/_components/tournament-list/index.tsx
+++ b/apps/web/src/app/home/_components/tournament-list/index.tsx
@@ -2,10 +2,10 @@ import Link from 'next/link';
 import { Suspense } from 'react';
 
 import { ChevronForwardIconFill } from '@/assets/icons';
+import TournamentCardSkeleton from '@/components/tournament-card/TournamentCardSkeleton';
 import { ROUTES } from '@/consts/route';
 import type { TournamentStatusT } from '@/types/tournament';
 
-import TournamentListSkeleton from './TournamentListSkeleton';
 import TournamentListClient from './client';
 
 const TOURNAMENT_LIST_STATUS: TournamentStatusT[] = ['PENDING', 'IN_PROGRESS'];
@@ -20,7 +20,15 @@ function TournamentList() {
         </Link>
       </div>
 
-      <Suspense fallback={<TournamentListSkeleton />}>
+      <Suspense
+        fallback={
+          <>
+            {Array.from({ length: 3 }).map((_, index) => (
+              <TournamentCardSkeleton key={index} />
+            ))}
+          </>
+        }
+      >
         <TournamentListClient statuses={TOURNAMENT_LIST_STATUS} />
       </Suspense>
     </section>
diff --git a/apps/web/src/app/home/_types/tournament.ts b/apps/web/src/app/home/_types/tournament.ts
deleted file mode 100644
index 69753f28..00000000
--- a/apps/web/src/app/home/_types/tournament.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export type PostCreateTournamentRequestT = {
-  name: string;
-  /** 초대 마감까지 남은 분 단위 시간 (1~1440). 미지정 시 서버 기본값 사용. */
-  inviteDurationMinutes?: number;
-};
-
-export type PostCreateTournamentResponseT = {
-  tournamentId: number;
-};
diff --git a/apps/web/src/app/home/page.tsx b/apps/web/src/app/home/page.tsx
index 543513c1..70840486 100644
--- a/apps/web/src/app/home/page.tsx
+++ b/apps/web/src/app/home/page.tsx
@@ -1,14 +1,16 @@
 import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
 
 import { getMe } from '@/apis/getMe';
+import { BasketIconFill } from '@/assets/icons';
 import PiKiLogo from '@/assets/images/piki-logo-text.svg';
 import BottomTabBar from '@/components/bottom-tab-bar';
+import CreateTournamentDialogContent from '@/components/common/create-tournament-dialog';
+import { Dialog, DialogTrigger } from '@/components/dialog';
 import { Header, HeaderIcon } from '@/components/header';
 import Spacing from '@/components/spacing';
 import { getQueryClient } from '@/utils/queryClient';
 
 import AddWishHomeDialog from './_components/AddWishHomeDialog';
-import CreateTournamentDialog from './_components/CreateTournamentDialog';
 import InviteTournamentDialog from './_components/InviteTournamentDialog';
 import MemberOnlyToast from './_components/MemberOnlyToast';
 import TournamentList from './_components/tournament-list';
@@ -35,7 +37,21 @@ async function HomePage() {
           {/** 위시 추가·토너먼트 생성·토너먼트 초대 */}
           <section className="grid grid-cols-2 gap-3 py-2.5">
             <AddWishHomeDialog />
-            <CreateTournamentDialog />
+            <Dialog>
+              <DialogTrigger asChild>
+                <button
+                  type="button"
+                  aria-label="새 토너먼트 만들기"
+                  className="flex h-[104px] cursor-pointer flex-col rounded-2xl bg-gray-900 p-4"
+                >
+                  <span className="text-left body-1-semibold whitespace-pre-line text-base-50">
+                    {'새 토너먼트\n만들기'}
+                  </span>
+                  <BasketIconFill className="size-7.5 self-end text-white" />
+                </button>
+              </DialogTrigger>
+              <CreateTournamentDialogContent />
+            </Dialog>
             <InviteTournamentDialog />
           </section>
 
diff --git a/apps/web/src/components/common/create-tournament-dialog/index.tsx b/apps/web/src/components/common/create-tournament-dialog/index.tsx
new file mode 100644
index 00000000..89c61ae2
--- /dev/null
+++ b/apps/web/src/components/common/create-tournament-dialog/index.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import { useState } from 'react';
+
+import { EditIconFill } from '@/assets/icons';
+import Button from '@/components/button';
+import { DialogContent, DialogDescription, DialogTitle } from '@/components/dialog';
+import Input from '@/components/input';
+import { usePostCreateTournament } from '@/hooks/usePostCreateTournament';
+
+/** 토너먼트 생성 시 기본 초대 마감 시각 — 현재 + 30분. */
+const DEFAULT_INVITE_DURATION_MINUTES = 30;
+
+function CreateTournamentDialogContent() {
+  const [name, setName] = useState('');
+  const { postCreateTournamentMutation, isPostCreateTournamentPending } = usePostCreateTournament();
+
+  const trimmedName = name.trim();
+  const isDisabled = trimmedName.length === 0 || isPostCreateTournamentPending;
+
+  const handleCreate = () => {
+    if (isDisabled) return;
+    postCreateTournamentMutation({
+      name: trimmedName,
+      inviteDurationMinutes: DEFAULT_INVITE_DURATION_MINUTES,
+    });
+  };
+
+  return (
+    <DialogContent showCloseButton={false} className="flex flex-col gap-5">
+      <DialogDescription className="sr-only">새 토너먼트 생성 다이얼로그</DialogDescription>
+      <DialogTitle className="text-center heading-1-bold text-text-neutral-primary">
+        새 토너먼트
+      </DialogTitle>
+      <div className="flex flex-col gap-[15px]">
+        <Input
+          label="토너먼트 이름"
+          placeholder="ex.이번주 신발 고르기"
+          value={name}
+          onChange={e => setName(e.target.value)}
+          autoFocus
+          maxLength={30}
+          right={isDisabled ? <EditIconFill className="size-5" /> : null}
+        />
+        <Button size="lg" variant="primary" disabled={isDisabled} onClick={handleCreate}>
+          생성하기
+        </Button>
+      </div>
+    </DialogContent>
+  );
+}
+
+export default CreateTournamentDialogContent;
diff --git a/apps/web/src/components/tournament-card/TournamentCardSkeleton.tsx b/apps/web/src/components/tournament-card/TournamentCardSkeleton.tsx
new file mode 100644
index 00000000..b0fd11e2
--- /dev/null
+++ b/apps/web/src/components/tournament-card/TournamentCardSkeleton.tsx
@@ -0,0 +1,30 @@
+import Skeleton from '@/components/skeleton';
+
+function TournamentCardSkeleton() {
+  return (
+    <article className="flex w-full items-start gap-2.5 rounded-xl bg-base-50 py-3 pr-3 pl-5">
+      {/* 이미지 썸네일 */}
+      <div className="relative h-[72px] w-[85px] shrink-0">
+        <div className="absolute top-0 left-[13px] flex size-[72px] items-center justify-center">
+          <Skeleton className="size-[63px] origin-center scale-[0.918] rotate-10 rounded-[16px] border-[3px] border-white opacity-60" />
+        </div>
+        <Skeleton className="absolute top-1 left-0 size-[63px] rounded-[16px] border-[3px] border-white" />
+      </div>
+
+      {/* 텍스트 영역 */}
+      <div className="flex flex-1 flex-col items-start gap-2 self-center">
+        <div className="flex items-center gap-2">
+          {/* 상태 칩 */}
+          <Skeleton className="h-[26px] w-14 rounded-lg" />
+          {/* 이름 */}
+          <Skeleton className="h-[22px] w-32" />
+        </div>
+
+        {/* 참여자 프로필 */}
+        <Skeleton shape="circle" className="size-5 border-[1.6px] border-white" />
+      </div>
+    </article>
+  );
+}
+
+export default TournamentCardSkeleton;
diff --git a/apps/web/src/app/home/_hooks/usePostCreateTournament.ts b/apps/web/src/hooks/usePostCreateTournament.ts
similarity index 93%
rename from apps/web/src/app/home/_hooks/usePostCreateTournament.ts
rename to apps/web/src/hooks/usePostCreateTournament.ts
index 40e0fc6c..6acf542d 100644
--- a/apps/web/src/app/home/_hooks/usePostCreateTournament.ts
+++ b/apps/web/src/hooks/usePostCreateTournament.ts
@@ -1,12 +1,11 @@
 import { useMutation, useQueryClient } from '@tanstack/react-query';
 import { useRouter } from 'next/navigation';
 
+import { postCreateTournament } from '@/apis/postCreateTournament';
 import { ANALYTICS_EVENT } from '@/consts/analytics';
 import { ROUTES } from '@/consts/route';
 import { logAnalyticsEvent } from '@/utils/analytics';
 
-import { postCreateTournament } from '../_apis/postCreateTournament';
-
 export const usePostCreateTournament = () => {
   const router = useRouter();
   const queryClient = useQueryClient();
diff --git a/apps/web/src/types/tournament.ts b/apps/web/src/types/tournament.ts
index dec87f4b..9bedfa37 100644
--- a/apps/web/src/types/tournament.ts
+++ b/apps/web/src/types/tournament.ts
@@ -36,3 +36,13 @@ export type GetTournamentListResponseT = {
 export type PostTournamentOCRResponseT = {
   itemIds: number[];
 };
+
+export type PostCreateTournamentRequestT = {
+  name: string;
+  /** 초대 마감까지 남은 분 단위 시간 (1~1440). 미지정 시 서버 기본값 사용. */
+  inviteDurationMinutes?: number;
+};
+
+export type PostCreateTournamentResponseT = {
+  tournamentId: number;
+};
