diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/request/NotificationRequest.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/request/NotificationRequest.java index 5230b68d..365a3769 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/request/NotificationRequest.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/request/NotificationRequest.java @@ -6,6 +6,7 @@ import java.util.List; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; public class NotificationRequest { @@ -14,25 +15,37 @@ public record UpdateMaster( Boolean isMasterEnabled ) {} - // 채널별 알림 설정 DTO (ADMIN 전용: slackWebhookUrl/discordWebhookUrl, disconnectSlack/disconnectDiscord) - // 외부 채널 URL: 값 있으면 설정 / disconnectXxx=true 면 삭제(연결 해제) / 둘 다 없으면 변경 없음 + // 변경: 멤버 스코프(브라우저/이메일)만 남김. 슬랙/디스코드 웹훅·활성화는 UpdateOrgSettings 로 이동 public record UpdateChannels( Boolean isBrowserPushEnabled, - Boolean isEmailEnabled, + Boolean isEmailEnabled + ) {} + + // 알림 기준 설정 DTO -> 각 멤버 + public record UpdateAlerts( + Boolean alertClicks, + Boolean alertReport + ) {} + + // 조직 단위 외부 채널 알림 설정 (ADMIN 전용) + // 외부 채널 웹훅 연결/활성화 + 외부 채널로 내보낼 알림 종류 설정을 하나의 DTO 로 통합 + public record UpdateOrgSettings( Boolean isSlackEnabled, + @Pattern( + regexp = "^https://hooks\\.slack\\.com/services/.+", + message = "슬랙 웹훅 URL 형식이 올바르지 않습니다." + ) String slackWebhookUrl, Boolean disconnectSlack, Boolean isDiscordEnabled, + @Pattern( + regexp = "^https://(discord|discordapp)\\.com/api/webhooks/.+", + message = "디스코드 웹훅 URL 형식이 올바르지 않습니다." + ) String discordWebhookUrl, - Boolean disconnectDiscord - ) {} - - // 알림 기준 설정 DTO - public record UpdateAlerts( - Boolean alertBudget50, - Boolean alertBudget80, - Boolean alertBudget100, - Boolean alertRapidClicks + Boolean disconnectDiscord, + Boolean alertClicks, + Boolean alertReport ) {} // 알림을 받을 멤버 설정 DTO(ADMIN 전용) diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/response/NotificationResponse.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/response/NotificationResponse.java index c1438dd0..7d2b9a98 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/response/NotificationResponse.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/dto/response/NotificationResponse.java @@ -12,10 +12,10 @@ public record MySettings( boolean isSlackConnected, boolean isDiscordEnabled, boolean isDiscordConnected, - boolean alertBudget50, - boolean alertBudget80, - boolean alertBudget100, - boolean alertRapidClicks + boolean alertClicks, + boolean alertReport, + boolean orgAlertClicks, + boolean orgAlertReport ) {} public record MemberSetting( diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/mapper/NotificationConverter.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/mapper/NotificationConverter.java index 7e30d4c8..3cd6c91d 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/mapper/NotificationConverter.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/application/mapper/NotificationConverter.java @@ -26,10 +26,10 @@ public static NotificationResponse.MySettings toMySettings( orgSetting != null && orgSetting.hasSlack(), orgSetting != null && orgSetting.isDiscordEnabled(), orgSetting != null && orgSetting.hasDiscord(), - setting.isAlertBudget50(), - setting.isAlertBudget80(), - setting.isAlertBudget100(), - setting.isAlertRapidClicks() + setting.isAlertClicks(), + setting.isAlertReport(), + orgSetting != null && orgSetting.isAlertClicks(), + orgSetting != null && orgSetting.isAlertReport() ); } @@ -47,17 +47,15 @@ public static NotificationResponse.MemberSetting toMemberSetting( ); } - // 기본 알림 설정(entity -> dto), 기본값: 예산 80프로 소진 시 이메일 알림 + // 기본 알림 설정(entity -> dto), 기본값: 마스터 알림 ON, 이메일 수신 ON / 그 외 알림 관련 모두 수신 X public static OrgMemberNotificationSetting toDefaultMemberSetting(OrgMember member) { return OrgMemberNotificationSetting.builder() .orgMember(member) .isMasterEnabled(true) .isBrowserPushEnabled(false) .isEmailEnabled(true) - .alertBudget50(false) - .alertBudget80(true) - .alertBudget100(false) - .alertRapidClicks(false) + .alertClicks(false) + .alertReport(true) .build(); } @@ -70,9 +68,9 @@ public static OrgNotificationSetting toDefaultOrgSetting(Organization organizati public static DiscordMessage toDiscordMessage(String title, String message) { DiscordMessage.Embed embed = new DiscordMessage.Embed( - title == null ? "" : title, message == null ? "" : message, 5814783 // TODO: 디스코드는 알림 메세지 설정 가능 : 현재는 파랑색 + title == null ? "" : title, message == null ? "" : message, 5814783 ); - return new DiscordMessage("WhereYouAd 알림", List.of(embed)); // TODO: 이름을 하드코딩 할지... 아니면 이것도 사용자에게 입력받을지..? + return new DiscordMessage("WhereYouAd 알림", List.of(embed)); } public static SlackMessage toSlackMessage(String title, String message) { diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/constant/NotificationType.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/constant/NotificationType.java new file mode 100644 index 00000000..e99186b1 --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/constant/NotificationType.java @@ -0,0 +1,6 @@ +package com.whereyouad.WhereYouAd.domains.notification.domain.constant; + +public enum NotificationType { + CLICKS, + REPORT +} diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationService.java index d747a09c..45ec7c32 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationService.java @@ -2,6 +2,7 @@ import com.whereyouad.WhereYouAd.domains.notification.application.dto.request.NotificationRequest; import com.whereyouad.WhereYouAd.domains.notification.application.dto.response.NotificationResponse; +import com.whereyouad.WhereYouAd.domains.notification.domain.constant.NotificationType; public interface NotificationService { @@ -13,11 +14,13 @@ public interface NotificationService { void updateAlerts(Long userId, Long orgId, NotificationRequest.UpdateAlerts request); + void updateOrgSettings(Long userId, Long orgId, NotificationRequest.UpdateOrgSettings request); + NotificationResponse.MemberSettingList getMemberSettings(Long userId, Long orgId, String encodedCursor, Integer size); void updateMemberSettings(Long userId, Long orgId, NotificationRequest.BulkUpdateMembers request); - void sendApiAlarmToOrg(Long orgId, String title, String message); + void sendApiAlarmToOrg(Long orgId, NotificationType type, String title, String message); // 설정한 채널이 실제로 동작하는지 테스트 발송 void sendTest(Long orgId, NotificationRequest.TestSend request); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationServiceImpl.java index f7a57752..660b0adb 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/service/NotificationServiceImpl.java @@ -3,6 +3,7 @@ import com.whereyouad.WhereYouAd.domains.notification.application.dto.request.NotificationRequest; import com.whereyouad.WhereYouAd.domains.notification.application.dto.response.NotificationResponse; import com.whereyouad.WhereYouAd.domains.notification.application.mapper.NotificationConverter; +import com.whereyouad.WhereYouAd.domains.notification.domain.constant.NotificationType; import com.whereyouad.WhereYouAd.domains.notification.exception.NotificationException; import com.whereyouad.WhereYouAd.domains.notification.exception.code.NotificationErrorCode; import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.OrgMemberNotificationSetting; @@ -28,7 +29,9 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import java.net.URISyntaxException; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -67,62 +70,61 @@ public void updateMaster(Long userId, Long orgId, NotificationRequest.UpdateMast setting.updateMaster(request.isMasterEnabled()); } - // 채널별 알림 설정 메서드 + // 채널별 알림 설정 메서드 -> 회원 개인 알림설정(이메일, 브라우저 푸시)만 @Override public void updateChannels(Long userId, Long orgId, NotificationRequest.UpdateChannels request) { OrgMember member = findMember(userId, orgId); OrgMemberNotificationSetting setting = findOrCreateSetting(member); setting.updateChannels(request.isBrowserPushEnabled(), request.isEmailEnabled()); - - // ADMIN 인 경우에만 외부 채널 URL / 수신 토글 변경 가능 - // URL 값 있으면 설정 / disconnectXxx=true 면 삭제 / 둘 다 없으면 변경 없음 - if (member.getRole() == OrgRole.ADMIN) { - boolean hasOrgChannelUpdate = - StringUtils.hasText(request.slackWebhookUrl()) || Boolean.TRUE.equals(request.disconnectSlack()) - || StringUtils.hasText(request.discordWebhookUrl()) || Boolean.TRUE.equals(request.disconnectDiscord()) - || request.isSlackEnabled() != null || request.isDiscordEnabled() != null; - - // url 필드 또는 활성화 여부(is~~Enabled) 가 요청에 포함된 경우만 업데이트 (평문 URL 은 암호화하여 저장) - if (hasOrgChannelUpdate) { - // 활성화와 연결 해제를 동시에 요청하는 모순 방지 (URL 삭제 전에 먼저 차단) - // ex. isSlackEnabled = true 로 하면서, disconnectSlack = true 로 전송하는 경우 - if ((Boolean.TRUE.equals(request.isSlackEnabled()) && Boolean.TRUE.equals(request.disconnectSlack())) || - (Boolean.TRUE.equals(request.isDiscordEnabled()) && Boolean.TRUE.equals(request.disconnectDiscord()))) { - throw new NotificationException(NotificationErrorCode.CHANNEL_REQUEST_CONFLICT); - } - - OrgNotificationSetting orgSetting = findOrCreateOrgSetting(member.getOrganization()); - - // URL 반영: 삭제 플래그 우선, 아니면 값이 있을 때만 설정 (삭제 시 엔티티가 enabled=false 자동 처리) - if (Boolean.TRUE.equals(request.disconnectSlack())) { - orgSetting.updateSlackWebhookUrl(null); - } else if (StringUtils.hasText(request.slackWebhookUrl())) { - orgSetting.updateSlackWebhookUrl(encryptOrNull(request.slackWebhookUrl())); - } - - if (Boolean.TRUE.equals(request.disconnectDiscord())) { - orgSetting.updateDiscordWebhookUrl(null); - } else if (StringUtils.hasText(request.discordWebhookUrl())) { - orgSetting.updateDiscordWebhookUrl(encryptOrNull(request.discordWebhookUrl())); - } - - // URL(요청 또는 기존 DB)이 없는 채널을 활성화하려 하면 거부 - if ((Boolean.TRUE.equals(request.isSlackEnabled()) && !orgSetting.hasSlack()) || - (Boolean.TRUE.equals(request.isDiscordEnabled()) && !orgSetting.hasDiscord())) { - throw new NotificationException(NotificationErrorCode.NO_CHANNEL_URL); - } - - orgSetting.updateChannelEnabled(request.isSlackEnabled(), request.isDiscordEnabled()); - } - } } - // 알림 기준 설정 메서드(예산 소진, 클릭 수 급증 등) + // 알림 기준 설정 메서드(클릭 급증 / 봇 클릭 감지 / 주,일간 보고서) @Override public void updateAlerts(Long userId, Long orgId, NotificationRequest.UpdateAlerts request) { OrgMember member = findMember(userId, orgId); OrgMemberNotificationSetting setting = findOrCreateSetting(member); - setting.updateAlerts(request.alertBudget50(), request.alertBudget80(), request.alertBudget100(), request.alertRapidClicks()); + setting.updateAlerts(request.alertClicks(), request.alertReport()); + } + + // 조직 단위 알림 트리거 설정 (ADMIN 전용) — 외부 채널로 내보낼 알림 종류 (클릭 급증 / 봇 클릭 감지 / 주,일간 보고서) 설정 + @Override + public void updateOrgSettings(Long userId, Long orgId, NotificationRequest.UpdateOrgSettings request) { + OrgMember member = findMember(userId, orgId); + requireAdmin(member); + + OrgNotificationSetting orgSetting = findOrCreateOrgSetting(member.getOrganization()); + + // 활성화와 연결 해제를 동시에 요청하는 모순 방지 (URL 삭제 전에 먼저 차단) + if ((Boolean.TRUE.equals(request.isSlackEnabled()) && Boolean.TRUE.equals(request.disconnectSlack())) || + (Boolean.TRUE.equals(request.isDiscordEnabled()) && Boolean.TRUE.equals(request.disconnectDiscord()))) { + throw new NotificationException(NotificationErrorCode.CHANNEL_REQUEST_CONFLICT); + } + + // URL 반영: 삭제 플래그 우선, 아니면 값이 있을 때만 설정 (삭제 시 엔티티가 enabled=false 자동 처리) + if (Boolean.TRUE.equals(request.disconnectSlack())) { + orgSetting.updateSlackWebhookUrl(null); + } else if (StringUtils.hasText(request.slackWebhookUrl())) { + validateWebhookUrl(request.slackWebhookUrl(), "hooks.slack.com"); + orgSetting.updateSlackWebhookUrl(encryptOrNull(request.slackWebhookUrl())); + } + + if (Boolean.TRUE.equals(request.disconnectDiscord())) { + orgSetting.updateDiscordWebhookUrl(null); + } else if (StringUtils.hasText(request.discordWebhookUrl())) { + validateWebhookUrl(request.discordWebhookUrl(), "discord.com", "discordapp.com"); + orgSetting.updateDiscordWebhookUrl(encryptOrNull(request.discordWebhookUrl())); + } + + // URL(요청 또는 기존 DB)이 없는 채널을 활성화하려 하면 거부 + if ((Boolean.TRUE.equals(request.isSlackEnabled()) && !orgSetting.hasSlack()) || + (Boolean.TRUE.equals(request.isDiscordEnabled()) && !orgSetting.hasDiscord())) { + throw new NotificationException(NotificationErrorCode.NO_CHANNEL_URL); + } + + orgSetting.updateChannelEnabled(request.isSlackEnabled(), request.isDiscordEnabled()); + + // 외부 채널로 내보낼 알림 종류 설정 + orgSetting.updateAlerts(request.alertClicks(), request.alertReport()); } @Transactional(readOnly = true) @@ -182,13 +184,20 @@ public void updateMemberSettings(Long userId, Long orgId, NotificationRequest.Bu // 디스코드 / 슬랙 알림 전송 메서드 // 조직 내에 웹훅 URL 이 설정되어 있는 경우 일괄 전송 + // 추가 : NotificationType 을 입력받아 해당 조직의 수신여부를 확인하고 수신 차단되어 있을경우 알림을 발송하지 않습니다. /// *** 각 플랫폼 스케줄러에서 조직으로 디스코드 / 슬랙 알림을 보내려면 이 메서드를 사용하면 됩니다 *** @Override @Transactional(propagation = Propagation.NOT_SUPPORTED) - public void sendApiAlarmToOrg(Long orgId, String title, String message) { + public void sendApiAlarmToOrg(Long orgId, NotificationType type, String title, String message) { OrgNotificationSetting setting = orgSettingRepository.findById(orgId).orElse(null); + // 조직 단위로 외부 채널 알림 설정에 이 알림 종류가 꺼져 있으면(또는 설정 없음) 발송 skip + if (setting == null || !isAlertTypeEnabled(setting, type)) { + log.debug("[외부 알림 발송 skip] 알림 종류 비활성화/설정없음, type={}, orgId={}", type, orgId); + return; + } + boolean slackActive = setting != null && setting.hasSlack() && setting.isSlackEnabled(); boolean discordActive = setting != null && setting.hasDiscord() && setting.isDiscordEnabled(); @@ -230,6 +239,36 @@ private void sendApiAlarm(OrgNotificationSetting setting, Long orgId, String tit } } + // 웹훅 URL이 실제 파싱 가능한 https URL이며 허용 host인지 검증 + private void validateWebhookUrl(String url, String... allowedHosts) { + final URI uri; + try { + uri = new URI(url.trim()); // 형식이 URL이 아니면 URISyntaxException + } catch (URISyntaxException e) { + throw new NotificationException(NotificationErrorCode.INVALID_CHANNEL_URL); + } + + // https + host 존재 확인 + if (!"https".equalsIgnoreCase(uri.getScheme()) || uri.getHost() == null) { + throw new NotificationException(NotificationErrorCode.INVALID_CHANNEL_URL); + } + + // 허용된 host 인지 확인 (slack 또는 discord 가 맞는가?) + boolean hostAllowed = Arrays.stream(allowedHosts) + .anyMatch(h -> h.equalsIgnoreCase(uri.getHost())); + if (!hostAllowed) { + throw new NotificationException(NotificationErrorCode.INVALID_CHANNEL_URL); + } + } + + // 알림 종류 입력 받아 실제 조직에서 수신 설정 되어있는지 여부 반환 + private boolean isAlertTypeEnabled(OrgNotificationSetting setting, NotificationType type) { + return switch (type) { + case CLICKS -> setting.isAlertClicks(); + case REPORT -> setting.isAlertReport(); + }; + } + // 외부 채널 skip 사유 로깅: URL 미설정 vs 설정됐으나 수신 비활성화 private void logChannelSkip(DeliveryChannel channel, Long orgId, boolean hasUrl) { if (hasUrl) { diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/exception/code/NotificationErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/exception/code/NotificationErrorCode.java index 89a1eee6..9bf90594 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/exception/code/NotificationErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/exception/code/NotificationErrorCode.java @@ -13,6 +13,7 @@ public enum NotificationErrorCode implements BaseErrorCode { NO_CHANNEL_CONFIGURED(HttpStatus.BAD_REQUEST, "NOTIFICATION_400_1", "발송할 외부 채널(슬랙/디스코드)이 설정되어 있지 않습니다."), NO_CHANNEL_URL(HttpStatus.BAD_REQUEST, "NOTIFICATION_400_2", "외부 채널 알림을 활성화 하기 위한 해당 채널 웹훅 URL 이 입력되지 않았습니다."), CHANNEL_REQUEST_CONFLICT(HttpStatus.BAD_REQUEST, "NOTIFICATION_400_3", "채널 알림 활성화와 연결 해제(웹훅 삭제)를 동시에 요청할 수 없습니다."), + INVALID_CHANNEL_URL(HttpStatus.BAD_REQUEST, "NOTIFICATION_400_4", "웹훅 URL 형식이 올바르지 않습니다."), // 403 FORBIDDEN(HttpStatus.FORBIDDEN, "NOTIFICATION_403_1", "해당 작업을 수행할 권한이 없습니다."), diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgMemberNotificationSetting.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgMemberNotificationSetting.java index 38558874..5e2e74d7 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgMemberNotificationSetting.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgMemberNotificationSetting.java @@ -4,6 +4,7 @@ import com.whereyouad.WhereYouAd.global.common.BaseEntity; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.ColumnDefault; @Entity @Table(name = "org_member_notification_setting") @@ -31,17 +32,13 @@ public class OrgMemberNotificationSetting extends BaseEntity { @Column(name = "is_email_enabled", nullable = false) private boolean isEmailEnabled; - @Column(name = "alert_budget_50", nullable = false) - private boolean alertBudget50; + @Column(name = "alert_clicks", nullable = false) + @ColumnDefault("false") + private boolean alertClicks; - @Column(name = "alert_budget_80", nullable = false) - private boolean alertBudget80; - - @Column(name = "alert_budget_100", nullable = false) - private boolean alertBudget100; - - @Column(name = "alert_rapid_clicks", nullable = false) - private boolean alertRapidClicks; + @Column(name = "alert_report", nullable = false) + @ColumnDefault("false") + private boolean alertReport; public void updateMaster(Boolean isMasterEnabled) { if (isMasterEnabled != null) this.isMasterEnabled = isMasterEnabled; @@ -52,10 +49,8 @@ public void updateChannels(Boolean isBrowserPushEnabled, Boolean isEmailEnabled) if (isEmailEnabled != null) this.isEmailEnabled = isEmailEnabled; } - public void updateAlerts(Boolean alertBudget50, Boolean alertBudget80, Boolean alertBudget100, Boolean alertRapidClicks) { - if (alertBudget50 != null) this.alertBudget50 = alertBudget50; - if (alertBudget80 != null) this.alertBudget80 = alertBudget80; - if (alertBudget100 != null) this.alertBudget100 = alertBudget100; - if (alertRapidClicks != null) this.alertRapidClicks = alertRapidClicks; + public void updateAlerts(Boolean alertClicks, Boolean alertReport) { + if (alertClicks != null) this.alertClicks = alertClicks; + if (alertReport != null) this.alertReport = alertReport; } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java index 5eba4fd9..63a7adf2 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/OrgNotificationSetting.java @@ -38,6 +38,20 @@ public class OrgNotificationSetting extends BaseEntity { @ColumnDefault("false") private boolean isDiscordEnabled; + @Column(name = "alert_clicks", nullable = false) + @ColumnDefault("false") + private boolean alertClicks; + + @Column(name = "alert_report", nullable = false) + @ColumnDefault("false") + private boolean alertReport; + + // 조직 단위 외부 채널 알림 토글 업데이트 메서드 + public void updateAlerts(Boolean alertClicks, Boolean alertReport) { + if (alertClicks != null) this.alertClicks = alertClicks; + if (alertReport != null) this.alertReport = alertReport; + } + // 외부 채널 알림 수신 여부 토글 업데이트 메서드 // 웹훅 URL 이 등록되어 있더라도, 알림 수신 비활성화만 가능하도록 따로 메서드 추가 public void updateChannelEnabled(Boolean isSlackEnabled, Boolean isDiscordEnabled) { diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/NotificationController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/NotificationController.java index 8542c185..3882929c 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/NotificationController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/NotificationController.java @@ -61,6 +61,17 @@ public ResponseEntity> updateAlerts( return ResponseEntity.ok(DataResponse.ok()); } + @PatchMapping("/settings/{orgId}/org") + @Override + public ResponseEntity> updateOrgSettings( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable Long orgId, + @RequestBody @Valid NotificationRequest.UpdateOrgSettings request + ) { + notificationService.updateOrgSettings(userId, orgId, request); + return ResponseEntity.ok(DataResponse.ok()); + } + @GetMapping("/settings/{orgId}/members") @Override public ResponseEntity> getMemberSettings( diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/docs/NotificationControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/docs/NotificationControllerDocs.java index 97f2cd11..464a47bc 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/docs/NotificationControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/presentation/docs/NotificationControllerDocs.java @@ -37,9 +37,7 @@ ResponseEntity> updateMaster( @RequestBody NotificationRequest.UpdateMaster request ); - @Operation(summary = "알림 채널 설정 변경", description = "브라우저 푸시, 이메일, 슬랙, 디스코드 알림 채널을 설정합니다. " + - "slackWebhookUrl/discordWebhookUrl은 ADMIN만 반영됩니다. " + - "웹훅 URL은 값이 있으면 설정, disconnectSlack/disconnectDiscord=true면 삭제(연결 해제), 둘 다 없으면 변경 없음입니다.") + @Operation(summary = "알림 채널 설정 변경", description = "회원 개인의 브라우저 푸시, 이메일 알림 채널을 설정합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "변경 성공"), @ApiResponse(responseCode = "400", description = "NOTIFICATION_400_2: URL 없이 알림 활성화\n\n NOTIFICATION_400_3 : 활성화+연결해제 동시 요청"), @@ -51,7 +49,7 @@ ResponseEntity> updateChannels( @RequestBody NotificationRequest.UpdateChannels request ); - @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 리스크 알림 트리거(예산 소진율, 클릭 급증)를 설정합니다.") + @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 알림 트리거(클릭 관련, 주·일간 보고서)를 설정합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "변경 성공"), @ApiResponse(responseCode = "404", description = "해당 조직의 멤버가 아닙니다.") @@ -62,6 +60,22 @@ ResponseEntity> updateAlerts( @RequestBody NotificationRequest.UpdateAlerts request ); + @Operation(summary = "조직 알림 설정 변경", + description = "조직 공용 외부 채널(Slack/Discord) 웹훅 연결·활성화와, 외부 채널로 내보낼 알림 종류(클릭 급증, 봇 클릭 감지, 주·일간 보고서)를 설정합니다. ADMIN만 접근 가능합니다. " + + "웹훅 URL은 값이 있으면 설정, disconnectSlack/disconnectDiscord=true면 삭제(연결 해제), 둘 다 없으면 변경 없음입니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공"), + @ApiResponse(responseCode = "400", description = "NOTIFICATION_400_2: URL 없이 알림 활성화\n\n NOTIFICATION_400_3 : 활성화+연결해제 동시 요청\n\n NOTIFICATION_400_4 : 웹훅 URL 형식이 올바르지 않습니다."), + @ApiResponse(responseCode = "403", description = "ADMIN 권한이 없습니다."), + @ApiResponse(responseCode = "404", description = "해당 조직의 멤버가 아닙니다.") + }) + ResponseEntity> updateOrgSettings( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable Long orgId, + @RequestBody NotificationRequest.UpdateOrgSettings request + ); + + @Operation(summary = "멤버 알림 설정 목록 조회", description = "조직 내 멤버별 알림 수신 여부를 조회합니다. ADMIN만 접근 가능합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "조회 성공"),