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

18 KiB
Raw Permalink Blame History

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

物品一覧取得

GET /api/v1/items?page=1&per_page=20&search=keywords&is_on_loan=false&is_disposed=false

Response:

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;
}

物品作成

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"
}

物品詳細取得

GET /api/v1/items/:id

物品更新

PUT /api/v1/items/:id
Content-Type: application/json

物品削除

DELETE /api/v1/items/:id

物品廃棄

POST /api/v1/items/:id/dispose

ラベルID検索

GET /api/v1/items/by-label/:label_id

3. 貸出管理API

貸出一覧取得

GET /api/v1/loans?page=1&per_page=20&item_id=1&student_number=12345&active_only=true

Response:

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;
}

貸出作成

POST /api/v1/loans
Content-Type: application/json

{
  "item_id": 1,
  "student_number": "12345678",
  "student_name": "山田太郎",
  "organization": "情報メディアシステム学類",
  "remarks": "授業用"
}

貸出詳細取得

GET /api/v1/loans/:id

返却処理

POST /api/v1/loans/:id/return
Content-Type: application/json

{
  "return_date": "2024-01-15T10:30:00Z",  // optional, defaults to now
  "remarks": "正常に返却"
}

4. 画像管理API

画像アップロード

POST /api/v1/images/upload
Content-Type: multipart/form-data

Form fields:
- image: File (JPEG, PNG, GIF, WebP, max 10MB)

Response:

interface ImageUploadResponse {
  url: string;
  filename: string;
  size: number;
}

エラーレスポンス

interface ErrorResponse {
  error: string;
  message?: string;
}

UI/UX要件

1. 全体レイアウト

ヘッダー

  • アプリ名: "HyperDashi"
  • ナビゲーションメニュー: 物品管理、貸出管理
  • 検索バー(グローバル検索)

サイドナビ

  • 物品管理
    • 物品一覧
    • 物品登録
    • 廃棄物品
  • 貸出管理
    • 貸出一覧
    • 新規貸出
    • 返却処理

メインコンテンツエリア

  • ページタイトル
  • アクションボタン
  • データテーブルまたはフォーム

2. 物品管理画面

物品一覧画面 (/items)

機能要件

  • Excel風のデータグリッド表示
  • ページネーション20件/ページ)
  • 検索機能物品名、ラベルID、型番、備考
  • フィルタリング(貸出状態、廃棄状態)
  • ソート機能(各カラム)
  • 行選択(単一・複数)
  • アクション編集、削除、廃棄、QRコード表示

表示カラム

  • ラベルID
  • 物品名
  • 型番
  • 購入年度
  • 購入金額
  • 貸出状態(チップ表示)
  • 廃棄状態(チップ表示)
  • 最終更新日
  • アクション(編集、削除ボタン)

UI要素

// フィルター・検索バー
<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)

機能要件

  • 必須フィールドバリデーション
  • リアルタイムバリデーション
  • 画像アップロード(ドラッグ&ドロップ対応)
  • 配列フィールド(接続名、ケーブル色、収納場所)の動的追加・削除
  • フォーム状態の保存(一時保存機能)

フォームフィールド

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;
}

バリデーションルール

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

<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

<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)

機能要件

  • 物品選択(利用可能な物品のみ)
  • 学生情報入力
  • バリデーション
  • 貸出可能性チェック

フォームフィールド

interface LoanFormData {
  item_id: number;        // 必須
  student_number: string; // 必須
  student_name: string;   // 必須
  organization?: string;
  remarks?: string;
}

返却処理画面 (/loans/:id/return)

機能要件

  • 返却日時設定(デフォルト:現在時刻)
  • 返却時の備考入力
  • 物品状態確認

4. 共通コンポーネント

レスポンシブデザイン

  • モバイル対応768px以下
  • タブレット対応768px-1024px
  • デスクトップ対応1024px以上

通知システム

// Snackbar通知
const showNotification = (message: string, severity: 'success' | 'error' | 'warning' | 'info') => {
  // MUI Snackbar + Alert実装
};

// 使用例
showNotification('物品が正常に登録されました', 'success');
showNotification('エラーが発生しました', 'error');

ローディング状態

// スケルトンローディング
<Skeleton variant="rectangular" height={400} />

// プログレスインジケーター
<CircularProgress />

// データテーブルローディング
<DataGrid loading={isLoading} ... />

エラーハンドリング

// エラーバウンダリ
<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. テスト実装

開発環境設定

必要なパッケージ

{
  "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)

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)

{
  "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 クライアント実装例

// 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フロントエンドを実装してください。