From 6cb32484f5e8ee9ad2c6c6346de7f3ab93683725 Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 11:13:20 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Update:=20=EC=8A=A4=ED=8C=9F=20location=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/place/place.entity.ts | 2 +- src/spot/entities/spot.entity.ts | 38 +++++++++++++++++++------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/place/place.entity.ts b/src/place/place.entity.ts index 8999c29..9c10505 100644 --- a/src/place/place.entity.ts +++ b/src/place/place.entity.ts @@ -3,7 +3,7 @@ import { Field, ObjectType, Float } from "@nestjs/graphql"; // https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword @ObjectType() export class Place { - @Field(() => String) + @Field(() => String, { description: "kakao place id" }) id: string; @Field(() => String) diff --git a/src/spot/entities/spot.entity.ts b/src/spot/entities/spot.entity.ts index 17f9ca5..91a9063 100644 --- a/src/spot/entities/spot.entity.ts +++ b/src/spot/entities/spot.entity.ts @@ -1,16 +1,20 @@ -import { ObjectType, Field, Int, Float } from "@nestjs/graphql"; +import { ObjectType, Field, ID, Int, Float } from "@nestjs/graphql"; import * as mongoose from "mongoose"; import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; @ObjectType() @Schema({ timestamps: true }) // graphql 은 timestamp 삽입 어떻게 할까? export class Spot { + @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "Spot" }) + @Field(() => ID, { description: "mongodb default id" }) + _id: Spot; + @Prop({ required: true, unique: true }) - @Field(() => String, { description: "카카오 Place id" }) + @Field(() => String, { description: "kakao place id" }) id: string; @Prop({ required: true }) - @Field((type) => [String], { description: "emoji id list" }) + @Field((type) => [String], { description: "list of emoji ids" }) emojis: string[]; @Prop({ required: true }) @@ -49,18 +53,22 @@ export class Spot { @Prop() distance?: string; - @Field((type) => Float, { nullable: true }) - @Prop() - x?: number; - - @Field((type) => Float, { nullable: true }) - @Prop() - y?: number; - - // https://github.com/LotfiMEZIANI/Three-in-one-blog-post/blob/8cc58d094bad5c1a3ca0514ec1d6a6282724313b/src/app/person/person.model.ts#L19 - // @Field(() => [Emoji]) - // @Prop({ type: [mongoose.Types.ObjectId], ref: Emoji.name }) - // emojis: mongoose.Types.ObjectId[] | Emoji[]; + @Field(() => String) + @Prop({ + type: { + type: String, + enum: ["Point"], + default: "Point", + required: true, + }, + coordinates: { + type: [Number], + required: true, + index: "2dsphere", + default: [0, 0], + }, + }) + location: string; } export type SpotDocument = Spot & mongoose.Document; From 8f4321e06d8fedbb29eccbdd7d508b07ee4eeb0e Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 14:54:28 +0900 Subject: [PATCH 2/6] =?UTF-8?q?Update:=20place=20=EC=BA=90=EC=8B=B1/api=20?= =?UTF-8?q?exception=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/place/kakaoMapSearch/search.service.ts | 37 ++++++++++++++++++---- src/place/place.resolver.ts | 22 +++++++++---- src/schema.gql | 12 ++++--- src/spot/spot.service.ts | 7 ++-- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/place/kakaoMapSearch/search.service.ts b/src/place/kakaoMapSearch/search.service.ts index 331e072..9a8cb7e 100644 --- a/src/place/kakaoMapSearch/search.service.ts +++ b/src/place/kakaoMapSearch/search.service.ts @@ -1,9 +1,16 @@ import Axios, { AxiosResponse } from "axios"; -import { Injectable, Inject, CACHE_MANAGER } from "@nestjs/common"; +import { + Injectable, + Inject, + CACHE_MANAGER, + HttpException, + HttpStatus, +} from "@nestjs/common"; import { Cache } from "cache-manager"; import { ConfigService } from "../../config/config.service"; import { KeywordSearchDto } from "./search.dto"; +import { Place } from "../place.entity"; @Injectable() export class SearchService { @@ -15,7 +22,7 @@ export class SearchService { // https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword async searchByKeyworld( keywordSearchDto: KeywordSearchDto - ): Promise> { + ): Promise> { const baseUrl = this.configService.get("KAKAO_DEV_HOST"); return Axios.get(baseUrl, { headers: { @@ -28,14 +35,32 @@ export class SearchService { }, }) .then((response) => response.data.documents) - .catch((err) => console.error(err)); + .catch((err) => { + if (err.response.status == 400) { + console.error(err.response); + throw new HttpException("no matched place", HttpStatus.BAD_REQUEST); + } else { + console.error(err.response); + throw new HttpException( + "kakao api server error", + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + }); } + // https://github.com/BryanDonovan/node-cache-manager async setPlaceFromCacheById(key, value) { - return this.cacheManager.set(key, value); + this.cacheManager.set(key, value, { ttl: 300 }, function (err) { + console.error(err); + throw new HttpException( + "set place cache error", + HttpStatus.INTERNAL_SERVER_ERROR + ); + }); } - async getPlaceFromCacheById(key) { - return this.cacheManager.get(key); + async getPlaceFromCacheById(id): Promise { + return await this.cacheManager.get(id); } } diff --git a/src/place/place.resolver.ts b/src/place/place.resolver.ts index f2cded3..f61665e 100644 --- a/src/place/place.resolver.ts +++ b/src/place/place.resolver.ts @@ -1,5 +1,5 @@ import { Args, Query, Resolver } from "@nestjs/graphql"; - +import { HttpException, HttpStatus } from "@nestjs/common"; import { Place } from "./place.entity"; import { SearchService } from "./kakaoMapSearch/search.service"; import { KeywordSearchDto } from "./kakaoMapSearch/search.dto"; @@ -13,21 +13,31 @@ export class PlaceResolver { @Args("filters") filters: KeywordSearchDto ): Promise { const places: any = await this.searchService.searchByKeyworld(filters); + console.log(places); - for await (let p of places) { + for (let p of places) { const isCached = await this.searchService.getPlaceFromCacheById(p.id); - console.log(isCached); if (isCached) continue; + this.searchService.setPlaceFromCacheById(p.id, p); } return places; } - // get place from cache (for test) @Query(() => Place) async getPlace( @Args("placeId", { type: () => String }) placeId: string - ): Promise { - return await this.searchService.getPlaceFromCacheById(placeId); + ): Promise { + const place: Place = await this.searchService.getPlaceFromCacheById( + placeId + ); + + if (place === undefined) { + return new HttpException( + `There is no cached place with ${placeId}`, + HttpStatus.BAD_REQUEST + ); + } + return place; } } diff --git a/src/schema.gql b/src/schema.gql index c3b9e6e..1e20095 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -3,10 +3,13 @@ # ------------------------------------------------------ type Spot { - """카카오 Place id""" + """mongodb default id""" + _id: ID! + + """kakao place id""" id: String! - """emoji id list""" + """list of emoji ids""" emojis: [String!]! place_name: String! category_name: String @@ -17,11 +20,11 @@ type Spot { road_address_name: String place_url: String distance: String - x: Float - y: Float + location: String! } type Place { + """kakao place id""" id: String! place_name: String! category_name: String @@ -44,6 +47,7 @@ type DeleteSpotDto { type Query { findSpots: [Spot!]! + getSpots(y: Float!, x: Float!): [Spot!]! placesByKeyworld(filters: KeywordSearchDto!): [Place!]! getPlace(placeId: String!): Place! } diff --git a/src/spot/spot.service.ts b/src/spot/spot.service.ts index 238cfbd..236d0ff 100644 --- a/src/spot/spot.service.ts +++ b/src/spot/spot.service.ts @@ -19,8 +19,7 @@ export class SpotService { createSpotInput.id ); - console.log(place); - place.emoji = createSpotInput.emoji; + // place.emoji = createSpotInput.emoji; // TODO: cache miss .... const createdSpot = new this.spotModel(place); @@ -49,4 +48,8 @@ export class SpotService { async remove(id: string) { return this.spotModel.remove({ id: id }).exec(); } + + async getSpot(cx: number, cy: number) { + return this.spotModel; + } } From e5d2078574b8ee63a77712752ff6915fad86f4fa Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 15:27:27 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Refactor:=20forEach()=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/place/kakaoMapSearch/search.service.ts | 2 +- src/place/place.resolver.ts | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/place/kakaoMapSearch/search.service.ts b/src/place/kakaoMapSearch/search.service.ts index 9a8cb7e..81d7950 100644 --- a/src/place/kakaoMapSearch/search.service.ts +++ b/src/place/kakaoMapSearch/search.service.ts @@ -61,6 +61,6 @@ export class SearchService { } async getPlaceFromCacheById(id): Promise { - return await this.cacheManager.get(id); + return this.cacheManager.get(id); } } diff --git a/src/place/place.resolver.ts b/src/place/place.resolver.ts index f61665e..ba03db2 100644 --- a/src/place/place.resolver.ts +++ b/src/place/place.resolver.ts @@ -13,14 +13,13 @@ export class PlaceResolver { @Args("filters") filters: KeywordSearchDto ): Promise { const places: any = await this.searchService.searchByKeyworld(filters); - console.log(places); - - for (let p of places) { - const isCached = await this.searchService.getPlaceFromCacheById(p.id); - if (isCached) continue; - - this.searchService.setPlaceFromCacheById(p.id, p); - } + places.forEach(async (place) => { + const cachedPlace: Place | null = await this.searchService.getPlaceFromCacheById( + place.id + ); + const isCached = cachedPlace !== null; + isCached || this.searchService.setPlaceFromCacheById(place.id, place); + }); return places; } @@ -28,16 +27,16 @@ export class PlaceResolver { async getPlace( @Args("placeId", { type: () => String }) placeId: string ): Promise { - const place: Place = await this.searchService.getPlaceFromCacheById( + const cachedPlace: Place | null = await this.searchService.getPlaceFromCacheById( placeId ); - if (place === undefined) { + if (cachedPlace === undefined) { return new HttpException( `There is no cached place with ${placeId}`, HttpStatus.BAD_REQUEST ); } - return place; + return cachedPlace; } } From f8bdfa16612e7b181bc53b2304c9fde0ee4272d4 Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 20:32:55 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Rename:=20world=20->=20word=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/place/kakaoMapSearch/search.dto.ts | 2 +- src/place/place.resolver.ts | 6 +++--- src/schema.gql | 10 ++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/place/kakaoMapSearch/search.dto.ts b/src/place/kakaoMapSearch/search.dto.ts index d236b3e..0036a8d 100644 --- a/src/place/kakaoMapSearch/search.dto.ts +++ b/src/place/kakaoMapSearch/search.dto.ts @@ -6,7 +6,7 @@ import { registerEnumType, } from "@nestjs/graphql"; -enum SortType { +export enum SortType { distance = "distance", accuracy = "accuracy", } diff --git a/src/place/place.resolver.ts b/src/place/place.resolver.ts index ba03db2..a948066 100644 --- a/src/place/place.resolver.ts +++ b/src/place/place.resolver.ts @@ -9,10 +9,10 @@ export class PlaceResolver { constructor(private readonly searchService: SearchService) {} @Query(() => [Place]) - async placesByKeyworld( + async placesByKeyword( @Args("filters") filters: KeywordSearchDto ): Promise { - const places: any = await this.searchService.searchByKeyworld(filters); + const places: Place[] = await this.searchService.searchByKeyword(filters); places.forEach(async (place) => { const cachedPlace: Place | null = await this.searchService.getPlaceFromCacheById( place.id @@ -24,7 +24,7 @@ export class PlaceResolver { } @Query(() => Place) - async getPlace( + async getPlaceFromCache( @Args("placeId", { type: () => String }) placeId: string ): Promise { const cachedPlace: Place | null = await this.searchService.getPlaceFromCacheById( diff --git a/src/schema.gql b/src/schema.gql index 1e20095..2d1d20a 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -3,9 +3,6 @@ # ------------------------------------------------------ type Spot { - """mongodb default id""" - _id: ID! - """kakao place id""" id: String! @@ -21,6 +18,8 @@ type Spot { place_url: String distance: String location: String! + x: Float + y: Float } type Place { @@ -47,9 +46,8 @@ type DeleteSpotDto { type Query { findSpots: [Spot!]! - getSpots(y: Float!, x: Float!): [Spot!]! - placesByKeyworld(filters: KeywordSearchDto!): [Place!]! - getPlace(placeId: String!): Place! + placesByKeyword(filters: KeywordSearchDto!): [Place!]! + getPlaceFromCache(placeId: String!): Place! } input KeywordSearchDto { From b3a1cd86f5cd768d20af426d088af1b6b08e079d Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 20:34:43 +0900 Subject: [PATCH 5/6] =?UTF-8?q?Update:=20spot=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cache에 place없을 경우 검색해서 가져옴 - spot의 location 추가 ($.geonear) --- src/place/kakaoMapSearch/search.service.ts | 20 +++++++++++-- src/spot/entities/spot.entity.ts | 12 +++++--- src/spot/spot.resolver.ts | 18 ++++++++--- src/spot/spot.service.ts | 35 ++++++++++++++++++---- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/place/kakaoMapSearch/search.service.ts b/src/place/kakaoMapSearch/search.service.ts index 81d7950..3249c90 100644 --- a/src/place/kakaoMapSearch/search.service.ts +++ b/src/place/kakaoMapSearch/search.service.ts @@ -8,9 +8,11 @@ import { } from "@nestjs/common"; import { Cache } from "cache-manager"; +import { CreateSpotInput } from "src/spot/dto/create-spot.input"; import { ConfigService } from "../../config/config.service"; import { KeywordSearchDto } from "./search.dto"; import { Place } from "../place.entity"; +import { SortType } from "src/place/kakaoMapSearch/search.dto"; @Injectable() export class SearchService { @@ -20,9 +22,7 @@ export class SearchService { ) {} // https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword - async searchByKeyworld( - keywordSearchDto: KeywordSearchDto - ): Promise> { + async searchByKeyword(keywordSearchDto: KeywordSearchDto): Promise { const baseUrl = this.configService.get("KAKAO_DEV_HOST"); return Axios.get(baseUrl, { headers: { @@ -63,4 +63,18 @@ export class SearchService { async getPlaceFromCacheById(id): Promise { return this.cacheManager.get(id); } + + async getIdenticalPlace( + createSpotInput: CreateSpotInput + ): Promise { + const places: Place[] = await this.searchByKeyword({ + query: createSpotInput.place_name, + x: createSpotInput.x, + y: createSpotInput.y, + radius: 1, + sort: SortType.distance, + }); + console.log(places); + return places.length >= 1 ? places[0] : null; + } } diff --git a/src/spot/entities/spot.entity.ts b/src/spot/entities/spot.entity.ts index 91a9063..ba3ed9a 100644 --- a/src/spot/entities/spot.entity.ts +++ b/src/spot/entities/spot.entity.ts @@ -5,10 +5,6 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; @ObjectType() @Schema({ timestamps: true }) // graphql 은 timestamp 삽입 어떻게 할까? export class Spot { - @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "Spot" }) - @Field(() => ID, { description: "mongodb default id" }) - _id: Spot; - @Prop({ required: true, unique: true }) @Field(() => String, { description: "kakao place id" }) id: string; @@ -69,6 +65,14 @@ export class Spot { }, }) location: string; + + @Field((type) => Float, { nullable: true }) + @Prop() + x?: number; + + @Field((type) => Float, { nullable: true }) + @Prop() + y?: number; } export type SpotDocument = Spot & mongoose.Document; diff --git a/src/spot/spot.resolver.ts b/src/spot/spot.resolver.ts index 09b9199..24cea12 100644 --- a/src/spot/spot.resolver.ts +++ b/src/spot/spot.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Query, Mutation, Args, Int } from "@nestjs/graphql"; +import { Resolver, Query, Mutation, Args, Int, Float } from "@nestjs/graphql"; import { SpotService } from "src/spot/spot.service"; import { Spot } from "src/spot/entities/spot.entity"; import { CreateSpotInput } from "src/spot/dto/create-spot.input"; @@ -10,13 +10,15 @@ export class SpotResolver { constructor(private readonly spotService: SpotService) {} @Mutation(() => Spot) - async createSpot(@Args("createSpotInput") createSpotInput: CreateSpotInput) { + async createSpot( + @Args("createSpotInput") createSpotInput: CreateSpotInput + ): Promise { const spot = await this.spotService.findOne(createSpotInput.id); if (spot === null) { - return this.spotService.create(createSpotInput); + return await this.spotService.create(createSpotInput); } else { - return this.spotService.update(spot, createSpotInput.emoji); + return await this.spotService.update(spot, createSpotInput.emoji); } } @@ -25,6 +27,14 @@ export class SpotResolver { return await this.spotService.findAll(); } + // @Query(() => [Spot]) + // async getSpots( + // @Args("x", { type: () => Float }) x: number, + // @Args("y", { type: () => Float }) y: number + // ) { + // return await this.spotService.getSpot(x, y); + // } + // @Query(() => Spot, { name: "spot" }) // async findOne(@Args("id", { type: () => Int }) id: number) { // return this.spotService.findOne(id); diff --git a/src/spot/spot.service.ts b/src/spot/spot.service.ts index 236d0ff..4459603 100644 --- a/src/spot/spot.service.ts +++ b/src/spot/spot.service.ts @@ -2,10 +2,12 @@ import { Injectable } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { Model, Types } from "mongoose"; import { SearchService } from "src/place/kakaoMapSearch/search.service"; +import { SortType } from "src/place/kakaoMapSearch/search.dto"; import { CreateSpotInput } from "src/spot/dto/create-spot.input"; import { UpdateSpotInput } from "src/spot/dto/update-spot.input"; import { Spot, SpotDocument } from "src/spot/entities/spot.entity"; +import { Place } from "src/place/place.entity"; @Injectable() export class SpotService { @@ -14,21 +16,42 @@ export class SpotService { private readonly searchService: SearchService ) {} - async create(createSpotInput: CreateSpotInput) { - const place = await this.searchService.getPlaceFromCacheById( + async create(createSpotInput: CreateSpotInput): Promise { + let place: + | Place + | undefined = await this.searchService.getPlaceFromCacheById( createSpotInput.id ); - // place.emoji = createSpotInput.emoji; - // TODO: cache miss .... + if (place === undefined) { + const placeResult = await this.searchService.getIdenticalPlace( + createSpotInput + ); - const createdSpot = new this.spotModel(place); + if (placeResult === undefined) { + // TODO: custom place 만들기 + // pass + } else { + place = placeResult; + } + } + + const location = { type: "Point", coordinates: [place.x, place.y] }; + const createSpotDto = { + id: createSpotInput.id, + emojis: [createSpotInput.emoji], + location, + ...place, + }; + const createdSpot = new this.spotModel(createSpotDto); + console.log(createdSpot); + // TODO: save error handling return createdSpot.save(); } async update(spot: any, emoji: string): Promise { spot.emojis.push(emoji); - return await spot.save(); + return spot.save(); // const update = { $push: { emojis: emoji } }; // return await this.spotModel.findOneAndUpdate(filter, update); } From a7f21f1770467b4fa7538d0f32a50bbc26f190a1 Mon Sep 17 00:00:00 2001 From: minkj1992 Date: Fri, 22 Jan 2021 20:42:32 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=EB=A1=9C=EC=BB=AC/=EC=83=8C=EB=B0=95=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.module.ts | 2 +- src/config/config.service.ts | 5 +++-- src/main.ts | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/config/config.module.ts b/src/config/config.module.ts index c8bc35a..2b8b2b4 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -5,7 +5,7 @@ import { ConfigService } from "./config.service"; providers: [ { provide: ConfigService, - useValue: new ConfigService(".env.dev"), + useValue: new ConfigService(), }, ], exports: [ConfigService], diff --git a/src/config/config.service.ts b/src/config/config.service.ts index e22c60b..eace94f 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -4,8 +4,8 @@ import * as fs from "fs"; export class ConfigService { private readonly envConfig: { [key: string]: string }; - constructor(filePath: string) { - console.log(`제민욱${process.env.NODE_ENV}`); + constructor() { + console.log(`${process.env.NODE_ENV}`); if (process.env.NODE_ENV === "prod") { this.envConfig = { @@ -24,6 +24,7 @@ export class ConfigService { MONGO_CACHE_NAME: process.env.MONGO_CACHE_NAME, }; } else { + const filePath = ".env.dev"; this.envConfig = dotenv.parse(fs.readFileSync(filePath)); } } diff --git a/src/main.ts b/src/main.ts index 770cba2..76408b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,11 +7,9 @@ async function bootstrap() { logger: ["error", "warn"], }); - const configService = new ConfigService(".env.dev"); + const configService = new ConfigService(); app.enableCors(); - // app.setGlobalPrefix(configService.get("NODE_ENV")); - await app.listen(process.env.PORT || configService.get("NODE_PORT")); console.log(`Application is running on: ${await app.getUrl()}`); }