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()}`); } 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/kakaoMapSearch/search.service.ts b/src/place/kakaoMapSearch/search.service.ts index 331e072..3249c90 100644 --- a/src/place/kakaoMapSearch/search.service.ts +++ b/src/place/kakaoMapSearch/search.service.ts @@ -1,9 +1,18 @@ 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 { 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 { @@ -13,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: { @@ -28,14 +35,46 @@ 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 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/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/place/place.resolver.ts b/src/place/place.resolver.ts index f2cded3..a948066 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"; @@ -9,25 +9,34 @@ 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); - - for await (let p of places) { - const isCached = await this.searchService.getPlaceFromCacheById(p.id); - console.log(isCached); - if (isCached) continue; - this.searchService.setPlaceFromCacheById(p.id, p); - } + const places: Place[] = await this.searchService.searchByKeyword(filters); + 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; } - // get place from cache (for test) @Query(() => Place) - async getPlace( + async getPlaceFromCache( @Args("placeId", { type: () => String }) placeId: string - ): Promise { - return await this.searchService.getPlaceFromCacheById(placeId); + ): Promise { + const cachedPlace: Place | null = await this.searchService.getPlaceFromCacheById( + placeId + ); + + if (cachedPlace === undefined) { + return new HttpException( + `There is no cached place with ${placeId}`, + HttpStatus.BAD_REQUEST + ); + } + return cachedPlace; } } diff --git a/src/schema.gql b/src/schema.gql index c3b9e6e..2d1d20a 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -3,10 +3,10 @@ # ------------------------------------------------------ type Spot { - """카카오 Place id""" + """kakao place id""" id: String! - """emoji id list""" + """list of emoji ids""" emojis: [String!]! place_name: String! category_name: String @@ -17,11 +17,13 @@ type Spot { road_address_name: String place_url: String distance: String + location: String! x: Float y: Float } type Place { + """kakao place id""" id: String! place_name: String! category_name: String @@ -44,8 +46,8 @@ type DeleteSpotDto { type Query { findSpots: [Spot!]! - placesByKeyworld(filters: KeywordSearchDto!): [Place!]! - getPlace(placeId: String!): Place! + placesByKeyword(filters: KeywordSearchDto!): [Place!]! + getPlaceFromCache(placeId: String!): Place! } input KeywordSearchDto { diff --git a/src/spot/entities/spot.entity.ts b/src/spot/entities/spot.entity.ts index 17f9ca5..ba3ed9a 100644 --- a/src/spot/entities/spot.entity.ts +++ b/src/spot/entities/spot.entity.ts @@ -1,4 +1,4 @@ -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"; @@ -6,11 +6,11 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; @Schema({ timestamps: true }) // graphql 은 timestamp 삽입 어떻게 할까? export class 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,6 +49,23 @@ export class Spot { @Prop() distance?: string; + @Field(() => String) + @Prop({ + type: { + type: String, + enum: ["Point"], + default: "Point", + required: true, + }, + coordinates: { + type: [Number], + required: true, + index: "2dsphere", + default: [0, 0], + }, + }) + location: string; + @Field((type) => Float, { nullable: true }) @Prop() x?: number; @@ -56,11 +73,6 @@ export class Spot { @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[]; } 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 238cfbd..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,22 +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 ); - console.log(place); - 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); } @@ -49,4 +71,8 @@ export class SpotService { async remove(id: string) { return this.spotModel.remove({ id: id }).exec(); } + + async getSpot(cx: number, cy: number) { + return this.spotModel; + } }