태스크 모델 설정
협업 앱의 실시간 잠금 처리를 위해 Task 모델에 locked_by (사용자 참조)와 locked_at (날짜/시간) 컬럼을 추가합니다.
ruby
class AddLockingToTasks < ActiveRecord::Migration[8.0]
def change
add_reference :tasks, :locked_by, foreign_key: { to_table: :users }
add_column :tasks, :locked_at, :datetime
end
end
Task 모델은 잠금 로직을 처리하고 변경 사항을 연결된 사용자에게 브로드캐스트합니다.
ruby
# app/models/task.rb
class Task < ApplicationRecord
after_create_commit { broadcast_prepend_to "tasks", target: "tasks" }
after_update_commit { broadcast_replace_to 'tasks' } # 잠금/해제 시 업데이트 브로드캐스트
after_destroy_commit { broadcast_remove_to 'tasks' }
def locked_by?(user)
locked_by == user
end
def locked?
locked_by.present?
end
def lock!(user)
update!(locked_by: user, locked_at: Time.current)
end
def unlock!
update!(locked_by: nil, locked_at: nil)
end
end
after_update_commit 콜백은 태스크가 잠기거나 해제될 때마다 Turbo Streams를 통해 모든 사용자에게 업데이트된 버전을 전송하는 핵심 역할을 합니다.
편집 시작 시 태스크 잠금
사용자가 편집 버튼을 클릭하면 태스크는 즉시 잠기고 변경 사항이 브로드캐스트됩니다.
ruby
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
def edit
if @task.locked? && !@task.locked_by?(current_user)
redirect_to root_path, alert: "This task is currently being edited by #{@task.locked_by.name}."
return
end
@task.lock!(current_user)
end
def update
if @task.update(task_params)
@task.unlock!
redirect_to root_path, notice: "Task was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def unlock
@task.unlock! if @task.locked_by?(current_user)
head :ok
end
end
edit 액션에서는 이미 잠겨있는 경우 현재 사용자가 잠근 것이 아니라면 리디렉션하고, 그렇지 않으면 태스크를 잠급니다. update 액션 성공 시 태스크를 해제합니다. unlock 액션은 명시적으로 잠금을 해제하는 엔드포인트입니다.
모든 사용자에게 잠금 상태 표시
태스크 부분 템플릿은 잠금 상태에 따라 다른 UI를 표시합니다. ```erb
```
index 페이지는 turbo_stream_from "tasks"를 통해 Turbo Stream 채널을 구독하여, 태스크의 변경(잠금, 해제, 생성, 업데이트, 삭제)이 발생할 때마다 모든 연결된 클라이언트가 즉시 업데이트를 받습니다.
사용자가 페이지를 벗어날 때 잠금 해제
사용자가 저장 또는 취소를 클릭하지 않고 탭을 닫거나 뒤로 가기 버튼을 누르는 등 페이지를 이탈할 때 잠금이 자동으로 해제되어야 합니다. StimulusJS 컨트롤러는 turbo:before-visit (앱 내비게이션) 및 beforeunload (탭 닫기, 페이지 새로고침) 이벤트를 수신하여 잠금 해제 요청을 보냅니다.
javascript
// app/javascript/controllers/task_lock_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { taskId: Number }
connect() {
this.unlock = this.unlock.bind(this)
document.addEventListener("turbo:before-visit", this.unlock)
window.addEventListener("beforeunload", this.unlock)
}
unlock() {
const formData = new FormData()
formData.append("authenticity_token", document.querySelector("[name='csrf-token']").content)
navigator.sendBeacon(`/tasks/${this.taskIdValue}/unlock`, formData)
}
}
일반적인 AJAX 요청은 페이지 언로드 시 취소될 수 있으므로, navigator.sendBeacon()을 사용하여 페이지가 닫히는 중에도 데이터를 안정적으로 전송합니다.