はじめに
前回のPart 3では、悩み投稿機能の実装とデータベース設計について詳しく解説しました。今回のPart 4では、以下の内容について説明します:
- 回答機能の実装
- 「いいね」機能の実装
- 高度なセキュリティ対策
- コンテンツモデレーション
これらの機能追加とベストプラクティスの適用により、Ballonの機能性、安全性、そしてユーザー体験を向上させます。
1. 回答機能の実装
まず、回答機能を実装します。ユーザーが悩みに対して回答を投稿できるようにします。
データベース設計
回答のためのテーブルを作成します:
class CreateAnswers < ActiveRecord::Migration[5.2]
def change
create_table :answers do |t|
t.references :user, foreign_key: true
t.references :worry, foreign_key: true
t.text :content, null: false
t.boolean :anonymous, default: false
t.timestamps
end
end
end
モデルの実装
Answerモデルを実装します:
class Answer < ActiveRecord::Base
belongs_to :user
belongs_to :worry
has_many :likes, dependent: :destroy
validates :content, presence: true, length: { maximum: 1000 }
validates :user_id, presence: true
validates :worry_id, presence: true
scope :recent, -> { order(created_at: :desc) }
def liked_by?(user)
likes.where(user_id: user.id).exists?
end
before_save :sanitize_content
private
def sanitize_content
self.content = Sanitize.fragment(self.content)
end
end
既存のWorryモデルとUserモデルにも関連を追加します:
class Worry < ActiveRecord::Base
# 既存のコードに追加
has_many :answers, dependent: :destroy
end
class User < ActiveRecord::Base
# 既存のコードに追加
has_many :answers
end
コントローラーロジック
app.rbに回答機能のルーティングとロジックを追加します:
# 回答の作成
post '/worries/:worry_id/answers' do
authenticate!
@worry = Worry.find(params[:worry_id])
@answer = @worry.answers.new(content: params[:content], user: current_user, anonymous: params[:anonymous])
if @answer.save
redirect "/worries/#{@worry.id}"
else
@errors = @answer.errors.full_messages
erb :worry_show
end
end
# 回答の削除
delete '/answers/:id' do
authenticate!
answer = current_user.answers.find(params[:id])
worry_id = answer.worry_id
answer.destroy
redirect "/worries/#{worry_id}"
end
ビューの更新
悩み詳細ページ(worry_show.erb)を更新して、回答の表示と投稿フォームを追加します:
<h2><%= h(@worry.title) %></h2>
<p><%= h(@worry.content) %></p>
<h3>回答</h3>
<% @worry.answers.recent.each do |answer| %>
<div class="answer">
<p><%= h(answer.content) %></p>
<small>
回答者: <%= answer.anonymous ? "匿名" : h(answer.user.username) %>
at <%= answer.created_at.strftime('%Y-%m-%d %H:%M') %>
</small>
<% if answer.user == current_user %>
<form action="/answers/<%= answer.id %>" method="post" style="display: inline;">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="削除" onclick="return confirm('本当に削除しますか?');">
</form>
<% end %>
</div>
<% end %>
<h4>回答を投稿</h4>
<form action="/worries/<%= @worry.id %>/answers" method="post">
<textarea name="content" required></textarea>
<label>
<input type="checkbox" name="anonymous" value="1"> 匿名で投稿する
</label>
<input type="submit" value="回答を投稿">
</form>
2. 「いいね」機能の実装
次に、回答に対する「いいね」機能を実装します。
データベース設計
「いいね」のためのテーブルを作成します:
class CreateLikes < ActiveRecord::Migration[5.2]
def change
create_table :likes do |t|
t.references :user, foreign_key: true
t.references :answer, foreign_key: true
t.timestamps
end
add_index :likes, [:user_id, :answer_id], unique: true
end
end
モデルの実装
Likeモデルを実装します:
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :answer
validates :user_id, uniqueness: { scope: :answer_id }
end
UserモデルとAnswerモデルにLikeの関連を追加します:
class User < ActiveRecord::Base
# 既存のコードに追加
has_many :likes
has_many :liked_answers, through: :likes, source: :answer
end
class Answer < ActiveRecord::Base
# 既存のコードに追加
has_many :likes, dependent: :destroy
has_many :liking_users, through: :likes, source: :user
end
コントローラーロジック
app.rbに「いいね」機能のルーティングとロジックを追加します:
# いいねの追加
post '/answers/:answer_id/likes' do
authenticate!
answer = Answer.find(params[:answer_id])
like = current_user.likes.new(answer: answer)
if like.save
redirect "/worries/#{answer.worry_id}"
else
# エラー処理
redirect "/worries/#{answer.worry_id}"
end
end
# いいねの削除
delete '/answers/:answer_id/likes' do
authenticate!
answer = Answer.find(params[:answer_id])
like = current_user.likes.find_by(answer: answer)
like.destroy if like
redirect "/worries/#{answer.worry_id}"
end
ビューの更新
worry_show.erbを更新して、「いいね」ボタンを追加します:
<% @worry.answers.recent.each do |answer| %>
<div class="answer">
<!-- 既存のコード -->
<p>いいね数: <%= answer.likes.count %></p>
<% if current_user %>
<% if answer.liked_by?(current_user) %>
<form action="/answers/<%= answer.id %>/likes" method="post" style="display: inline;">
<input type="hidden" name="_method" value="delete">
<input type="submit" value="いいねを取り消す">
</form>
<% else %>
<form action="/answers/<%= answer.id %>/likes" method="post" style="display: inline;">
<input type="submit" value="いいね">
</form>
<% end %>
<% end %>
</div>
<% end %>
3. 高度なセキュリティ対策
中高生を対象とするサービスであるため、セキュリティには特に注意を払う必要があります。
CSRF対策
Sinatraには標準でCSRF対策が含まれていないため、rack-protection
gemを使用して実装します。
require 'rack/protection'
use Rack::Protection::AuthenticityToken
そして、フォームに CSRF トークンを追加します:
<form action="/some_action" method="post">
<%= csrf_token_tag %>
<!-- フォームの内容 -->
</form>
csrf_token_tag
メソッドを定義するために、以下のヘルパーを追加します:
helpers do
def csrf_token_tag
Rack::Protection::AuthenticityToken.token(session)
Rack::Protection::AuthenticityToken.token(session)
"<input type='hidden' name='authenticity_token' value='#{Rack::Protection::AuthenticityToken.token(session)}'>"
end
end
XSS対策
ユーザー入力をエスケープするために、Rack::Utils.escape_html
メソッドを使用します。以下のヘルパーメソッドを追加します:
helpers do
def h(text)
Rack::Utils.escape_html(text)
end
end
セキュアなセッション設定
セッションをより安全にするために、以下の設定を追加します:
use Rack::Session::Cookie,
key: 'rack.session',
path: '/',
expire_after: 2592000, # 30日
secret: ENV['SESSION_SECRET'],
same_site: :strict
SESSION_SECRET
は環境変数から読み込むようにし、公開リポジトリにコミットしないようにします。
強力なパスワードポリシー
ユーザー登録時にパスワードの強度をチェックするバリデーションを追加します:
class User < ActiveRecord::Base
# 既存のバリデーションに追加
validate :password_complexity
private
def password_complexity
return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/
errors.add :password, 'パスワードは8文字以上で、大文字、小文字、数字、特殊文字をそれぞれ1つ以上含む必要があります。'
end
end
4. コンテンツモデレーション
中高生のメンタルヘルスに配慮したコンテンツモデレーションを実装します。
キーワードフィルタリング
特定のキーワードを含む投稿を自動的にフラグ付けまたはモデレーションキューに入れます。
class Worry < ActiveRecord::Base
# 既存のコードに追加
before_save :check_sensitive_content
private
def check_sensitive_content
sensitive_words = ['自殺', '薬物', '虐待', # その他センシティブな単語]
if sensitive_words.any? { |word| self.content.include?(word) }
self.needs_moderation = true
end
end
end
モデレーションシステム
フラグ付けされた投稿を管理者が確認できるようにします。
# 管理者用:モデレーションが必要な悩みの一覧
get '/admin/moderation' do
authenticate_admin!
@worries_for_moderation = Worry.where(needs_moderation: true)
erb :admin_moderation
end
# 管理者用:悩みの承認または削除
post '/admin/moderation/:id' do
authenticate_admin!
worry = Worry.find(params[:id])
if params[:approve]
worry.update(needs_moderation: false)
elsif params[:delete]
worry.destroy
end
redirect '/admin/moderation'
end
専門家への紹介
深刻な悩みの場合、専門家への相談を促すメッセージを表示します。
<% if @worry.needs_expert_advice? %>
<div class="alert">
この悩みについては、専門家に相談することをおすすめします。
<a href="/resources">相談窓口一覧はこちら</a>
</div>
<% end %>
ポジティブな環境作り
回答の内容が否定的または攻撃的でないかチェックし、ポジティブな表現を促します。
class Answer < ActiveRecord::Base
# 既存のコードに追加
validate :encourage_positivity
private
def encourage_positivity
negative_words = ['バカ', 'ダメ', '無理', # その他ネガティブな単語]
if negative_words.any? { |word| self.content.include?(word) }
errors.add(:content, 'もう少し前向きな言葉で表現してみましょう。相手の気持ちを考えて回答することが大切です。')
end
end
end
まとめ
Part 4では、Ballonの回答機能と「いいね」機能の実装、高度なセキュリティ対策、そしてコンテンツモデレーションについて詳しく解説しました。これらの機能と対策により、Ballonはより安全で使いやすい悩み相談プラットフォームとなります。
特に以下の点に注目しました:
- 回答機能の実装:ユーザーが悩みに対して回答を投稿できるようになり、コミュニティでの支援が可能になりました。
- 「いいね」機能:有益な回答を評価する仕組みを導入し、質の高い回答を促進します。
- セキュリティ対策:CSRF対策、XSS対策、セキュアなセッション管理など、多層的なセキュリティ対策を実装しました。
- コンテンツモデレーション:キーワードフィルタリング、管理者用モデレーションシステム、専門家への紹介機能など、中高生のメンタルヘルスに配慮した仕組みを導入しました。
中高生向けサービスとしての特別な配慮事項
Ballonは中高生を対象としているため、以下のような特別な配慮を行っています:
-
年齢に適した言葉遣い:
システムメッセージやエラーメッセージを、中高生にわかりやすい言葉で表現します。helpers.rbdef user_friendly_message(key) messages = { welcome: "Ballonへようこそ!みんなで支え合う場所だよ。", password_error: "パスワードは8文字以上で、大文字、小文字、数字、記号をそれぞれ1つ以上含めてね。", content_too_long: "書いてくれてありがとう!でも少し長いかも。もう少し短くできるかな?", # 他のメッセージ... } messages[key] || "なにかうまくいかないみたい。もう一度試してみてね。" end
-
利用時間の制限:
健全な生活リズムを守るため、深夜の利用を控えめにするよう促します。app.rbbefore do @current_hour = Time.now.hour if @current_hour >= 22 || @current_hour < 6 @late_night_warning = true end end
views/layout.erb<% if @late_night_warning %> <div class="alert"> 夜遅くまで起きているのかな?十分な睡眠は大切だよ。明日また来てね! </div> <% end %>
-
ポジティブな表現の奨励:
否定的な表現を使用した場合、より前向きな表現を提案します。models.rbclass Answer < ActiveRecord::Base # 既存のコードに追加 before_save :suggest_positive_expression private def suggest_positive_expression negative_phrases = { "できない" => "まだ難しいかもしれない", "無理" => "チャレンジが必要", "だめ" => "改善の余地がある" # 他のフレーズ... } negative_phrases.each do |negative, positive| if self.content.include?(negative) self.content += "\n\n[アドバイス] 「#{negative}」という表現の代わりに、「#{positive}」というように言い換えてみるのはどうかな?ポジティブな表現を使うと、相手も前向きな気持ちになれるよ。" end end end end
-
教育的要素の組み込み:
悩みの解決過程を通じて、問題解決スキルや感情管理スキルを学べるような仕組みを提供します。app.rbget '/learn/problem_solving' do @steps = [ {title: "問題を明確にする", description: "どんな状況で、何が困っているのか具体的に書き出してみよう。"}, {title: "可能な解決策を考える", description: "思いつく限りのアイデアを出してみよう。この時点では実現可能かどうかは気にしなくていいよ。"}, {title: "それぞれの解決策のメリット・デメリットを考える", description: "各アイデアについて、良い点と悪い点を考えてみよう。"}, {title: "最適な解決策を選ぶ", description: "メリットとデメリットを比較して、一番良さそうな方法を選んでみよう。"}, {title: "実行する", description: "選んだ方法を実際に試してみよう。うまくいかなくても大丈夫、それも大切な経験だよ。"}, {title: "結果を評価する", description: "どうだった?うまくいったところ、改善が必要なところを振り返ってみよう。"} ] erb :problem_solving_guide end
views/problem_solving_guide.erb<h2>問題解決のステップ</h2> <p>困ったことがあったときは、以下のステップを試してみよう!</p> <% @steps.each_with_index do |step, index| %> <div class="step"> <h3><%= index + 1 %>. <%= step[:title] %></h3> <p><%= step[:description] %></p> </div> <% end %>
これらの配慮により、Ballonは単なる悩み相談サービスではなく、中高生の成長と学びを支援するプラットフォームとして機能します。
次回予告
次回のPart 5では、以下の内容について解説する予定です:
- パフォーマンス最適化:大規模なユーザーベースに対応するための戦略
- アクセシビリティ対応:様々な利用者に配慮したデザインと実装
- 多言語対応:国際的な展開を視野に入れた機能拡張
- アナリティクスとレポーティング:サービス改善のためのデータ活用
また、Ballonの開発を通じて学んだ教訓や、中高生向けサービス開発における倫理的考慮事項についても議論する予定です。
引き続き、Ballonの開発過程を通じて、Sinatraを使用したWebアプリケーション開発の実践的なスキルと、若者向けサービス開発における重要な考慮事項を学んでいきましょう。
ご質問やフィードバックがありましたら、コメント欄にてお待ちしています!