Peace 1 month ago
parent 0bda898e8b
commit 774a623304
  1. 4
      backend/dist/app.module.js
  2. 2
      backend/dist/app.module.js.map
  3. 2
      backend/dist/tsconfig.build.tsbuildinfo
  4. 6
      backend/src/app.module.ts
  5. 13
      backend/src/common/core.module.ts
  6. 15
      backend/src/common/error-logs/error-logs.service.ts
  7. 86
      backend/src/common/filters/global-exception.filter.ts
  8. 28
      backend/src/health/dto/health-response.dto.ts
  9. 18
      backend/src/health/health.controller.spec.ts
  10. 22
      backend/src/health/health.controller.ts
  11. 7
      backend/src/health/health.module.ts

@ -18,6 +18,8 @@ const users_module_1 = require("./users/users.module");
const jwt_1 = require("@nestjs/jwt"); const jwt_1 = require("@nestjs/jwt");
const profiles_module_1 = require("./profiles/profiles.module"); const profiles_module_1 = require("./profiles/profiles.module");
const sensors_module_1 = require("./sensors/sensors.module"); const sensors_module_1 = require("./sensors/sensors.module");
const health_module_1 = require("./health/health.module");
const core_module_1 = require("./common/core.module");
let AppModule = class AppModule { let AppModule = class AppModule {
}; };
exports.AppModule = AppModule; exports.AppModule = AppModule;
@ -45,6 +47,8 @@ exports.AppModule = AppModule = __decorate([
users_module_1.UsersModule, users_module_1.UsersModule,
profiles_module_1.ProfilesModule, profiles_module_1.ProfilesModule,
sensors_module_1.SensorsModule, sensors_module_1.SensorsModule,
core_module_1.CoreModule,
health_module_1.HealthModule,
], ],
controllers: [app_controller_1.AppController], controllers: [app_controller_1.AppController],
providers: [app_service_1.AppService, jwt_1.JwtService], providers: [app_service_1.AppService, jwt_1.JwtService],

@ -1 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,2CAA6D;AAC7D,6CAAgD;AAChD,+BAA4B;AAC5B,oDAAgD;AAChD,uDAAmD;AACnD,qCAAyC;AACzC,gEAA4D;AAC5D,6DAAyD;AA6BlD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA3BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,MAAM,CAAC,GAAG,CAAS,SAAS,CAAC;oBACnC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAS,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;oBACzD,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,CAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;oBACrD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAS,UAAU,CAAC,KAAK,YAAY;oBAC5D,gBAAgB,EAAE,IAAI;oBACtB,OAAO,EAAE,IAAI;iBACd,CAAC;aACH,CAAC;YACF,wBAAU;YACV,0BAAW;YACX,gCAAc;YACd,8BAAa;SACd;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,gBAAU,CAAC;KACpC,CAAC;GACW,SAAS,CAAG"} {"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,2CAA6D;AAC7D,6CAAgD;AAChD,+BAA4B;AAC5B,oDAAgD;AAChD,uDAAmD;AACnD,qCAAyC;AACzC,gEAA4D;AAC5D,6DAAyD;AACzD,0DAAsD;AACtD,sDAAkD;AA+B3C,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA7BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,MAAM,CAAC,GAAG,CAAS,SAAS,CAAC;oBACnC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAS,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;oBACzD,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,CAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;oBACrD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAS,UAAU,CAAC,KAAK,YAAY;oBAC5D,gBAAgB,EAAE,IAAI;oBACtB,OAAO,EAAE,IAAI;iBACd,CAAC;aACH,CAAC;YACF,wBAAU;YACV,0BAAW;YACX,gCAAc;YACd,8BAAa;YACb,wBAAU;YACV,4BAAY;SACb;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,gBAAU,CAAC;KACpC,CAAC;GACW,SAAS,CAAG"}

File diff suppressed because one or more lines are too long

@ -8,7 +8,9 @@ import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { ProfilesModule } from './profiles/profiles.module'; import { ProfilesModule } from './profiles/profiles.module';
import { SensorsModule } from './sensors/sensors.module'; import { SensorsModule } from './sensors/sensors.module';
import { HealthModule } from './health/health.module';
import { CoreModule } from './common/core.module';
@Module({ @Module({
imports: [ imports: [
@ -33,6 +35,8 @@ import { SensorsModule } from './sensors/sensors.module';
UsersModule, UsersModule,
ProfilesModule, ProfilesModule,
SensorsModule, SensorsModule,
CoreModule,
HealthModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, JwtService], providers: [AppService, JwtService],

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ErrorLog } from './entities/error-log.entity';
import { ErrorLogsService } from './error-logs/error-logs.service';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './filters/global-exception.filter';
@Module({
imports: [TypeOrmModule.forFeature([ErrorLog])],
providers: [ErrorLogsService, { provide: APP_FILTER, useClass: GlobalExceptionFilter }],
exports: [ErrorLogsService],
})
export class CoreModule {}

@ -0,0 +1,15 @@
import { InjectRepository } from '@nestjs/typeorm';
import { ErrorLog } from '../entities/error-log.entity';
import { Repository } from 'typeorm';
export class ErrorLogsService {
constructor(@InjectRepository(ErrorLog) private readonly repo: Repository<ErrorLog>) {}
async write(log: Partial<ErrorLog>): Promise<void> {
try {
await this.repo.insert(log);
} catch {
// 로깅 실패는 애플리케이션 흐름에 영향을 주지 않음
}
}
}

@ -0,0 +1,86 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ErrorLogsService } from '../error-logs/error-logs.service';
type JwtUser = { userId?: number; username?: string };
type AuthedRequest = Request & { user?: JwtUser };
function getStatus(exception: unknown): number {
return exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
}
function extractMessage(exception: unknown): string {
if (exception instanceof HttpException) {
const res = exception.getResponse();
if (typeof res === 'string') return res;
if (res && typeof res === 'object') {
const msg = (res as Record<string, unknown>).message;
if (typeof msg == 'string') return msg;
if (Array.isArray(msg)) return msg.join(', ');
const err = (res as Record<string, unknown>).error;
if (typeof err === 'string') return err;
}
return exception.message ?? 'Http exception';
}
if (exception instanceof Error) return exception.message;
return 'Internal server error';
}
function extractStack(exception: unknown): string | undefined {
if (exception instanceof HttpException) return undefined;
if (exception instanceof Error) return exception.stack;
return undefined;
}
@Catch()
@Injectable()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
constructor(private readonly errorLogs: ErrorLogsService) {}
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const req = ctx.getRequest<AuthedRequest>();
const res = ctx.getResponse<Response>();
const status = getStatus(exception);
const message = extractMessage(exception);
const stack = extractStack(exception);
void this.errorLogs
.write({
method: req.method,
path: req.originalUrl ?? req.url,
statusCode: status,
message,
stack,
userId: req.user?.userId,
ip: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || undefined,
})
.catch((err: unknown) => {
const msg = err instanceof Error ? err.message : String(err);
this.logger.warn(`Failed to persist error log: ${msg}`);
});
res.status(status).json({
statusCode: status,
message,
path: req.originalUrl || req.url,
timestamp: new Date().toISOString(),
});
}
}

@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
export class HealthResponseDto {
@ApiProperty({ description: '서버헬스', enum: ['ok', 'down'], example: 'ok' })
status!: 'ok' | 'down';
@ApiProperty({ description: 'DB헬스', enum: ['up', 'down'], example: 'up' })
db!: 'up' | 'down';
@ApiProperty({ description: '체크시간', example: '2025-01-01T00:00:00.000Z' })
time!: string;
static ok(): HealthResponseDto {
const d = new HealthResponseDto();
d.status = 'ok';
d.db = 'up';
d.time = new Date().toISOString();
return d;
}
static down(): HealthResponseDto {
const d = new HealthResponseDto();
d.status = 'down';
d.db = 'down';
d.time = new Date().toISOString();
return d;
}
}

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthController } from './health.controller';
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
}).compile();
controller = module.get<HealthController>(HealthController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

@ -0,0 +1,22 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { DataSource } from 'typeorm';
import { HealthResponseDto } from './dto/health-response.dto';
@ApiTags('헬스체크')
@Controller('health')
export class HealthController {
constructor(private readonly dataSource: DataSource) {}
@Get()
@ApiOperation({ summary: '어플리케이션 헬스체크' })
@ApiOkResponse({ description: '상태', type: HealthResponseDto })
async get(): Promise<HealthResponseDto> {
try {
await this.dataSource.query('SELECT 1'); // DB ping
return HealthResponseDto.ok();
} catch {
return HealthResponseDto.down();
}
}
}

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController]
})
export class HealthModule {}
Loading…
Cancel
Save