diff --git a/backend/src/modules/course-generation/course-generation.runner.ts b/backend/src/modules/course-generation/course-generation.runner.ts index 077c8d9..0d6f11e 100644 --- a/backend/src/modules/course-generation/course-generation.runner.ts +++ b/backend/src/modules/course-generation/course-generation.runner.ts @@ -54,15 +54,24 @@ export class CourseGenerationRunner private readonly pipeline: CourseGenerationPipeline, ) {} - /** 서버가 한 대라 기동할 때 남아 있는 작업은 모두 지난 프로세스의 것이다 */ + /** + * 서버가 한 대라 기동할 때 남아 있는 작업은 모두 지난 프로세스의 것이다. + * 정리일 뿐이라 실패해도 기동은 막지 않는다 — 마이그레이션 전이라 테이블이 없을 때 서버 전체가 뜨지 않았다(2026-11-11) + */ async onApplicationBootstrap(): Promise { - const result = await this.generations.update( - { status: In([...ACTIVE_STATUSES]) }, - this.failedValues(), - ); - if (result.affected) { - this.logger.warn( - `지난 실행에서 끝나지 못한 코스 생성 ${result.affected}건을 실패로 바꿨다`, + try { + const result = await this.generations.update( + { status: In([...ACTIVE_STATUSES]) }, + this.failedValues(), + ); + if (result.affected) { + this.logger.warn( + `지난 실행에서 끝나지 못한 코스 생성 ${result.affected}건을 실패로 바꿨다`, + ); + } + } catch (error) { + this.logger.error( + `[SYS_001] 끝나지 못한 코스 생성을 정리하지 못했다: ${error instanceof Error ? error.message : String(error)}`, ); } } diff --git a/backend/test/course-generation.e2e-spec.ts b/backend/test/course-generation.e2e-spec.ts index 7ea6951..6200fb9 100644 --- a/backend/test/course-generation.e2e-spec.ts +++ b/backend/test/course-generation.e2e-spec.ts @@ -171,6 +171,9 @@ describe('AI 코스 생성 API (e2e)', () => { }), }) .compile(); + // 운영처럼 마이그레이션을 기동 훅보다 먼저 돌린다 — 빈 DB 에서 실행기의 기동 정리가 테이블을 찾는다 + ds = moduleFixture.get(DataSource); + await ds.runMigrations(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api'); app.use(new RequestIdMiddleware().use.bind(new RequestIdMiddleware())); @@ -184,9 +187,6 @@ describe('AI 코스 생성 API (e2e)', () => { app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector))); await app.init(); - - ds = app.get(DataSource); - await ds.runMigrations(); await cleanup(); const places = ds.getRepository(Place); @@ -216,8 +216,12 @@ describe('AI 코스 생성 API (e2e)', () => { }); afterAll(async () => { - await cleanup(); - await app.close(); + // 준비가 실패해도 앱은 닫는다 — 닫지 않으면 주기 작업이 남아 jest 가 끝나지 않는다 + try { + await cleanup(); + } finally { + await app.close(); + } }); afterEach(() => { diff --git a/backend/test/festival.e2e-spec.ts b/backend/test/festival.e2e-spec.ts index 78c2569..1fae675 100644 --- a/backend/test/festival.e2e-spec.ts +++ b/backend/test/festival.e2e-spec.ts @@ -66,6 +66,9 @@ describe('축제·행사 조회 API (e2e)', () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); + // 운영처럼 마이그레이션을 기동 훅보다 먼저 돌린다 — 빈 DB 에서 기동 훅(코스 생성 정리)이 테이블을 찾는다 + ds = moduleFixture.get(DataSource); + await ds.runMigrations(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api'); app.use(new RequestIdMiddleware().use.bind(new RequestIdMiddleware())); @@ -79,9 +82,6 @@ describe('축제·행사 조회 API (e2e)', () => { app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector))); await app.init(); - - ds = app.get(DataSource); - await ds.runMigrations(); await cleanup(); const repo = ds.getRepository(FestivalEvent); @@ -156,8 +156,12 @@ describe('축제·행사 조회 API (e2e)', () => { }); afterAll(async () => { - await cleanup(); - await app.close(); + // 준비가 실패해도 앱은 닫는다 — 닫지 않으면 주기 작업이 남아 jest 가 끝나지 않는다 + try { + await cleanup(); + } finally { + await app.close(); + } }); const get = (path: string) => request(app.getHttpServer()).get(path); diff --git a/backend/test/place.e2e-spec.ts b/backend/test/place.e2e-spec.ts index 3debd6c..92449e8 100644 --- a/backend/test/place.e2e-spec.ts +++ b/backend/test/place.e2e-spec.ts @@ -83,6 +83,9 @@ describe('장소 조회 API (e2e)', () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); + // 운영처럼 마이그레이션을 기동 훅보다 먼저 돌린다 — 빈 DB 에서 기동 훅(코스 생성 정리)이 테이블을 찾는다 + ds = moduleFixture.get(DataSource); + await ds.runMigrations(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api'); app.use(new RequestIdMiddleware().use.bind(new RequestIdMiddleware())); @@ -96,9 +99,6 @@ describe('장소 조회 API (e2e)', () => { app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new TransformInterceptor(app.get(Reflector))); await app.init(); - - ds = app.get(DataSource); - await ds.runMigrations(); await cleanup(); const places = ds.getRepository(Place); @@ -245,8 +245,12 @@ describe('장소 조회 API (e2e)', () => { }); afterAll(async () => { - await cleanup(); - await app.close(); + // 준비가 실패해도 앱은 닫는다 — 닫지 않으면 주기 작업이 남아 jest 가 끝나지 않는다 + try { + await cleanup(); + } finally { + await app.close(); + } }); const get = (path: string) => request(app.getHttpServer()).get(path);