Search

15. TypeORM을 이용한 쿼리 빌더

자동차 견적 시스템 만들기

자동차 견적을 정확하게 뽑는 건 쉽지 않지만, 데이터를 기준으로 대략적인 시세를 추정하는 건 가능하다. 이제 사용자가 보낸 조건에 따라 유사한 자동차를 필터링하고 평균값을 계산해 견적을 산출하는 시스템을 설계해보자.
이 과정의 핵심은 정확한 시세 추정보다는, TypeORM을 통해 복합적인 조건의 쿼리를 어떻게 작성하고 실행할 수 있는지를 배우는 것에 있다.
견적 생성 로직은 아래 다이어그램처럼 생각할 수 있다.
각 단계는 다음과 같은 목적을 갖는다.
1.
제조사 + 모델이 동일한 보고서만 필터링
완전히 동일한 차종만 대상으로 삼기
2.
경도/위도 기준 ±5도 범위 필터링 (약 300마일 이내, 약 480km 이내)
최대한 지리적으로 가까운 차량 선택
3.
연도 기준 ±3년 필터링
신차/구형차 차이를 줄이기 위한 조치
4.
마일리지 차이순 정렬
주행 거리 차이가 적은 차량 우선 선택
5.
상위 3개의 평균 가격 반환
상위 3개 차량 평균으로 최종 견적 계산

견적 생성을 위한 쿼리 작성하기

이제 위 기준을 기반으로 실제 쿼리를 작성해보자.
ReportsServicecreateEstimate() 메서드를 만든다.
// reports.service.ts createEstimate(estimateDto: GetEstimateDto) { return this.repo .createQueryBuilder() .select('*') .where('make = :make', { make: estimateDto.make }) .getRawMany(); }
TypeScript
복사
이 쿼리는 make가 사용자가 입력한 값과 일치하는 모든 레코드를 반환한다.
파라미터 바인딩
참고로, :make는 파라미터 바인딩을 위한 문법이다. { make: estimateDto.make }는 실제 값을 바인딩하여 SQL 인젝션 공격을 방지하는 역할을 한다.
컨트롤러에서 위 메서드를 호출하도록 한다.
// reports.controller.ts @Get('/estimates') getEstimate(@Query() query: GetEstimateDto) { return this.reportsService.createEstimate(query); }
TypeScript
복사
이제 API 클라이언트를 통해 다음과 같이 요청을 보내면 제조사(make)가 도요타(toyota)인 모든 차량을 받을 수 있다. 이때 make를 다른 제조사로 바꿔서 보내면 데이터가 없기 때문에 빈 배열이 반환된다.
GET http://localhost:3000/reports?make=toyota&model=corolla&lng=0&lat=0&mileage=20000&year=1980
TypeScript
복사
이제 남은 조건들을 추가해보자.
두 번째 조건을 추가하고 싶다면 where() 대신 andWhere()를 사용해야 한다. where()를 두 번 호출하면 앞의 조건이 덮어쓰기 되기 때문이다. 즉, 오버라이딩된다.
.andWhere('report.model = :model', { model: estimateDto.model })
TypeScript
복사
위치 데이터를 활용해 경도(lng), 위도(lat)가 ±5도 범위에 있는 보고서만 조회한다.
.andWhere('ABS(report.lng - :lng) BETWEEN 0 AND 5', { lng: estimateDto.lng }) .andWhere('ABS(report.lat - :lat) BETWEEN 0 AND 5', { lat: estimateDto.lat })
TypeScript
복사
마찬가지로 연도는 ±3년 범위를 기준으로 필터링한다.
.andWhere('ABS(report.year - :year) BETWEEN 0 AND 3', { year: estimateDto.year })
TypeScript
복사
마일리지를 기준으로 입력값과 가장 가까운 보고서를 찾기 위해 절댓값 차이 기준으로 정렬한다.
이때 orderBy()는 매개변수 객체를 두 번째 인자로 받을 수 없기 때문에 .setParameters()를 사용해야 한다.
.orderBy('ABS(report.mileage - :mileage)', 'ASC') .setParameters({ mileage: estimateDto.mileage });
TypeScript
복사
상위 3개 결과만 조회하기 위해 .limit()을 이용한다.
.limit(3)
TypeScript
복사
최종적으로 조회된 보고서들에서 평균 가격을 계산하려면 AVG(price)를 select()에 추가하고, getRawOne()으로 결과를 하나의 행으로 받는다.
.select('AVG(report.price)', 'price') .getRawOne();
TypeScript
복사
전체 쿼리는 아래와 같이 구성된다. DTO를 분리하여 사용함으로써 데이터 구조를 명확히 하고, 유효성 검사를 쉽게 수행할 수 있게 했다.
createEstimate({ make, model, lng, lat, year, mileage }: GetEstimateDto) { return this.repo .createQueryBuilder() .select('AVG(price)', 'price') .where('make = :make', { make }) .andWhere('model = :model', { model }) .andWhere('ABS(lng - :lng) BETWEEN 0 AND 5', { lng }) .andWhere('ABS(lat - :lat) BETWEEN 0 AND 5', { lat }) .andWhere('ABS(year - :year) BETWEEN 0 AND 3', { year }) .orderBy('ABS(mileage - :mileage)', 'ASC') .setParameters({ mileage }) .limit(3) .getRawOne(); }
TypeScript
복사

견적 논리 테스트하기

먼저, 견적 생성을 위해 몇 가지 자동차 데이터를 생성한다.
첫 번째 차량
{ "make": "ford", "model": "mustang", "year": 1980, "mileage": 50000, "lng": 45, "lat": 45, "price": 10000 }
JSON
복사
두 번째 차량
{ // ... 나머지 속성은 첫 번째 차량과 동일 "year": 1981, "price": 15000 }
JSON
복사
세 번째 차량
{ // ... 나머지 속성은 첫 번째 차량과 동일 "year": 1982, "price": 20000 }
JSON
복사
이제 이 데이터를 바탕으로 견적을 생성하는 쿼리를 실행하면, 예상대로 평균 가격인 15,000이 나오는 것을 확인할 수 있다.
GET http://localhost:3000/reports?make=ford&model=mustang&lng=45&lat=45&mileage=20000&year=1981
JSON
복사
{ "price": 15000 }
JSON
복사
하지만, 견적을 생성하는 과정에서 사용되는 데이터들은 반드시 관리자의 승인을 받은 데이터여야만 한다. 따라서, 쿼리문에 approved IS TRUE 조건을 추가한 후, 승인 된 보고서만을 반영하는 방식으로 견적 생성이 제대로 동작하는지도 확인해보자.
// reports.service.ts createEstimate({ make, model, lng, lat, year, mileage }: GetEstimateDto) { return this.repo .createQueryBuilder() .select('AVG(price)', 'price') .where('make = :make', { make }) .andWhere('model = :model', { model }) .andWhere('ABS(lng - :lng) BETWEEN 0 AND 5', { lng }) .andWhere('ABS(lat - :lat) BETWEEN 0 AND 5', { lat }) .andWhere('ABS(year - :year) BETWEEN 0 AND 3', { year }) .andWhere('approved IS TRUE') .orderBy('ABS(mileage - :mileage)', 'ASC') .setParameters({ mileage }) .limit(3) .getRawOne(); }
TypeScript
복사
다시 견적을 생성해보면 생성한 세 가지 데이터 모두 승인되지 않은 보고서이기 때문에 값을 계산할 수 없어 null이 나오는 것을 확인할 수 있다.
{ "price": null }
TypeScript
복사
이제 애플리케이션에서 필요한 코드가 모두 작성되었다. 인증 논리와 보고서 논리도 마무리되었고, 모든 기능이 정상적으로 작동함을 확인했다. 따라서 애플리케이션 배포를 준비할 수 있는 상태가 되었다.