chore: test deploy snapshot
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,17 +59,32 @@ const MANAGEMENT_APP_OPTIONS = [
|
||||
{ value: 'error-log', label: '에러 로그', description: '앱 로그와 장애 이력 조회', category: '관리' },
|
||||
] as const;
|
||||
|
||||
const PLAY_APP_OPTIONS = getReadyPlayAppEntries().map((entry) => ({
|
||||
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,
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
@@ -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<AppOption[]>(buildAppOptions);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([]);
|
||||
const [qrPreviewTokenId, setQrPreviewTokenId] = useState<string | null>(null);
|
||||
const [conversationDrawer, setConversationDrawer] = useState<ConversationDrawerState | null>(null);
|
||||
const [conversationDrawerKey, setConversationDrawerKey] = useState(0);
|
||||
const [form] = Form.useForm<SharedResourceTokenFormValue>();
|
||||
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({
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
label={`앱 권한 ${APP_OPTIONS.length}`}
|
||||
label={`앱 권한 ${appOptions.length}`}
|
||||
name="allowedAppIds"
|
||||
extra="공유 토큰 상세에서 최종 허용 앱 목록을 직접 저장합니다."
|
||||
>
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<div className="shared-resource-management-page__permission-grid">
|
||||
{APP_OPTIONS.map((option) => (
|
||||
{appOptions.map((option) => (
|
||||
<label key={option.value} className="shared-resource-management-page__permission-card">
|
||||
<div className="shared-resource-management-page__permission-card-header">
|
||||
<div className="shared-resource-management-page__permission-card-copy">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { Alert, App, Button, Card, Checkbox, Descriptions, Empty, Form, Input, InputNumber, List, Modal, Space, Switch, Table, Tabs, Tag, Typography } from 'antd';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getReadyPlayAppEntries } from '../../views/play/apps/apps/appsRegistry';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
getReadyPlayAppEntries,
|
||||
loadPlayAppEntriesFromServer,
|
||||
} from '../../views/play/apps/apps/appsRegistry';
|
||||
import { confirmWithKeyboard } from './modalKeyboard';
|
||||
import {
|
||||
deleteTokenSetting,
|
||||
@@ -66,16 +69,6 @@ const MANAGEMENT_APP_OPTIONS: AppOption[] = [
|
||||
{ value: 'error-log', label: '에러 로그', description: '앱 로그와 장애 이력 조회', category: '관리' },
|
||||
];
|
||||
|
||||
const PLAY_APP_OPTIONS: AppOption[] = getReadyPlayAppEntries().map((entry) => ({
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
description: entry.searchDescription ?? `${entry.name} 앱 실행`,
|
||||
category: 'Play',
|
||||
}));
|
||||
|
||||
const APP_OPTIONS: AppOption[] = [...MANAGEMENT_APP_OPTIONS, ...PLAY_APP_OPTIONS];
|
||||
const APP_OPTION_LABEL_MAP = new Map(APP_OPTIONS.map((item) => [item.value, item.label] as const));
|
||||
|
||||
const EMPTY_FORM_VALUE: TokenSettingFormValue = {
|
||||
id: '',
|
||||
name: '',
|
||||
@@ -175,8 +168,21 @@ function formatQuotaSummary(setting: TokenSettingRecord) {
|
||||
return [`7일 ${formatTokenLimit(setting.maxTokensPer7Days)}`, `5시간 ${formatTokenLimit(setting.maxTokensPer5Hours)}`].join(' / ');
|
||||
}
|
||||
|
||||
function resolveAppLabels(appIds: string[]) {
|
||||
return appIds.map((item) => APP_OPTION_LABEL_MAP.get(item) ?? item);
|
||||
function buildPlayAppOptionsFromCache() {
|
||||
return getReadyPlayAppEntries().map((entry) => ({
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
description: entry.searchDescription ?? `${entry.name} 앱 실행`,
|
||||
category: 'Play',
|
||||
}));
|
||||
}
|
||||
|
||||
function buildAppOptions() {
|
||||
return [...MANAGEMENT_APP_OPTIONS, ...buildPlayAppOptionsFromCache()];
|
||||
}
|
||||
|
||||
function buildAppOptionLabelMap(appOptions: AppOption[]) {
|
||||
return new Map(appOptions.map((item) => [item.value, item.label] as const));
|
||||
}
|
||||
|
||||
export function TokenSettingManagementPage({
|
||||
@@ -223,11 +229,33 @@ export function TokenSettingManagementPage({
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState('');
|
||||
const [saveSuccessMessage, setSaveSuccessMessage] = useState('');
|
||||
const [activeDetailTab, setActiveDetailTab] = useState<'basic' | 'quota' | 'apps' | 'history'>('basic');
|
||||
const [appOptions, setAppOptions] = useState<AppOption[]>(buildAppOptions);
|
||||
const [activities, setActivities] = useState<TokenSettingActivityRecord[]>([]);
|
||||
const [isActivityLoading, setIsActivityLoading] = useState(false);
|
||||
const [form] = Form.useForm<TokenSettingFormValue>();
|
||||
const [modalApi, modalContextHolder] = Modal.useModal();
|
||||
const lastHydratedFormKeyRef = useRef('');
|
||||
const appOptionLabelMap = useMemo(() => buildAppOptionLabelMap(appOptions), [appOptions]);
|
||||
const resolveAppLabelsMemo = useCallback(
|
||||
(appIds: string[]) => appIds.map((item) => appOptionLabelMap.get(item) ?? item),
|
||||
[appOptionLabelMap],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
void loadPlayAppEntriesFromServer().then(() => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAppOptions(buildAppOptions());
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const selectedTokenSetting = useMemo(
|
||||
() => tokenSettings.find((item) => item.id === selectedTokenSettingId) ?? null,
|
||||
@@ -491,7 +519,7 @@ export function TokenSettingManagementPage({
|
||||
{item.description || '설명 없음'}
|
||||
</div>
|
||||
<Space size={[6, 6]} wrap>
|
||||
{resolveAppLabels(item.allowedAppIds).map((label) => (
|
||||
{resolveAppLabelsMemo(item.allowedAppIds).map((label) => (
|
||||
<Tag key={`${item.id}-${label}`}>{label}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
@@ -723,7 +751,7 @@ export function TokenSettingManagementPage({
|
||||
},
|
||||
{
|
||||
key: 'apps',
|
||||
label: `앱 권한 ${APP_OPTIONS.length}`,
|
||||
label: `앱 권한 ${appOptions.length}`,
|
||||
children: (
|
||||
<div className="token-setting-management-page__section-scroll">
|
||||
<Form.Item
|
||||
@@ -743,7 +771,7 @@ export function TokenSettingManagementPage({
|
||||
>
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<div className="token-setting-management-page__app-grid">
|
||||
{APP_OPTIONS.map((option) => (
|
||||
{appOptions.map((option) => (
|
||||
<div key={option.value} className="token-setting-management-page__app-card">
|
||||
<div className="token-setting-management-page__app-card-header">
|
||||
<div className="token-setting-management-page__app-card-title">
|
||||
|
||||
@@ -120,8 +120,9 @@ const SHARE_ROOM_SWITCH_CACHE_MESSAGE_LIMIT = 32;
|
||||
const SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS = 1000;
|
||||
const SHARE_EDGE_NAVIGATION_HOTZONE_PX = 38;
|
||||
const SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX = 16;
|
||||
const SHARE_EDGE_GESTURE_OPEN_APPS_PX = 100;
|
||||
const SHARE_EDGE_GESTURE_OPEN_APPS_PX = 72;
|
||||
const SHARE_EDGE_APPS_GESTURE_HOTZONE_PX = 110;
|
||||
const SHARE_EDGE_GESTURE_OPEN_APPS_MAX_VERTICAL_DEVIATION_PX = 34;
|
||||
const SHARE_MINIMIZED_ACTION_TAP_TOLERANCE_PX = 8;
|
||||
const SHARE_HISTORY_PAGE_SIZE = 40;
|
||||
const SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS = [
|
||||
@@ -2053,6 +2054,27 @@ function isMobileTouchViewport() {
|
||||
&& (window.matchMedia('(pointer: coarse)').matches || window.matchMedia('(hover: none)').matches);
|
||||
}
|
||||
|
||||
function getShareViewportWidth() {
|
||||
if (typeof window === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.floor(
|
||||
Math.min(
|
||||
window.innerWidth,
|
||||
window.visualViewport?.width ?? window.innerWidth,
|
||||
document.documentElement?.clientWidth ?? window.innerWidth,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function isShareAppOpenGestureSwipe(touch: Touch, startX: number, startY: number) {
|
||||
const movedLeft = startX - touch.clientX;
|
||||
const movedUpDown = Math.abs(touch.clientY - startY);
|
||||
|
||||
return movedLeft > SHARE_EDGE_GESTURE_OPEN_APPS_PX && movedUpDown <= SHARE_EDGE_GESTURE_OPEN_APPS_MAX_VERTICAL_DEVIATION_PX;
|
||||
}
|
||||
|
||||
function isShareEdgeGestureIgnoredTarget(target: EventTarget | null) {
|
||||
if (isTypingTarget(target)) {
|
||||
return true;
|
||||
@@ -9763,9 +9785,7 @@ export function ChatSharePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
touch.clientX >= window.innerWidth - SHARE_EDGE_APPS_GESTURE_HOTZONE_PX
|
||||
) {
|
||||
if (touch.clientX >= Math.max(0, getShareViewportWidth() - SHARE_EDGE_APPS_GESTURE_HOTZONE_PX)) {
|
||||
const now = performance.now();
|
||||
event.preventDefault();
|
||||
tracking = {
|
||||
@@ -9805,7 +9825,9 @@ export function ChatSharePage() {
|
||||
const deltaX = touch.clientX - tracking.startX;
|
||||
|
||||
if (tracking.direction === 'apps') {
|
||||
if (deltaX <= SHARE_EDGE_GESTURE_OPEN_APPS_PX * -1) {
|
||||
const isSwipeLeftEnough = isShareAppOpenGestureSwipe(touch, tracking.startX, tracking.startY);
|
||||
|
||||
if (isSwipeLeftEnough) {
|
||||
if (deltaX <= SHARE_EDGE_GESTURE_MIN_HORIZONTAL_PX * -1) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -9837,7 +9859,11 @@ export function ChatSharePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tracking.direction === 'apps' && !tracking.opened) {
|
||||
if (
|
||||
tracking.direction === 'apps'
|
||||
&& !tracking.opened
|
||||
&& isShareAppOpenGestureSwipe(touch, tracking.startX, tracking.startY)
|
||||
) {
|
||||
const deltaX = touch.clientX - tracking.startX;
|
||||
|
||||
if (deltaX <= SHARE_EDGE_GESTURE_OPEN_APPS_PX * -1) {
|
||||
@@ -9868,6 +9894,7 @@ export function ChatSharePage() {
|
||||
};
|
||||
}, [
|
||||
activeProcessInspectorRequestId,
|
||||
activeShareRoomSessionId,
|
||||
isCreateRoomOpen,
|
||||
isOriginReplyModalOpen,
|
||||
isRoomSettingsOpen,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user