/* * 조치 표에 **일꾼이 쓸 네 칸**을 더한다. * * ── 왜 표에 두나 ──────────────────────────────────────────────────────────── * 물러섬을 메모리에 두면 재기동에 사라지고, 그러면 물러서던 것이 전부 즉시 재시도가 된다. 그리고 * 일꾼이 여러 대가 되면 같은 답을 봐야 한다. * * dispatch_note_next 저쪽 말 중 「다음에 할 일」 한 줄 — 원인은 dispatch_note * retry 다시 해서 될 일인가 (again · after-fix · never) * attempts 몇 번 넘겨 봤나 — 목록의 「몇 번 해 봤나」 * next_attempt_at 다음에 넘겨 볼 시각(ISO) * * ── 타입을 짐작하지 않는다 ────────────────────────────────────────────────── * 엔티티가 `@Column({ nullable: true })` 로 두면 그것이 어느 컬럼 타입이 되는지는 **드라이버마다 * 다르다.** `'varchar'` 라고 적으면 sqlite·postgres·mysql 에서는 맞고 MSSQL·Oracle 에서 어긋난다. * * 그래서 같은 성질로 **이미 서 있는 형제 칸**의 타입을 읽어 그대로 쓴다 — 드라이버가 스스로 고른 * 답이라 다섯 곳에서 다 맞는다. 형제가 없으면 짐작하지 않고 멈춘다. * * 글자 셋 dispatch_note 에서 * 수 하나 effect_seen 에서 * * plant 레인이 오늘 `equipmentClassIds` 마이그레이션에서 같은 방법을 썼다. * * ── 멱등 ──────────────────────────────────────────────────────────────────── * 칸이 이미 있으면 아무것도 하지 않는다 — 개발 sqlite 는 엔티티 선언만으로 이미 만들어 둔다. * 표가 아직 없는 첫 기동에서도 할 일이 없다. */ import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm' const TABLE = 'twin_command_records' /** 글자 칸의 형제 — 같은 성질(선택·자유 문자열)로 이미 서 있다. */ const TEXT_SIBLING = 'dispatch_note' /** 수 칸의 형제 — 선택·정수. */ const INT_SIBLING = 'effect_seen' const TEXT_COLUMNS = ['dispatch_note_next', 'retry', 'next_attempt_at'] as const const INT_COLUMNS = ['attempts'] as const export class AddDispatchRetryColumns1786700000000 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const table = await queryRunner.getTable(TABLE) if (!table) return /* 첫 기동 — 엔티티가 만든다 */ const typeOf = (sibling: string): string | undefined => table.columns.find(c => c.name === sibling)?.type const textType = typeOf(TEXT_SIBLING) const intType = typeOf(INT_SIBLING) /* * 형제가 없으면 **멈춘다.** 짐작한 타입으로 칸을 세우면 어느 드라이버에서 어긋나는지 개발에서 * 안 잡히고, 그 배포에서만 난다. */ if (!textType || !intType) { throw new Error( `${TABLE} 에서 형제 칸을 못 찾았습니다 — ${TEXT_SIBLING}(${textType ?? '없음'}) · ` + `${INT_SIBLING}(${intType ?? '없음'}). 타입을 짐작하지 않고 멈춥니다` ) } for (const name of TEXT_COLUMNS) { if (table.columns.some(c => c.name === name)) continue await queryRunner.addColumn(TABLE, new TableColumn({ name, type: textType, isNullable: true })) } for (const name of INT_COLUMNS) { if (table.columns.some(c => c.name === name)) continue await queryRunner.addColumn(TABLE, new TableColumn({ name, type: intType, isNullable: true })) } } public async down(queryRunner: QueryRunner): Promise { const table = await queryRunner.getTable(TABLE) if (!table) return for (const name of [...TEXT_COLUMNS, ...INT_COLUMNS]) { if (!table.columns.some(c => c.name === name)) continue await queryRunner.dropColumn(TABLE, name) } } }