# レガシー産業DXにおける技術選定の決定版フレームワーク：木材流通業界の実例から

> 電話・FAX・Excelが主流のレガシー産業で、どのようにDXを実現するか。TypeScript、Python、Golang、AWS Terraformの技術選定から、実装、運用まで、木材流通業界での成功事例を基に実践的なフレームワークを公開します。

- 公開日: 2025-01-08
- 著者: 友田 陽大
- タグ: レガシー産業DX, 技術選定, 業界DX, デジタル変革, TypeScript, Python, Golang, AWS
- URL: https://tomodahinata.com/blog/legacy-industry-dx-technology-selection-framework
- カテゴリ: B2B SaaS・DX戦略
- 総合ガイド: https://tomodahinata.com/blog/award-winning-b2b-saas-architecture-deep-dive

## 要点

- レガシー産業DXの技術選定は業界適合性・非エンジニア対応・段階的導入・長期保守性・コスト最適化の5軸で評価する
- 8種類のステークホルダーの認証・認可にはカスタム属性が柔軟なAWS Cognitoを選定した
- PDF/Excelの重いバッチ処理にはPython（Flask + SQLAlchemy）、帳票はopenpyxl＋LibreOffice変換で再現する
- 非エンジニア対応はExcelライクな操作性とわかりやすいエラーメッセージで使えるUIを設計する
- 段階的導入で既存業務と併存させ、いきなりの全面移行を避ける

---

# レガシー産業DXにおける技術選定の決定版フレームワーク：木材流通業界の実例から

## はじめに：なぜレガシー産業DXは難しいのか

製造業、建設業、物流業、農林水産業——これらの「レガシー産業」は、依然として電話、FAX、Excelでの業務が主流です。デジタル化の恩恵を受けられず、**非効率な業務フロー、情報の属人化、人材不足**に悩んでいます。

私はこの2年間、木材流通業界という「超レガシー産業」のDXに挑戦し、**経済産業大臣賞を受賞**するプロダクトを開発しました。本記事では、その経験から導き出した**「レガシー産業DXにおける技術選定の決定版フレームワーク」**を公開します。

特に、**「どの技術を選ぶべきか」** という技術選定の意思決定に焦点を当て、実践的な指針を提供します。

---

## フレームワーク概要：5つの評価軸

レガシー産業DXの技術選定では、以下の5つの評価軸で技術を選定します。

| 評価軸 | 重要度 | 説明 |
|--------|--------|------|
| **業界適合性** | ★★★★★ | 業界特有の複雑さ（商流、ユーザー属性、業務フロー）に対応できるか |
| **非エンジニア対応** | ★★★★☆ | 現場の非エンジニアが使いこなせるか（UI/UX、マニュアル） |
| **段階的導入** | ★★★★☆ | 既存業務フローと併存しながら、段階的に移行できるか |
| **長期保守性** | ★★★★★ | 10年以上の長期運用に耐えられるか（技術的負債、属人化リスク） |
| **コスト最適化** | ★★★☆☆ | 初期コスト、ランニングコストが適切か |

---

## ケーススタディ：木材流通業界DXの技術選定

### 業界の特徴：8種類のステークホルダーと複雑な商流

木材流通業界には、以下の8種類のステークホルダーが存在します。

```
林業 → 市場 → 製材所 → プレカット → 工務店
                ↓
            メーカー → 問屋 → その他
```

各ステークホルダーで以下が異なります：
- **実行可能な機能**（発注、在庫管理、見積作成）
- **閲覧可能な情報**（自社の在庫のみ、市場の全在庫、取引先の在庫）
- **価格設定権限**（市場価格、卸価格、小売価格）

### 技術選定の決定プロセス

#### 軸1: 業界適合性 - AWS Cognito + カスタムロジック

**要件:**
- 8種類のユーザー属性ごとの認証・認可
- 各属性で異なる権限管理

**候補技術の比較:**

| 技術 | メリット | デメリット | 評価 |
|------|---------|-----------|------|
| Firebase Auth | 簡単、無料枠大 | カスタム属性の柔軟性低い | × |
| Auth0 | 柔軟性高い | コスト高（MAU課金） | △ |
| AWS Cognito | AWS統合、カスタム属性 | 学習コスト | ◎ |
| 自前実装 | 完全カスタマイズ | セキュリティリスク、工数大 | × |

**決定:** AWS Cognito

**理由:**
```typescript
// AWS Cognitoのカスタム属性で8種類のユーザー属性を管理
interface UserAttributes {
  'custom:user_type': 'lumber_mill' | 'market' | 'manufacturer' | 'precut' | 'builder' | 'wholesaler' | 'other';
  'custom:company_id': string;
  'custom:permissions': string; // "create_order,view_inventory,manage_users"
}

// 権限チェック関数
const hasPermission = (user: UserAttributes, permission: string): boolean => {
  const permissions = user['custom:permissions'].split(',');
  return permissions.includes(permission);
};
```

---

#### 軸2: フロントエンド - React + TypeScript + Vite

**要件:**
- 複雑なユーザー属性ごとのUI制御
- 大量のデータ操作（在庫一覧、発注履歴）
- 高速なレスポンス

**候補技術の比較:**

| 技術 | メリット | デメリット | 評価 |
|------|---------|-----------|------|
| jQuery | 学習コスト低 | 複雑なUI管理困難 | × |
| Vue.js | シンプル、学習容易 | 大規模アプリに不向き | △ |
| React + TypeScript | 型安全、エコシステム | 学習コスト | ◎ |
| Next.js | SSR/SSG最適化 | オーバースペック（SPA向き） | △ |

**決定:** React + TypeScript + Vite

**理由:**
```typescript
// TypeScriptによる型安全なユーザー属性管理
type UserType = 'lumber_mill' | 'market' | 'manufacturer';

interface User {
  id: string;
  userType: UserType;
  companyId: string;
  permissions: Set<Permission>;
}

// ユーザー属性ごとの条件付きレンダリング
const Dashboard: React.FC<{ user: User }> = ({ user }) => {
  return (
    <div>
      {user.permissions.has('create_order') && <CreateOrderButton />}
      {user.permissions.has('view_inventory') && <InventoryList />}
      {user.userType === 'market' && <MarketPriceChart />}
    </div>
  );
};
```

**Tanstack Query（React Query）の導入:**
```typescript
import { useQuery } from '@tanstack/react-query';

const useInventory = (companyId: string) => {
  return useQuery({
    queryKey: ['inventory', companyId],
    queryFn: () => fetchInventory(companyId),
    staleTime: 5 * 60 * 1000, // 5分間キャッシュ
    refetchOnWindowFocus: true,
  });
};

// コンポーネントでの使用
const InventoryList: React.FC = () => {
  const { data, isLoading, error } = useInventory(user.companyId);

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return <Table data={data} />;
};
```

**メリット:**
- APIキャッシュの効率化（無駄なリクエスト削減）
- データフェッチの責務分離（コンポーネントとロジックの分離）

---

#### 軸3: バックエンド - Python (Flask + SQLAlchemy)

**要件:**
- PDF/Excelの重いバッチ処理（並列処理）
- 複雑なビジネスロジック（価格計算、在庫管理）
- 既存ExcelデータのDB化

**候補技術の比較:**

| 技術 | メリット | デメリット | 評価 |
|------|---------|-----------|------|
| Node.js (Express) | TypeScript統一、軽量 | 重い処理に不向き | △ |
| Python (Flask) | データ処理、並列処理 | パフォーマンス（GIL） | ◎ |
| Golang (Gin) | 高速、並行処理 | エコシステム小 | △ |
| Java (Spring Boot) | エンタープライズ実績 | 重い、学習コスト | × |

**決定:** Python (Flask + SQLAlchemy)

**理由:**
```python
# 帳票生成：openpyxl で Excel を作り、LibreOffice ヘッドレスで PDF 化
import subprocess
from openpyxl import load_workbook

def generate_invoice(order) -> tuple[str, str]:
    """請求書を Excel テンプレートから生成し、PDF へ変換する。"""
    wb = load_workbook("templates/invoice.xlsx")
    ws = wb.active
    ws["B2"] = order.invoice_number
    ws["B3"] = order.customer_name
    # ... 明細を流し込む
    xlsx_path = f"invoices/{order.id}.xlsx"
    wb.save(xlsx_path)

    # 業務帳票はレイアウト再現性が重要なので、表計算をそのまま PDF 化する
    subprocess.run(
        ["libreoffice", "--headless", "--convert-to", "pdf", "--outdir", "invoices/", xlsx_path],
        check=True,
    )
    return xlsx_path, xlsx_path.replace(".xlsx", ".pdf")


# Excel→DB 取り込み：openpyxl（read_only）＋ psycopg2 の execute_values で一括 INSERT
from openpyxl import load_workbook
from psycopg2.extras import execute_values

def import_excel_to_db(conn, file_path: str) -> None:
    """既存 Excel を DB へ一括インポートする（S3 トリガーの Lambda で実行）。"""
    wb = load_workbook(file_path, read_only=True, data_only=True)
    rows = []
    for r in wb.active.iter_rows(min_row=2, values_only=True):
        product_id, name, price, stock = r
        if product_id is None or price is None:
            continue  # 欠損行はスキップ
        rows.append((product_id, name, float(price), stock))

    with conn.cursor() as cur:
        execute_values(
            cur,
            "INSERT INTO products (product_id, name, price, stock) VALUES %s",
            rows,  # 行ループの個別 INSERT ではなく一括投入（高速・低負荷）
        )
    conn.commit()
```

**メリット:**
- **openpyxl**: テンプレート Excel をそのまま編集でき、業界の帳票レイアウトを忠実に再現
- **LibreOffice ヘッドレス変換**: Excel と寸分違わぬ PDF を生成（帳票は見た目の一致が重要）
- **execute_values による一括 INSERT**: 行ごとの ORM add より桁違いに速く、取り込みは S3 イベント駆動 Lambda へ分離してサーバ本体を圧迫しない

---

#### 軸4: インフラ - AWS (ECS on Fargate + Terraform)

**要件:**
- スケーラブルなSaaS基盤
- 再現性の高いインフラ（IaC）
- コンテナベースのマイクロサービス

**候補技術の比較:**

| 技術 | メリット | デメリット | 評価 |
|------|---------|-----------|------|
| AWS EC2 | 自由度高 | スケール手動、管理コスト | × |
| AWS ECS on Fargate | サーバーレス、自動スケール | 若干高コスト | ◎ |
| Kubernetes (EKS) | 柔軟性最高 | 複雑、オーバースペック | △ |
| Heroku | 簡単 | コスト高、柔軟性低 | × |

**決定:** AWS ECS on Fargate + Terraform

**Terraformによるインフラコード化:**
```hcl
# ECS Cluster
resource "aws_ecs_cluster" "main" {
  name = "lumber-saas-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
  family                   = "lumber-saas-app"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "1024"
  memory                   = "2048"

  container_definitions = jsonencode([{
    name  = "app"
    image = "${var.ecr_repository_url}:latest"
    portMappings = [{
      containerPort = 8080
      protocol      = "tcp"
    }]
    environment = [
      { name = "DATABASE_URL", value = var.database_url },
      { name = "COGNITO_USER_POOL_ID", value = aws_cognito_user_pool.main.id }
    ]
  }])
}

# Auto Scaling
resource "aws_appautoscaling_target" "ecs_target" {
  max_capacity       = 10
  min_capacity       = 2
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.app.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "ecs_policy" {
  name               = "scale-up"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.ecs_target.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs_target.service_namespace

  target_tracking_scaling_policy_configuration {
    target_value = 70.0
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
  }
}
```

**メリット:**
- **Fargate**: サーバー管理不要、自動スケール
- **Terraform**: インフラ全体をコード化、再現性100%
- **Auto Scaling**: CPU使用率に応じて自動スケール（コスト最適化）

---

## 非エンジニア対応：UI/UXの徹底的な簡略化

### 課題：現場の非エンジニアが使えるか

レガシー産業の現場では、ITリテラシーが低いユーザーが多数います。「Excelは使えるが、Webシステムは使えない」というユーザーに対応する必要があります。

### 実装：直感的なUI設計

**1. Excelライクな操作性**
```typescript
// ag-Grid を使用したExcelライクなテーブル
import { AgGridReact } from 'ag-grid-react';

const InventoryTable: React.FC = () => {
  const columnDefs = [
    { field: 'product_name', headerName: '商品名', editable: true },
    { field: 'stock', headerName: '在庫数', editable: true },
    { field: 'price', headerName: '単価', editable: true },
  ];

  const onCellValueChanged = (params: any) => {
    // セル編集時、自動保存
    updateInventory(params.data);
  };

  return (
    <AgGridReact
      columnDefs={columnDefs}
      rowData={inventoryData}
      onCellValueChanged={onCellValueChanged}
      enableRangeSelection={true} // Excel風の範囲選択
    />
  );
};
```

**2. わかりやすいエラーメッセージ**
```typescript
// ❌ 悪い例
throw new Error('ValidationError: field "price" must be positive');

// ✅ 良い例
throw new Error('価格は0円以上で入力してください。現在の入力値: -1000円');
```

---

## 段階的導入：既存業務との併存

### 課題：「いきなり全面移行」は失敗する

レガシー産業では、「いきなりExcelを廃止してWebシステムに移行」すると、現場が混乱し、失敗します。

### 戦略：3段階導入

```
Phase 1（1-3ヶ月）: 情報共有のみ（Excel併用OK）
  ↓
Phase 2（4-6ヶ月）: 発注機能の一部移行（Excel併用）
  ↓
Phase 3（7-12ヶ月）: 全面移行（Excel廃止）
```

**実装例:**
```python
# Phase 1: Excel アップロード機能
@app.route('/api/inventory/upload', methods=['POST'])
def upload_excel():
    """既存Excelをアップロードして、DBに同期"""
    file = request.files['file']
    df = pd.read_excel(file)

    # DB同期
    sync_inventory_from_dataframe(df)

    return jsonify({'message': 'Excelをアップロードしました。Web画面でも確認できます。'})

# Phase 2: Excelダウンロード機能（Excel併用）
@app.route('/api/inventory/download', methods=['GET'])
def download_excel():
    """Web上のデータをExcelでダウンロード"""
    inventory = Inventory.query.all()
    df = pd.DataFrame([inv.to_dict() for inv in inventory])

    output = io.BytesIO()
    df.to_excel(output, index=False)
    output.seek(0)

    return send_file(output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                     as_attachment=True, download_name='inventory.xlsx')
```

---

## まとめ：レガシー産業DX技術選定の5原則

1. **業界適合性を最優先せよ** - 汎用的なソリューションではなく、業界特有の複雑さに対応できる技術を選ぶ
2. **非エンジニア対応を徹底せよ** - ITリテラシーが低いユーザーでも使える、直感的なUI/UXを設計
3. **段階的導入を計画せよ** - いきなり全面移行せず、既存業務と併存しながら段階的に移行
4. **長期保守性を確保せよ** - 10年以上の長期運用に耐えられる、技術的負債を生まない設計
5. **IaCで再現性を担保せよ** - インフラをコード化し、災害復旧やスケールアウトに対応

---

## あなたのレガシー産業DXも実現可能です

製造業、建設業、物流業、農林水産業——あらゆるレガシー産業のDXをサポートします。要件定義から設計、実装、インフラ構築まで、ワンストップで対応可能です。

**無料技術相談（30分）** を実施していますので、お気軽にご連絡ください。

[お問い合わせはこちら](/contact)
