はじめに
この記事は「キャリア・クエスト」の技術解説の続きです。Part 1では、プロジェクトの概要、技術選択の理由、アーキテクチャ設計、そしてデータベース設計について説明しました。Part 2では、以下の内容を詳しく解説します:
- ユーザー認証システムの構築
- キャリアシミュレーションロジックの実装
- フロントエンドの実装と工夫点
- 開発で直面した課題と解決策
目次
1. ユーザー認証システムの構築
ユーザー認証システムは、アプリケーションのセキュリティにとって非常に重要です。Flask-Loginを使用して、安全で使いやすい認証システムを実装しました。
ユーザー登録の実装
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')
このコードの主なポイントは以下の通りです:
-
Flask-Login
を使用して、ログイン状態の管理を簡素化しています。 - パスワードは
generate_password_hash
関数を使用してハッシュ化しています。これにより、平文でのパスワード保存を避け、セキュリティを向上させています。 - ユーザー名の重複チェックを行い、既存のユーザー名での登録を防いでいます。
- フラッシュメッセージを使用して、ユーザーに適切なフィードバックを提供しています。
ログイン機能の実装
@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'))
ログイン機能の実装ポイント:
-
check_password_hash
関数を使用して、入力されたパスワードとハッシュ化されたパスワードを安全に比較しています。 - ログイン成功時は
login_user
関数を使用してユーザーをログイン状態にします。 -
@login_required
デコレータを使用して、ログインが必要なルートを保護しています。 - ログアウト機能も実装し、
logout_user
関数でセッションを終了させています。
2. キャリアシミュレーションロジックの実装
キャリアシミュレーションは本アプリケーションの核となる機能です。ユーザーの選択に基づいて、キャリアパスをシミュレートします。
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'))
シミュレーションロジックの主なポイント:
- ユーザーのキャリアパスを
career_path
フィールドに JSON 形式で保存しています。これにより、複雑なパスの履歴を柔軟に管理できます。 - 各選択をタイムラインイベントとして記録し、ユーザーの行動履歴を追跡しています。
- 次の選択肢は
CareerChoice
モデルのnext_choices
フィールドから動的に取得します。これにより、柔軟なシナリオ設計が可能になります。 - ゲーム内通貨(タイムクリスタル)の管理も行っており、ユーザーのアクションに応じて増減させています。
3. フロントエンドの実装と工夫点
フロントエンドは主に HTML、CSS、JavaScript を使用して実装しました。特に、ユーザーエクスペリエンスを向上させるために以下の点に注力しました。
インタラクティブな選択肢の表示
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コードは、選択肢ボタンをクリックした際の動作を定義しています。ユーザーの選択をスムーズに処理し、サーバーにデータを送信します。
タイムラインの動的更新
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: 複雑なキャリアパスの管理
問題: ユーザーの選択肢が増えるにつれて、可能なキャリアパスの組み合わせが指数関数的に増加し、管理が困難になりました。
解決策:
- キャリアパスをグラフ構造として設計し、各ノードを
CareerChoice
モデルとして表現しました。 - 次の選択肢を動的に生成するアルゴリズムを実装し、ハードコードされたパスに依存しないようにしました。
- A/Bテストを導入し、新しいキャリアパスの有効性を検証できるようにしました。
課題2: パフォーマンスの最適化
問題: ユーザー数の増加に伴い、特にタイムライン機能でパフォーマンスの低下が見られました。
解決策:
- データベースにインデックスを追加し、クエリの速度を改善しました。
- Redisを導入し、頻繁にアクセスされるデータをキャッシュしました。
- ページネーションを実装し、一度に表示するデータ量を制限しました。
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アプリケーションの設計と実装について多くの学びがありました。特に、ユーザーの行動に基づいて動的にコンテンツを生成し、それを効率的に管理・表示する方法は、他の多くのプロジェクトにも応用できる貴重な経験となりました。
今後は、機械学習を活用してよりパーソナライズされたキャリア提案を行うなど、さらなる機能の拡張を計画しています。
この記事が「キャリア・クエスト」の技術的な側面を理解する助けになれば幸いです。質問やフィードバックがあれば、ぜひコメントでお知らせください。