알림 백엔드: 컨트롤러 + Job + Mailer + 라우팅
ID: 9ecebc07-5bdb-4c0d-8bf4-8f1cf25f2420
## 목표
웹 푸시 구독 관리, 알림 발송 Job, 이메일 Mailer를 구현합니다.
## 선행 조건
- notification-foundation 에이전트가 PushSubscription 모델과 마이그레이션을 완료한 후 시작
## 작업 내용
### 1. PushSubscriptionsController 생성
파일: `app/controllers/push_subscriptions_controller.rb`
```ruby
class PushSubscriptionsController < ApplicationController
before_action :authenticate_user!
# POST /push/subscribe - 구독 등록
def create
subscription = current_user.push_subscriptions.find_or_initialize_by(
endpoint: params[:endpoint]
)
subscription.assign_attributes(
p256dh: params[:p256dh],
auth: params[:auth],
browser_info: params[:browser_info],
device_info: params[:device_info],
is_active: true
)
if subscription.save
render json: { success: true, id: subscription.id }
else
render json: { success: false, errors: subscription.errors.full_messages }, status: :unprocessable_entity
end
end
# GET /push/vapid_public_key - VAPID 공개키 반환
def vapid_public_key
render json: { vapid_public_key: Rails.application.credentials.dig(:vapid, :public_key) || ENV["VAPID_PUBLIC_KEY"] }
end
# DELETE /push/unsubscribe - 구독 해제
def destroy
subscription = current_user.push_subscriptions.find_by(endpoint: params[:endpoint])
if subscription
subscription.update(is_active: false)
render json: { success: true }
else
render json: { success: false }, status: :not_found
end
end
end
```
### 2. 라우팅 추가
파일: `config/routes.rb`에 추가:
```ruby
# 푸시 알림
scope :push do
post "subscribe", to: "push_subscriptions#create"
get "vapid_public_key", to: "push_subscriptions#vapid_public_key"
delete "unsubscribe", to: "push_subscriptions#destroy"
end
```
### 3. NotificationService 생성
파일: `app/services/notification_service.rb`
- 웹 푸시 발송 로직 (web-push gem 사용)
- 이메일 발송 로직 (Action Mailer)
- 발송 방식 선택 로직 (push → email → kakao 순서)
```ruby
class NotificationService
def initialize(user, title:, message:, url: nil)
@user = user
@title = title
@message = message
@url = url || "/"
end
def send_all
results = { push: nil, email: nil }
setting = @user.user_setting
methods = setting&.notification_methods_array || ["push"]
results[:push] = send_web_push if methods.include?("push")
results[:email] = send_email if methods.include?("email")
results
end
private
def send_web_push
subscriptions = @user.push_subscriptions.active
return { success: false, reason: "no_subscriptions" } if subscriptions.empty?
vapid = {
subject: "mailto:#{ENV.fetch('VAPID_EMAIL', 'admin@logbible.co.kr')}",
public_key: ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key)),
private_key: ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key))
}
payload = {
title: @title,
body: @message,
icon: "/icon-192x192.png",
badge: "/badge-72x72.png",
url: @url,
actions: [{ action: "open", title: "열기" }]
}.to_json
sent = 0
failed = 0
subscriptions.find_each do |sub|
begin
WebPush.payload_send(
message: payload,
endpoint: sub.endpoint,
p256dh: sub.p256dh,
auth: sub.auth,
vapid: vapid
)
sub.update(last_notification_sent: Time.current)
sent += 1
rescue WebPush::ExpiredSubscription
sub.update(is_active: false)
failed += 1
rescue => e
Rails.logger.error("Push failed for #{sub.id}: #{e.message}")
failed += 1
end
end
{ success: sent > 0, sent: sent, failed: failed }
end
def send_email
NotificationMailer.qt_reminder(@user, @title, @message, @url).deliver_later
{ success: true }
rescue => e
Rails.logger.error("Email failed for #{@user.id}: #{e.message}")
{ success: false, reason: e.message }
end
end
```
### 4. NotificationCronJob 생성
파일: `app/jobs/notification_cron_job.rb`
```ruby
class NotificationCronJob < ApplicationJob
queue_as :default
def perform
current_hour = Time.current.in_time_zone("Asia/Seoul").hour
# notification_enabled이고, notification_time이 현재 시간인 사용자 조회
UserSetting.where(notification_enabled: true)
.where("CAST(strftime('%H', notification_time) AS INTEGER) = ?", current_hour)
.includes(user: [:push_subscriptions], current_session: { qt_theme: :qt_contents })
.find_each do |setting|
next unless setting.current_session # 활성 세션 없으면 skip
session = setting.current_session
theme = session.qt_theme
# 오늘 날짜 기준 day_number 계산
day_number = (Date.current - session.start_date.to_date).to_i + 1
content = theme.qt_contents.find_by(day_number: day_number)
next unless content # 오늘 콘텐츠 없으면 skip
title = "📖 QT 시간입니다!"
message = "#{theme.title} - #{content.bible_chapter}"
url = "/qt/today"
NotificationService.new(
setting.user,
title: title,
message: message,
url: url
).send_all
end
end
end
```
### 5. Solid Queue 반복 스케줄 설정
파일: `config/recurring.yml` 생성:
```yaml
production:
notification_cron:
class: NotificationCronJob
schedule: every hour
description: "매 시간 알림 발송 체크"
```
### 6. NotificationMailer 생성
파일: `app/mailers/notification_mailer.rb`
```ruby
class NotificationMailer < ApplicationMailer
def qt_reminder(user, title, message, url)
@user = user
@title = title
@message = message
@url = url
mail(to: @user.email, subject: title)
end
end
```
뷰 파일: `app/views/notification_mailer/qt_reminder.html.erb`
- 간단한 HTML 이메일 템플릿 (Tailwind 미사용, 인라인 CSS)
- LogBible 로고, 제목, 메시지, CTA 버튼
### 7. SettingsController 업데이트
- `settings_params`에 `notification_methods` 추가
## 완료 기준
- POST /push/subscribe 동작
- GET /push/vapid_public_key 동작
- DELETE /push/unsubscribe 동작
- NotificationCronJob이 올바른 사용자에게 알림 발송
- NotificationMailer 이메일 발송
- config/recurring.yml 존재
- bin/rails test 통과
첨부 이미지
이미지 추가 (Ctrl+V로 붙여넣기 또는 클릭)
JPEG, PNG, GIF, WebP / 최대 10MB
활동 로그
-
팀팀리드 상태 변경: 리뷰 → 완료
2026년 03월 02일 05:13:04
-
Nnotification-backend 상태 변경: 할 일 → 리뷰
2026년 03월 02일 05:07:11
-
Nnotification-backend 알림 백엔드 구현 완료
2026년 03월 02일 05:07:11
상세 보기
## 생성된 파일
- `app/controllers/push_subscriptions_controller.rb` - 구독 등록/해제/VAPID키 반환
- `app/services/notification_service.rb` - 웹 푸시 + 이메일 발송 통합 서비스
- `app/jobs/notification_cron_job.rb` - 매 시간 알림 발송 체크 Job
- `app/mailers/notification_mailer.rb` - QT 알림 이메일 Mailer
- `app/views/notification_mailer/qt_reminder.html.erb` - 이메일 템플릿## 수정된 파일
- `Gemfile` - web-push gem (~> 3.0) 추가
- `config/routes.rb` - /push/subscribe, /push/vapid_public_key, /push/unsubscribe 라우트 추가
- `config/recurring.yml` - NotificationCronJob 매 시간 스케줄 추가
- `app/controllers/settings_controller.rb` - settings_params에 :notification_methods 추가
- `test/fixtures/user_settings.yml` - notification_enabled, notification_time, notification_methods, current_session 추가## 테스트 파일
- `test/controllers/push_subscriptions_controller_test.rb` (7 tests)
- `test/services/notification_service_test.rb` (7 tests)
- `test/jobs/notification_cron_job_test.rb` (3 tests)
- `test/mailers/notification_mailer_test.rb` (1 test)## 테스트 결과
- 18/18 테스트 통과 (0 failures, 0 errors)
- 전체 391 runs, 898 assertions, 0 failures
- 기존 1개 에러(Admin::QtThemesControllerTest)는 제 변경과 무관## 참고사항
- web-push gem 설치 시 openssl native extension 빌드 필요 → Docker에 build-essential 설치 필요
- QtContent 모델은 `bible_passage` 필드 사용 (지시서의 `bible_chapter`가 아님)
- Dockerfile에 build-essential 추가 필요 (현재 런타임 컨테이너에 수동 설치함) -
Nnotification-backend 알림 백엔드 구현 시작
2026년 03월 02일 04:56:11
상세 보기
## 구현 범위
- PushSubscriptionsController (구독 등록/해제/VAPID키)
- 라우팅 설정 (/push/*)
- NotificationService (웹 푸시 + 이메일)
- NotificationCronJob (매 시간 알림 체크)
- NotificationMailer + 이메일 템플릿
- SettingsController 파라미터 업데이트
- Solid Queue recurring 스케줄