Subscription 서비스의 알림 도메인은 숙박시설 및 키오스크의 실시간 이벤트를 구독하고 발행하는 기능을 제공합니다. GraphQL Subscription을 통해 WebSocket 기반 실시간 통신을 지원하며, 다양한 우선순위와 타입의 알림을 처리할 수 있습니다.
타입 정의
AccommodationNotification
숙박시설 알림 객체입니다.
type AccommodationNotification {
id: ID!
accommodationId: ID
type: String
data: String
createdAt: Date
priority: Int
roomTypeId: ID
description: String
template: String
objective: String
relative: String
# 계산된 필드
dotType: String
}
필드 설명
알림 유형 (예: "emergency", "info", "warning")
알림 관련 데이터 (JSON 문자열)
- 객실 정보, 고객 정보 등 알림 상세 내용 포함
알림 우선순위
1: 일반 (초록)
2: 경고 (노랑)
3+: 긴급 (빨강)
알림 템플릿 식별자
ROOM_CHECKED_IN: 체크인 완료
RESERVATION_MISSED_HANDLED: 예약 누락 처리
AUTH_REQUESTED: 인증 요청
AUTH_REQUEST_CHECKIN: 체크인 인증 요청
AUTH_REQUEST_CHECKOUT: 체크아웃 인증 요청
알림 대상 또는 목적 (예: "checkin", "checkout")
우선순위 기반 표시 타입 (계산된 필드)
"success": priority = 1
"warning": priority = 2
"error": priority >= 3
"disabled": priority = null
KioskSystemNotification
키오스크 시스템 알림 객체입니다.
type KioskSystemNotification {
id: ID!
kioskId: ID!
type: String!
data: String!
createdAt: Date!
}
DataEvent
데이터 이벤트 객체입니다.
type DataEvent {
type: String!
data: String!
}
이벤트 타입
RoomState: 객실 상태 변경
AuthRequest: 인증 요청
AuthRequestCheckin: 체크인 인증 요청
AuthRequestCheckout: 체크아웃 인증 요청
Queries
getAccommodationNotifications
숙박시설의 알림 목록을 페이지네이션하여 조회합니다.
GraphQL Signature
query GetAccommodationNotifications(
$accommodationId: ID!
$first: Int
$last: Int
$after: String
$before: String
$filter: AccommodationNotificationQueryFilter
) {
getAccommodationNotifications(
accommodationId: $accommodationId
first: $first
last: $last
after: $after
before: $before
filter: $filter
) {
edges {
cursor
node {
id
type
priority
dotType
description
template
objective
relative
roomTypeId
data
createdAt
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
파라미터
처음부터 가져올 항목 수 (forward pagination)
끝에서부터 가져올 항목 수 (backward pagination)
filter
AccommodationNotificationQueryFilter
필터 조건:
type: 알림 유형
dateFrom: 시작 날짜
dateTo: 종료 날짜
objective: 목적
relative: 관련 엔티티
roomTypeId: 객실 유형 ID
roomId: 객실 ID
sortKey: 정렬 키
sortDirection: 정렬 방향 ("asc" 또는 "desc")
edges
[AccommodationNotificationEdge]!
required
알림 엣지 배열
- 필수 권한:
MAID 이상
- 해당 숙박시설에 대한 관리 권한 필요
query {
getAccommodationNotifications(
accommodationId: "01HQKS9V8X"
first: 20
filter: {
type: "emergency"
dateFrom: "2025-01-01T00:00:00Z"
dateTo: "2025-01-31T23:59:59Z"
sortKey: "createdAt"
sortDirection: "desc"
}
) {
edges {
node {
id
type
priority
dotType
template
description
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
Mutations
publishAccommodationNotification
숙박시설 알림을 생성하고 실시간으로 구독자들에게 전송합니다.
GraphQL Signature
mutation PublishAccommodationNotification(
$input: AddAccommodationNotificationInput!
) {
publishAccommodationNotification(input: $input) {
id
accommodationId
type
priority
dotType
template
description
data
createdAt
}
}
파라미터
input
AddAccommodationNotificationInput!
required
알림 생성 입력:
accommodationId (ID!, required): 숙박시설 ID
type (String!, required): 알림 유형
priority (Int): 우선순위 (1-3+)
data (String): 알림 데이터 (JSON)
objective (String): 목적
relative (String): 관련 엔티티
template (String): 템플릿 식별자
description (String): 설명
roomTypeId (ID): 객실 유형 ID
AccommodationNotification
AccommodationNotification!
required
생성된 알림 객체
- 필수 권한: 해당 숙박시설에 대한 관리 권한
- 알림 발행 후 자동으로 구독자들에게 실시간 전송
mutation {
publishAccommodationNotification(
input: {
accommodationId: "01HQKS9V8X"
type: "emergency"
priority: 3
template: "RESERVATION_MISSED_HANDLED"
description: "301호 예약 누락"
objective: "room"
relative: "01HQKS9V8Y"
roomTypeId: "01HQKS9V8Z"
data: "{\"roomNumber\": \"301\", \"guestName\": \"홍길동\"}"
}
) {
id
type
priority
dotType
createdAt
}
}
- EventStore에 알림 이벤트 생성
- Saga 처리를 통해 데이터베이스에 저장
- PubSub을 통해 실시간 구독자들에게 전송
- 템플릿이
AUTH_REQUESTED인 경우 카카오톡 알림톡 자동 발송
publishKioskSystemNoficiation
키오스크 시스템 알림을 발행합니다.
함수명에 오타가 있습니다 (Noficiation → Notification). 호환성 유지를 위해 현재 이름 사용 중입니다.
GraphQL Signature
mutation PublishKioskSystemNotification(
$input: AddKioskSystemNotificationInput!
) {
publishKioskSystemNoficiation(input: $input)
}
파라미터
input
AddKioskSystemNotificationInput!
required
키오스크 시스템 알림 입력:
kioskId (ID!, required): 키오스크 ID
type (String): 알림 유형
data (String): 알림 데이터 (JSON)
createdAt (Date): 생성 시각
- 필수 권한:
ADMIN (관리자만 시스템 알림 발행 가능)
mutation {
publishKioskSystemNoficiation(
input: {
kioskId: "01HQKS9V8X"
type: "restart"
data: "{\"reason\": \"system_update\"}"
}
)
}
handleEmergencyNotification
긴급 알림을 처리하고 상태를 업데이트합니다.
GraphQL Signature
mutation HandleEmergencyNotification(
$input: HandleEmergencyNotificationInput!
) {
handleEmergencyNotification(input: $input)
}
파라미터
input
HandleEmergencyNotificationInput!
required
긴급 알림 처리 입력:
accommodationId (ID!, required): 숙박시설 ID
notificationId (ID!, required): 알림 ID
- 해당 알림을 조회
- 템플릿을
RESERVATION_MISSED_HANDLED로 변경
- 우선순위를 1(일반)로 낮춤
- 구독자들에게 업데이트된 알림 전송
mutation {
handleEmergencyNotification(
input: {
accommodationId: "01HQKS9V8X"
notificationId: "01HQKS9V8X2N3P4Q5T"
}
)
}
Subscriptions
accommodationSubscription
숙박시설의 실시간 알림을 구독합니다.
GraphQL Signature
subscription OnAccommodationNotification($accommodationId: ID!) {
accommodationSubscription(accommodationId: $accommodationId) {
id
accommodationId
type
priority
dotType
template
description
objective
relative
roomTypeId
data
createdAt
}
}
파라미터
AccommodationNotification
AccommodationNotification!
required
실시간으로 수신되는 알림 객체
- 필수 권한:
MAID 이상
- 해당 숙박시설에 대한 관리 권한 필요
import { gql, useSubscription } from '@apollo/client';
const ACCOMMODATION_SUBSCRIPTION = gql`
subscription OnAccommodationNotification($accommodationId: ID!) {
accommodationSubscription(accommodationId: $accommodationId) {
id
type
priority
dotType
template
description
createdAt
data
}
}
`;
function NotificationListener({ accommodationId }: { accommodationId: string }) {
const { data, loading, error } = useSubscription(
ACCOMMODATION_SUBSCRIPTION,
{
variables: { accommodationId },
}
);
useEffect(() => {
if (data?.accommodationSubscription) {
const notification = data.accommodationSubscription;
// 우선순위별 처리
switch (notification.dotType) {
case 'error':
toast.error(notification.description);
playUrgentSound();
break;
case 'warning':
toast.warning(notification.description);
break;
case 'success':
toast.success(notification.description);
break;
}
}
}, [data]);
if (loading) return <p>구독 연결 중...</p>;
if (error) return <p>구독 에러: {error.message}</p>;
return <NotificationDisplay notification={data?.accommodationSubscription} />;
}
수신 이벤트 예시
{
"data": {
"accommodationSubscription": {
"id": "01HQKS9V8X2N3P4Q5U",
"type": "info",
"priority": 1,
"dotType": "success",
"template": "ROOM_CHECKED_IN",
"description": "302호 체크인 완료",
"createdAt": "2025-01-15T15:00:00Z"
}
}
}
dataSubscription
숙박시설의 실시간 데이터 이벤트를 구독합니다.
GraphQL Signature
subscription OnDataEvent($accommodationId: ID!) {
dataSubscription(accommodationId: $accommodationId) {
type
data
}
}
파라미터
- 필수 권한:
MAID 이상
- 해당 숙박시설에 대한 관리 권한 필요
이벤트 타입
| type | 설명 | data 형식 |
|---|
RoomState | 객실 상태 변경 | { id: string, stateUpdatedAt: number } |
AuthRequest | 인증 요청 | { kioskId: string, notificationId: string, iat: number, ... } |
AuthRequestCheckin | 체크인 인증 요청 | { kioskId: string, iat: number, ... } |
AuthRequestCheckout | 체크아웃 인증 요청 | { kioskId: string, iat: number, ... } |
import { gql, useSubscription } from '@apollo/client';
const DATA_SUBSCRIPTION = gql`
subscription OnDataEvent($accommodationId: ID!) {
dataSubscription(accommodationId: $accommodationId) {
type
data
}
}
`;
function DataEventListener({ accommodationId }: { accommodationId: string }) {
const { data } = useSubscription(DATA_SUBSCRIPTION, {
variables: { accommodationId },
});
useEffect(() => {
if (data?.dataSubscription) {
const event = data.dataSubscription;
const eventData = JSON.parse(event.data);
switch (event.type) {
case 'RoomState':
// 객실 상태 업데이트
updateRoomState(eventData.id, eventData.stateUpdatedAt);
break;
case 'AuthRequest':
// 인증 요청 처리
handleAuthRequest({
kioskId: eventData.kioskId,
notificationId: eventData.notificationId,
...eventData,
});
break;
case 'AuthRequestCheckin':
// 체크인 인증 요청
showAuthDialog('checkin', eventData);
break;
case 'AuthRequestCheckout':
// 체크아웃 인증 요청
showAuthDialog('checkout', eventData);
break;
default:
console.log('Unknown event type:', event.type);
}
}
}, [data]);
return null;
}
수신 이벤트 예시
// 객실 상태 변경
{
"data": {
"dataSubscription": {
"type": "RoomState",
"data": "{\"id\":\"01HQKS9V8Y\",\"stateUpdatedAt\":1705329600000}"
}
}
}
// 인증 요청
{
"data": {
"dataSubscription": {
"type": "AuthRequest",
"data": "{\"kioskId\":\"01HQKS9V8X\",\"notificationId\":\"01HQKS9V8Z\",\"accommodationName\":\"벤딧 호텔\",\"name\":\"301\",\"iat\":1705329600000}"
}
}
}
kioskSystemSubscription
키오스크 시스템 알림을 구독합니다.
GraphQL Signature
subscription OnKioskSystemNotification($kioskId: ID!) {
kioskSystemSubscription(kioskId: $kioskId) {
id
kioskId
type
data
createdAt
}
}
파라미터
KioskSystemNotification
KioskSystemNotification!
required
실시간으로 수신되는 키오스크 시스템 알림
- 필수 권한: 키오스크 자체 인증
- 해당 키오스크 ID와 토큰 일치 확인
- 구독 시작 시 키오스크 연결 상태를
connected로 업데이트
- WebSocket 연결 유지 중 시스템 알림 수신
- 연결 해제 시 자동으로 연결 상태 업데이트
import { gql, useSubscription } from '@apollo/client';
const KIOSK_SYSTEM_SUBSCRIPTION = gql`
subscription OnKioskSystemNotification($kioskId: ID!) {
kioskSystemSubscription(kioskId: $kioskId) {
id
type
data
createdAt
}
}
`;
function KioskSystemListener({ kioskId }: { kioskId: string }) {
const { data } = useSubscription(KIOSK_SYSTEM_SUBSCRIPTION, {
variables: { kioskId },
});
useEffect(() => {
if (data?.kioskSystemSubscription) {
const notification = data.kioskSystemSubscription;
const systemData = JSON.parse(notification.data);
switch (notification.type) {
case 'restart':
// 키오스크 재시작
handleRestart(systemData);
break;
case 'update':
// 시스템 업데이트
handleUpdate(systemData);
break;
case 'config':
// 설정 변경
updateConfig(systemData);
break;
default:
console.log('Unknown system notification:', notification.type);
}
}
}, [data]);
return null;
}
사용 시나리오
시나리오 1: 대시보드 실시간 알림
대시보드에서 숙박시설의 모든 이벤트를 실시간으로 모니터링합니다.
// 1. 숙박시설 알림 구독
useSubscription(ACCOMMODATION_SUBSCRIPTION, {
variables: { accommodationId: '01HQKS9V8X' },
onData: ({ data }) => {
const notification = data.data.accommodationSubscription;
// 우선순위별 알림 표시
if (notification.priority >= 3) {
showUrgentAlert(notification);
playSound('urgent');
} else {
showToast(notification);
}
// 알림 목록 업데이트
addToNotificationList(notification);
},
});
// 2. 데이터 이벤트 구독 (객실 상태 실시간 업데이트)
useSubscription(DATA_SUBSCRIPTION, {
variables: { accommodationId: '01HQKS9V8X' },
onData: ({ data }) => {
const event = data.data.dataSubscription;
if (event.type === 'RoomState') {
const { id, stateUpdatedAt } = JSON.parse(event.data);
updateRoomStateInUI(id, stateUpdatedAt);
}
},
});
시나리오 2: 키오스크 인증 요청 처리
키오스크에서 인증 요청을 보내고 대시보드에서 처리합니다.
// 키오스크: 인증 요청 발행
await publishAccommodationNotification({
accommodationId: '01HQKS9V8X',
type: 'auth',
priority: 2,
template: 'AUTH_REQUESTED',
description: kioskId,
data: JSON.stringify({
accommodationName: '벤딧 호텔',
name: '301',
}),
});
// 대시보드: 데이터 구독으로 인증 요청 수신
useSubscription(DATA_SUBSCRIPTION, {
variables: { accommodationId: '01HQKS9V8X' },
onData: ({ data }) => {
const event = data.data.dataSubscription;
if (event.type === 'AuthRequest') {
const authData = JSON.parse(event.data);
showAuthRequestDialog({
kioskId: authData.kioskId,
roomNumber: authData.name,
notificationId: authData.notificationId,
});
}
},
});
// 대시보드: 인증 승인 후 처리
await handleEmergencyNotification({
accommodationId: '01HQKS9V8X',
notificationId: authData.notificationId,
});
시나리오 3: 긴급 알림 처리
예약 누락과 같은 긴급 상황을 처리합니다.
// 1. 긴급 알림 발행
const notification = await publishAccommodationNotification({
accommodationId: '01HQKS9V8X',
type: 'emergency',
priority: 3,
template: 'RESERVATION_MISSED',
description: '301호 예약 누락',
objective: 'room',
relative: roomId,
data: JSON.stringify({
roomNumber: '301',
guestName: '홍길동',
}),
});
// 2. 대시보드에서 즉시 수신 및 표시
// (accommodationSubscription으로 자동 수신)
// 3. 담당자가 처리 완료
await handleEmergencyNotification({
accommodationId: '01HQKS9V8X',
notificationId: notification.id,
});
// 4. 모든 구독자에게 업데이트된 알림 전송
// (우선순위 1로 변경, 템플릿 RESERVATION_MISSED_HANDLED)
에러 처리
권한 부족
{
"errors": [
{
"message": "Insufficient permissions",
"extensions": {
"code": "FORBIDDEN"
}
}
]
}
존재하지 않는 알림
{
"errors": [
{
"message": "Notification not found",
"extensions": {
"code": "NOT_FOUND"
}
}
]
}
WebSocket 연결 실패
클라이언트에서 재연결 로직을 구현해야 합니다:
const wsLink = new GraphQLWsLink(
createClient({
url: 'wss://development.vpms.io/graphql',
connectionParams: {
authorization: `Bearer ${accessToken}`,
},
retryAttempts: 5,
retryWait: async (retries) => {
await new Promise(resolve =>
setTimeout(resolve, Math.min(1000 * 2 ** retries, 30000))
);
},
})
);
성능 최적화
필터링 활용
불필요한 알림을 필터링하여 네트워크 트래픽을 줄입니다:
query {
getAccommodationNotifications(
accommodationId: "01HQKS9V8X"
first: 50
filter: {
type: "emergency"
priority: 3
dateFrom: "2025-01-01T00:00:00Z"
}
) {
edges {
node {
id
description
createdAt
}
}
}
}
구독 최적화
필요한 필드만 요청하여 데이터 전송량을 최소화합니다:
subscription {
accommodationSubscription(accommodationId: "01HQKS9V8X") {
id
priority
dotType
description
# data 필드는 필요시에만 요청
}
}
관련 API