Skip to content
Merged
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ dependencies {
//HTTP 클라이언트
implementation 'com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3'

// Firebase Admin SDK
implementation 'com.google.firebase:firebase-admin:9.7.0'

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,32 @@ public boolean hasAccess(User user, StudentCouncil writer) {
return user.getSchool() != null &&
user.getSchool().getSchoolId().equals(writer.getSchool().getSchoolId());
}

@Override public String topic(StudentCouncil writer) {
return "school_" + writer.getSchool().getSchoolId();
}
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find StudentCouncil entity file
fd -t f "StudentCouncil.java" --type f

Repository: our-campUS/our-campUS-BE

Length of output: 148


🏁 Script executed:

# Find and examine CouncilType.java to get full context
fd -t f "CouncilType.java" --type f

Repository: our-campUS/our-campUS-BE

Length of output: 145


🏁 Script executed:

# Search for StudentCouncil class definition and its fields
rg -nP --type=java "class\s+StudentCouncil" -A 30

Repository: our-campUS/our-campUS-BE

Length of output: 50380


🏁 Script executed:

cat -n src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java

Repository: our-campUS/our-campUS-BE

Length of output: 1615


🏁 Script executed:

# Search for hasAccess method to see how it handles null checks
rg -nP "hasAccess" --type=java -B2 -A5

Repository: our-campUS/our-campUS-BE

Length of output: 4230


🏁 Script executed:

# Check if @ManyToOne on school field has explicit optional parameter
rg -nP "@ManyToOne|@JoinColumn.*school" src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java -A1

Repository: our-campUS/our-campUS-BE

Length of output: 266


🏁 Script executed:

# Find where topic() method is called
rg -nP "\.topic\(" --type=java -B2 -A2

Repository: our-campUS/our-campUS-BE

Length of output: 1682


🏁 Script executed:

# Check StudentCouncil entity to see if college and major have nullable constraints
rg -nP "@JoinColumn.*college|@JoinColumn.*major" src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java -B1 -A1

Repository: our-campUS/our-campUS-BE

Length of output: 273


필드의 null 안전성 검증 필요 (college, major의 경우)

StudentCouncil의 school 필드는 데이터베이스 레벨에서 nullable = false 제약이 있어 SCHOOL_COUNCIL의 topic() 메서드는 즉시적인 NPE 위험이 낮습니다. 하지만 collegemajor 필드는 nullable이 기본값이므로 COLLEGE_COUNCIL과 MAJOR_COUNCIL의 topic() 메서드는 null 체크 없이 이들 필드에 접근하고 있습니다.

또한 hasAccess() 메서드는 user의 필드만 null 검증하고 writer의 필드는 검증하지 않으므로, 방어적 프로그래밍 관점에서 topic() 메서드에서도 일관되게 null 체크를 수행하는 것이 좋습니다. 특히 lazy-loading 환경에서 예상치 못한 상황이 발생할 수 있습니다.

모든 council type의 topic() 메서드에 명시적인 null 체크를 추가하여 일관성과 안정성을 확보하기를 권장합니다.

🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 13 - 15, The topic() implementations for COLLEGE_COUNCIL and
MAJOR_COUNCIL access StudentCouncil.getCollege() and getMajor() without null
checks; add explicit null-safety in each CouncilType.topic(StudentCouncil
writer) (and mirror the defensive style of hasAccess()) by validating writer and
the specific field (college/major) before using them — if null, either throw a
clear IllegalStateException/IllegalArgumentException with context (e.g.,
"College is null for council topic") or return a sensible fallback topic string;
ensure SCHOOL_COUNCIL/topic still assumes non-null school but optionally add an
assertion for consistency.

},
COLLEGE_COUNCIL {
@Override
public boolean hasAccess(User user, StudentCouncil writer) {
return user.getCollege() != null &&
user.getCollege().getCollegeId().equals(writer.getCollege().getCollegeId());
}
@Override public String topic(StudentCouncil writer) {
return "college_" + writer.getCollege().getCollegeId();
}
Comment on lines +23 to +25
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Null 안전성 검증 필요

writer.getCollege()이 null일 경우 NPE가 발생할 수 있습니다. SCHOOL_COUNCIL과 동일한 null 안전성 문제가 있습니다.

🛡️ Null 안전성을 위한 수정안
 @Override public String topic(StudentCouncil writer) {
+	if (writer.getCollege() == null) {
+		throw new IllegalStateException("COLLEGE_COUNCIL writer must have a college");
+	}
 	return "college_" + writer.getCollege().getCollegeId();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override public String topic(StudentCouncil writer) {
return "college_" + writer.getCollege().getCollegeId();
}
@Override public String topic(StudentCouncil writer) {
if (writer.getCollege() == null) {
throw new IllegalStateException("COLLEGE_COUNCIL writer must have a college");
}
return "college_" + writer.getCollege().getCollegeId();
}
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 23 - 25, CouncilType.topic 메서드에서 writer.getCollege()가 null일 경우 NPE가
발생하므로 writer.getCollege()와 관련된 null 체크를 추가해 SCHOOL_COUNCIL과 동일한 방식으로 처리하세요; 즉
CouncilType.topic(…)에서 writer.getCollege()가 null이면 적절한 예외(예:
IllegalStateException/IllegalArgumentException) 또는 기본 토픽 문자열을 반환하도록 방어 코드를 넣고
로그/메시지에 writer 식별자와 함께 상태를 명시하도록 수정하세요.

},
MAJOR_COUNCIL {
@Override
public boolean hasAccess(User user, StudentCouncil writer) {
return user.getMajor() != null &&
user.getMajor().getMajorId().equals(writer.getMajor().getMajorId());
}
@Override public String topic(StudentCouncil writer) {
return "major_" + writer.getMajor().getMajorId();
}
Comment on lines +33 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Null 안전성 검증 필요

writer.getMajor()가 null일 경우 NPE가 발생할 수 있습니다. SCHOOL_COUNCIL, COLLEGE_COUNCIL과 동일한 null 안전성 문제가 있습니다.

🛡️ Null 안전성을 위한 수정안
 @Override public String topic(StudentCouncil writer) {
+	if (writer.getMajor() == null) {
+		throw new IllegalStateException("MAJOR_COUNCIL writer must have a major");
+	}
 	return "major_" + writer.getMajor().getMajorId();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override public String topic(StudentCouncil writer) {
return "major_" + writer.getMajor().getMajorId();
}
@Override public String topic(StudentCouncil writer) {
if (writer.getMajor() == null) {
throw new IllegalStateException("MAJOR_COUNCIL writer must have a major");
}
return "major_" + writer.getMajor().getMajorId();
}
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 33 - 35, The topic method in CouncilType (topic(StudentCouncil
writer)) calls writer.getMajor().getMajorId() without null checks which can NPE;
update topic to first check for null writer.getMajor() (and null majorId) and
return a safe fallback (e.g., "major_unknown" or an empty topic) when null, and
apply the same null-safety fixes to the SCHOOL_COUNCIL and COLLEGE_COUNCIL
branches so none of them dereference getMajor() or getMajorId() without guards.

};

public abstract boolean hasAccess(User user, StudentCouncil writer);
public abstract String topic(StudentCouncil writer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.campus.campus.domain.councilpost.application;

import java.util.Map;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import com.campus.campus.domain.councilpost.application.dto.request.CouncilPostCreatedEvent;
import com.campus.campus.domain.councilpost.domain.entity.PostCategory;
import com.campus.campus.global.firebase.application.service.FirebaseCloudMessageService;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class CouncilPostPushListener {

private static final String DATA_KEY_TYPE = "type";
private static final String DATA_TYPE_COUNCIL_POST_CREATED = "COUNCIL_POST_CREATED";
private static final String DATA_KEY_POST_ID = "postId";
private static final String DATA_KEY_CATEGORY = "category";

private final FirebaseCloudMessageService firebaseCloudMessageService;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleCouncilPostCreatedEvent(CouncilPostCreatedEvent event) {

String title = event.councilName();
String body = resolveBody(event.category());

log.info("[PUSH] after_commit event received. topic={}, postId={}, category={}",
event.topic(), event.postId(), event.category());

firebaseCloudMessageService.sendToTopic(
event.topic(),
title,
body,
Map.of(
DATA_KEY_TYPE, DATA_TYPE_COUNCIL_POST_CREATED,
DATA_KEY_POST_ID, String.valueOf(event.postId()),
DATA_KEY_CATEGORY, event.category().name()
)
);
}

private String resolveBody(PostCategory category) {
return switch (category) {
case PARTNERSHIP -> "새 제휴 게시글이 등록되었습니다.";
case EVENT -> "새 행사글이 등록되었습니다.";
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.campus.campus.domain.councilpost.application.dto.request;

import com.campus.campus.domain.councilpost.domain.entity.PostCategory;

public record CouncilPostCreatedEvent(
Long postId,
String councilName,
PostCategory category,
String topic
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Component;

import com.campus.campus.domain.council.domain.entity.StudentCouncil;
import com.campus.campus.domain.councilpost.application.dto.request.CouncilPostCreatedEvent;
import com.campus.campus.domain.councilpost.application.dto.request.PostRequest;
import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse;
import com.campus.campus.domain.councilpost.application.dto.response.GetLikedPostResponse;
Expand Down Expand Up @@ -171,4 +172,15 @@ public LikePost createLikePost(User user, StudentCouncilPost post) {
.user(user)
.build();
}

public CouncilPostCreatedEvent createPostCreatedEvent(StudentCouncilPost post, StudentCouncil writer) {
String topic = writer.getCouncilType().topic(writer);

return new CouncilPostCreatedEvent(
post.getId(),
writer.getCouncilName(),
post.getCategory(),
topic
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.springframework.dao.DataIntegrityViolationException;
Expand All @@ -18,9 +17,9 @@
import com.campus.campus.domain.council.domain.entity.CouncilType;
import com.campus.campus.domain.councilpost.application.dto.response.GetActivePartnershipListForUserResponse;
import com.campus.campus.domain.councilpost.application.dto.response.GetLikedPostResponse;
import com.campus.campus.domain.councilpost.application.dto.response.GetPostForUserResponse;
import com.campus.campus.domain.councilpost.application.dto.response.LikePostResponse;
import com.campus.campus.domain.councilpost.application.dto.response.PostListItemResponse;
import com.campus.campus.domain.councilpost.application.dto.response.GetPostForUserResponse;
import com.campus.campus.domain.councilpost.application.exception.CollegeNotSetException;
import com.campus.campus.domain.councilpost.application.exception.MajorNotSetException;
import com.campus.campus.domain.councilpost.application.exception.PostNotFoundException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.ArrayList;
import java.util.List;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -43,14 +44,14 @@
@RequiredArgsConstructor
public class StudentCouncilPostService {

private static final int MAX_IMAGE_COUNT = 10;
private static final long UPCOMING_EVENT_WINDOW_HOURS = 72L;
private final StudentCouncilPostRepository postRepository;
private final StudentCouncilRepository studentCouncilRepository;
private final PostImageRepository postImageRepository;
private final PresignedUrlService presignedUrlService;
private final StudentCouncilPostMapper studentCouncilPostMapper;

private static final int MAX_IMAGE_COUNT = 10;
private static final long UPCOMING_EVENT_WINDOW_HOURS = 72L;
private final ApplicationEventPublisher eventPublisher;
private final PlaceService placeService;
private final StudentCouncilPostRepository studentCouncilPostRepository;
private final PartnershipService partnershipService;
Expand Down Expand Up @@ -78,14 +79,16 @@ public GetPostResponse create(Long councilId, PostRequest dto) {
writer, place, dto, normalized.startDateTime(), normalized.endDateTime()
);

postRepository.save(post);
StudentCouncilPost saved = postRepository.save(post);

if (dto.imageUrls() != null) {
for (String imageUrl : dto.imageUrls()) {
postImageRepository.save(studentCouncilPostMapper.createPostImage(post, imageUrl));
}
}

eventPublisher.publishEvent(studentCouncilPostMapper.createPostCreatedEvent(saved, writer));

List<String> imageUrls = postImageRepository
.findAllByPostOrderByIdAsc(post)
.stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.campus.campus.domain.councilpost.domain.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public record SavedPlaceInfo(
String telephone,

@Schema(description = "위도/경도")
@NotBlank
Coordinate coordinate,

@Schema(description = "이미지 url")
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/campus/campus/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.campus.campus.global.config;

import java.util.concurrent.ThreadPoolExecutor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

@Bean(name = "fcmTaskExecutor")
public ThreadPoolTaskExecutor fcmTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);

executor.setQueueCapacity(1000);

executor.setThreadNamePrefix("fcm-");

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

executor.initialize();
return executor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.campus.campus.global.firebase.application.dto;

import io.swagger.v3.oas.annotations.media.Schema;


public record FcmMessageRequestDto(

@Schema(description = "유저ID", example = "1")
Long userId,

@Schema(description = "메시지 발송인", example = "시스템")
String title,

@Schema(description = "메시지 내용", example = "이제 서연님에게 화이팅을 할 수 있어요.")
String body
) {
Comment on lines +6 to +16
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

요청 DTO에 Validation(@NotNull/@notblank) 추가 + Schema 설명 정합성 맞추기 권장.
지금 상태면 API 계층에서 null/blank가 그대로 들어와 downstream(Firebase)에서 실패/예외가 날 수 있습니다. 또한 title 설명이 “메시지 발송인”으로 되어 있어 필드 의미와 어긋나 보입니다.

Proposed diff
 package com.campus.campus.global.firebase.application.dto;

 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;

 public record FcmMessageRequestDto(

 	@Schema(description = "유저ID", example = "1")
+	@NotNull
 	Long userId,

-	@Schema(description = "메시지 발송인", example = "시스템")
+	@Schema(description = "메시지 제목", example = "시스템")
+	@NotBlank
 	String title,

 	@Schema(description = "메시지 내용", example = "이제 서연님에게 화이팅을 할 수 있어요.")
+	@NotBlank
 	String body
 ) {
 }
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/global/firebase/application/dto/FcmMessageRequestDto.java
around lines 6 - 16, FcmMessageRequestDto currently lacks validation and has an
incorrect Schema for title; annotate the record components with validation
annotations (e.g., annotate userId with @NotNull, title and body with @NotBlank)
and update the @Schema descriptions to match each field's meaning (change
title's description from "메시지 발송인" to something like "메시지 제목", keep body as
message content, and document userId as required). Ensure you import the correct
validation package (jakarta.validation or javax.validation depending on project)
and confirm callers (controllers) validate the DTO (use @Valid) so invalid
requests are rejected before reaching Firebase.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.campus.campus.global.firebase.application.service;

import java.util.Map;
import java.util.concurrent.ThreadPoolExecutor;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import com.campus.campus.global.firebase.exception.FcmTopicSendFailedException;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class FirebaseCloudMessageService {

private final ThreadPoolTaskExecutor fcmTaskExecutor;

public void sendToTopic(String topic, String title, String body, Map<String, String> data) {
Message.Builder builder = Message.builder()
.setTopic(topic)
.setNotification(Notification.builder()
.setTitle(title)
.setBody(body)
.build());

if (data != null) {
data.forEach(builder::putData);
}

try {
String messageId = FirebaseMessaging.getInstance().send(builder.build());

log.info("[FCM] sent. topic={}, messageId={}, title={}, body={}, dataKeys={}, data={}, exec={}",
topic,
messageId,
title,
body,
(data == null ? "[]" : data.keySet()),
(data == null ? "{}" : data),
execSnapshot()
);

} catch (FirebaseMessagingException e) {
log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

로그 포맷 오류를 수정하세요.

Line 51의 로그 메시지에서 exec{}exec={}여야 합니다. 등호가 누락되어 로그 파싱에 문제가 발생할 수 있습니다.

🐛 수정 제안
-		log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}",
+		log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec={}",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}",
log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec={}",
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java
at line 51, The log format in FirebaseCloudMessageService is wrong: replace the
malformed placeholder "exec{}" with "exec={}" in the log.error call so the
message template has correct key=value placeholders; locate the log.error(...)
invocation (the send/failure logging in FirebaseCloudMessageService) and update
the format string to use "exec={}" ensuring number of placeholders matches
provided arguments.

topic,
e.getErrorCode(),
e.getMessage(),
execSnapshot(),
e
);
throw new FcmTopicSendFailedException(e);
}
}

//각각 스레드풀 로그 보기 위한 모니터링 메서드
private String execSnapshot() {
ThreadPoolExecutor tp = fcmTaskExecutor.getThreadPoolExecutor();

return "pool=" + tp.getPoolSize() + "/" + tp.getMaximumPoolSize()
+ ",active=" + tp.getActiveCount()
+ ",queue=" + tp.getQueue().size()
+ ",remain=" + tp.getQueue().remainingCapacity();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.campus.campus.global.firebase.application.service;

import java.io.IOException;
import java.io.InputStream;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import com.campus.campus.global.firebase.exception.FirebaseInitializationFailedException;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;

@Component
@ConditionalOnProperty(prefix = "firebase", name = "enabled", havingValue = "true", matchIfMissing = true)
public class FirebaseInitializer {

@Value("${firebase.credentials.path}")
private String firebaseCredentialsPath;

@PostConstruct
public void initialize() {
if (!FirebaseApp.getApps().isEmpty()) {
return;
}

try (InputStream serviceAccount = new ClassPathResource(firebaseCredentialsPath).getInputStream()) {

FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
FirebaseApp.initializeApp(options);

} catch (IOException e) {
throw new FirebaseInitializationFailedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.campus.campus.global.firebase.exception;

import org.springframework.http.HttpStatus;

import com.campus.campus.global.common.exception.ErrorCodeInterface;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorCode implements ErrorCodeInterface {

FIREBASE_INITIALIZATION_FAILED(6201, HttpStatus.INTERNAL_SERVER_ERROR, "Firebase 초기화에 실패했습니다."),
FCM_TOPIC_SEND_FAILED(6202, HttpStatus.INTERNAL_SERVER_ERROR, "푸시 알림 전송에 실패했습니다.");

private final int code;
private final HttpStatus status;
private final String message;
}
Loading