feat(hold-overview): restructure WIP/Hold pages — migrate Pareto, unify filters and lot table

- Move Hold Pareto charts from WIP Overview to Hold Overview with conditional visibility by holdType
- WIP Overview hold cards now navigate to Hold Overview instead of filtering matrix
- Add 6-field FilterPanel to Hold Overview (reuse WIP Overview's FilterPanel)
- Default holdType to "all" when entering Hold Overview directly
- Unify lot table to 13 columns (shared HoldLotTable component) across Hold Overview and Hold Detail
- Hold Detail back button now returns to Hold Overview instead of WIP Overview
- Backend: thread WIP filter params through hold-overview summary/matrix/lots APIs
- Fix nativeModuleRegistry CSS imports for hold-overview and query-tool
- Migrate ParetoSection.vue and pareto CSS to wip-shared for cross-page reuse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-03-03 18:35:52 +08:00
parent 777751311c
commit da2c2f7879
25 changed files with 1258 additions and 296 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-03

View File

@@ -0,0 +1,72 @@
## Context
WIP 即時概況和 Hold 即時概況共用部分 Hold 資料呈現邏輯但目前職責劃分不清Hold 柏拉圖放在 WIP 頁面、Hold 頁面篩選能力薄弱、Lot 明細欄位不一致。兩頁的後端 service 層(`wip_service.py`)已共用 `_select_with_snapshot_indexes()` 作為核心查詢引擎,支援 workorder/lotid/pj_type/firstname/waferdesc 篩選,但 Hold Overview 的 route 層和 service function 簽名尚未穿透這些參數。
**現有架構:**
- `ParetoSection.vue` 位於 `wip-overview/components/`,僅 WIP 使用
- Hold Overview 的 `FilterBar` 僅有 holdType + reason 兩個篩選欄位
- Hold Overview 和 Hold Detail 各有獨立的 `LotTable.vue`,欄位不同(前者有 Hold Reason 無 Spec後者相反
- Hold Detail 返回按鈕指向 WIP Overview
## Goals / Non-Goals
**Goals:**
- Hold 相關視覺化(柏拉圖)集中到 Hold 即時概況
- WIP 即時概況的 Hold 卡片改為跳轉入口(而非本地篩選)
- Hold 即時概況具備與 WIP 相同的 6 欄位篩選能力
- 統一 Lot 明細表格為 13 欄(含 Hold Reason + Spec
- 修正 Hold 即時概況版面窄的問題
- Hold Detail 導航回到 Hold Overview而非 WIP Overview
**Non-Goals:**
- 不變更 Hold 柏拉圖的資料來源 API繼續使用 `/api/wip/overview/hold`
- 不變更 Hold Detail 的內部功能AgeDistribution、DistributionTable 等)
- 不重構後端 `wip_service.py` 的核心查詢邏輯
- 不新增 API endpoint只擴充現有 endpoint 的參數)
## Decisions
### D1. 柏拉圖資料來源:沿用 `/api/wip/overview/hold`
Hold 即時概況的柏拉圖直接呼叫現有 `/api/wip/overview/hold` API而非新增 Hold Overview 專用 endpoint。
**理由:** 該 API 回傳 `{items: [{reason, lots, qty, holdType}]}`,正好是 `ParetoSection` + `splitHoldByType()` 所需格式。已有的 treemap API 回傳格式不同workcenter×reason 分組),不適合柏拉圖。
### D2. ParetoSection 搬遷至 `wip-shared/components/`
`ParetoSection.vue``wip-overview/components/` 移至 `wip-shared/components/`,柏拉圖 CSS 抽取為 `wip-shared/pareto-styles.css`
**理由:** `wip-shared/` 是既有的共用目錄(已有 `styles.css`),符合專案的組件共用慣例。
### D3. 統一 LotTable 為 `wip-shared/components/HoldLotTable.vue`
以 Hold Overview 的 `LotTable.vue` 為基礎,新增 Spec 欄位,形成 13 欄統一表格。兩頁均改用此共用組件。
**理由:** 後端 `get_hold_detail_lots` 已同時回傳 `holdReason``spec`wip_service.py:3079-3080純前端調整。共用組件避免欄位不同步。
### D4. FilterPanel 直接 import WIP Overview 的組件
Hold Overview 的 FilterPanel 直接 `import FilterPanel from '../wip-overview/components/FilterPanel.vue'`,不複製組件。
**理由:** FilterPanel 的 props interfacefilters/options/loading + apply/clear/draft-change events是穩定的且內部已正確處理 MultiSelect 相對路徑。避免組件重複。
### D5. 柏拉圖根據 holdType 條件顯示
- holdType = 'all'(預設)→ 顯示品質異常 + 非品質異常兩張柏拉圖
- holdType = 'quality' → 僅顯示品質異常柏拉圖
- holdType = 'non-quality' → 僅顯示非品質異常柏拉圖
**理由:** 使用者選擇特定 holdType 後,無關的柏拉圖顯示空資料會造成困惑。
### D6. 後端參數穿透策略
`get_hold_detail_summary``get_hold_detail_lots` 加入 5 個 Optional[str] 參數,傳遞至 `_select_with_snapshot_indexes()`cache path和 Oracle fallback。`get_wip_matrix` 已原生支援,只需在 route 層解析參數。
**理由:** 最小變更量,`_select_with_snapshot_indexes` 已支援全部篩選欄位。
## Risks / Trade-offs
- **[ParetoSection 搬遷路徑斷裂]** → 搬遷後需確認 WIP Overview 不再 import 舊路徑。透過 `npm run build` 驗證。
- **[FilterPanel 跨目錄 import]** → 若 FilterPanel 內部路徑變更會影響 Hold Overview。風險低該組件穩定。未來可考慮提升至 shared。
- **[Hold Overview API 回應時間增加]** → 新增 FilterPanel 觸發 filter-options API。此 API 已有 debounce120ms和 cache影響可忽略。
- **[WIP Hold 卡片不再 toggle matrix]** → 使用者行為改變。但 Hold 卡片的 toggle 功能使用率低跳轉到專頁更直觀。RUN/QUEUE 卡片保持原行為。

View File

@@ -0,0 +1,32 @@
## Why
Hold 即時概況版面單薄(僅 FilterBar + SummaryCards + Matrix + LotTable缺乏視覺密度且篩選能力有限只有 holdType + reason。而 WIP 即時概況承載了兩張 Hold 柏拉圖,但使用者分析 Hold 數據時需在兩頁間來回切換。此次重構將 Hold 相關視覺化集中到 Hold 即時概況,並統一篩選器與 Lot 明細欄位,讓兩頁各司其職。
## What Changes
- 將 WIP 即時概況的品質異常 / 非品質異常 Hold 柏拉圖移至 Hold 即時概況
- WIP 即時概況的「品質異常」「非品質異常」StatusCard 點擊由篩選 matrix 改為跳轉至 Hold 即時概況(帶 hold_type 參數)
- Hold 即時概況直接進入時 Hold Type 預設「全部」(原為「品質異常」)
- Hold 即時概況加入 WIP 即時概況的 6 欄位 FilterPanelworkorder/lotid/package/type/firstname/waferdesc
- Hold Detail 返回按鈕改指 Hold 即時概況(原指 WIP 即時概況)
- 統一 Hold 即時概況與 Hold Detail 的 Lot 明細欄位(合併 Hold Reason + Spec 為 13 欄)
- Hold 即時概況版面修正FilterBar 寬度過大、缺少 content-grid 包裹)
- 後端 Hold Overview API 穿透 WIP 篩選參數workorder/lotid/type/firstname/waferdesc
## Capabilities
### New Capabilities
_(無新增 capability)_
### Modified Capabilities
- `hold-overview-page`: Hold 即時概況加入柏拉圖、FilterPanel、預設 holdType 改為 all、版面修正、統一 LotTable
- `wip-overview-page`: 移除 Hold 柏拉圖、Hold 卡片改為跳轉導航
- `hold-detail-page`: 返回按鈕改指 Hold Overview、改用統一 LotTable
## Impact
- **前端**`wip-overview/App.vue`(移除柏拉圖、卡片跳轉)、`hold-overview/App.vue`(核心重構)、`hold-detail/App.vue`導航修正、共用組件新增ParetoSection 搬遷、HoldLotTable、CSS 重構
- **後端**`hold_overview_routes.py`3 API 加入篩選參數)、`wip_service.py`2 函式簽名擴充)、`hold_routes.py`redirect 改向)
- **API**`/api/hold-overview/summary``/api/hold-overview/matrix``/api/hold-overview/lots` 新增 workorder/lotid/type/firstname/waferdesc 可選參數(向後相容)

View File

@@ -0,0 +1,49 @@
## MODIFIED Requirements
### Requirement: Hold Detail page SHALL display hold reason analysis
The page SHALL show summary statistics for a specific hold reason.
#### Scenario: Summary cards rendering
- **WHEN** the page loads with `?reason={reason}` in the URL
- **THEN** the page SHALL call `GET /api/wip/hold-detail/summary` with the reason
- **THEN** five cards SHALL display: Total Lots, Total QTY, 平均當站滯留, 最久當站滯留, 影響站群
- **THEN** age values SHALL display with "天" suffix
#### Scenario: Hold type classification
- **WHEN** the page loads
- **THEN** the header gradient color SHALL be red (#ef4444) for quality holds or orange (#f97316) for non-quality holds
- **THEN** a badge SHALL display "品質異常" or "非品質異常" accordingly
- **THEN** classification SHALL use a frontend `NON_QUALITY_HOLD_REASONS` constant set (11 values matching backend `sql/filters.py`)
#### Scenario: Missing reason parameter
- **WHEN** the page loads without a `reason` URL parameter
- **THEN** the page SHALL redirect to `/hold-overview`
### Requirement: Hold Detail page SHALL display paginated lot details
The page SHALL display detailed lot information with server-side pagination.
#### Scenario: Lot table rendering
- **WHEN** lot data is loaded from `GET /api/wip/hold-detail/lots`
- **THEN** a table SHALL display with 13 columns: LOTID, WORKORDER, QTY, Product, Package, Workcenter, Hold Reason, Spec, Age, Hold By, Dept, Hold Comment, Future Hold Comment
- **THEN** age values SHALL display with "天" suffix
#### Scenario: Filter indicator
- **WHEN** any filter is active (workcenter, package, or age range)
- **THEN** a blue filter indicator bar SHALL display showing active filters (e.g., "篩選: Workcenter=WC-A, Age=3-7天")
- **THEN** clicking the "×" on the indicator SHALL clear all filters
#### Scenario: Pagination
- **WHEN** total pages exceeds 1
- **THEN** Prev/Next buttons and page info SHALL display
- **THEN** page info SHALL show "顯示 {start} - {end} / {total}"
#### Scenario: Filter changes reset pagination
- **WHEN** any filter is toggled
- **THEN** pagination SHALL reset to page 1
### Requirement: Hold Detail page SHALL have back navigation to Hold Overview
The page SHALL provide a way to return to the Hold Overview page.
#### Scenario: Back button
- **WHEN** user clicks the "← Hold Overview" button in the header
- **THEN** the page SHALL navigate to `/hold-overview`

View File

@@ -0,0 +1,126 @@
## MODIFIED Requirements
### Requirement: Hold Overview page SHALL display a filter bar with Hold Type and Reason
The page SHALL provide a filter bar for selecting hold type and hold reason.
#### Scenario: Hold Type radio default
- **WHEN** the page loads without a `hold_type` URL parameter
- **THEN** the Hold Type filter SHALL default to "全部"
- **THEN** three radio options SHALL display: 品質異常, 非品質異常, 全部
#### Scenario: Hold Type from URL parameter
- **WHEN** the page loads with `?hold_type=quality` or `?hold_type=non-quality`
- **THEN** the Hold Type filter SHALL be set to the specified value
#### Scenario: Hold Type change reloads all data
- **WHEN** user changes the Hold Type selection
- **THEN** all API calls (summary, matrix, hold pareto, lots) SHALL reload with the new hold_type parameter
- **THEN** any active matrix filters SHALL be cleared
#### Scenario: Reason dropdown populated from current data
- **WHEN** summary data is loaded
- **THEN** the Reason dropdown SHALL display "全部" plus all distinct hold reasons from the data
- **THEN** selecting a specific reason SHALL reload all API calls filtered by that reason
- **THEN** any active matrix filters SHALL be cleared
### Requirement: Hold Overview page SHALL display paginated lot details
The page SHALL display a detailed lot table with server-side pagination.
#### Scenario: Lot table rendering
- **WHEN** lot data is loaded from `GET /api/hold-overview/lots`
- **THEN** a table SHALL display with 13 columns: LOTID, WORKORDER, QTY, Product, Package, Workcenter, Hold Reason, Spec, Age, Hold By, Dept, Hold Comment, Future Hold Comment
- **THEN** age values SHALL display with "天" suffix
#### Scenario: Lot table responds to all cascade filters
- **WHEN** matrixFilter is `{ workcenter: WC-A, package: PKG-1 }`
- **THEN** lots API SHALL be called with `workcenter=WC-A&package=PKG-1`
- **THEN** only lots matching all active filters SHALL be displayed
#### Scenario: Pagination
- **WHEN** total lots exceeds per_page (50)
- **THEN** Prev/Next buttons and page info SHALL display
- **THEN** page info SHALL show "顯示 {start} - {end} / {total}"
#### Scenario: Filter changes reset pagination
- **WHEN** any filter changes (filter bar, matrix click, or WIP filter apply)
- **THEN** pagination SHALL reset to page 1
#### Scenario: Empty lot result
- **WHEN** a query returns zero lots
- **THEN** the lot table SHALL display a "No data" placeholder
### Requirement: Hold Overview page SHALL have back navigation
The page SHALL provide navigation back to WIP Overview.
#### Scenario: Back button
- **WHEN** user clicks the "← WIP Overview" button in the header
- **THEN** the page SHALL navigate to `/wip-overview`
## ADDED Requirements
### Requirement: Hold Overview page SHALL display Hold Pareto analysis
The page SHALL display Pareto charts for quality and non-quality hold reasons, fetched from `/api/wip/overview/hold`.
#### Scenario: Pareto chart rendering
- **WHEN** hold data is loaded from `GET /api/wip/overview/hold`
- **THEN** hold items SHALL be split into quality and non-quality groups using `splitHoldByType()`
- **THEN** each group SHALL display an ECharts dual-axis Pareto chart (bar=QTY, line=cumulative %)
- **THEN** items SHALL be sorted by QTY descending
- **THEN** quality chart SHALL use red color (#ef4444), non-quality SHALL use orange (#f97316)
#### Scenario: Pareto visibility by holdType
- **WHEN** holdType is "all"
- **THEN** both quality and non-quality Pareto charts SHALL display
- **WHEN** holdType is "quality"
- **THEN** only the quality Pareto chart SHALL display
- **WHEN** holdType is "non-quality"
- **THEN** only the non-quality Pareto chart SHALL display
#### Scenario: Pareto chart drill-down
- **WHEN** user clicks a bar in the Pareto chart or a reason link in the table
- **THEN** the page SHALL navigate to `/hold-detail?reason={reason}`
#### Scenario: Empty hold data
- **WHEN** a hold type has no items
- **THEN** the chart area SHALL display a "目前無資料" message
### Requirement: Hold Overview page SHALL support WIP-style multi-field filtering
The page SHALL provide the same 6-field FilterPanel as WIP Overview (workorder, lotid, package, type, firstname, waferdesc).
#### Scenario: FilterPanel rendering
- **WHEN** the page loads
- **THEN** a FilterPanel SHALL display with 6 multi-select fields: WORKORDER, LOT ID, PACKAGE, TYPE, Wafer LOT, Wafer Type
- **THEN** filter options SHALL be loaded from `GET /api/wip/meta/filter-options` with `status=HOLD` and current holdType
#### Scenario: Apply filters
- **WHEN** user selects filter values and clicks "套用篩選"
- **THEN** all API calls (summary, matrix, hold pareto, lots) SHALL reload with the filter values
- **THEN** the URL SHALL be updated to include the filter values
#### Scenario: Clear filters
- **WHEN** user clicks "清除篩選"
- **THEN** all filter inputs SHALL be cleared and data SHALL reload without WIP filters
- **THEN** holdType and reason filters SHALL be preserved
#### Scenario: Filter options update
- **WHEN** filters are changed (draft mode)
- **THEN** filter options SHALL reload with debounce (120ms) reflecting cross-filter narrowing
### Requirement: Hold Overview API endpoints SHALL accept WIP filter parameters
The backend Hold Overview endpoints SHALL support optional workorder, lotid, type, firstname, waferdesc query parameters.
#### Scenario: Summary API with WIP filters
- **WHEN** `GET /api/hold-overview/summary?hold_type=quality&workorder=WO-001`
- **THEN** the summary SHALL only include lots matching workorder WO-001
#### Scenario: Matrix API with WIP filters
- **WHEN** `GET /api/hold-overview/matrix?hold_type=all&package=PKG-A`
- **THEN** the matrix SHALL only include lots matching package PKG-A
#### Scenario: Lots API with WIP filters
- **WHEN** `GET /api/hold-overview/lots?hold_type=quality&lotid=LOT-001`
- **THEN** the lot list SHALL only include lots matching LOT ID LOT-001
#### Scenario: Backward compatibility
- **WHEN** WIP filter parameters are omitted
- **THEN** the API SHALL behave identically to the current implementation (no filtering)

View File

@@ -0,0 +1,28 @@
## MODIFIED Requirements
### Requirement: Overview page SHALL display WIP status breakdown cards
The page SHALL display four clickable status cards (RUN, QUEUE, 品質異常, 非品質異常) with lot and quantity counts.
#### Scenario: Status cards rendering
- **WHEN** summary data is loaded
- **THEN** four status cards SHALL be displayed with color coding (green=RUN, yellow=QUEUE, red=品質異常, orange=非品質異常)
- **THEN** each card SHALL show lot count and quantity
#### Scenario: RUN/QUEUE card click filters matrix
- **WHEN** user clicks the RUN or QUEUE status card
- **THEN** the matrix table SHALL reload with the selected status filter
- **THEN** the clicked card SHALL show an active visual state
- **THEN** clicking the same card again SHALL deactivate the filter and restore all cards
- **THEN** the URL SHALL be updated to reflect the active status filter
#### Scenario: Hold card click navigates to Hold Overview
- **WHEN** user clicks the "品質異常" status card
- **THEN** the page SHALL navigate to `/hold-overview?hold_type=quality`
- **WHEN** user clicks the "非品質異常" status card
- **THEN** the page SHALL navigate to `/hold-overview?hold_type=non-quality`
## REMOVED Requirements
### Requirement: Overview page SHALL display Hold Pareto analysis
**Reason**: Hold Pareto charts are moved to the Hold Overview page where they are more relevant.
**Migration**: Users can access the same Pareto analysis on the Hold Overview page. Clicking the "品質異常" or "非品質異常" status cards navigates directly there.

View File

@@ -0,0 +1,54 @@
## 1. 後端Hold Overview API 支援 WIP 篩選參數
- [x] 1.1 `wip_service.py``get_hold_detail_summary` 加入 workorder/lotid/pj_type/firstname/waferdesc 5 個 Optional[str] 參數,傳入 `_select_with_snapshot_indexes()` 和 Oracle fallback
- [x] 1.2 `wip_service.py``get_hold_detail_lots` 加入同樣 5 個參數,傳入 `_select_with_snapshot_indexes()` 和 Oracle fallback
- [x] 1.3 `hold_overview_routes.py` — 3 個 API (summary/matrix/lots) 從 request.args 解析 workorder/lotid/type/firstname/waferdesc 並傳入對應 service function
## 2. 共用基礎設施ParetoSection 搬遷、CSS 抽取、HoldLotTable
- [x] 2.1 從 `wip-overview/style.css` 抽取 `.pareto-grid``.pareto-section``.pareto-header` 等柏拉圖相關 CSS 到新檔案 `wip-shared/pareto-styles.css`,原位改為 `@import`
- [x] 2.2 將 `wip-overview/components/ParetoSection.vue` 移至 `wip-shared/components/ParetoSection.vue`WIP Overview import 路徑更新
- [x] 2.3 新增 `wip-shared/components/HoldLotTable.vue`:以 hold-overview LotTable 為基礎,新增 Spec 欄位形成 13 欄統一表格LOTID, WORKORDER, QTY, Product, Package, Workcenter, Hold Reason, Spec, Age, Hold By, Dept, Hold Comment, Future Hold Comment
## 3. WIP 即時概況:移除柏拉圖 + Hold 卡片改跳轉
- [x] 3.1 `wip-overview/App.vue` — 移除 hold ref、fetchHold()、splitHold computed、navigateToHoldDetail()、template 的 `<section class="pareto-grid">` 區塊、對應 import (splitHoldByType, ParetoSection)
- [x] 3.2 `wip-overview/App.vue` — 修改 `toggleStatusFilter()`quality-hold → `navigateToRuntimeRoute('/hold-overview?hold_type=quality')`non-quality-hold → `navigateToRuntimeRoute('/hold-overview?hold_type=non-quality')`RUN/QUEUE 保持原行為
## 4. Hold 即時概況:加入柏拉圖
- [x] 4.1 `hold-overview/App.vue` — import ParetoSectionfrom wip-shared、splitHoldByTypefrom wip-derive、navigateToRuntimeRoute新增 hold ref、fetchHold()、splitHold computed、showQualityPareto/showNonQualityPareto computed、navigateToHoldDetail()
- [x] 4.2 `hold-overview/App.vue` — loadAllData 的 Promise.all 加入 fetchHold(signal),結果存入 hold.value
- [x] 4.3 `hold-overview/App.vue` — Template 在 Matrix 後加入 pareto-grid 區段,根據 holdType 條件顯示品質異常 / 非品質異常柏拉圖
- [x] 4.4 `hold-overview/style.css` — 頂部加入 `@import '../wip-shared/pareto-styles.css'`
## 5. Hold 即時概況:加入 WIP FilterPanel
- [x] 5.1 `hold-overview/App.vue` — import FilterPanelfrom wip-overview/components、buildWipOverviewQueryParamsfrom wip-derive新增 filters reactive state + filterOptions ref + debounce 機制
- [x] 5.2 `hold-overview/App.vue` — 新增 buildAllFilterParams() 合併 holdType/reason 與 WIP 6 欄位;更新 fetchSummary/fetchMatrix/fetchLots 改用 buildAllFilterParams()
- [x] 5.3 `hold-overview/App.vue` — 新增 loadFilterOptions() 呼叫 `/api/wip/meta/filter-options`(帶 status=HOLD + holdType、applyFilters()、clearAllFilters()、onFilterDraftChange()
- [x] 5.4 `hold-overview/App.vue` — Template 在 FilterBar 前加入 FilterPanel 組件
- [x] 5.5 `hold-overview/App.vue` — updateUrlState() 加入 6 欄位序列化onMounted 解析 URL 的 WIP 篩選參數
- [x] 5.6 `hold-overview/main.js` — 加入 `import '../resource-shared/styles.css'`MultiSelect 需要)
## 6. Hold 即時概況:預設 holdType 改為 'all'
- [x] 6.1 `hold-overview/App.vue` — 所有 holdType 預設值和 fallback 從 'quality' 改為 'all'filterBar initial、normalizeHoldType、buildFilterBarParams、handleFilterChange
- [x] 6.2 `hold-overview/components/FilterBar.vue` — 所有 holdType 預設值和 fallback 從 'quality' 改為 'all'
## 7. Hold Detail返回按鈕改指 Hold Overview + 共用 LotTable
- [x] 7.1 `hold-detail/App.vue` — 返回按鈕 navigateToRuntimeRoute 從 '/wip-overview' 改為 '/hold-overview',按鈕文字改為 '← Hold Overview'
- [x] 7.2 `hold-detail/App.vue` — no-reason redirect 從 '/wip-overview' 改為 '/hold-overview'
- [x] 7.3 `hold-detail/App.vue` — import 改用 `HoldLotTable` from `'../wip-shared/components/HoldLotTable.vue'`,取代 `./components/LotTable.vue`
- [x] 7.4 `hold_routes.py` — server-side redirect 從 '/wip-overview' 改為 '/hold-overview'
## 8. Hold 即時概況:版面修正 + 共用 LotTable
- [x] 8.1 `hold-overview/style.css``.hold-overview-hold-type-group` flex 改為 `0 0 auto``.hold-type-segment` min-width 改為 320px
- [x] 8.2 `hold-overview/App.vue` — Template 加入 `.content-grid` wrapper 包裹 Matrix + Pareto + FilterIndicator + LotTable
- [x] 8.3 `hold-overview/App.vue` — LotTable import 改用 `HoldLotTable` from `'../wip-shared/components/HoldLotTable.vue'`
## 9. 驗證
- [x] 9.1 前端 `npm run build` 通過(確認無 import 路徑斷裂)