알람 트리거 이벤트를 수신하여 날씨/버스 정보 기반 상세 알람 멘트를 생성하고, Expo Push Notification을 통해 사용자의 모든 디바이스로 알람을 발송하는 서비스입니다.
Phase 2 완료: DB 기반 Push Token 관리, 멀티 디바이스 지원, 상세 알람 메시지 생성
- 알람 트리거 이벤트 구독
- 날씨/버스 정보 기반 상세 알람 멘트 생성 (TTS 최적화)
- Push Token DB 관리 (등록/갱신/삭제)
- Expo Push Notification API 호출
- 멀티 디바이스 푸시 알림 발송
- Port: 8085
- Database: MySQL (Push Token 관리)
- Kafka: 이벤트 구독
- 의존성: Spring Web, Spring Data JPA, Spring Kafka, Validation, Lombok
@Entity
@Table(name = "push_tokens")
public class PushToken extends BaseEntity {
private String userId; // 사용자 ID
private String deviceId; // 디바이스 ID (iPhone12, Galaxy21 등)
private String expoPushToken; // Expo Push Token
// createdAt, updatedAt (BaseEntity 상속)
}Unique 제약조건: (userId, deviceId) 조합
특징:
- 한 사용자가 여러 디바이스 등록 가능
- 디바이스별 토큰 갱신 가능
Push Token 등록 또는 갱신 (Upsert 패턴)
Request:
{
"userId": "user-uuid",
"deviceId": "iPhone12_ABC123",
"expoPushToken": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]"
}Response (201 Created):
{
"businessId": "uuid",
"userId": "user-uuid",
"deviceId": "iPhone12_ABC123",
"expoPushToken": "ExponentPushToken[xxxx]",
"createdAt": "2025-11-24T10:00:00",
"updatedAt": "2025-11-24T10:00:00"
}로직:
- 동일한 (userId, deviceId)가 존재하면 → 토큰만 업데이트
- 존재하지 않으면 → 새로 등록
디바이스 ID로 Push Token 삭제 (로그아웃, 앱 삭제 시)
Response: 204 No Content
record AlarmTriggered(
// 기본 정보
String userId, // 사용자 ID
String alarmTime, // 계산된 알람 시간 (HH:mm, 예: "06:45")
// 날씨 정보 (nullable - 타임아웃 시 null)
String temperature, // 기온 (℃, 예: "15")
String weatherCondition, // 날씨 상태 ("맑음", "흐림", "비", "눈", "눈 또는 비")
// 버스 정보 (nullable - 타임아웃 시 null)
String busRouteAbrv, // 버스 노선명 (예: "3375")
String stNm, // 정류장명 (예: "역삼역 앞 정류장")
Integer busArrivalMinutes, // 버스 도착까지 남은 시간 (분, 예: 45)
// 사용자 설정 정보 (항상 존재)
Integer preparationTime, // 준비 시간 (분, 예: 10)
Integer walkingTime, // 도보 시간 (분, 예: 5)
String desiredBoardingTime // 탑승 희망 시간 (HH:mm, 예: "08:00")
) {}발행자: alarm-creation-service
처리 로직:
- 이벤트 수신
- 날씨/버스 정보 기반 상세 멘트 생성
- DB에서 userId로 모든 Push Token 조회
- 모든 디바이스에 푸시 발송
모든 정보 있을 때 (비 오는 날):
현재 시각 오전 6시 45분입니다. 오늘 기온은 약 15도 오전 날씨는 비입니다. 우산을 챙기세요. 역삼역 앞 정류장에 3375번 버스는 약 45분 뒤 도착 예정입니다.
맑은 날:
현재 시각 오전 6시 45분입니다. 오늘 기온은 약 20도 오전 날씨는 맑음입니다. 역삼역 앞 정류장에 3375번 버스는 약 45분 뒤 도착 예정입니다.
날씨만 없을 때:
현재 시각 오전 6시 45분입니다. 역삼역 앞 정류장에 3375번 버스는 약 45분 뒤 도착 예정입니다.
버스만 없을 때:
현재 시각 오전 6시 45분입니다. 오늘 기온은 약 15도 오전 날씨는 비입니다. 우산을 챙기세요.
둘 다 없을 때:
현재 시각 오전 6시 45분입니다.
- 시간 포맷: "06:45" → "오전 6시 45분" (TTS 최적화)
- 날씨 조언:
- 비/눈/눈 또는 비 → "우산을 챙기세요" 추가
- 맑음/흐림 → 추가 멘트 없음
- 버스 표기: "3375번 버스", "약 45분 뒤"
- Null 처리: 정보가 없으면 해당 부분 생략
- Endpoint:
https://exp.host/--/api/v2/push/send - Method: POST
- Content-Type: application/json
{
"to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"sound": "default",
"title": "Dynaram 알람",
"body": "현재 시각 오전 6시 45분입니다. 오늘 기온은 약 15도...",
"data": {
"userId": "user-uuid",
"alarmTime": "06:45"
}
}List<PushToken> tokens = pushTokenRepository.findByUserId(userId);
for (PushToken token : tokens) {
expoPushClient.sendPushNotification(
token.getExpoPushToken(),
userId,
message,
alarmTime
);
}참고: 실제 알람 소리(TTS + 알람음)는 Frontend에서 재생합니다.
1. 앱 실행 시 (자동):
const expoPushToken = await Notifications.getExpoPushTokenAsync();
await fetch('http://localhost:8085/api/push-tokens', {
method: 'POST',
body: JSON.stringify({
userId: currentUserId,
deviceId: getDeviceId(),
expoPushToken: expoPushToken.data
})
});2. 로그아웃 시:
await fetch(`http://localhost:8085/api/push-tokens/${deviceId}`, {
method: 'DELETE'
});push_tokens 테이블:
+----+---------+------------+---------------------------+
| id | user_id | device_id | expo_push_token |
+----+---------+------------+---------------------------+
| 1 | user123 | iPhone12 | ExponentPushToken[xxxx] |
| 2 | user123 | Galaxy21 | ExponentPushToken[yyyy] |
+----+---------+------------+---------------------------+
→ user123이 알람을 설정하면 아이폰과 갤럭시 모두에서 알람이 울림
- Spring Boot 3.x
- Spring Web (REST API, Expo Push API 호출)
- Spring Data JPA (Push Token 관리)
- MySQL (push_tokens 테이블)
- Spring Kafka (이벤트 구독)
- Validation (DTO 검증)
- Lombok
- build.gradle 의존성 추가 (Web, Kafka)
- application.yml 설정 (Kafka, Expo Push Token 하드코딩)
- 이벤트 스키마 작성 (AlarmTriggered)
- Kafka Consumer 구현 (alarm.triggered 구독)
- 알람 멘트 생성 서비스 (AlarmMessageGenerator)
- Expo Push Notification 클라이언트 구현 (ExpoPushClient)
- RestTemplate Bean 설정
- NotificationService 전체 로직 연결
- 빌드 및 실행 테스트 완료
- MySQL 의존성 추가 (JPA, MySQL Connector)
- application.yml MySQL 설정 추가
- BaseEntity 작성 (alarm-setting-service 패턴 일치)
- PushToken Entity 작성
- PushTokenRepository 작성
- REST API 구현
- POST /api/push-tokens (등록/갱신)
- DELETE /api/push-tokens/{deviceId} (삭제)
- DTO 작성 (PushTokenRequest, PushTokenResponse)
- PushTokenService 작성
- PushTokenController 작성
- AlarmTriggered 이벤트 스키마 Phase 2 확장 (10개 필드)
- AlarmMessageGenerator Phase 2 구현
- 시간 포맷팅 (TTS 최적화)
- 날씨 정보 포맷팅
- 버스 정보 포맷팅
- Null 처리
- NotificationService DB 기반 조회로 변경
- ExpoPushClient 멀티 디바이스 지원
- 문서화 (README.md 업데이트)
# MySQL 실행 (Docker)
docker-compose up -d mysql
# 서비스 실행
./gradlew :notification-service:bootRun
# 또는
cd notification-service
./gradlew bootRuncurl -X POST http://localhost:8085/api/push-tokens \
-H "Content-Type: application/json" \
-d '{
"userId": "user-uuid",
"deviceId": "iPhone12",
"expoPushToken": "ExponentPushToken[xxxxxx]"
}'{
"userId": "user-uuid",
"alarmTime": "06:45",
"temperature": "15",
"weatherCondition": "비",
"busRouteAbrv": "3375",
"stNm": "역삼역 앞 정류장",
"busArrivalMinutes": 45,
"preparationTime": 10,
"walkingTime": 5,
"desiredBoardingTime": "08:00"
}→ 결과: user-uuid의 모든 디바이스에 상세 알람 푸시 발송
현재 동작:
alarm.triggered이벤트를 받으면 즉시 푸시 알림 발송- 알림 메시지는
alarmTime기준으로 생성되어 미래 시각 표시
문제 상황:
테스트 시나리오:
- 현재 실제 시각: 20:42
- 이벤트 수신: 20:43
- 계산된 알람 시간 (alarmTime): 21:06
- 버스 도착 예정 (alarmTime 기준): 21:16 (10분 후)
실제 발생:
→ 20:43에 즉시 알림 발송 (21:06에 발송되어야 함)
→ 메시지: "현재 시각 21시 06분입니다. 버스 도착까지 약 10분 남았습니다"
(실제로는 20:43인데 21:06이라고 표시됨)
기대 동작:
alarmTime(21:06)까지 대기 후 푸시 알림 발송- 발송 시점에 실제 현재 시각과 메시지 내용이 일치
현재 구현 이유:
- 이벤트 수신 즉시 알림이 발송되는지 테스트 목적
- 전체 이벤트 플로우 검증용 (alarm-creation → notification)
해결 방안:
- Spring
@Scheduled또는 Quartz Scheduler를 사용한 알람 스케줄링 - alarm.triggered 이벤트 수신 시:
- 알람 정보를 DB에 저장
- alarmTime까지 대기하도록 스케줄러 등록
- 정확한 시간에 푸시 알림 발송
현재 상태:
- MVP 테스트 환경에서 즉시 발송 방식으로 구현
- 프로덕션 배포 전 스케줄링 기능 구현 필요