캘린더 RSVP 게시물

:information_source: 요약 RSVP 이벤트에 대한 짧은 주제 답변을 생성합니다
:hammer_and_wrench: 저장소 링크 GitHub - mariodsantana/discourse-calendar-rsvp-posts · GitHub
:open_book: 설치 가이드 Discourse에 플러그인 설치하는 방법

기능

  • 실시간 알림 - RSVP 이벤트가 이제 주제에 답변 게시물을 생성하므로, 모든 RSVP는 주제 관찰자들에게 알림을 트리거합니다.
  • 최소한의 혼란 - 이전 알림 게시물이 삭제되므로, 언제든지 최대 2개의 게시물(1개의 히스토리 + 1개의 최신 알림)만 유지됩니다.
  • 완전한 히스토리 - 모든 RSVP 활동이 시간 순서대로 타임스탬프와 함께 보존됩니다.
  • 변덕스러운 RSVP 변경 방지 - 타임스탬프 덕분에 반복적인 RSVP 변경 사항이 명확하게 드러납니다.

구성

관련 사이트 설정(관리자 > 설정 > 플러그인)은 주로 어떤 RSVP 변경 사항이 게시물을 트리거해야 하는지(참석, 관심, 불참 또는 기존 RSVP 제거)를 구성하며, 과거에 시작하는 이벤트의 RSVP 변경 사항에 대해서도 트리거할지 여부를 결정합니다.

나머지 설정은 히스토리 모드를 전환합니다. 히스토리 모드가 비활성화된 경우, 주제에는 최신 알림 게시물만 유지됩니다. 히스토리 모드가 활성화된 경우, 플러그인은 다음과 같이 추가적인 “히스토리” 댓글을 관리합니다:

  • 첫 번째 RSVP 시, RSVP를 알리는 간단한 알림 게시물을 생성합니다.
  • 두 번째 RSVP 시, 첫 번째 게시물을 타임스탬프가 있는 히스토리 게시물로 변환한 후, 새로운 알림 게시물을 생성합니다.
  • 이후 RSVP 시, RSVP를 타임스탬프와 함께 히스토리 게시물에 추가하고, 이전 알림 게시물을 삭제한 후, 새로운 알림 게시물을 생성합니다.

설정

이름 설명
calendar_rsvp_posts_on_new_going 새로운 “참석” 시 게시물 생성
calendar_rsvp_posts_on_new_interested 새로운 “관심” 시 게시물 생성
calendar_rsvp_posts_on_new_not_going 새로운 “불참” 시 게시물 생성
calendar_rsvp_posts_on_removed_rsvp RSVP가 제거될 때 게시물 생성
calendar_rsvp_posts_allow_past_events 과거에 시작하는 이벤트에 대해 게시물을 생성할지 여부
calendar_rsvp_posts_enable_history 타임스탬프가 있는 히스토리 게시물 유지 (기본값: 활성화)
6개의 좋아요

이것은 많은 사람들에게 이벤트 관리를 향상시킬 것입니다!! 이렇게 제공해 주셔서 정말 감사합니다!!

1개의 좋아요

플러그인 감사합니다 :+1:

작은 질문 하나만 드릴게요. 답변을 파일로 번역할 수 있는 기능이 있나요?

이 아이디어 좋아요. 히스토리 게시물에 CSV 파일을 다운로드할 수 있는 링크를 추가할 수 있겠네요. 그런 걸 생각하고 계신 건가요?

잘 표현하지 못했습니다 :sweat_smile: . 답변 알림을 어떻게 번역하면 되는지 알고 싶었어요. 알림이 영어로 되어 있어서 한국어로 설정하고 싶거든요.

1개의 좋아요

ㅋㅋ - 이제 이해했어요. 현재 영어 텍스트가 소스 코드에 직접 들어가 있네요. 저는 디스커스에 초보이지만, 플러그인을 번역 가능하게 만드는 표준적인 방법이 분명 있을 거예요. 시간이 나면 한번 알아볼게요.

아마 CSV 링크도 구현할 것 같아요. :wink:

1개의 좋아요

걱정 마세요 :wink: . 천천히 하세요, 저는 급하지 않거든요

좋아요! 저장소에 새 버전이 올라왔습니다. i18n 지원뿐만 아니라, 누군가 RSVP를 취소했을 때 업데이트가 게시되지 않던 버그도 찾아서 수정했습니다.

문제가 있으면 알려주세요!

1개의 좋아요

안녕하세요. 이벤트에 참여하거나 참여하지 않는 사용자와 관련하여 문제가 있었습니다. 이벤트에 두 개의 아바타가 하나만 표시되어야 할 때 두 개가 표시되었고, 새로고침을 하면 해결되었지만 사용자에게 혼란을 주었습니다. 제 쪽에서 코드를 수정했는데, 이것이 도움이 될지 모르겠습니다.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Create short topic replies for RSVP events
# version: 0.4
# authors: Mario Santana

after_initialize do
 module ::CalendarRsvpPosts
   PLUGIN_NAME = "discourse-calendar-rsvp-posts"
   
   def self.history_marker
     I18n.t('calendar_rsvp_posts.markers.history')
   end
   
   def self.notification_marker
     I18n.t('calendar_rsvp_posts.markers.notification')
   end

   # Helper to decide if we should ignore this event
   def self.should_post_for_event?(event)
     return false if event.nil?
     return true if SiteSetting.calendar_rsvp_posts_allow_past_events
     return false if event.starts_at.nil?
     event.starts_at >= Time.current
   end

   # Find any RSVP post for an event (history or simple notification)
   def self.find_rsvp_posts(event)
     return [] if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ? OR raw LIKE ?", 
              "%#{history_marker}%",
              "%#{notification_marker}%")
       .order(created_at: :asc)
   end

   # Find the history post specifically
   def self.find_history_post(event)
     return nil if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{history_marker}%")
       .order(created_at: :asc)
       .first
   end

   # Find and delete all notification posts (but not history post)
   def self.delete_notification_posts(event)
     return if event.nil? || event.post.nil? || event.post.topic.nil?
     
     notification_posts = event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{notification_marker}%")
     
     notification_posts.each do |post|
       begin
         PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
       rescue StandardError => e
         Rails.logger.warn("calendar-rsvp-posts: failed to delete notification post #{post.id}: #{e}")
       end
     end
   end

   # Build a history entry line with timestamp
   def self.build_history_entry(username, action_label, extra_text = nil)
     timestamp = Time.current.strftime("%Y-%m-%d %H:%M UTC")
     entry = "- **#{timestamp}** - #{username} #{action_label}"
     entry += " (#{extra_text})" if extra_text.present?
     entry
   end

   # Build or update the history post content
   def self.build_history_raw(event, new_entry)
     event_title = (event.name.presence || event.post.topic.title).to_s
     parts = []
     parts << history_marker
     parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
     parts << ""
     parts << new_entry
     parts.join("\n")
   end

   # Append new entry to existing history
   def self.append_to_history(existing_raw, new_entry)
     lines = existing_raw.split("\n")
     header_end_idx = lines.index { |line| line.start_with?("### RSVP History") }
     
     if header_end_idx
       insert_idx = header_end_idx + 2
       lines.insert(insert_idx, new_entry)
     else
       lines << new_entry
     end
     
     lines.join("\n")
   end

   # Convert a simple notification post into history format
   def self.convert_to_history(simple_post, event)
     raw = simple_post.raw
     
     going_label = I18n.t('calendar_rsvp_posts.actions.going').gsub('(', '\\(').gsub(')', '\\)')
     interested_label = I18n.t('calendar_rsvp_posts.actions.interested').gsub('(', '\\(').gsub(')', '\\)')
     not_going_label = I18n.t('calendar_rsvp_posts.actions.not_going').gsub('(', '\\(').gsub(')', '\\)')
     removed_label = I18n.t('calendar_rsvp_posts.actions.removed').gsub('(', '\\(').gsub(')', '\\)')
     
     pattern = /\*\*([^\*]+)\s+(#{Regexp.escape(going_label)}|#{Regexp.escape(interested_label)}|#{Regexp.escape(not_going_label)}|#{Regexp.escape(removed_label)})\*\*/
     match = raw.match(pattern)
     
     if match
       username = match[1]
       action = match[2]
       
       extra_match = raw.match(/\.\s+([^.]+)\.$/)
       extra_text = extra_match ? extra_match[1] : nil
       
       timestamp = simple_post.created_at.strftime("%Y-%m-%d %H:%M UTC")
       first_entry = "- **#{timestamp}** - #{username} #{action}"
       first_entry += " (#{extra_text})" if extra_text.present?
       
       event_title = (event.name.presence || event.post.topic.title).to_s
       parts = []
       parts << history_marker
       parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
       parts << ""
       parts << first_entry
       parts.join("\n")
     else
       event_title = (event.name.presence || event.post.topic.title).to_s
       history_marker + "\n### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}\n\n" + raw
     end
   end

   # Build a notification post
   def self.build_notification_raw(username, action_label, event, extra_text = nil)
     event_title = (event.name.presence || event.post.topic.title).to_s
     extra_text_formatted = extra_text.present? ? "#{extra_text} " : ""
     
     notification_raw = notification_marker + "\n"
     notification_raw += I18n.t('calendar_rsvp_posts.notification.template', 
                               event_title: event_title,
                               extra: extra_text_formatted,
                               username: username,
                               action: action_label)
     notification_raw
   end

   # Centrale logic method to handle the creation/update of posts
   def self.publish_rsvp_update(event, username, action_label, extra_text = nil)
     if SiteSetting.calendar_rsvp_posts_enable_history
       history_post = find_history_post(event)
       all_rsvp_posts = find_rsvp_posts(event)
       new_entry = build_history_entry(username, action_label, extra_text)

       if all_rsvp_posts.empty?
         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       elsif history_post.nil?
         first_post = all_rsvp_posts.first
         history_raw = convert_to_history(first_post, event)
         history_raw = append_to_history(history_raw, new_entry)
         
         PostRevisor.new(first_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: history_raw,
           skip_validations: true,
           skip_revision: false
         )

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       else
         updated_raw = append_to_history(history_post.raw, new_entry)
         PostRevisor.new(history_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: updated_raw,
           skip_validations: true,
           skip_revision: false
         )

         delete_notification_posts(event)

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       end
     else
       all_rsvp_posts = find_rsvp_posts(event)
       
       all_rsvp_posts.each do |post|
         begin
           PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: failed to delete post #{post.id}: #{e}")
         end
       end

       notification_raw = build_notification_raw(username, action_label, event, extra_text)
       PostCreator.create!(
         Discourse.system_user,
         topic_id: event.post.topic_id,
         raw: notification_raw,
         skip_validations: true
       )
     end
   end
 end

 # ==========================================
 # Background Job Definition
 # ==========================================
 module ::Jobs
   class ProcessCalendarRsvpPost < ::Jobs::Base
     def execute(args)
       event_id = args[:event_id]
       username = args[:username]
       action_label = args[:action_label]
       extra_text = args[:extra_text]

       event = DiscoursePostEvent::Event.find_by(id: event_id)
       return unless event

       ::CalendarRsvpPosts.publish_rsvp_update(event, username, action_label, extra_text)
     end
   end
 end

 # ==========================================
 # Event Handlers
 # ==========================================

 # Handler for create/update attendance
 proc_handler = proc do |invitee|
   begin
     event = invitee&.event
     next if event.nil?
     next unless CalendarRsvpPosts.should_post_for_event?(event)

     going_val = DiscoursePostEvent::Invitee.statuses[:going]
     interested_val = DiscoursePostEvent::Invitee.statuses[:interested]
     not_going_val = DiscoursePostEvent::Invitee.statuses[:not_going]

     new_status = invitee.status
     prev_status =
       if invitee.respond_to?(:previous_changes) && invitee.previous_changes["status"]
         invitee.previous_changes["status"][0]
       else
         nil
       end

     next if prev_status && prev_status == new_status

     username = invitee.user&.username || "someone"
     action_label = nil

     if new_status == going_val && SiteSetting.calendar_rsvp_posts_on_new_going
       action_label = I18n.t('calendar_rsvp_posts.actions.going')
     elsif new_status == interested_val && SiteSetting.calendar_rsvp_posts_on_new_interested
       action_label = I18n.t('calendar_rsvp_posts.actions.interested')
     elsif new_status == not_going_val && SiteSetting.calendar_rsvp_posts_on_new_not_going
       action_label = I18n.t('calendar_rsvp_posts.actions.not_going')
     end

     next if action_label.nil?

     extra_text = nil
     if event.max_attendees.present?
       current_going = event.going_count
       prev_going =
         if prev_status == going_val && new_status != going_val
           current_going + 1
         elsif prev_status != going_val && new_status == going_val
           current_going - 1
         else
           current_going
         end

       was_full = prev_going >= event.max_attendees
       is_full = current_going >= event.max_attendees

       if was_full && !is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.spots_available')
       elsif !was_full && is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.now_full')
       end
     end

     # Enqueue the job instead of blocking the request
     Jobs.enqueue(:process_calendar_rsvp_post, 
       event_id: event.id, 
       username: username, 
       action_label: action_label, 
       extra_text: extra_text
     )

   rescue StandardError => e
     Rails.logger.warn("calendar-rsvp-posts: handler error: #{e}")
     Rails.logger.warn(e.backtrace.join("\n"))
   end
 end

 on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

 # Handler for explicit invitee deletions (removed RSVP)
 if defined?(DiscoursePostEvent::Invitee)
   DiscoursePostEvent::Invitee.class_eval do
     after_destroy do
       event = self.event
       
       should_process = event.present? &&
                        SiteSetting.calendar_rsvp_posts_on_removed_rsvp &&
                        (SiteSetting.calendar_rsvp_posts_allow_past_events || event.starts_at.nil? || event.starts_at >= Time.current)
       
       if should_process
         begin
           username = self.user&.username || "someone"
           action_label = I18n.t('calendar_rsvp_posts.actions.removed')

           # Enqueue the job instead of blocking the request
           Jobs.enqueue(:process_calendar_rsvp_post, 
             event_id: event.id, 
             username: username, 
             action_label: action_label,
             extra_text: nil
           )
           
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: after_destroy handler error: #{e}")
           Rails.logger.warn(e.backtrace.join("\n"))
         end
       end
     end
   end
 end
end
1개의 좋아요

훌륭한 아이디어입니다. UI 혼란을 초래할 수 있는 이유를 잘 이해할 수 있습니다. 레이스 컨디션(race condition)을 방지하기 위해 Mutex를 추가하고 플러그인의 새 버전을 배포했습니다.

생각하시는 점이 있으시면 알려주세요!

1개의 좋아요

2026.9.0-latest 버전 이후로 이벤트 등록 알림을 더 이상 받지 못하고 있습니다. 이 문제가 저 혼자만 겪고 있는지 모르겠습니다.

디스코urs 캘린더가 이벤트로 대체되었기 때문에 이 문제가 발생하는 이유를 파악할 수 있을 것 같습니다.

저희 쪽에서 plugin.rb 파일을 수정하여 새로운 버전의 discourse와 호환되도록 했습니다.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Create short topic replies for RSVP events
# version: 0.9
# authors: Mario Santana, gilles

after_initialize do
  module ::CalendarRsvpPosts
    PLUGIN_NAME = "discourse-calendar-rsvp-posts"

    def self.event_class
      "DiscourseEvents::Events::Event".safe_constantize ||
      "DiscourseEvents::Event".safe_constantize ||
      "DiscourseCalendar::Event".safe_constantize ||
      "DiscoursePostEvent::Event".safe_constantize
    end

    def self.invitee_class
      "DiscourseEvents::Events::Invitee".safe_constantize ||
      "DiscourseEvents::Invitee".safe_constantize ||
      "DiscourseCalendar::Invitee".safe_constantize ||
      "DiscoursePostEvent::Invitee".safe_constantize
    end
    
    def self.history_marker
      I18n.t('calendar_rsvp_posts.markers.history')
    end
    
    def self.notification_marker
      I18n.t('calendar_rsvp_posts.markers.notification')
    end

    # 이 이벤트를 무시해야 하는지 결정하는 헬퍼
    def self.should_post_for_event?(event)
      return false if event.nil?
      return true if SiteSetting.calendar_rsvp_posts_allow_past_events
      return false if event.starts_at.nil?
      event.starts_at >= Time.current
    end

    # 이벤트의 RSVP 게시글(역사 또는 단순 알림)을 찾습니다
    def self.find_rsvp_posts(event)
      return [] if event.nil? || event.post.nil? || event.post.topic.nil?
      
      event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ? OR raw LIKE ?", 
               "%#{history_marker}%",
               "%#{notification_marker}%")
        .order(created_at: :asc)
    end

    # 특히 역사 게시글을 찾습니다
    def self.find_history_post(event)
      return nil if event.nil? || event.post.nil? || event.post.topic.nil?
      
      event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ?", "%#{history_marker}%")
        .order(created_at: :asc)
        .first
    end

    # 모든 알림 게시글을 찾아 삭제합니다(역사 게시글은 제외)
    def self.delete_notification_posts(event)
      return if event.nil? || event.post.nil? || event.post.topic.nil?
      
      notification_posts = event.post.topic.posts
        .where(user_id: Discourse.system_user.id)
        .where("raw LIKE ?", "%#{notification_marker}%")
      
      notification_posts.each do |post|
        begin
          PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
        rescue StandardError => e
          Rails.logger.warn("calendar-rsvp-posts: failed to delete notification post #{post.id}: #{e}")
        end
      end
    end

    # 타임스탬프가 포함된 역사 항목 줄을 구성합니다
    def self.build_history_entry(username, action_label, extra_text = nil)
      timestamp = Time.current.strftime("%Y-%m-%d %H:%M UTC")
      entry = "- **#{timestamp}** - #{username} #{action_label}"
      entry += " (#{extra_text})" if extra_text.present?
      entry
    end

    # 역사 게시글 내용을 구성하거나 업데이트합니다
    def self.build_history_raw(event, new_entry)
      event_title = (event.name.presence || event.post.topic.title).to_s
      parts = []
      parts << history_marker
      parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
      parts << ""
      parts << new_entry
      parts.join("\n")
    end

    # 기존 역사에 새 항목을 추가합니다
    def self.append_to_history(existing_raw, new_entry)
      lines = existing_raw.split("\n")
      header_end_idx = lines.index { |line| line.start_with?("### RSVP History") }
      
      if header_end_idx
        insert_idx = header_end_idx + 2
        lines.insert(insert_idx, new_entry)
      else
        lines << new_entry
      end
      
      lines.join("\n")
    end

    # 단순 알림 게시글을 역사 형식으로 변환합니다
    def self.convert_to_history(simple_post, event)
      raw = simple_post.raw
      
      going_label = I18n.t('calendar_rsvp_posts.actions.going').gsub('(', '\\(').gsub(')', '\\)')
      interested_label = I18n.t('calendar_rsvp_posts.actions.interested').gsub('(', '\\(').gsub(')', '\\)')
      not_going_label = I18n.t('calendar_rsvp_posts.actions.not_going').gsub('(', '\\(').gsub(')', '\\)')
      removed_label = I18n.t('calendar_rsvp_posts.actions.removed').gsub('(', '\\(').gsub(')', '\\)')
      
      pattern = /\*\*([^\*]+)\s+(#{Regexp.escape(going_label)}|#{Regexp.escape(interested_label)}|#{Regexp.escape(not_going_label)}|#{Regexp.escape(removed_label)})\*\*/
      match = raw.match(pattern)
      
      if match
        username = match[1]
        action = match[2]
        
        extra_match = raw.match(/\.\s+([^.]+)\.$/)
        extra_text = extra_match ? extra_match[1] : nil
        
        timestamp = simple_post.created_at.strftime("%Y-%m-%d %H:%M UTC")
        first_entry = "- **#{timestamp}** - #{username} #{action}"
        first_entry += " (#{extra_text})" if extra_text.present?
        
        event_title = (event.name.presence || event.post.topic.title).to_s
        parts = []
        parts << history_marker
        parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
        parts << ""
        parts << first_entry
        parts.join("\n")
      else
        event_title = (event.name.presence || event.post.topic.title).to_s
        history_marker + "\n### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}\n\n" + raw
      end
    end

    # 알림 게시글을 구성합니다
    def self.build_notification_raw(username, action_label, event, extra_text = nil)
      event_title = (event.name.presence || event.post.topic.title).to_s
      extra_text_formatted = extra_text.present? ? "#{extra_text} " : ""
      
      notification_raw = notification_marker + "\n"
      notification_raw += I18n.t('calendar_rsvp_posts.notification.template', 
                                 event_title: event_title,
                                 extra: extra_text_formatted,
                                 username: username,
                                 action: action_label)
      notification_raw
    end

    # 게시글 생성/업데이트를 처리하는 중앙 로직 메서드
    def self.publish_rsvp_update(event, username, action_label, extra_text = nil)
      if SiteSetting.calendar_rsvp_posts_enable_history
        history_post = find_history_post(event)
        all_rsvp_posts = find_rsvp_posts(event)
        new_entry = build_history_entry(username, action_label, extra_text)

        if all_rsvp_posts.empty?
          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        elsif history_post.nil?
          first_post = all_rsvp_posts.first
          history_raw = convert_to_history(first_post, event)
          history_raw = append_to_history(history_raw, new_entry)
          
          PostRevisor.new(first_post, event.post.topic).revise!(
            Discourse.system_user,
            raw: history_raw,
            skip_validations: true,
            skip_revision: false
          )

          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        else
          updated_raw = append_to_history(history_post.raw, new_entry)
          PostRevisor.new(history_post, event.post.topic).revise!(
            Discourse.system_user,
            raw: updated_raw,
            skip_validations: true,
            skip_revision: false
          )

          delete_notification_posts(event)

          notification_raw = build_notification_raw(username, action_label, event, extra_text)
          PostCreator.create!(
            Discourse.system_user,
            topic_id: event.post.topic_id,
            raw: notification_raw,
            skip_validations: true
          )
        end
      else
        all_rsvp_posts = find_rsvp_posts(event)
        
        all_rsvp_posts.each do |post|
          begin
            PostDestroyer.new(Discourse.system_user, post, context: "calendar-rsvp-posts cleanup").destroy
          rescue StandardError => e
            Rails.logger.warn("calendar-rsvp-posts: failed to delete post #{post.id}: #{e}")
          end
        end

        notification_raw = build_notification_raw(username, action_label, event, extra_text)
        PostCreator.create!(
          Discourse.system_user,
          topic_id: event.post.topic_id,
          raw: notification_raw,
          skip_validations: true
        )
      end
    end
  end

  # ==========================================
  # 백그라운드 작업 정의
  # ==========================================
  module ::Jobs
    class ProcessCalendarRsvpPost < ::Jobs::Base
      def execute(args)
        event_id = args[:event_id]
        username = args[:username]
        action_label = args[:action_label]
        extra_text = args[:extra_text]

        event_cls = ::CalendarRsvpPosts.event_class
        return unless event_cls

        event = event_cls.find_by(id: event_id)
        return unless event

        ::CalendarRsvpPosts.publish_rsvp_update(event, username, action_label, extra_text)
      end
    end
  end

  # ==========================================
  # 이벤트 핸들러
  # ==========================================

  # 참석 생성/업데이트 핸들러
  proc_handler = proc do |invitee|
    begin
      event = invitee&.event
      next if event.nil?
      next unless CalendarRsvpPosts.should_post_for_event?(event)

      invitee_cls = CalendarRsvpPosts.invitee_class
      next unless invitee_cls

      going_val = invitee_cls.statuses[:going]
      interested_val = invitee_cls.statuses[:interested]
      not_going_val = invitee_cls.statuses[:not_going]

      new_status = invitee.status
      prev_status =
        if invitee.respond_to?(:previous_changes) && invitee.previous_changes["status"]
          invitee.previous_changes["status"][0]
        else
          nil
        end

      next if prev_status && prev_status == new_status

      username = invitee.user&.username || "someone"
      action_label = nil

      if new_status == going_val && SiteSetting.calendar_rsvp_posts_on_new_going
        action_label = I18n.t('calendar_rsvp_posts.actions.going')
      elsif new_status == interested_val && SiteSetting.calendar_rsvp_posts_on_new_interested
        action_label = I18n.t('calendar_rsvp_posts.actions.interested')
      elsif new_status == not_going_val && SiteSetting.calendar_rsvp_posts_on_new_not_going
        action_label = I18n.t('calendar_rsvp_posts.actions.not_going')
      end

      next if action_label.nil?

      extra_text = nil
      if event.max_attendees.present?
        current_going = event.going_count
        prev_going =
          if prev_status == going_val && new_status != going_val
            current_going + 1
          elsif prev_status != going_val && new_status == going_val
            current_going - 1
          else
            current_going
          end

        was_full = prev_going >= event.max_attendees
        is_full = current_going >= event.max_attendees

        if was_full && !is_full
          extra_text = I18n.t('calendar_rsvp_posts.capacity.spots_available')
        elsif !was_full && is_full
          extra_text = I18n.t('calendar_rsvp_posts.capacity.now_full')
        end
      end

      # 요청을 차단하지 않고 작업을 큐에 추가합니다
      Jobs.enqueue(:process_calendar_rsvp_post, 
        event_id: event.id, 
        username: username, 
        action_label: action_label, 
        extra_text: extra_text
      )

    rescue StandardError => e
      Rails.logger.warn("calendar-rsvp-posts: handler error: #{e}")
      Rails.logger.warn(e.backtrace.join("\n"))
    end
  end

  on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

  # 명시적인 초대 삭제(제거된 RSVP)를 위한 핸들러
  invitee_cls = CalendarRsvpPosts.invitee_class
  if invitee_cls
    invitee_cls.class_eval do
      after_destroy do
        event = self.event
        
        should_process = event.present? &&
                         SiteSetting.calendar_rsvp_posts_on_removed_rsvp &&
                         (SiteSetting.calendar_rsvp_posts_allow_past_events || event.starts_at.nil? || event.starts_at >= Time.current)
        
        if should_process
          begin
            username = self.user&.username || "someone"
            action_label = I18n.t('calendar_rsvp_posts.actions.removed')

            # 요청을 차단하지 않고 작업을 큐에 추가합니다
            Jobs.enqueue(:process_calendar_rsvp_post, 
              event_id: event.id, 
              username: username, 
              action_label: action_label,
              extra_text: nil
            )
            
          rescue StandardError => e
            Rails.logger.warn("calendar-rsvp-posts: after_destroy handler error: #{e}")
            Rails.logger.warn(e.backtrace.join("\n"))
          end
        end
      end
    end
  end
end