diff --git a/config/src/main/resources/config-repo/notification-service.yml b/config/src/main/resources/config-repo/notification-service.yml index a682b79e..6543fb75 100644 --- a/config/src/main/resources/config-repo/notification-service.yml +++ b/config/src/main/resources/config-repo/notification-service.yml @@ -40,3 +40,7 @@ gemini: api: key: ${GEMINI_KEY} model: gemini-2.5-flash + +slack: + sdk: + bot-token: ${SLACK_BOT_TOKEN} \ No newline at end of file diff --git a/notification/build.gradle b/notification/build.gradle index ce1798ad..4bdfea4c 100644 --- a/notification/build.gradle +++ b/notification/build.gradle @@ -31,6 +31,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + // Slack SDK + implementation "com.slack.api:slack-api-client:1.46.0" + // webflux implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/notification/src/main/java/com/hubEleven/notification/slack/application/command/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/application/command/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/application/port/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/application/port/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/application/port/SlackClient.java b/notification/src/main/java/com/hubEleven/notification/slack/application/port/SlackClient.java new file mode 100644 index 00000000..e3fad1ab --- /dev/null +++ b/notification/src/main/java/com/hubEleven/notification/slack/application/port/SlackClient.java @@ -0,0 +1,18 @@ +package com.hubEleven.notification.slack.application.port; + +public interface SlackClient { + + SlackSendResult sendToChannel(String channel, String text); + + SlackSendResult sendDmByEmail(String email, String text); + + record SlackSendResult(boolean success, String channelId, String ts, String error) { + public static SlackSendResult ok(String channelId, String ts) { + return new SlackSendResult(true, channelId, ts, null); + } + + public static SlackSendResult fail(String error) { + return new SlackSendResult(false, null, null, error); + } + } +} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/application/service/impl/SlackServiceImpl.java b/notification/src/main/java/com/hubEleven/notification/slack/application/service/impl/SlackServiceImpl.java index 75e5efd3..c49b80e8 100644 --- a/notification/src/main/java/com/hubEleven/notification/slack/application/service/impl/SlackServiceImpl.java +++ b/notification/src/main/java/com/hubEleven/notification/slack/application/service/impl/SlackServiceImpl.java @@ -13,20 +13,20 @@ import com.hubEleven.notification.slack.application.dto.response.SlackMessageResult; import com.hubEleven.notification.slack.application.service.SlackService; import com.hubEleven.notification.slack.application.validator.SlackValidator; +import com.hubEleven.notification.slack.domain.event.SlackMessageSavedEvent; import com.hubEleven.notification.slack.domain.model.SlackMessage; import com.hubEleven.notification.slack.domain.repository.SlackMessageRepository; import com.hubEleven.notification.slack.domain.service.SlackDomainService; import com.hubEleven.notification.slack.domain.vo.SlackMessageContext; import com.hubEleven.notification.slack.domain.vo.SlackMessageItem; -import com.hubEleven.notification.slack.domain.vo.SlackMessageStatus; import com.hubEleven.notification.slack.exception.SlackErrorCode; -import com.hubEleven.notification.slack.infrastructure.client.SlackWebhookClient; import java.time.LocalDateTime; import java.util.Collections; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -37,11 +37,11 @@ public class SlackServiceImpl implements SlackService { private final SlackMessageRepository slackMessageRepository; - private final SlackWebhookClient slackWebhookClient; private final SlackDomainService slackDomainService; private final AiRequestLogRepository aiRequestLogRepository; private final ObjectMapper objectMapper; private final SlackValidator slackValidator; + private final ApplicationEventPublisher eventPublisher; @Override @Transactional @@ -60,7 +60,9 @@ public SlackMessageResult createMessage(CreateSlackMessageCommand command) { SlackMessage saved = slackMessageRepository.save(slackMessage); - sendToSlackAsync(saved.getId(), formattedMessage); + eventPublisher.publishEvent(SlackMessageSavedEvent.of(saved.getId())); + + log.info("슬랙 메시지 저장 완료 - messageId={}", saved.getId()); return SlackMessageResult.from(saved); } @@ -78,6 +80,8 @@ public SlackMessageResult updateMessage(UpdateSlackMessageCommand command) { slackMessage.updateMessage(command.message()); SlackMessage updated = slackMessageRepository.save(slackMessage); + log.info("슬랙 메시지 수정 완료 - messageId={}", command.messageId()); + return SlackMessageResult.from(updated); } @@ -110,50 +114,58 @@ public CommonPageResponse searchMessages( SearchSlackMessageCommand command, CommonPageRequest pageReq) { slackValidator.slackRead(); - var page = + return PagingUtils.convert( slackMessageRepository.search( command.status(), command.channel(), command.dateFrom(), command.dateTo(), - pageReq.toPageable()); - - return PagingUtils.convert(page, SlackMessageResult::from); + pageReq.toPageable()), + SlackMessageResult::from); } private ResponsePayload findAiPayloadOrThrow(UUID orderId) { - var logEntry = - aiRequestLogRepository - .findByOrderId(orderId) - .orElseThrow(() -> new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL)); - - String raw = logEntry.getRawResponse(); - String cleaned = cleanJsonResponse(raw); - - try { - var payload = objectMapper.readValue(cleaned, ResponsePayload.class); - - if (payload.finalDispatchDeadline == null || payload.finalDispatchDeadline.isBlank()) { - throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); - } - if (payload.messageBody == null || payload.messageBody.isBlank()) { - throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); - } - return payload; - } catch (Exception e) { - throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); - } + return aiRequestLogRepository + .findByOrderId(orderId) + .map( + logEntry -> { + String raw = logEntry.getRawResponse(); + String cleaned = cleanJsonResponse(raw); + + try { + ResponsePayload payload = objectMapper.readValue(cleaned, ResponsePayload.class); + + if (payload.finalDispatchDeadline == null + || payload.finalDispatchDeadline.isBlank()) { + throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); + } + if (payload.messageBody == null || payload.messageBody.isBlank()) { + throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); + } + return payload; + } catch (Exception e) { + throw new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL); + } + }) + .orElseThrow(() -> new GlobalException(AiErrorCode.AI_RESPONSE_PARSE_FAIL)); } private String cleanJsonResponse(String rawJson) { - if (rawJson == null || rawJson.isBlank()) return rawJson; + if (rawJson == null || rawJson.isBlank()) { + return rawJson; + } String cleaned = rawJson.trim(); - if (cleaned.startsWith("```json")) cleaned = cleaned.substring(7); - else if (cleaned.startsWith("```")) cleaned = cleaned.substring(3); + if (cleaned.startsWith("```json")) { + cleaned = cleaned.substring(7); + } else if (cleaned.startsWith("```")) { + cleaned = cleaned.substring(3); + } - if (cleaned.endsWith("```")) cleaned = cleaned.substring(0, cleaned.length() - 3); + if (cleaned.endsWith("```")) { + cleaned = cleaned.substring(0, cleaned.length() - 3); + } return cleaned.trim(); } @@ -189,7 +201,9 @@ private SlackMessageContext buildSlackMessageContext(UUID orderId, ResponsePaylo } private LocalDateTime parseLocalDateTime(String value) { - if (value == null || value.isBlank()) return null; + if (value == null || value.isBlank()) { + return null; + } try { return LocalDateTime.parse(value.trim()); } catch (Exception ignore) { @@ -205,42 +219,6 @@ private String nullToEmpty(String s) { return (s == null) ? "" : s; } - private void sendToSlackAsync(UUID messageId, String messageText) { - String title = "배송 예상 시간 알림"; - - slackWebhookClient - .sendMessage(title, messageText) - .subscribe( - ok -> { - if (Boolean.TRUE.equals(ok)) { - updateMessageStatus(messageId, SlackMessageStatus.SENT); - log.info("슬랙 메시지 전송 성공 messageId={}", messageId); - } else { - updateMessageStatus(messageId, SlackMessageStatus.FAILED); - log.error("슬랙 웹훅 응답이 실패 messageId={}", messageId); - } - }, - error -> { - updateMessageStatus(messageId, SlackMessageStatus.FAILED); - log.error("슬랙 메시지 전송 중 오류가 발생 messageId={}", messageId, error); - }); - } - - @Transactional - protected void updateMessageStatus(UUID messageId, SlackMessageStatus status) { - slackMessageRepository - .findById(messageId) - .ifPresent( - message -> { - if (status == SlackMessageStatus.SENT) { - message.markAsSent(); - } else if (status == SlackMessageStatus.FAILED) { - message.markAsFailed(); - } - slackMessageRepository.save(message); - }); - } - private static final class ResponsePayload { public String finalDispatchDeadline; public String messageBody; diff --git a/notification/src/main/java/com/hubEleven/notification/slack/application/validator/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/application/validator/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/domain/event/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/domain/event/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/domain/event/SlackMessageSavedEvent.java b/notification/src/main/java/com/hubEleven/notification/slack/domain/event/SlackMessageSavedEvent.java new file mode 100644 index 00000000..82c6f92d --- /dev/null +++ b/notification/src/main/java/com/hubEleven/notification/slack/domain/event/SlackMessageSavedEvent.java @@ -0,0 +1,14 @@ +package com.hubEleven.notification.slack.domain.event; + +import java.time.LocalDateTime; +import java.util.UUID; + +/* + SlackMessage가 생성된 후, 커밋 이후 전송 처리를 트리거하기 위한 도메인 이벤트 + 실제 전송은 AFTER_COMMIT 핸들러에서 수행 +*/ +public record SlackMessageSavedEvent(UUID messageId, LocalDateTime occurredAt) { + public static SlackMessageSavedEvent of(UUID messageId) { + return new SlackMessageSavedEvent(messageId, LocalDateTime.now()); + } +} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/domain/event/handler/SlackDomainEventHandler.java b/notification/src/main/java/com/hubEleven/notification/slack/domain/event/handler/SlackDomainEventHandler.java new file mode 100644 index 00000000..3da4e689 --- /dev/null +++ b/notification/src/main/java/com/hubEleven/notification/slack/domain/event/handler/SlackDomainEventHandler.java @@ -0,0 +1,76 @@ +package com.hubEleven.notification.slack.domain.event.handler; + +import com.commonLib.common.exception.GlobalException; +import com.hubEleven.notification.slack.application.port.SlackClient; +import com.hubEleven.notification.slack.domain.event.SlackMessageSavedEvent; +import com.hubEleven.notification.slack.domain.model.SlackMessage; +import com.hubEleven.notification.slack.domain.repository.SlackMessageRepository; +import com.hubEleven.notification.slack.domain.vo.SlackMessageStatus; +import com.hubEleven.notification.slack.exception.SlackErrorCode; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SlackDomainEventHandler { + + private final SlackMessageRepository slackMessageRepository; + private final SlackClient slackClient; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handle(SlackMessageSavedEvent event) { + UUID messageId = event.messageId(); + + try { + SlackMessage slackMessage = + slackMessageRepository + .findById(messageId) + .orElseThrow(() -> new GlobalException(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND)); + + if (slackMessage.getStatus() == SlackMessageStatus.SENT) { + log.info("슬랙 전송 스킵 - messageId={}", messageId); + return; + } + + String channel = slackMessage.getChannel(); + String text = slackMessage.getMessage(); + + SlackClient.SlackSendResult result; + if (channel != null && !channel.isBlank()) { + result = slackClient.sendToChannel(channel, text); + } else { + String email = slackMessage.getRecipientId(); + result = slackClient.sendDmByEmail(email, text); + } + + if (result.success()) { + slackMessage.markAsSent(); + slackMessageRepository.save(slackMessage); + log.info( + "슬랙 전송 성공 - messageId={}, channelId={}, ts={}", + messageId, + result.channelId(), + result.ts()); + } else { + slackMessage.markAsFailed(); + slackMessageRepository.save(slackMessage); + log.warn("슬랙 전송 실패 - messageId={}, error={}", messageId, result.error()); + } + + } catch (Exception e) { + slackMessageRepository + .findById(messageId) + .ifPresent( + m -> { + m.markAsFailed(); + slackMessageRepository.save(m); + }); + log.error("슬랙 전송 처리 중 예외 발생 - messageId={}", messageId, e); + } + } +} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/domain/vo/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/domain/vo/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/client/SlackWebhookClient.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/client/SlackWebhookClient.java deleted file mode 100644 index a484fdd2..00000000 --- a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/client/SlackWebhookClient.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.hubEleven.notification.slack.infrastructure.client; - -import com.hubEleven.notification.slack.infrastructure.config.SlackProperties; -import com.hubEleven.notification.slack.infrastructure.dto.request.SlackWebhookRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@Slf4j -@Component -@RequiredArgsConstructor -public class SlackWebhookClient { - - private final WebClient slackWebClient; - private final SlackProperties slackProperties; - - public Mono sendMessage(String title, String messageText) { - SlackWebhookRequest request = SlackWebhookRequest.create(title, messageText); - - return slackWebClient - .post() - .uri(slackProperties.webhookUrl()) - .bodyValue(request) - .retrieve() - .bodyToMono(String.class) - .map("ok"::equalsIgnoreCase) - .doOnSuccess( - ok -> { - if (Boolean.TRUE.equals(ok)) log.info("Slack webhook sent OK"); - else log.warn("Slack webhook responded non-ok"); - }) - .doOnError(err -> log.error("Slack webhook send failed", err)); - } -} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackProperties.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackProperties.java deleted file mode 100644 index ed889318..00000000 --- a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackProperties.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.hubEleven.notification.slack.infrastructure.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "slack") -public record SlackProperties(String webhookUrl) {} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackSdkConfig.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackSdkConfig.java new file mode 100644 index 00000000..7c5d44b9 --- /dev/null +++ b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackSdkConfig.java @@ -0,0 +1,28 @@ +package com.hubEleven.notification.slack.infrastructure.config; + +import com.slack.api.Slack; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(SlackSdkConfig.class) +@ConfigurationProperties(prefix = "slack.sdk") +public class SlackSdkConfig { + + private String botToken; + + public String getBotToken() { + return botToken; + } + + public void setBotToken(String botToken) { + this.botToken = botToken; + } + + @Bean + public Slack slack() { + return Slack.getInstance(); + } +} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackWebhookClientConfig.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackWebhookClientConfig.java deleted file mode 100644 index 2a4bf199..00000000 --- a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/config/SlackWebhookClientConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.hubEleven.notification.slack.infrastructure.config; - -import io.netty.channel.ChannelOption; -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.handler.timeout.WriteTimeoutHandler; -import java.time.Duration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; - -@Configuration -@EnableConfigurationProperties(SlackProperties.class) -public class SlackWebhookClientConfig { - - @Bean - public WebClient slackWebClient() { - int timeoutMs = 30000; - - HttpClient httpClient = - HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeoutMs) - .responseTimeout(Duration.ofMillis(timeoutMs)) - .doOnConnected( - conn -> - conn.addHandlerLast(new ReadTimeoutHandler(timeoutMs / 1000)) - .addHandlerLast(new WriteTimeoutHandler(timeoutMs / 1000))); - - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build(); - } -} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/request/SlackWebhookRequest.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/request/SlackWebhookRequest.java deleted file mode 100644 index fd54086d..00000000 --- a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/request/SlackWebhookRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.hubEleven.notification.slack.infrastructure.dto.request; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -public record SlackWebhookRequest( - @JsonProperty("text") String text, @JsonProperty("attachments") List attachments) { - public static SlackWebhookRequest create(String title, String messageText) { - Attachment attachment = new Attachment(messageText); - return new SlackWebhookRequest(title, List.of(attachment)); - } - - public record Attachment(@JsonProperty("text") String text) {} -} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/response/SlackWebhookResponse.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/response/SlackWebhookResponse.java deleted file mode 100644 index ceee9d3e..00000000 --- a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/dto/response/SlackWebhookResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.hubEleven.notification.slack.infrastructure.dto.response; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public record SlackWebhookResponse( - @JsonProperty("ok") Boolean ok, @JsonProperty("error") String error) {} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/external/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/external/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/external/adapter/SlackSdkClientAdapter.java b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/external/adapter/SlackSdkClientAdapter.java new file mode 100644 index 00000000..58d08da6 --- /dev/null +++ b/notification/src/main/java/com/hubEleven/notification/slack/infrastructure/external/adapter/SlackSdkClientAdapter.java @@ -0,0 +1,92 @@ +package com.hubEleven.notification.slack.infrastructure.external.adapter; + +import com.hubEleven.notification.slack.application.port.SlackClient; +import com.hubEleven.notification.slack.infrastructure.config.SlackSdkConfig; +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.methods.response.conversations.ConversationsOpenResponse; +import com.slack.api.methods.response.users.UsersLookupByEmailResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SlackSdkClientAdapter implements SlackClient { + + private final Slack slack; + private final SlackSdkConfig slackSdkConfig; + + private MethodsClient methods() { + return slack.methods(slackSdkConfig.getBotToken()); + } + + @Override + public SlackSendResult sendToChannel(String channel, String text) { + try { + ChatPostMessageResponse res = + methods().chatPostMessage(req -> req.channel(channel).text(text)); + + if (Boolean.TRUE.equals(res.isOk())) { + log.info("슬랙 채널 전송 성공 - channel={}, ts={}", res.getChannel(), res.getTs()); + return SlackSendResult.ok(res.getChannel(), res.getTs()); + } + + log.warn("슬랙 채널 전송 실패 - channel={}, error={}", channel, res.getError()); + return SlackSendResult.fail(res.getError()); + + } catch (Exception e) { + log.error("슬랙 채널 전송 중 예외 발생 - channel={}", channel, e); + return SlackSendResult.fail(e.getMessage()); + } + } + + @Override + public SlackSendResult sendDmByEmail(String email, String text) { + try { + UsersLookupByEmailResponse userRes = methods().usersLookupByEmail(req -> req.email(email)); + + if (!Boolean.TRUE.equals(userRes.isOk()) || userRes.getUser() == null) { + String error = userRes.getError(); + log.warn("슬랙 유저 조회 실패(이메일) - email={}, error={}", email, error); + return SlackSendResult.fail(error); + } + + String userId = userRes.getUser().getId(); + + ConversationsOpenResponse openRes = + methods().conversationsOpen(req -> req.users(List.of(userId))); + + if (!Boolean.TRUE.equals(openRes.isOk()) || openRes.getChannel() == null) { + String error = openRes.getError(); + log.warn("슬랙 DM 채널 오픈 실패 - email={}, userId={}, error={}", email, userId, error); + return SlackSendResult.fail(error); + } + + String channelId = openRes.getChannel().getId(); + + ChatPostMessageResponse msgRes = + methods().chatPostMessage(req -> req.channel(channelId).text(text)); + + if (Boolean.TRUE.equals(msgRes.isOk())) { + log.info( + "슬랙 DM 전송 성공 - email={}, channelId={}, ts={}", + email, + msgRes.getChannel(), + msgRes.getTs()); + return SlackSendResult.ok(msgRes.getChannel(), msgRes.getTs()); + } + + log.warn( + "슬랙 DM 전송 실패 - email={}, channelId={}, error={}", email, channelId, msgRes.getError()); + return SlackSendResult.fail(msgRes.getError()); + + } catch (Exception e) { + log.error("슬랙 DM 전송 중 예외 발생 - email={}", email, e); + return SlackSendResult.fail(e.getMessage()); + } + } +} diff --git a/notification/src/main/java/com/hubEleven/notification/slack/presentation/dto/request/.gitkeep b/notification/src/main/java/com/hubEleven/notification/slack/presentation/dto/request/.gitkeep deleted file mode 100644 index e69de29b..00000000