Skip to main content

개요

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
}

필드 설명

id
ID!
required
알림 고유 식별자 (ULID)
accommodationId
ID
알림이 속한 숙박시설 ID
type
String
알림 유형 (예: "emergency", "info", "warning")
data
String
알림 관련 데이터 (JSON 문자열)
  • 객실 정보, 고객 정보 등 알림 상세 내용 포함
createdAt
Date
알림 생성 시각
priority
Int
알림 우선순위
  • 1: 일반 (초록)
  • 2: 경고 (노랑)
  • 3+: 긴급 (빨강)
roomTypeId
ID
관련 객실 유형 ID
description
String
알림 설명 또는 키오스크 ID
template
String
알림 템플릿 식별자
  • ROOM_CHECKED_IN: 체크인 완료
  • RESERVATION_MISSED_HANDLED: 예약 누락 처리
  • AUTH_REQUESTED: 인증 요청
  • AUTH_REQUEST_CHECKIN: 체크인 인증 요청
  • AUTH_REQUEST_CHECKOUT: 체크아웃 인증 요청
objective
String
알림 대상 또는 목적 (예: "checkin", "checkout")
relative
String
관련 엔티티 (예: 객실 ID, 예약 ID)
dotType
String
우선순위 기반 표시 타입 (계산된 필드)
  • "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!
}
id
ID!
required
시스템 알림 고유 식별자
kioskId
ID!
required
알림을 받을 키오스크 ID
type
String!
required
시스템 알림 유형
data
String!
required
시스템 알림 데이터 (JSON 문자열)
createdAt
Date!
required
알림 생성 시각

DataEvent

데이터 이벤트 객체입니다.
type DataEvent {
  type: String!
  data: String!
}
type
String!
required
이벤트 타입
  • RoomState: 객실 상태 변경
  • AuthRequest: 인증 요청
  • AuthRequestCheckin: 체크인 인증 요청
  • AuthRequestCheckout: 체크아웃 인증 요청
data
String!
required
이벤트 데이터 (JSON 문자열)

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
  }
}

파라미터

accommodationId
ID!
required
조회할 숙박시설 ID
first
Int
처음부터 가져올 항목 수 (forward pagination)
last
Int
끝에서부터 가져올 항목 수 (backward pagination)
after
String
이 커서 이후의 항목들을 가져옴
before
String
이 커서 이전의 항목들을 가져옴
filter
AccommodationNotificationQueryFilter
필터 조건:
  • type: 알림 유형
  • dateFrom: 시작 날짜
  • dateTo: 종료 날짜
  • objective: 목적
  • relative: 관련 엔티티
  • roomTypeId: 객실 유형 ID
  • roomId: 객실 ID
  • sortKey: 정렬 키
  • sortDirection: 정렬 방향 ("asc" 또는 "desc")

응답

edges
[AccommodationNotificationEdge]!
required
알림 엣지 배열
pageInfo
PageInfo!
required
페이지네이션 정보
totalCount
Int!
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
  }
}

동작

  1. EventStore에 알림 이벤트 생성
  2. Saga 처리를 통해 데이터베이스에 저장
  3. PubSub을 통해 실시간 구독자들에게 전송
  4. 템플릿이 AUTH_REQUESTED인 경우 카카오톡 알림톡 자동 발송

publishKioskSystemNoficiation

키오스크 시스템 알림을 발행합니다.
함수명에 오타가 있습니다 (NoficiationNotification). 호환성 유지를 위해 현재 이름 사용 중입니다.

GraphQL Signature

mutation PublishKioskSystemNotification(
  $input: AddKioskSystemNotificationInput!
) {
  publishKioskSystemNoficiation(input: $input)
}

파라미터

input
AddKioskSystemNotificationInput!
required
키오스크 시스템 알림 입력:
  • kioskId (ID!, required): 키오스크 ID
  • type (String): 알림 유형
  • data (String): 알림 데이터 (JSON)
  • createdAt (Date): 생성 시각

응답

success
Boolean!
required
알림 발행 성공 여부

권한

  • 필수 권한: 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

응답

success
Boolean!
required
처리 성공 여부

권한

  • 필수 권한: 해당 숙박시설에 대한 관리 권한

동작

  1. 해당 알림을 조회
  2. 템플릿을 RESERVATION_MISSED_HANDLED로 변경
  3. 우선순위를 1(일반)로 낮춤
  4. 구독자들에게 업데이트된 알림 전송

예제

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
  }
}

파라미터

accommodationId
ID!
required
구독할 숙박시설 ID

응답

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
  }
}

파라미터

accommodationId
ID!
required
구독할 숙박시설 ID

응답

DataEvent
DataEvent!
required
실시간으로 수신되는 데이터 이벤트

권한

  • 필수 권한: 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
  }
}

파라미터

kioskId
ID!
required
구독할 키오스크 ID

응답

KioskSystemNotification
KioskSystemNotification!
required
실시간으로 수신되는 키오스크 시스템 알림

권한

  • 필수 권한: 키오스크 자체 인증
  • 해당 키오스크 ID와 토큰 일치 확인

동작

  1. 구독 시작 시 키오스크 연결 상태를 connected로 업데이트
  2. WebSocket 연결 유지 중 시스템 알림 수신
  3. 연결 해제 시 자동으로 연결 상태 업데이트

예제

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