티스토리 뷰
watchPosition vs getCurrentPosition
실시간으로 현재 위치 정보를 받아 와야 하는가? 현재 위치 정보만 단한번 받아와야 하는가?
우리 프로젝트의 플로우를 다시한번 생각해 보았다.
이런 식인건데 유저는 어차피 도움 요청할때 유저는 버튼을 클릭하니까 그때 위치를 받으면 된다.
Helper는 요청을 수락할때 해당 버튼을 누르면 그때 위치를 받으면 된다.
그러면 우리는 getCurrentPosition을 사용하면 된다! 라고 생각을 했다.
하지만 여기서 간과한것이 하나 있었다.
지금까지는 서버로 배포한 상태가 아니여서 로컬을 사용하고 로컬컴퓨터의 주소만 가져오고 그 외의 비교군 주소는
인섬니아로 테스트 했기 때문에 helper가 바로 수락을 할수 있었던거다.
왜냐하면 내가 보내고 내가 다시 요청을 받았다고 가정했기 때문.
하지만 우리는 하나의 로직을 더 추가 해야한다.
내 주위 사람을 찾아야 한다.
너무 당연한것이였다.
택시를 예로 들면 내가 택시를 타려면 모든 택시에 요청을 보내고 내가 타려는 택시의 택시기사가 수락을 누르는것과 같이
내가 구조요청을 하면 나를 구조할 사람이 수락할때까지 주위 사람을 찾아야 한다.
이렇게 되면 현재 나의 주소만 찾는것이 될수가 없다.
실시간으로 모든 유저의 주소를 찾아야 한다.
그래야 내 주위 사람이 있는지 없는지 파악할수가 있기 때문이다.
맞이한 문제 1
mysql로 했던걸 프로젝트 DB인 postgresql로 교체하는 와중에 생긴 에러이다.
query failed: INSERT INTO "location"("created_at", "updated_at", "user_id", "latitude", "longitude", "location") VALUES (DEFAULT, DEFAULT, DEFAULT, $1, $2, ST_GeomFromText('POINT(126.97855056428719 37.566207002027255)')) RETURNING "id", "created_at", "updated_at" -- PARAMETERS: [37.566207002027255,126.97855056428719]
error: error: function st_geomfromtext(unknown) does not exist
st_geomfromtext()함수가 존재 하지 않는다고 한다.
MySQL에서는 자동으로 Spatial Data Type이 적용되어 따로 설치해주는게 없어서 현재 상황이 당황스러웠다.
해결방법은 쉬웠다. postgreSQL에서 따로 postgis익스텐션을깔아주기만 하면 됐다.
하지만 우리는 Docker-Compose로 yml파일에정의를 해둔다음 프로젝트를 시작했었다..
어떻게 해야 하지 고민을하다가 도커 허브에 관련 installed extention으로 yml파일을 설정하여 설치 하였다.
version: '3'
services:
codered_postgresql:
image: postgis/postgis
여기서 다시 에러가 생겼다.
AggregateError:
at internalConnectMultiple (node:net:1114:18)
at afterConnectMultiple (node:net:1667:5)
[Nest] 23284 - 2024. 04. 08. 오후 8:25:29 ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Error: Connection terminated unexpectedly
at Connection.<anonymous> (C:\developer\code_red\node_modules\pg\lib\client.js:132:73)
at Object.onceWrapper (node:events:628:28)
at Connection.emit (node:events:514:28)
at Socket.<anonymous> (C:\developer\code_red\node_modules\pg\lib\connection.js:63:12)
at Socket.emit (node:events:514:28)
at TCP.<anonymous> (node:net:337:12)
일단 AggregateError는 여러 에러를 하나의 에러 객체로 묶어서 나타낸느 에러라고 한다.
여기에는 네트워크 연결문제, api호출문제, 코드 오류등의 여러 오류의 정보가 포함된다고 한다.
결국 여기서는 힌트를 얻지 못하였다. 그럼 두번째 에러를 확인해보자.
typeorm에서 데이터베이스 연결을 실패했다고 한다.
또한 Error: Connection terminated unexpectedly는
pg라이브러리(PostgreSQL 연결에사용)에서 발생하는 핵심 에러 라고한다.
어디가 문제지? 하면서 같은 에러로 2시간을 끙끙 앓았던것 같다.
문제 해결은 시간이 해주었다..
본인은 개발자의 덕목중 하나인 문제 해결능력. 문제의 근본원인을 찾아 가야했다.
안되던게 되면 안되던 원인을 찾아야 한다.
안되던 원인을 찾을수가 없었다..
이게 무슨 소리인가하면 yml파일을 저장하고 docker의 파일을 다 지운다음 똑같은 파일을 다시 업로드 해주었다.
여기에서 postgis의 설치 시간이 따로 있는건가? 나의 네트워크가 느려서 좀 시간이 걸린걸까? 인터넷이 느렸나?
똑같은 코드로 똑같이 테스트를 하니 그 상태로 에러가 뜨지않고 서버가 잘 돌아갔다..
단순 서버 에러가 아니고, 정말 작은 .과 같은 에러가 아니라면 그 원인은 나는 찾지 못하였다..
혹시나 계속해서 시도해가지고 생기는 잠시 작동되는건가 싶어서 다시 똑같이 반복해서 삭제하고 설치, 삭제 설치를
반복한결과 다시는 같은 에러가 뜨는 일은 없었다..
그렇게 기도를 하며 에러를 해결하게 되었다..
맞이한 문제 2
error: error: insert or update on table "location" violates foreign key constraint "FK_ba3b695bc9d4bd35cc12839507f"
단순히 외래키 위반했다는것이다.
외래키로서 받아오는 값이 존재하지 않아서 생기는 문제였다.
즉, 존재하지 않는 userId로 테스트를 하여 생긴 문제여서 기존의 id로 테스트 하니 잘되었다.
맞이한 문제 3
const helpers = await this.locationRepository
.createQueryBuilder()
.select('*')
.addSelect(
`st_distancesphere(POINT(:longitude, :latitude), location) AS distance`,
)
.from((subQuery) => {
return subQuery
.select('*')
.from('location', 'helper') // 서브쿼리에서 location 테이블을 "helper" 별칭으로 사용합니다.
.where(
`st_distancesphere(POINT(:longitude, :latitude), location) <= :distanceThreshold`,
{
longitude: longitude,
latitude: latitude,
distanceThreshold: 1000,
},
)
.andWhere(
`st_distancesphere(POINT(:longitude, :latitude), location) > 0`,
);
}, 'helper')
.orderBy('distance')
.getRawMany();
error: error: function st_distancesphere(point, geometry) does not exist
QueryFailedError: function st_distancesphere(point, geometry) does not exist
해당 쿼리 함수가 존재하지 않는다는 에러였다.
첫번째 에러와 똑같은 에러여서 postgis가 다운됬는지 확인해보았지만 여전히 잘 돌아가고 있었다.
postgresql
SELECT *,ST_DISTANCE(POINT(127.2250368, 37.4013952),location)AS distance
FROM location
WHERE
ST_DISTANCE(POINT(127.2250368, 37.4013952),location) <= 1000
ORDER BY distance;
function st_distance(point, geometry) does not exist
데이터베이스에서 직접 쿼리문을 사용하여 테스트를 해보았음에도 해당 함수가 존재하지 않는다고 한다.
그럼 뭐가 문제지? 하면서 보는중
mysql과 postgreSQL은 다르다는걸 확인 해 볼수가 있었다.
각각의 SQL은 저마다의 조금씩 다른 함수명을 가지고 있어서 DOCS같은 곳에서 확인을 해보아야 했다.
postgis에서는 최단 거리를 잴때 ST_DISTANCE()함수를 사용한다고 한다.
또한 postgis에서 Point는 WKT형식의 지오메트리를 사용하고자 할때 사용하고
직접 좌표값을 계산하거나 위치를 반환할때는 ST_MakePoint를 사용한다고 한다.
그렇게 쿼리문을 수정해주니
postgreSQL에서도 오류없이 값이 잘 나왔다.
SELECT *,
ST_Distance(ST_SetSRID(ST_MakePoint(127.2250368, 37.4013952), 4326)::geography,
ST_SetSRID(location, 4326)::geography) AS distance_meters
FROM location
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(127.2250368, 37.4013952), 4326), ST_SetSRID(location, 4326)) <= 1000
ORDER BY distance_meters;
TypeORM에서도
const queryBuilder = this.locationRepository
.createQueryBuilder('location')
.select([
'location.*',
`ST_Distance(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)::geography,
ST_SetSRID(location.location, 4326)::geography) AS distance_meters`,
])
.setParameter('longitude', longitude)
.setParameter('latitude', latitude)
.where(
`ST_Distance(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326),
ST_SetSRID(location.location, 4326)) <= :distanceThreshold`,
{
distanceThreshold,
},
)
.orderBy('distance_meters');
같은 방법으로 설정을 해주니 오류 없이 최단 거리 순으로 정렬되어 데이터가 잘 정렬되었다.
기능 구현 - NESTJS(비즈니스 로직)
이전에 작성했던 코드는 로우 쿼리로 작성하여 SQL injection 공격에 취약한 부분들이 많이 있었다.
그 부분들을 typeorm으로 작성하여 최대한 보안이 되도록 노력은 해보았지만 아직도 부분부분 취약한 부분이 있어
더 보완이 필요할것 같다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Location } from './entities/location.entity';
import { Repository } from 'typeorm';
import { LocationDto } from './dto/location.dto';
import { MaydayRecords } from './entities/mayday-records.entity';
@Injectable()
export class MaydayService {
constructor(
@InjectRepository(Location)
private readonly locationRepository: Repository<Location>,
@InjectRepository(MaydayRecords)
private readonly maydayRecordsRepository: Repository<MaydayRecords>,
) {}
// 내위치 정보 저장
async saveMyLocation(location: LocationDto, userId: number) {
const { latitude, longitude } = location;
const user = await this.findUserId(userId);
if (user) {
await this.locationRepository
.createQueryBuilder()
.update()
.set({
latitude: latitude,
longitude: longitude,
location: () => `ST_GeomFromText('POINT(${longitude} ${latitude})')`,
})
.where('user_id = :userId', { userId: userId })
.execute();
} else {
await this.locationRepository
.createQueryBuilder()
.insert()
.into('location')
.values({
user_id: userId,
latitude: latitude,
longitude: longitude,
location: () => `ST_GeomFromText('POINT(${longitude} ${latitude})')`,
})
.execute();
}
}
// 내 위치 기반 유저 찾기
async findHelper(userId: number) {
const user = await this.findUserId(userId);
const { latitude, longitude } = user;
try {
const distanceThreshold = 1000;
const queryBuilder = this.locationRepository
.createQueryBuilder('location')
.select([
'location.*',
`ST_Distance(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)::geography,
ST_SetSRID(location.location, 4326)::geography) AS distance_meters`,
])
.setParameter('longitude', longitude)
.setParameter('latitude', latitude)
.where(
`ST_Distance(ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326),
ST_SetSRID(location.location, 4326)) <= :distanceThreshold`,
{
distanceThreshold,
},
)
.orderBy('distance_meters');
const helpersArray = await queryBuilder.getRawMany();
const helpers = helpersArray.filter(
(helper) => helper.distance_meters > 0,
);
return helpers.length;
} catch (err) {
console.error('An error occurred while finding helpers:', err);
return 'Failed';
}
}
// 알림 받은 유저 정보 저장 및 거리 계산
async acceptRescue(
userId: number,
helperId: number,
locationDto: LocationDto,
) {
const { latitude, longitude } = locationDto;
const user = await this.findUserId(userId);
const distanceMeter = await this.shortestDistance(
latitude,
longitude,
user.latitude,
user.longitude,
);
const distance = distanceMeter.shortest_distance / 1000;
const latestRecord = await this.maydayRecordsRepository.findOne({
where: { user_id: userId },
order: { created_at: 'DESC' },
});
await this.maydayRecordsRepository.update(
{ id: latestRecord.id },
{ helper_id: helperId, distance: distance },
);
return Number(distance.toFixed(1));
}
// 거리 계산
async shortestDistance(lat1, lon1, lat2, lon2) {
const distance = await this.locationRepository
.createQueryBuilder()
.select(
`
ST_Distance(
ST_SetSRID(ST_MakePoint(:lon1, :lat1), 4326)::geography,
ST_SetSRID(ST_MakePoint(:lon2, :lat2), 4326)::geography
) AS shortest_distance
`,
)
.setParameter('lon1', lon1)
.setParameter('lat1', lat1)
.setParameter('lon2', lon2)
.setParameter('lat2', lat2)
.getRawOne();
return distance;
}
// 유저 아이디로 찾기
async findUserId(userId: number) {
const user = await this.locationRepository
.createQueryBuilder()
.select()
.where('user_id = :userId', { userId: userId })
.getOne();
return user;
}
}
참고 자료
GeolocationAPI
공간 쿼리
'project > sparta' 카테고리의 다른 글
[최종 프로젝트]시간에 따른 거리 증가 및 알림 보내기 (0) | 2024.04.21 |
---|---|
[최종 프로젝트] 크롤링하여 문맥파악하기<python- selenium> (1) | 2024.04.18 |
[최종 프로젝트]실시간 구조 요청 보내기2 (위치 기반 서비스) (0) | 2024.04.11 |
[최종 프로젝트]실시간 구조 요청 보내기1 (위치 기반 서비스) (0) | 2024.04.08 |
[최종 프로젝트] Puppeteer 크롤링하기2<문자열 유사성 알고리즘> (0) | 2024.04.05 |