From 5b3e70910cceaa9d2e337a85d9e1fbebba146ec4 Mon Sep 17 00:00:00 2001 From: how2ice Date: Fri, 29 May 2026 17:42:33 +0900 Subject: [PATCH] chore: test deploy snapshot --- .../work-server/src/routes/play-app.ts | 38 +- src/app/main/SharedResourceManagementPage.tsx | 70 +- src/app/main/TokenSettingManagementPage.tsx | 66 +- src/app/main/pages/ChatSharePage.tsx | 39 +- .../apps/template1/Template1PlayAppView.css | 987 +++++- .../apps/template1/Template1PlayAppView.tsx | 2671 ++++++++++++++++- 6 files changed, 3701 insertions(+), 170 deletions(-) diff --git a/etc/servers/work-server/src/routes/play-app.ts b/etc/servers/work-server/src/routes/play-app.ts index 0674a16..2f0a247 100644 --- a/etc/servers/work-server/src/routes/play-app.ts +++ b/etc/servers/work-server/src/routes/play-app.ts @@ -1,5 +1,6 @@ -import type { FastifyInstance } from 'fastify'; +import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { z } from 'zod'; +import { env } from '../config/env.js'; import { db } from '../db/client.js'; type PlayAppEnvironment = 'preview' | 'test' | 'prod'; @@ -18,6 +19,23 @@ type PlayAppSeedEntry = { }; const PLAY_APP_TABLE = 'play_apps'; + +function getRequestAccessToken(request: FastifyRequest) { + const tokenHeader = request.headers['x-access-token']; + return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim(); +} + +function ensurePlayAppWriteAuthorized(request: FastifyRequest, reply: FastifyReply) { + if (getRequestAccessToken(request) === env.SERVER_COMMAND_ACCESS_TOKEN) { + return true; + } + + reply.code(403).send({ + message: '권한 토큰이 필요합니다.', + }); + return false; +} + const DEFAULT_ENTRIES: PlayAppSeedEntry[] = [ { id: 'baseball-ticket-bay', @@ -546,15 +564,27 @@ export async function registerPlayAppRoutes(app: FastifyInstance) { }; }); - app.post('/api/play-apps', async (request) => { + app.post('/api/play-apps', async (request, reply) => { + if (!ensurePlayAppWriteAuthorized(request, reply)) { + return; + } + return createPlayAppEntry(request.body); }); - app.put('/api/play-apps/:id', async (request) => { + app.put('/api/play-apps/:id', async (request, reply) => { + if (!ensurePlayAppWriteAuthorized(request, reply)) { + return; + } + return updatePlayAppEntry(request.params, request.body); }); - app.delete('/api/play-apps/:id', async (request) => { + app.delete('/api/play-apps/:id', async (request, reply) => { + if (!ensurePlayAppWriteAuthorized(request, reply)) { + return; + } + return deletePlayAppEntry(request.params); }); } diff --git a/src/app/main/SharedResourceManagementPage.tsx b/src/app/main/SharedResourceManagementPage.tsx index 68e2def..92dcb3a 100644 --- a/src/app/main/SharedResourceManagementPage.tsx +++ b/src/app/main/SharedResourceManagementPage.tsx @@ -1,7 +1,10 @@ import { DeleteOutlined, LinkOutlined, PlusOutlined, QrcodeOutlined, ReloadOutlined, SaveOutlined, StopOutlined, UnorderedListOutlined } from '@ant-design/icons'; import { Alert, App, Button, Card, Checkbox, Drawer, Empty, Flex, Form, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tabs, Tag, Typography } from 'antd'; -import { useEffect, useLayoutEffect, useMemo, useState, type Key, type MouseEvent as ReactMouseEvent } from 'react'; -import { getReadyPlayAppEntries } from '../../views/play/apps/apps/appsRegistry'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState, type Key, type MouseEvent as ReactMouseEvent } from 'react'; +import { + getReadyPlayAppEntries, + loadPlayAppEntriesFromServer, +} from '../../views/play/apps/apps/appsRegistry'; import { copyTextToClipboard } from '../../utils/clipboard'; import { openExternalLinkInNewWindow } from './mainChatPanel/linkNavigation'; import { resolveChatPathForSession } from './chatSessionRouting'; @@ -56,18 +59,33 @@ const MANAGEMENT_APP_OPTIONS = [ { value: 'error-log', label: '에러 로그', description: '앱 로그와 장애 이력 조회', category: '관리' }, ] as const; -const PLAY_APP_OPTIONS = getReadyPlayAppEntries().map((entry) => ({ - value: entry.id, - label: entry.name, - description: entry.searchDescription ?? `${entry.name} 앱 실행`, - category: 'Play' as const, -})); - -const APP_OPTIONS = [...MANAGEMENT_APP_OPTIONS, ...PLAY_APP_OPTIONS]; -const APP_OPTION_LABEL_MAP = new Map(APP_OPTIONS.map((option) => [option.value, option.label] as const)); const ADMIN_PERMISSION_PRESET: SharedResourcePermission[] = ['view', 'download', 'comment', 'upload', 'manage']; const ADMIN_ALLOWED_APP_PRESET = MANAGEMENT_APP_OPTIONS.map((option) => option.value); +type AppOption = { + value: string; + label: string; + description: string; + category: '관리' | 'Play'; +}; + +function buildPlayAppOptionsFromCache() { + return getReadyPlayAppEntries().map((entry) => ({ + value: entry.id, + label: entry.name, + description: entry.searchDescription ?? `${entry.name} 앱 실행`, + category: 'Play' as const, + })); +} + +function buildAppOptions() { + return [...MANAGEMENT_APP_OPTIONS, ...buildPlayAppOptionsFromCache()]; +} + +function buildAppOptionLabelMap(appOptions: AppOption[]) { + return new Map(appOptions.map((option) => [option.value, option.label] as const)); +} + type SharedResourceManagementSharedPreview = { managedResourceTokenId?: string | null; sharePath?: string | null; @@ -188,10 +206,6 @@ function formatPermissionLabel(permission: SharedResourcePermission) { return PERMISSION_OPTIONS.find((option) => option.value === permission)?.label ?? permission; } -function formatAppLabel(appId: string) { - return APP_OPTION_LABEL_MAP.get(appId) ?? appId; -} - function formatDurationMinutesLabel(totalMinutes: number | null | undefined) { const normalizedMinutes = Number(totalMinutes ?? 0); @@ -513,12 +527,34 @@ export function SharedResourceManagementPage({ const [detailData, setDetailData] = useState<{ token: SharedResourceTokenRecord; activities: SharedResourceTokenActivityRecord[] } | null>(null); const [nowMs, setNowMs] = useState(() => Date.now()); const [activeDetailTab, setActiveDetailTab] = useState<'basic' | 'settings' | 'history'>('basic'); + const [appOptions, setAppOptions] = useState(buildAppOptions); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [qrPreviewTokenId, setQrPreviewTokenId] = useState(null); const [conversationDrawer, setConversationDrawer] = useState(null); const [conversationDrawerKey, setConversationDrawerKey] = useState(0); const [form] = Form.useForm(); const [modalApi, modalContextHolder] = Modal.useModal(); + const appOptionLabelMap = useMemo(() => buildAppOptionLabelMap(appOptions), [appOptions]); + const formatAppLabel = useCallback( + (appId: string) => appOptionLabelMap.get(appId) ?? appId, + [appOptionLabelMap], + ); + + useEffect(() => { + let active = true; + + void loadPlayAppEntriesFromServer().then(() => { + if (!active) { + return; + } + + setAppOptions(buildAppOptions()); + }); + + return () => { + active = false; + }; + }, []); useLayoutEffect(() => { if (disableInstallMetadata || typeof window === 'undefined') { @@ -1604,13 +1640,13 @@ export function SharedResourceManagementPage({
- {APP_OPTIONS.map((option) => ( + {appOptions.map((option) => (