0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flask + SQLAlchemyで作る「キャリア・クエスト」- 仮想キャリア体験プラットフォームの裏側(Part 2)

Posted at

はじめに

この記事は「キャリア・クエスト」の技術解説の続きです。Part 1では、プロジェクトの概要、技術選択の理由、アーキテクチャ設計、そしてデータベース設計について説明しました。Part 2では、以下の内容を詳しく解説します:

  • ユーザー認証システムの構築
  • キャリアシミュレーションロジックの実装
  • フロントエンドの実装と工夫点
  • 開発で直面した課題と解決策

目次

  1. ユーザー認証システムの構築
  2. キャリアシミュレーションロジックの実装
  3. フロントエンドの実装と工夫点
  4. 開発で直面した課題と解決策

1. ユーザー認証システムの構築

ユーザー認証システムは、アプリケーションのセキュリティにとって非常に重要です。Flask-Loginを使用して、安全で使いやすい認証システムを実装しました。

ユーザー登録の実装

auth.py
from flask import Flask, request, render_template, redirect, url_for, flash
from flask_login import LoginManager, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
from models import User, db

app = Flask(__name__)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        
        user = User.query.filter_by(username=username).first()
        if user:
            flash('このユーザー名は既に使用されています。', 'error')
            return redirect(url_for('register'))
        
        new_user = User(username=username, password=generate_password_hash(password, method='sha256'))
        db.session.add(new_user)
        db.session.commit()
        
        flash('登録が完了しました。ログインしてください。', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html')

このコードの主なポイントは以下の通りです:

  1. Flask-Loginを使用して、ログイン状態の管理を簡素化しています。
  2. パスワードはgenerate_password_hash関数を使用してハッシュ化しています。これにより、平文でのパスワード保存を避け、セキュリティを向上させています。
  3. ユーザー名の重複チェックを行い、既存のユーザー名での登録を防いでいます。
  4. フラッシュメッセージを使用して、ユーザーに適切なフィードバックを提供しています。

ログイン機能の実装

auth.py
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter_by(username=username).first()
        
        if user and check_password_hash(user.password, password):
            login_user(user)
            flash('ログインに成功しました。', 'success')
            return redirect(url_for('dashboard'))
        else:
            flash('ログインに失敗しました。ユーザー名またはパスワードを確認してください。', 'error')
    
    return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('ログアウトしました。', 'info')
    return redirect(url_for('index'))

ログイン機能の実装ポイント:

  1. check_password_hash関数を使用して、入力されたパスワードとハッシュ化されたパスワードを安全に比較しています。
  2. ログイン成功時はlogin_user関数を使用してユーザーをログイン状態にします。
  3. @login_requiredデコレータを使用して、ログインが必要なルートを保護しています。
  4. ログアウト機能も実装し、logout_user関数でセッションを終了させています。

2. キャリアシミュレーションロジックの実装

キャリアシミュレーションは本アプリケーションの核となる機能です。ユーザーの選択に基づいて、キャリアパスをシミュレートします。

simulation.py
import json
from models import User, TimelineEvent, CareerChoice, db

def simulate_career_choice(user_id, choice_id):
    user = User.query.get(user_id)
    choice = CareerChoice.query.get(choice_id)
    
    if not user or not choice:
        return False, "Invalid user or choice"
    
    # キャリアパスの更新
    career_path = json.loads(user.career_path)
    career_path.append(choice_id)
    user.career_path = json.dumps(career_path)
    
    # タイムラインイベントの作成
    event = TimelineEvent(
        user_id=user.id,
        event_type='career_choice',
        description=f'選択: {choice.name}'
    )
    db.session.add(event)
    
    # 次の選択肢を決定
    next_choices = json.loads(choice.next_choices)
    
    # ユーザーの状態更新(例:タイムクリスタルの増減)
    user.time_crystals += 1  # 選択ごとにタイムクリスタルを1つ獲得
    
    db.session.commit()
    
    return True, next_choices

@app.route('/make_choice', methods=['POST'])
@login_required
def make_choice():
    choice_id = request.form.get('choice_id')
    success, result = simulate_career_choice(current_user.id, choice_id)
    
    if success:
        flash('キャリア選択が反映されました。', 'success')
        return redirect(url_for('game', next_choices=result))
    else:
        flash('選択の処理中にエラーが発生しました。', 'error')
        return redirect(url_for('game'))

シミュレーションロジックの主なポイント:

  1. ユーザーのキャリアパスをcareer_pathフィールドに JSON 形式で保存しています。これにより、複雑なパスの履歴を柔軟に管理できます。
  2. 各選択をタイムラインイベントとして記録し、ユーザーの行動履歴を追跡しています。
  3. 次の選択肢はCareerChoiceモデルのnext_choicesフィールドから動的に取得します。これにより、柔軟なシナリオ設計が可能になります。
  4. ゲーム内通貨(タイムクリスタル)の管理も行っており、ユーザーのアクションに応じて増減させています。

3. フロントエンドの実装と工夫点

フロントエンドは主に HTML、CSS、JavaScript を使用して実装しました。特に、ユーザーエクスペリエンスを向上させるために以下の点に注力しました。

インタラクティブな選択肢の表示

game.js
document.addEventListener('DOMContentLoaded', function() {
    const choiceButtons = document.querySelectorAll('.choice-button');
    
    choiceButtons.forEach(button => {
        button.addEventListener('click', function() {
            const choiceId = this.dataset.choiceId;
            document.getElementById('choice_id').value = choiceId;
            document.getElementById('choice-form').submit();
        });
    });
});

このJavaScriptコードは、選択肢ボタンをクリックした際の動作を定義しています。ユーザーの選択をスムーズに処理し、サーバーにデータを送信します。

タイムラインの動的更新

timeline.js
function updateTimeline() {
    fetch('/api/timeline')
        .then(response => response.json())
        .then(data => {
            const timelineContainer = document.getElementById('timeline-container');
            timelineContainer.innerHTML = '';
            data.forEach(event => {
                const eventElement = document.createElement('div');
                eventElement.className = 'timeline-event';
                eventElement.innerHTML = `
                    <h3>${event.event_type}</h3>
                    <p>${event.description}</p>
                    <small>${new Date(event.timestamp).toLocaleString()}</small>
                `;
                timelineContainer.appendChild(eventElement);
            });
        });
}

// 5秒ごとにタイムラインを更新
setInterval(updateTimeline, 5000);

このコードは、ユーザーのタイムラインを定期的に更新します。Ajax を使用してサーバーから最新のイベントデータを取得し、DOM を動的に更新しています。

4. 開発で直面した課題と解決策

課題1: 複雑なキャリアパスの管理

問題: ユーザーの選択肢が増えるにつれて、可能なキャリアパスの組み合わせが指数関数的に増加し、管理が困難になりました。

解決策:

  1. キャリアパスをグラフ構造として設計し、各ノードをCareerChoiceモデルとして表現しました。
  2. 次の選択肢を動的に生成するアルゴリズムを実装し、ハードコードされたパスに依存しないようにしました。
  3. A/Bテストを導入し、新しいキャリアパスの有効性を検証できるようにしました。

課題2: パフォーマンスの最適化

問題: ユーザー数の増加に伴い、特にタイムライン機能でパフォーマンスの低下が見られました。

解決策:

  1. データベースにインデックスを追加し、クエリの速度を改善しました。
  2. Redisを導入し、頻繁にアクセスされるデータをキャッシュしました。
  3. ページネーションを実装し、一度に表示するデータ量を制限しました。
optimization.py
from flask_caching import Cache

cache = Cache(app, config={'CACHE_TYPE': 'redis'})

@app.route('/timeline')
@cache.cached(timeout=60)  # 1分間キャッシュ
def get_timeline():
    page = request.args.get('page', 1, type=int)
    events = TimelineEvent.query.filter_by(user_id=current_user.id)\
        .order_by(TimelineEvent.timestamp.desc())\
        .paginate(page=page, per_page=20)
    return render_template('timeline.html', events=events)

このコードでは、Redisを使用したキャッシュとページネーションを組み合わせて、タイムライン表示のパフォーマンスを大幅に改善しています。

まとめ

「キャリア・クエスト」の開発を通じて、複雑なビジネスロジックを持つWebアプリケーションの設計と実装について多くの学びがありました。特に、ユーザーの行動に基づいて動的にコンテンツを生成し、それを効率的に管理・表示する方法は、他の多くのプロジェクトにも応用できる貴重な経験となりました。

今後は、機械学習を活用してよりパーソナライズされたキャリア提案を行うなど、さらなる機能の拡張を計画しています。

この記事が「キャリア・クエスト」の技術的な側面を理解する助けになれば幸いです。質問やフィードバックがあれば、ぜひコメントでお知らせください。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?