Vue 3 総合プロジェクト:Eコマース・バックエンド管理システム—8つのモジュール(30レッスンの卒業プロジェクト)
これは、Vue 3 チュートリアルの最終プロジェクトです。レッスン 29 で学んだすべての内容を、実際の SaaS 型 e コマースのバックエンドに応用していきます。GitHubで88k⭐を獲得しているPanJiaChen/vue-element-adminアーキテクチャをモデルとして、5,000社の販売業者と30,000点のSKUをサポートするECバックエンドを構築していきます。
このプロジェクト全体は8つの主要なモジュールで構成されており、所要時間は10~12時間です。このプロジェクトを完了すると、エンタープライズレベルのVueアプリケーションを独自に開発する能力が身につきます。
1. 学習内容
- 8つの主要モジュール(プロジェクトの初期化/ログインと認証/ダッシュボード/製品/注文/ユーザー/マーケティング/デプロイ)の完全な実装
- Vue 3 + Vite + Pinia + Vue Router + Element Plus + ECharts のフルスタック統合
- パフォーマンスの最適化(10,000件のテーブル仮想スクロール)
- 5 役割ベースのアクセス制御(RBAC)
- ECharts チャート 5 種類(ダッシュボード)
- 本番環境へのデプロイ(Vercel / Netlify / CDN)
- 完全なTypeScript
2. SaaS系Eコマース企業におけるプロダクトマネージャーの「フルスタック要件」
(1) 課題:5,000社の販売業者と30,000点のSKUに対応するECバックエンドをどのように構築するか
アリスは、管理ダッシュボード・プロジェクトの責任者に昇進しました:
Business Scenarios:
- 5,000 Small and Medium-Sized Business Owners(Per household 30-300 SKU)
- 30,000 Items SKU
- Daily 10,000 Order
- 5 Role Permissions(admin / manager / editor / viewer / guest)
- Real-time Data(Dashboard、Order、Notice)
- Multilingual(5 Language)
プロダクトマネージャーのチャーリーが仕様書を作成しました:
「アリス、当社のSaaSプラットフォームには包括的な管理画面が必要です。必要なモジュールは8つあります:ダッシュボード、製品、注文、ユーザー、マーケティング、設定、ログイン、デプロイメント。どのモジュールも不可欠です。これまでに学んだ29のレッスンをすべて活用して、Vue 3で10~12時間以内に構築してください。」
(2) Vue 3 総合プロジェクト:8モジュール構成
src/
├── api/ # API Packaging
│ ├── auth.ts
│ ├── product.ts
│ ├── order.ts
│ └── user.ts
├── assets/ # Static Resources
├── components/ # Public Components
│ ├── BaseButton.vue
│ ├── BaseTable.vue # Secondary Packaging Element Plus
│ ├── BasePagination.vue
│ ├── BaseDialog.vue
│ └── charts/ # Charts
│ ├── SalesChart.vue
│ ├── UserChart.vue
│ └── OrderChart.vue
├── composables/ # Composite Functions
│ ├── useAuth.ts
│ ├── usePermission.ts
│ ├── useTable.ts
│ └── usePagination.ts
├── layouts/ # Layout
│ ├── DefaultLayout.vue # After logging in
│ └── AuthLayout.vue # Login Page
├── router/ # Routing
│ └── index.ts
├── stores/ # Pinia
│ ├── auth.ts
│ ├── product.ts
│ ├── order.ts
│ └── app.ts # Global(Topic / Sidebar)
├── types/ # TS Type
│ ├── user.ts
│ ├── product.ts
│ └── api.ts
├── utils/ # Tools
│ ├── request.ts # axios Packaging
│ ├── auth.ts
│ └── format.ts
├── views/ # Page
│ ├── dashboard/ # Dashboard
│ ├── login/ # Log In
│ ├── product/ # Products
│ ├── order/ # Order
│ ├── user/ # User
│ ├── marketing/ # Marketing
│ └── error/ # 403 / 404
├── App.vue
└── main.ts
(3) 収益
このプロジェクトを構築した後:
- プロジェクトを完遂した経験:履歴書に記載があると有利です
- エンタープライズグレードの機能:完全に統合されたルーター + Pinia + Element Plus
- デプロイ可能:実際に本番環境での運用が可能なSaaSバックエンド
- 全30回の完全コース:講義+実践演習+導入
3. モジュール 1:プロジェクトの初期化(1.5 時間)
(1) package.json を完成させる
(1) ▶ サンプル:プロジェクトの初期設定を完了する (難易度: ⭐⭐)
Vue 3 + Vite + Pinia + Element Plus + ECharts + TypeScript を使用して、ゼロから完全なプロジェクトをセットアップする方法:
{
"name": "mercury-admin",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:e2e": "playwright test",
"lint": "eslint . --ext .vue,.ts,.tsx"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"pinia": "^2.1.0",
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.0",
"echarts": "^5.4.0",
"vue-echarts": "^7.0.0",
"axios": "^1.6.0",
"dayjs": "^1.11.0",
"vueuse": "^10.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0",
"typescript": "^5.3.0",
"unplugin-vue-components": "^0.26.0",
"unplugin-auto-import": "^0.17.0",
"unplugin-icons": "^0.17.0",
"@iconify-json/carbon": "^1.1.0",
"sass": "^1.69.0",
"vitest": "^1.0.0",
"@vue/test-utils": "^2.4.0",
"@playwright/test": "^1.40.0"
}
}
(2) vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import'
import Components from 'unplugin-vue-components'
import Icons from 'unplugin-icons/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import path from 'path'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia', 'vueuse'],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
}),
Icons({ autoInstall: true })
],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
},
server: {
port: 5173,
proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } }
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts-vendor': ['echarts', 'vue-echarts']
}
}
}
}
})
4. モジュール 2:ログイン + JWT 認証 + 権限ベースのルーティング (2時間)
(1) ピニア・オーソライズド・ストア
(1) ▶ サンプル:JWT ログイン + 5 つのロールによる権限管理 (難易度: ⭐⭐)
Pinia Setup Store を使用して、トークン、ユーザー、ロール、権限を管理します:
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { loginApi, getUserInfoApi } from '@/api/auth'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>(localStorage.getItem('token') || '')
const user = ref<User | null>(null)
const roles = ref<string[]>([])
const permissions = ref<string[]>([])
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => roles.value.includes('admin'))
async function login(credentials: { username: string; password: string }) {
const { data } = await loginApi(credentials)
token.value = data.token
user.value = data.user
roles.value = data.roles
permissions.value = data.permissions
localStorage.setItem('token', data.token)
}
async function fetchUserInfo() {
const { data } = await getUserInfoApi()
user.value = data.user
roles.value = data.roles
permissions.value = data.permissions
}
function logout() {
token.value = ''
user.value = null
roles.value = []
permissions.value = []
localStorage.removeItem('token')
}
return { token, user, roles, permissions, isLoggedIn, isAdmin, login, fetchUserInfo, logout }
}, {
persist: {
key: 'mercury-auth',
paths: ['token', 'user', 'roles', 'permissions']
}
})
(2) ルートガード
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/views/login/LoginPage.vue') },
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: { requiresAuth: true },
children: [
{ path: '', redirect: '/dashboard' },
{ path: 'dashboard', component: () => import('@/views/dashboard/DashboardPage.vue') },
{ path: 'products', component: () => import('@/views/product/ProductListPage.vue') }
]
}
]
})
router.beforeEach((to, from, next) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return next({ name: 'login', query: { redirect: to.fullPath } })
}
next()
})
export default router
(3) カスタム権限ディレクティブ
// directives/permission.ts
import { useAuthStore } from '@/stores/auth'
export const permission = {
mounted(el: HTMLElement, binding: { value: string }) {
const auth = useAuthStore()
if (!auth.permissions.includes(binding.value)) {
el.parentNode?.removeChild(el)
}
}
}
<button v-permission="'product.create'">Create Product</button>
5. モジュール 3:ダッシュボード(2 時間)
(1) EChartsのチャート5つ
(1) ▶ サンプル:つのEChartsチャートを搭載したダッシュボード(難易度:⭐⭐)
ダッシュボードのコアコード:5つのチャート+4つのKPI+Element Plusのグリッドレイアウト:
<!-- views/dashboard/DashboardPage.vue -->
<template>
<div class="dashboard">
<!-- 1. KPI Card -->
<div class="kpi-grid">
<KpiCard title="Today's Sales" :value="stats.todaySales" :change="+12%" />
<KpiCard title="Orders" :value="stats.todayOrders" :change="+5%" />
<KpiCard title="Active Users" :value="stats.activeUsers" :change="+8%" />
<KpiCard title="Conversion" :value="stats.conversion" :change="-2%" />
</div>
<!-- 2. 5 View Chart -->
<el-row :gutter="20">
<el-col :span="12">
<SalesChart :data="salesData" />
</el-col>
<el-col :span="12">
<UserChart :data="userData" />
</el-col>
<el-col :span="24">
<OrderChart :data="orderData" />
</el-col>
<el-col :span="12">
<CategoryChart :data="categoryData" />
</el-col>
<el-col :span="12">
<TopProductsTable :data="topProducts" />
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getDashboardStatsApi } from '@/api/dashboard'
const stats = ref({})
const salesData = ref([])
const userData = ref([])
const orderData = ref([])
const categoryData = ref([])
const topProducts = ref([])
onMounted(async () => {
const { data } = await getDashboardStatsApi()
Object.assign(stats.value, data.stats)
salesData.value = data.sales
userData.value = data.users
orderData.value = data.orders
categoryData.value = data.categories
topProducts.value = data.topProducts
})
</script>
(2) ECharts コンポーネントのカプセル化
<!-- components/charts/SalesChart.vue -->
<template>
<el-card title="Sales Trend">
<v-chart :option="chartOption" autoresize style="height: 300px" />
</el-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
const props = defineProps<{
data: Array<{ date: string; sales: number }>
}>()
const chartOption = computed(() => ({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: props.data.map(d => d.date) },
yAxis: { type: 'value' },
series: [{
name: 'Sales',
type: 'line',
data: props.data.map(d => d.sales),
smooth: true
}]
}))
</script>
6. モジュール4:プロダクトマネジメント(1.5時間)
(1) 製品一覧 + CRUD
(1) ▶ サンプル:製品の CRUD 操作 + アクセス制御 (難易度: ⭐⭐)
プロダクトマネジメントの核心:BaseTableの再カプセル化 + 6列表示 + 権限ディレクティブ + 削除確認:
<!-- views/product/ProductListPage.vue -->
<template>
<BaseTable
:data="products"
:columns="columns"
:total="total"
:loading="loading"
@search="onSearch"
@page="onPage"
>
<template #toolbar>
<el-button type="primary" v-permission="'product.create'" @click="onCreate">
Create Product
</el-button>
</template>
<template #column-status="{ row }">
<el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
</template>
<template #column-actions="{ row }">
<el-button v-permission="'product.edit'" @click="onEdit(row)">Edit</el-button>
<el-button v-permission="'product.delete'" type="danger" @click="onDelete(row)">Delete</el-button>
</template>
</BaseTable>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getProductsApi, deleteProductApi } from '@/api/product'
const products = ref<Product[]>([])
const total = ref(0)
const loading = ref(false)
const search = ref({ name: '', category: '', status: '' })
const page = ref(1)
const size = ref(20)
const columns = [
{ key: 'id', label: 'ID', width: 80 },
{ key: 'name', label: 'Name' },
{ key: 'category', label: 'Category' },
{ key: 'price', label: 'Price', formatter: (row) => `$${row.price}` },
{ key: 'stock', label: 'Stock' },
{ key: 'status', label: 'Status' }
]
async function fetchData() {
loading.value = true
const { data } = await getProductsApi({ ...search.value, page: page.value, size: size.value })
products.value = data.items
total.value = data.total
loading.value = false
}
function onSearch() { page.value = 1; fetchData() }
function onPage(p) { page.value = p; fetchData() }
async function onDelete(row) {
await ElMessageBox.confirm(`Delete ${row.name}?`, 'Confirm')
await deleteProductApi(row.id)
ElMessage.success('Deleted')
fetchData()
}
onMounted(fetchData)
</script>
(2) 製品編集ダイアログボックス
<!-- views/product/ProductEditDialog.vue -->
<template>
<el-dialog v-model="visible" :title="isEdit ? 'Edit Product' : 'Create Product'" width="700px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-form-item label="Name" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Category" prop="category">
<el-select v-model="form.category">
<el-option v-for="c in categories" :key="c" :value="c" :label="c" />
</el-select>
</el-form-item>
<el-form-item label="Price" prop="price">
<el-input-number v-model="form.price" :min="0" />
</el-form-item>
<el-form-item label="Stock" prop="stock">
<el-input-number v-model="form.stock" :min="0" />
</el-form-item>
<el-form-item label="Image">
<el-upload action="/api/upload" :show-file-list="false">
<img v-if="form.image" :src="form.image" class="preview" />
<el-button v-else>Upload</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">Cancel</el-button>
<el-button type="primary" @click="onSave">Save</el-button>
</template>
</el-dialog>
</template>
7. モジュール 5:注文管理(1 時間)
(1) 注文一覧 + 詳細
(1) ▶ サンプル:注文リストと詳細ダイアログボックス(難易度:⭐)
「Order Core」:BaseTable リスト + 詳細ドロワー + ステータス遷移:
<template>
<BaseTable :data="orders" :columns="columns" :total="total">
<template #column-status="{ row }">
<el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
</template>
<template #column-actions="{ row }">
<el-button @click="viewDetail(row)">View</el-button>
<el-button v-if="row.status === 'pending'" @click="confirmOrder(row)">Confirm</el-button>
</template>
</BaseTable>
<el-dialog v-model="detailVisible" title="Order Detail" width="800px">
<h3>Order #{{ currentOrder?.id }}</h3>
<p>User: {{ currentOrder?.user }}</p>
<p>Total: ${{ currentOrder?.total }}</p>
<h4>Items:</h4>
<ul>
<li v-for="item in currentOrder?.items" :key="item.id">
{{ item.name }} × {{ item.quantity }} = ${{ item.price * item.quantity }}
</li>
</ul>
</el-dialog>
</template>
8. モジュール 6:ユーザー管理(1 時間)
(1) ユーザー一覧 + 役割管理
(1) ▶ サンプル:ユーザーロールの管理 + RBAC の編集(難易度:⭐)
ユーザー管理コア:リスト+タグのロール+チェックボックスグループの編集ロール:
<template>
<BaseTable :data="users" :columns="columns">
<template #column-roles="{ row }">
<el-tag v-for="role in row.roles" :key="role" :type="roleType(role)">
{{ role }}
</el-tag>
</template>
<template #column-actions="{ row }">
<el-button @click="editRoles(row)">Edit Roles</el-button>
</template>
</BaseTable>
<el-dialog v-model="rolesDialog" title="Edit Roles">
<el-checkbox-group v-model="selectedRoles">
<el-checkbox v-for="r in allRoles" :key="r" :value="r">{{ r }}</el-checkbox>
</el-checkbox-group>
<template #footer>
<el-button @click="saveRoles">Save</el-button>
</template>
</el-dialog>
</template>
(2) 5つの役割ごとの権限
// utils/roles.ts
export const ROLES = {
admin: {
label: 'Admin',
permissions: ['*'] // Full Access
},
manager: {
label: 'Manager',
permissions: ['product.*', 'order.*', 'user.read']
},
editor: {
label: 'Editor',
permissions: ['product.read', 'product.edit', 'order.read']
},
viewer: {
label: 'Viewer',
permissions: ['*.read']
},
guest: {
label: 'Guest',
permissions: ['product.read']
}
}
9. モジュール7:マーケティングキャンペーン(1時間)
(1) クーポン管理
(1) ▶ サンプル:クーポン一覧+期間(難易度 ⭐)
マーケティングモジュールのコア機能:el-table + タグステータス + 時刻形式:
<template>
<el-table :data="coupons" stripe>
<el-table-column prop="code" label="Code" />
<el-table-column prop="type" label="Type" :formatter="formatType" />
<el-table-column prop="discount" label="Discount" />
<el-table-column label="Status">
<template #default="{ row }">
<el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Valid Period">
<template #default="{ row }">
{{ formatDate(row.startAt) }} ~ {{ formatDate(row.endAt) }}
</template>
</el-table-column>
</el-table>
</template>
10. モジュール 8:導入(1 時間)
(1) ビルドの最適化
(1) ▶ サンプル:Vite における手動のチャンク分割の最適化 (難易度: ⭐⭐)
モジュールコアのデプロイ:手動によるチャンク分割(vue / echarts / ui / utils):
// vite.config.ts(Production Environment Optimization)
build: {
outDir: 'dist',
sourcemap: true, // Used for Sentry
minify: 'esbuild',
cssMinify: 'lightningcss',
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts-vendor': ['echarts', 'vue-echarts'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'dayjs']
}
}
}
}
(2) Vercelへのデプロイ
npm install -g vercel
vercel --prod
// vercel.json
{
"build": { "command": "npm run build" },
"outputDirectory": "dist",
"framework": "vite",
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
(3) パフォーマンスの監視
// main.ts
import * as Sentry from '@sentry/vue'
Sentry.init({
app,
dsn: 'https://your-dsn@sentry.io/123',
tracesSampleRate: 0.1
})
11. 8つのモジュールすべてのコード量統計
| モジュール | コード行数 | 主な機能 |
|---|---|---|
| 1 プロジェクトの初期化 | 300 | Vite + Pinia + Router + Element Plus + ECharts |
| 2 認証 | 400 | JWT + ルートガード + パーミッション・ディレクティブ + 5 つのロール |
| 3つのダッシュボード | 500 | 5つのECharts + 4つのKPI + リアルタイムデータ |
| 4 プロダクトマネジメント | 600 | CRUD + 一覧表示 + 編集 + 画像アップロード + 権限設定 |
| 5 注文管理 | 400 | 一覧 + 詳細 + ステータス変更 + エクスポート |
| 6 ユーザー管理 | 300 | 一覧 + 役割管理 + 権限編集 |
| 7つのマーケティングキャンペーン | 300 | クーポン + 期間 + ステートマシン |
| 8 デプロイとリリース | 200 | Viteのビルド最適化 + Vercel + Sentry |
| 合計 | 3,000 | 8モジュールからなる完全なSaaSバックエンド |
12. クイックリファレンス:よくある5つの間違い
| エラー | 症状 | 解決策 |
|---|---|---|
| 初回読み込み時に画面が真っ白になる | ルーター/Piniaの読み込み順序エラー | main.ts内で、app.use(pinia) の前に app.use(router) を呼び出してください |
| 権限の有効期限が切れています | 役割が更新されていません | ウォッチ権限 |
| 10,000回のスタッター発生 | テーブルのパフォーマンス低下 | 仮想スクロール el-table-v2 |
| EChartsが表示されない | コンテナの高さが0 | style="height: 300px" を設定 |
| デプロイ後に404エラーが発生 | SPAのルーティングに関する問題 | Vercelによる設定の書き換え |
❓ よくある質問
el-table-v2(Element Plus 公式バーチャルスクロール版)または vue-virtual-scroller を使用してください。パフォーマンスが100倍向上します。vercel --prod 1つのコマンドで完了。📖 まとめ
- Vue 3 総合プロジェクト:8モジュール構成のEコマースSaaSバックエンド(10~12時間)
- 技術スタック:Vue 3 + Vite + Pinia + Vue Router + Element Plus + ECharts + TypeScript
- モジュール 8:プロジェクトの初期化/ログインと認証/ダッシュボード/製品/注文/ユーザー/マーケティング/デプロイ
- 5つの役割に基づく権限システム(管理者/マネージャー/編集者/閲覧者/ゲスト)
- EChartsのチャート5種類(折れ線/棒グラフ/円グラフ/ファンネル/リーダーボード)
- Vercelへのデプロイ(1つのコマンド+SPAのリライト)
- 完全なTypeScript対応 + パフォーマンス最適化(仮想スクロール/手動チャンク/Sentry)
13. 🎉 レッスン30を完了しました!
全30レッスンのVue 3コースを修了されたことをお祝い申し上げます! 基礎からエンタープライズレベルのプロジェクトまで、これまでに以下の内容を習得されました:
- ✅ View 3 Composition API の習得
- ✅ Pinia + Vue Router + Element Plus のフルスタック統合
- ✅ TypeScript + Vite のプロジェクト設定
- ✅ パフォーマンスの最適化 + エラー処理 + テスト
- ✅ デプロイとリリース(Vercel + Sentry)
- ✅ エンタープライズレベルのVueアプリケーションを独自に開発できる
今後の対応に関する提言:
- 🔥 5言語(中国語 → 英語 → 日本語/ポルトガル語/アラビア語)での翻訳を開始
- 🔥 React入門チュートリアル(35~40時間)
- 🔥 Nuxt 3 入門チュートリアル(30時間)
- 🔥 実践的なプロジェクトのアップグレード(AI機能搭載)
📝 練習問題
(1) 基礎問題(難易度:⭐)
モジュール 1(プロジェクトの初期化)とモジュール 2(ログインと認証)を完了してください:
- Vite + Vue 3 + Pinia + Element Plus
- ログインページ + ダッシュボード(プレースホルダー) 3.5 ロール権限の基礎
(2) 上級問題(難易度:⭐⭐)
4つの主要モジュール(初期化/ログイン/ダッシュボード/製品)を完了してください:
- Vite および TypeScript の設定を完了する
- 5つのEChartsチャート
- 製品のCRUD(10フィールド)
(3) チャレンジ問題(難易度:⭐⭐⭐)
8つのモジュールすべてを完了し、デプロイを行う:
- 全8モジュール(10~12時間)
- TypeScriptの厳格な型付け
- パフォーマンスの最適化(仮想スクロール/手動チャンク)
- テストカバレッジ(5つのコンポーネント+3つのE2Eテスト)
- Vercelへのデプロイ + Sentryとの連携
- 30レッスンのVueチュートリアルを修了——卒業! 🎉



