From 5cf8733f42c6a76d483ba5aa5240b21cdabeba16 Mon Sep 17 00:00:00 2001 From: TransparentDeveloper <2younsin@gmail.com> Date: Tue, 5 May 2026 21:15:48 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EC=84=A0(sandbox):=20#96=20UI?= =?UTF-8?q?=EB=A5=BC=20MarketEngine=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EA=B3=BC=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 옛 $lib/domain/sandbox 제거, store/컴포넌트가 SandboxBoardController DTO를 사용하도록 교체. snapshot.ts의 옛 sandbox 도메인 의존 제거. --- .../sandbox/__tests__/sandbox-board.test.ts | 239 ------------------ src/lib/domain/sandbox/errors.ts | 15 -- src/lib/domain/sandbox/index.ts | 10 - src/lib/domain/sandbox/sandbox-board.ts | 131 ---------- src/lib/domain/sandbox/types.ts | 32 --- .../sandbox/components/CaptainRoster.svelte | 6 +- .../sandbox/components/PlayerCard.svelte | 5 +- .../sandbox/components/PlayerPool.svelte | 4 +- .../sandbox/stores/sandbox-store.svelte.ts | 87 ++++++- src/lib/types/snapshot.ts | 22 +- src/routes/sandbox/[templateId]/+page.svelte | 48 +--- 11 files changed, 121 insertions(+), 478 deletions(-) delete mode 100644 src/lib/domain/sandbox/__tests__/sandbox-board.test.ts delete mode 100644 src/lib/domain/sandbox/errors.ts delete mode 100644 src/lib/domain/sandbox/index.ts delete mode 100644 src/lib/domain/sandbox/sandbox-board.ts delete mode 100644 src/lib/domain/sandbox/types.ts diff --git a/src/lib/domain/sandbox/__tests__/sandbox-board.test.ts b/src/lib/domain/sandbox/__tests__/sandbox-board.test.ts deleted file mode 100644 index 4d18df2..0000000 --- a/src/lib/domain/sandbox/__tests__/sandbox-board.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, it, expect } from 'bun:test'; -import { SandboxBoard } from '../sandbox-board.ts'; -import { SandboxError } from '../errors.ts'; -import type { SandboxPlayerType } from '../types.ts'; - -const PLAYERS: SandboxPlayerType[] = [ - { id: 'p1', name: '감스트', position: 'TOP', tier: 'S' }, - { id: 'p2', name: '따효니', position: 'MID', tier: 'A' }, - { id: 'p3', name: '침착맨', position: 'ADC', tier: 'S+' }, - { id: 'p4', name: '우왁굳', position: 'SUPPORT', tier: 'A+' } -]; - -describe('SandboxBoard', () => { - describe('create', () => { - it('감독 수만큼 빈 로스터가 생성된다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 3, - players: PLAYERS - }); - expect(board.captains).toHaveLength(3); - expect(board.captains[0]!.name).toBe('감독 1'); - expect(board.captains[1]!.name).toBe('감독 2'); - expect(board.captains[2]!.name).toBe('감독 3'); - expect(board.pool).toHaveLength(4); - for (const captain of board.captains) { - expect(board.rosters[captain.id]).toEqual([]); - } - }); - - it('선수풀에 모든 선수가 포함된다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - expect(board.pool.map((p) => p.id)).toEqual(['p1', 'p2', 'p3', 'p4']); - }); - }); - - describe('assign', () => { - it('선수를 pool에서 감독 로스터로 이동한다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const captainId = board.captains[0]!.id; - const next = board.assign('p1', captainId); - expect(next.pool).toHaveLength(3); - expect(next.pool.find((p) => p.id === 'p1')).toBeUndefined(); - expect(next.rosters[captainId]).toHaveLength(1); - expect(next.rosters[captainId]![0]!.id).toBe('p1'); - }); - - it('원본 보드는 변경되지 않는다 (불변)', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const captainId = board.captains[0]!.id; - board.assign('p1', captainId); - expect(board.pool).toHaveLength(4); - expect(board.rosters[captainId]).toHaveLength(0); - }); - - it('pool에 없는 선수를 assign하면 PLAYER_NOT_IN_POOL 에러', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const captainId = board.captains[0]!.id; - expect(() => board.assign('nonexistent', captainId)).toThrow(SandboxError); - try { - board.assign('nonexistent', captainId); - } catch (e) { - expect((e as SandboxError).code).toBe('PLAYER_NOT_IN_POOL'); - } - }); - - it('존재하지 않는 감독에게 assign하면 CAPTAIN_NOT_FOUND 에러', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - expect(() => board.assign('p1', 'no-captain')).toThrow(SandboxError); - try { - board.assign('p1', 'no-captain'); - } catch (e) { - expect((e as SandboxError).code).toBe('CAPTAIN_NOT_FOUND'); - } - }); - }); - - describe('unassign', () => { - it('감독 로스터에서 pool로 복귀시킨다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const captainId = board.captains[0]!.id; - const assigned = board.assign('p1', captainId); - const unassigned = assigned.unassign('p1'); - expect(unassigned.pool).toHaveLength(4); - expect(unassigned.pool.find((p) => p.id === 'p1')).toBeDefined(); - expect(unassigned.rosters[captainId]).toHaveLength(0); - }); - - it('원본 보드는 변경되지 않는다 (불변)', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const captainId = board.captains[0]!.id; - const assigned = board.assign('p1', captainId); - assigned.unassign('p1'); - expect(assigned.rosters[captainId]).toHaveLength(1); - expect(assigned.pool).toHaveLength(3); - }); - - it('어느 로스터에도 없는 선수는 PLAYER_NOT_IN_ROSTER 에러', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - expect(() => board.unassign('p1')).toThrow(SandboxError); - try { - board.unassign('p1'); - } catch (e) { - expect((e as SandboxError).code).toBe('PLAYER_NOT_IN_ROSTER'); - } - }); - }); - - describe('move', () => { - it('한 감독 로스터에서 다른 감독 로스터로 이동한다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap1 = board.captains[0]!.id; - const cap2 = board.captains[1]!.id; - const assigned = board.assign('p1', cap1); - const moved = assigned.move('p1', cap2); - expect(moved.rosters[cap1]).toHaveLength(0); - expect(moved.rosters[cap2]).toHaveLength(1); - expect(moved.rosters[cap2]![0]!.id).toBe('p1'); - expect(moved.pool).toHaveLength(3); - }); - - it('같은 감독으로 move하면 변경 없이 새 인스턴스 반환', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap1 = board.captains[0]!.id; - const assigned = board.assign('p1', cap1); - const same = assigned.move('p1', cap1); - expect(same).not.toBe(assigned); - expect(same.rosters[cap1]).toHaveLength(1); - }); - - it('원본 보드는 변경되지 않는다 (불변)', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap1 = board.captains[0]!.id; - const cap2 = board.captains[1]!.id; - const assigned = board.assign('p1', cap1); - assigned.move('p1', cap2); - expect(assigned.rosters[cap1]).toHaveLength(1); - expect(assigned.rosters[cap2]).toHaveLength(0); - }); - - it('로스터에 없는 선수를 move하면 PLAYER_NOT_IN_ROSTER 에러', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap2 = board.captains[1]!.id; - expect(() => board.move('p1', cap2)).toThrow(SandboxError); - }); - - it('존재하지 않는 감독으로 move하면 CAPTAIN_NOT_FOUND 에러', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap1 = board.captains[0]!.id; - const assigned = board.assign('p1', cap1); - expect(() => assigned.move('p1', 'no-captain')).toThrow(SandboxError); - }); - }); - - describe('toResult', () => { - it('감독별 로스터를 SandboxResultTeamType 배열로 변환한다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const cap1 = board.captains[0]!.id; - const cap2 = board.captains[1]!.id; - const final = board.assign('p1', cap1).assign('p2', cap2); - const result = final.toResult(); - expect(result).toHaveLength(2); - expect(result[0]!.captain).toBe('감독 1'); - expect(result[0]!.players).toHaveLength(1); - expect(result[0]!.players[0]).toEqual({ name: '감스트', position: 'TOP', tier: 'S' }); - expect(result[1]!.captain).toBe('감독 2'); - expect(result[1]!.players).toHaveLength(1); - expect(result[1]!.players[0]).toEqual({ name: '따효니', position: 'MID', tier: 'A' }); - }); - - it('빈 로스터도 빈 players 배열로 포함된다', () => { - const board = SandboxBoard.create({ - templateId: 'tpl-1', - captainsCount: 2, - players: PLAYERS - }); - const result = board.toResult(); - expect(result).toHaveLength(2); - expect(result[0]!.players).toEqual([]); - expect(result[1]!.players).toEqual([]); - }); - }); -}); diff --git a/src/lib/domain/sandbox/errors.ts b/src/lib/domain/sandbox/errors.ts deleted file mode 100644 index d4a153f..0000000 --- a/src/lib/domain/sandbox/errors.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type SandboxErrorCode = - | 'PLAYER_NOT_IN_POOL' - | 'PLAYER_NOT_IN_ROSTER' - | 'CAPTAIN_NOT_FOUND' - | 'DUPLICATE_ASSIGNMENT'; - -export class SandboxError extends Error { - readonly code: SandboxErrorCode; - - constructor(code: SandboxErrorCode, message?: string) { - super(message ?? code); - this.code = code; - this.name = 'SandboxError'; - } -} diff --git a/src/lib/domain/sandbox/index.ts b/src/lib/domain/sandbox/index.ts deleted file mode 100644 index 5840fed..0000000 --- a/src/lib/domain/sandbox/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { SandboxBoard } from './sandbox-board.ts'; -export { SandboxError } from './errors.ts'; -export type { SandboxErrorCode } from './errors.ts'; -export type { - SandboxPlayerType, - SandboxCaptainType, - SandboxBoardParamsType, - SandboxResultPlayerType, - SandboxResultTeamType -} from './types.ts'; diff --git a/src/lib/domain/sandbox/sandbox-board.ts b/src/lib/domain/sandbox/sandbox-board.ts deleted file mode 100644 index 226b737..0000000 --- a/src/lib/domain/sandbox/sandbox-board.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { - SandboxBoardParamsType, - SandboxCaptainType, - SandboxPlayerType, - SandboxResultTeamType -} from './types.ts'; -import { SandboxError } from './errors.ts'; - -export class SandboxBoard { - readonly templateId: string; - readonly captains: readonly SandboxCaptainType[]; - readonly pool: readonly SandboxPlayerType[]; - readonly rosters: Readonly>; - - constructor(params: SandboxBoardParamsType) { - this.templateId = params.templateId; - this.captains = params.captains; - this.pool = params.pool; - this.rosters = params.rosters; - } - - static create(params: { - templateId: string; - captainsCount: number; - players: readonly SandboxPlayerType[]; - }): SandboxBoard { - const captains: SandboxCaptainType[] = Array.from({ length: params.captainsCount }, (_, i) => ({ - id: `captain-${i + 1}`, - name: `감독 ${i + 1}` - })); - const rosters: Record = {}; - for (const captain of captains) { - rosters[captain.id] = []; - } - return new SandboxBoard({ - templateId: params.templateId, - captains, - pool: [...params.players], - rosters - }); - } - - assign(playerId: string, toCaptainId: string): SandboxBoard { - if (!this.captains.some((c) => c.id === toCaptainId)) { - throw new SandboxError('CAPTAIN_NOT_FOUND'); - } - const playerIndex = this.pool.findIndex((p) => p.id === playerId); - if (playerIndex === -1) { - throw new SandboxError('PLAYER_NOT_IN_POOL'); - } - const player = this.pool[playerIndex]!; - const nextPool = [...this.pool.slice(0, playerIndex), ...this.pool.slice(playerIndex + 1)]; - const nextRosters = { ...this.rosters }; - nextRosters[toCaptainId] = [...(nextRosters[toCaptainId] ?? []), player]; - return new SandboxBoard({ - templateId: this.templateId, - captains: this.captains, - pool: nextPool, - rosters: nextRosters - }); - } - - unassign(playerId: string): SandboxBoard { - let found: SandboxPlayerType | null = null; - let fromCaptainId: string | null = null; - for (const captain of this.captains) { - const roster = this.rosters[captain.id] ?? []; - const player = roster.find((p) => p.id === playerId); - if (player) { - found = player; - fromCaptainId = captain.id; - break; - } - } - if (!found || !fromCaptainId) { - throw new SandboxError('PLAYER_NOT_IN_ROSTER'); - } - const nextRosters = { ...this.rosters }; - nextRosters[fromCaptainId] = (nextRosters[fromCaptainId] ?? []).filter( - (p) => p.id !== playerId - ); - return new SandboxBoard({ - templateId: this.templateId, - captains: this.captains, - pool: [...this.pool, found], - rosters: nextRosters - }); - } - - move(playerId: string, toCaptainId: string): SandboxBoard { - if (!this.captains.some((c) => c.id === toCaptainId)) { - throw new SandboxError('CAPTAIN_NOT_FOUND'); - } - let found: SandboxPlayerType | null = null; - let fromCaptainId: string | null = null; - for (const captain of this.captains) { - const roster = this.rosters[captain.id] ?? []; - const player = roster.find((p) => p.id === playerId); - if (player) { - found = player; - fromCaptainId = captain.id; - break; - } - } - if (!found || !fromCaptainId) { - throw new SandboxError('PLAYER_NOT_IN_ROSTER'); - } - const nextRosters = { ...this.rosters }; - nextRosters[fromCaptainId] = (nextRosters[fromCaptainId] ?? []).filter( - (p) => p.id !== playerId - ); - nextRosters[toCaptainId] = [...(nextRosters[toCaptainId] ?? []), found]; - return new SandboxBoard({ - templateId: this.templateId, - captains: this.captains, - pool: this.pool, - rosters: nextRosters - }); - } - - toResult(): SandboxResultTeamType[] { - return this.captains.map((captain) => ({ - captain: captain.name, - players: (this.rosters[captain.id] ?? []).map((p) => ({ - name: p.name, - position: p.position as string | null, - tier: p.tier - })) - })); - } -} diff --git a/src/lib/domain/sandbox/types.ts b/src/lib/domain/sandbox/types.ts deleted file mode 100644 index 59cbf40..0000000 --- a/src/lib/domain/sandbox/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { GamePositionType } from '$lib/domain/template'; - -export interface SandboxPlayerType { - readonly id: string; - readonly name: string; - readonly position: GamePositionType | null; - readonly tier: string; -} - -export interface SandboxCaptainType { - readonly id: string; - readonly name: string; -} - -export interface SandboxBoardParamsType { - readonly templateId: string; - readonly captains: readonly SandboxCaptainType[]; - readonly pool: readonly SandboxPlayerType[]; - readonly rosters: Readonly>; -} - -/** 결과 스냅샷용 (toResult 반환 타입) */ -export interface SandboxResultPlayerType { - name: string; - position: string | null; - tier: string; -} - -export interface SandboxResultTeamType { - captain: string; - players: SandboxResultPlayerType[]; -} diff --git a/src/lib/features/sandbox/components/CaptainRoster.svelte b/src/lib/features/sandbox/components/CaptainRoster.svelte index b0e165f..e78ec85 100644 --- a/src/lib/features/sandbox/components/CaptainRoster.svelte +++ b/src/lib/features/sandbox/components/CaptainRoster.svelte @@ -1,10 +1,10 @@