diff --git a/src/app/api/expansions/[id]/run/route.ts b/src/app/api/expansions/[id]/run/route.ts index a738819..ec68f35 100644 --- a/src/app/api/expansions/[id]/run/route.ts +++ b/src/app/api/expansions/[id]/run/route.ts @@ -12,6 +12,7 @@ import { RunExpansionSchema, DirectionSchema } from "@/lib/validation"; import { getImageGenProvider } from "@/lib/image-gen"; import { getUserIdFromSession } from "@/lib/auth"; import { emitRoomEvent } from "@/lib/sse-emitter"; +import { logger } from "@/lib/logger"; import type { Direction } from "@/types"; export async function POST( @@ -123,7 +124,7 @@ export async function POST( }), ]); emitRoomEvent(expansion.roomId, "room_update"); - console.error("Image generation failed:", err instanceof Error ? err.message : String(err)); + logger.error("Image generation failed:", err); return serverError("Image generation failed"); } } diff --git a/src/app/api/rooms/[id]/generate-initial/route.ts b/src/app/api/rooms/[id]/generate-initial/route.ts index 70a1389..99d97e7 100644 --- a/src/app/api/rooms/[id]/generate-initial/route.ts +++ b/src/app/api/rooms/[id]/generate-initial/route.ts @@ -4,6 +4,7 @@ import { notFound, unauthorized, forbidden, conflict } from "@/lib/errors"; import { getUserIdFromSession } from "@/lib/auth"; import { getImageGenProvider } from "@/lib/image-gen"; import { emitRoomEvent } from "@/lib/sse-emitter"; +import { logger } from "@/lib/logger"; import type { PromptJson } from "@/types"; /** GENERATING 状態のまま放置を許容する最大時間(5分) */ @@ -80,7 +81,7 @@ export async function POST( return NextResponse.json({ status: "DONE", imageUrl: result.imagePath }); } catch (error) { - console.error("Initial tile generation failed:", error instanceof Error ? error.message : String(error)); + logger.error("Initial tile generation failed:", error); // 失敗: status FAILED await prisma.room.update({ diff --git a/src/hooks/use-room.ts b/src/hooks/use-room.ts index 811490a..5d167a0 100644 --- a/src/hooks/use-room.ts +++ b/src/hooks/use-room.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef } from "react"; +import { logger } from "@/lib/logger"; import type { RoomDetail } from "@/types"; const FALLBACK_POLL_INTERVAL = 3_000; @@ -21,7 +22,7 @@ export function useRoom(roomId: string) { setError(errData.message || "Failed to fetch room"); } } catch (err) { - console.error("Failed to fetch room:", err instanceof Error ? err.message : String(err)); + logger.error("Failed to fetch room:", err); setError("Network error"); } finally { setLoading(false); @@ -56,7 +57,7 @@ export function useRoom(roomId: string) { eventSource.onerror = () => { if (!pollTimer) { - console.warn("SSE connection lost, falling back to polling"); + logger.warn("SSE connection lost, falling back to polling"); pollTimer = setInterval(fetchRoom, FALLBACK_POLL_INTERVAL); } }; diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts index 79986ea..abc4b7c 100644 --- a/src/hooks/use-user.ts +++ b/src/hooks/use-user.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from "react"; +import { logger } from "@/lib/logger"; export interface User { id: string; @@ -19,7 +20,7 @@ export function useUser() { setUser(null); } } catch (error) { - console.error("Failed to fetch user:", error instanceof Error ? error.message : String(error)); + logger.error("Failed to fetch user:", error); setUser(null); } finally { setLoading(false); diff --git a/src/lib/auto-adopt.ts b/src/lib/auto-adopt.ts index c0ac2c2..e97db5e 100644 --- a/src/lib/auto-adopt.ts +++ b/src/lib/auto-adopt.ts @@ -2,6 +2,7 @@ import { createId } from "@paralleldrive/cuid2"; import { Prisma } from "@prisma/client"; import { prisma } from "./prisma"; import { emitRoomEvent } from "./sse-emitter"; +import { logger } from "./logger"; // DONE 状態のまま放置された Expansion を自動決定するまでの時間 (ms) // デフォルト 1 分。AUTO_ADOPT_AFTER_MS 環境変数で変更可能。 @@ -115,9 +116,9 @@ export async function autoAdoptStaleExpansions(roomId: string): Promise { } catch (err) { // バッチ処理が失敗した場合(例: ユニーク制約違反)、 // 堅牢性のために従来の逐次処理にフォールバックする - console.warn( + logger.warn( `[auto-adopt] Batched transaction failed, falling back to sequential:`, - err instanceof Error ? err.message : String(err) + err ); await autoAdoptSequentialFallback(byCell, roomId); } @@ -218,22 +219,14 @@ async function autoAdoptSequentialFallback( ]); changed = true; } catch (err) { - console.warn( - `[auto-adopt] expansion ${pick.id} fallback failed:`, - err instanceof Error ? err.message : String(err) - ); + logger.warn(`[auto-adopt] expansion ${pick.id} fallback failed:`, err); // P2: リトライループ防止 — 失敗した全候補を REJECTED に更新 await prisma.expansion .updateMany({ where: { id: { in: candidates.map((e) => e.id) }, status: "DONE" }, data: { status: "REJECTED" }, }) - .catch((e2) => - console.warn( - `[auto-adopt] fallback reject failed:`, - e2 instanceof Error ? e2.message : String(e2) - ) - ); + .catch((e2) => logger.warn(`[auto-adopt] fallback reject failed:`, e2)); } } else { // 全却下 @@ -253,10 +246,7 @@ async function autoAdoptSequentialFallback( ]); changed = true; } catch (err) { - console.warn( - `[auto-adopt] all-reject for cell failed:`, - err instanceof Error ? err.message : String(err) - ); + logger.warn(`[auto-adopt] all-reject for cell failed:`, err); } } } diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..2aa86ac --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,24 @@ +/** + * Centralized logger. + * Error objects output stack trace (or message if unavailable) for debuggability, + * while preventing raw Error objects from being passed to console methods. + */ + +const formatArg = (arg: unknown): unknown => { + if (arg instanceof Error) { + return arg.stack || arg.message; + } + return arg; +}; + +export const logger = { + info: (...args: unknown[]) => { + console.info(...args.map(formatArg)); + }, + warn: (...args: unknown[]) => { + console.warn(...args.map(formatArg)); + }, + error: (...args: unknown[]) => { + console.error(...args.map(formatArg)); + }, +};