はじめに
一晩でカスタマーサポートシステムのプロトタイプを構築する機会がありました。Rails 7によるAPI開発とバニラJavaScriptによるフロントエンド実装という、よくある構成ですが、その実装過程と最終的なアーキテクチャを淡々と記録します。システム概要
基本構成
- バックエンド: Rails 7 (APIモード)
- フロントエンド: バニラHTML/CSS/JavaScript
- データベース: SQLite (開発用)
- 認証: JWT Bearer Token
- API仕様: OpenAPI 3.0 (1,191行)
機能範囲
- 有人オペレーター向けチケット管理システム
- ダッシュボード機能
- 顧客情報管理
- FAQ管理・検索
- 応答テンプレート機能
- チーム管理機能
バックエンドアーキテクチャ
Rails API構成
api/
├── app/
│ ├── controllers/
│ │ ├── application_controller.rb
│ │ └── concerns/
│ ├── models/
│ │ ├── customer.rb # 顧客モデル
│ │ └── concerns/
│ └── jobs/
├── config/
│ └── routes.rb # API ルーティング
└── db/
└── migrate/
└── create_customers.rb
APIエンドポイント設計
RESTful原則に基づく設計:config/routes.rb
namespace :api do namespace :v1 do # 認証 namespace :auth do post :login delete :logout get :me end# ダッシュボード get 'dashboard/stats' get 'dashboard/urgent-tickets'
# チケット管理 (inquiries controllerにマッピング) resources :tickets, controller: 'inquiries' do member do patch :take patch :status get :responses post :responses end end
# 顧客管理 resources :customers do member do get :history end end
# FAQ管理 resources :faqs do member do post :vote post :apply end collection do get :search get :analytics end end end end
データモデル設計
Customer モデル:class Customer < ApplicationRecord # リレーション has_many :inquiries, dependent: :destroy# enum定義 enum :segment, { vip: 0, regular: 1, new_user: 2 }, default: :regular
# バリデーション validates :name, presence: true, length: { maximum: 255 } validates :contact_person, presence: true validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :past_inquiries_count, numericality: { greater_than_or_equal_to: 0 }
# スコープ定義 scope :by_segment, ->(segment) { where(segment: segment) } scope :with_recent_inquiries, ->(days = 30) { where('last_inquiry_date >= ?', days.days.ago) } scope :vip_customers, -> { where(segment: :vip) }
# インスタンスメソッド def increment_inquiry_count! increment!(:past_inquiries_count) touch(:last_inquiry_date) end
private
def normalize_email self.email = email&.downcase&.strip end end
フロントエンドアーキテクチャ
ディレクトリ構成
frontend/
├── index.html # メインダッシュボード
├── ticket-detail.html # チケット詳細画面
├── script.js # メインJavaScript (762行)
├── ticket-detail.js # チケット詳細ロジック
├── styles.css # スタイル定義
└── ticket-detail.css # チケット詳細スタイル
JavaScriptアーキテクチャ
モジュール構成:// script.js の主要構成要素// 1. グローバル状態管理 let currentTab = 'unassigned'; let currentSection = 'dashboard'; let currentUser = null; let dashboardStats = null;
// 2. 初期化系関数 function initializeNavigation() { / ナビゲーション制御 / } function initializeTabs() { / タブ切り替え制御 / } function initializeButtons() { / ボタンイベント制御 / }
// 3. データ取得・表示系 async function loadDashboardData() { / ダッシュボード表示 / } async function loadTicketsData() { / チケット一覧表示 / } function renderTicketsTable(tickets) { / テーブル描画 / }
// 4. 業務ロジック系 async function handleTakeTicket(button) { / チケット取得処理 / } function handleFAQSearch(e) { / FAQ検索処理 / }
// 5. UI制御系 function switchSection(sectionName) { / セクション切り替え / } function animateStatCards() { / 統計カードアニメーション / } function showNotification(message, type) { / 通知表示 / }
API通信層設計
認証付きAPIクライアント:エラーハンドリング戦略:// AuthenticatedAPIClient パターン function getAPIClient() { return window.apiClient || new AuthenticatedAPIClient(); }
// 使用例 const apiClient = getAPIClient(); const response = await apiClient.get('/tickets?status=unassigned');
// 統一されたエラーハンドリング
try {
const response = await apiClient.get('/dashboard/stats');
dashboardStats = response;
updateDashboardDisplay(dashboardStats);
} catch (error) {
console.error('Dashboard data loading error:', error);
// フォールバック: モックデータ表示
loadMockDashboardData();
// ユーザー通知
UIUtils.showToast('接続警告', 'APIサーバーに接続できません', 'warning', 5000);
}
OpenAPI仕様書
仕様書規模
- 総行数: 1,191行
- エンドポイント数: 25個
- スキーマ定義: 23個
- 認証方式: JWT Bearer Token
主要エンドポイント
認証系:POST /auth/login- ユーザーログインDELETE /auth/logout- ログアウトGET /auth/me- 現在ユーザー情報
GET /tickets- チケット一覧 (フィルタ・ソート対応)GET /tickets/{id}- チケット詳細PATCH /tickets/{id}/take- チケット取得PATCH /tickets/{id}/status- ステータス更新GET|POST /tickets/{id}/responses- 応答履歴・送信
GET /faqs/search- FAQ検索GET /faqs/suggestions/{ticket_id}- チケット関連FAQ提案POST /faqs/{id}/apply- FAQ適用記録GET /faqs/analytics- FAQ分析データ
技術的特徴
1. プログレッシブローディング
async function loadTicketDetail(ticketId) {
// 1. キャッシュされた基本情報をまず表示
const cachedTicket = this.cache.get(ticket-${ticketId});
if (cachedTicket) {
this.renderBasicInfo(cachedTicket);
}
// 2. 並行して詳細情報を取得
const [fullTicket, customerHistory, faqSuggestions] = await Promise.all([
this.systemManager.getTicketDetail(ticketId),
this.systemManager.getCustomerHistory(ticketId),
this.systemManager.getFAQSuggestions(ticketId)
]);
// 3. 段階的に画面を更新
this.updateTicketInfo(fullTicket);
this.renderCustomerHistory(customerHistory);
this.renderFAQSuggestions(faqSuggestions);
}
2. データ正規化レイヤー
function createTicketRow(ticket) {
// APIレスポンスとモックデータの両方に対応
const ticketId = ticket.id || ticket.ticket_id;
const customerName = ticket.customer_name || ticket.customer?.name || ticket.customer || '顧客名不明';
const title = ticket.title || ticket.subject || '件名なし';
const priority = ticket.priority || 'normal';
const elapsed = ticket.elapsed || formatElapsedTime(ticket.created_at);
// 統一フォーマットで行を生成
return row;
}
3. UI状態管理
// グローバル状態による画面制御
function switchSection(sectionName) {
// すべてのセクションを非表示
document.querySelectorAll('.section').forEach(section => {
section.classList.remove('active');
});
// 対象セクションを表示
document.getElementById(sectionName).classList.add('active');
currentSection = sectionName;
// セクション固有の初期化処理
switch(sectionName) {
case 'tickets': loadTicketsData(); break;
case 'faq': loadFAQData(); break;
case 'dashboard': loadDashboardData(); break;
}
}
開発効率の要因
1. Rails APIモードの威力
- Scaffold活用: モデル生成・マイグレーション・バリデーションが迅速
- Routing DSL: RESTfulなルーティングが簡潔に定義可能
- ActiveRecord: 複雑なクエリもスコープとして簡潔に記述
2. バニラJSの簡潔性
- ビルドツール不要: 直接ブラウザで実行可能
- デバッグ容易: ブラウザDevToolsで直接デバッグ
- 学習コスト低: フレームワーク固有の概念が不要
3. OpenAPI駆動開発
- 契約ファーストアプローチ: フロントエンド・バックエンドの並行開発
- 自動ドキュメント生成: Swagger UIでインタラクティブなAPI仕様書
- 型安全性: レスポンススキーマの明確な定義
制約と課題
1. スケーラビリティ制約
- SQLite使用: 本格運用には不適切
- バニラJS: 大規模化時の状態管理複雑化
- 認証方式: JWTの永続化戦略未考慮
2. プロダクション対応課題
- エラーハンドリング: 例外ケースの網羅不足
- セキュリティ: CORS、CSRF対策の詳細化必要
- パフォーマンス: データベースインデックス設計の最適化
3. 運用面課題
- ログ設計: 監査ログ・アクセスログの体系化
- 監視: ヘルスチェック・メトリクス収集の実装
- デプロイ: CI/CD パイプラインの構築
