본문으로 건너뛰기

TimeMachine 통합 API 구현 가이드

개요

이 문서는 AccessCode 도메인과 TimeMachine 도메인을 통합하는 API 구현에 대한 기술적인 가이드를 제공합니다. TimeMachine 통합을 통해 Access Code의 가상 시간 관리 및 사용자 등록과의 연계 기능을 제공합니다.

아키텍처

컴포넌트 구조

src/
└── domains/
└── access-code/
├── controllers/
│ └── time-machine.controller.ts
├── services/
│ └── time-machine-integration.service.ts
├── clients/
│ └── time-machine-client.ts
├── dtos/
│ ├── time-machine-options.dto.ts
│ └── time-machine-status.dto.ts
└── interfaces/
└── virtual-time-info.interface.ts

의존성

  • TimeMachine 도메인: 가상 시간 관리
  • AccessCode 도메인: 코드 생성 및 관리
  • User 도메인: 사용자 등록 관리

인터페이스 정의

VirtualTimeInfo 인터페이스

export interface VirtualTimeInfo {
useVirtualTime: boolean;
virtualTime: string;
realTime: string;
offset?: {
days: number;
hours: number;
minutes: number;
};
}

TimeMachineStatus 인터페이스

export interface TimeMachineStatus {
integratedWithTimeMachine: boolean;
totalVirtualTimeAccessCodes: number;
activeVirtualTimeAccessCodes: number;
totalUserRegistrationsWithVirtualTime: number;
averageVirtualTimeOffset: {
days: number;
hours: number;
minutes: number;
};
}

AccessCodeTimeMachineInfo 인터페이스

export interface AccessCodeTimeMachineInfo {
codeId: string;
timeMachineEnabled: boolean;
virtualTimeStartDate?: string;
expirationBasedOnVirtualTime: boolean;
createdAt: string;
expiresAt: string;
realCreatedAt: string;
realExpiresAt: string;
virtualTimeOffset?: {
days: number;
hours: number;
minutes: number;
};
associatedUserRegistration?: {
userId: string;
timeMachineEnabled: boolean;
virtualTimeStartDate?: string;
};
}

API 구현

TimeMachine 클라이언트

@Injectable()
export class TimeMachineClient {
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly logger: Logger,
) {
this.logger = new Logger(TimeMachineClient.name);
}

private get apiUrl(): string {
return this.configService.get<string>('timeMachine.apiUrl');
}

async getCurrentVirtualTime(): Promise<VirtualTimeInfo> {
try {
const response = await this.httpService.get<VirtualTimeInfo>(
`${this.apiUrl}/v1/time-machine/current-time`,
).toPromise();

return response.data;
} catch (error) {
this.logger.error(`TimeMachine API 호출 실패: ${error.message}`);
return {
useVirtualTime: false,
virtualTime: Date.now().toString(),
realTime: Date.now().toString(),
};
}
}

async initializeUserVirtualTime(userId: string, params: InitializeUserVirtualTimeParams): Promise<void> {
try {
await this.httpService.post(
`${this.apiUrl}/v1/time-machine/users/${userId}/initialize`,
params,
).toPromise();
} catch (error) {
this.logger.error(`사용자 가상 시간 초기화 실패: ${error.message}`);
throw new InternalServerErrorException({
code: 4005,
message: 'TIME_MACHINE_SYNC_ERROR',
detail: 'TimeMachine과의 동기화 중 오류 발생',
});
}
}

async getUserVirtualTimeSettings(userId: string): Promise<UserVirtualTimeSettings | null> {
try {
const response = await this.httpService.get<UserVirtualTimeSettings>(
`${this.apiUrl}/v1/time-machine/users/${userId}/settings`,
).toPromise();

return response.data;
} catch (error) {
if (error.response?.status === 404) {
return null;
}

this.logger.error(`사용자 가상 시간 설정 조회 실패: ${error.message}`);
throw new InternalServerErrorException({
code: 4005,
message: 'TIME_MACHINE_SYNC_ERROR',
detail: 'TimeMachine과의 동기화 중 오류 발생',
});
}
}
}

TimeMachine 통합 서비스

@Injectable()
export class TimeMachineIntegrationService {
constructor(
private readonly timeMachineClient: TimeMachineClient,
private readonly accessCodeRepository: AccessCodeRepository,
private readonly userRegistrationRepository: UserRegistrationRepository,
private readonly logger: Logger,
private readonly pubSubClient: PubSub
) {
this.logger = new Logger(TimeMachineIntegrationService.name);
}

async validateTimeMachineOptions(options: TimeMachineOptionsDto): Promise<void> {
// 미래 시간 설정 불가
const currentTime = new Date();
const virtualTime = new Date(options.virtualTimeStartDate);

if (virtualTime > currentTime) {
throw new BadRequestException({
code: 4003,
message: 'FUTURE_VIRTUAL_TIME',
detail: '현재보다 미래의 가상 시간으로 설정 불가',
});
}

// 너무 오래된 과거 시간 설정 제한
const maxPastDays = this.configService.get('timeMachine.maxPastDays', 365);
const minAllowedDate = new Date();
minAllowedDate.setDate(minAllowedDate.getDate() - maxPastDays);

if (virtualTime < minAllowedDate) {
throw new BadRequestException({
code: 4004,
message: 'VIRTUAL_TIME_TOO_OLD',
detail: '설정 가능한 범위보다 오래된 가상 시간',
});
}
}

async getTimeMachineStatus(): Promise<TimeMachineStatus> {
// TimeMachine 상태 정보 조회
const [
totalVirtualTimeAccessCodes,
activeVirtualTimeAccessCodes,
totalUserRegistrationsWithVirtualTime,
] = await Promise.all([
this.accessCodeRepository.countTimeMachineEnabled(),
this.accessCodeRepository.countActiveTimeMachineEnabled(),
this.userRegistrationRepository.countTimeMachineEnabled(),
]);

// 평균 가상 시간 오프셋 계산
const averageOffset = await this.calculateAverageVirtualTimeOffset();

return {
integratedWithTimeMachine: true,
totalVirtualTimeAccessCodes,
activeVirtualTimeAccessCodes,
totalUserRegistrationsWithVirtualTime,
averageVirtualTimeOffset: averageOffset,
};
}

async getAccessCodeTimeMachineInfo(codeId: string): Promise<AccessCodeTimeMachineInfo> {
// Access Code 조회
const accessCode = await this.accessCodeRepository.findById(codeId);

if (!accessCode) {
throw new NotFoundException({
code: 3005,
message: 'CODE_NOT_FOUND',
detail: '요청한 코드를 찾을 수 없음',
});
}

// 현재 실제 시간
const realTime = new Date();

// 가상 시간 오프셋 계산
const virtualTimeOffset = accessCode.timeMachineEnabled && accessCode.virtualTimeStartDate
? this.calculateTimeOffset(accessCode.virtualTimeStartDate, realTime)
: null;

// 실제 생성 및 만료 시간
const realCreatedAt = accessCode.createdAt;
let realExpiresAt = new Date(realCreatedAt);
realExpiresAt.setDate(realCreatedAt.getDate() + accessCode.usagePeriod);

// 연결된 사용자 등록 정보 조회
let associatedUserRegistration = null;
if (accessCode.userId) {
const userRegistration = await this.userRegistrationRepository.findByUserId(accessCode.userId);
if (userRegistration) {
associatedUserRegistration = {
userId: userRegistration.userId,
timeMachineEnabled: userRegistration.timeMachineEnabled,
virtualTimeStartDate: userRegistration.virtualTimeStartDate?.getTime(),
};
}
}

return {
codeId: accessCode.id,
timeMachineEnabled: accessCode.timeMachineEnabled,
virtualTimeStartDate: accessCode.virtualTimeStartDate ? accessCode.virtualTimeStartDate.getTime() : null,
expirationBasedOnVirtualTime: accessCode.expirationBasedOnVirtualTime,
createdAt: accessCode.createdAt.getTime(),
expiresAt: accessCode.expiresAt.getTime(),
realCreatedAt: realCreatedAt.getTime(),
realExpiresAt: realExpiresAt.getTime(),
virtualTimeOffset,
associatedUserRegistration,
};
}

async initializeUserVirtualTime(userId: string, virtualTimeStartDate: Date): Promise<void> {
this.logger.log(`사용자 ${userId}의 가상 시간 초기화 시작: ${virtualTimeStartDate.getTime()}`);

try {
// TimeMachine API를 통해 사용자 가상 시간 초기화
await this.timeMachineClient.initializeUserVirtualTime(userId, {
virtualTimeStartDate: virtualTimeStartDate.getTime(),
source: 'ACCESS_CODE',
reason: 'Access Code 사용에 따른 초기화',
});

// 사용자 등록 정보 업데이트
await this.updateUserRegistration(userId, virtualTimeStartDate);

this.logger.log(`사용자 ${userId}의 가상 시간 초기화 완료`);

// GCP Pub/Sub을 통한 이벤트 발행
const topic = this.pubSubClient.topic('access-code-events');
const eventData = {
eventType: 'access-code.virtual-time-initialized',
payload: {
userId: userId,
virtualTimeStartDate: virtualTimeStartDate.getTime(),
source: 'ACCESS_CODE'
},
version: '1.0',
occurredAt: Date.now()
};

await topic.publish(Buffer.from(JSON.stringify(eventData)));
} catch (error) {
this.logger.error(`사용자 ${userId}의 가상 시간 초기화 실패: ${error.message}`);
throw new TimeMachineSyncException();
}
}

private calculateTimeOffset(date1: Date, date2: Date): { days: number; hours: number; minutes: number } {
// 두 날짜 간 차이 계산 (밀리초)
const diffMs = Math.abs(date1.getTime() - date2.getTime());

// 일, 시간, 분 계산
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));

return { days, hours, minutes };
}

private async calculateAverageVirtualTimeOffset(): Promise<{ days: number; hours: number; minutes: number }> {
// 가상 시간을 사용하는 모든 Access Code 조회
const accessCodes = await this.accessCodeRepository.findAllWithTimeMachine();

if (!accessCodes.length) {
return { days: 0, hours: 0, minutes: 0 };
}

// 현재 실제 시간
const realTime = new Date();

// 각 코드의 오프셋 계산
let totalOffsetMs = 0;

for (const code of accessCodes) {
if (code.virtualTimeStartDate) {
const diffMs = Math.abs(code.virtualTimeStartDate.getTime() - realTime.getTime());
totalOffsetMs += diffMs;
}
}

// 평균 오프셋 계산
const avgOffsetMs = totalOffsetMs / accessCodes.length;

// 일, 시간, 분으로 변환
const days = Math.floor(avgOffsetMs / (1000 * 60 * 60 * 24));
const hours = Math.floor((avgOffsetMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((avgOffsetMs % (1000 * 60 * 60)) / (1000 * 60));

return { days, hours, minutes };
}
}

TimeMachine 컨트롤러

@Controller('access-codes/time-machine')
export class TimeMachineController {
constructor(
private readonly timeMachineService: TimeMachineIntegrationService,
) {}

@Get('status')
@UseGuards(AuthGuard)
async getTimeMachineStatus(): Promise<TimeMachineStatus> {
return this.timeMachineService.getTimeMachineStatus();
}

@Get(':codeId')
@UseGuards(AuthGuard)
async getAccessCodeTimeMachineInfo(
@Param('codeId') codeId: string,
): Promise<AccessCodeTimeMachineInfo> {
return this.timeMachineService.getAccessCodeTimeMachineInfo(codeId);
}
}

Access Code 생성 시 TimeMachine 통합

Access Code 생성 시 TimeMachine 옵션을 처리하는 방법을 설명합니다.

코드 생성 시 TimeMachine 통합

@Injectable()
export class AccessCodeService {
constructor(
// ... 기존 의존성
private readonly timeMachineService: TimeMachineIntegrationService,
) {}

async createAccessCode(dto: CreateAccessCodeDto, ipAddress: string): Promise<AccessCode> {
// ... 기존 코드

// TimeMachine 옵션 처리
if (dto.timeMachineOptions?.useTimeMachine) {
// 옵션 유효성 검증
await this.timeMachineService.validateTimeMachineOptions(dto.timeMachineOptions);

// 만료일 계산 (가상 시간 기준)
if (dto.timeMachineOptions.expirationBasedOnVirtualTime) {
const virtualTimeStartDate = new Date(dto.timeMachineOptions.virtualTimeStartDate);

accessCode.expiresAt = new Date(virtualTimeStartDate);
accessCode.expiresAt.setDate(virtualTimeStartDate.getDate() + dto.usagePeriod);
}

// TimeMachine 관련 필드 설정
accessCode.timeMachineEnabled = true;
accessCode.virtualTimeStartDate = new Date(dto.timeMachineOptions.virtualTimeStartDate);
accessCode.expirationBasedOnVirtualTime = dto.timeMachineOptions.expirationBasedOnVirtualTime;
}

// ... 기존 코드

return accessCode;
}
}

Access Code 사용 시 TimeMachine 통합

Access Code 사용 시 TimeMachine과의 통합 방법을 설명합니다.

코드 사용 시 사용자 가상 시간 초기화

@Injectable()
export class AccessCodeService {
// ... 기존 코드

async useAccessCode(codeId: string, dto: UseAccessCodeDto, ipAddress: string): Promise<UsedAccessCode> {
// ... 기존 코드

// TimeMachine 옵션 처리
let virtualTimeUsed = null;

if (dto.timeMachineOptions?.useTimeMachine) {
// 옵션 유효성 검증
await this.timeMachineService.validateTimeMachineOptions(dto.timeMachineOptions);

// 가상 시간 설정
virtualTimeUsed = new Date(dto.timeMachineOptions.virtualTimeStartDate);

// 기존 코드 설정 덮어쓰기 여부
if (dto.timeMachineOptions.overrideCodeSettings) {
accessCode.timeMachineEnabled = true;
accessCode.virtualTimeStartDate = virtualTimeUsed;
}
}

// ... 기존 코드

// 사용자 등록 시 TimeMachine 설정 동기화
if (accessCode.timeMachineEnabled &&
(accessCode.virtualTimeStartDate || virtualTimeUsed) &&
dto.timeMachineOptions?.synchronizeWithUserRegistration !== false) {
await this.timeMachineService.initializeUserVirtualTime(
dto.userId,
accessCode.virtualTimeStartDate || virtualTimeUsed,
);
}

// ... 기존 코드
}
}

일괄 코드 생성 시 TimeMachine 통합

대량의 Access Code를 생성할 때 TimeMachine과의 통합 방법을 설명합니다.

일괄 코드 생성 시 TimeMachine 옵션 처리

@Injectable()
export class AccessCodeService {
// ... 기존 코드

async createBatchAccessCodes(dto: BatchCreateAccessCodeDto, ipAddress: string): Promise<BatchResult> {
// ... 기존 코드

// 배치 TimeMachine 옵션 설정
if (dto.timeMachineOptions?.useTimeMachineForAll) {
// 옵션 유효성 검증
await this.timeMachineService.validateTimeMachineOptions({
useTimeMachine: true,
virtualTimeStartDate: dto.timeMachineOptions.commonVirtualTimeStartDate,
});

batch.useTimeMachineForAll = true;
batch.commonVirtualTimeStartDate = new Date(dto.timeMachineOptions.commonVirtualTimeStartDate);
}

// ... 기존 코드

// 개별 코드 TimeMachine 옵션 설정
for (let i = 0; i < dto.count; i++) {
// ... 기존 코드

// TimeMachine 옵션 추가
if (dto.timeMachineOptions?.useTimeMachineForAll) {
codeDto.timeMachineOptions = {
useTimeMachine: true,
virtualTimeStartDate: dto.timeMachineOptions.commonVirtualTimeStartDate,
expirationBasedOnVirtualTime: dto.timeMachineOptions.expirationBasedOnVirtualTime,
timeMachineReason: dto.timeMachineOptions.reason,
};
}

// ... 기존 코드
}

// ... 기존 코드
}
}

시간 의존적 작업 처리

TimeMachine 통합 환경에서 시간 의존적 작업(만료 처리 등)을 처리하는 방법을 설명합니다.

코드 만료 처리

@Injectable()
export class ExpirationService {
constructor(
private readonly accessCodeRepository: AccessCodeRepository,
private readonly timeMachineClient: TimeMachineClient,
private readonly pubSubClient: PubSub,
private readonly logger: Logger
) {
this.logger = new Logger(ExpirationService.name);
}

@Cron('0 0 * * *') // 매일 자정에 실행
async processExpiredCodes(): Promise<void> {
// 오늘 만료되는 코드 조회 (실제 시간 기준)
const today = new Date();
today.setHours(0, 0, 0, 0);

const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);

const expiredCodes = await this.accessCodeRepository.findCodesExpiringBetween(
today,
tomorrow,
false, // 가상 시간 기준 아님
);

// 처리
for (const code of expiredCodes) {
await this.markCodeAsExpired(code.id);
}

// 가상 시간 기준으로 만료되는 코드 처리
await this.processVirtualTimeExpiredCodes();
}

async processVirtualTimeExpiredCodes(): Promise<void> {
// 현재 가상 시간 조회
const virtualTimeInfo = await this.timeMachineClient.getCurrentVirtualTime();

if (!virtualTimeInfo.useVirtualTime) {
return; // 가상 시간 사용 안 함
}

// 가상 시간 기준 오늘 만료되는 코드 조회
const virtualToday = new Date(virtualTimeInfo.virtualTime);
virtualToday.setHours(0, 0, 0, 0);

const virtualTomorrow = new Date(virtualToday);
virtualTomorrow.setDate(virtualToday.getDate() + 1);

const expiredCodes = await this.accessCodeRepository.findCodesExpiringBetween(
virtualToday,
virtualTomorrow,
true, // 가상 시간 기준
);

// 처리
for (const code of expiredCodes) {
await this.markCodeAsExpired(code.id, true);
}
}

private async markCodeAsExpired(codeId: string, isVirtualTime = false): Promise<void> {
const code = await this.accessCodeRepository.findById(codeId);

if (!code || code.status !== AccessCodeStatus.UNUSED) {
return;
}

code.status = AccessCodeStatus.EXPIRED;
await this.accessCodeRepository.save(code);

// GCP Pub/Sub을 통한 만료 이벤트 발행
const topic = this.pubSubClient.topic('access-code-events');
const eventData = {
eventType: 'access-code.expired',
payload: {
accessCodeId: code.id,
expiredAt: Date.now().toString()
},
version: '1.0',
occurredAt: Date.now().toString()
};

await topic.publish(Buffer.from(JSON.stringify(eventData)));
}
}

이벤트 통합 구현

Pub/Sub 이벤트 인터페이스

// 이벤트 타입 정의
export enum AccessCodeEventType {
CREATED_WITH_TIME_MACHINE = 'access-code.created-with-time-machine',
USED_WITH_TIME_MACHINE = 'access-code.used-with-time-machine',
VIRTUAL_TIME_INITIALIZED = 'access-code.virtual-time-initialized',
EXPIRED_BY_VIRTUAL_TIME = 'access-code.expired-by-virtual-time'
}

// Pub/Sub 메시지 인터페이스
export interface PubSubMessage {
message: {
data: string;
messageId: string;
publishTime: string;
attributes?: Record<string, string>;
};
subscription: string;
}

// 이벤트 페이로드 인터페이스
export interface AccessCodeCreatedWithTimeMachinePayload {
accessCodeId: string;
code: string;
createdAt: string;
expiresAt: string;
virtualTimeStartDate: string;
type: string;
}

export interface AccessCodeUsedWithTimeMachinePayload {
accessCodeId: string;
userId: string;
usedAt: string;
virtualTimeStartDate: string;
}

export interface VirtualTimeInitializedPayload {
userId: string;
virtualTimeStartDate: string;
source: string;
}

export interface AccessCodeExpiredByVirtualTimePayload {
accessCodeId: string;
expiredAt: string;
virtualTimeUsed: boolean;
}

이벤트 구독 컨트롤러

@Controller('webhooks/pubsub')
export class TimeMachineEventController {
constructor(
private readonly logger: Logger,
private readonly auditService: AuditService,
) {
this.logger = new Logger(TimeMachineEventController.name);
}

@Post('access-code-events')
@HttpCode(HttpStatus.OK)
async handlePubSubMessage(@Body() message: PubSubMessage): Promise<any> {
try {
const eventData = JSON.parse(
Buffer.from(message.message.data, 'base64').toString()
);

switch (eventData.eventType) {
case AccessCodeEventType.CREATED_WITH_TIME_MACHINE:
await this.handleAccessCodeCreatedWithTimeMachine(eventData.payload);
break;

case AccessCodeEventType.USED_WITH_TIME_MACHINE:
await this.handleAccessCodeUsedWithTimeMachine(eventData.payload);
break;

case AccessCodeEventType.VIRTUAL_TIME_INITIALIZED:
await this.handleVirtualTimeInitialized(eventData.payload);
break;

case AccessCodeEventType.EXPIRED_BY_VIRTUAL_TIME:
await this.handleAccessCodeExpiredByVirtualTime(eventData.payload);
break;
}

return { success: true };
} catch (error) {
this.logger.error(`Pub/Sub 메시지 처리 오류: ${error.message}`);
// 비즈니스 로직 오류는 200을 반환하여 재시도 방지
if (error instanceof BusinessLogicException) {
return { success: false, error: error.message };
}

// 그 외 오류는 400 반환하여 Pub/Sub이 재시도하도록 함
throw new BadRequestException('Invalid Pub/Sub message');
}
}

private async handleAccessCodeCreatedWithTimeMachine(payload: AccessCodeCreatedWithTimeMachinePayload): Promise<void> {
this.logger.log(`TimeMachine 옵션으로 Access Code 생성: ${payload.accessCodeId}`);

// 감사 로그 기록
await this.auditService.logAccessCodeCreatedWithTimeMachine({
accessCodeId: payload.accessCodeId,
virtualTimeStartDate: payload.virtualTimeStartDate,
createdAt: payload.createdAt
});
}

private async handleAccessCodeUsedWithTimeMachine(payload: AccessCodeUsedWithTimeMachinePayload): Promise<void> {
this.logger.log(`TimeMachine 옵션으로 Access Code 사용: ${payload.accessCodeId}`);

// 감사 로그 기록
await this.auditService.logAccessCodeUsedWithTimeMachine({
accessCodeId: payload.accessCodeId,
userId: payload.userId,
virtualTimeStartDate: payload.virtualTimeStartDate,
usedAt: payload.usedAt
});
}

private async handleVirtualTimeInitialized(payload: VirtualTimeInitializedPayload): Promise<void> {
this.logger.log(`사용자 ${payload.userId}의 가상 시간 초기화: ${payload.virtualTimeStartDate}`);

// 감사 로그 기록
await this.auditService.logVirtualTimeInitialized({
userId: payload.userId,
virtualTimeStartDate: payload.virtualTimeStartDate,
source: payload.source || 'ACCESS_CODE',
});
}

private async handleAccessCodeExpiredByVirtualTime(payload: AccessCodeExpiredByVirtualTimePayload): Promise<void> {
this.logger.log(`가상 시간으로 Access Code 만료: ${payload.accessCodeId}`);

// 감사 로그 기록
await this.auditService.logAccessCodeExpiredByVirtualTime({
accessCodeId: payload.accessCodeId,
expiredAt: payload.expiredAt,
virtualTimeUsed: payload.virtualTimeUsed
});
}
}

오류 처리

TimeMachine 통합 관련 오류 처리 방법을 설명합니다.

TimeMachine 관련 예외 정의

export class InvalidVirtualTimeException extends BadRequestException {
constructor() {
super({
code: 4001,
message: 'INVALID_VIRTUAL_TIME',
detail: '가상 시간 설정이 유효하지 않음',
});
}
}

export class TimeMachineDisabledException extends ConflictException {
constructor() {
super({
code: 4002,
message: 'TIME_MACHINE_DISABLED',
detail: 'TimeMachine 기능이 비활성화됨',
});
}
}

export class FutureVirtualTimeException extends BadRequestException {
constructor() {
super({
code: 4003,
message: 'FUTURE_VIRTUAL_TIME',
detail: '현재보다 미래의 가상 시간으로 설정 불가',
});
}
}

export class VirtualTimeTooOldException extends BadRequestException {
constructor(maxPastDays: number) {
super({
code: 4004,
message: 'VIRTUAL_TIME_TOO_OLD',
detail: `설정 가능한 범위(${maxPastDays}일)보다 오래된 가상 시간`,
});
}
}

export class TimeMachineSyncException extends InternalServerErrorException {
constructor() {
super({
code: 4005,
message: 'TIME_MACHINE_SYNC_ERROR',
detail: 'TimeMachine과의 동기화 중 오류 발생',
});
}
}

오류 처리 예제

@Injectable()
export class TimeMachineIntegrationService {
// ... 기존 코드

async validateTimeMachineOptions(options: TimeMachineOptionsDto): Promise<void> {
try {
// TimeMachine 서비스 가용성 확인
const status = await this.isTimeMachineAvailable();
if (!status) {
throw new TimeMachineDisabledException();
}

// 미래 시간 설정 불가
const currentTime = new Date();
const virtualTime = new Date(options.virtualTimeStartDate);

if (virtualTime > currentTime) {
throw new FutureVirtualTimeException();
}

// 너무 오래된 과거 시간 설정 제한
const maxPastDays = this.configService.get('timeMachine.maxPastDays', 365);
const minAllowedDate = new Date();
minAllowedDate.setDate(minAllowedDate.getDate() - maxPastDays);

if (virtualTime < minAllowedDate) {
throw new VirtualTimeTooOldException(maxPastDays);
}
} catch (error) {
// 오류 로깅
this.logger.error(`TimeMachine 옵션 검증 실패: ${error.message}`);

// 기본 오류 변환
if (!(error instanceof HttpException)) {
throw new InvalidVirtualTimeException();
}

throw error;
}
}

private async isTimeMachineAvailable(): Promise<boolean> {
try {
const response = await this.httpService.get(
`${this.configService.get('timeMachine.apiUrl')}/health`,
).toPromise();

return response.status === 200;
} catch (error) {
this.logger.error(`TimeMachine 서비스 가용성 확인 실패: ${error.message}`);
return false;
}
}
}

모니터링 및 로깅

TimeMachine 통합 관련 모니터링 및 로깅 방법을 설명합니다.

로깅 설정

@Injectable()
export class TimeMachineIntegrationService {
private readonly logger = new Logger(TimeMachineIntegrationService.name);

// ... 기존 코드

async initializeUserVirtualTime(userId: string, virtualTimeStartDate: Date): Promise<void> {
this.logger.log(`사용자 ${userId}의 가상 시간 초기화 시작: ${virtualTimeStartDate.getTime()}`);

try {
// TimeMachine API를 통해 사용자 가상 시간 초기화
await this.timeMachineClient.initializeUserVirtualTime(userId, {
virtualTimeStartDate: virtualTimeStartDate.getTime(),
source: 'ACCESS_CODE',
reason: 'Access Code 사용에 따른 초기화',
});

// 사용자 등록 정보 업데이트
await this.updateUserRegistration(userId, virtualTimeStartDate);

this.logger.log(`사용자 ${userId}의 가상 시간 초기화 완료`);

// GCP Pub/Sub을 통한 이벤트 발행
const topic = this.pubSubClient.topic('access-code-events');
const eventData = {
eventType: 'access-code.virtual-time-initialized',
payload: {
userId: userId,
virtualTimeStartDate: virtualTimeStartDate.getTime(),
source: 'ACCESS_CODE'
},
version: '1.0',
occurredAt: Date.now()
};

await topic.publish(Buffer.from(JSON.stringify(eventData)));
} catch (error) {
this.logger.error(`사용자 ${userId}의 가상 시간 초기화 실패: ${error.message}`);
throw new TimeMachineSyncException();
}
}
}

메트릭

@Injectable()
export class TimeMachineMetricService {
private readonly virtualTimeCodesCreated = new Counter({
name: 'access_code_virtual_time_created_total',
help: 'TimeMachine으로 생성된 총 Access Code 수',
});

private readonly virtualTimeCodesUsed = new Counter({
name: 'access_code_virtual_time_used_total',
help: 'TimeMachine으로 사용된 총 Access Code 수',
});

private readonly virtualTimeInitialized = new Counter({
name: 'access_code_virtual_time_initialized_total',
help: 'Access Code에 의해 초기화된 총 사용자 가상 시간 수',
labelNames: ['source'],
});

recordVirtualTimeCodeCreated(): void {
this.virtualTimeCodesCreated.inc();
}

recordVirtualTimeCodeUsed(): void {
this.virtualTimeCodesUsed.inc();
}

recordVirtualTimeInitialized(source: string): void {
this.virtualTimeInitialized.inc({ source });
}
}

변경 이력

버전날짜작성자변경 내용
0.1.02025-04-24bok@weltcorp.com최초 작성