feat: 오픈 게임 취소와 유산 정산 경로 추가

별도 최고위험 권한으로 실행되는 원자적 취소 작업을 추가한다. 버려진 게임과 장수 기록의 보존·삭제 옵션, 오픈 원금 전액 환급 및 획득 포인트 보전율, 취소 상태와 관리자·과거기록 UI를 함께 반영한다.
This commit is contained in:
2026-08-18 13:31:49 +00:00
parent a7b11811de
commit 383d173790
36 changed files with 1967 additions and 83 deletions
@@ -4,6 +4,7 @@ export const GATEWAY_PROFILE_STATUSES = [
'RUNNING',
'PAUSED',
'COMPLETED',
'CANCELLED',
'STOPPED',
'DISABLED',
] as const;
@@ -52,6 +53,12 @@ const CAPABILITIES: Record<GatewayProfileStatus, GatewayProfileCapabilities> = {
turnsRunning: false,
operatorResumable: false,
},
CANCELLED: {
runtimeExpected: false,
userAccessible: false,
turnsRunning: false,
operatorResumable: false,
},
STOPPED: {
runtimeExpected: false,
userAccessible: false,
@@ -21,6 +21,6 @@ describe('gateway profile status capabilities', () => {
});
it('defines capabilities for every persisted status', () => {
expect(GATEWAY_PROFILE_STATUSES.map((status) => gatewayProfileCapabilities(status))).toHaveLength(7);
expect(GATEWAY_PROFILE_STATUSES.map((status) => gatewayProfileCapabilities(status))).toHaveLength(8);
});
});
+58 -6
View File
@@ -375,22 +375,74 @@ model HallOfFame {
@@map("hall")
}
enum GameHistoryStatus {
OPEN
COMPLETED
ABANDONED
}
enum GameCancellationHistoryMode {
RETAIN_ABANDONED
DELETE
}
enum GameCancellationGeneralMode {
RETAIN
DELETE
}
model GameHistory {
id Int @id @default(autoincrement())
serverId String @map("server_id")
id Int @id @default(autoincrement())
serverId String @map("server_id")
date DateTime
winnerNation Int? @map("winner_nation")
map String? @map("map")
winnerNation Int? @map("winner_nation")
map String? @map("map")
season Int
scenario Int
scenarioName String @map("scenario_name")
env Json @default(dbgenerated("'{}'::jsonb"))
scenarioName String @map("scenario_name")
status GameHistoryStatus @default(OPEN)
env Json @default(dbgenerated("'{}'::jsonb"))
@@unique([serverId])
@@index([date])
@@map("ng_games")
}
model GameInheritanceBaseline {
serverId String @map("server_id")
userId String @map("user_id")
openingPoint Float @map("opening_point")
source String @default("OPENING")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@id([serverId, userId])
@@index([userId, createdAt])
@@map("game_inheritance_baseline")
}
model GameCancellation {
id String @id
serverId String @unique @map("server_id")
originalSeason Int @map("original_season")
scenario Int
scenarioName String @map("scenario_name")
openedAt DateTime @map("opened_at")
cancelledAt DateTime @map("cancelled_at")
cancelledBy String @map("cancelled_by")
reason String
historyMode GameCancellationHistoryMode @map("history_mode")
generalMode GameCancellationGeneralMode @map("general_mode")
earnedPointRetentionPercent Int @map("earned_point_retention_percent")
participantCount Int @map("participant_count")
preservedGeneralCount Int @map("preserved_general_count")
settlement Json @default(dbgenerated("'{}'::jsonb"))
createdAt DateTime @default(now()) @map("created_at")
@@index([cancelledAt])
@@map("game_cancellation")
}
model OldNation {
id Int @id @default(autoincrement())
serverId String @map("server_id")
@@ -0,0 +1,2 @@
ALTER TYPE "GatewayProfileStatus" ADD VALUE IF NOT EXISTS 'CANCELLED';
ALTER TYPE "GatewayOperationType" ADD VALUE IF NOT EXISTS 'CANCEL_GAME';
+2
View File
@@ -19,6 +19,7 @@ enum GatewayProfileStatus {
RUNNING
PAUSED
COMPLETED
CANCELLED
STOPPED
DISABLED
}
@@ -34,6 +35,7 @@ enum GatewayBuildStatus {
enum GatewayOperationType {
RESET
DEPLOY
CANCEL_GAME
START
STOP
}
@@ -0,0 +1,50 @@
CREATE TYPE "GameHistoryStatus" AS ENUM ('OPEN', 'COMPLETED', 'ABANDONED');
CREATE TYPE "GameCancellationHistoryMode" AS ENUM ('RETAIN_ABANDONED', 'DELETE');
CREATE TYPE "GameCancellationGeneralMode" AS ENUM ('RETAIN', 'DELETE');
ALTER TABLE "ng_games"
ADD COLUMN "status" "GameHistoryStatus" NOT NULL DEFAULT 'OPEN';
UPDATE "ng_games"
SET "status" = 'COMPLETED'
WHERE "winner_nation" IS NOT NULL;
CREATE TABLE "game_inheritance_baseline" (
"server_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"opening_point" DOUBLE PRECISION NOT NULL,
"source" TEXT NOT NULL DEFAULT 'OPENING',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "game_inheritance_baseline_pkey" PRIMARY KEY ("server_id", "user_id")
);
CREATE INDEX "game_inheritance_baseline_user_id_created_at_idx"
ON "game_inheritance_baseline"("user_id", "created_at");
CREATE TABLE "game_cancellation" (
"id" TEXT NOT NULL,
"server_id" TEXT NOT NULL,
"original_season" INTEGER NOT NULL,
"scenario" INTEGER NOT NULL,
"scenario_name" TEXT NOT NULL,
"opened_at" TIMESTAMP(3) NOT NULL,
"cancelled_at" TIMESTAMP(3) NOT NULL,
"cancelled_by" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"history_mode" "GameCancellationHistoryMode" NOT NULL,
"general_mode" "GameCancellationGeneralMode" NOT NULL,
"earned_point_retention_percent" INTEGER NOT NULL,
"participant_count" INTEGER NOT NULL,
"preserved_general_count" INTEGER NOT NULL,
"settlement" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "game_cancellation_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "game_cancellation_server_id_key" ON "game_cancellation"("server_id");
CREATE INDEX "game_cancellation_cancelled_at_idx" ON "game_cancellation"("cancelled_at");
ALTER TABLE "game_cancellation"
ADD CONSTRAINT "game_cancellation_retention_percent_check"
CHECK ("earned_point_retention_percent" BETWEEN 0 AND 100);
+2
View File
@@ -21,6 +21,8 @@ export interface DatabaseClient {
rankData: GamePrisma.RankDataDelegate;
hallOfFame: GamePrisma.HallOfFameDelegate;
gameHistory: GamePrisma.GameHistoryDelegate;
gameInheritanceBaseline: GamePrisma.GameInheritanceBaselineDelegate;
gameCancellation: GamePrisma.GameCancellationDelegate;
oldNation: GamePrisma.OldNationDelegate;
oldGeneral: GamePrisma.OldGeneralDelegate;
emperor: GamePrisma.EmperorDelegate;