From 75e2e8f0baaacafd254fae352052ca98d4dfc6e5 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Fri, 13 Mar 2026 16:51:44 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20@konect=20=EB=A9=98=EC=85=98=20?= =?UTF-8?q?=EC=8B=9C=EC=97=90=EB=A7=8C=20AI=20=EC=9D=91=EB=8B=B5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20Slack=20m?= =?UTF-8?q?rkdwn=20=EB=B3=BC=EB=93=9C=20=EB=B3=80=ED=99=98=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - message 이벤트에서 isAppMention 체크 제거: 스레드 내 다른 앱(@Linear 등) 멘션 시 AI가 호출되던 문제 수정 - app_mention 이벤트에서만 멘션 처리하도록 변경 (AI) 접두사 방식은 유지) - Claude 응답의 **텍스트** 마크다운을 Slack mrkdwn *텍스트* 볼드로 변환하는 convertMarkdownToSlack 메서드 추가 --- .../konect/infrastructure/slack/ai/SlackAIService.java | 7 ++++++- .../infrastructure/slack/ai/SlackEventController.java | 9 --------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java index 4df927c4..29e54c3e 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java @@ -22,6 +22,7 @@ public class SlackAIService { private static final Pattern AI_PREFIX_PATTERN = Pattern.compile("^[Aa][Ii]\\)\\s*(.+)$"); private static final Pattern MENTION_PATTERN = Pattern.compile("^<@[^>]+>\\s*"); + private static final Pattern MARKDOWN_BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*"); private static final String AI_RESPONSE_PREFIX = ":robot_face: *AI 응답*\n"; private static final int MAX_HISTORY_MESSAGES = 10; private static final String EMPTY_QUERY_MESSAGE = @@ -169,7 +170,11 @@ private List> mergeConsecutiveRoles(List return merged; } + private String convertMarkdownToSlack(String text) { + return MARKDOWN_BOLD_PATTERN.matcher(text).replaceAll("*$1*"); + } + private String formatSlackResponse(String response) { - return String.format(":robot_face: *AI 응답*\n%s", response); + return String.format(":robot_face: *AI 응답*\n%s", convertMarkdownToSlack(response)); } } diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java index be7fedbb..fe0fa1dd 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java @@ -1,6 +1,5 @@ package gg.agit.konect.infrastructure.slack.ai; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -115,14 +114,6 @@ private void handleEvent(Map event) { if (slackAIService.isAIQuery(text)) { log.debug("AI 질문 감지"); slackAIService.processAIQuery(text, channelId, effectiveThreadTs, null); - } else if (threadTs != null && slackAIService.isAppMention(text)) { - List> aiReplies = - slackAIService.fetchAIThreadReplies(channelId, threadTs); - if (!aiReplies.isEmpty()) { - log.debug("AI 스레드 내 후속 질문 감지"); - slackAIService.processAIQuery( - text, channelId, effectiveThreadTs, aiReplies); - } } } From bcfbe967c356d1f86deb2b3071a908d86d21ad2b Mon Sep 17 00:00:00 2001 From: JanooGwan <103417427+JanooGwan@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:05:25 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app_mention 핸들러에서 threadTs 존재 시 fetchAIThreadReplies 호출해 대화 히스토리 전달 - convertMarkdownToSlack에 null 방어 코드 추가 - MARKDOWN_BOLD_PATTERN에 Pattern.DOTALL 플래그 추가해 멀티라인 볼드 지원 - formatSlackResponse에서 AI_RESPONSE_PREFIX 상수 활용으로 변경 --- .../slack/ai/SlackAIService.java | 22 ++++++++------ .../slack/ai/SlackEventController.java | 29 ++++++++++++------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java index 29e54c3e..e2f4bda7 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java @@ -22,13 +22,14 @@ public class SlackAIService { private static final Pattern AI_PREFIX_PATTERN = Pattern.compile("^[Aa][Ii]\\)\\s*(.+)$"); private static final Pattern MENTION_PATTERN = Pattern.compile("^<@[^>]+>\\s*"); - private static final Pattern MARKDOWN_BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*"); - private static final String AI_RESPONSE_PREFIX = ":robot_face: *AI 응답*\n"; + private static final Pattern MARKDOWN_BOLD_PATTERN = + Pattern.compile("\\*\\*(.+?)\\*\\*", Pattern.DOTALL); + private static final String AI_RESPONSE_PREFIX = ":robot_face: *AI \uc751\ub2f5*\n"; private static final int MAX_HISTORY_MESSAGES = 10; private static final String EMPTY_QUERY_MESSAGE = - "질문 내용이 비어있습니다. 예: `AI) 가입자 수 알려줘` 또는 `@봇이름 동아리 수는?`"; + "\uc9c8\ubb38 \ub0b4\uc6a9\uc774 \ube44\uc5b4\uc788\uc2b5\ub2c8\ub2e4. \uc608: `AI) \uac00\uc785\uc790 \uc218 \uc54c\ub824\uc918` \ub610\ub294 `@\ubd07\uc774\ub984 \ub3d9\uc544\ub9ac \uc218\ub294?`"; private static final String ERROR_MESSAGE = - ":warning: 죄송합니다. 요청을 처리하는 중 오류가 발생했습니다."; + ":warning: \uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uc694\uccad\uc744 \ucc98\ub9ac\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4."; private final ClaudeClient claudeClient; private final SlackClient slackClient; @@ -85,13 +86,13 @@ public void processAIQuery(String text, String channelId, String threadTs, String userQuery = extractQuery(text); if (userQuery == null || userQuery.isBlank()) { - log.debug("빈 질문으로 처리 중단"); + log.debug("\ube48 \uc9c8\ubb38\uc73c\ub85c \ucc98\ub9ac \uc911\ub2e8"); slackClient.postThreadReply(channelId, threadTs, formatSlackResponse(EMPTY_QUERY_MESSAGE)); return; } - log.debug("AI 질문 처리 시작: {}", userQuery); + log.debug("AI \uc9c8\ubb38 \ucc98\ub9ac \uc2dc\uc791: {}", userQuery); List> replies = cachedReplies != null ? cachedReplies : new ArrayList<>(); @@ -104,12 +105,12 @@ public void processAIQuery(String text, String channelId, String threadTs, String response = claudeClient.chat(messages); - log.debug("AI 응답 생성 완료"); + log.debug("AI \uc751\ub2f5 \uc0dd\uc131 \uc644\ub8cc"); slackClient.postThreadReply(channelId, threadTs, formatSlackResponse(response)); } catch (Exception e) { - log.error("AI 질문 처리 중 오류 발생", e); + log.error("AI \uc9c8\ubb38 \ucc98\ub9ac \uc911 \uc624\ub958 \ubc1c\uc0dd", e); slackClient.postThreadReply(channelId, threadTs, ERROR_MESSAGE); } } @@ -171,10 +172,13 @@ private List> mergeConsecutiveRoles(List } private String convertMarkdownToSlack(String text) { + if (text == null) { + return null; + } return MARKDOWN_BOLD_PATTERN.matcher(text).replaceAll("*$1*"); } private String formatSlackResponse(String response) { - return String.format(":robot_face: *AI 응답*\n%s", convertMarkdownToSlack(response)); + return AI_RESPONSE_PREFIX + convertMarkdownToSlack(response); } } diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java index fe0fa1dd..4de63e24 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java @@ -1,5 +1,6 @@ package gg.agit.konect.infrastructure.slack.ai; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -47,7 +48,7 @@ public ResponseEntity handleSlackEvent( ) { Map payload = parsePayload(rawBody); if (payload == null) { - log.warn("Slack 요청 본문 파싱 실패"); + log.warn("Slack \uc694\uccad \ubcf8\ubb38 \ud30c\uc2f1 \uc2e4\ud328"); return ResponseEntity.badRequest().build(); } @@ -55,21 +56,21 @@ public ResponseEntity handleSlackEvent( if ("url_verification".equals(type)) { String challenge = (String)payload.get("challenge"); - log.info("Slack URL 검증 요청 처리"); + log.info("Slack URL \uac80\uc99d \uc694\uccad \ucc98\ub9ac"); return ResponseEntity.ok(Map.of("challenge", challenge)); } if (!signatureVerifier.isValidRequest(timestamp, signature, rawBody)) { - log.warn("Slack 서명 검증 실패"); + log.warn("Slack \uc11c\uba85 \uac80\uc99d \uc2e4\ud328"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - log.debug("Slack 이벤트 수신: type={}", type); + log.debug("Slack \uc774\ubca4\ud2b8 \uc218\uc2e0: type={}", type); if ("event_callback".equals(type)) { String eventId = (String)payload.get("event_id"); if (eventId != null && !processedEventIds.add(eventId)) { - log.debug("중복 이벤트 무시: event_id={}", eventId); + log.debug("\uc911\ubcf5 \uc774\ubca4\ud2b8 \ubb34\uc2dc: event_id={}", eventId); return ResponseEntity.ok().build(); } if (processedEventIds.size() > EVENT_CACHE_MAX_SIZE) { @@ -89,7 +90,7 @@ private Map parsePayload(String rawBody) { return objectMapper.readValue(rawBody, new TypeReference>() { }); } catch (JsonProcessingException e) { - log.error("JSON 파싱 실패", e); + log.error("JSON \ud30c\uc2f1 \uc2e4\ud328", e); return null; } } @@ -102,7 +103,7 @@ private void handleEvent(Map event) { String ts = (String)event.get("ts"); String threadTs = (String)event.get("thread_ts"); - log.debug("이벤트 처리: eventType={}", eventType); + log.debug("\uc774\ubca4\ud2b8 \ucc98\ub9ac: eventType={}", eventType); if (subtype != null) { return; @@ -112,15 +113,23 @@ private void handleEvent(Map event) { if ("message".equals(eventType) && text != null) { if (slackAIService.isAIQuery(text)) { - log.debug("AI 질문 감지"); + log.debug("AI \uc9c8\ubb38 \uac10\uc9c0"); slackAIService.processAIQuery(text, channelId, effectiveThreadTs, null); } } if ("app_mention".equals(eventType) && text != null) { String normalizedText = slackAIService.normalizeAppMentionText(text); - log.debug("앱 멘션 감지"); - slackAIService.processAIQuery(normalizedText, channelId, effectiveThreadTs, null); + log.debug("\uc571 \uba58\uc158 \uac10\uc9c0"); + if (threadTs != null) { + List> aiReplies = + slackAIService.fetchAIThreadReplies(channelId, threadTs); + slackAIService.processAIQuery( + normalizedText, channelId, effectiveThreadTs, + aiReplies.isEmpty() ? null : aiReplies); + } else { + slackAIService.processAIQuery(normalizedText, channelId, effectiveThreadTs, null); + } } } } From ddb163015be134f45a1e383e9f50d2ca3d54c3e5 Mon Sep 17 00:00:00 2001 From: JanooGwan Date: Fri, 13 Mar 2026 17:16:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=A4=80=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/slack/ai/SlackAIService.java | 8 ++++++-- .../infrastructure/slack/ai/SlackEventController.java | 11 ++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java index 29e54c3e..ebce30c8 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackAIService.java @@ -22,7 +22,8 @@ public class SlackAIService { private static final Pattern AI_PREFIX_PATTERN = Pattern.compile("^[Aa][Ii]\\)\\s*(.+)$"); private static final Pattern MENTION_PATTERN = Pattern.compile("^<@[^>]+>\\s*"); - private static final Pattern MARKDOWN_BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*"); + private static final Pattern MARKDOWN_BOLD_PATTERN = + Pattern.compile("\\*\\*(.+?)\\*\\*", Pattern.DOTALL); private static final String AI_RESPONSE_PREFIX = ":robot_face: *AI 응답*\n"; private static final int MAX_HISTORY_MESSAGES = 10; private static final String EMPTY_QUERY_MESSAGE = @@ -171,10 +172,13 @@ private List> mergeConsecutiveRoles(List } private String convertMarkdownToSlack(String text) { + if (text == null) { + return null; + } return MARKDOWN_BOLD_PATTERN.matcher(text).replaceAll("*$1*"); } private String formatSlackResponse(String response) { - return String.format(":robot_face: *AI 응답*\n%s", convertMarkdownToSlack(response)); + return AI_RESPONSE_PREFIX + convertMarkdownToSlack(response); } } diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java index fe0fa1dd..ba225863 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/ai/SlackEventController.java @@ -1,5 +1,6 @@ package gg.agit.konect.infrastructure.slack.ai; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -120,7 +121,15 @@ private void handleEvent(Map event) { if ("app_mention".equals(eventType) && text != null) { String normalizedText = slackAIService.normalizeAppMentionText(text); log.debug("앱 멘션 감지"); - slackAIService.processAIQuery(normalizedText, channelId, effectiveThreadTs, null); + if (threadTs != null) { + List> aiReplies = + slackAIService.fetchAIThreadReplies(channelId, threadTs); + slackAIService.processAIQuery( + normalizedText, channelId, effectiveThreadTs, + aiReplies.isEmpty() ? null : aiReplies); + } else { + slackAIService.processAIQuery(normalizedText, channelId, effectiveThreadTs, null); + } } } }