From 0c7608cd6541cab6c834b475000c9bdd9e5dd3a6 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:02:46 +0900 Subject: [PATCH 01/19] =?UTF-8?q?chore:=20amplitude=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f92a07a..1cc7ccf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@amplitude/analytics-node": "^1.5.14", "@aws-sdk/client-s3": "^3.820.0", "@aws-sdk/s3-request-presigner": "^3.820.0", "@eslint/plugin-kit": "^0.3.5", diff --git a/yarn.lock b/yarn.lock index dcf5b4a..3e14035 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,27 @@ # yarn lockfile v1 +"@amplitude/analytics-connector@^1.6.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz#8a811ff5c8ee46bdfea0e8f61c7578769b5778ed" + integrity sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q== + +"@amplitude/analytics-core@^2.26.2": + version "2.26.2" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-core/-/analytics-core-2.26.2.tgz#e156e7815760d3a214d9ec7fa87b79f717533049" + integrity sha512-XIOzNiUCxzJwKuoK+N8rVjl0OlrfTszM+C9GyFxOYwn1zgZZEYCq0AqX1OIpy+vl+Bx3mLKZbRzxTl3eX46hLQ== + dependencies: + "@amplitude/analytics-connector" "^1.6.4" + tslib "^2.4.1" + +"@amplitude/analytics-node@^1.5.14": + version "1.5.14" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-node/-/analytics-node-1.5.14.tgz#67a444030fa29831407b3b149a311f6d98f42264" + integrity sha512-vNby1oxqOuFYvvxWTJJ+DH4byhuzwJO+qerZQrM/D/ZgDWK11TFu8FxuxMK39BHaYpaEo5vpdSGV/5gXRaDeNQ== + dependencies: + "@amplitude/analytics-core" "^2.26.2" + tslib "^2.4.1" + "@angular-devkit/core@19.2.15": version "19.2.15" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-19.2.15.tgz#35af566f9c69d3eca9c183936ee8527d9725a006" @@ -7065,7 +7086,7 @@ tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.8.1, tslib@^2.1.0, tslib@^2.6.2: +tslib@2.8.1, tslib@^2.1.0, tslib@^2.4.1, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 5f9c10e8fd279d45ce3289c7cb81f7cc35689fc7 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:03:33 +0900 Subject: [PATCH 02/19] =?UTF-8?q?chore:=20amplitude=20api=20key=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/config/configuration.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/config/configuration.ts b/src/shared/config/configuration.ts index 9bdff1c..7521b32 100644 --- a/src/shared/config/configuration.ts +++ b/src/shared/config/configuration.ts @@ -41,4 +41,9 @@ export default () => ({ url: process.env.FRONTEND_URL, loginRedirectPath: process.env.FRONTEND_LOGIN_REDIRECT_PATH, }, + + // Amplitude + amplitude: { + apiKey: process.env.AMPLITUDE_API_KEY, + }, }); From 80827916fa55880e2d3789cc09bf881a718c340e Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:03:56 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20amplitude=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20amplitude=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/analytics/analytics.module.ts | 27 +++++++++++++++++++++++++++ src/app.module.ts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 src/analytics/analytics.module.ts diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts new file mode 100644 index 0000000..9c72080 --- /dev/null +++ b/src/analytics/analytics.module.ts @@ -0,0 +1,27 @@ +import { init } from '@amplitude/analytics-node'; +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AnalyticsService } from './infrastructure/analytics.service'; +import { CqrsModule } from '@nestjs/cqrs'; +import { AuthEventsHandler } from './application/auth-event.handler'; + +export const AMPLITUDE_CLIENT = Symbol('AMPLITUDE_CLIENT'); + +@Module({ + imports: [ConfigModule, CqrsModule], + providers: [ + { + provide: 'AMPLITUDE_CLIENT', + useFactory: (configService: ConfigService) => { + return init(configService.getOrThrow('amplitude.apiKey'), { + flushIntervalMillis: 1000, + }); + }, + inject: [ConfigService], + }, + AnalyticsService, + AuthEventsHandler, + ], + exports: [AnalyticsService], +}) +export class AnalyticsModule {} diff --git a/src/app.module.ts b/src/app.module.ts index 8b83b0a..cee699e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { MediaModule } from './media/media.module'; import { ScrapModule } from './scrap/scrap.module'; import mikroOrmConfig from './shared/config/mikro-orm.config'; import config from 'src/shared/config/configuration'; +import { AnalyticsModule } from './analytics/analytics.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import config from 'src/shared/config/configuration'; isGlobal: true, load: [config], }), + AnalyticsModule, AuthModule, ArticleModule, UserModule, From 60e3f9965d0f848230cb299c23044b1a59dfd170 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:04:30 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/domain/event/login-succeeded.event.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/auth/domain/event/login-succeeded.event.ts diff --git a/src/auth/domain/event/login-succeeded.event.ts b/src/auth/domain/event/login-succeeded.event.ts new file mode 100644 index 0000000..4ce1fdf --- /dev/null +++ b/src/auth/domain/event/login-succeeded.event.ts @@ -0,0 +1,10 @@ +import { BaseDomainEvent } from 'src/shared/core/domain/base.domain-event'; +import { Identifier } from 'src/shared/core/domain/identifier'; + +export class LoginSucceededEvent implements BaseDomainEvent { + readonly timesstamp: Date; + + constructor(public readonly userId: Identifier) { + this.timesstamp = new Date(); + } +} From 598bca60ab574ca85577c45f8e22ebdaa93aaa93 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:05:21 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20auth=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/domain/entity/auth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/auth/domain/entity/auth.ts b/src/auth/domain/entity/auth.ts index 458f177..8b84662 100644 --- a/src/auth/domain/entity/auth.ts +++ b/src/auth/domain/entity/auth.ts @@ -4,6 +4,7 @@ import { OAuthProviderType } from '../value-object/oauth-provider.enum'; import { AggregateRoot } from 'src/shared/core/domain/base.aggregate'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; +import { LoginSucceededEvent } from '../event/login-succeeded.event'; export interface AuthProps extends BaseEntityProps { oauthId: string; @@ -21,6 +22,7 @@ export class Auth extends AggregateRoot { public static create(props: AuthProps): Auth { const auth = new Auth(props); auth.validate(); + auth.addDomainEvent(new LoginSucceededEvent(auth.userId)); return auth; } From dad69f7d05ce7a612b5d299b9c0a0db22d7cb19e Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:05:45 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20amplitude=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/analytics.service.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/analytics/infrastructure/analytics.service.ts diff --git a/src/analytics/infrastructure/analytics.service.ts b/src/analytics/infrastructure/analytics.service.ts new file mode 100644 index 0000000..a6de819 --- /dev/null +++ b/src/analytics/infrastructure/analytics.service.ts @@ -0,0 +1,21 @@ +import { track } from '@amplitude/analytics-node'; +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class AnalyticsService { + private readonly logger = new Logger(AnalyticsService.name); + + trackEvent(userId: string, eventType: string, eventProperties?: Record) { + try { + track({ + user_id: userId, + event_type: eventType, + event_properties: eventProperties, + }); + } catch (e: unknown) { + if (e instanceof Error) { + this.logger.error(`Amplitude tracking failed ${e.message}`); + } + } + } +} From f8fe71daf2a199f91b3cfad0a36662d217f761f2 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:06:32 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20auth=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20amplitude=EC=99=80=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/auth-event.handler.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/analytics/application/auth-event.handler.ts diff --git a/src/analytics/application/auth-event.handler.ts b/src/analytics/application/auth-event.handler.ts new file mode 100644 index 0000000..b69fdb6 --- /dev/null +++ b/src/analytics/application/auth-event.handler.ts @@ -0,0 +1,22 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { AuthCreatedEvent } from 'src/auth/domain/event/auth-created.event'; +import { AnalyticsService } from '../infrastructure/analytics.service'; +import { LoginSucceededEvent } from 'src/auth/domain/event/login-succeeded.event'; + +@EventsHandler(AuthCreatedEvent, LoginSucceededEvent) +export class AuthEventsHandler implements IEventHandler { + constructor(private readonly analyticsService: AnalyticsService) {} + + handle(event: AuthCreatedEvent | LoginSucceededEvent) { + if (event instanceof AuthCreatedEvent) { + this.analyticsService.trackEvent(event.userId.value, 'signup_success', { + email: event.email, + role: event.role, + }); + } + + if (event instanceof LoginSucceededEvent) { + this.analyticsService.trackEvent(event.userId.value, 'login_success'); + } + } +} From b4f60c826c3f11c6c37efb053d6cb39af4b6741f Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 13 Oct 2025 07:06:52 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=ED=96=89=20=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/application/oauth-login/oauth-login.handler.ts | 7 ++++++- src/auth/application/renew-token/renew-token.use-case.ts | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/auth/application/oauth-login/oauth-login.handler.ts b/src/auth/application/oauth-login/oauth-login.handler.ts index 0c7d49f..5d88196 100644 --- a/src/auth/application/oauth-login/oauth-login.handler.ts +++ b/src/auth/application/oauth-login/oauth-login.handler.ts @@ -48,7 +48,10 @@ export class OAuthLoginUseCase { // 유저 생성 및 정보 가져오기 private async findOrCreateAuth(oauthId: string, provider: OAuthProviderType, email: string): Promise { const existingAuth = await this.authRepository.findByOAuthIdandProvider(oauthId, provider); - if (existingAuth) return existingAuth; + if (existingAuth) { + this.eventBus.publish(existingAuth); + return existingAuth; + } const userId = Identifier.create(); @@ -67,6 +70,8 @@ export class OAuthLoginUseCase { await this.authRepository.save(auth); + await this.eventBus.publishAll(auth.pullDomainEvents()); + return auth; } diff --git a/src/auth/application/renew-token/renew-token.use-case.ts b/src/auth/application/renew-token/renew-token.use-case.ts index 9795989..54b4905 100644 --- a/src/auth/application/renew-token/renew-token.use-case.ts +++ b/src/auth/application/renew-token/renew-token.use-case.ts @@ -6,6 +6,7 @@ import { RenewTokenRequestDto } from './dto/renew-token.request.dto'; import { RenewTokenResponseDto } from './dto/renew-token.response.dto'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; +import { EventBus } from '@nestjs/cqrs'; @Injectable() export class RenewTokenUseCase { @@ -13,6 +14,7 @@ export class RenewTokenUseCase { private readonly jwtProvider: JwtProvider, @Inject(AUTH_REPOSITORY) private readonly authRepository: AuthRepository, + private readonly eventBus: EventBus, ) {} async execute(reqeustDto: RenewTokenRequestDto): Promise { @@ -26,6 +28,8 @@ export class RenewTokenUseCase { auth.updateRefreshToken(newJti, new Date()); await this.authRepository.update(auth); + await this.eventBus.publishAll(auth.pullDomainEvents()); + return { accessToken, refreshToken }; } } From 0ae12ff5e30b82107156c442b4caa21dc4bae3c9 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:40:06 +0900 Subject: [PATCH 09/19] =?UTF-8?q?fix:=20auth=20=EC=95=B0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=ED=8A=9C=EB=93=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/auth-event.handler.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/analytics/application/auth-event.handler.ts b/src/analytics/application/auth-event.handler.ts index b69fdb6..a79e984 100644 --- a/src/analytics/application/auth-event.handler.ts +++ b/src/analytics/application/auth-event.handler.ts @@ -3,20 +3,23 @@ import { AuthCreatedEvent } from 'src/auth/domain/event/auth-created.event'; import { AnalyticsService } from '../infrastructure/analytics.service'; import { LoginSucceededEvent } from 'src/auth/domain/event/login-succeeded.event'; -@EventsHandler(AuthCreatedEvent, LoginSucceededEvent) -export class AuthEventsHandler implements IEventHandler { +@EventsHandler(AuthCreatedEvent) +export class AuthCreatedEventHandler implements IEventHandler { constructor(private readonly analyticsService: AnalyticsService) {} - handle(event: AuthCreatedEvent | LoginSucceededEvent) { - if (event instanceof AuthCreatedEvent) { - this.analyticsService.trackEvent(event.userId.value, 'signup_success', { - email: event.email, - role: event.role, - }); - } + handle(event: AuthCreatedEvent) { + this.analyticsService.trackEvent(event.userId.value, 'Signed Up', { + email: event.email, + role: event.role, + }); + } +} + +@EventsHandler(LoginSucceededEvent) +export class LoginSucceededEventHandler implements IEventHandler { + constructor(private readonly analyticsService: AnalyticsService) {} - if (event instanceof LoginSucceededEvent) { - this.analyticsService.trackEvent(event.userId.value, 'login_success'); - } + handle(event: LoginSucceededEvent) { + this.analyticsService.trackEvent(event.userId.value, 'Logged In', {}); } } From c92f4737249a143603963eceb9ac533e2203055e Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:41:13 +0900 Subject: [PATCH 10/19] =?UTF-8?q?fix:=20=EA=B3=84=EC=A0=95=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20provider=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/auth/application/oauth-login/oauth-login.handler.ts | 2 +- src/auth/domain/event/auth-created.event.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/auth/application/oauth-login/oauth-login.handler.ts b/src/auth/application/oauth-login/oauth-login.handler.ts index 5d88196..bd5fc74 100644 --- a/src/auth/application/oauth-login/oauth-login.handler.ts +++ b/src/auth/application/oauth-login/oauth-login.handler.ts @@ -55,7 +55,7 @@ export class OAuthLoginUseCase { const userId = Identifier.create(); - await this.eventBus.publish(new AuthCreatedEvent(userId, email, Role.GENERAL)); + await this.eventBus.publish(new AuthCreatedEvent(userId, email, Role.GENERAL, provider)); const auth = Auth.create({ id: Identifier.create(), diff --git a/src/auth/domain/event/auth-created.event.ts b/src/auth/domain/event/auth-created.event.ts index 5d483fe..f891d5b 100644 --- a/src/auth/domain/event/auth-created.event.ts +++ b/src/auth/domain/event/auth-created.event.ts @@ -1,6 +1,7 @@ import { BaseDomainEvent } from 'src/shared/core/domain/base.domain-event'; import { Identifier } from 'src/shared/core/domain/identifier'; import { Role } from 'src/user/command/domain/value-object/role.enum'; +import { OAuthProviderType } from '../value-object/oauth-provider.enum'; export class AuthCreatedEvent implements BaseDomainEvent { readonly timesstamp: Date; @@ -9,6 +10,7 @@ export class AuthCreatedEvent implements BaseDomainEvent { public readonly userId: Identifier, public readonly email: string, public readonly role: Role, + public readonly provider: OAuthProviderType, ) { this.timesstamp = new Date(); } From fa230a2823bc9c4717273e43edb6845a86c624c9 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:42:05 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20pro?= =?UTF-8?q?vider=20=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/auth/domain/entity/auth.ts | 2 +- src/auth/domain/event/login-succeeded.event.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/auth/domain/entity/auth.ts b/src/auth/domain/entity/auth.ts index 8b84662..6f46780 100644 --- a/src/auth/domain/entity/auth.ts +++ b/src/auth/domain/entity/auth.ts @@ -22,7 +22,7 @@ export class Auth extends AggregateRoot { public static create(props: AuthProps): Auth { const auth = new Auth(props); auth.validate(); - auth.addDomainEvent(new LoginSucceededEvent(auth.userId)); + auth.addDomainEvent(new LoginSucceededEvent(auth.userId, auth.provider)); return auth; } diff --git a/src/auth/domain/event/login-succeeded.event.ts b/src/auth/domain/event/login-succeeded.event.ts index 4ce1fdf..fd3d01d 100644 --- a/src/auth/domain/event/login-succeeded.event.ts +++ b/src/auth/domain/event/login-succeeded.event.ts @@ -1,10 +1,14 @@ import { BaseDomainEvent } from 'src/shared/core/domain/base.domain-event'; import { Identifier } from 'src/shared/core/domain/identifier'; +import { OAuthProviderType } from '../value-object/oauth-provider.enum'; export class LoginSucceededEvent implements BaseDomainEvent { readonly timesstamp: Date; - constructor(public readonly userId: Identifier) { + constructor( + public readonly userId: Identifier, + public readonly provider: OAuthProviderType, + ) { this.timesstamp = new Date(); } } From 4a5f2b368194ff8a4d2fc668feedb77fd798bbd0 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:44:22 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20tag?= =?UTF-8?q?s=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-scrap/add-scrap.handler.ts | 31 ++++++++++--------- .../command/domain/event/scrap-added.event.ts | 6 +++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/scrap/command/application/add-scrap/add-scrap.handler.ts b/src/scrap/command/application/add-scrap/add-scrap.handler.ts index 8c0f736..4114cb9 100644 --- a/src/scrap/command/application/add-scrap/add-scrap.handler.ts +++ b/src/scrap/command/application/add-scrap/add-scrap.handler.ts @@ -2,14 +2,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { SCRAP_COMMAND_REPOSITORY, ScrapCommandRepository } from '../../domain/scrap.command.repository'; import { Scrap } from '../../domain/scrap'; import { Identifier } from 'src/shared/core/domain/identifier'; -import { - ARTICLE_COMMAND_REPOSITORY, - ArticleCommandRepository, -} from 'src/article/command/domain/article.command.repository'; import { CommandHandler, EventBus } from '@nestjs/cqrs'; import { AddScrapCommand } from './add-scrap.command'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; +import { + ARTICLE_QUERY_REPOSITORY, + ArticleQueryRepository, +} from 'src/article/query/domain/repository/article.query.repository'; @Injectable() @CommandHandler(AddScrapCommand) @@ -18,8 +18,8 @@ export class AddScrapHandler { @Inject(SCRAP_COMMAND_REPOSITORY) private readonly scrapCommandRepository: ScrapCommandRepository, private readonly eventBus: EventBus, - @Inject(ARTICLE_COMMAND_REPOSITORY) - private readonly articleCommandRepository: ArticleCommandRepository, + @Inject(ARTICLE_QUERY_REPOSITORY) + private readonly articleQueryRepository: ArticleQueryRepository, ) {} async execute(command: AddScrapCommand): Promise { @@ -28,16 +28,19 @@ export class AddScrapHandler { const existingScrap = await this.scrapCommandRepository.findByArticleIdAndUserId(articleId, userId); if (existingScrap) throw new CustomException(CustomExceptionCode.SCRAP_ALREADY_EXISTS); - const article = await this.articleCommandRepository.findById(articleId); + const article = await this.articleQueryRepository.findById(articleId); if (!article) throw new CustomException(CustomExceptionCode.ARTICLE_NOT_FOUND); - const scrap = Scrap.create({ - id: Identifier.create(), - userId: Identifier.from(userId), - articleId: Identifier.from(articleId), - createdAt: now, - updatedAt: now, - }); + const scrap = Scrap.create( + { + id: Identifier.create(), + userId: Identifier.from(userId), + articleId: Identifier.from(articleId), + createdAt: now, + updatedAt: now, + }, + article.tags, + ); await this.scrapCommandRepository.save(scrap); diff --git a/src/scrap/command/domain/event/scrap-added.event.ts b/src/scrap/command/domain/event/scrap-added.event.ts index d88d481..4afbad9 100644 --- a/src/scrap/command/domain/event/scrap-added.event.ts +++ b/src/scrap/command/domain/event/scrap-added.event.ts @@ -3,7 +3,11 @@ import { BaseDomainEvent } from 'src/shared/core/domain/base.domain-event'; export class ScrapAddedEvent implements BaseDomainEvent { readonly timesstamp: Date; - constructor(public readonly articleId: string) { + constructor( + public readonly userId: string, + public readonly articleId: string, + public readonly tags: string[], + ) { this.timesstamp = new Date(); } } From afea34bcff363a5e1d18bebf43c45acb4575ad9a Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:47:47 +0900 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EC=97=90=20tag?= =?UTF-8?q?s=20=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 --- .../application/delete-scrap/delete-scrap.handler.ts | 10 ++++++++++ src/scrap/command/domain/event/scrap-deleted.event.ts | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/scrap/command/application/delete-scrap/delete-scrap.handler.ts b/src/scrap/command/application/delete-scrap/delete-scrap.handler.ts index cd3768d..da7d339 100644 --- a/src/scrap/command/application/delete-scrap/delete-scrap.handler.ts +++ b/src/scrap/command/application/delete-scrap/delete-scrap.handler.ts @@ -4,6 +4,10 @@ import { CommandHandler, EventBus } from '@nestjs/cqrs'; import { DeleteScrapCommand } from './delete-scrap.command'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; +import { + ARTICLE_QUERY_REPOSITORY, + ArticleQueryRepository, +} from 'src/article/query/domain/repository/article.query.repository'; @Injectable() @CommandHandler(DeleteScrapCommand) @@ -12,6 +16,8 @@ export class DeleteScrapHandler { private readonly eventBus: EventBus, @Inject(SCRAP_COMMAND_REPOSITORY) private readonly scrapCommandRepository: ScrapCommandRepository, + @Inject(ARTICLE_QUERY_REPOSITORY) + private readonly articleQueryRepository: ArticleQueryRepository, ) {} async execute(command: DeleteScrapCommand) { @@ -19,9 +25,13 @@ export class DeleteScrapHandler { const scrap = await this.scrapCommandRepository.findByArticleIdAndUserId(articleId, userId); if (!scrap) throw new CustomException(CustomExceptionCode.SCRAP_NOT_FOUND); + const article = await this.articleQueryRepository.findById(articleId); + if (!article) throw new CustomException(CustomExceptionCode.ARTICLE_NOT_FOUND); await this.scrapCommandRepository.deleteByArticleIdAndUserId(articleId, userId); + scrap.delete(article.tags); + const events = scrap.pullDomainEvents(); await this.eventBus.publishAll(events); } diff --git a/src/scrap/command/domain/event/scrap-deleted.event.ts b/src/scrap/command/domain/event/scrap-deleted.event.ts index 0a82121..d246ad4 100644 --- a/src/scrap/command/domain/event/scrap-deleted.event.ts +++ b/src/scrap/command/domain/event/scrap-deleted.event.ts @@ -3,7 +3,11 @@ import { BaseDomainEvent } from 'src/shared/core/domain/base.domain-event'; export class ScrapDeletedEvent implements BaseDomainEvent { readonly timesstamp: Date; - constructor(public readonly articleId: string) { + constructor( + public readonly userId: string, + public readonly articleId: string, + public readonly tags: string[], + ) { this.timesstamp = new Date(); } } From 6057ed136bb3810437326c5b66df01626fe81b40 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:48:34 +0900 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scrap/command/domain/scrap.ts | 8 ++++---- .../command/infrastructure/scrap.mapper.ts | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/scrap/command/domain/scrap.ts b/src/scrap/command/domain/scrap.ts index 5927e8b..975329c 100644 --- a/src/scrap/command/domain/scrap.ts +++ b/src/scrap/command/domain/scrap.ts @@ -16,11 +16,11 @@ export class Scrap extends AggregateRoot { super(props); } - public static create(props: ScrapProps): Scrap { + public static create(props: ScrapProps, tags: string[]): Scrap { const scrap = new Scrap(props); scrap.validate(); - scrap.addDomainEvent(new ScrapAddedEvent(props.articleId.value)); + scrap.addDomainEvent(new ScrapAddedEvent(props.userId.value, props.articleId.value, tags)); return scrap; } @@ -31,8 +31,8 @@ export class Scrap extends AggregateRoot { } } - public delete(): void { - this.addDomainEvent(new ScrapDeletedEvent(this.articleId.value)); + public delete(tags: string[]): void { + this.addDomainEvent(new ScrapDeletedEvent(this.userId.value, this.articleId.value, tags)); } get articleId(): Identifier { diff --git a/src/scrap/command/infrastructure/scrap.mapper.ts b/src/scrap/command/infrastructure/scrap.mapper.ts index b6f9141..bca4e46 100644 --- a/src/scrap/command/infrastructure/scrap.mapper.ts +++ b/src/scrap/command/infrastructure/scrap.mapper.ts @@ -6,13 +6,16 @@ import { UserEntity } from 'src/user/command/infrastructure/user.entity'; export class ScrapMapper { static toDomain(entity: ScrapEntity): Scrap { - return Scrap.create({ - id: Identifier.from(entity.id), - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - articleId: Identifier.from(entity.article.id), - userId: Identifier.from(entity.user.id), - }); + return Scrap.create( + { + id: Identifier.from(entity.id), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + articleId: Identifier.from(entity.article.id), + userId: Identifier.from(entity.user.id), + }, + [], + ); } static toEntity(domain: Scrap): ScrapEntity { From 6e0c701ccd3be1da312e805265fb06987a3eb76f Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:49:07 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20scrap=20=EC=95=B0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=ED=8A=9C=EB=93=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/scrap-event.handler.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/analytics/application/scrap-event.handler.ts diff --git a/src/analytics/application/scrap-event.handler.ts b/src/analytics/application/scrap-event.handler.ts new file mode 100644 index 0000000..53dc9d4 --- /dev/null +++ b/src/analytics/application/scrap-event.handler.ts @@ -0,0 +1,26 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { ScrapAddedEvent } from 'src/scrap/command/domain/event/scrap-added.event'; +import { ScrapDeletedEvent } from 'src/scrap/command/domain/event/scrap-deleted.event'; +import { AnalyticsService } from '../infrastructure/analytics.service'; + +@EventsHandler(ScrapAddedEvent) +export class ScrapAddedEventHandler implements IEventHandler { + constructor(private readonly analyticsService: AnalyticsService) {} + + handle(event: ScrapAddedEvent) { + this.analyticsService.trackEvent(event.userId, 'Article Scrapped', { + articleId: event.articleId, + }); + } +} + +@EventsHandler(ScrapDeletedEvent) +export class ScrapDeletedEventHandler implements IEventHandler { + constructor(private readonly analyticsService: AnalyticsService) {} + + handle(event: ScrapDeletedEvent) { + this.analyticsService.trackEvent(event.userId, 'Article Unscrapped', { + articleId: event.articleId, + }); + } +} From 6a66d1e90cf271a02a5cb02095f1453db6c00a77 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:49:34 +0900 Subject: [PATCH 16/19] =?UTF-8?q?chore:=20analytics=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?import=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/analytics/analytics.module.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts index 9c72080..65377c2 100644 --- a/src/analytics/analytics.module.ts +++ b/src/analytics/analytics.module.ts @@ -3,10 +3,18 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AnalyticsService } from './infrastructure/analytics.service'; import { CqrsModule } from '@nestjs/cqrs'; -import { AuthEventsHandler } from './application/auth-event.handler'; +import { AuthCreatedEventHandler, LoginSucceededEventHandler } from './application/auth-event.handler'; +import { ScrapAddedEventHandler, ScrapDeletedEventHandler } from './application/scrap-event.handler'; export const AMPLITUDE_CLIENT = Symbol('AMPLITUDE_CLIENT'); +const eventHandlers = [ + AuthCreatedEventHandler, + LoginSucceededEventHandler, + ScrapAddedEventHandler, + ScrapDeletedEventHandler, +]; + @Module({ imports: [ConfigModule, CqrsModule], providers: [ @@ -20,7 +28,7 @@ export const AMPLITUDE_CLIENT = Symbol('AMPLITUDE_CLIENT'); inject: [ConfigService], }, AnalyticsService, - AuthEventsHandler, + ...eventHandlers, ], exports: [AnalyticsService], }) From 2d350d6d939a2773f8e2ff049fdd9987c48ece0f Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:49:50 +0900 Subject: [PATCH 17/19] =?UTF-8?q?chore:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20query=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=B0=B8=EC=A1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scrap/command/scrap.command.module.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scrap/command/scrap.command.module.ts b/src/scrap/command/scrap.command.module.ts index 6c7ace0..2118a45 100644 --- a/src/scrap/command/scrap.command.module.ts +++ b/src/scrap/command/scrap.command.module.ts @@ -5,17 +5,17 @@ import { ScrapEntity } from './infrastructure/scrap.entity'; import { DeleteScrapHandler } from './application/delete-scrap/delete-scrap.handler'; import { SCRAP_COMMAND_REPOSITORY } from './domain/scrap.command.repository'; import { ScrapCommandRepositoryImpl } from './infrastructure/scrap.command.repository.impl'; -import { ArticleCommandModule } from 'src/article/command/article.command.module'; -import { ARTICLE_COMMAND_REPOSITORY } from 'src/article/command/domain/article.command.repository'; -import { ArticleCommandRepositoryImpl } from 'src/article/command/infrastructure/article.command.repository.impl'; import { ArticleEntity } from 'src/article/command/infrastructure/article.entity'; import { AddScrapHandler } from './application/add-scrap/add-scrap.handler'; import { CqrsModule } from '@nestjs/cqrs'; +import { ARTICLE_QUERY_REPOSITORY } from 'src/article/query/domain/repository/article.query.repository'; +import { ArticleQueryRepositoryImpl } from 'src/article/query/infrastructure/article.query.repository.impl'; +import { ArticleQueryModule } from 'src/article/query/article.query.module'; const usecases = [AddScrapHandler, DeleteScrapHandler]; @Module({ - imports: [MikroOrmModule.forFeature([ScrapEntity, ArticleEntity]), ArticleCommandModule, CqrsModule], + imports: [MikroOrmModule.forFeature([ScrapEntity, ArticleEntity]), ArticleQueryModule, CqrsModule], providers: [ ...usecases, { @@ -23,8 +23,8 @@ const usecases = [AddScrapHandler, DeleteScrapHandler]; useClass: ScrapCommandRepositoryImpl, }, { - provide: ARTICLE_COMMAND_REPOSITORY, - useClass: ArticleCommandRepositoryImpl, + provide: ARTICLE_QUERY_REPOSITORY, + useClass: ArticleQueryRepositoryImpl, }, ], controllers: [ScrapCommandController], From 3ae5e5b114a35668913be266ad4c4b6d47955f5d Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Wed, 15 Oct 2025 20:51:51 +0900 Subject: [PATCH 18/19] =?UTF-8?q?chore:=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1cc7ccf..5acad97 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@types/passport-jwt": "^4.0.1", "axios": "^1.11.0", "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", + "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", "express": "^5.1.0", "mysql2": "^3.14.0", diff --git a/yarn.lock b/yarn.lock index 3e14035..b79170a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3615,7 +3615,7 @@ class-transformer@^0.5.1: resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== -class-validator@^0.14.1: +class-validator@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.2.tgz#a3de95edd26b703e89c151a2023d3c115030340d" integrity sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw== From bbea95a40dbbbee79cc271f7e1f8cd1ca4a37e29 Mon Sep 17 00:00:00 2001 From: liverforpresent Date: Mon, 20 Oct 2025 02:44:21 +0900 Subject: [PATCH 19/19] =?UTF-8?q?fix:=20=EC=95=B0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=ED=8A=9C=EB=93=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-scrap/add-scrap.handler.ts | 24 +++++++------------ src/scrap/command/domain/scrap.ts | 5 +--- .../command/infrastructure/scrap.mapper.ts | 17 ++++++------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/scrap/command/application/add-scrap/add-scrap.handler.ts b/src/scrap/command/application/add-scrap/add-scrap.handler.ts index 4114cb9..2148660 100644 --- a/src/scrap/command/application/add-scrap/add-scrap.handler.ts +++ b/src/scrap/command/application/add-scrap/add-scrap.handler.ts @@ -10,6 +10,7 @@ import { ARTICLE_QUERY_REPOSITORY, ArticleQueryRepository, } from 'src/article/query/domain/repository/article.query.repository'; +import { ScrapAddedEvent } from '../../domain/event/scrap-added.event'; @Injectable() @CommandHandler(AddScrapCommand) @@ -31,23 +32,16 @@ export class AddScrapHandler { const article = await this.articleQueryRepository.findById(articleId); if (!article) throw new CustomException(CustomExceptionCode.ARTICLE_NOT_FOUND); - const scrap = Scrap.create( - { - id: Identifier.create(), - userId: Identifier.from(userId), - articleId: Identifier.from(articleId), - createdAt: now, - updatedAt: now, - }, - article.tags, - ); + const scrap = Scrap.create({ + id: Identifier.create(), + userId: Identifier.from(userId), + articleId: Identifier.from(articleId), + createdAt: now, + updatedAt: now, + }); await this.scrapCommandRepository.save(scrap); - const events = scrap.pullDomainEvents(); - - for (const event of events) { - await this.eventBus.publish(event); - } + this.eventBus.publish(new ScrapAddedEvent(scrap.userId.value, scrap.articleId.value, article.tags)); } } diff --git a/src/scrap/command/domain/scrap.ts b/src/scrap/command/domain/scrap.ts index 975329c..7babe2f 100644 --- a/src/scrap/command/domain/scrap.ts +++ b/src/scrap/command/domain/scrap.ts @@ -1,7 +1,6 @@ import { AggregateRoot } from 'src/shared/core/domain/base.aggregate'; import { BaseEntityProps } from 'src/shared/core/domain/base.entity'; import { Identifier } from 'src/shared/core/domain/identifier'; -import { ScrapAddedEvent } from './event/scrap-added.event'; import { ScrapDeletedEvent } from './event/scrap-deleted.event'; import { CustomException } from 'src/shared/exception/custom-exception'; import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code'; @@ -16,12 +15,10 @@ export class Scrap extends AggregateRoot { super(props); } - public static create(props: ScrapProps, tags: string[]): Scrap { + public static create(props: ScrapProps): Scrap { const scrap = new Scrap(props); scrap.validate(); - scrap.addDomainEvent(new ScrapAddedEvent(props.userId.value, props.articleId.value, tags)); - return scrap; } diff --git a/src/scrap/command/infrastructure/scrap.mapper.ts b/src/scrap/command/infrastructure/scrap.mapper.ts index bca4e46..b6f9141 100644 --- a/src/scrap/command/infrastructure/scrap.mapper.ts +++ b/src/scrap/command/infrastructure/scrap.mapper.ts @@ -6,16 +6,13 @@ import { UserEntity } from 'src/user/command/infrastructure/user.entity'; export class ScrapMapper { static toDomain(entity: ScrapEntity): Scrap { - return Scrap.create( - { - id: Identifier.from(entity.id), - createdAt: entity.createdAt, - updatedAt: entity.updatedAt, - articleId: Identifier.from(entity.article.id), - userId: Identifier.from(entity.user.id), - }, - [], - ); + return Scrap.create({ + id: Identifier.from(entity.id), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + articleId: Identifier.from(entity.article.id), + userId: Identifier.from(entity.user.id), + }); } static toEntity(domain: Scrap): ScrapEntity {