diff --git a/package-lock.json b/package-lock.json index 03f0ffd..1a125a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "shinnyang", - "version": "0.1.3", + "version": "0.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shinnyang", - "version": "0.1.3", - "license": "UNLICENSED", + "version": "0.2.8", + "license": "MIT", "dependencies": { "@hapi/joi": "^17.1.1", "@nestjs/axios": "^3.0.1", @@ -18,6 +18,7 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.1.16", "@nestjs/typeorm": "^10.0.1", + "@willsoto/nestjs-prometheus": "^6.0.0", "axios": "^1.6.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", @@ -25,6 +26,7 @@ "cross-env": "^7.0.3", "helmet": "^7.1.0", "pg": "^8.11.3", + "prom-client": "^15.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "typeorm": "^0.3.17" @@ -2043,6 +2045,14 @@ "npm": ">=5.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2769,6 +2779,15 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@willsoto/nestjs-prometheus": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@willsoto/nestjs-prometheus/-/nestjs-prometheus-6.0.0.tgz", + "integrity": "sha512-Krmda5CT9xDPjab8Eqdqiwi7xkZSX60A5rEGVLEDjUG6J6Rw5SCZ/BPaRk+MxNGWzUrRkM7K5FtTg38vWIOt1Q==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "prom-client": "^15.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -3223,6 +3242,11 @@ "node": ">=8" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -7976,6 +8000,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prom-client": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.0.tgz", + "integrity": "sha512-cCD7jLTqyPdjEPBo/Xk4Iu8jxjuZgZJ3e/oET3L+ZwOuap/7Cw3dH/TJSsZKs1TQLZ2IHpIlRAKw82ef06kmMw==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -9064,6 +9100,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/terser": { "version": "5.25.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz", diff --git a/src/app.module.ts b/src/app.module.ts index 59b7142..aa5cb98 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,6 +11,7 @@ import { LetterModule } from './letters/letter.module'; import { AppController } from './app.controller'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { MailsModule } from './mails/mails.module'; +import { StatisticsModule } from './statistics/statistics.module'; @Module({ imports: [ PrometheusModule.register(), @@ -39,6 +40,7 @@ import { MailsModule } from './mails/mails.module'; CommonModule, LetterModule, MailsModule, + StatisticsModule, ], controllers: [AppController], providers: [], diff --git a/src/statistics/dto/create-statistic.dto.ts b/src/statistics/dto/create-statistic.dto.ts new file mode 100644 index 0000000..53a4b09 --- /dev/null +++ b/src/statistics/dto/create-statistic.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEnum, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { LETTER_TYPE } from 'src/letters/entities/letter.entity'; + +export class CreateKakaoShareStatisticRequestDto { + @ApiProperty({ + description: ` + 카카오톡 공유 메세지가 전달된 채딩방의 타입 + MemoChat: 나와의 채팅방 + DirectChat: 다른 사용자와의 1:1 채팅방 + MultiChat: 다른 사용자들과의 그룹 채팅방 + OpenDirectChat: 1:1 오픈채팅방 + OpenMultiChat: 그룹 오픈채팅방`, + }) + @IsString() + CHAT_TYPE: string; + @ApiProperty() + @IsString() + HASH_CHAT_ID: string; + @ApiProperty() + @IsNumber() + TEMPLATE_ID: number; + + @ApiProperty() + @IsOptional() + @IsUUID('all') + letterId?: string; + + @ApiProperty({ description: '답장하기 일 경우, 답장하는 메일의 아이디' }) + @IsOptional() + @IsEnum(LETTER_TYPE) + letterType?: LETTER_TYPE; +} diff --git a/src/statistics/entities/statistic.entity.ts b/src/statistics/entities/statistic.entity.ts new file mode 100644 index 0000000..f87c5e1 --- /dev/null +++ b/src/statistics/entities/statistic.entity.ts @@ -0,0 +1,33 @@ +import { BaseEntity } from 'src/common/entities/base.entity'; +import { LETTER_TYPE } from 'src/letters/entities/letter.entity'; +import { Column, Entity } from 'typeorm'; + +@Entity({ name: 'KA_SHARE_ST' }) +export class KakaoShareCallbackStatistic extends BaseEntity { + @Column({ + name: 'chat_type', + comment: `카카오톡 공유 메시지가 전달된 채팅방의 타입`, + }) + chatType: string; + + @Column({ + name: 'hash_chat_id', + comment: '카카오톡 공유 메세지를 수신한 채팅방의 참고용 ID', + }) + hashChatId: string; + + @Column({ name: 'template_id', comment: '메세지 템플릿 ID' }) + templateId: number; + + @Column({ name: 'letter_id', type: 'uuid' }) + letterId: string; + + @Column({ + name: 'letter_type', + comment: '편지 타입', + type: 'enum', + enum: LETTER_TYPE, + nullable: true, + }) + letterType: LETTER_TYPE; +} diff --git a/src/statistics/statistics.controller.ts b/src/statistics/statistics.controller.ts new file mode 100644 index 0000000..0542897 --- /dev/null +++ b/src/statistics/statistics.controller.ts @@ -0,0 +1,15 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { StatisticsService } from './statistics.service'; +import { CreateKakaoShareStatisticRequestDto } from './dto/create-statistic.dto'; +import { ApiTags } from '@nestjs/swagger'; + +@Controller('statistics') +@ApiTags('Statistics API') +export class StatisticsController { + constructor(private readonly statisticsService: StatisticsService) {} + + @Post('share/kakao') + create(@Body() createStatisticDto: CreateKakaoShareStatisticRequestDto) { + return this.statisticsService.create(createStatisticDto); + } +} diff --git a/src/statistics/statistics.module.ts b/src/statistics/statistics.module.ts new file mode 100644 index 0000000..1f2fc5c --- /dev/null +++ b/src/statistics/statistics.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { StatisticsService } from './statistics.service'; +import { StatisticsController } from './statistics.controller'; + +@Module({ + controllers: [StatisticsController], + providers: [StatisticsService], +}) +export class StatisticsModule {} diff --git a/src/statistics/statistics.service.ts b/src/statistics/statistics.service.ts new file mode 100644 index 0000000..fd3c5dd --- /dev/null +++ b/src/statistics/statistics.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { CreateKakaoShareStatisticRequestDto } from './dto/create-statistic.dto'; +import { KakaoShareCallbackStatistic } from './entities/statistic.entity'; + +@Injectable() +export class StatisticsService { + constructor(@InjectDataSource() private readonly dataSource: DataSource) {} + async create( + createKakaoShareStatisticRequestDto: CreateKakaoShareStatisticRequestDto, + ) { + const repository = this.dataSource.getRepository( + KakaoShareCallbackStatistic, + ); + + await repository.save( + repository.create(createKakaoShareStatisticRequestDto), + ); + return null; + } +}