From 3d75164ffa275bea319490f93ebca759f69e0794 Mon Sep 17 00:00:00 2001 From: Zuhanit Date: Sun, 8 Mar 2026 11:52:54 +0900 Subject: [PATCH 1/3] feat: migrate static file requests from backend to R2 Replace backend API calls for static assets (anim, terrain) with direct requests to NEXT_PUBLIC_STATIC_BASE_URL (R2 bucket). Co-Authored-By: Claude Sonnet 4.6 --- frontend/hooks/useImage.ts | 27 +++++++++++++++------------ frontend/hooks/useTileGroup.ts | 11 ++++++----- frontend/hooks/useTilesetData.ts | 7 ++++--- frontend/utils/globalConfig.ts | 1 + 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/frontend/hooks/useImage.ts b/frontend/hooks/useImage.ts index 1a6a0bf..1856f98 100644 --- a/frontend/hooks/useImage.ts +++ b/frontend/hooks/useImage.ts @@ -1,4 +1,3 @@ -import api from "@/lib/api"; import { useQueries, useQuery } from "@tanstack/react-query"; import { SCImageBundle, @@ -8,6 +7,7 @@ import { ImageVersion, } from "@/types/SCImage"; import axios from "axios"; +import { globalConfig } from "@/utils/globalConfig"; import { useUsemapStore } from "@/components/pages/editor-page"; import useTileGroup from "./useTileGroup"; import useTilesetData from "./useTilesetData"; @@ -43,18 +43,18 @@ export function useImage({ version, imageIndex }: SCImage) { imageIndex: imageIndex, }); - const base = `/static/anim/${v}/${idx}`; + const base = `${globalConfig.STATIC_BASE_URL}/anim/${v}/${idx}`; const { data, isLoading, isSuccess } = useQuery({ queryKey: ["scImage", v, idx], queryFn: async () => { const [diffuse, teamColor, meta] = await Promise.all([ - (await api.get(`${base}/diffuse.png`, { responseType: "blob" })) + (await axios.get(`${base}/diffuse.png`, { responseType: "blob" })) .data, safeGet( - api.get(`${base}/team_color.png`, { responseType: "blob" }), + axios.get(`${base}/team_color.png`, { responseType: "blob" }), ), - (await api.get(`${base}/meta.json`)).data, + (await axios.get(`${base}/meta.json`)).data, ]); return { diffuse, teamColor, meta }; @@ -70,9 +70,9 @@ export function useImageManifest(version: ImageVersion) { return useQuery({ queryKey: [version, "manifest"], queryFn: async () => { - const res = await api.get< + const res = await axios.get< Record - >(`/static/anim/${version}/manifest.json`); + >(`${globalConfig.STATIC_BASE_URL}/anim/${version}/manifest.json`); const obj = res.data; // already parsed JSON return new Map( @@ -91,20 +91,23 @@ export function useImages(imageIDs: Set, version: ImageVersion) { enabled: !!manifest?.get(id)?.diffuse, // skip if diffuse missing queryKey: ["unitImage", version, id], queryFn: async () => { - const base = `/static/anim/${version}/${id}`; + const base = `${globalConfig.STATIC_BASE_URL}/anim/${version}/${id}`; // ① diffuse + meta (필수) const [diffuse, meta] = await Promise.all([ - (await api.get(`${base}/diffuse.png`, { responseType: "blob" })) - .data, - (await api.get(`${base}/meta.json`)).data, + ( + await axios.get(`${base}/diffuse.png`, { + responseType: "blob", + }) + ).data, + (await axios.get(`${base}/meta.json`)).data, ]); // ② team_color (선택) let teamColor: Blob | undefined; if (manifest?.get(id)?.team_color) { teamColor = await safeGet( - api.get(`${base}/team_color.png`, { responseType: "blob" }), + axios.get(`${base}/team_color.png`, { responseType: "blob" }), ); } diff --git a/frontend/hooks/useTileGroup.ts b/frontend/hooks/useTileGroup.ts index 8dd57b8..6ed7d20 100644 --- a/frontend/hooks/useTileGroup.ts +++ b/frontend/hooks/useTileGroup.ts @@ -1,17 +1,18 @@ -import api from "@/lib/api"; +import { globalConfig } from "@/utils/globalConfig"; +import axios from "axios"; import { useEffect, useState } from "react"; type TileGroup = number[][]; -export default function useTileGroup(): TileGroup | null { +export default function useTileGroup(): TileGroup | null { const [tileGroup, setTileGroup] = useState(null); - + useEffect(() => { (async () => { - const response = await api.get("/api/v1/tileset/cv5/badlands"); + const response = await axios.get(`${globalConfig.STATIC_BASE_URL}/terrain/badlands/cv5_group.json`); setTileGroup(response.data); })(); }, []); - + return tileGroup; } \ No newline at end of file diff --git a/frontend/hooks/useTilesetData.ts b/frontend/hooks/useTilesetData.ts index adae580..c796e89 100644 --- a/frontend/hooks/useTilesetData.ts +++ b/frontend/hooks/useTilesetData.ts @@ -1,4 +1,5 @@ -import api from "@/lib/api"; +import { globalConfig } from "@/utils/globalConfig"; +import axios from "axios"; import { decompressSync } from "fflate"; import { useEffect, useState } from "react"; @@ -8,8 +9,8 @@ export default function useTilesetData(): Uint8Array | null { useEffect(() => { (async () => { try { - const response = await api.get( - "/static/terrain/badlands/megatile_color.gz", + const response = await axios.get( + `${globalConfig.STATIC_BASE_URL}/terrain/badlands/megatile_color.gz`, { responseType: "arraybuffer", }, diff --git a/frontend/utils/globalConfig.ts b/frontend/utils/globalConfig.ts index 663878f..bdb0c6c 100644 --- a/frontend/utils/globalConfig.ts +++ b/frontend/utils/globalConfig.ts @@ -1,3 +1,4 @@ export const globalConfig = { BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + STATIC_BASE_URL: process.env.NEXT_PUBLIC_STATIC_BASE_URL, }; From 283e7819bcbe76157dfb96a21f8f749962270ede Mon Sep 17 00:00:00 2001 From: Zuhanit Date: Sun, 8 Mar 2026 11:53:02 +0900 Subject: [PATCH 2/3] feat: remove static file serving from backend Delete tileset API endpoints and StaticFiles mount since static assets are now served directly from R2. Also remove preprocess output copy from Dockerfile. Co-Authored-By: Claude Sonnet 4.6 --- backend/Dockerfile | 6 +--- backend/app/api/v1/endpoints/tileset.py | 37 ------------------------- backend/app/main.py | 26 +---------------- 3 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 backend/app/api/v1/endpoints/tileset.py diff --git a/backend/Dockerfile b/backend/Dockerfile index cf8bb3a..1109495 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -15,12 +15,8 @@ COPY backend/ /app/ # Install dependencies RUN uv sync --no-dev -# Copy preprocess output (tileset static data) -# tileset.py resolves 6 parent dirs up from its location to reach project root -COPY preprocess/output/ /preprocess/output/ - # Create required runtime directories -RUN mkdir -p /app/output /app/logs /app/static +RUN mkdir -p /app/output /app/logs EXPOSE 8000 diff --git a/backend/app/api/v1/endpoints/tileset.py b/backend/app/api/v1/endpoints/tileset.py deleted file mode 100644 index 23f4fc7..0000000 --- a/backend/app/api/v1/endpoints/tileset.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import APIRouter -from fastapi.responses import FileResponse -from pathlib import Path - - -router = APIRouter() - - -@router.get("/megatile/{tileset_name}") -async def send_tileset(tileset_name: str): - file_path = ( - Path(__file__).resolve().parent.parent.parent.parent.parent.parent - / "preprocess" - / "output" - / "terrain" - / tileset_name - / "megatile_color.gz" - ) - - return FileResponse( - file_path, - media_type="application/octet-stream", - ) - - -@router.get("/cv5/{tileset_name}") -async def send_cv5_group(tileset_name: str): - file_path = ( - Path(__file__).resolve().parent.parent.parent.parent.parent.parent - / "preprocess" - / "output" - / "terrain" - / tileset_name - / "cv5_group.json" - ) - - return FileResponse(file_path, media_type="application/json") diff --git a/backend/app/main.py b/backend/app/main.py index 0392a28..ff639ac 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,12 +1,10 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from app.api.v1.endpoints import map, user, tileset +from app.api.v1.endpoints import map, user from app.core.w_logging import get_logger, setup_logging from fastapi.responses import JSONResponse from firebase_admin import auth as firebase_auth from firebase_admin._auth_utils import InvalidIdTokenError -from starlette.responses import Response setup_logging() @@ -29,27 +27,6 @@ allow_headers=["*"], ) - -class StaticFilesCache(StaticFiles): - """Cached Static Files, see https://stackoverflow.com/questions/66093397/how-to-disable-starlette-static-files-caching""" - - def __init__( - self, - *args, - cachecontrol="public, max-age=31536000, s-maxage=31536000, immutable", - **kwargs, - ): - self.cachecontrol = cachecontrol - super().__init__(*args, **kwargs) - - def file_response(self, *args, **kwargs) -> Response: - resp: Response = super().file_response(*args, **kwargs) - resp.headers.setdefault("Cache-Control", self.cachecontrol) - return resp - - -app.mount("/static", StaticFilesCache(directory="static"), name="static") - logger = get_logger("fastapi") @@ -85,7 +62,6 @@ async def log_reqeust(request: Request, call_next): app.include_router(map.router, prefix="/api/v1/maps", tags=["Maps"]) app.include_router(user.router, prefix="/api/v1/user", tags=["User"]) -app.include_router(tileset.router, prefix="/api/v1/tileset", tags=["Tileset"]) @app.get("/") From c89504bcce2054f9ac845525f55f512542e10240 Mon Sep 17 00:00:00 2001 From: Zuhanit Date: Sun, 8 Mar 2026 11:53:07 +0900 Subject: [PATCH 3/3] docs: add NEXT_PUBLIC_STATIC_BASE_URL setup instruction Co-Authored-By: Claude Sonnet 4.6 --- frontend/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/README.md b/frontend/README.md index 1b2d1d7..60971ce 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -17,6 +17,11 @@ NEXT_PUBLIC_FIREBASE_APP_ID= NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= ``` +3. Add static file URL in .env +```jsx +NEXT_PUBLIC_STATIC_BASE_URL= +``` + ### Note: Is it safe to expose Firebase apiKey to the public? See this Q&A: [Is it safe to expose Firebase apiKey to the public?](https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public)