Skip to content

Commit 9e9d455

Browse files
committed
feat: 챗봇 상세 기능 구현
1 parent 5553b71 commit 9e9d455

9 files changed

Lines changed: 536 additions & 238 deletions

File tree

src/main/java/com/workingdead/chatbot/command/WendyCommand.java

Lines changed: 136 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.workingdead.chatbot.command;
22

33
import com.workingdead.chatbot.scheduler.WendyScheduler;
4-
import com.workingdead.meet.service.WendyService;
4+
import com.workingdead.chatbot.service.WendyService;
55
import net.dv8tion.jda.api.entities.Member;
66
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
77
import net.dv8tion.jda.api.entities.emoji.Emoji;
8+
import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent;
9+
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
810
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
911
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
1012
import net.dv8tion.jda.api.hooks.ListenerAdapter;
13+
import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu;
14+
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
1115
import org.springframework.stereotype.Component;
1216

1317
import java.util.List;
@@ -22,6 +26,10 @@ public class WendyCommand extends ListenerAdapter {
2226

2327
private final Map<String, String> participantCheckMessages = new ConcurrentHashMap<>();
2428
private final Map<String, Boolean> waitingForDateInput = new ConcurrentHashMap<>();
29+
30+
private static final String ATTENDEE_SELECT_MENU_ID = "wendy-attendees";
31+
private static final String WEEK_SELECT_MENU_ID = "wendy-weeks";
32+
private static final String WEEK_SELECT_MENU_REVOTE_ID = "wendy-weeks-revote";
2533

2634
public WendyCommand(WendyService wendyService, WendyScheduler wendyScheduler) {
2735
this.wendyService = wendyService;
@@ -54,14 +62,14 @@ public void onMessageReceived(MessageReceivedEvent event) {
5462
return;
5563
}
5664

57-
// 2.1~2.2 날짜 범위 입력
58-
if (waitingForDateInput.getOrDefault(channelId, false)) {
59-
Integer weeks = extractWeeks(content);
60-
if (weeks != null) {
61-
handleDateInput(channel, member, weeks, false);
62-
return;
63-
}
64-
}
65+
// // 2.1~2.2 날짜 범위 입력
66+
// if (waitingForDateInput.getOrDefault(channelId, false)) {
67+
// Integer weeks = extractWeeks(content);
68+
// if (weeks != null) {
69+
// handleDateInput(channel, member, weeks, false);
70+
// return;
71+
// }
72+
// }
6573

6674
// 4.2 재투표
6775
if (content.equals("웬디 재투표")) {
@@ -75,30 +83,84 @@ public void onMessageReceived(MessageReceivedEvent event) {
7583
return;
7684
}
7785
}
78-
86+
7987
@Override
80-
public void onMessageReactionAdd(MessageReactionAddEvent event) {
81-
if (event.getUser() != null && event.getUser().isBot()) return;
82-
83-
String channelId = event.getChannel().getId();
84-
String messageId = event.getMessageId();
85-
86-
String checkMessageId = participantCheckMessages.get(channelId);
87-
if (checkMessageId == null || !checkMessageId.equals(messageId)) {
88+
public void onEntitySelectInteraction(EntitySelectInteractionEvent event) {
89+
if (!ATTENDEE_SELECT_MENU_ID.equals(event.getComponentId())) {
8890
return;
8991
}
90-
91-
if (!event.getReaction().getEmoji().equals(Emoji.fromUnicode("✅"))) {
92+
93+
String channelId = event.getChannel().getId();
94+
if (!wendyService.isSessionActive(channelId)) {
9295
return;
9396
}
94-
95-
event.retrieveMember().queue(member -> {
96-
if (member != null) {
97-
wendyService.addParticipant(channelId, member.getId(), member.getEffectiveName());
98-
System.out.println("[Command] Participant added: " + member.getEffectiveName());
99-
}
97+
98+
event.getMentions().getMembers().forEach(member -> {
99+
wendyService.addParticipant(channelId, member.getId(), member.getEffectiveName());
100+
System.out.println("[Command] Participant added via select menu: " + member.getEffectiveName());
100101
});
102+
103+
event.reply("참석자 명단이 업데이트됐어요!").setEphemeral(true).queue();
104+
101105
}
106+
107+
@Override
108+
public void onStringSelectInteraction(StringSelectInteractionEvent event) {
109+
String componentId = event.getComponentId();
110+
if (!WEEK_SELECT_MENU_ID.equals(componentId) && !WEEK_SELECT_MENU_REVOTE_ID.equals(componentId)) {
111+
return;
112+
}
113+
114+
String channelId = event.getChannel().getId();
115+
if (!wendyService.isSessionActive(channelId)) {
116+
return;
117+
}
118+
119+
// 하나만 선택하게 설정할 예정이므로 첫 번째 값만 사용
120+
String value = event.getValues().get(0);
121+
int weeks;
122+
try {
123+
weeks = Integer.parseInt(value);
124+
} catch (NumberFormatException e) {
125+
event.reply("선택한 값이 올바르지 않아요. 다시 시도해주세요!").setEphemeral(true).queue();
126+
return;
127+
}
128+
129+
TextChannel channel = event.getChannel().asTextChannel();
130+
Member member = event.getMember();
131+
if (member == null) {
132+
event.reply("사용자 정보를 가져올 수 없어요. 다시 시도해주세요!").setEphemeral(true).queue();
133+
return;
134+
}
135+
136+
boolean isRevote = WEEK_SELECT_MENU_REVOTE_ID.equals(componentId);
137+
handleDateInput(channel, member, weeks, isRevote);
138+
event.reply("투표 날짜 범위를 선택하셨어요!").setEphemeral(true).queue();
139+
}
140+
141+
// @Override
142+
// public void onMessageReactionAdd(MessageReactionAddEvent event) {
143+
// if (event.getUser() != null && event.getUser().isBot()) return;
144+
//
145+
// String channelId = event.getChannel().getId();
146+
// String messageId = event.getMessageId();
147+
//
148+
// String checkMessageId = participantCheckMessages.get(channelId);
149+
// if (checkMessageId == null || !checkMessageId.equals(messageId)) {
150+
// return;
151+
// }
152+
//
153+
// if (!event.getReaction().getEmoji().equals(Emoji.fromUnicode("✅"))) {
154+
// return;
155+
// }
156+
//
157+
// event.retrieveMember().queue(member -> {
158+
// if (member != null) {
159+
// wendyService.addParticipant(channelId, member.getId(), member.getEffectiveName());
160+
// System.out.println("[Command] Participant added: " + member.getEffectiveName());
161+
// }
162+
// });
163+
// }
102164

103165
private void handleStart(TextChannel channel) {
104166
String channelId = channel.getId();
@@ -110,32 +172,50 @@ private void handleStart(TextChannel channel) {
110172
안녕하세요! 일정 조율 도우미 웬디에요 :D
111173
지금부터 여러분의 일정 조율을 도와드릴게요
112174
""").queue();
113-
114-
channel.sendMessage("인원 파악을 위해 참석자분들은 ✅를 남겨주세요!")
115-
.queue(message -> {
116-
participantCheckMessages.put(channelId, message.getId());
117-
message.addReaction(Emoji.fromUnicode("✅")).queue();
118-
System.out.println("[Command] Session started: " + channelId);
119-
});
120-
121-
channel.sendMessage("몇 주 뒤의 일정을 계획하시나요? :D\n(ex. 2주 뒤)").queue();
122-
waitingForDateInput.put(channelId, true);
175+
176+
177+
// 참석자 입력용 엔티티 셀렉트 메뉴 (유저 선택 드롭다운)
178+
EntitySelectMenu attendeeMenu = EntitySelectMenu.create(ATTENDEE_SELECT_MENU_ID, EntitySelectMenu.SelectTarget.USER)
179+
.setPlaceholder("참석자분들을 선택하거나 검색해서 골라주세요")
180+
.setRequiredRange(1, 25)
181+
.build();
182+
183+
channel.sendMessage("인원 파악을 위해 참석자분들을 알려주세요!\n원하는 참석자들을 아래 드롭다운에서 선택해주세요.")
184+
.setActionRow(attendeeMenu)
185+
.queue();
186+
187+
// 2.1 날짜 범위 파악 질문 (드롭다운 방식)
188+
StringSelectMenu weekMenu = StringSelectMenu.create(WEEK_SELECT_MENU_ID)
189+
.setPlaceholder("몇 주 뒤의 일정을 계획하시나요?")
190+
.addOption("이번 주", "0")
191+
.addOption("1주 뒤", "1")
192+
.addOption("2주 뒤", "2")
193+
.addOption("3주 뒤", "3")
194+
.addOption("4주 뒤", "4")
195+
.addOption("5주 뒤", "5")
196+
.addOption("6주 뒤", "6")
197+
.build();
198+
199+
channel.sendMessage("몇 주 뒤의 일정을 계획하시나요? :D")
200+
.setActionRow(weekMenu)
201+
.queue();
123202
}
124203

125204
private void handleDateInput(TextChannel channel, Member member, int weeks, boolean isRevote) {
126205
String channelId = channel.getId();
127-
String userName = member.getEffectiveName();
206+
String userMention = member.getAsMention();
207+
String channelName = channel.getName();
128208

129209
waitingForDateInput.put(channelId, false);
130210

131-
channel.sendMessage(userName + " 님이 " + weeks + "주 뒤를 선택하셨어요!").queue();
211+
channel.sendMessage(userMention + " 님이 " + weeks + "주 뒤를 선택하셨어요!").queue();
132212
channel.sendMessage("해당 일정의 투표를 만들어드릴게요 :D").queue();
133213
channel.sendMessage("(투표 늦게 하는 사람 대머리🧑‍🦲)").queue();
134214
channel.sendMessage("투표를 생성 중입니다🛜").queue();
135215

136216
String voteUrl = isRevote
137-
? wendyService.recreateVote(channelId, weeks)
138-
: wendyService.createVote(channelId, weeks);
217+
? wendyService.recreateVote(channelId, channelName, weeks)
218+
: wendyService.createVote(channelId, channelName, weeks);
139219

140220
channel.sendMessage(voteUrl).queue();
141221
wendyScheduler.startSchedule(channel);
@@ -150,8 +230,22 @@ private void handleRevote(TextChannel channel) {
150230
}
151231

152232
wendyScheduler.stopSchedule(channelId);
153-
channel.sendMessage("몇 주 뒤의 일정을 계획하시나요? :D\n(ex. 2주 뒤)").queue();
154-
waitingForDateInput.put(channelId, true);
233+
234+
235+
StringSelectMenu weekMenu = StringSelectMenu.create(WEEK_SELECT_MENU_REVOTE_ID)
236+
.setPlaceholder("몇 주 뒤의 일정을 다시 계획하시나요?")
237+
.addOption("이번 주", "0")
238+
.addOption("1주 뒤", "1")
239+
.addOption("2주 뒤", "2")
240+
.addOption("3주 뒤", "3")
241+
.addOption("4주 뒤", "4")
242+
.addOption("5주 뒤", "5")
243+
.addOption("6주 뒤", "6")
244+
.build();
245+
246+
channel.sendMessage("몇 주 뒤의 일정을 계획하시나요? :D")
247+
.setActionRow(weekMenu)
248+
.queue();
155249
}
156250

157251
private void handleEnd(TextChannel channel) {

src/main/java/com/workingdead/chatbot/scheduler/WendyScheduler.java

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.workingdead.chatbot.scheduler;
22

3-
import com.workingdead.chatbot.dto.VoteResult;
4-
import com.workingdead.meet.service.WendyService;
3+
import com.workingdead.meet.dto.VoteResultDtos.RankingRes;
4+
import com.workingdead.meet.dto.VoteResultDtos.VoteResultRes;
5+
import com.workingdead.chatbot.service.WendyService;
56
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
67
import org.springframework.stereotype.Component;
78

@@ -29,10 +30,7 @@ public void startSchedule(TextChannel channel) {
2930

3031
// 2.3 투표 현황: 10분 후 첫 공유
3132
tasks.add(scheduler.schedule(() -> shareVoteStatus(channel), 10, TimeUnit.MINUTES));
32-
33-
// 1분마다 신규 투표자 체크
34-
tasks.add(scheduler.scheduleAtFixedRate(() -> checkNewVoterAndShare(channel), 11, 1, TimeUnit.MINUTES));
35-
33+
3634
// 2.4 미투표자 독촉
3735
tasks.add(scheduler.schedule(() -> remindNonVoters(channel, RemindTiming.MIN_15), 15, TimeUnit.MINUTES));
3836
tasks.add(scheduler.schedule(() -> remindNonVoters(channel, RemindTiming.HOUR_1), 1, TimeUnit.HOURS));
@@ -46,34 +44,47 @@ public void startSchedule(TextChannel channel) {
4644

4745
private void shareVoteStatus(TextChannel channel) {
4846
try {
49-
VoteResult result = wendyService.getVoteStatus(channel.getId());
47+
VoteResultRes result = wendyService.getVoteStatus(channel.getId());
48+
String shareUrl = wendyService.getShareUrl(channel.getId());
5049

51-
if (result == null || result.isEmpty()) {
50+
if (result == null || result.rankings() == null || result.rankings().isEmpty()) {
5251
channel.sendMessage("""
5352
웬디가 투표 현황을 공유드려요! :D
5453
5554
엥 아직 아무도 투표를 안 했네요 :(
56-
""").queue();
55+
56+
투표하러 가기: """ + shareUrl
57+
).queue();
5758
return;
5859
}
5960

6061
StringBuilder sb = new StringBuilder();
6162
sb.append("웬디가 투표 현황을 공유드려요! :D\n");
62-
63-
if (result.getVoteUrl() != null) {
64-
sb.append(result.getVoteUrl()).append("\n\n");
63+
64+
if (shareUrl != null && !shareUrl.isBlank()) {
65+
sb.append("\n투표하러 가기: ").append(shareUrl).append("\n\n");
66+
} else {
67+
sb.append("\n투표 링크가 준비되지 않았어요 😢\n\n");
6568
}
6669

67-
for (VoteResult.RankResult rank : result.getRankings()) {
68-
sb.append("📌").append(rank.getRank()).append("순위 ")
69-
.append(rank.getDateTime()).append("\n");
70-
71-
if (rank.getVoters() != null && !rank.getVoters().isEmpty()) {
72-
String voterStr = rank.getVoters().stream()
73-
.map(VoteResult.Voter::toString)
74-
.collect(Collectors.joining(", "));
70+
for (RankingRes rank : result.rankings()) {
71+
if (rank.rank() == null) continue;
72+
73+
String periodLabel = "LUNCH".equals(rank.period()) ? "점심" : "저녁";
74+
sb.append("📌")
75+
.append(rank.rank()).append("순위 ")
76+
.append(rank.date()).append(" ").append(periodLabel).append("\n");
77+
78+
if (rank.voters() != null && !rank.voters().isEmpty()) {
79+
String voterStr = rank.voters().stream()
80+
.map(v -> v.participantName() +
81+
(v.priorityIndex() != null ? "(" + v.priorityIndex() + ")" : ""))
82+
.collect(Collectors.joining(", "));
7583
sb.append("투표자: ").append(voterStr).append("\n");
7684
}
85+
sb.append("\n");
86+
87+
7788
}
7889

7990
channel.sendMessage(sb.toString()).queue();
@@ -109,7 +120,11 @@ private void remindNonVoters(TextChannel channel, RemindTiming timing) {
109120
case MIN_15, HOUR_1 -> mentions + " 투표가 시작됐어요! 다른 분들을 위해 빠른 참여 부탁드려요 :D";
110121
case HOUR_6 -> "다들 " + mentions + " 님의 투표를 기다리고 있어요🙌";
111122
case HOUR_12 -> mentions + " 웬디 기다리다 지쳐버림…🥹 대머리신가요?";
112-
case HOUR_24 -> "최후통첩✉️\n" + mentions + "\n\n00:00까지 투표 불참 시, 1순위 일정으로 확정됩니다";
123+
case HOUR_24 -> {
124+
String bestDateTime = wendyService.getTopRankedDateTime(channel.getId());
125+
String deadline = wendyService.getVoteDeadline(channel.getId());
126+
yield "최후통첩✉️\n" + mentions + "\n\n: " + deadline + "까지 투표 불참 시, " + bestDateTime + "으로 확정됩니다";
127+
}
113128
};
114129

115130
channel.sendMessage(message).queue();
@@ -126,6 +141,14 @@ public void stopSchedule(String channelId) {
126141
System.out.println("[Scheduler] Schedule stopped: " + channelId);
127142
}
128143
}
129-
144+
145+
146+
public void triggerImmediateStatusShare(TextChannel channel) {
147+
shareVoteStatus(channel);
148+
System.out.println("[Scheduler] Immediate vote status share triggered: " + channel.getId());
149+
}
150+
151+
152+
130153
private enum RemindTiming { MIN_15, HOUR_1, HOUR_6, HOUR_12, HOUR_24 }
131154
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.workingdead.chatbot.service;
2+
3+
import com.workingdead.meet.dto.VoteResultDtos.VoteResultRes;
4+
import net.dv8tion.jda.api.entities.Member;
5+
6+
import java.util.List;
7+
8+
public interface WendyService {
9+
void startSession(String channelId, List<Member> participants);
10+
boolean isSessionActive(String channelId);
11+
void endSession(String channelId);
12+
13+
void addParticipant(String channelId, String memberId, String memberName);
14+
void removeParticipant(String channelId, String memberId);
15+
16+
String createVote(String channelId, String channelName, int weeks);
17+
VoteResultRes getVoteStatus(String channelId);
18+
boolean hasNewVoter(String channelId);
19+
List<String> getNonVoterIds(String channelId);
20+
21+
String getShareUrl(String channelId);
22+
23+
boolean hasPreviousVote(String channelId);
24+
String recreateVote(String channelId, String channelName, int weeks);
25+
26+
String getVoteDeadline(String channelId);
27+
String getTopRankedDateTime(String channelId);
28+
}

0 commit comments

Comments
 (0)