Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0146241
chore : FCM 의존성 추가
Daae-Kim Dec 27, 2025
8d45b9c
remove : fcmEntity 삭제
Daae-Kim Dec 29, 2025
887c07e
feat : memberPushTokenEntity, model 생성
Daae-Kim Dec 29, 2025
c0e3f7d
feat : 알림 provider enum 생성, 제공자 NotFound 예외 추가
Daae-Kim Dec 29, 2025
1073813
feat : 알림토큰 삭제 권한, not found 예외 작성
Daae-Kim Jan 4, 2026
2afc1cc
feat : request dto, controller, swagger 구현
Daae-Kim Jan 4, 2026
4f5f7ff
feat : usecase, model, converter, service 구현
Daae-Kim Jan 4, 2026
8ddd568
feat : entity, repository, jpaRepository 구현
Daae-Kim Jan 4, 2026
aa4fcd5
chore : notification api security 체인 등록
Daae-Kim Jan 4, 2026
b7c8b39
chore : spotless apply
Daae-Kim Jan 4, 2026
82642b7
refactor : NotBlank 사용
Daae-Kim Jan 4, 2026
4b61f34
refactor : 불필요한 component 삭제
Daae-Kim Jan 4, 2026
bb95894
refactor : status, 에러메세지 수정
Daae-Kim Jan 4, 2026
8890a5f
refactor : unique 제약조건 추가
Daae-Kim Jan 4, 2026
412629b
refactor : 패키지 오타 수정
Daae-Kim Jan 4, 2026
ae39fe3
refactor : 패키지 오타 수정
Daae-Kim Jan 4, 2026
df6033f
refactor : sl4j 삭제, delete 반환값 void 변경
Daae-Kim Jan 4, 2026
711cc76
chore : spotlessApply
Daae-Kim Jan 4, 2026
99acb17
feat : lastActiveAt 필드 추가
Daae-Kim Jan 5, 2026
ee5b4d7
feat : 토큰 생성 로직 수정
Daae-Kim Jan 5, 2026
f568b5e
chore : spotlessApply
Daae-Kim Jan 5, 2026
a6a5df2
refactor : 중복 토큰 조회 메서드 수정
Daae-Kim Jan 5, 2026
17bafe1
chore : spotlessApply
Daae-Kim Jan 5, 2026
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
4 changes: 4 additions & 0 deletions eeos/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ dependencies {

// OpenFeign
implementation(libs.spring.cloud.starter.openfeign)

// Firebase Admin SDK
implementation(libs.firebase.admin)

}

dependencyManagement {
Expand Down
4 changes: 4 additions & 0 deletions eeos/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ spotless = "7.2.1"
asciidoctor = "4.0.2"
epages-restdocs = "0.18.2"
swagger = "5.10.3"
firebase-admin = "9.2.0"

# Security
jwt = "0.12.6"
Expand Down Expand Up @@ -57,6 +58,9 @@ jbcrypt = { group = "org.mindrot", name = "jbcrypt", version.ref = "jbcrypt" }
# Lombok
lombok = { group = "org.projectlombok", name = "lombok" }

# Firebase Admin SDK
firebase-admin = { group = "com.google.firebase", name = "firebase-admin", version.ref = "firebase-admin" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ SecurityFilterChain authenticated(HttpSecurity httpSecurity) throws Exception {
.requestMatchers("/api/admin/**")
.requestMatchers("/api/team-building/**")
.requestMatchers("/api/semester-periods/**")
.requestMatchers("/api/calendars/**");
.requestMatchers("/api/calendars/**")
.requestMatchers("/api/pushToken", "/api/pushToken/**");
});

httpSecurity.authorizeHttpRequests(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.blackcompany.eeos.notification.application.dto;

import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class CreateMemberPushTokenRequest {
@NotNull private String pushToken;
@NotNull private String provider;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.blackcompany.eeos.notification.application.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class DeleteMemberPushTokenRequest {
@NotBlank private String pushToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.blackcompany.eeos.notification.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class DeniedDeletePushTokenException extends BusinessException {
private static final String FAIL_CODE = "5008";

public DeniedDeletePushTokenException() {
super(FAIL_CODE, HttpStatus.UNAUTHORIZED);
}

@Override
public String getMessage() {
return "토큰 수정 권한이 없습니다.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.blackcompany.eeos.notification.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class NotFoundNotificationProviderException extends BusinessException {

private static final String FAIL_CODE = "5006";
private final String provider;

public NotFoundNotificationProviderException(String provider) {
super(FAIL_CODE, HttpStatus.NOT_FOUND);
this.provider = provider;
}

@Override
public String getMessage() {
return String.format("%s 는 존재하지 않는 외부 제공자 입니다.", provider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.blackcompany.eeos.notification.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class NotFoundPushTokenException extends BusinessException {

private static final String FAIL_CODE = "5007";
private final String pushToken;

public NotFoundPushTokenException(String pushToken) {

super(FAIL_CODE, HttpStatus.NOT_FOUND);
this.pushToken = pushToken;
}

@Override
public String getMessage() {
return String.format("%s 는 존재하지 않는 알림토큰 입니다.", pushToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.blackcompany.eeos.notification.application.model;

import com.blackcompany.eeos.common.support.AbstractModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberPushTokenModel implements AbstractModel {
private Long id;
private Long memberId;
private NotificationProvider notificationProvider;
private String pushToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.blackcompany.eeos.notification.application.model;

import com.blackcompany.eeos.notification.application.exception.NotFoundNotificationProviderException;
import java.util.Arrays;
import lombok.Getter;

@Getter
public enum NotificationProvider {
FCM("fcm"),
ETC("etc");

private final String provider;

NotificationProvider(String provider) {
this.provider = provider;
}

public static NotificationProvider find(String name) {
return Arrays.stream(NotificationProvider.values())
.filter(provider -> provider.name().equalsIgnoreCase(name))
.findFirst()
.orElseThrow(() -> new NotFoundNotificationProviderException(name));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.blackcompany.eeos.notification.application.model.converter;

import com.blackcompany.eeos.common.support.converter.AbstractEntityConverter;
import com.blackcompany.eeos.notification.application.model.MemberPushTokenModel;
import com.blackcompany.eeos.notification.persistence.MemberPushTokenEntity;
import org.springframework.stereotype.Component;

@Component
public class MemberPushTokenEntityConverter
implements AbstractEntityConverter<MemberPushTokenEntity, MemberPushTokenModel> {

@Override
public MemberPushTokenModel from(MemberPushTokenEntity source) {
return MemberPushTokenModel.builder()
.id(source.getId())
.memberId(source.getMemberId())
.notificationProvider(source.getProvider())
.pushToken(source.getPushToken())
.build();
}

@Override
public MemberPushTokenEntity toEntity(MemberPushTokenModel source) {
return MemberPushTokenEntity.builder()
.id(source.getId())
.memberId(source.getMemberId())
.pushToken(source.getPushToken())
.provider(source.getNotificationProvider())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.blackcompany.eeos.notification.application.respository;

import com.blackcompany.eeos.notification.application.model.MemberPushTokenModel;
import com.blackcompany.eeos.notification.application.model.NotificationProvider;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface MemberPushTokenRepository {
Optional<MemberPushTokenModel> findByPushToken(String pushToken);

List<MemberPushTokenModel> findByMemberId(Long memberId);

List<MemberPushTokenModel> findByMemberIdAndProvider(
Long memberId, NotificationProvider provider);

void deleteByPushToken(String pushToken);

int deleteByMemberId(Long memberId);

int deleteByUpdatedDateBefore(LocalDateTime updatedDate);

MemberPushTokenModel save(MemberPushTokenModel memberPushToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.blackcompany.eeos.notification.application.service;

import com.blackcompany.eeos.notification.application.dto.CreateMemberPushTokenRequest;
import com.blackcompany.eeos.notification.application.dto.DeleteMemberPushTokenRequest;
import com.blackcompany.eeos.notification.application.exception.DeniedDeletePushTokenException;
import com.blackcompany.eeos.notification.application.exception.NotFoundPushTokenException;
import com.blackcompany.eeos.notification.application.model.MemberPushTokenModel;
import com.blackcompany.eeos.notification.application.model.NotificationProvider;
import com.blackcompany.eeos.notification.application.respository.MemberPushTokenRepository;
import com.blackcompany.eeos.notification.application.usecase.CreateMemberPushTokenUsecase;
import com.blackcompany.eeos.notification.application.usecase.DeleteAllMemberPushTokensUsecase;
import com.blackcompany.eeos.notification.application.usecase.DeleteMemberPushTokenUsecase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class NotificationTokenService
implements CreateMemberPushTokenUsecase,
DeleteAllMemberPushTokensUsecase,
DeleteMemberPushTokenUsecase {

private final MemberPushTokenRepository memberPushTokenRepository;

@Override
@Transactional
public void create(Long memberId, CreateMemberPushTokenRequest request) {

NotificationProvider provider = NotificationProvider.find(request.getProvider());
if (memberPushTokenRepository.findByPushToken(request.getPushToken()).isEmpty()) {
MemberPushTokenModel model =
MemberPushTokenModel.builder()
.memberId(memberId)
.pushToken(request.getPushToken())
.notificationProvider(provider)
.build();

memberPushTokenRepository.save(model);
}
}

@Override
@Transactional
public void deleteAllMemberPushTokens(Long memberId) {
memberPushTokenRepository.deleteByMemberId(memberId);
}

@Override
@Transactional
public void delete(Long memberId, DeleteMemberPushTokenRequest request) {
MemberPushTokenModel model =
memberPushTokenRepository
.findByPushToken(request.getPushToken())
.orElseThrow(() -> new NotFoundPushTokenException(request.getPushToken()));
if (!model.getMemberId().equals(memberId)) {
throw new DeniedDeletePushTokenException();
}
memberPushTokenRepository.deleteByPushToken(request.getPushToken());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.blackcompany.eeos.notification.application.usecase;

import com.blackcompany.eeos.notification.application.dto.CreateMemberPushTokenRequest;
import org.springframework.stereotype.Component;

@Component
public interface CreateMemberPushTokenUsecase {
void create(Long memberId, CreateMemberPushTokenRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.blackcompany.eeos.notification.application.usecase;

import org.springframework.stereotype.Component;

@Component
public interface DeleteAllMemberPushTokensUsecase {
void deleteAllMemberPushTokens(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.blackcompany.eeos.notification.application.usecase;

import com.blackcompany.eeos.notification.application.dto.DeleteMemberPushTokenRequest;
import org.springframework.stereotype.Component;

@Component
public interface DeleteMemberPushTokenUsecase {
void delete(Long memberId, DeleteMemberPushTokenRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.blackcompany.eeos.notification.persistence;

import com.blackcompany.eeos.notification.application.model.NotificationProvider;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface JpaMemberPushTokenRepository extends JpaRepository<MemberPushTokenEntity, Long> {

Optional<MemberPushTokenEntity> findByPushToken(String token);

List<MemberPushTokenEntity> findByMemberId(Long memberId);

List<MemberPushTokenEntity> findByMemberIdAndProvider(
Long memberId, NotificationProvider provider);

@Modifying
@Query("DELETE FROM MemberPushTokenEntity t WHERE t.memberId = :memberId")
int deleteByMemberId(@Param("memberId") Long memberId);

void deleteByPushToken(String token);

int deleteByUpdatedDateBefore(Timestamp updatedDate);
}
Loading