diff --git a/.env.example b/.env.example index 5f072538..de61f188 100644 --- a/.env.example +++ b/.env.example @@ -31,13 +31,15 @@ GATEWAY_DB_SCHEMA=public GATEWAY_WORKSPACE_ROOT=/path/to/core2026 GATEWAY_WORKTREE_ROOT=/path/to/core2026/.worktrees GATEWAY_USER_ICON_DIR=uploads/user-icons -GATEWAY_USER_ICON_PUBLIC_URL=http://localhost:13000/user-icons +GATEWAY_USER_ICON_PUBLIC_URL=https://sam-image.hided.net/icons GATEWAY_IMAGE_UPLOAD_URL=https://sam-image.hided.net GATEWAY_IMAGE_UPLOAD_SECRET_FILE=/run/secrets/image_upload_core2026_secret GATEWAY_SHARED_ICON_PUBLIC_URL=https://sam-image.hided.net/icons GAME_IMAGE_UPLOAD_URL=https://sam-image.hided.net GAME_IMAGE_UPLOAD_SECRET_FILE=/run/secrets/image_upload_core2026_secret GAME_CONTENT_IMAGE_PUBLIC_URL=https://sam-image.hided.net/uploads/core2026 +IMAGE_SYNC_URL=https://sam-image.hided.net +IMAGE_SYNC_SECRET_FILE=/run/secrets/image_sync_core2026_secret GATEWAY_LOCAL_REGISTRATION_ENABLED=true GATEWAY_LOCAL_ACCOUNT_GRACE_DAYS=7 # Optional. Keep the PEM private key outside Git; if omitted, a per-process RSA key is generated. @@ -65,10 +67,11 @@ TRPC_PATH=/trpc # Frontend public URLs # These values are public and become part of the browser bundle. VITE_GATEWAY_WEB_URL=/gateway/ +VITE_IMAGE_PUBLIC_URL=https://sam-image.hided.net # Comma-separated public hostnames accepted by Vite preview. Use * only behind a trusted reverse proxy. VITE_PREVIEW_ALLOWED_HOSTS=localhost,dev-sam-e2e.hided.net -# Account-uploaded icons are served by the Gateway and remain a public browser URL. -VITE_GATEWAY_USER_ICON_BASE_URL=/gateway/api/user-icons +# Account-uploaded icons are public immutable files on the image service. +VITE_GATEWAY_USER_ICON_BASE_URL=https://sam-image.hided.net/icons VITE_BOARD_COMMUNITY_URL=/xe/community # Set the remaining links only after their public routes are confirmed. VITE_BOARD_REQUEST_URL= diff --git a/README.md b/README.md index fa70d4e4..873bca31 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,14 @@ mount하고 다음 서버 전용 변수를 설정합니다. GATEWAY_IMAGE_UPLOAD_URL=https://sam-image.hided.net GATEWAY_IMAGE_UPLOAD_SECRET_FILE=/run/secrets/image_upload_core2026_secret GATEWAY_SHARED_ICON_PUBLIC_URL=https://sam-image.hided.net/icons +GATEWAY_USER_ICON_PUBLIC_URL=https://sam-image.hided.net/icons GAME_IMAGE_UPLOAD_URL=https://sam-image.hided.net GAME_IMAGE_UPLOAD_SECRET_FILE=/run/secrets/image_upload_core2026_secret GAME_CONTENT_IMAGE_PUBLIC_URL=https://sam-image.hided.net/uploads/core2026 IMAGE_SYNC_URL=https://sam-image.hided.net IMAGE_SYNC_SECRET_FILE=/run/secrets/image_sync_core2026_secret +VITE_IMAGE_PUBLIC_URL=https://sam-image.hided.net +VITE_GATEWAY_USER_ICON_BASE_URL=https://sam-image.hided.net/icons ``` Gateway가 인증과 50KB·크기·형식을 확인한 뒤 60초짜리 HMAC 요청으로 서버 간 @@ -107,6 +110,14 @@ PUT을 수행합니다. game-api의 국방·외교·정찰 편집기 첨부 이 사용하되 `/uploads/core2026/` bind 경로에 저장합니다. 공유 비밀값은 `VITE_*`, 브라우저 응답 또는 Cloudflare로 전달하지 않습니다. +두 frontend는 별도 설정이 없으면 `https://sam-image.hided.net`을 공개 이미지 +origin으로 사용합니다. 게임 자산은 `/game`, 공용 장수 아이콘은 `/icons`, +Core2026 사용자 아이콘은 서버가 발급한 `users/core2026/<파일>`을 `/icons` +base 아래에 붙여 읽습니다. 로컬 +이미지 fixture가 필요한 비교 환경만 `VITE_IMAGE_PUBLIC_URL=/image`와 기존 +`VITE_GAME_ASSET_URL`을 명시적으로 덮어씁니다. 모든 `VITE_*` 값은 공개값이며, +업로드 HMAC secret은 반드시 위의 server-side secret 파일로만 주입합니다. + Gitea webhook을 놓친 경우에는 별도의 `image_sync_core2026_secret`을 mount한 서버 컨테이너에서 `pnpm sync:image`를 실행합니다. 특정 이미지 저장소 commit을 확인하면서 동기화하려면 `pnpm sync:image -- --commit `를 diff --git a/app/game-api/src/messages/targets.ts b/app/game-api/src/messages/targets.ts index fa5c8158..699324a1 100644 --- a/app/game-api/src/messages/targets.ts +++ b/app/game-api/src/messages/targets.ts @@ -7,6 +7,8 @@ const DEFAULT_NATION = { color: '#000000', }; +const DEFAULT_SHARED_ICON_PUBLIC_URL = 'https://sam-image.hided.net/icons'; + export const resolveNationInfo = async ( db: DatabaseClient, nationId: number @@ -30,7 +32,7 @@ export const buildTargetFromGeneral = async (db: DatabaseClient, general: Genera nationId: general.nationId, nationName: nation.name, color: nation.color, - icon: general.imageServer ? `d_pic/${picture}` : `/image/icons/${picture}`, + icon: general.imageServer ? `d_pic/${picture}` : `${DEFAULT_SHARED_ICON_PUBLIC_URL}/${picture}`, }; }; diff --git a/app/game-frontend/e2e/board.spec.ts b/app/game-frontend/e2e/board.spec.ts index ed0178da..ca1ea528 100644 --- a/app/game-frontend/e2e/board.spec.ts +++ b/app/game-frontend/e2e/board.spec.ts @@ -85,7 +85,7 @@ const readImage = async (relative: string): Promise => { const installImages = async (page: Page) => { for (const filename of ['back_walnut.jpg', 'back_green.jpg', 'back_blue.jpg']) { - await page.route(`**/image/game/${filename}`, async (route) => { + await page.route(`https://sam-image.hided.net/game/${filename}`, async (route) => { await route.fulfill({ status: 200, contentType: 'image/jpeg', @@ -93,7 +93,7 @@ const installImages = async (page: Page) => { }); }); } - await page.route('**/image/icons/**', async (route) => { + await page.route('https://sam-image.hided.net/icons/**', async (route) => { await route.fulfill({ status: 200, contentType: 'image/jpeg', @@ -270,6 +270,10 @@ test('matches the ref meeting-room geometry, typography, textures, and controls' await installApi(page, state); await page.setViewportSize({ width: 1000, height: 800 }); await gotoBoard(page); + await expect(page.locator('.general-icon').first()).toHaveAttribute( + 'src', + 'https://sam-image.hided.net/icons/22.jpg' + ); if (artifactRoot) { await page.screenshot({ path: resolve(artifactRoot, 'board-core-desktop.png'), @@ -331,9 +335,9 @@ test('matches the ref meeting-room geometry, typography, textures, and controls' expect(geometry.font.family).toContain('Pretendard'); expect(geometry.font).toMatchObject({ size: '14px', lineHeight: '18.2px' }); expect(geometry.whiteSpace).toBe('pre'); - expect(geometry.walnut).toContain('back_walnut.jpg'); - expect(geometry.green).toContain('back_green.jpg'); - expect(geometry.blue).toContain('back_blue.jpg'); + expect(geometry.walnut).toContain('https://sam-image.hided.net/game/back_walnut.jpg'); + expect(geometry.green).toContain('https://sam-image.hided.net/game/back_green.jpg'); + expect(geometry.blue).toContain('https://sam-image.hided.net/game/back_blue.jpg'); expect(geometry.submitStyle).toEqual({ backgroundColor: 'rgb(68, 68, 68)', borderColor: 'rgb(61, 61, 61)', diff --git a/app/game-frontend/src/assets/styles/tokens.css b/app/game-frontend/src/assets/styles/tokens.css index f740a9af..4ece75b8 100644 --- a/app/game-frontend/src/assets/styles/tokens.css +++ b/app/game-frontend/src/assets/styles/tokens.css @@ -20,7 +20,7 @@ --sammo-button-info-border: #2f89c5; --sammo-button-navigation-bg: #00582c; --sammo-button-navigation-border: #004f28; - --sammo-texture-walnut: url('/image/game/back_walnut.jpg'); - --sammo-texture-green: url('/image/game/back_green.jpg'); - --sammo-texture-blue: url('/image/game/back_blue.jpg'); + --sammo-texture-walnut: url('https://sam-image.hided.net/game/back_walnut.jpg'); + --sammo-texture-green: url('https://sam-image.hided.net/game/back_green.jpg'); + --sammo-texture-blue: url('https://sam-image.hided.net/game/back_blue.jpg'); } diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index 3504d19e..78290f3e 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -7,6 +7,7 @@ import MapCityBasic from './MapCityBasic.vue'; import MapCityDetail from './MapCityDetail.vue'; import { useMapViewerStore } from '../../stores/mapViewer'; import { buildAssetUrl } from '../../utils/mapAssets'; +import { configuredGameAssetUrl } from '../../utils/imageAssets'; interface MapSummary { year: number; @@ -102,7 +103,7 @@ const resolveStateClass = (state: number): CityStateClass => { return 'wrong'; }; -const assetBaseUrl = computed(() => import.meta.env.VITE_GAME_ASSET_URL?.trim() || '/image/game'); +const assetBaseUrl = computed(configuredGameAssetUrl); const resolveAsset = (path: string) => buildAssetUrl(assetBaseUrl.value, path); const nationById = computed(() => { diff --git a/app/game-frontend/src/components/main/MessagePlate.vue b/app/game-frontend/src/components/main/MessagePlate.vue index f678161b..228fa5f6 100644 --- a/app/game-frontend/src/components/main/MessagePlate.vue +++ b/app/game-frontend/src/components/main/MessagePlate.vue @@ -1,7 +1,7 @@