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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.notification.application.dto.request;

public class NotificationRequest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.notification.application.dto.response;

public class NotificationResponse {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.notification.application.mapper;

public class NotificationConverter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.whereyouad.WhereYouAd.domains.notification.domain.constant;

public enum DeliveryChannel {
EMAIL,
SLACK,
DISCORD,
BROWSER_PUSH,
IN_APP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.notification.domain.constant;

public enum DeliveryStatus {
PENDING,
SUCCESS,
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.notification.domain.service;

public interface NotificationService {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.whereyouad.WhereYouAd.domains.notification.domain.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class NotificationServiceImpl implements NotificationService {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.whereyouad.WhereYouAd.domains.notification.exception;

import com.whereyouad.WhereYouAd.global.exception.AppException;
import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;

public class NotificationException extends AppException {
public NotificationException(BaseErrorCode code) {
super(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.whereyouad.WhereYouAd.domains.notification.exception.code;

import com.whereyouad.WhereYouAd.global.exception.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum NotificationErrorCode implements BaseErrorCode {

;

private final HttpStatus httpStatus;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.entity;

import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Table(name = "notification")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notification_id")
private Long id;

@Column(name = "title", nullable = false, length = 200)
private String title;

@Column(name = "message", nullable = false, length = 2000)
private String message;

@Column(name = "link_url", length = 1024)
private String linkUrl;

@Column(name = "metadata_json", length = 4000)
private String metadataJson;

@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;

// 연관 관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id", nullable = false)
private Organization organization;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.entity;

import com.whereyouad.WhereYouAd.domains.notification.domain.constant.DeliveryChannel;
import com.whereyouad.WhereYouAd.domains.notification.domain.constant.DeliveryStatus;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Table(name = "notification_delivery")
@Getter
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

중복 배송 레코드 방지를 위한 유니크 제약이 필요합니다.

현재 테이블 정의에는 동일 대상/채널 조합 중복을 막는 제약이 없어, 동시성 상황에서 동일 알림이 여러 건 생성될 수 있습니다. 재시도 카운트 기반 설계라면 DB 레벨 유니크 키로 보호하는 게 맞습니다.

제안 diff
-@Table(name = "notification_delivery")
+@Table(
+    name = "notification_delivery",
+    uniqueConstraints = {
+        `@UniqueConstraint`(
+            name = "uk_notification_delivery_target_channel",
+            columnNames = {"notification_id", "user_id", "channel"}
+        )
+    }
+)

membership_id로 전환한다면 해당 컬럼으로 제약 컬럼도 함께 바꿔주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/NotificationDelivery.java`
around lines 13 - 14, The NotificationDelivery entity lacks a unique constraint
to prevent duplicate delivery records for the same target/channel combination,
which can cause multiple identical notifications to be created in concurrent
scenarios. Add a unique constraint to the `@Table` annotation for the
NotificationDelivery class that enforces uniqueness on the appropriate column
combination (such as membership_id or the target/channel fields) to protect
against concurrent duplicate insertions at the database level.

@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class NotificationDelivery extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "delivery_id")
private Long id;

@Enumerated(EnumType.STRING)
@Column(name = "channel", nullable = false, length = 20)
private DeliveryChannel channel;

@Builder.Default
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 10)
private DeliveryStatus status = DeliveryStatus.PENDING;

@Column(name = "sent_at")
private LocalDateTime sentAt;

@Column(name = "failed_at")
private LocalDateTime failedAt;

@Column(name = "failure_reason", length = 500)
private String failureReason;

@Builder.Default
@Column(name = "retry_count", nullable = false)
private int retryCount = 0;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "notification_id", nullable = false)
private Notification notification;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membership_id", nullable = false)
private OrgMember orgMember;

public void markSuccess() {
this.status = DeliveryStatus.SUCCESS;
this.sentAt = LocalDateTime.now();
}

public void markFailed(String reason) {
this.status = DeliveryStatus.FAILED;
this.failedAt = LocalDateTime.now();
this.failureReason = reason;
this.retryCount++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.entity;

import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "org_member_notification_setting")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrgMemberNotificationSetting extends BaseEntity {

@Id
@Column(name = "membership_id")
private Long membershipId;

@OneToOne(fetch = FetchType.LAZY)
@MapsId // PK == FK(식별 관계)
@JoinColumn(name = "membership_id")
private OrgMember orgMember;

@Column(name = "is_master_enabled", nullable = false)
private boolean isMasterEnabled;

@Column(name = "is_browser_push_enabled", nullable = false)
private boolean isBrowserPushEnabled;

@Column(name = "is_email_enabled", nullable = false)
private boolean isEmailEnabled;

@Column(name = "is_slack_enabled", nullable = false)
private boolean isSlackEnabled;

@Column(name = "is_discord_enabled", nullable = false)
private boolean isDiscordEnabled;

@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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.entity;

import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import com.whereyouad.WhereYouAd.global.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "org_notification_setting")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrgNotificationSetting extends BaseEntity {

@Id
@Column(name = "org_id")
private Long orgId;

@OneToOne(fetch = FetchType.LAZY)
@MapsId // PK == FK(식별 관계)
@JoinColumn(name = "org_id")
private Organization organization;

@Column(name = "slack_webhook_url", length = 512)
private String slackWebhookUrl;

@Column(name = "discord_webhook_url", length = 512)
private String discordWebhookUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.entity;

import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;

import java.time.LocalDateTime;

@Entity
@Table(name = "user_notification")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserNotification {
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_notification_id")
private Long id;

@ColumnDefault("false")
@Column(name = "is_read", nullable = false)
private boolean isRead;
Comment on lines +23 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

@Builder 사용 시 boolean 필드 기본값 설정이 필요합니다

@ColumnDefault("false")는 DDL(테이블 생성 SQL)에만 영향을 주고, Java 객체를 @Builder로 생성할 때는 적용되지 않습니다.

문제 상황:

UserNotification notification = UserNotification.builder()
    .notification(notif)
    .user(user)
    .build();
// isRead는 false가 아니라 Java의 primitive boolean 기본값 false가 됨
// 하지만 Builder 패턴 사용 시 명시적으로 설정하지 않으면 의도가 불명확함

해결 방법:
@Builder.Default를 함께 사용하여 Builder 패턴에서도 기본값이 명확히 설정되도록 해주세요.

🛠️ 수정 제안
+import lombok.Builder.Default;
+
 `@ColumnDefault`("false")
 `@Column`(name = "is_read", nullable = false)
+@Builder.Default
 private boolean isRead = false;

이렇게 하면:

  • DDL 생성 시: DEFAULT false 적용 ✓
  • Builder 사용 시: 명시하지 않으면 false 적용 ✓
  • 코드 가독성: 기본값이 false임을 명확히 표현 ✓
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/notification/persistence/entity/UserNotification.java`
around lines 23 - 25, The isRead field in the UserNotification entity class
needs an explicit default value for the Builder pattern. The `@ColumnDefault`
annotation only affects DDL generation and does not apply when objects are
created using `@Builder`. Add the `@Builder.Default` annotation to the isRead field
alongside the existing `@ColumnDefault` annotation to ensure the default value of
false is applied both when the database table is created and when
UserNotification objects are instantiated through the Builder pattern without
explicitly setting the isRead field.

Source: Coding guidelines


@Column(name = "read_at")
private LocalDateTime readAt;

// 연관관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "notification_id", nullable = false)
private Notification notification;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.repository;

import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.NotificationDelivery;
import org.springframework.data.jpa.repository.JpaRepository;


public interface NotificationDeliveryRepository extends JpaRepository<NotificationDelivery, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.repository;

import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.Notification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NotificationRepository extends JpaRepository<Notification, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.repository;

import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.OrgMemberNotificationSetting;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrgMemberNotificationSettingRepository extends JpaRepository<OrgMemberNotificationSetting, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.repository;

import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.OrgNotificationSetting;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrgNotificationSettingRepository extends JpaRepository<OrgNotificationSetting, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.whereyouad.WhereYouAd.domains.notification.persistence.repository;

import com.whereyouad.WhereYouAd.domains.notification.persistence.entity.UserNotification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserNotificationRepository extends JpaRepository<UserNotification, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.whereyouad.WhereYouAd.domains.notification.presentation;

import com.whereyouad.WhereYouAd.domains.notification.presentation.docs.NotificationControllerDocs;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/notification")
@RequiredArgsConstructor
public class NotificationController implements NotificationControllerDocs {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.whereyouad.WhereYouAd.domains.notification.presentation.docs;

public interface NotificationControllerDocs {
}
Loading