chore: TypeORM 마이그레이션 셋업 + 초기 스키마 마이그레이션
운영(synchronize 비활성, migrationsRun 활성)에서 스키마를 구성할 마이그레이션이 전무해 신규 운영 DB 배포 시 테이블이 생성되지 않던 문제를 해결한다. - data-source.ts(CLI 전용 DataSource) + package.json 마이그레이션 스크립트 (typeorm/migration:generate/create/run/revert) 추가 - 초기 마이그레이션(CreateInitialSchema) 생성 — 전체 스키마(users·projects· project_members·tasks·task_assignees·checklist_items·comments·attachments· activities·notifications·refresh_sessions·ai_conversations·ai_messages, 인덱스·FK 포함), uuid-ossp 확장 생성 보장 - 임시 빈 DB에 up/down 적용 검증, 현재 dev 스키마와 드리프트 없음(No changes) 확인 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import 'reflect-metadata';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
// TypeORM CLI 전용 DataSource — 마이그레이션 생성/실행/되돌리기(generate/run/revert)에 사용한다.
|
||||
// 런타임(앱)은 app.module 의 TypeOrmModule 설정을 사용하며, 운영에서는 migrationsRun=true 로
|
||||
// 부팅 시 이 폴더(dist/migrations)의 마이그레이션을 자동 실행한다.
|
||||
// DB 접속값은 환경변수에서 읽는다(dev/CI/운영에서 주입). app.module 과 동일한 키를 사용.
|
||||
export default new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
username: process.env.DB_USER || process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
database: process.env.DB_NAME || process.env.DB_DATABASE || 'project_db',
|
||||
// CLI 는 절대 스키마 자동 동기화를 하지 않는다(마이그레이션만 사용)
|
||||
synchronize: false,
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateInitialSchema1782060613058 implements MigrationInterface {
|
||||
name = 'CreateInitialSchema1782060613058';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// uuid_generate_v4() 기본값에 필요한 확장 — 신규(빈) 운영 DB 에서도 보장
|
||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password_hash" character varying, "provider" character varying NOT NULL DEFAULT 'local', "email_verified" boolean NOT NULL DEFAULT false, "email_verify_token_hash" character varying, "email_verify_expires_at" TIMESTAMP WITH TIME ZONE, "password_reset_token_hash" character varying, "password_reset_expires_at" TIMESTAMP WITH TIME ZONE, "name" character varying NOT NULL, "role" character varying, "avatar_url" character varying, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "project_members" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "role_type" character varying NOT NULL DEFAULT 'member', "is_owner" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "projectId" uuid, "userId" uuid, CONSTRAINT "UQ_326b2a901eb18ac24eabc9b0581" UNIQUE ("projectId", "userId"), CONSTRAINT "PK_0b2f46f804be4aea9234c78bcc9" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "projects" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" character varying, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6271df0a7aed1d6c0691ce6ac50" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "checklist_items" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "text" character varying NOT NULL, "category" character varying NOT NULL DEFAULT '필수', "done" boolean NOT NULL DEFAULT false, "review_note" text, "sort_order" integer NOT NULL DEFAULT '0', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "taskId" uuid, CONSTRAINT "PK_bae00945a1d4789bd648e583e29" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "seq" integer NOT NULL, "title" character varying NOT NULL, "content" jsonb NOT NULL DEFAULT '[]', "status" character varying NOT NULL DEFAULT 'todo', "due_date" TIMESTAMP WITH TIME ZONE, "approval_note" text, "approval_requested_at" TIMESTAMP WITH TIME ZONE, "changes_note" text, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "project_id" uuid, "issuerId" uuid, "approval_requested_by" uuid, CONSTRAINT "UQ_011566614ffa01ec7281ee29754" UNIQUE ("project_id", "seq"), CONSTRAINT "PK_8d12ff38fcc62aaba2cab748772" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_73f95adb41a77c1bfd7e19dca7" ON "tasks" ("seq") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "attachments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "stored_name" character varying NOT NULL, "size" integer NOT NULL, "mime" character varying NOT NULL, "kind" character varying NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "task_id" uuid, "uploader_id" uuid, CONSTRAINT "PK_5e1f050bcff31e3084a1d662412" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "comments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "text" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "task_id" uuid, "author_id" uuid, "parent_id" uuid, "attachment_id" uuid, CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "payload" jsonb NOT NULL DEFAULT '{}', "read" boolean NOT NULL DEFAULT false, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "recipient_id" uuid, CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_92be92313f09987bc6ad70ab41" ON "notifications" ("recipient_id", "created_at") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "refresh_sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "current_jti" uuid NOT NULL, "prev_jti" uuid, "prev_at" TIMESTAMP WITH TIME ZONE, "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "user_id" uuid NOT NULL, CONSTRAINT "PK_9190032f6967b7971dca07d69f3" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ai_conversations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(120) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "user_id" uuid NOT NULL, CONSTRAINT "PK_60db12765b82858ba00c8aa4ae2" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "activities" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "task_seq" integer, "type" character varying NOT NULL, "payload" jsonb NOT NULL DEFAULT '{}', "created_at" TIMESTAMP NOT NULL DEFAULT now(), "project_id" uuid, "actor_id" uuid, CONSTRAINT "PK_7f4004429f731ffb9c88eb486a8" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_7297db78eb572fa29db5aaf815" ON "activities" ("project_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_8291258c0c575bdab03443ef47" ON "activities" ("task_seq") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ai_messages" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "role" character varying NOT NULL, "content" text NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "conversation_id" uuid NOT NULL, CONSTRAINT "PK_a390434d4a515ba18a41bc996c2" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "task_assignees" ("task_id" uuid NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_7ae8012667c1cc4ca8266002afc" PRIMARY KEY ("task_id", "user_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0141288f2306f20da9a60ec8d6" ON "task_assignees" ("task_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_bb8051e376a2b083e074678cb6" ON "task_assignees" ("user_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "project_members" ADD CONSTRAINT "FK_d19892d8f03928e5bfc7313780c" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "project_members" ADD CONSTRAINT "FK_08d1346ff91abba68e5a637cfdb" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "checklist_items" ADD CONSTRAINT "FK_1c0abaeeca0b213ddd07675c578" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_9eecdb5b1ed8c7c2a1b392c28d4" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_950b234a262e85a895b9f9e634e" FOREIGN KEY ("issuerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_60f1ed134a6425431bc66b0eefd" FOREIGN KEY ("approval_requested_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "attachments" ADD CONSTRAINT "FK_e62fd181b97caa6b150b09220b1" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "attachments" ADD CONSTRAINT "FK_73407cf2d2a0e64546bacf309a7" FOREIGN KEY ("uploader_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_18c2493067c11f44efb35ca0e03" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_e6d38899c31997c45d128a8973b" FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_d6f93329801a93536da4241e386" FOREIGN KEY ("parent_id") REFERENCES "comments"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" ADD CONSTRAINT "FK_45d9ed1dd52f601ae9ec8034351" FOREIGN KEY ("attachment_id") REFERENCES "attachments"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" ADD CONSTRAINT "FK_5332a4daa46fd3f4e6625dd275d" FOREIGN KEY ("recipient_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refresh_sessions" ADD CONSTRAINT "FK_a7ab4fd82c654c85b9de53d971a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ai_conversations" ADD CONSTRAINT "FK_12fdbf99ca0da93085d61edd3bb" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "activities" ADD CONSTRAINT "FK_7297db78eb572fa29db5aaf8158" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "activities" ADD CONSTRAINT "FK_78e06b2e7a3bd9b9239de4aa285" FOREIGN KEY ("actor_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ai_messages" ADD CONSTRAINT "FK_de21fcb2d1df7fd6ca70f555b6d" FOREIGN KEY ("conversation_id") REFERENCES "ai_conversations"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_assignees" ADD CONSTRAINT "FK_0141288f2306f20da9a60ec8d69" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_assignees" ADD CONSTRAINT "FK_bb8051e376a2b083e074678cb60" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_assignees" DROP CONSTRAINT "FK_bb8051e376a2b083e074678cb60"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "task_assignees" DROP CONSTRAINT "FK_0141288f2306f20da9a60ec8d69"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ai_messages" DROP CONSTRAINT "FK_de21fcb2d1df7fd6ca70f555b6d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "activities" DROP CONSTRAINT "FK_78e06b2e7a3bd9b9239de4aa285"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "activities" DROP CONSTRAINT "FK_7297db78eb572fa29db5aaf8158"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ai_conversations" DROP CONSTRAINT "FK_12fdbf99ca0da93085d61edd3bb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refresh_sessions" DROP CONSTRAINT "FK_a7ab4fd82c654c85b9de53d971a"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notifications" DROP CONSTRAINT "FK_5332a4daa46fd3f4e6625dd275d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_45d9ed1dd52f601ae9ec8034351"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_d6f93329801a93536da4241e386"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_e6d38899c31997c45d128a8973b"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "comments" DROP CONSTRAINT "FK_18c2493067c11f44efb35ca0e03"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "attachments" DROP CONSTRAINT "FK_73407cf2d2a0e64546bacf309a7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "attachments" DROP CONSTRAINT "FK_e62fd181b97caa6b150b09220b1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" DROP CONSTRAINT "FK_60f1ed134a6425431bc66b0eefd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" DROP CONSTRAINT "FK_950b234a262e85a895b9f9e634e"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tasks" DROP CONSTRAINT "FK_9eecdb5b1ed8c7c2a1b392c28d4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "checklist_items" DROP CONSTRAINT "FK_1c0abaeeca0b213ddd07675c578"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "project_members" DROP CONSTRAINT "FK_08d1346ff91abba68e5a637cfdb"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "project_members" DROP CONSTRAINT "FK_d19892d8f03928e5bfc7313780c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_bb8051e376a2b083e074678cb6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0141288f2306f20da9a60ec8d6"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "task_assignees"`);
|
||||
await queryRunner.query(`DROP TABLE "ai_messages"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_8291258c0c575bdab03443ef47"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7297db78eb572fa29db5aaf815"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "activities"`);
|
||||
await queryRunner.query(`DROP TABLE "ai_conversations"`);
|
||||
await queryRunner.query(`DROP TABLE "refresh_sessions"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_92be92313f09987bc6ad70ab41"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "notifications"`);
|
||||
await queryRunner.query(`DROP TABLE "comments"`);
|
||||
await queryRunner.query(`DROP TABLE "attachments"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_73f95adb41a77c1bfd7e19dca7"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "tasks"`);
|
||||
await queryRunner.query(`DROP TABLE "checklist_items"`);
|
||||
await queryRunner.query(`DROP TABLE "projects"`);
|
||||
await queryRunner.query(`DROP TABLE "project_members"`);
|
||||
await queryRunner.query(`DROP TABLE "users"`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user