- TypeScript 소스 파일 내 모든 import 구문에서 불필요한 `.js` 확장자를 제거하여 모듈 참조 방식을 표준화했습니다. - 핸들러, 서비스, 리포지토리 및 테스트 코드를 포함한 프로젝트 전반의 import 경로를 일관성 있게 정리했습니다.
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import { HttpError } from "../middleware/errors";
|
|
import { getGame, listByDate, type GameWithId } from "../repositories/gameRepository";
|
|
import {
|
|
getUserVote,
|
|
submitVote,
|
|
changeVote,
|
|
getCounts,
|
|
getUserDateVotes,
|
|
} from "../repositories/voteRepository";
|
|
import type { Game, VoteEntry } from "../types/panit";
|
|
import { fromTimestamp, parseDateString, type DateString } from "../types/dateString";
|
|
import { MemCache } from "../lib/memCache";
|
|
|
|
type SummaryCounts = { homeCount: number; awayCount: number };
|
|
|
|
const summaryCache = new MemCache<SummaryCounts>(5_000);
|
|
|
|
function gameDate(game: Game): DateString {
|
|
return fromTimestamp(game.time);
|
|
}
|
|
|
|
function sideOf(game: Game, teamCode: string): "home" | "away" {
|
|
if (teamCode === game.homeTeamCode) return "home";
|
|
if (teamCode === game.awayTeamCode) return "away";
|
|
throw new HttpError(400, `teamCode ${teamCode} not in game ${game.homeTeamCode}/${game.awayTeamCode}`);
|
|
}
|
|
|
|
async function loadWaitingGame(gameId: string): Promise<Game> {
|
|
const game = await getGame(gameId);
|
|
if (!game) throw new HttpError(404, `game not found: ${gameId}`);
|
|
if (game.status !== "scheduled") {
|
|
throw new HttpError(409, `game status is ${game.status}, voting closed`);
|
|
}
|
|
return game;
|
|
}
|
|
|
|
export async function createPrediction(
|
|
uid: string,
|
|
body: { gameId?: string; selectedTeamCode?: string }
|
|
): Promise<{ ok: true }> {
|
|
if (!body.gameId || !body.selectedTeamCode) {
|
|
throw new HttpError(400, "gameId and selectedTeamCode required");
|
|
}
|
|
const game = await loadWaitingGame(body.gameId);
|
|
const side = sideOf(game, body.selectedTeamCode);
|
|
|
|
const existing = await getUserVote(body.gameId, uid);
|
|
if (existing) throw new HttpError(409, "already voted; use PUT to change");
|
|
|
|
await submitVote({
|
|
gameId: body.gameId,
|
|
uid,
|
|
date: gameDate(game),
|
|
side,
|
|
team: body.selectedTeamCode,
|
|
});
|
|
summaryCache.delete(body.gameId);
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function updatePrediction(
|
|
uid: string,
|
|
body: { gameId?: string; selectedTeamCode?: string }
|
|
): Promise<{ ok: true; changed: boolean }> {
|
|
if (!body.gameId || !body.selectedTeamCode) {
|
|
throw new HttpError(400, "gameId and selectedTeamCode required");
|
|
}
|
|
const game = await loadWaitingGame(body.gameId);
|
|
const newSide = sideOf(game, body.selectedTeamCode);
|
|
|
|
const existing = await getUserVote(body.gameId, uid);
|
|
if (!existing) throw new HttpError(400, "no prior vote; use POST");
|
|
|
|
if (existing.team === body.selectedTeamCode) return { ok: true, changed: false };
|
|
|
|
const oldSide = sideOf(game, existing.team);
|
|
await changeVote({
|
|
gameId: body.gameId,
|
|
uid,
|
|
date: gameDate(game),
|
|
oldSide,
|
|
newSide,
|
|
newTeam: body.selectedTeamCode,
|
|
});
|
|
summaryCache.delete(body.gameId);
|
|
return { ok: true, changed: true };
|
|
}
|
|
|
|
export async function getMyVotes(
|
|
uid: string,
|
|
date: string
|
|
): Promise<Record<string, VoteEntry>> {
|
|
try {
|
|
return getUserDateVotes(uid, parseDateString(date));
|
|
} catch (err) {
|
|
throw new HttpError(400, (err as Error).message);
|
|
}
|
|
}
|
|
|
|
export async function listGamesByDate(date: string): Promise<GameWithId[]> {
|
|
try {
|
|
return await listByDate(parseDateString(date));
|
|
} catch (err) {
|
|
throw new HttpError(400, (err as Error).message);
|
|
}
|
|
}
|
|
|
|
export async function getSummary(
|
|
gameId: string
|
|
): Promise<{ homeCount: number; awayCount: number }> {
|
|
if (!gameId) throw new HttpError(400, "gameId required");
|
|
return summaryCache.getOrFetch(gameId, () => getCounts(gameId));
|
|
}
|