From jurislm-claude-plugins-stock
This skill should be used when the user asks to develop features for the "台股看盤" stock app, work with TWSE/Yahoo Finance APIs, manage portfolio or watchlist functionality, build ETF analysis features, or debug stock-related components. Activate when user mentions stock.jurislm.com, terry90918/stock repo, or Taiwan stock market app development.
npx claudepluginhub terry90918/jurislm-claude-pluginsThis skill uses the workspace's default tool permissions.
台股看盤是一個 Taiwan stock portfolio tracking & analysis 應用,提供即時 TWSE/OTC 報價、投資組合管理、股息分析、ETF 重疊分析與 0050 績效對標。
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
台股看盤是一個 Taiwan stock portfolio tracking & analysis 應用,提供即時 TWSE/OTC 報價、投資組合管理、股息分析、ETF 重疊分析與 0050 績效對標。
TWSE/Yahoo API → src/lib/ (fetch + 商業邏輯) → src/app/api/ (路由) → src/hooks/ (客戶端狀態) → src/components/ (UI)
src/app/api/**/route.ts:約 69 個 API routessrc/hooks/*.ts:20 個 hooks 檔案tests/e2e/*.spec.ts:14 個 E2E spec files| 層 | 職責 | 位置 |
|---|---|---|
| 外部 API | TWSE OpenAPI、Yahoo Finance、RSS | 網路請求 |
| Lib | 純邏輯 + 快取(無 React) | src/lib/ |
| API Routes | JSON endpoints,統一回傳格式 | src/app/api/ |
| Hooks | 客戶端狀態管理 + TTL 快取 | src/hooks/ |
| Components | Server/Client 元件分離 | src/components/ |
| Database | PostgreSQL + Prisma ORM | prisma/schema.prisma |
所有 API endpoints 統一回傳:
{ success: boolean, data?: T, error?: string }
Dynamic route params 使用 Next.js 15+ 的 params: Promise<{ param: string }> pattern(必須 await)。
| Model | 用途 | 關鍵關聯 |
|---|---|---|
| Portfolio | 投資組合容器 | → holdings[], watchlistGroups[] |
| Holding | 觀察清單/持股項目 | → portfolio, group?, transactions[] |
| Transaction | 買賣紀錄(BUY/SELL) | → holding(cascade delete) |
| WatchlistGroup | 群組管理 | → portfolio(cascade), holdings[] |
| Dividend | 除權息資料 | unique: [symbol, year, quarter] |
| StockPrice | 報價快取 | unique: symbol |
重要限制:
WATCHLIST_LIMIT = 50(每組合觀察清單上限)GROUP_LIMIT = 20(群組數量上限)GROUP_NAME_MAX_LENGTH = 20(群組名稱長度上限)"2330"),Yahoo Finance 查詢附加 .TW / .TWO| 模組 | 功能 | 快取策略 |
|---|---|---|
twse.ts | TWSE API 整合 | 股票列表 1 小時,日報價 5 分鐘 |
yahoo-finance.ts | 歷史價格 + 股息 | 無快取(按需) |
stock-price.ts | 報價 DB 快取層 | 寫入 DB,讀取時 fallback |
benchmark.ts | 投資組合 vs 0050 對標 | — |
benchmark-yearly.ts | 逐年績效比較 | — |
portfolio-calc.ts | 交易處理(加權平均成本法) | — |
quarterly-analysis.ts | 季度價格區間熱圖 | — |
etf-overlap.ts | ETF 重疊分析(Jaccard 係數) | — |
formatters.ts | 數字格式化工具 | — |
所有 custom hook 遵循相同 pattern:
function useXxx() {
const [data, setData] = useState<T>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => { /* fetch /api/... */ }, []);
useEffect(() => { fetchData(); }, [fetchData]);
return { data, isLoading, error, refresh: fetchData, ...actions };
}
關鍵 hook:
usePortfolio() — 最複雜,先 fetch portfolio 再用 Promise.all 載入所有報價useMarketIndices() — data 是 MarketOverview 物件({taiex, otc, electronics, financial}),不是陣列useBenchmark() / useYearlyBenchmark() — 5 分鐘客戶端 TTL 快取useHotRankings() — 5 分鐘快取,支援切換 volume/gainers/losersStockCard、HoldingCard、PortfolioSummaryXxxDetail.tsx(server)+ XxxDetailClient.tsx(client)src/components/index.ts遵循 schema → lib → API → hook → component → page 順序:
prisma/schema.prisma → bunx prisma db pushsrc/lib/xxx.ts(純邏輯 + 測試)src/app/api/xxx/route.ts(統一回傳格式)src/hooks/useXxx.ts(遵循 hook pattern)src/components/xxx/(Server/Client 分離)// src/app/api/xxx/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
try {
const data = await fetchSomething();
return NextResponse.json({ success: true, data });
} catch (error) {
return NextResponse.json(
{ success: false, error: '取得資料失敗' },
{ status: 500 }
);
}
}
stock-price.ts 的 DB 快取是否過期.TW(上市)或 .TWO(上櫃)後綴getCachedStockPrice() 作為 fallbacktest()/it() 掃描約 2000+ 測試案例(持續增加)*.test.ts 或 __tests__/ 子目錄vi.fn() for fetch,renderHook/waitFor/act for hooksbun run test:run(非 bun test)tests/e2e/fixtures/test-base.ts(攔截 **/api/**)tests/e2e/fixtures/mock-data.tstests/e2e/global-setup.ts(避免首次編譯超時)eslint-plugin-playwright 專屬規則bunx playwright testbun run typecheck && bun run lint && bun run test:run # 必須全通過
bunx playwright test # E2E 驗證
$transaction:先 count 檢查限制再 create 有 race condition,必須包在 prisma.$transaction() 內orderedIds 時,必須驗證無重複、全部存在、完整集合(不可遺漏)/portfolio)需保留當前的 query parameters(如 ?tab=holdings),否則導航會丟失 tab 狀態useSyncExternalStore 的 getSnapshot 必須返回 referentially stable 的值Object.is 永遠 false → 無限 re-rendercachedSettings + cachedSettingsRaw 解決/api/market/indices 回傳 MarketOverview 物件({taiex, otc, ...}),不是陣列page.route() 用 **/api/**(非 /api/**)eslint-plugin-playwright,不要 globalIgnores[...configDefaults.exclude, 'tests/e2e/**']expect(x).toBeVisible().catch(() => {}) 會吞掉斷言失敗not.toBeVisible() → 用 toBeHidden()toHaveClass() 等 async matcher 必須 awaitreferences/api-reference.md — 完整 API endpoints 文件references/architecture.md — 詳細架構與資料流