hyperdashi-client/FRONTEND_SPEC.md
2025-07-05 11:52:57 +09:00

754 lines
No EOL
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HyperDashi フロントエンド実装仕様書
## 概要
HyperDashi物品管理システムのフロントエンドをPreact + TypeScript + MUI + Viteで実装する。
Excel風のUIを提供し、物品管理・貸出管理・画像アップロード機能を持つWebアプリケーションを構築する。
## 技術スタック
- **フレームワーク**: Preact 10.x
- **言語**: TypeScript 5.x
- **UIライブラリ**: MUI (Material-UI) 5.x
- **ビルドツール**: Vite 5.x
- **状態管理**: Preact/signals または Zustand
- **HTTP クライアント**: Fetch API または Axios
- **ルーティング**: Preact Router
- **フォームバリデーション**: React Hook Form + Yup/Zod
- **データテーブル**: MUI DataGrid または react-table
- **ファイルアップロード**: react-dropzone
## バックエンドAPI仕様
### ベースURL
```
http://127.0.0.1:8081
```
### 認証
現在は認証なし将来的にJWTトークン認証を追加予定
### エンドポイント一覧
#### 1. 基本エンドポイント
- `GET /` - サーバー情報
- `GET /health` - ヘルスチェック
#### 2. 物品管理API
**物品一覧取得**
```http
GET /api/v1/items?page=1&per_page=20&search=keywords&is_on_loan=false&is_disposed=false
```
Response:
```typescript
interface ItemsListResponse {
items: Item[];
total: number;
page: number;
per_page: number;
}
interface Item {
id: number;
name: string;
label_id: string;
model_number?: string;
remarks?: string;
purchase_year?: number;
purchase_amount?: number;
durability_years?: number;
is_depreciation_target?: boolean;
connection_names?: string[];
cable_color_pattern?: string[];
storage_locations?: string[];
is_on_loan?: boolean;
qr_code_type?: 'qr' | 'barcode' | 'none';
is_disposed?: boolean;
image_url?: string;
created_at: string;
updated_at: string;
}
```
**物品作成**
```http
POST /api/v1/items
Content-Type: application/json
{
"name": "テストマイク",
"label_id": "MIC001",
"model_number": "Sony WM-1000XM4",
"remarks": "高品質ワイヤレスマイク",
"purchase_year": 2023,
"purchase_amount": 35000.0,
"durability_years": 5,
"is_depreciation_target": true,
"connection_names": ["XLR", "USB-C"],
"cable_color_pattern": ["red", "black"],
"storage_locations": ["部屋A", "ラックB", "コンテナ1"],
"qr_code_type": "qr",
"image_url": "http://example.com/image.jpg"
}
```
**物品詳細取得**
```http
GET /api/v1/items/:id
```
**物品更新**
```http
PUT /api/v1/items/:id
Content-Type: application/json
```
**物品削除**
```http
DELETE /api/v1/items/:id
```
**物品廃棄**
```http
POST /api/v1/items/:id/dispose
```
**ラベルID検索**
```http
GET /api/v1/items/by-label/:label_id
```
#### 3. 貸出管理API
**貸出一覧取得**
```http
GET /api/v1/loans?page=1&per_page=20&item_id=1&student_number=12345&active_only=true
```
Response:
```typescript
interface LoansListResponse {
loans: LoanWithItem[];
total: number;
page: number;
per_page: number;
}
interface LoanWithItem {
id: number;
item_id: number;
item_name: string;
item_label_id: string;
student_number: string;
student_name: string;
organization?: string;
loan_date: string;
return_date?: string;
remarks?: string;
created_at: string;
updated_at: string;
}
interface Loan {
id: number;
item_id: number;
student_number: string;
student_name: string;
organization?: string;
loan_date: string;
return_date?: string;
remarks?: string;
created_at: string;
updated_at: string;
}
```
**貸出作成**
```http
POST /api/v1/loans
Content-Type: application/json
{
"item_id": 1,
"student_number": "12345678",
"student_name": "山田太郎",
"organization": "情報メディアシステム学類",
"remarks": "授業用"
}
```
**貸出詳細取得**
```http
GET /api/v1/loans/:id
```
**返却処理**
```http
POST /api/v1/loans/:id/return
Content-Type: application/json
{
"return_date": "2024-01-15T10:30:00Z", // optional, defaults to now
"remarks": "正常に返却"
}
```
#### 4. 画像管理API
**画像アップロード**
```http
POST /api/v1/images/upload
Content-Type: multipart/form-data
Form fields:
- image: File (JPEG, PNG, GIF, WebP, max 10MB)
```
Response:
```typescript
interface ImageUploadResponse {
url: string;
filename: string;
size: number;
}
```
### エラーレスポンス
```typescript
interface ErrorResponse {
error: string;
message?: string;
}
```
## UI/UX要件
### 1. 全体レイアウト
**ヘッダー**
- アプリ名: "HyperDashi"
- ナビゲーションメニュー: 物品管理、貸出管理
- 検索バー(グローバル検索)
**サイドナビ**
- 物品管理
- 物品一覧
- 物品登録
- 廃棄物品
- 貸出管理
- 貸出一覧
- 新規貸出
- 返却処理
**メインコンテンツエリア**
- ページタイトル
- アクションボタン
- データテーブルまたはフォーム
### 2. 物品管理画面
#### 物品一覧画面 (`/items`)
**機能要件**
- Excel風のデータグリッド表示
- ページネーション20件/ページ)
- 検索機能物品名、ラベルID、型番、備考
- フィルタリング(貸出状態、廃棄状態)
- ソート機能(各カラム)
- 行選択(単一・複数)
- アクション編集、削除、廃棄、QRコード表示
**表示カラム**
- ラベルID
- 物品名
- 型番
- 購入年度
- 購入金額
- 貸出状態(チップ表示)
- 廃棄状態(チップ表示)
- 最終更新日
- アクション(編集、削除ボタン)
**UI要素**
```tsx
// フィルター・検索バー
<Box sx={{ mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
placeholder="物品名、ラベルID、型番で検索"
value={searchText}
onChange={handleSearchChange}
InputProps={{
startAdornment: <SearchIcon />
}}
/>
</Grid>
<Grid item xs={6} md={2}>
<FormControl fullWidth>
<InputLabel>貸出状態</InputLabel>
<Select value={loanFilter} onChange={handleLoanFilterChange}>
<MenuItem value="">すべて</MenuItem>
<MenuItem value="available">貸出可能</MenuItem>
<MenuItem value="on_loan">貸出中</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6} md={2}>
<FormControl fullWidth>
<InputLabel>廃棄状態</InputLabel>
<Select value={disposalFilter} onChange={handleDisposalFilterChange}>
<MenuItem value="">すべて</MenuItem>
<MenuItem value="active">使用中</MenuItem>
<MenuItem value="disposed">廃棄済み</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={4}>
<Button variant="contained" startIcon={<AddIcon />} onClick={handleAddNew}>
新規登録
</Button>
</Grid>
</Grid>
</Box>
// データグリッド
<DataGrid
rows={items}
columns={columns}
pageSize={20}
rowsPerPageOptions={[10, 20, 50]}
checkboxSelection
disableSelectionOnClick
autoHeight
loading={loading}
components={{
Toolbar: GridToolbar,
}}
onPageChange={handlePageChange}
onSelectionModelChange={handleSelectionChange}
/>
```
#### 物品登録・編集画面 (`/items/new`, `/items/:id/edit`)
**機能要件**
- 必須フィールドバリデーション
- リアルタイムバリデーション
- 画像アップロード(ドラッグ&ドロップ対応)
- 配列フィールド(接続名、ケーブル色、収納場所)の動的追加・削除
- フォーム状態の保存(一時保存機能)
**フォームフィールド**
```tsx
interface ItemFormData {
name: string; // 必須
label_id: string; // 必須、英数字のみ
model_number?: string;
remarks?: string;
purchase_year?: number; // 1900-2100
purchase_amount?: number; // 0以上
durability_years?: number; // 1-100
is_depreciation_target?: boolean;
connection_names?: string[]; // 動的配列
cable_color_pattern?: string[]; // 動的配列
storage_locations?: string[]; // 動的配列
qr_code_type?: 'qr' | 'barcode' | 'none';
image_url?: string;
}
```
**バリデーションルール**
```typescript
const itemValidationSchema = yup.object({
name: yup.string().required('物品名は必須です').max(255, '255文字以内で入力してください'),
label_id: yup
.string()
.required('ラベルIDは必須です')
.matches(/^[A-Za-z0-9]+$/, '英数字のみ使用可能です')
.max(50, '50文字以内で入力してください'),
model_number: yup.string().max(255, '255文字以内で入力してください'),
purchase_year: yup
.number()
.min(1900, '1900年以降を入力してください')
.max(2100, '2100年以前を入力してください'),
purchase_amount: yup.number().min(0, '0以上の値を入力してください'),
durability_years: yup
.number()
.min(1, '1年以上を入力してください')
.max(100, '100年以下を入力してください'),
});
```
**画像アップロード UI**
```tsx
<Box sx={{ mb: 3 }}>
<Typography variant="h6" gutterBottom>物品画像</Typography>
<Dropzone
accept={{
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'image/webp': ['.webp']
}}
maxSize={10 * 1024 * 1024} // 10MB
onDrop={handleImageUpload}
>
{({getRootProps, getInputProps, isDragActive}) => (
<Paper
{...getRootProps()}
sx={{
p: 3,
textAlign: 'center',
border: '2px dashed',
borderColor: isDragActive ? 'primary.main' : 'grey.300',
cursor: 'pointer'
}}
>
<input {...getInputProps()} />
<CloudUploadIcon sx={{ fontSize: 48, color: 'grey.400', mb: 1 }} />
<Typography>
{isDragActive
? 'ここにファイルをドロップ'
: '画像をドラッグ&ドロップまたはクリックして選択'}
</Typography>
<Typography variant="caption" color="textSecondary">
JPEG, PNG, GIF, WebP (最大10MB)
</Typography>
</Paper>
)}
</Dropzone>
{previewUrl && (
<Box sx={{ mt: 2, textAlign: 'center' }}>
<img src={previewUrl} alt="プレビュー" style={{ maxWidth: 300, maxHeight: 200 }} />
<Button onClick={handleRemoveImage} color="error" startIcon={<DeleteIcon />}>
画像を削除
</Button>
</Box>
)}
</Box>
```
### 3. 貸出管理画面
#### 貸出一覧画面 (`/loans`)
**機能要件**
- 貸出履歴の一覧表示
- フィルタリング物品ID、学籍番号、アクティブ貸出のみ
- ソート機能
- 返却処理(一括・個別)
- 貸出状況の可視化
**表示カラム**
- 貸出ID
- 物品名ラベルID
- 学籍番号
- 氏名
- 所属
- 貸出日
- 返却予定日
- 返却日
- 状態(貸出中/返却済み)
- アクション
**フィルター UI**
```tsx
<Box sx={{ mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} md={3}>
<TextField
fullWidth
placeholder="学籍番号で検索"
value={studentNumberFilter}
onChange={handleStudentNumberChange}
/>
</Grid>
<Grid item xs={12} md={3}>
<Autocomplete
options={items}
getOptionLabel={(option) => `${option.name} (${option.label_id})`}
renderInput={(params) => (
<TextField {...params} placeholder="物品で検索" />
)}
onChange={handleItemFilterChange}
/>
</Grid>
<Grid item xs={6} md={2}>
<FormControl fullWidth>
<InputLabel>状態</InputLabel>
<Select value={statusFilter} onChange={handleStatusFilterChange}>
<MenuItem value="">すべて</MenuItem>
<MenuItem value="active">貸出中</MenuItem>
<MenuItem value="returned">返却済み</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={6} md={2}>
<Button variant="contained" startIcon={<AddIcon />} onClick={handleNewLoan}>
新規貸出
</Button>
</Grid>
</Grid>
</Box>
```
#### 新規貸出画面 (`/loans/new`)
**機能要件**
- 物品選択(利用可能な物品のみ)
- 学生情報入力
- バリデーション
- 貸出可能性チェック
**フォームフィールド**
```typescript
interface LoanFormData {
item_id: number; // 必須
student_number: string; // 必須
student_name: string; // 必須
organization?: string;
remarks?: string;
}
```
#### 返却処理画面 (`/loans/:id/return`)
**機能要件**
- 返却日時設定(デフォルト:現在時刻)
- 返却時の備考入力
- 物品状態確認
### 4. 共通コンポーネント
#### レスポンシブデザイン
- モバイル対応768px以下
- タブレット対応768px-1024px
- デスクトップ対応1024px以上
#### 通知システム
```tsx
// Snackbar通知
const showNotification = (message: string, severity: 'success' | 'error' | 'warning' | 'info') => {
// MUI Snackbar + Alert実装
};
// 使用例
showNotification('物品が正常に登録されました', 'success');
showNotification('エラーが発生しました', 'error');
```
#### ローディング状態
```tsx
// スケルトンローディング
<Skeleton variant="rectangular" height={400} />
// プログレスインジケーター
<CircularProgress />
// データテーブルローディング
<DataGrid loading={isLoading} ... />
```
#### エラーハンドリング
```tsx
// エラーバウンダリ
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>
// APIエラー処理
const handleApiError = (error: any) => {
if (error.response?.status === 404) {
showNotification('データが見つかりません', 'error');
} else if (error.response?.status >= 500) {
showNotification('サーバーエラーが発生しました', 'error');
} else {
showNotification('予期しないエラーが発生しました', 'error');
}
};
```
## プロジェクト構成
```
src/
├── components/ # 共通コンポーネント
│ ├── ui/ # 基本UIコンポーネント
│ ├── forms/ # フォームコンポーネント
│ ├── tables/ # テーブルコンポーネント
│ └── layout/ # レイアウトコンポーネント
├── pages/ # ページコンポーネント
│ ├── items/ # 物品管理ページ
│ ├── loans/ # 貸出管理ページ
│ └── dashboard/ # ダッシュボード
├── hooks/ # カスタムフック
├── services/ # API呼び出し
├── types/ # TypeScript型定義
├── utils/ # ユーティリティ関数
├── stores/ # 状態管理
└── constants/ # 定数定義
```
## 実装優先度
### Phase 1: 基本機能
1. プロジェクトセットアップVite + Preact + TypeScript + MUI
2. ルーティング設定
3. レイアウトコンポーネント
4. 物品一覧画面
5. 物品登録・編集画面
### Phase 2: 拡張機能
1. 貸出管理機能
2. 画像アップロード機能
3. 検索・フィルタリング機能強化
4. レスポンシブデザイン
### Phase 3: 改善・最適化
1. パフォーマンス最適化
2. エラーハンドリング強化
3. ユーザビリティ改善
4. テスト実装
## 開発環境設定
### 必要なパッケージ
```json
{
"dependencies": {
"preact": "^10.19.0",
"@preact/signals": "^1.2.0",
"preact-router": "^4.1.0",
"@mui/material": "^5.15.0",
"@mui/icons-material": "^5.15.0",
"@mui/x-data-grid": "^6.18.0",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"react-hook-form": "^7.48.0",
"yup": "^1.4.0",
"@hookform/resolvers": "^3.3.0",
"react-dropzone": "^14.2.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.8.0",
"vite": "^5.0.0",
"typescript": "^5.2.0",
"@types/node": "^20.10.0"
}
}
```
### Vite設定 (vite.config.ts)
```typescript
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
export default defineConfig({
plugins: [preact()],
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:8081',
changeOrigin: true,
},
},
},
build: {
target: 'esnext',
},
});
```
### TypeScript設定 (tsconfig.json)
```json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
```
## API クライアント実装例
```typescript
// src/services/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// リクエストインターセプター
api.interceptors.request.use((config) => {
// 将来的にJWTトークンを追加
return config;
});
// レスポンスインターセプター
api.interceptors.response.use(
(response) => response,
(error) => {
// エラーハンドリング
console.error('API Error:', error);
return Promise.reject(error);
}
);
export default api;
// src/services/items.ts
export const itemsApi = {
getItems: (params: GetItemsParams) =>
api.get<ItemsListResponse>('/items', { params }),
getItem: (id: number) =>
api.get<Item>(`/items/${id}`),
createItem: (data: CreateItemRequest) =>
api.post<Item>('/items', data),
updateItem: (id: number, data: UpdateItemRequest) =>
api.put<Item>(`/items/${id}`, data),
deleteItem: (id: number) =>
api.delete(`/items/${id}`),
disposeItem: (id: number) =>
api.post<Item>(`/items/${id}/dispose`),
};
```
この仕様書に基づいて、モダンで使いやすいExcel風UIを持つHyperDashiフロントエンドを実装してください。