diff --git a/app/game-api/src/router/dynasty/index.ts b/app/game-api/src/router/dynasty/index.ts index a6df100..2bc2e0c 100644 --- a/app/game-api/src/router/dynasty/index.ts +++ b/app/game-api/src/router/dynasty/index.ts @@ -2,22 +2,62 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { asRecord } from '@sammo-ts/common'; -import { router, procedure } from '../../trpc.js'; + +import { procedure, router } from '../../trpc.js'; const zDynastyDetailInput = z.object({ emperorId: z.number().int().positive(), }); const parseNumberArray = (value: unknown): number[] => - Array.isArray(value) ? value.filter((item): item is number => typeof item === 'number') : []; + Array.isArray(value) + ? value.filter((item): item is number => typeof item === 'number' && Number.isFinite(item)) + : []; + +const parseTextArray = (value: unknown): string[] => + Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []; + +const parseDisplayArray = (value: unknown): Array => + Array.isArray(value) + ? value.filter( + (item): item is string | number => + typeof item === 'string' || (typeof item === 'number' && Number.isFinite(item)) + ) + : []; + +const formatNationType = (typeCode: string): string => { + const separator = typeCode.indexOf('_'); + return separator < 0 ? typeCode : typeCode.slice(separator + 1); +}; + +const formatNationLevel = (level: number | null): string => { + if (level === null) { + return ''; + } + return ['방랑군', '호족', '군벌', '주자사', '주목', '공', '왕', '황제'][level] ?? String(level); +}; export const dynastyRouter = router({ getList: procedure.query(async ({ ctx }) => { - const rows = await ctx.db.emperor.findMany({ - orderBy: { id: 'desc' }, - }); + const [worldState, rows] = await Promise.all([ + ctx.db.worldState.findFirst({ + select: { + currentYear: true, + currentMonth: true, + }, + }), + ctx.db.emperor.findMany({ + orderBy: { id: 'desc' }, + }), + ]); return { + current: worldState + ? { + year: worldState.currentYear, + month: worldState.currentMonth, + } + : null, entries: rows.map((row) => ({ id: row.id, serverId: row.serverId ?? '', @@ -30,11 +70,19 @@ export const dynastyRouter = router({ power: row.power ?? 0, gennum: row.gennum ?? 0, citynum: row.citynum ?? 0, + l12name: row.l12name ?? '', + l11name: row.l11name ?? '', + l10name: row.l10name ?? '', + l9name: row.l9name ?? '', + l8name: row.l8name ?? '', + l7name: row.l7name ?? '', + l6name: row.l6name ?? '', + l5name: row.l5name ?? '', })), }; }), getDetail: procedure.input(zDynastyDetailInput).query(async ({ ctx, input }) => { - const emperor = await ctx.db.emperor.findFirst({ + const emperor = await ctx.db.emperor.findUnique({ where: { id: input.emperorId }, }); if (!emperor) { @@ -43,7 +91,6 @@ export const dynastyRouter = router({ const aux = asRecord(emperor.aux); const winnerNationId = typeof aux.winnerNationId === 'number' ? aux.winnerNationId : null; - const serverId = emperor.serverId ?? ''; const oldNationRows = await ctx.db.oldNation.findMany({ where: { serverId }, @@ -54,19 +101,26 @@ export const dynastyRouter = router({ .map((row) => { const data = asRecord(row.data); const nationId = row.nation ?? (typeof data.nation === 'number' ? data.nation : 0); + const typeCode = typeof data.type === 'string' ? data.type : ''; return { nation: nationId, isWinner: winnerNationId !== null && nationId === winnerNationId, - name: typeof data.name === 'string' ? data.name : row.nation === 0 ? '재야' : '미상', + name: typeof data.name === 'string' ? data.name : nationId === 0 ? '재야' : '미상', color: typeof data.color === 'string' ? data.color : '#000000', - type: typeof data.type === 'string' ? data.type : '', + type: typeCode, + typeName: formatNationType(typeCode), level: typeof data.level === 'number' ? data.level : null, tech: typeof data.tech === 'number' ? data.tech : null, - maxPower: typeof data.maxPower === 'number' ? data.maxPower : null, + maxPower: + typeof data.maxPower === 'number' + ? data.maxPower + : typeof data.power === 'number' + ? data.power + : null, maxCrew: typeof data.maxCrew === 'number' ? data.maxCrew : null, - maxCities: Array.isArray(data.maxCities) ? data.maxCities : [], + maxCities: parseDisplayArray(data.maxCities), generals: parseNumberArray(data.generals), - history: Array.isArray(data.history) ? data.history : [], + history: parseTextArray(data.history), date: row.date.toISOString(), }; }) @@ -89,6 +143,7 @@ export const dynastyRouter = router({ const nations = nationEntries.map((entry) => ({ ...entry, + levelName: formatNationLevel(entry.level), generalsFull: entry.generals.map((id) => ({ generalNo: id, name: generalMap.get(id)?.name ?? `#${id}`, @@ -131,7 +186,7 @@ export const dynastyRouter = router({ tiger: emperor.tiger ?? '', eagle: emperor.eagle ?? '', gen: emperor.gen ?? '', - history: Array.isArray(emperor.history) ? emperor.history : [], + history: parseTextArray(emperor.history), }, nations, }; diff --git a/app/game-api/src/router/yearbook/index.ts b/app/game-api/src/router/yearbook/index.ts index 1e6bae5..19f4429 100644 --- a/app/game-api/src/router/yearbook/index.ts +++ b/app/game-api/src/router/yearbook/index.ts @@ -7,7 +7,8 @@ import { LogCategory, LogScope } from '@sammo-ts/infra'; import type { GameApiContext } from '../../context.js'; import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js'; -import { procedure, router } from '../../trpc.js'; +import { authedProcedure, router } from '../../trpc.js'; +import { getMyGeneral } from '../shared/general.js'; type YearbookNation = { id: number; @@ -19,6 +20,7 @@ type YearbookNation = { }; const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1; +const zServerId = z.string().trim().min(1).max(64); const computeHash = (payload: unknown): string => createHash('sha256').update(JSON.stringify(payload)).digest('hex'); @@ -189,53 +191,86 @@ const buildLogs = async (ctx: GameApiContext, year: number, month: number) => { }; export const yearbookRouter = router({ - getRange: procedure.query(async ({ ctx }) => { - const worldState = await ctx.db.worldState.findFirst(); - if (!worldState) { - throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' }); - } + getRange: authedProcedure + .input( + z + .object({ + serverID: zServerId.optional(), + }) + .optional() + ) + .query(async ({ ctx, input }) => { + await getMyGeneral(ctx); + const worldState = await ctx.db.worldState.findFirst(); + if (!worldState) { + throw new TRPCError({ + code: 'PRECONDITION_FAILED', + message: 'World state is not initialized.', + }); + } + const targetProfileName = input?.serverID ?? ctx.profile.name; + const isCurrentProfile = targetProfileName === ctx.profile.name; - const firstRow = await ctx.db.yearbookHistory.findFirst({ - where: { profileName: ctx.profile.name }, - select: { year: true, month: true }, - orderBy: [{ year: 'asc' }, { month: 'asc' }], - }); + const firstRow = await ctx.db.yearbookHistory.findFirst({ + where: { profileName: targetProfileName }, + select: { year: true, month: true }, + orderBy: [{ year: 'asc' }, { month: 'asc' }], + }); - const lastRow = await ctx.db.yearbookHistory.findFirst({ - where: { profileName: ctx.profile.name }, - select: { year: true, month: true }, - orderBy: [{ year: 'desc' }, { month: 'desc' }], - }); + const lastRow = await ctx.db.yearbookHistory.findFirst({ + where: { profileName: targetProfileName }, + select: { year: true, month: true }, + orderBy: [{ year: 'desc' }, { month: 'desc' }], + }); - const currentYearMonth = joinYearMonth(worldState.currentYear, worldState.currentMonth); - const fallbackYearMonth = currentYearMonth - 1; - const firstYearMonth = firstRow ? joinYearMonth(firstRow.year, firstRow.month) : fallbackYearMonth; - const lastYearMonth = lastRow ? joinYearMonth(lastRow.year, lastRow.month) : fallbackYearMonth; + if (!isCurrentProfile && (!firstRow || !lastRow)) { + throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' }); + } - return { - firstYearMonth, - lastYearMonth, - currentYearMonth, - }; - }), - getHistory: procedure + const currentYearMonth = joinYearMonth(worldState.currentYear, worldState.currentMonth); + const fallbackYearMonth = currentYearMonth - 1; + const firstYearMonth = firstRow + ? joinYearMonth(firstRow.year, firstRow.month) + : fallbackYearMonth; + const lastYearMonth = lastRow + ? joinYearMonth(lastRow.year, lastRow.month) + : fallbackYearMonth; + const selectedYearMonth = isCurrentProfile ? currentYearMonth : lastYearMonth; + + return { + firstYearMonth, + lastYearMonth, + currentYearMonth: selectedYearMonth, + }; + }), + getHistory: authedProcedure .input( z.object({ year: z.number().int(), month: z.number().int().min(1).max(12), hash: z.string().optional(), + serverID: zServerId.optional(), }) ) .query(async ({ ctx, input }) => { + await getMyGeneral(ctx); const worldState = await ctx.db.worldState.findFirst(); if (!worldState) { throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' }); } + const targetProfileName = input.serverID ?? ctx.profile.name; + const isCurrentProfile = targetProfileName === ctx.profile.name; const isCurrent = + isCurrentProfile && worldState.currentYear === input.year && worldState.currentMonth === input.month; - const { globalHistory, globalAction } = await buildLogs(ctx, input.year, input.month); + const { globalHistory, globalAction } = isCurrentProfile + ? await buildLogs(ctx, input.year, input.month) + : { + globalHistory: [`●${input.month}월: 기록 없음`], + globalAction: [`●${input.month}월: 기록 없음`], + }; if (isCurrent) { const map = await loadPublicMap(ctx, false); @@ -260,7 +295,7 @@ export const yearbookRouter = router({ const row = await ctx.db.yearbookHistory.findFirst({ where: { - profileName: ctx.profile.name, + profileName: targetProfileName, year: input.year, month: input.month, }, @@ -285,4 +320,4 @@ export const yearbookRouter = router({ } return { notModified: false, hash, data }; }), -}); \ No newline at end of file +}); diff --git a/app/game-api/test/dynastyRouter.test.ts b/app/game-api/test/dynastyRouter.test.ts new file mode 100644 index 0000000..608671f --- /dev/null +++ b/app/game-api/test/dynastyRouter.test.ts @@ -0,0 +1,234 @@ +import { describe, expect, it } from 'vitest'; + +import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken'; +import type { RedisConnector } from '@sammo-ts/infra'; + +import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js'; +import { InMemoryFlushStore } from '../src/auth/flushStore.js'; +import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js'; +import type { DatabaseClient, GameApiContext, GameProfile } from '../src/context.js'; +import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js'; +import { appRouter } from '../src/router.js'; + +const profile: GameProfile = { + id: 'che', + scenario: 'default', + name: 'che:default', +}; + +const emperor = { + id: 7, + serverId: 'hwe_260725_fixture', + phase: '훼2기', + nationCount: '3 / 8', + nationName: '촉, 위, 오', + nationHist: '병가(2), 유가(1)', + genCount: '7 / 21', + personalHist: '의리(4)', + specialHist: '상재(2)', + name: '촉', + type: 'che_병가', + color: '#FF0000', + year: 215, + month: 4, + power: 34434, + gennum: 7, + citynum: 8, + pop: '12345 / 15000', + poprate: '82.3 %', + gold: 50000, + rice: 60000, + l12name: '유비', + l12pic: '', + l11name: '제갈량', + l11pic: '', + l10name: '관우', + l10pic: '', + l9name: '방통', + l9pic: '', + l8name: '장비', + l8pic: '', + l7name: '법정', + l7pic: '', + l6name: '조운', + l6pic: '', + l5name: '마량', + l5pic: '', + tiger: '관우【10】', + eagle: '방통【7】', + gen: '유비, 제갈량', + history: ['●촉이 천하를 통일'], + aux: { winnerNationId: 1, privateNote: 'not-returned' }, +}; + +const oldNation = { + id: 3, + serverId: emperor.serverId, + nation: 1, + data: { + nation: 1, + name: '촉', + color: '#FF0000', + type: 'che_병가', + level: 7, + tech: 4000, + power: 34434, + maxCrew: 120000, + maxCities: ['성도', '한중'], + generals: [11, 12], + history: ['유비가 황제로 즉위'], + owner: 'not-returned', + }, + date: new Date('2026-07-25T12:00:00.000Z'), +}; + +const authFor = (userId: string, roles: string[] = []): GameSessionTokenPayload => ({ + version: 1, + profile: profile.name, + issuedAt: '2026-07-25T00:00:00.000Z', + expiresAt: '2026-07-26T00:00:00.000Z', + sessionId: `session-${userId}`, + user: { + id: userId, + username: userId, + displayName: userId, + roles, + }, + sanctions: {}, +}); + +const buildContext = (auth: GameSessionTokenPayload | null): GameApiContext => { + const db = { + worldState: { + findFirst: async () => ({ currentYear: 220, currentMonth: 1 }), + }, + emperor: { + findMany: async () => [emperor], + findUnique: async ({ where }: { where: { id: number } }) => (where.id === emperor.id ? emperor : null), + }, + oldNation: { + findMany: async ({ where }: { where: { serverId: string } }) => + where.serverId === emperor.serverId ? [oldNation] : [], + }, + oldGeneral: { + findMany: async () => [ + { generalNo: 11, name: '유비', lastYearMonth: 21504 }, + { generalNo: 12, name: '제갈량', lastYearMonth: 21504 }, + ], + }, + }; + const redis = { + get: async () => null, + set: async () => null, + } as unknown as RedisConnector['client']; + + return { + db: db as unknown as DatabaseClient, + turnDaemon: new InMemoryTurnDaemonTransport(), + battleSim: new InMemoryBattleSimTransport(), + profile, + auth, + uploadDir: 'uploads', + uploadPath: '/uploads', + uploadPublicUrl: null, + redis, + accessTokenStore: new RedisAccessTokenStore(redis, profile.name), + flushStore: new InMemoryFlushStore(), + gameTokenSecret: 'test-secret', + }; +}; + +describe('dynasty public read model', () => { + it('returns the current row and the complete legacy officer summary', async () => { + const result = await appRouter.createCaller(buildContext(null)).dynasty.getList(); + + expect(result.current).toEqual({ year: 220, month: 1 }); + expect(result.entries).toEqual([ + expect.objectContaining({ + id: 7, + serverId: 'hwe_260725_fixture', + phase: '훼2기', + name: '촉', + l12name: '유비', + l11name: '제갈량', + l10name: '관우', + l9name: '방통', + l8name: '장비', + l7name: '법정', + l6name: '조운', + l5name: '마량', + }), + ]); + }); + + it('exposes the same public DTO to anonymous, general owners and admins', async () => { + const anonymous = appRouter.createCaller(buildContext(null)); + const owner = appRouter.createCaller(buildContext(authFor('owner-a'))); + const otherOwner = appRouter.createCaller(buildContext(authFor('owner-b'))); + const admin = appRouter.createCaller(buildContext(authFor('admin', ['admin']))); + + const [anonymousList, ownerList, otherOwnerList, adminList] = await Promise.all([ + anonymous.dynasty.getList(), + owner.dynasty.getList(), + otherOwner.dynasty.getList(), + admin.dynasty.getList(), + ]); + expect(ownerList).toEqual(anonymousList); + expect(otherOwnerList).toEqual(anonymousList); + expect(adminList).toEqual(anonymousList); + + const [anonymousDetail, ownerDetail, otherOwnerDetail, adminDetail] = await Promise.all([ + anonymous.dynasty.getDetail({ emperorId: emperor.id }), + owner.dynasty.getDetail({ emperorId: emperor.id }), + otherOwner.dynasty.getDetail({ emperorId: emperor.id }), + admin.dynasty.getDetail({ emperorId: emperor.id }), + ]); + expect(ownerDetail).toEqual(anonymousDetail); + expect(otherOwnerDetail).toEqual(anonymousDetail); + expect(adminDetail).toEqual(anonymousDetail); + }); + + it('returns only the legacy public archive fields and resolves general names', async () => { + const result = await appRouter.createCaller(buildContext(authFor('owner-a'))).dynasty.getDetail({ + emperorId: emperor.id, + }); + + expect(result.emperor).toEqual( + expect.objectContaining({ + personalHist: '의리(4)', + specialHist: '상재(2)', + tiger: '관우【10】', + eagle: '방통【7】', + history: ['●촉이 천하를 통일'], + }) + ); + expect(result.nations).toEqual([ + expect.objectContaining({ + name: '촉', + type: 'che_병가', + typeName: '병가', + levelName: '황제', + maxPower: 34434, + generalsFull: [ + { generalNo: 11, name: '유비', lastYearMonth: 21504 }, + { generalNo: 12, name: '제갈량', lastYearMonth: 21504 }, + ], + }), + ]); + expect(JSON.stringify(result)).not.toContain('privateNote'); + expect(JSON.stringify(result)).not.toContain('not-returned'); + expect(JSON.stringify(result)).not.toContain('"owner"'); + expect(JSON.stringify(result)).not.toContain('"data"'); + }); + + it('rejects invalid and missing record identifiers without querying another scope', async () => { + const caller = appRouter.createCaller(buildContext(null)); + + await expect(caller.dynasty.getDetail({ emperorId: 0 })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + await expect(caller.dynasty.getDetail({ emperorId: 999 })).rejects.toMatchObject({ + code: 'NOT_FOUND', + }); + }); +}); diff --git a/app/game-api/test/yearbookArchiveRouter.test.ts b/app/game-api/test/yearbookArchiveRouter.test.ts new file mode 100644 index 0000000..39b4316 --- /dev/null +++ b/app/game-api/test/yearbookArchiveRouter.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it } from 'vitest'; + +import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken'; +import type { RedisConnector } from '@sammo-ts/infra'; + +import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js'; +import { InMemoryFlushStore } from '../src/auth/flushStore.js'; +import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js'; +import type { DatabaseClient, GameApiContext, GameProfile } from '../src/context.js'; +import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js'; +import { appRouter } from '../src/router.js'; + +const profile: GameProfile = { + id: 'che', + scenario: 'default', + name: 'che:default', +}; + +const archiveServerId = 'hwe_260725_archive'; +const archiveRows = [ + { + id: 1, + profileName: archiveServerId, + year: 200, + month: 1, + map: { year: 200, month: 1, startYear: 190, cityList: [], nationList: [] }, + nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1000, cities: ['성도'] }], + hash: 'archive-1', + createdAt: new Date('2026-07-25T00:00:00.000Z'), + }, + { + id: 2, + profileName: archiveServerId, + year: 200, + month: 2, + map: { year: 200, month: 2, startYear: 190, cityList: [], nationList: [] }, + nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1200, cities: ['성도'] }], + hash: 'archive-2', + createdAt: new Date('2026-07-25T01:00:00.000Z'), + }, +]; + +const authFor = (userId: string): GameSessionTokenPayload => ({ + version: 1, + profile: profile.name, + issuedAt: '2026-07-25T00:00:00.000Z', + expiresAt: '2026-07-26T00:00:00.000Z', + sessionId: `session-${userId}`, + user: { + id: userId, + username: userId, + displayName: userId, + roles: [], + }, + sanctions: {}, +}); + +const buildContext = ( + auth: GameSessionTokenPayload | null, + options: { hasGeneral?: boolean } = {} +): GameApiContext => { + const db = { + general: { + findFirst: async ({ where }: { where: { userId: string } }) => + options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId }, + }, + worldState: { + findFirst: async () => ({ currentYear: 220, currentMonth: 1 }), + }, + yearbookHistory: { + findFirst: async (args: { + where: { profileName: string; year?: number; month?: number }; + orderBy?: Array<{ year?: 'asc' | 'desc'; month?: 'asc' | 'desc' }>; + }) => { + const candidates = archiveRows.filter( + (row) => + row.profileName === args.where.profileName && + (args.where.year === undefined || row.year === args.where.year) && + (args.where.month === undefined || row.month === args.where.month) + ); + if (!args.orderBy) { + return candidates[0] ?? null; + } + const descending = args.orderBy[0]?.year === 'desc'; + return (descending ? candidates.at(-1) : candidates[0]) ?? null; + }, + }, + }; + const redis = { + get: async () => null, + set: async () => null, + } as unknown as RedisConnector['client']; + + return { + db: db as unknown as DatabaseClient, + turnDaemon: new InMemoryTurnDaemonTransport(), + battleSim: new InMemoryBattleSimTransport(), + profile, + auth, + uploadDir: 'uploads', + uploadPath: '/uploads', + uploadPublicUrl: null, + redis, + accessTokenStore: new RedisAccessTokenStore(redis, profile.name), + flushStore: new InMemoryFlushStore(), + gameTokenSecret: 'test-secret', + }; +}; + +describe('historical yearbook access from dynasty', () => { + it('selects the archived server range without treating it as the live month', async () => { + const caller = appRouter.createCaller(buildContext(authFor('owner-a'))); + const result = await caller.yearbook.getRange({ serverID: archiveServerId }); + + expect(result).toEqual({ + firstYearMonth: 200 * 12, + lastYearMonth: 200 * 12 + 1, + currentYearMonth: 200 * 12 + 1, + }); + }); + + it('returns the same archived public history for distinct general owners', async () => { + const ownerA = appRouter.createCaller(buildContext(authFor('owner-a'))); + const ownerB = appRouter.createCaller(buildContext(authFor('owner-b'))); + + const [resultA, resultB] = await Promise.all([ + ownerA.yearbook.getHistory({ serverID: archiveServerId, year: 200, month: 2 }), + ownerB.yearbook.getHistory({ serverID: archiveServerId, year: 200, month: 2 }), + ]); + + expect(resultB).toEqual(resultA); + expect(resultA).toMatchObject({ + notModified: false, + data: { + year: 200, + month: 2, + nations: [{ id: 1, name: '촉', cities: ['성도'] }], + globalHistory: ['●2월: 기록 없음'], + globalAction: ['●2월: 기록 없음'], + }, + }); + }); + + it('requires both an authenticated user and a user-owned general like legacy v_history.php', async () => { + const anonymous = appRouter.createCaller(buildContext(null)); + const noGeneral = appRouter.createCaller(buildContext(authFor('owner-a'), { hasGeneral: false })); + + await expect(anonymous.yearbook.getRange({ serverID: archiveServerId })).rejects.toMatchObject({ + code: 'UNAUTHORIZED', + }); + await expect(noGeneral.yearbook.getRange({ serverID: archiveServerId })).rejects.toMatchObject({ + code: 'NOT_FOUND', + message: 'General not found', + }); + }); + + it('rejects unknown archived server IDs instead of falling back to the live profile', async () => { + const caller = appRouter.createCaller(buildContext(authFor('owner-a'))); + + await expect(caller.yearbook.getRange({ serverID: 'missing-server' })).rejects.toMatchObject({ + code: 'NOT_FOUND', + message: '연감 범위를 찾을 수 없습니다.', + }); + }); +}); diff --git a/app/game-frontend/src/router/index.ts b/app/game-frontend/src/router/index.ts index a412e4c..25aa8f1 100644 --- a/app/game-frontend/src/router/index.ts +++ b/app/game-frontend/src/router/index.ts @@ -240,6 +240,7 @@ const routes = [ component: YearbookView, meta: { requiresAuth: true, + requiresGeneral: true, }, }, { diff --git a/app/game-frontend/src/utils/legacyNationColor.ts b/app/game-frontend/src/utils/legacyNationColor.ts new file mode 100644 index 0000000..db00faa --- /dev/null +++ b/app/game-frontend/src/utils/legacyNationColor.ts @@ -0,0 +1,23 @@ +const lightTextBackgrounds = new Set([ + '', + '#330000', + '#FF0000', + '#800000', + '#A0522D', + '#FF6347', + '#808000', + '#008000', + '#2E8B57', + '#008080', + '#6495ED', + '#0000FF', + '#000080', + '#483D8B', + '#7B68EE', + '#800080', + '#A9A9A9', + '#000000', +]); + +export const legacyNationTextColor = (backgroundColor: string): '#FFFFFF' | '#000000' => + lightTextBackgrounds.has(backgroundColor.toUpperCase()) ? '#FFFFFF' : '#000000'; diff --git a/app/game-frontend/src/views/DynastyDetailView.vue b/app/game-frontend/src/views/DynastyDetailView.vue index 6683d09..6438e23 100644 --- a/app/game-frontend/src/views/DynastyDetailView.vue +++ b/app/game-frontend/src/views/DynastyDetailView.vue @@ -1,66 +1,12 @@ + + diff --git a/app/game-frontend/src/views/DynastyListView.vue b/app/game-frontend/src/views/DynastyListView.vue index f015ab3..129bb2c 100644 --- a/app/game-frontend/src/views/DynastyListView.vue +++ b/app/game-frontend/src/views/DynastyListView.vue @@ -1,35 +1,30 @@ + + + + + + + + + + + + + diff --git a/app/game-frontend/src/views/YearbookView.vue b/app/game-frontend/src/views/YearbookView.vue index 7a0169d..9c38a94 100644 --- a/app/game-frontend/src/views/YearbookView.vue +++ b/app/game-frontend/src/views/YearbookView.vue @@ -1,6 +1,6 @@