Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1045338
feat: FCM ์„œ๋ฒ„ ์ดˆ๊ธฐํ™” ๋ฐ ์—ฐ๊ฒฐ
Dec 10, 2025
316bff2
feat: Fcm, APNS Config ์„ค์ •
Dec 10, 2025
7c0393e
feat: FCM Token ๊ด€๋ฆฌ ๊ธฐ๋Šฅ
Dec 10, 2025
00db9fb
feat: ์•Œ๋ฆผ ์š”์ฒญ Dto
Dec 10, 2025
09e2908
feat: FCM์„œ๋ฒ„๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
Dec 10, 2025
14f4ac8
chore: ์—๋Ÿฌ ์ฝ”๋“œ ์ •๋ฆฌ, ์ถ”๊ฐ€
Dec 10, 2025
8ae5ce7
feat: notification์— ์•Œ๋ฆผ ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ก
Dec 10, 2025
57cf92c
feat: Facade๋กœ ๋ถ„๋ฆฌํ•ด ์„œ๋น„์Šค ์ฒ˜๋ฆฌ
Dec 10, 2025
6a83c21
feat: ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ๋กœ์ง
Dec 10, 2025
4005cbc
feat: ์•Œ๋ฆผ ํƒ€์ž… Enum
Dec 10, 2025
afb29c2
chore: ํŒจํ‚ค์ง€ ์ด๋ฆ„ ๋ณ€๊ฒฝ
Dec 10, 2025
949c2ec
chore: dto ์ •๋ฆฌ
Dec 10, 2025
b63056d
feat: party ์•Œ๋ฆผ ๊ธฐ๋Šฅ
Dec 11, 2025
4e39954
feat: chat ์•Œ๋ฆผ ๊ธฐ๋Šฅ, title๋„ ์•Œ๋ฆผ ํ…Œ์ด๋ธ”์— ํฌํ•จ๋˜๊ฒŒ ์ˆ˜์ •
Dec 11, 2025
f5e4606
chore: ํ—ฌํผ ๋ฉ”์„œ๋“œ ์ค‘๋ณต ๋ถ„๋ฆฌ
Dec 11, 2025
13dddd2
feat: FCM ์‘๋‹ต ๊ธฐ๋ฐ˜ ํ† ํฐ ๋น„ํ™œ์„ฑํ™” ์ฒ˜๋ฆฌ
Dec 11, 2025
c9d1c76
feat: ๋น„ํ™œ์„ฑํ† ํฐ ์Šค์ผ€์ค„๋Ÿฌ ์‚ญ์ œ ์ฒ˜๋ฆฌ
Dec 11, 2025
6438f78
chore: ํŒจํ‚ค์ง€ ๊ตฌ์กฐ ์ •๋ฆฌ
Dec 11, 2025
ddb6905
chore: fcm ํ™˜๊ฒฝ ์„ค์ •
Dec 11, 2025
3d75048
fix: ๋ฆฌ๋ทฐ ๋ฐ˜์˜
Dec 11, 2025
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {

// sms
implementation 'com.solapi:sdk:1.0.3'

// firebase
implementation 'com.google.firebase:firebase-admin:9.1.1'
}

tasks.named('test') {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ita/tinybite/TinyBiteApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
public class TinyBiteApplication {
public static void main(String[] args) {
SpringApplication.run(TinyBiteApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ita.tinybite.domain.notification.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import ita.tinybite.domain.notification.dto.request.FcmTokenRequest;
import ita.tinybite.domain.notification.service.FcmTokenService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/fcm")
public class FcmTokenController {

private final FcmTokenService fcmTokenService;

// token ์ด๋ฏธ ์กด์žฌ ์‹œ ์—…๋ฐ์ดํŠธ ํ•ด์คŒ
@PostMapping("/token")
public ResponseEntity<Void> registerToken(@RequestBody @Valid FcmTokenRequest request,
@RequestHeader(name = "User-ID") Long currentUserId) {
fcmTokenService.saveOrUpdateToken(currentUserId, request.token());
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ita.tinybite.domain.notification.converter;

import org.springframework.stereotype.Component;

import ita.tinybite.domain.notification.entity.Notification;
import ita.tinybite.domain.notification.enums.NotificationType;

@Component
public class NotificationLogConverter {

public Notification toEntity(Long targetUserId, String type, String title,String detail) {

NotificationType notificationType = NotificationType.valueOf(type);

return Notification.builder()
.userId(targetUserId)
.notificationType(notificationType)
.notificationTitle(title)
.notificationDetail(detail)
.isRead(false)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ita.tinybite.domain.notification.converter;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Component;

import ita.tinybite.domain.notification.dto.request.NotificationMulticastRequest;

@Component
public class NotificationRequestConverter {

public NotificationMulticastRequest toMulticastRequest(
List<String> tokens,
String title,
String body,
Map<String, String> data) {

return NotificationMulticastRequest.builder()
.tokens(tokens)
.title(title)
.body(body)
.data(data)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ita.tinybite.domain.notification.dto.request;

import jakarta.validation.constraints.NotNull;

public record FcmTokenRequest(
@NotNull(message = "FCM ํ† ํฐ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.")
String token
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ita.tinybite.domain.notification.dto.request;

import java.util.List;
import java.util.Map;

import com.google.firebase.internal.NonNull;
import com.google.firebase.messaging.MulticastMessage;
import com.google.firebase.messaging.Notification;

import lombok.Builder;

@Builder
public record NotificationMulticastRequest(
@NonNull List<String> tokens,
String title,
String body,
Map<String, String> data
) implements NotificationRequest {

public MulticastMessage.Builder buildSendMessage() {
MulticastMessage.Builder builder = MulticastMessage.builder()
.setNotification(toNotification())
.addAllTokens(tokens);

if (this.data != null && !this.data.isEmpty()) {
builder.putAllData(this.data);
}

return builder;
}

public Notification toNotification() {
return Notification.builder()
.setTitle(title)
.setBody(body)
.build();
}

@Override
public Map<String, String> data() {
return this.data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ita.tinybite.domain.notification.dto.request;

import java.util.Map;

import com.google.firebase.messaging.Notification;

public interface NotificationRequest {
String title();
String body();
Notification toNotification();
Map<String, String> data();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ita.tinybite.domain.notification.dto.request;

import java.util.Map;

import com.google.firebase.internal.NonNull;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;

import lombok.Builder;

@Builder
public record NotificationSingleRequest(
@NonNull String token,
String title,
String body,
Map<String, String> data
) implements NotificationRequest {

public Message.Builder buildMessage() {
Message.Builder builder = Message.builder()
.setToken(token)
.setNotification(toNotification());

if (this.data != null && !this.data.isEmpty()) {
builder.putAllData(this.data);
}

return builder;
}

public Notification toNotification() {
return Notification.builder()
.setTitle(title)
.setBody(body)
.build();
}

@Override
public Map<String, String> data() {
return this.data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ita.tinybite.domain.notification.entity;

import ita.tinybite.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "token", nullable = false)
private String token;

@Builder.Default
@Column(name = "is_active", nullable = false)
private Boolean isActive = Boolean.TRUE;

public void updateToken(String newToken) {
this.token = newToken;
this.isActive = Boolean.TRUE;
}
Comment on lines +38 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๐Ÿ› ๏ธ Refactor suggestion | ๐ŸŸ  Major

ํ† ํฐ ๋น„ํ™œ์„ฑํ™”๋ฅผ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

updateToken์€ ํ† ํฐ์„ ํ•ญ์ƒ ํ™œ์„ฑํ™”ํ•˜์ง€๋งŒ, ๋ฐ˜๋Œ€ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. PR ์„ค๋ช…์— ๋”ฐ๋ฅด๋ฉด NotificationTransactionHelper๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์„ ๋น„ํ™œ์„ฑํ™”ํ•œ๋‹ค๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ์—”ํ‹ฐํ‹ฐ์— ๋ช…์‹œ์ ์ธ ๋น„ํ™œ์„ฑํ™” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

 	public void updateToken(String newToken) {
 		this.token = newToken;
 		this.isActive = Boolean.TRUE;
 	}
+
+	public void markInactive() {
+		this.isActive = Boolean.FALSE;
+	}
๐Ÿค– Prompt for AI Agents
In src/main/java/ita/tinybite/domain/notification/entity/FcmToken.java around
lines 38 to 41, add an explicit method to deactivate the token because
updateToken always activates it; implement a public deactivateToken() (or
markInactive()) that sets isActive = Boolean.FALSE (and optionally clears or
retains token per domain needs), and use this method from
NotificationTransactionHelper when marking tokens invalid.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ita.tinybite.domain.notification.entity;

import ita.tinybite.domain.notification.enums.NotificationType;
import ita.tinybite.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

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

@Column(name = "user_id", nullable = false)
private Long userId;

@Enumerated(EnumType.STRING)
@Column(name = "notification_type", nullable = false)
private NotificationType notificationType;

@Column(name = "notification_title", nullable = false)
private String notificationTitle;

@Column(name = "notification_detail", columnDefinition = "TEXT")
private String notificationDetail;

@Builder.Default
@Column(name = "is_read", nullable = false)
private Boolean isRead = Boolean.FALSE;

public void markAsRead() {
if (Boolean.FALSE.equals(this.isRead)) {
this.isRead = Boolean.TRUE;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ita.tinybite.domain.notification.enums;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public enum NotificationType {
// ์ฑ„ํŒ…
CHAT_NEW_MESSAGE,
CHAT_UNREAD_REMINDER,

// ํŒŒํ‹ฐ ์ฐธ์—ฌ
PARTY_APPROVAL,
PARTY_REJECTION,
PARTY_AUTO_CLOSE,
PARTY_ORDER_COMPLETE,
PARTY_DELIVERY_REMINDER,
PARTY_COMPLETE,

// ํŒŒํ‹ฐ ์šด์˜
PARTY_NEW_REQUEST,
PARTY_MEMBER_LEAVE,
PARTY_MANAGER_DELIVERY_REMINDER,

// ๋งˆ์ผ€ํŒ… ์•Œ๋ฆผ
MARKETING_LOCAL_NEW_PARTY,
MARKETING_WEEKLY_POPULAR,
MARKETING_PROMOTION_EVENT;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ita.tinybite.domain.notification.infra.creator;

import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class APNsConfigCreator {

public static ApnsConfig createDefaultConfig() {
return ApnsConfig.builder()
.setAps(Aps.builder()
.setSound("default")
.setBadge(1)
.build()
)
.build();
}

// ์ด๋ฒคํŠธ๋ณ„๋กœ ๋™์ ์ธ ๋ฑƒ์ง€ ์ˆซ์ž ์„ค์ •
public static ApnsConfig createConfigWithBadge(int badgeCount) {
return ApnsConfig.builder()
.setAps(Aps.builder()
.setSound("default")
.setBadge(badgeCount)
.build()
)
.build();
}
}
Loading