본 포스트는 휴대폰의 위치 기능을 사용해서 현재 나와 가까운 곳, 근처에 있는 로또 1등 판매점을 구하는 방법에 대한 설명이며, 개발환경은 Node.js, NestJs, MySQL, React 입니다.

 

후배와 사이드 프로젝트로 로또 앱을 하나 만들면서 언젠가부터 로또에 진심이 되어 버렸습니다. 43년을 살면서 복권 한 장 사보지 않았었지만 이제 매주 빼먹지 않고 로또를 구매하고 있습니다. 이 블로그에서 설명하는 결과물은 아래 앱에서 보실 수 있습니다. 

 

구글 플레이스토어, 애플  앱 스토어에서 "로또킷"으로 검색하시면 되고, 링크는 아래와 같습니다.

 

[구글 플레이스토어]

https://play.google.com/store/apps/details?id=com.nextact.lottoapp 

 

로또킷 - 로또 선물함 - Google Play 앱

로또 행운번호 생성, 주변 로또판매점 바로가기, 로또 번호 선물하기 등 다양한 로또 정보를 제공하는 로또킷(lottoKit)입니다.

play.google.com

 

[애플 앱 스토어]

http://itunes.apple.com/app/id1658994153

 

‎로또킷

‎로또킷(lottoKit) 제공 기능을 소개 합니다 - 로또킷 A.I 분석 및 랜덤 조건으로 로또 행운번호를 자동생성 해보세요 - 로또킷은 내 주변 1등 로또 판매점을 안내해주고 바로 찾아갈 수 있도록 길

apps.apple.com

 

다시 본론으로 넘어가서 위 앱에서 내 위치와 가장 가까운 곳에 있는 로또 1등 판매점을 구하는 로직을 설명하겠습니다.

 

1. 1등 판매점 DB 생성

사용자의 위치 기반으로 연산을 해야 하기 때문에 1등 판매점 정보를 DB로 가지고 있어야 합니다. 개발할 당시에는 API도 없거니와 API가 있다고 해도 매번 트래픽을 발생시켜서 구하기 위해서는 사용자가 상당한 시간을 기다려야 합니다.

 

일단 간단히 아래와 같이 테이블을 만들어서 데이터를 적재합니다. (이 부분은 여러 방법이 있으므로 사용할 테이블 구조에 대해서만 설명하겠습니다.)

CREATE TABLE TB_TOP_STORE (
    TIMES INT NOT NULL,
    CODE VARCHAR(20) NOT NULL,
    NAME VARCHAR(40),
    ADDR VARCHAR(120),
    LAT VARCHAR(14),
    LON VARCHAR(14)
)

 

컬럼 순서대로 당첨회차, 판매점 코드, 판매점 명, 판매점 주소, 위도, 경도 입니다. 

 

2. 1등 판매점 기준 정하기

글을 작성하는 현재 기준에도 이미 1048회를 판매하고 있습니다. 1회에 당첨되거나 한 곳은 크게 의미가 없을 것 같아서 저 같은 경우는 최근 10회차 이내에 1등을 배출한 지점을 1등 판매점이라고 정의했습니다.

 

위 테이블 구조에서 최근 10회차 이내의 1등 당첨 판매점을 구하는 Query는 아래와 같이 작성합니다. (한 회차, 한 지점에서 여러 건의 1등 당첨이 있을 수 있으므로 DISTINCT 로 SELECT 합니다.)

SELECT  DISTINCT TIMES
     ,  CODE
     ,  NAME
     ,  ADDR
     ,  LAT
     ,  LON
  FROM  TB_TOP_STORE
 WHERE  TIMES > (SELECT  MAX(TIMES)
                   FROM  TB_TOP_STORE) - 10

 

3. 서버 API 만들기

위 Query를 결과에서 사용자의 위치와 가장 가까운 판매점을 구하는 서버 로직은 대강 아래와 같습니다. 사용자의 위치는 클라이언트 애플리케이션에서 현재 위치의 위도와 경도를 전달 받아야 합니다.

 

위 테이블의 데이터 구조를 받는 데이터 타입은 아래와 같이 정의합니다.

interface IStoreInfo {
    times: number;
    code: string;
    name: string;
    addr: string;
    lat: string;
    lon: string;
    dist: number;
}

 

테이블 구조와 동일하게 선언하되 현재 위치와의 거리를 연산하기 위해 dist라는 항목만 추가합니다.

 

Repository에서 데이터를 SELECT 합니다.

async selectTopStore(): Promise<IStoreInfo[]> {
    const topStore: IStoreInfo[] = await getConnection().query(this.queries.selectTopStore) as IStoreInfo[];
    return topStore;
}

 

여기까지 진행하면 10회차 이내의 1등 판매점이 모두 추출됩니다. 거리 오름차순으로 3건만 추출해야 하므로 먼저 아래 로직을 사용해서 사용자와 판매점 간의 거리를 구합니다.

/**
 * 두 지점 사이의 거리를 구한다.
 * @param slat 출발지 위도
 * @param slon 출발지 경도
 * @param dlat 도착지 위도
 * @param dlon 도착지 경도
 */
getDist(slat: number, slon: number, dlat: number, dlon: number): number {
    if (slat === dlat && slon === dlon) return 0;

    const radSlat: number = Math.PI * slat / 180;
    const radDlat: number = Math.PI * dlat / 180;
        
    const theta: number = slon - dlon;
    const radTheta: number = Math.PI * theta / 180;

    let dist: number = Math.sin(radSlat) * Math.sin(radDlat)
                     + Math.cos(radSlat) * Math.cos(radDlat) * Math.cos(radTheta);
    if (dist > 1) dist = 1;
        
    dist = Math.acos(dist);
    dist = dist * 180 / Math.PI;
    dist = dist * 60 * 1.1515 * 1.609344 * 1000;

    dist = dist < 100 ? Math.round(dist / 10) * 10 : Math.round(dist / 100) * 100;
    return dist;
}

 

위 코드를 이용해 지점 간의 거리를 구한 후 오름차순 정렬하여 가장 가까운 3건을 추출합니다.

async selectTopStore(lat: string, lon: string): Promise<IStoreInfo[]> {
    const topStore: IStoreInfo[] = await this.storeRepository.selectTopStore();

    const sorted: IStoreInfo[] = topStore.map((store: IStoreInfo) => {
        const dist: number = this.getDist(Number(lat), Number(lon), Number(store.lat), Number(store.lon));
        store.dist = dist;
        return store;
    }).sort((a: IStoreInfo, b: IStoreInfo) => a.dist - b.dist);

    return sorted.filter((store: IStoreInfo, inx: number) => inx < 3); 
}

 

4. 클라이언트에서 현재 위치 구하기

클라이언트에서는 아래 코드를 이용하여 현재 좌표를 구하여 서버로 전달하면 됩니다.

navigator.geolocation.getCurrentPosition((pos: GeolocationPosition) => {
    const slat: number = pos.coords.latitude;
    const slng: number = pos.coords.longitude;
});

 

위 코드의 결과물은 아래와 같습니다.

300x250

+ Recent posts