본문 바로가기
게임서버-스파르타코딩NodeJs_7기/CH6 최종 프로젝트

GEO 발키 사용

by GREEN나무 2025. 3. 4.
728x90
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class GeoService implements OnModuleInit, OnModuleDestroy {
  private readonly S_GEO_KEY = 'sub-achievement'; // Valkey 내 Geo 데이터 키
  private readonly P_GEO_KEY = 'pinkmong-appear-location'; // Valkey 내 Geo 데이터 키
  private readonly client: Redis;

  constructor() {
    this.client = new Redis(); // Redis 클라이언트 초기화
  }

  /**
   * 모듈이 초기화될 때 실행됩니다.
   */
  onModuleInit() {
    // 필요 시 초기화 로직 추가
  }

  /**
   * 모듈이 종료될 때 실행됩니다.
   */
  onModuleDestroy() {
    this.client.quit(); // Redis 연결 종료
  }

  /**
   * Redis에 여러 작업을 일괄 처리하는 파이프라인 생성
   */
  multi() {
    return this.client.multi(); // multi()는 Redis의 파이프라인 메서드
  }

  /**
   * Redis Geo 데이터에 위치 정보를 추가하고, 추가 속성은 Hash에 저장
   * @param key Redis 키
   * @param data 북마크 데이터 객체
   */
  async geoAddBookmarkS(
    key: string,
    data: {
      id: number;
      achievement_id: number;
      title: string;
      content: string;
      longitude: number;
      latitude: number;
      sub_achievement_images: string[];
      mission_type: string;
      expiration_at: string | '';
      created_at: string | '';
      updated_at: string | '';
    },
  ) {
    const member = data.id.toString(); // 멤버는 고유 식별자로 사용 (문자열 필요)
    // GEO에 위치 데이터 저장
    await this.client.geoadd(key, data.longitude, data.latitude, member);
    // Hash에 추가 속성 저장
    const hashKey = `bookmarkS:${data.id}`;
    await this.client.hset(hashKey, {
      achievement_id: data.achievement_id,
      title: data.title,
      content: data.content,
      sub_achievement_images: data.sub_achievement_images,
      mission_type: data.mission_type,
      expiration_at: data.expiration_at,
      created_at: data.created_at,
      updated_at: data.updated_at,
    });
  }

  /**
   * Redis Geo 데이터에 항목 추가 (간단한 위치 데이터 사용)
   * @param key Redis 키
   * @param longitude 경도
   * @param latitude 위도
   * @param member 멤버
   */
  // 핑크몽 위치 수정하기
  async geoAddBookmarkP(
    key: string,
    data: {
      id: number;
      title: string; // 제목
      latitude: number; // 위도
      longitude: number; // 경도
      region_theme: string; // 지역 테마 (forest, desert 등)
      created_at: string | '';
      updated_at: string | '';
      deleted_at: string | '';
    },
  ) {
    const member = data.id.toString(); // 멤버는 고유 식별자로 사용 (문자열 필요)
    // GEO에 위치 데이터 저장
    await this.client.geoadd(key, data.longitude, data.latitude, member);
    const hashKey = `bookmarkP:${data.id}`;
    await this.client.hset(hashKey, {
      title: data.title,
      region_theme: data.region_theme,
    });
  }
  //////////////////////////////

  // geo 읽어서 맵에 북마커 추가하기
  /*
  async addBookmarker() {// zrange로 모든 멤버를 가져오고, geopos로 해당 멤버들의 좌표를 조회
    const nearbyIds1 = (await this.client.georadius(this.S_GEO_KEY)) as string[];
    const bookmarkDetails1 = await Promise.all(
      nearbyIds1.map(async (id) => {
        const hashKey = `bookmarkS:${id}`;
        const details = await this.client.hgetall(hashKey);
        return {id, title, nearestIds1.latitude, nearestIds1.longitudesub_achievement_images,...details }; // ID와 상세 정보를 함께 반환
        // Hash로 저장된 데이터는  ...details 로 객체의 모든 속성을 전개할 수 있다.
      }),
    );

    const nearestIds2 = (await this.client.geosearch(
      this.P_GEO_KEY,
    )) as string[];
    const bookmarkDetails2=await Promise.all(
      nearestIds2.map(async (id) => {
        const hashKey = `bookmarkP:${id}`;
        const details = await this.client.hgetall(hashKey);
        return { id,title,nearestIds2.latitude,nearestIds2.longitude,...details }; // ID와 상세 정보를 함께 반환
        // Hash로 저장된 데이터는  ...details 로 객체의 모든 속성을 전개할 수 있다.
      }),
    );
/*
    const arr = {
      latitude,
      longitude,
      imageUrl: bookmarkDetails1.sub_achievement_images || null,
      title,
    };*/ /*
    return [...bookmarkDetails1,...bookmarkDetails2];
  }*/
  async addBookmarker() {
    try {
      //zrange로 모든 멤버를 가져오고, geopos로 해당 멤버들의 좌표를 조회
      // 1. S_GEO_KEY에서 모든 Geo 데이터 가져오기
      const sGeoData = await this.client.geopos(
        this.S_GEO_KEY,
        ...(await this.client.zrange(this.S_GEO_KEY, 0, -1)),
      );
      //Redis 클라이언트를 통해 this.S_GEO_KEY라는 키에 저장된 데이터를 조회
      const sMembers = await this.client.zrange(this.S_GEO_KEY, 0, -1);
      /**this.S_GEO_KEY:
이건 클래스 내에서 정의된 상수로, Redis에서 사용할 키(key)를 나타냅니다.
코드에서 private readonly S_GEO_KEY = 'sub-achievement';로 정의되어 있으니, 'sub-achievement'라는 이름의 키를 의미합니다.
Redis에서 GEO 데이터를 저장할 때 이 키 아래에 데이터가 정리되어 있다고 가정합니다.
0:
ZRANGE 명령어의 시작 인덱스(start index)를 나타냅니다.
Redis에서 Sorted Set(정렬된 집합)의 인덱스는 0부터 시작합니다.
여기서는 0부터 데이터를 가져오라는 뜻입니다. 즉, 첫 번째 항목부터 시작합니다.
-1:
ZRANGE 명령어의 끝 인덱스(end index)를 나타냅니다.
Redis에서 -1은 "마지막 요소"를 의미합니다.
즉, 끝 인덱스를 -1로 설정하면 Sorted Set의 마지막 항목까지 가져오라는 뜻입니다.
zrange:
Redis의 명령어로, Sorted Set에서 지정된 범위의 멤버들을 반환합니다.
zrange key start end 형식으로 사용되며, 여기서는 this.S_GEO_KEY라는 키에서 인덱스 0부터 마지막(-1)까지 모든 멤버를 가져옵니다. */
      // 2. S_GEO_KEY의 Hash 데이터 가져오기
      const bookmarkDetails1 = await Promise.all(
        sMembers.map(async (member, index) => {
          const hashKey = `bookmarkS:${member}`;
          const details = await this.client.hgetall(hashKey);
          const [longitude, latitude] = sGeoData[index] || [];
          return {
            id: member,
            title: details.title || '',
            latitude: latitude ? parseFloat(latitude) : null,
            longitude: longitude ? parseFloat(longitude) : null,
            sub_achievement_images: details.sub_achievement_images || null,
            ...details,
          };
        }),
      );

      // 3. P_GEO_KEY에서 모든 Geo 데이터 가져오기
      const pGeoData = await this.client.geopos(
        this.P_GEO_KEY,
        ...(await this.client.zrange(this.P_GEO_KEY, 0, -1)),
      );
      const pMembers = await this.client.zrange(this.P_GEO_KEY, 0, -1);

      // 4. P_GEO_KEY의 Hash 데이터 가져오기
      const bookmarkDetails2 = await Promise.all(
        pMembers.map(async (member, index) => {
          const hashKey = `bookmarkP:${member}`;
          const details = await this.client.hgetall(hashKey);
          const [longitude, latitude] = pGeoData[index] || [];
          return {
            id: member,
            title: details.title || '',
            latitude: latitude ? parseFloat(latitude) : null,
            longitude: longitude ? parseFloat(longitude) : null,
            ...details,
          };
        }),
      );

      // 5. 두 결과 합치기
      return [...bookmarkDetails1, ...bookmarkDetails2];
    } catch (error) {
      console.error('Error in addBookmarker:', error);
      throw error;
    }
  }

  /////////////////////////////////////

  /**
   * 반경 5m 이내 북마크 검색 및 상세 정보 반환
   * @param latitude 사용자 위도
   * @param longitude 사용자 경도
   * @returns 반경 내 북마크 상세 정보 목록
   */
  async getNearbyBookmarksS(
    latitude: number,
    longitude: number,
  ): Promise<any[]> {
    console.log('범위탐색');
    // 1. GEO에서 반경 5m 내의 북마크 ID 목록 가져오기
    const nearbyIds = (await this.client.georadius(
      this.S_GEO_KEY,
      longitude,
      latitude,
      5,
      'm',
    )) as string[];
    console.log('범위탐색 nearbyIds: ', nearbyIds);
    // 2. ID 목록을 기반으로 Hash에서 상세 정보 가져오기
    const bookmarkDetails = await Promise.all(
      nearbyIds.map(async (id) => {
        const hashKey = `bookmarkS:${id}`;
        const details = await this.client.hgetall(hashKey);
        return { id, ...details }; // ID와 상세 정보를 함께 반환
        // Hash로 저장된 데이터는  ...details 로 객체의 모든 속성을 전개할 수 있다.
      }),
    );

    console.log('범위탐색 bookmarkDetails: ', bookmarkDetails);
    return bookmarkDetails; // 널이 반환됨
  }

  /**
   * 반경 5m 이내 북마크 검색 및 상세 정보 반환
   * @param latitude 사용자 위도
   * @param longitude 사용자 경도
   * @returns 반경 내 북마크 상세 정보 목록
   */
  async getNearbyBookmarkP(
    latitude: number,
    longitude: number,
  ): Promise<any | null> {
    // 1. GEO에서 반경 5m 내의 가장 가까운 북마크 ID 가져오기
    const nearestIds = (await this.client.geosearch(
      this.P_GEO_KEY,
      'FROMLONLAT',
      longitude,
      latitude,
      'BYRADIUS',
      5,
      'm',
      'ASC', // 가장 가까운 순으로 정렬
      'COUNT',
      1, // 1개만 가져옴
    )) as string[];

    if (!nearestIds || nearestIds.length === 0) return null; // 반경 내 북마크가 없으면 null 반환

    // 2. 해당 ID의 Hash에서 상세 정보 가져오기
    const nearestId = nearestIds[0];
    const hashKey = `bookmarkP:${nearestId}`;
    const details = await this.client.hgetall(hashKey);

    return details && Object.keys(details).length > 0
      ? { id: nearestId, ...details }
      : null;
  }
}