From 42a193aeeb339b8263812f3628252a88a1a4d932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:46:41 +0900 Subject: [PATCH 01/26] =?UTF-8?q?:recycle:=20refactor:=20OrgMemberNotifica?= =?UTF-8?q?tionSetting=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=20=EC=98=88=EC=82=B0=20=EA=B4=80=EB=A0=A8=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20&=20=ED=81=B4=EB=A6=AD=EC=88=98=20?= =?UTF-8?q?=EA=B8=89=EC=A6=9D,=20=EB=B4=87=20=ED=81=B4=EB=A6=AD,=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=EC=95=8C=EB=A6=BC=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B9=84=EC=A6=88?= =?UTF-8?q?=EB=8B=88=EC=8A=A4=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/OrgMemberNotificationSetting.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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..e28b30c5 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 @@ -31,18 +31,15 @@ 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_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_bot_clicks", nullable = false) + private boolean alertBotClicks; + + @Column(name = "alert_report", nullable = false) + private boolean alertReport; + public void updateMaster(Boolean isMasterEnabled) { if (isMasterEnabled != null) this.isMasterEnabled = isMasterEnabled; } @@ -52,10 +49,9 @@ 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; + public void updateAlerts(Boolean alertRapidClicks, Boolean alertBotClicks, Boolean alertReport) { + if (alertBotClicks != null) this.alertBotClicks = alertBotClicks; + if (alertReport != null) this.alertReport = alertReport; if (alertRapidClicks != null) this.alertRapidClicks = alertRapidClicks; } } From 5c22806e6932b5f3b6a1f2a70f816d1cad710398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:49:03 +0900 Subject: [PATCH 02/26] =?UTF-8?q?:recycle:=20refactor:=20NotificationConve?= =?UTF-8?q?rter=20=EB=82=B4=EB=B6=80=20OrgMemberNotificationSetting=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=A0=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/NotificationConverter.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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..8d061c84 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,9 @@ 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.isAlertRapidClicks(), + setting.isAlertBotClicks(), + setting.isAlertReport() ); } @@ -47,17 +46,16 @@ 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) + .alertBotClicks(false) + .alertReport(false) // TODO : 리포트 알림은 기본값 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) { From dc8f64f8f1d404a4d01ce0787cc4a0309627131a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:50:30 +0900 Subject: [PATCH 03/26] =?UTF-8?q?:recycle:=20refactor:=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9A=94=EC=B2=AD=20DTO=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?=EC=98=88=EC=82=B0=20=EA=B4=80=EB=A0=A8=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20&=20=ED=81=B4=EB=A6=AD=20=EA=B8=89?= =?UTF-8?q?=EC=A6=9D,=20=EB=B4=87=20=ED=81=B4=EB=A6=AD,=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=95=8C=EB=A6=BC=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/request/NotificationRequest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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..b9a5bddc 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 @@ -27,12 +27,11 @@ public record UpdateChannels( Boolean disconnectDiscord ) {} - // 알림 기준 설정 DTO + // 알림 기준 설정 DTO -> 각 멤버 public record UpdateAlerts( - Boolean alertBudget50, - Boolean alertBudget80, - Boolean alertBudget100, - Boolean alertRapidClicks + Boolean alertRapidClicks, + Boolean alertBotClicks, + Boolean alertReport ) {} // 알림을 받을 멤버 설정 DTO(ADMIN 전용) From b83c4ab0dd616215c4d8466deb7181fcfa72555d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:51:02 +0900 Subject: [PATCH 04/26] =?UTF-8?q?:recycle:=20refactor:=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9D=91=EB=8B=B5=20DTO=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?=EC=98=88=EC=82=B0=20=EA=B4=80=EB=A0=A8=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20&=20=ED=81=B4=EB=A6=AD=20=EA=B8=89?= =?UTF-8?q?=EC=A6=9D,=20=EB=B4=87=20=ED=81=B4=EB=A6=AD,=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=95=8C=EB=A6=BC=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/response/NotificationResponse.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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..39ad5832 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,9 @@ public record MySettings( boolean isSlackConnected, boolean isDiscordEnabled, boolean isDiscordConnected, - boolean alertBudget50, - boolean alertBudget80, - boolean alertBudget100, - boolean alertRapidClicks + boolean alertRapidClicks, + boolean alertBotClicks, + boolean alertReport ) {} public record MemberSetting( From 39130076092218663944f5a4397669824da05615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:52:30 +0900 Subject: [PATCH 05/26] =?UTF-8?q?:recycle:=20refactor:=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=20Service=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/domain/service/NotificationServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..01a3145f 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 @@ -117,12 +117,12 @@ public void updateChannels(Long userId, Long orgId, NotificationRequest.UpdateCh } } - // 알림 기준 설정 메서드(예산 소진, 클릭 수 급증 등) + // 알림 기준 설정 메서드(클릭 급증 / 봇 클릭 감지 / 주,일간 보고서) @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.alertRapidClicks(), request.alertBotClicks(), request.alertReport()); } @Transactional(readOnly = true) From e1cb3c04b44029c6381a2dccc10184223e180c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:53:25 +0900 Subject: [PATCH 06/26] =?UTF-8?q?:art:=20style:=20Swagger=20Docs=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/docs/NotificationControllerDocs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..16c38e71 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 @@ -51,7 +51,7 @@ ResponseEntity> updateChannels( @RequestBody NotificationRequest.UpdateChannels request ); - @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 리스크 알림 트리거(예산 소진율, 클릭 급증)를 설정합니다.") + @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 알림 트리거(클릭 급증, 봇 클릭 감지, 주·일간 보고서)를 설정합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "변경 성공"), @ApiResponse(responseCode = "404", description = "해당 조직의 멤버가 아닙니다.") From d62feeaeef2172c91c77e379336e212f226eec69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:54:29 +0900 Subject: [PATCH 07/26] =?UTF-8?q?:sparkles:=20feat:=20OrgNotificationSetti?= =?UTF-8?q?ng=20=EB=82=B4=EB=B6=80=20=EC=95=8C=EB=A6=BC=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20(=ED=81=B4=EB=A6=AD=EC=88=98=20=EA=B8=89=EC=A6=9D,?= =?UTF-8?q?=20=EB=B4=87=20=ED=81=B4=EB=A6=AD,=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EC=95=8C=EB=A6=BC)=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/OrgNotificationSetting.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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..67985533 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,22 @@ public class OrgNotificationSetting extends BaseEntity { @ColumnDefault("false") private boolean isDiscordEnabled; + @Column(name = "alert_rapid_clicks", nullable = false) + private boolean alertRapidClicks; + + @Column(name = "alert_bot_clicks", nullable = false) + private boolean alertBotClicks; + + @Column(name = "alert_report", nullable = false) + private boolean alertReport; + + // 조직 단위 외부 채널 알림 토글 업데이트 메서드 + public void updateAlerts(Boolean alertRapidClicks, Boolean alertBotClicks, Boolean alertReport) { + if (alertRapidClicks != null) this.alertRapidClicks = alertRapidClicks; + if (alertBotClicks != null) this.alertBotClicks = alertBotClicks; + if (alertReport != null) this.alertReport = alertReport; + } + // 외부 채널 알림 수신 여부 토글 업데이트 메서드 // 웹훅 URL 이 등록되어 있더라도, 알림 수신 비활성화만 가능하도록 따로 메서드 추가 public void updateChannelEnabled(Boolean isSlackEnabled, Boolean isDiscordEnabled) { From f9737bedc5e08c8361fb9928ca3949e3df75a5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:55:13 +0900 Subject: [PATCH 08/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD,=20=EC=9D=91=EB=8B=B5=20DTO=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EC=A1=B0=EC=A7=81=20=EB=8B=A8=EC=9C=84=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=EC=B1=84=EB=84=90=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/NotificationRequest.java | 23 ++++++++++++------- .../dto/response/NotificationResponse.java | 5 +++- 2 files changed, 19 insertions(+), 9 deletions(-) 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 b9a5bddc..f96d1ec7 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 @@ -14,21 +14,28 @@ 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 alertRapidClicks, + Boolean alertBotClicks, + Boolean alertReport + ) {} + + // 조직 단위 외부 채널 알림 설정 (ADMIN 전용) + // 외부 채널 웹훅 연결/활성화 + 외부 채널로 내보낼 알림 종류 설정을 하나의 DTO 로 통합 + public record UpdateOrgSettings( Boolean isSlackEnabled, String slackWebhookUrl, Boolean disconnectSlack, Boolean isDiscordEnabled, String discordWebhookUrl, - Boolean disconnectDiscord - ) {} - - // 알림 기준 설정 DTO -> 각 멤버 - public record UpdateAlerts( + Boolean disconnectDiscord, Boolean alertRapidClicks, Boolean alertBotClicks, Boolean alertReport 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 39ad5832..919f29fa 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 @@ -14,7 +14,10 @@ public record MySettings( boolean isDiscordConnected, boolean alertRapidClicks, boolean alertBotClicks, - boolean alertReport + boolean alertReport, + boolean orgAlertRapidClicks, // 조직 외부채널 발송 설정 (ADMIN 설정) + boolean orgAlertBotClicks, + boolean orgAlertReport ) {} public record MemberSetting( From 5ebc9b8e2a05b1f40b94e43934da80588a56c2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:55:41 +0900 Subject: [PATCH 09/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=A2=85=EB=A5=98=20=EA=B5=AC=EB=B6=84=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20NotificationType=20enum=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/domain/constant/NotificationType.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/constant/NotificationType.java 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..1695a741 --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/notification/domain/constant/NotificationType.java @@ -0,0 +1,5 @@ +package com.whereyouad.WhereYouAd.domains.notification.domain.constant; + +public enum NotificationType { + RAPID_CLICKS, BOT_CLICKS, REPORT +} From d6e61af795f4808dd15618f16383ed04aade18f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:56:07 +0900 Subject: [PATCH 10/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=A1=B0=EC=A7=81?= =?UTF-8?q?=EB=B3=84=20=EC=99=B8=EB=B6=80=EC=B1=84=EB=84=90=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Converter=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/NotificationConverter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 8d061c84..70aa9a35 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 @@ -28,7 +28,10 @@ public static NotificationResponse.MySettings toMySettings( orgSetting != null && orgSetting.hasDiscord(), setting.isAlertRapidClicks(), setting.isAlertBotClicks(), - setting.isAlertReport() + setting.isAlertReport(), + orgSetting != null && orgSetting.isAlertRapidClicks(), + orgSetting != null && orgSetting.isAlertBotClicks(), + orgSetting != null && orgSetting.isAlertReport() ); } From e436f46eda5f890e8b1e5b7dd11eac4db997bfb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:56:42 +0900 Subject: [PATCH 11/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=A1=B0=EC=A7=81?= =?UTF-8?q?=EB=B3=84=20=EC=99=B8=EB=B6=80=EC=B1=84=EB=84=90=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20&=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=9B=B9=ED=9B=85=20URL=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/NotificationService.java | 5 +- .../service/NotificationServiceImpl.java | 102 ++++++++++-------- 2 files changed, 62 insertions(+), 45 deletions(-) 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 01a3145f..bab3c62c 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; @@ -67,54 +68,12 @@ 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()); - } - } } // 알림 기준 설정 메서드(클릭 급증 / 봇 클릭 감지 / 주,일간 보고서) @@ -125,6 +84,45 @@ public void updateAlerts(Long userId, Long orgId, NotificationRequest.UpdateAler setting.updateAlerts(request.alertRapidClicks(), request.alertBotClicks(), 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())) { + 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()); + + // 외부 채널로 내보낼 알림 종류 설정 + orgSetting.updateAlerts(request.alertRapidClicks(), request.alertBotClicks(), request.alertReport()); + } + @Transactional(readOnly = true) @Override public NotificationResponse.MemberSettingList getMemberSettings(Long userId, Long orgId, String encodedCursor, Integer size) { @@ -182,13 +180,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 +235,15 @@ private void sendApiAlarm(OrgNotificationSetting setting, Long orgId, String tit } } + // 알림 종류 입력 받아 실제 조직에서 수신 설정 되어있는지 여부 반환 + private boolean isAlertTypeEnabled(OrgNotificationSetting setting, NotificationType type) { + return switch (type) { + case RAPID_CLICKS -> setting.isAlertRapidClicks(); + case BOT_CLICKS -> setting.isAlertBotClicks(); + case REPORT -> setting.isAlertReport(); + }; + } + // 외부 채널 skip 사유 로깅: URL 미설정 vs 설정됐으나 수신 비활성화 private void logChannelSkip(DeliveryChannel channel, Long orgId, boolean hasUrl) { if (hasUrl) { From fc3e4b6a40896a9605b85774ce425817f05e9bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:57:02 +0900 Subject: [PATCH 12/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=A1=B0=EC=A7=81?= =?UTF-8?q?=EB=B3=84=20=EC=99=B8=EB=B6=80=EC=B1=84=EB=84=90=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/NotificationController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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..7308a39c 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 NotificationRequest.UpdateOrgSettings request + ) { + notificationService.updateOrgSettings(userId, orgId, request); + return ResponseEntity.ok(DataResponse.ok()); + } + @GetMapping("/settings/{orgId}/members") @Override public ResponseEntity> getMemberSettings( From 25d73386a3799b90b0abca7101045939ab50830f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 15:57:19 +0900 Subject: [PATCH 13/26] =?UTF-8?q?:art:=20style:=20Swagger=20Docs=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/NotificationControllerDocs.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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 16c38e71..109d8c09 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 : 활성화+연결해제 동시 요청"), @@ -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 : 활성화+연결해제 동시 요청"), + @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 = "조회 성공"), From ea761f41f9e7bcf2919724769eb775bf6b62eb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 16:56:02 +0900 Subject: [PATCH 14/26] =?UTF-8?q?:sparkles:=20feat:=20OrgMemberNotificatio?= =?UTF-8?q?nSetting=20=EB=82=B4=EB=B6=80=20ColumnDefault=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=BD=94=EB=93=9C=EB=9E=98=EB=B9=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/entity/OrgMemberNotificationSetting.java | 4 ++++ 1 file changed, 4 insertions(+) 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 e28b30c5..167377d0 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") @@ -32,12 +33,15 @@ public class OrgMemberNotificationSetting extends BaseEntity { private boolean isEmailEnabled; @Column(name = "alert_rapid_clicks", nullable = false) + @ColumnDefault("false") private boolean alertRapidClicks; @Column(name = "alert_bot_clicks", nullable = false) + @ColumnDefault("false") private boolean alertBotClicks; @Column(name = "alert_report", nullable = false) + @ColumnDefault("false") private boolean alertReport; public void updateMaster(Boolean isMasterEnabled) { From 2624c31f24826864170ab5f36fd9218f7792b224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Fri, 10 Jul 2026 16:56:44 +0900 Subject: [PATCH 15/26] =?UTF-8?q?:sparkles:=20feat:=20OrgNotificationSetti?= =?UTF-8?q?ng=20=EB=82=B4=EB=B6=80=20ColumnDefault=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EC=BD=94=EB=93=9C=EB=9E=98=EB=B9=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/entity/OrgNotificationSetting.java | 3 +++ 1 file changed, 3 insertions(+) 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 67985533..7d6d83ce 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 @@ -39,12 +39,15 @@ public class OrgNotificationSetting extends BaseEntity { private boolean isDiscordEnabled; @Column(name = "alert_rapid_clicks", nullable = false) + @ColumnDefault("false") private boolean alertRapidClicks; @Column(name = "alert_bot_clicks", nullable = false) + @ColumnDefault("false") private boolean alertBotClicks; @Column(name = "alert_report", nullable = false) + @ColumnDefault("false") private boolean alertReport; // 조직 단위 외부 채널 알림 토글 업데이트 메서드 From ca416771b88ee32caaa6545de0b5a683cb704540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:40:43 +0900 Subject: [PATCH 16/26] =?UTF-8?q?:recycle:=20refactor:=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=82=B4=EB=B6=80=20=EB=B4=87=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=95=8C=EB=A6=BC=20&=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=88=98=20=EC=95=8C=EB=A6=BC=20->=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/OrgMemberNotificationSetting.java | 13 ++++--------- .../persistence/entity/OrgNotificationSetting.java | 13 ++++--------- 2 files changed, 8 insertions(+), 18 deletions(-) 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 167377d0..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 @@ -32,13 +32,9 @@ public class OrgMemberNotificationSetting extends BaseEntity { @Column(name = "is_email_enabled", nullable = false) private boolean isEmailEnabled; - @Column(name = "alert_rapid_clicks", nullable = false) + @Column(name = "alert_clicks", nullable = false) @ColumnDefault("false") - private boolean alertRapidClicks; - - @Column(name = "alert_bot_clicks", nullable = false) - @ColumnDefault("false") - private boolean alertBotClicks; + private boolean alertClicks; @Column(name = "alert_report", nullable = false) @ColumnDefault("false") @@ -53,9 +49,8 @@ public void updateChannels(Boolean isBrowserPushEnabled, Boolean isEmailEnabled) if (isEmailEnabled != null) this.isEmailEnabled = isEmailEnabled; } - public void updateAlerts(Boolean alertRapidClicks, Boolean alertBotClicks, Boolean alertReport) { - if (alertBotClicks != null) this.alertBotClicks = alertBotClicks; + public void updateAlerts(Boolean alertClicks, Boolean alertReport) { + if (alertClicks != null) this.alertClicks = alertClicks; if (alertReport != null) this.alertReport = alertReport; - if (alertRapidClicks != null) this.alertRapidClicks = alertRapidClicks; } } 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 7d6d83ce..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,22 +38,17 @@ public class OrgNotificationSetting extends BaseEntity { @ColumnDefault("false") private boolean isDiscordEnabled; - @Column(name = "alert_rapid_clicks", nullable = false) + @Column(name = "alert_clicks", nullable = false) @ColumnDefault("false") - private boolean alertRapidClicks; - - @Column(name = "alert_bot_clicks", nullable = false) - @ColumnDefault("false") - private boolean alertBotClicks; + private boolean alertClicks; @Column(name = "alert_report", nullable = false) @ColumnDefault("false") private boolean alertReport; // 조직 단위 외부 채널 알림 토글 업데이트 메서드 - public void updateAlerts(Boolean alertRapidClicks, Boolean alertBotClicks, Boolean alertReport) { - if (alertRapidClicks != null) this.alertRapidClicks = alertRapidClicks; - if (alertBotClicks != null) this.alertBotClicks = alertBotClicks; + public void updateAlerts(Boolean alertClicks, Boolean alertReport) { + if (alertClicks != null) this.alertClicks = alertClicks; if (alertReport != null) this.alertReport = alertReport; } From 644460a7986ff5ff6e074d4c24e0ec69b23f6d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:41:16 +0900 Subject: [PATCH 17/26] =?UTF-8?q?:recycle:=20refactor:=20NotificationType?= =?UTF-8?q?=20enum=20=EA=B0=92=20RAPID=5FCLICKS,=20BOT=5FCLICKS=20->=20CLI?= =?UTF-8?q?CKS=20=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/notification/domain/constant/NotificationType.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 1695a741..e99186b1 100644 --- 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 @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.notification.domain.constant; public enum NotificationType { - RAPID_CLICKS, BOT_CLICKS, REPORT + CLICKS, + REPORT } From 9e71959f3093b40e0a69c4aa0648dbd721dff240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:41:51 +0900 Subject: [PATCH 18/26] =?UTF-8?q?:recycle:=20refactor:=20=EB=B4=87=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD,=20=ED=81=B4=EB=A6=AD=EC=88=98=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20->=20=ED=81=B4=EB=A6=AD=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=9A=94=EC=B2=AD=20&=20=EC=9D=91=EB=8B=B5=20DTO?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/request/NotificationRequest.java | 6 ++---- .../application/dto/response/NotificationResponse.java | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) 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 f96d1ec7..567a088e 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 @@ -22,8 +22,7 @@ public record UpdateChannels( // 알림 기준 설정 DTO -> 각 멤버 public record UpdateAlerts( - Boolean alertRapidClicks, - Boolean alertBotClicks, + Boolean alertClicks, Boolean alertReport ) {} @@ -36,8 +35,7 @@ public record UpdateOrgSettings( Boolean isDiscordEnabled, String discordWebhookUrl, Boolean disconnectDiscord, - Boolean alertRapidClicks, - Boolean alertBotClicks, + Boolean alertClicks, Boolean alertReport ) {} 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 919f29fa..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,11 +12,9 @@ public record MySettings( boolean isSlackConnected, boolean isDiscordEnabled, boolean isDiscordConnected, - boolean alertRapidClicks, - boolean alertBotClicks, + boolean alertClicks, boolean alertReport, - boolean orgAlertRapidClicks, // 조직 외부채널 발송 설정 (ADMIN 설정) - boolean orgAlertBotClicks, + boolean orgAlertClicks, boolean orgAlertReport ) {} From 108a5bbeed6e21c79553b9c401ba02307664785a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:42:08 +0900 Subject: [PATCH 19/26] =?UTF-8?q?:recycle:=20refactor:=20=EB=B4=87=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD,=20=ED=81=B4=EB=A6=AD=EC=88=98=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20->=20=ED=81=B4=EB=A6=AD=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20Converter=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/NotificationConverter.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 70aa9a35..6fdeba92 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,11 +26,9 @@ public static NotificationResponse.MySettings toMySettings( orgSetting != null && orgSetting.hasSlack(), orgSetting != null && orgSetting.isDiscordEnabled(), orgSetting != null && orgSetting.hasDiscord(), - setting.isAlertRapidClicks(), - setting.isAlertBotClicks(), + setting.isAlertClicks(), setting.isAlertReport(), - orgSetting != null && orgSetting.isAlertRapidClicks(), - orgSetting != null && orgSetting.isAlertBotClicks(), + orgSetting != null && orgSetting.isAlertClicks(), orgSetting != null && orgSetting.isAlertReport() ); } @@ -56,8 +54,7 @@ public static OrgMemberNotificationSetting toDefaultMemberSetting(OrgMember memb .isMasterEnabled(true) .isBrowserPushEnabled(false) .isEmailEnabled(true) - .alertRapidClicks(false) - .alertBotClicks(false) + .alertClicks(false) .alertReport(false) // TODO : 리포트 알림은 기본값 true 로 하는게 나을지...? .build(); } From 86ff63c7fa96e23f3f364135ce7f3621aa1f14e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:42:26 +0900 Subject: [PATCH 20/26] =?UTF-8?q?:recycle:=20refactor:=20=EB=B4=87=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD,=20=ED=81=B4=EB=A6=AD=EC=88=98=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20->=20=ED=81=B4=EB=A6=AD=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=ED=95=A9=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20Service=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/NotificationServiceImpl.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 bab3c62c..7bf64578 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 @@ -81,7 +81,7 @@ public void updateChannels(Long userId, Long orgId, NotificationRequest.UpdateCh public void updateAlerts(Long userId, Long orgId, NotificationRequest.UpdateAlerts request) { OrgMember member = findMember(userId, orgId); OrgMemberNotificationSetting setting = findOrCreateSetting(member); - setting.updateAlerts(request.alertRapidClicks(), request.alertBotClicks(), request.alertReport()); + setting.updateAlerts(request.alertClicks(), request.alertReport()); } // 조직 단위 알림 트리거 설정 (ADMIN 전용) — 외부 채널로 내보낼 알림 종류 (클릭 급증 / 봇 클릭 감지 / 주,일간 보고서) 설정 @@ -120,7 +120,7 @@ public void updateOrgSettings(Long userId, Long orgId, NotificationRequest.Updat orgSetting.updateChannelEnabled(request.isSlackEnabled(), request.isDiscordEnabled()); // 외부 채널로 내보낼 알림 종류 설정 - orgSetting.updateAlerts(request.alertRapidClicks(), request.alertBotClicks(), request.alertReport()); + orgSetting.updateAlerts(request.alertClicks(), request.alertReport()); } @Transactional(readOnly = true) @@ -238,9 +238,8 @@ private void sendApiAlarm(OrgNotificationSetting setting, Long orgId, String tit // 알림 종류 입력 받아 실제 조직에서 수신 설정 되어있는지 여부 반환 private boolean isAlertTypeEnabled(OrgNotificationSetting setting, NotificationType type) { return switch (type) { - case RAPID_CLICKS -> setting.isAlertRapidClicks(); - case BOT_CLICKS -> setting.isAlertBotClicks(); - case REPORT -> setting.isAlertReport(); + case CLICKS -> setting.isAlertClicks(); + case REPORT -> setting.isAlertReport(); }; } From fed53b26c346a2a77ff729631f3d01cce5f113d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:42:43 +0900 Subject: [PATCH 21/26] =?UTF-8?q?:art:=20style:=20Swagger=20Docs=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/docs/NotificationControllerDocs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 109d8c09..1f3851b3 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 @@ -49,7 +49,7 @@ ResponseEntity> updateChannels( @RequestBody NotificationRequest.UpdateChannels request ); - @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 알림 트리거(클릭 급증, 봇 클릭 감지, 주·일간 보고서)를 설정합니다.") + @Operation(summary = "알림 목표 설정 변경", description = "비즈니스 알림 트리거(클릭 관련, 주·일간 보고서)를 설정합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "변경 성공"), @ApiResponse(responseCode = "404", description = "해당 조직의 멤버가 아닙니다.") From b1f2c77919c8014ff69e194eaa8797f2831075d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 14:46:32 +0900 Subject: [PATCH 22/26] =?UTF-8?q?:recycle:=20refactor:=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=20true=20=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20Converter=20=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/application/mapper/NotificationConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6fdeba92..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 @@ -55,7 +55,7 @@ public static OrgMemberNotificationSetting toDefaultMemberSetting(OrgMember memb .isBrowserPushEnabled(false) .isEmailEnabled(true) .alertClicks(false) - .alertReport(false) // TODO : 리포트 알림은 기본값 true 로 하는게 나을지...? + .alertReport(true) .build(); } From 92612c246e6194e8b7f5aa5e235a43e43ba0857d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 17:21:07 +0900 Subject: [PATCH 23/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EC=B1=84=EB=84=90=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20DTO=20=EB=82=B4=EB=B6=80=20URL=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/dto/request/NotificationRequest.java | 9 +++++++++ .../presentation/NotificationController.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) 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 567a088e..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 { @@ -30,9 +31,17 @@ public record UpdateAlerts( // 외부 채널 웹훅 연결/활성화 + 외부 채널로 내보낼 알림 종류 설정을 하나의 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, Boolean alertClicks, 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 7308a39c..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 @@ -66,7 +66,7 @@ public ResponseEntity> updateAlerts( public ResponseEntity> updateOrgSettings( @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId, - @RequestBody NotificationRequest.UpdateOrgSettings request + @RequestBody @Valid NotificationRequest.UpdateOrgSettings request ) { notificationService.updateOrgSettings(userId, orgId, request); return ResponseEntity.ok(DataResponse.ok()); From 3432bd5c6b61bd3b8f8d74cb4ed14db15b7a7ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 17:21:27 +0900 Subject: [PATCH 24/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EC=B1=84=EB=84=90=20=EC=9B=B9=ED=9B=85?= =?UTF-8?q?=20URL=20=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/exception/code/NotificationErrorCode.java | 1 + 1 file changed, 1 insertion(+) 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", "해당 작업을 수행할 권한이 없습니다."), From 178580df54725524f5284bb265ccaafab064d000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 17:21:47 +0900 Subject: [PATCH 25/26] =?UTF-8?q?:sparkles:=20feat:=20=EC=99=B8=EB=B6=80?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=EC=B1=84=EB=84=90=20=EC=9B=B9=ED=9B=85?= =?UTF-8?q?=20URL=20=EA=B4=80=EB=A0=A8=20Service=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/NotificationServiceImpl.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 7bf64578..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 @@ -29,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; @@ -102,12 +104,14 @@ public void updateOrgSettings(Long userId, Long orgId, NotificationRequest.Updat 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())); } @@ -235,6 +239,28 @@ 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) { From e1a79561d55ea4ab260991503af40db066d303f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=A4=80=EC=98=81?= Date: Mon, 13 Jul 2026 17:22:08 +0900 Subject: [PATCH 26/26] =?UTF-8?q?:art:=20style:=20Swagger=20Docs=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=EC=BD=94=EB=93=9C=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/docs/NotificationControllerDocs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1f3851b3..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 @@ -65,7 +65,7 @@ ResponseEntity> updateAlerts( "웹훅 URL은 값이 있으면 설정, disconnectSlack/disconnectDiscord=true면 삭제(연결 해제), 둘 다 없으면 변경 없음입니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "변경 성공"), - @ApiResponse(responseCode = "400", description = "NOTIFICATION_400_2: URL 없이 알림 활성화\n\n NOTIFICATION_400_3 : 활성화+연결해제 동시 요청"), + @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 = "해당 조직의 멤버가 아닙니다.") })