Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- feature/kakao-api
jobs:
deploy:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -60,6 +61,8 @@ jobs:

export DB_PASSWORD='${{ secrets.DB_PASSWORD }}'
export DISCORD_TOKEN='${{ secrets.DISCORD_TOKEN }}'
export KAKAO_REST_API_KEY='${{ secrets.KAKAO_REST_API_KEY }}'
export KAKAO_EVENT_API_KEY='${{ secrets.KAKAO_EVENT_API_KEY }}'

nohup java -jar build/libs/workingdead-0.0.1-SNAPSHOT.jar > app.log 2>&1 &

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public void addParticipant(String channelId, String memberId, String memberName)
return;
}

ParticipantRes pRes = participantService.add(voteId, memberName);
ParticipantRes pRes = participantService.add(voteId, null, memberName);
System.out.println("[Discord When:D] Participant added AFTER vote: " + memberName
+ " (discordId=" + memberId + ", participantId=" + pRes.id() + ")");
}
Expand Down

Large diffs are not rendered by default.

41 changes: 37 additions & 4 deletions src/main/java/com/workingdead/chatbot/kakao/dto/KakaoRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ public class KakaoRequest {
public static class Chat {
private String id; // 채팅방 ID (botGroupKey)
private String type; // 채팅방 타입
private ChatProperties properties;
}

@Getter
@Setter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public static class ChatProperties {
private String botGroupKey;
}

@Getter
Expand All @@ -51,6 +60,7 @@ public static class UserRequest {
private String utterance;
private String lang;
private User user;
private Chat chat;
}

@Getter
Expand Down Expand Up @@ -80,6 +90,7 @@ public static class Properties {
private String plusfriendUserKey;
private String appUserId;
private Boolean isFriend;
private String botUserKey;
}

@Getter
Expand Down Expand Up @@ -152,10 +163,26 @@ public String getBotId() {

/**
* 그룹 채팅방 키 (botGroupKey) 조회
* - 카카오 요청에서는 userRequest.chat.id 가 botGroupKey로 사용 가능하며,
* userRequest.chat.properties.botGroupKey 로도 전달됩니다.
*/
public String getBotGroupKey() {
// 1) userRequest.chat.id (요청에 기본 포함)
if (userRequest != null && userRequest.getChat() != null) {
String id = userRequest.getChat().getId();
if (id != null && !id.isBlank()) return id;

// 2) userRequest.chat.properties.botGroupKey
if (userRequest.getChat().getProperties() != null) {
String key = userRequest.getChat().getProperties().getBotGroupKey();
if (key != null && !key.isBlank()) return key;
}
}

// 3) 호환: 최상위 chat.id (현재 JSON에는 없지만 혹시 몰라 유지)
if (chat != null) {
return chat.getId();
String id = chat.getId();
if (id != null && !id.isBlank()) return id;
}
return null;
}
Expand All @@ -164,16 +191,22 @@ public String getBotGroupKey() {
* 그룹 채팅방 여부 확인
*/
public boolean isGroupChat() {
return chat != null && chat.getId() != null;
String key = getBotGroupKey();
return key != null && !key.isBlank();
}

/**
* 그룹챗 사용자 식별용 botUserKey
* - PRD 기준: 멘션/참석자 식별 키로 사용
* - 현재 구조에서는 user.id를 사용
* - 카카오 요청에서는 userRequest.user.properties.botUserKey 로 전달됩니다.
* - fallback: userRequest.user.id
*/
public String getBotUserKey() {
if (userRequest != null && userRequest.getUser() != null) {
Properties p = userRequest.getUser().getProperties();
if (p != null) {
String key = p.getBotUserKey();
if (key != null && !key.isBlank()) return key;
}
String id = userRequest.getUser().getId();
return (id == null || id.isBlank()) ? null : id;
}
Expand Down
126 changes: 19 additions & 107 deletions src/main/java/com/workingdead/chatbot/kakao/service/KakaoNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
import com.workingdead.meet.dto.VoteResultDtos.VoteResultRes;
import com.workingdead.meet.service.ParticipantService;
import com.workingdead.meet.service.VoteResultService;
import java.time.Duration;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -69,16 +66,11 @@ public void sendEventToGroup(String botGroupKey, String eventName) {
/**
* 투표 현황 공유 (이벤트 메시지)
*/
public void shareVoteStatus(String sessionKey) {
public void shareVoteStatus(String botGroupKey) {
try {
Long voteId = kakaoWendyService.getVoteIdBySessionKey(sessionKey);
Long voteId = kakaoWendyService.getVoteIdByBotGroupKey(botGroupKey);
if (voteId == null) return;

LocalDateTime createdAt = kakaoWendyService.getVoteCreatedAtBySessionKey(sessionKey);
if (createdAt == null) return;

long elapsedSeconds = Duration.between(createdAt, LocalDateTime.now()).getSeconds();

List<ParticipantStatusRes> statuses = participantService.getParticipantStatusByVoteId(voteId);
long submittedCount = statuses.stream().filter(s -> Boolean.TRUE.equals(s.submitted())).count();
long totalCount = statuses.size();
Expand All @@ -89,106 +81,76 @@ public void shareVoteStatus(String sessionKey) {
return;
}


boolean allSubmitted = totalCount > 0 && submittedCount == totalCount;
boolean majoritySubmitted = totalCount > 0 && submittedCount * 2 >= totalCount; // 과반(>=)

// 3분 전 & 과반 미달이면 아무것도 안 보냄
if (elapsedSeconds < 180 && !majoritySubmitted && !allSubmitted) {
return;
}

// 3분 경과했는데 0명 투표면 안내 메시지
if (elapsedSeconds >= 180 && submittedCount == 0) {
sendToGroupIfPossible(voteId, "status_nobody_voted");
return;
if (submittedCount == 0) {
sendEventToGroup(botGroupKey, "status_nobody_voted");
} else {
sendEventToGroup(botGroupKey, "status_vote_result");
}

// 결과 공유
sendToGroupIfPossible(voteId, "status_vote_result");

// 전원 투표 완료면 완료 단계로 이동 트리거
boolean allSubmitted = totalCount > 0 && submittedCount == totalCount;
if (allSubmitted) {
sendToGroupIfPossible(voteId, "status_all_done");
sendEventToGroup(botGroupKey, "status_all_done");
}


} catch (Exception e) {
log.error("[Kakao Notifier] Failed to share vote status: {}", e.getMessage());
}
}

public void sendFinalNotice(String sessionKey) {
public void sendFinalNotice(String botGroupKey) {
try {
Long voteId = kakaoWendyService.getVoteIdBySessionKey(sessionKey);
Long voteId = kakaoWendyService.getVoteIdByBotGroupKey(botGroupKey);
if (voteId == null) return;

List<String> nonVoters = getNonVoterNames(voteId);
if (nonVoters.isEmpty()) return; // 미투표자 없으면 전송 X

sendToGroupIfPossible(voteId, "final_notice_24h");
sendEventToGroup(botGroupKey, "final_notice_24h");
} catch (Exception e) {
log.error("[Kakao Notifier] sendFinalNotice failed: {}", e.getMessage(), e);
}
}

public void finalizeIfNoResponse(String sessionKey) {
public void finalizeIfNoResponse(String botGroupKey) {
try {
Long voteId = kakaoWendyService.getVoteIdBySessionKey(sessionKey);
Long voteId = kakaoWendyService.getVoteIdByBotGroupKey(botGroupKey);
if (voteId == null) return;

List<String> nonVoters = getNonVoterNames(voteId);
if (nonVoters.isEmpty()) return; // 이미 다 했으면 확정 처리 X

sendToGroupIfPossible(voteId, "finalize_after_60m");
sendEventToGroup(botGroupKey, "finalize_after_60m");

} catch (Exception e) {
log.error("[Kakao Notifier] finalizeIfNoResponse failed: {}", e.getMessage(), e);
}
}

private void sendToGroupIfPossible(Long voteId, String eventName) {
String botGroupKey = kakaoWendyService.getBotGroupKeyByVoteId(voteId);
if (botGroupKey != null && !botGroupKey.isBlank()) {
sendEventToGroup(botGroupKey, eventName);
} else {
log.info("[Kakao Notifier] Individual chat: voteId={}, eventName={} (cannot push)", voteId, eventName);
}
}

/**
* 미투표자 리마인드 (이벤트 메시지)
*/
public void remindNonVoters(String sessionKey, RemindTiming timing) {
public void remindNonVoters(String botGroupKey, RemindTiming timing) {
try {
Long voteId = kakaoWendyService.getVoteIdBySessionKey(sessionKey);
Long voteId = kakaoWendyService.getVoteIdByBotGroupKey(botGroupKey);
if (voteId == null) {
log.warn("[Kakao Notifier] No vote found for sessionKey: {}", sessionKey);
log.warn("[Kakao Notifier] No vote found for botGroupKey: {}", botGroupKey);
return;
}

List<String> nonVoters = getNonVoterNames(voteId);
if (nonVoters.isEmpty()) {
log.info("[Kakao Notifier] No non-voters. Skip reminder: sessionKey={}, timing={}", sessionKey, timing);
log.info("[Kakao Notifier] No non-voters. Skip reminder: botGroupKey={}, timing={}", botGroupKey, timing);
return;
}


String eventName = switch (timing) {
case MIN_30 -> "remind_30min";
case HOUR_2 -> "remind_2hour";
case HOUR_6 -> "remind_6hour";
case HOUR_12 -> "remind_12hour";
};

// botGroupKey 조회 (그룹챗인 경우에만 이벤트 발송)
String botGroupKey = kakaoWendyService.getBotGroupKeyByVoteId(voteId);
if (botGroupKey != null) {
sendEventToGroup(botGroupKey, eventName);
} else {
log.info("[Kakao Notifier] Reminder for individual chat (cannot push): sessionKey={}, timing={}",
sessionKey, timing);
}
sendEventToGroup(botGroupKey, eventName);

} catch (Exception e) {
log.error("[Kakao Notifier] Failed to send reminder: {}", e.getMessage());
Expand Down Expand Up @@ -247,56 +209,6 @@ public List<String> getNonVoterNames(Long voteId) {
.collect(Collectors.toList());
}

/**
* 카카오 메시지 API 호출 (템플릿)
*
* 참고: 실제 사용하려면 카카오 비즈메시지 설정 필요
* - 카카오톡 채널 개설
* - 발신 프로필 등록
* - 알림톡 템플릿 승인
*/
// public boolean sendKakaoMessage(String userKey, String templateId, Map<String, String> templateArgs) {
// try {
// String adminKey = kakaoConfig.getAdminKey();
// if (adminKey == null || adminKey.isBlank()) {
// log.warn("[Kakao Notifier] Admin key not configured. Message not sent.");
// return false;
// }
//
// HttpHeaders headers = new HttpHeaders();
// headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// headers.set("Authorization", "KakaoAK " + adminKey);
//
// // 템플릿 기반 메시지 구성
// Map<String, Object> templateObject = new HashMap<>();
// templateObject.put("object_type", "text");
// templateObject.put("text", templateArgs.getOrDefault("message", "웬디 알림"));
// templateObject.put("link", Map.of(
// "web_url", templateArgs.getOrDefault("link", "https://whendy.netlify.app"),
// "mobile_web_url", templateArgs.getOrDefault("link", "https://whendy.netlify.app")
// ));
//
// String templateObjectJson = objectMapper.writeValueAsString(templateObject);
// String body = "receiver_uuids=[\"" + userKey + "\"]&template_object=" + templateObjectJson;
//
// HttpEntity<String> request = new HttpEntity<>(body, headers);
//
// ResponseEntity<String> response = kakaoRestTemplate.exchange(
// KAKAO_FRIEND_MESSAGE_URL,
// HttpMethod.POST,
// request,
// String.class
// );
//
// log.info("[Kakao Notifier] Message sent. Response: {}", response.getBody());
// return response.getStatusCode().is2xxSuccessful();
//
// } catch (Exception e) {
// log.error("[Kakao Notifier] Failed to send Kakao message: {}", e.getMessage());
// return false;
// }
// }

private String getDayLabel(DayOfWeek dayOfWeek) {
return switch (dayOfWeek) {
case MONDAY -> "월";
Expand Down
Loading