Skip to content

Conversation

@haeyoon1
Copy link
Member

@haeyoon1 haeyoon1 commented Sep 19, 2025

작업 내용

👴🏻 기존 실행 방식

image SlashCommand 실행 후 옵션으로 만든 입력 필드에 내용을 입력하는 구조였습니다.

해당 형태는 다음과 같은 단점이 있습니다.

  • 긴 메세지를 입력하기에 옵션 칸은 불편합니다
  • message에 엔터 입력 불가 -> 이건 slash command 옵션으로 입력할 경우는 해결이 불가능한 문제라고하네요 (모달로 바꾸면 해결 가능)
  • # 등 마크다운 사용 불가
  • 시간을 직접 0000-00-00 00:00 형태로 수동으로 입력하기 번거롭다. -> 자동완성으로 현재 날짜를 기본으로 담아주려고 하였으나 이 역시 slash command 옵션으로 입력할 경우에는 불가능하다고 합니다 (모달로 바꾸면 해결 가능)

따라서 일부 옵션 입력은 모달에서 입력하는 것으로 대체해 위와 같은 문제를 해결하고자 했습니다.


👶🏻 변경 후 실행 방식

  1. Slash Command 정의 후 옵션으로 언급할 멤버 혹은 그룹을 입력합니다.
  • 모달창에서는 멤버 언급이 불가능해(디스코드 측 정책으로 인함) 언급을 위한 파트는 모달과 분리했습니다.
스크린샷 2025-09-19 오후 5 01 50
  1. 메세지를 보낼 채널은 드롭다운으로 선택합니다.
  • 해당 옵션을 드롭다운으로 바꾼데에는 특별한 이유는 없습니다... 가시성을 조금 더 좋게하려고 다음과 같이 변경했는데, 오히려 입력 하는 방식이 slash command option / dropdown / modal 이렇게 세가지가 되니 오히려 정신없어 보일 수 도 있을 것 같아 의견이 궁금합니다!
  • 다만 드롭다운이나 자동완성 같은 컴포넌트는 모달 내부에 넣을 수 없다고 합니다.
스크린샷 2025-09-19 오후 5 06 54
  1. 모달 형태로 메세지 내용과, 시간을 입력합니다.
  • 내용입력 시 마크다운 및 엔터 사용 가능합니다
  • 시간은 현재 시간이 기본으로 입력됩니다. (포멧 잘못 입력하는 방지하기 위함 + 0000-00-00 00:00를 모두 입력하는데 번거로움 방지하기 위함)
스크린샷 2025-09-19 오후 5 04 30

참고 사항

기존 ScheduledCommandListener 하나로 작동하던 CommandListener를 세개로 분리했습니다.

  • ScheduledMessageCommandListener
  • ScheduledMessageModalLauncher
  • ScheduledMessageSubmitListener

🌊 플로우

  1. /scheduled-message 입력 → ScheduledMessageCommandListener 가 실행되어 멤버 옵션 파싱 및 채널 선택 드롭다운 표시
  2. 드롭다운에서 채널을 고르면 → ScheduledMessageModalLauncher 가 실행되어 메시지/시간 입력 모달을 띄움
  3. 모달에 메시지와 시간을 입력해 제출 → ScheduledMessageSubmitListener 가 실행되어 입력값 검증 후 예약 메시지를 DB에 저장
  4. 예약 시간이 되면 → 스케줄링 로직이 동작하여 Discord 채널에 메시지가 전송됨

⚙️ 기능

각 클래스의 기능은 다음과 같습니다

  1. ScheduledMessageCommandListener
    • 명령어 입력 권한 제한, 채널 제한
    • 멤버 입력 (선택)
    • 채널 선택 드롭다운 생성 + 자동완성 (필수)
  2. ScheduledMessageModalLauncher
    • 모달 생성
      • message 입력 (필수)
      • time 입력 (기본값 = 현재 시간, 필수)
    • event.replyModal(modal).queue(); 에서 모달 입력 처리
  3. ScheduledMessageSubmitListener
    • 메시지 / 시간 / 채널 / 멘션 파싱
    • ScheduledMessageService 호출 후 메세지 예약메세지 db 채널에 저장
  4. ScheduledMessageScheduler
  • 출력 시 백틱 처리되어있던 멤버 id 백틱 해제 -> 멘션

🤔 질문사항

  1. 리스너를 세개로 분리했는데 이에 대해 적절한지(잘 분리되었는지) 의견이 궁금합니다! 또한 슬래시커멘드 + 드롭다운 + 모달 세가지 형태로 입력받는 것이 오히려 사용에 불편함을 주지 않을지또한 궁금합니다.

  2. JdaConfiguration 클래스에 관한 질문

  • 기존에는 SlashCommandListener 한 개만 등록 되어있었습니다 -> 슬래시 커맨드 관련 이벤트만 처리 가능
  • ListenerAdapter를 상속받는 클래스들(ScheduledMessageSubmitListener, ScheduledMessageModalLauncher 등)도 JDA에 함께 등록하도록 수정했습니다. (ListenerAdapter를 등록하면 모달, 드롭다운, 버튼 등 다양한 이벤트를 직접 처리 가능하다고합니다!)
    다만 기존의 SlashCommandListenerMapper 자체도 ListenerAdapter를 상속받고 있는데, 지금처럼 상위 클래스인 ListenerAdapter를 전부 등록하는 방식이 맞는지, 아니면 기존 구조를 활용해야하는지 의문이 드는데 어떻게 생각하시나요?

관련 이슈

PR 체크리스트

  • 테스트는 모두 통과했나요?
  • 빌드는 성공했나요?
  • 코드 포맷팅을 진행했나요?
  • PR 내부의 예시는 삭제하셨나요?

@haeyoon1 haeyoon1 linked an issue Sep 19, 2025 that may be closed by this pull request
3 tasks
@TaeyeonRoyce TaeyeonRoyce added the refactor Code refactoring, dependency updates, and other housekeeping activities label Sep 22, 2025
Copy link
Member

@TaeyeonRoyce TaeyeonRoyce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 많으셨습니다!
고민 끝에 더 나은 방식과 더 나은 사용자 경험을 완성 하셨네요. 덕분에 해당 기능을 자주 사용하게 될 것 같아요!
리뷰 남겼으니 확인 해주세요~


질문에 대한 답변

  • 슬래시커멘드 + 드롭다운 + 모달 세가지 형태로 입력받는 것이 오히려 사용에 불편함을 주지 않을지
    사용 하는데 불편함은 없어보여요! 오히려 단계별로 잘 구분 되어서 사용 하는 입장에서 더 쉽게 쓸 수 있을 것 같습니다.
  • 리스너를 세개로 분리했는데 이에 대해 적절한지(잘 분리되었는지) 의견이 궁금합니다!
    필요에 맞게 잘 분리해주셨어요. 기존 디코봇은 slashCommand에 대한 상호작용만 처리하는 listener였는데요. 각 입력 소스에 맞추어 listener를 분리하여 구현 하는게 적절한 책임이라 생각했어요. 추가된 modal 입력드롭다운 선택을 통한 입력을 담당하는 각각의 객체로 분리하는건 좋은 것 같습니다.👍
    다만, 현재 구현된 ScheduledMessageModalLauncherScheduledMessageSubmitListener는 "해당 형태의 모든 입력을 처리" 하고 있습니다.
    만약, 디코봇이 여러 드롭다운이나 모달을 지원한다면 SlashCommandListenerMapper와 같은 미들웨어가 필요하게 될 거에요...!

SlashCommandListenerMapper의 경우, 입력 받은 slash 명령어에 따라 분기처리 하여 적절한 CommandListener로 요청을 보냅니다. ex) reviewer-match -> ReviewMatchListener.onAction() , fortune-today -> FortuneTodayListener.onAction()
이처럼 모달이나 드롭다운 입력이 다양해지면 입력값에 따른 분기를 처리하는 미들웨어가 필요해집니다!

  • ListenerAdapter를 전부 등록하는 방식이 맞는지, 아니면 기존 구조를 활용해야하는지 의문이 드는데 어떻게 생각하시나요?
    코드에 남겨두긴 했는데, 결론적으론 전부 등록하는 것 보단 기존 방식으로 하는게 좋아보여요!


List<Object> listeners = new ArrayList<>();
listeners.add(slashCommandListenerMapper);
listeners.addAll(applicationContext.getBeansOfType(ListenerAdapter.class).values()); // 하드코딩
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplicationContext에서 빈을 조회하지 말고, jda listener로 등록할 빈을 직접 지정하여 주입 받는게 나아보여요.
ListnerAdapter를 구현한 모든 빈이 등록 되는 방식은 적용 범위가 넓어서 추후 제어하기에 불편 해보이네요.

추가된 ScheduledMessageModalLauncher, ScheduledMessageSubmitListener 모두 빈으로 등록 되어 있으니 상단의 listeners.add(slashCommandListenerMapper)처럼 직접 주입 받아서 사용 하시죠!

Copy link
Member Author

@haeyoon1 haeyoon1 Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JdaConfiguration 클래스 안에서
ListenerAdapter를 상속받은 모든 클래스를 한꺼번에 등록하는 방식(=기존 방식)을 사용하지 말고

return JDABuilder.createLight(token)
                ...
                .addEventListeners( 
                    slashCommandListenerMapper, // 기존
                    scheduledMessageModalLauncher, // 추가
                    scheduledMessageSubmitListener // 추가
                )
                ....

이렇게 직접 클래스를 넣으라는 말로 이해했는데 맞을까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞아요~

import org.springframework.stereotype.Component;

@Component
public class ScheduledMessageCommandListener implements AutoCompleteInteractionListener {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 채널 지정은 드롭다운으로 변경 되었으니 자동완성을 지원하는AutoCompleteInteractionListener가 아닌 SlashCommandListener로 변경 하시죠! 적합한 책임을 가지는 객체로 정의하는게 좋을 것 같아요.

하단의 AutoCompleteInteractionListener의 메서드인 onCommandAutoCompleteInteraction()도 필요 없어지겠네요!

try {
String[] parts = event.getModalId().split(":", 3);

final ScheduledMessageChannel channelEnum = ScheduledMessageChannel.valueOf(parts[1]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split으로 생성된 array의 각 위치별 값이 설명이 되어 있으면 좋아요!

Suggested change
final ScheduledMessageChannel channelEnum = ScheduledMessageChannel.valueOf(parts[1]);
String channelName = parts[1];
final ScheduledMessageChannel channelEnum = ScheduledMessageChannel.valueOf(channelName);

Copy link
Member

@TaeyeonRoyce TaeyeonRoyce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생 하셨습니다~👍👍
유용하게 사용 될 것 같아요. 하나만 수정하고 바로 머지하시죠!

.addEventListeners(slashCommandListenerMapper)
.addEventListeners( // 수동 입력
slashCommandListenerMapper,
scheduledMessageSubmitListener
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 등록 하나가 누락 되었는데, 이것만 추가 하고 머지하시죠!

Suggested change
scheduledMessageSubmitListener
scheduledMessageSubmitListener,
scheduledMessageModalLauncher

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 관련해서 질문이 있습니다🤔

ScheduledMessageModalLauncher를 Listener에 등록할 경우 모달 제출 시 다음과 같은 에러가 발생했습니다:

[ErrorResponseException] 10062: Failed to acknowledge this interaction
net.dv8tion.jda.api.exceptions.ErrorResponseException: 10062: Unknown interaction

이 에러는 하나의 Interaction을 두 개의 Listener(ScheduledMessageModalLauncher, ScheduledMessageSubmitListener)가 동시에 수신하면서 중복 응답이 발생했기 때문입니다.

그래서 실제로 이벤트를 처리해야 하는 SlashCommandListenerMapper(=명령어 등록)와 ScheduledMessageSubmitListener(=모달 제출) 두 개 Bean만 등록하도록 수정했습니다.
(이렇게 수정했을 시 정상 작동은 하면서 위의 오류가 발생하지 않습니다!)

Copy link
Member Author

@haeyoon1 haeyoon1 Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

많은 리스너들 중 왜 하필 저 두 리스너만 충돌이 일어날까?에 대해 찾아보았으나 충분한 이해가 되지 않습니다....🤯
일단 플로우를 찾아보았을 때 아래와 같다고 하네요. 🚨 표시된 곳이 문제가 일어나는 부분들인 것 같은데 알맞게 이해한 것인지 태연님 생각이 궁금합니다!

① 드롭다운 선택 → 모달 띄우기 (ScheduledMessageModalLauncher)

@Override
public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) {
    // 모달 생성
    
    // 첫번째 ACK
    event.replyModal(modal).queue();
}

JDA가 Discord에 첫 번째 응답 전송

"사용자가 드롭다운을 선택 → 모달을 띄워줬으니 이벤트 응답 완료!"


② 사용자가 모달에 메시지/시간을 입력하고 제출함

ScheduledMessageSubmitListener 에서 ModalInteractionEvent 발생

→ 🚨🚨🚨 새로운 Interaction(ModalInteractionEvent)을 모든 ListenerAdapter에게 broadcast


ScheduledMessageSubmitListener 가 이벤트를 받아 처리

@Override
public void onModalInteraction(@NotNull ModalInteractionEvent event) {
    // 올바른 모달 ID면 예약 메시지 등록 처리

    // 메시지 파싱 + 유효성 검증

    // 성공적으로 저장 후 Discord에 응답
    event.reply("✅ 메시지가 예약되었습니다.").queue(); // 두 번째 ACK
}

⚠️ 하지만 ScheduledMessageModalLauncher 도 ListenerAdapter 이기 때문에

🚨🚨🚨 JDA는 이 ModalInteractionEventScheduledMessageModalLauncher에게도 전달

-> 내부적으로

[JDA 내부 이벤트 브로드캐스트]
 ├── ScheduledMessageSubmitListener.onModalInteraction(event)
 └── ScheduledMessageModalLauncher.onModalInteraction(event?) ← 중복


⑤ Discord 입장에서는 중복 응답

  • ScheduledMessageSubmitListenerevent.reply("✅ 메시지가...")
  • ScheduledMessageModalLauncher → 내부적으로 이미 event.replyModal() 로 응답한 적 있음

→ Discord API는 해당 이벤트가 이미 응답된 Interaction임을 감지

→ 에러 발생

[ErrorResponseException] 10062: Failed to acknowledge this interaction
40060: Interaction already acknowledged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Code refactoring, dependency updates, and other housekeeping activities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: 예약 메세지 기능 수정

2 participants