Files
Genesis_GameServer/GameServer/Data/PortfolioSchemaInitializer.cs
2026-09-03 19:18:46 +09:00

117 lines
5.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace GameServer.Data
{
/// <summary>
/// 포트폴리오 전용 테이블(tb_portfolio_*)을 서버 기동 시점에 보장한다.
/// CREATE TABLE IF NOT EXISTS 를 사용하므로 몇 번을 실행해도 안전(멱등)하며,
/// DB 장애가 게임 서버 기동을 막지 않도록 어떤 예외도 밖으로 던지지 않는다.
/// </summary>
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;";
/// <summary>
/// 테이블 생성 + (비어 있을 때만) 기본 카테고리 4건 시딩.
/// 실패해도 예외를 던지지 않고 로그만 남긴다.
/// </summary>
public static async Task EnsureCreatedAsync(IServiceProvider services, ILogger logger)
{
try
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 문장 하나씩 실행 (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})");
}
}
}