feat: 드라이브를 전사 공유 드라이브로 전환하고 전역 감사 로그 추가
개인별 격리(owner 기준)를 제거해 로그인 사용자 전원이 하나의 공유 트리를 보고 관리하도록 전환한다. 수정/삭제는 전원 허용하며, 행위 추적을 위해 전역 감사 로그(drive_activities, GET /drive/activity)를 추가한다. 업로더 탈퇴 시 공유 파일이 사라지지 않도록 owner FK를 CASCADE에서 SET NULL로 변경한다. - 백엔드: 접근 권한 해석 제거(단순 로더화), 업로드/이름변경/삭제 6지점에 감사 적재, DriveAuditService 신설, 운영 마이그레이션 1783200000000 추가 - 프론트: 목록 행에 업로더 표시, 툴바 '기록' 패널 추가 - 프로젝트 폴더 연동의 '본인 소유 폴더만' 제한 해제(공유 모델 일관성) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
// 전사 공유 드라이브 전환 + 전역 감사 로그.
|
||||
// - drive_activities: 누가 무엇을(추가/이름변경/삭제) 했는지 전역 감사 로그(프로젝트 무관).
|
||||
// - drive_files / drive_folders 의 owner_id FK: CASCADE → SET NULL 로 변경
|
||||
// (공유 드라이브에서 업로더/생성자가 탈퇴해도 공유 파일·폴더는 보존).
|
||||
// 운영(migrationsRun)에서만 실행되며, dev 는 synchronize 로 자동 반영된다.
|
||||
// 멱등·견고: IF NOT EXISTS / owner_id 의 기존 FK 를 동적으로 모두 제거 후 재생성(제약 이름 상이 대비).
|
||||
export class AddDriveSharedAndAudit1783200000000 implements MigrationInterface {
|
||||
name = 'AddDriveSharedAndAudit1783200000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ===== 감사 로그 테이블 =====
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "drive_activities" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"type" character varying NOT NULL,
|
||||
"payload" jsonb NOT NULL DEFAULT '{}',
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"actor_id" uuid,
|
||||
CONSTRAINT "PK_drive_activities" PRIMARY KEY ("id")
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_drive_activities_actor" ON "drive_activities" ("actor_id")`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'FK_drive_activities_actor'
|
||||
) THEN
|
||||
ALTER TABLE "drive_activities"
|
||||
ADD CONSTRAINT "FK_drive_activities_actor"
|
||||
FOREIGN KEY ("actor_id") REFERENCES "users"("id")
|
||||
ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// ===== owner_id FK 를 SET NULL 로 교체(drive_files, drive_folders) =====
|
||||
for (const table of ['drive_files', 'drive_folders']) {
|
||||
// 혹시 NOT NULL 이면 해제(공유 파일은 업로더 없이도 존재 가능)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${table}" ALTER COLUMN "owner_id" DROP NOT NULL`,
|
||||
);
|
||||
// owner_id 컬럼에 걸린 기존 외래키를 이름과 무관하게 모두 제거 → SET NULL 로 재생성
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT con.conname
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid = con.conrelid
|
||||
JOIN pg_attribute att
|
||||
ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
|
||||
WHERE rel.relname = '${table}'
|
||||
AND con.contype = 'f'
|
||||
AND att.attname = 'owner_id'
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE "${table}" DROP CONSTRAINT %I', r.conname);
|
||||
END LOOP;
|
||||
ALTER TABLE "${table}"
|
||||
ADD CONSTRAINT "FK_${table}_owner"
|
||||
FOREIGN KEY ("owner_id") REFERENCES "users"("id")
|
||||
ON DELETE SET NULL ON UPDATE NO ACTION;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// owner_id FK 를 CASCADE 로 되돌린다(이름 무관 제거 후 재생성)
|
||||
for (const table of ['drive_files', 'drive_folders']) {
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT con.conname
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid = con.conrelid
|
||||
JOIN pg_attribute att
|
||||
ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey)
|
||||
WHERE rel.relname = '${table}'
|
||||
AND con.contype = 'f'
|
||||
AND att.attname = 'owner_id'
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE "${table}" DROP CONSTRAINT %I', r.conname);
|
||||
END LOOP;
|
||||
ALTER TABLE "${table}"
|
||||
ADD CONSTRAINT "FK_${table}_owner"
|
||||
FOREIGN KEY ("owner_id") REFERENCES "users"("id")
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "drive_activities" DROP CONSTRAINT IF EXISTS "FK_drive_activities_actor"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "drive_activities"`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user