using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace GameServer.Data { /// /// 포트폴리오 전용 테이블(tb_portfolio_*)을 서버 기동 시점에 보장한다. /// CREATE TABLE IF NOT EXISTS 를 사용하므로 몇 번을 실행해도 안전(멱등)하며, /// DB 장애가 게임 서버 기동을 막지 않도록 어떤 예외도 밖으로 던지지 않는다. /// public static class PortfolioSchemaInitializer { // 카테고리 테이블 private const string CreateCategoryTableSql = @" CREATE TABLE IF NOT EXISTS tb_portfolio_category ( category_no INT AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(64) NOT NULL, name VARCHAR(100) NOT NULL, description VARCHAR(255) NULL, accent VARCHAR(32) NULL, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_portfolio_category_slug (slug) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; // 게시물 테이블 private const string CreatePostTableSql = @" CREATE TABLE IF NOT EXISTS tb_portfolio_post ( post_no INT AUTO_INCREMENT PRIMARY KEY, category_no INT NOT NULL, title VARCHAR(200) NOT NULL, summary VARCHAR(500) NULL, content_md MEDIUMTEXT NULL, thumbnail_url VARCHAR(500) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY ix_portfolio_post_category (category_no), CONSTRAINT fk_portfolio_post_category FOREIGN KEY (category_no) REFERENCES tb_portfolio_category (category_no) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; // 게시물 이미지 테이블 private const string CreatePostImageTableSql = @" CREATE TABLE IF NOT EXISTS tb_portfolio_post_image ( image_no INT AUTO_INCREMENT PRIMARY KEY, post_no INT NOT NULL, image_url VARCHAR(500) NOT NULL, caption VARCHAR(255) NULL, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY ix_portfolio_post_image_post (post_no), CONSTRAINT fk_portfolio_post_image_post FOREIGN KEY (post_no) REFERENCES tb_portfolio_post (post_no) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"; /// /// 테이블 생성 + (비어 있을 때만) 기본 카테고리 4건 시딩. /// 실패해도 예외를 던지지 않고 로그만 남긴다. /// public static async Task EnsureCreatedAsync(IServiceProvider services, ILogger logger) { try { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // 문장 하나씩 실행 (MySql 커맨드는 다중 문장을 한 번에 처리하지 않는 경우가 있다) await db.Database.ExecuteSqlRawAsync(CreateCategoryTableSql); await db.Database.ExecuteSqlRawAsync(CreatePostTableSql); await db.Database.ExecuteSqlRawAsync(CreatePostImageTableSql); var categoryCount = await db.PortfolioCategories.CountAsync(); if (categoryCount == 0) { await SeedCategoriesAsync(db); logger.LogInformation("포트폴리오 기본 카테고리 4건을 시딩했습니다."); } logger.LogInformation("포트폴리오 스키마 준비 완료 (카테고리 {Count}건).", categoryCount == 0 ? 4 : categoryCount); } catch (Exception ex) { // 스키마 준비 실패가 게임 서버 기동을 막으면 안 되므로 여기서 삼킨다. logger.LogError(ex, "포트폴리오 스키마 초기화에 실패했습니다. 포트폴리오 API가 동작하지 않을 수 있습니다."); } } // 기본 카테고리 4건 (카테고리 테이블이 완전히 비어 있을 때만 호출된다) private static async Task SeedCategoriesAsync(AppDbContext db) { var now = DateTime.UtcNow; await InsertCategoryAsync(db, "game", "게임", "Unity 게임 프로젝트", "#7c5cff", 1, now); await InsertCategoryAsync(db, "dev", "개발", "서버 · 툴 · 시스템", "#20d5c4", 2, now); await InsertCategoryAsync(db, "art", "아트", "그래픽 · UI · 연출", "#ff5c9a", 3, now); await InsertCategoryAsync(db, "log", "기록", "작업 일지와 회고", "#ffb74d", 4, now); } // 값은 반드시 파라미터로 전달한다 (문자열 결합 금지) private static async Task InsertCategoryAsync( AppDbContext db, string slug, string name, string description, string accent, int sortOrder, DateTime createdAt) { await db.Database.ExecuteSqlInterpolatedAsync( $@"INSERT INTO tb_portfolio_category (slug, name, description, accent, sort_order, created_at) VALUES ({slug}, {name}, {description}, {accent}, {sortOrder}, {createdAt})"); } } }