서버 게시글
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using GameServer.Models;
|
||||
using GameServer.Models.Portfolio;
|
||||
|
||||
namespace GameServer.Data
|
||||
{
|
||||
@@ -11,5 +12,10 @@ namespace GameServer.Data
|
||||
}
|
||||
public DbSet<UserCharacterModel> UserCharacters { get; set; }
|
||||
public DbSet<CharacterModel> Characters { get; set; }
|
||||
|
||||
// 포트폴리오
|
||||
public DbSet<PortfolioCategoryModel> PortfolioCategories { get; set; }
|
||||
public DbSet<PortfolioPostModel> PortfolioPosts { get; set; }
|
||||
public DbSet<PortfolioPostImageModel> PortfolioPostImages { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
116
GameServer/Data/PortfolioSchemaInitializer.cs
Normal file
116
GameServer/Data/PortfolioSchemaInitializer.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user