chore: test deploy snapshot

This commit is contained in:
2026-05-27 11:19:49 +09:00
parent 4c4b3c8d2c
commit 7e9c3bd097
5 changed files with 122 additions and 39 deletions

View File

@@ -1591,6 +1591,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(), accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(),
chatTypeId: z.string().trim().min(1).max(120).optional().nullable(), chatTypeId: z.string().trim().min(1).max(120).optional().nullable(),
chatTypeLabel: z.string().trim().min(1).max(200).optional().nullable(), chatTypeLabel: z.string().trim().min(1).max(200).optional().nullable(),
title: z.string().trim().min(1).max(200).optional().nullable(),
notifyOffline: z.boolean().optional().nullable(), notifyOffline: z.boolean().optional().nullable(),
}).parse(request.body ?? {}); }).parse(request.body ?? {});
const managedContext = await resolveManagedChatShareContext(params.token); const managedContext = await resolveManagedChatShareContext(params.token);
@@ -1655,13 +1656,14 @@ export async function registerChatRoutes(app: FastifyInstance) {
let updatedConversation = await getChatConversation(tokenPayload.sessionId, getRequestClientId(request)); let updatedConversation = await getChatConversation(tokenPayload.sessionId, getRequestClientId(request));
if (payload.chatTypeId || payload.notifyOffline != null) { if (payload.chatTypeId || payload.title || payload.notifyOffline != null) {
updatedConversation = await updateChatConversationContext(tokenPayload.sessionId, { updatedConversation = await updateChatConversationContext(tokenPayload.sessionId, {
clientId: getRequestClientId(request), clientId: getRequestClientId(request),
chatTypeId: payload.chatTypeId?.trim() || undefined, chatTypeId: payload.chatTypeId?.trim() || undefined,
lastChatTypeId: payload.chatTypeId?.trim() || undefined, lastChatTypeId: payload.chatTypeId?.trim() || undefined,
contextLabel: payload.chatTypeLabel?.trim() || undefined, contextLabel: payload.chatTypeLabel?.trim() || undefined,
contextDescription: payload.chatTypeId ? null : undefined, contextDescription: payload.chatTypeId ? null : undefined,
title: payload.title?.trim() || undefined,
notifyOffline: payload.notifyOffline ?? undefined, notifyOffline: payload.notifyOffline ?? undefined,
}); });
} }

View File

@@ -2569,6 +2569,7 @@ export async function saveChatShareRoomSettings(
accessPinPromptTtlMinutes?: number | null; accessPinPromptTtlMinutes?: number | null;
chatTypeId?: string | null; chatTypeId?: string | null;
chatTypeLabel?: string | null; chatTypeLabel?: string | null;
title?: string | null;
notifyOffline?: boolean | null; notifyOffline?: boolean | null;
}, },
) { ) {
@@ -2595,6 +2596,7 @@ export async function saveChatShareRoomSettings(
accessPinPromptTtlMinutes: input.accessPinPromptTtlMinutes, accessPinPromptTtlMinutes: input.accessPinPromptTtlMinutes,
chatTypeId: input.chatTypeId, chatTypeId: input.chatTypeId,
chatTypeLabel: input.chatTypeLabel, chatTypeLabel: input.chatTypeLabel,
title: input.title,
notifyOffline: input.notifyOffline, notifyOffline: input.notifyOffline,
}), }),
}, },

View File

@@ -1826,6 +1826,18 @@
min-width: 0; min-width: 0;
} }
.chat-share-page__first-inquiry-title-row {
display: flex;
align-items: flex-start;
gap: 6px;
min-width: 0;
}
.chat-share-page__first-inquiry-title-row .chat-share-page__section-action.ant-btn {
flex: 0 0 auto;
margin-top: -2px;
}
.chat-share-page__first-inquiry-menu-badge { .chat-share-page__first-inquiry-menu-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -1,4 +1,4 @@
import { AppstoreOutlined, CheckOutlined, CloseOutlined, CopyOutlined, DeleteOutlined, DownOutlined, EyeInvisibleOutlined, EyeOutlined, FileTextOutlined, FilterOutlined, FullscreenOutlined, LeftOutlined, PlusOutlined, ReloadOutlined, RightOutlined, SearchOutlined, SendOutlined, SettingOutlined, ThunderboltOutlined, UpOutlined } from '@ant-design/icons'; import { AppstoreOutlined, CheckOutlined, CloseOutlined, CopyOutlined, DeleteOutlined, DownOutlined, EditOutlined, EyeInvisibleOutlined, EyeOutlined, FileTextOutlined, FilterOutlined, FullscreenOutlined, LeftOutlined, PlusOutlined, ReloadOutlined, RightOutlined, SearchOutlined, SendOutlined, SettingOutlined, ThunderboltOutlined, UpOutlined } from '@ant-design/icons';
import { Alert, App, Button, Checkbox, Drawer, Dropdown, Input, Modal, Select, Spin, Tabs, Tag, Typography, type MenuProps } from 'antd'; import { Alert, App, Button, Checkbox, Drawer, Dropdown, Input, Modal, Select, Spin, Tabs, Tag, Typography, type MenuProps } from 'antd';
import type { TextAreaRef } from 'antd/es/input/TextArea'; import type { TextAreaRef } from 'antd/es/input/TextArea';
import { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type FocusEvent, type KeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react'; import { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ClipboardEvent, type FocusEvent, type KeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react';
@@ -3006,6 +3006,7 @@ export function ChatSharePage() {
const [isRoomSettingsOpen, setIsRoomSettingsOpen] = useState(false); const [isRoomSettingsOpen, setIsRoomSettingsOpen] = useState(false);
const [isSavingRoomSettings, setIsSavingRoomSettings] = useState(false); const [isSavingRoomSettings, setIsSavingRoomSettings] = useState(false);
const [roomSettingsTabKey, setRoomSettingsTabKey] = useState<'chat-type' | 'default-contexts' | 'room-context' | 'notifications' | 'security'>('chat-type'); const [roomSettingsTabKey, setRoomSettingsTabKey] = useState<'chat-type' | 'default-contexts' | 'room-context' | 'notifications' | 'security'>('chat-type');
const [editingRoomTitle, setEditingRoomTitle] = useState('');
const [editingRoomChatTypeId, setEditingRoomChatTypeId] = useState<string | null>(null); const [editingRoomChatTypeId, setEditingRoomChatTypeId] = useState<string | null>(null);
const [editingRoomDefaultContextIds, setEditingRoomDefaultContextIds] = useState<string[]>([]); const [editingRoomDefaultContextIds, setEditingRoomDefaultContextIds] = useState<string[]>([]);
const [isEditingRoomDefaultContextsDirty, setIsEditingRoomDefaultContextsDirty] = useState(false); const [isEditingRoomDefaultContextsDirty, setIsEditingRoomDefaultContextsDirty] = useState(false);
@@ -3482,6 +3483,12 @@ export function ChatSharePage() {
} }
const nextChatTypeId = currentSharedChatTypeId ?? enabledChatTypes[0]?.id ?? null; const nextChatTypeId = currentSharedChatTypeId ?? enabledChatTypes[0]?.id ?? null;
const fallbackRoomTitle =
snapshot.conversation.title?.trim()
|| snapshot.targetRequest.userText?.trim()
|| snapshot.requests?.[0]?.userText?.trim()
|| '-';
setEditingRoomTitle(fallbackRoomTitle);
setEditingRoomChatTypeId(nextChatTypeId); setEditingRoomChatTypeId(nextChatTypeId);
setEditingRoomDefaultContextIds(resolveShareRoomDefaultContextIds(activeRoomContextSettings, chatTypeDefaults, nextChatTypeId)); setEditingRoomDefaultContextIds(resolveShareRoomDefaultContextIds(activeRoomContextSettings, chatTypeDefaults, nextChatTypeId));
setIsEditingRoomDefaultContextsDirty(false); setIsEditingRoomDefaultContextsDirty(false);
@@ -3501,9 +3508,12 @@ export function ChatSharePage() {
currentSharedChatTypeId, currentSharedChatTypeId,
enabledChatTypes, enabledChatTypes,
snapshot?.conversation.notifyOffline, snapshot?.conversation.notifyOffline,
snapshot?.conversation.title,
snapshot?.requests,
snapshot?.share.accessPinPromptTtlMinutes, snapshot?.share.accessPinPromptTtlMinutes,
snapshot?.share.hasAccessPin, snapshot?.share.hasAccessPin,
snapshot?.conversation.sessionId, snapshot?.conversation.sessionId,
snapshot?.targetRequest.userText,
]); ]);
useEffect(() => { useEffect(() => {
if (!isRoomSettingsOpen) { if (!isRoomSettingsOpen) {
@@ -3542,6 +3552,7 @@ export function ChatSharePage() {
const nextCustomContextContent = editingRoomCustomContextContent.trim(); const nextCustomContextContent = editingRoomCustomContextContent.trim();
const shouldPersistRoomDefaultContextIds = !areStringListsEqual(normalizedDefaultContextIds, inheritedDefaultContextIds); const shouldPersistRoomDefaultContextIds = !areStringListsEqual(normalizedDefaultContextIds, inheritedDefaultContextIds);
const shouldPersistRoomCustomContext = Boolean(nextCustomContextTitle || nextCustomContextContent); const shouldPersistRoomCustomContext = Boolean(nextCustomContextTitle || nextCustomContextContent);
const normalizedRoomTitle = editingRoomTitle.trim();
const normalizedAccessPin = editingRoomAccessPin.trim(); const normalizedAccessPin = editingRoomAccessPin.trim();
const currentHasAccessPin = snapshot?.share.hasAccessPin === true; const currentHasAccessPin = snapshot?.share.hasAccessPin === true;
const currentAccessPinPromptTtlMinutes = resolveAccessPinPromptTtlMinutes(snapshot?.share.accessPinPromptTtlMinutes); const currentAccessPinPromptTtlMinutes = resolveAccessPinPromptTtlMinutes(snapshot?.share.accessPinPromptTtlMinutes);
@@ -3549,10 +3560,17 @@ export function ChatSharePage() {
canManageSharedRoomSettings canManageSharedRoomSettings
&& Boolean(nextChatType) && Boolean(nextChatType)
&& ( && (
currentSharedChatTypeId !== nextChatType.id normalizedRoomTitle !== (snapshot?.conversation.title?.trim() || '')
|| !(snapshot?.conversation.title?.trim())
|| currentSharedChatTypeId !== nextChatType.id
|| snapshot.conversation.notifyOffline !== editingRoomNotifyOffline || snapshot.conversation.notifyOffline !== editingRoomNotifyOffline
); );
if (canManageSharedRoomSettings && !normalizedRoomTitle) {
message.warning('채팅방 이름을 입력하세요.');
return;
}
if (canEditSharedRoomAccessPin && editingRoomUseAccessPin && !currentHasAccessPin && !normalizedAccessPin) { if (canEditSharedRoomAccessPin && editingRoomUseAccessPin && !currentHasAccessPin && !normalizedAccessPin) {
message.warning('비밀번호를 새로 켜려면 숫자 4자리를 입력하세요.'); message.warning('비밀번호를 새로 켜려면 숫자 4자리를 입력하세요.');
return; return;
@@ -3623,6 +3641,7 @@ export function ChatSharePage() {
accessPinPromptTtlMinutes: editingRoomUseAccessPin ? editingRoomAccessPinPromptTtlMinutes : null, accessPinPromptTtlMinutes: editingRoomUseAccessPin ? editingRoomAccessPinPromptTtlMinutes : null,
chatTypeId: shouldSaveConversationSettings ? nextChatType?.id ?? null : undefined, chatTypeId: shouldSaveConversationSettings ? nextChatType?.id ?? null : undefined,
chatTypeLabel: shouldSaveConversationSettings ? nextChatType?.name ?? null : undefined, chatTypeLabel: shouldSaveConversationSettings ? nextChatType?.name ?? null : undefined,
title: shouldSaveConversationSettings ? normalizedRoomTitle : undefined,
notifyOffline: shouldSaveConversationSettings ? editingRoomNotifyOffline : undefined, notifyOffline: shouldSaveConversationSettings ? editingRoomNotifyOffline : undefined,
}); });
@@ -3669,6 +3688,7 @@ export function ChatSharePage() {
canManageSharedRoomSettings, canManageSharedRoomSettings,
defaultContexts, defaultContexts,
editingRoomChatTypeId, editingRoomChatTypeId,
editingRoomTitle,
editingRoomAccessPin, editingRoomAccessPin,
editingRoomAccessPinPromptTtlMinutes, editingRoomAccessPinPromptTtlMinutes,
editingRoomCustomContextContent, editingRoomCustomContextContent,
@@ -5214,6 +5234,15 @@ export function ChatSharePage() {
return currentRequest ?? latestRequest ?? sortedRequests[0] ?? null; return currentRequest ?? latestRequest ?? sortedRequests[0] ?? null;
}, [currentRequest, displayedRequests, latestRequest, sortedRequests]); }, [currentRequest, displayedRequests, latestRequest, sortedRequests]);
const headerTitleText = useMemo(() => {
const savedConversationTitle = snapshot?.conversation.title?.trim() || '';
if (savedConversationTitle) {
return savedConversationTitle;
}
return headerInquiryRequest?.userText.trim() || '-';
}, [headerInquiryRequest?.userText, snapshot?.conversation.title]);
const hiddenBeforeCount = expandMode === 'latest' && latestRequestIndex >= 0 ? latestRequestIndex : 0; const hiddenBeforeCount = expandMode === 'latest' && latestRequestIndex >= 0 ? latestRequestIndex : 0;
const hiddenAfterCount = const hiddenAfterCount =
expandMode === 'latest' && latestRequestIndex >= 0 ? Math.max(0, sortedRequests.length - latestRequestIndex - 1) : 0; expandMode === 'latest' && latestRequestIndex >= 0 ? Math.max(0, sortedRequests.length - latestRequestIndex - 1) : 0;
@@ -6538,13 +6567,28 @@ export function ChatSharePage() {
<section className="chat-share-page__first-inquiry"> <section className="chat-share-page__first-inquiry">
<div className="chat-share-page__first-inquiry-head"> <div className="chat-share-page__first-inquiry-head">
<div className="chat-share-page__first-inquiry-copy"> <div className="chat-share-page__first-inquiry-copy">
<Title <div className="chat-share-page__first-inquiry-title-row">
level={5} <Title
className="chat-share-page__first-inquiry-title" level={5}
ellipsis={{ rows: 1, tooltip: headerInquiryRequest.userText.trim() || '-' }} className="chat-share-page__first-inquiry-title"
> ellipsis={{ rows: 1, tooltip: headerTitleText }}
{headerInquiryRequest.userText.trim() || '-'} >
</Title> {headerTitleText}
</Title>
{canOpenSharedRoomSettings ? (
<Button
type="text"
size="small"
className="chat-share-page__section-action chat-share-page__section-action--tool"
aria-label="채팅방 이름 및 설정 편집"
title="채팅방 이름 및 설정 편집"
icon={<EditOutlined />}
onClick={() => {
openSharedRoomSettings();
}}
/>
) : null}
</div>
</div> </div>
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
@@ -7023,6 +7067,19 @@ export function ChatSharePage() {
label: '채팅유형', label: '채팅유형',
children: ( children: (
<div className="chat-share-page__room-settings-panel"> <div className="chat-share-page__room-settings-panel">
<div className="chat-share-page__room-settings-panel-head">
<Text strong> </Text>
<Text type="secondary"> .</Text>
</div>
<Input
value={editingRoomTitle}
placeholder="예: 관리자 공유 채팅"
readOnly={!canManageSharedRoomSettings}
maxLength={200}
onChange={(event) => {
setEditingRoomTitle(event.target.value);
}}
/>
<div className="chat-share-page__room-settings-panel-head"> <div className="chat-share-page__room-settings-panel-head">
<Text strong> </Text> <Text strong> </Text>
<Text type="secondary"> .</Text> <Text type="secondary"> .</Text>

View File

@@ -159,6 +159,14 @@ type BootstrapInstallMetadataResult = {
themeColor: string; themeColor: string;
} | null; } | null;
type BootstrapInstallMetadataDefinition = {
description: string;
scope?: string;
shortName?: string;
themeColor: string;
title: string;
} | null;
const PLAY_APP_INSTALL_METADATA: Record<string, { title: string; themeColor: string }> = { const PLAY_APP_INSTALL_METADATA: Record<string, { title: string; themeColor: string }> = {
'baseball-ticket-bay': { title: 'Baseball Ticket Bay', themeColor: '#1b3f91' }, 'baseball-ticket-bay': { title: 'Baseball Ticket Bay', themeColor: '#1b3f91' },
'photoprism': { title: 'PhotoPrism', themeColor: '#0f766e' }, 'photoprism': { title: 'PhotoPrism', themeColor: '#0f766e' },
@@ -190,13 +198,8 @@ function createCurrentRouteInstallManifestObjectUrl(options: {
}); });
} }
export function applyBootstrapInstallMetadataForCurrentRoute(): BootstrapInstallMetadataResult { function resolveBootstrapInstallMetadataDefinition(pathname: string, search: string): BootstrapInstallMetadataDefinition {
if (typeof window === 'undefined') { const searchParams = new URLSearchParams(search);
return null;
}
const pathname = window.location.pathname;
const searchParams = new URLSearchParams(window.location.search);
if (pathname === '/play/apps') { if (pathname === '/play/apps') {
const appId = searchParams.get('app')?.trim() ?? ''; const appId = searchParams.get('app')?.trim() ?? '';
@@ -207,45 +210,52 @@ export function applyBootstrapInstallMetadataForCurrentRoute(): BootstrapInstall
} }
return { return {
manifestObjectUrl: createCurrentRouteInstallManifestObjectUrl({
title: metadata.title,
shortName: metadata.title,
description: `${metadata.title} 앱을 홈 화면에서 바로 엽니다.`,
themeColor: metadata.themeColor,
scope: pathname,
}),
title: metadata.title, title: metadata.title,
shortName: metadata.title,
description: `${metadata.title} 앱을 홈 화면에서 바로 엽니다.`,
themeColor: metadata.themeColor, themeColor: metadata.themeColor,
scope: pathname,
}; };
} }
if (pathname === '/plans/shared-resource') { if (pathname === '/plans/shared-resource') {
return { return {
manifestObjectUrl: createCurrentRouteInstallManifestObjectUrl({
title: '공유 리소스 관리',
shortName: '공유 리소스',
description: '공유 리소스 관리 화면을 홈 화면 앱으로 바로 엽니다.',
themeColor: '#0f766e',
scope: pathname,
}),
title: '공유 리소스 관리', title: '공유 리소스 관리',
shortName: '공유 리소스',
description: '공유 리소스 관리 화면을 홈 화면 앱으로 바로 엽니다.',
themeColor: '#0f766e', themeColor: '#0f766e',
scope: pathname,
}; };
} }
if (pathname.startsWith('/shares/')) { if (pathname.startsWith('/chat/share/') || pathname.startsWith('/shares/')) {
return { return {
manifestObjectUrl: createCurrentRouteInstallManifestObjectUrl({
title: '리소스 공유 채팅방',
shortName: '공유채팅',
description: '리소스 공유 채팅방을 홈 화면 앱으로 바로 엽니다.',
themeColor: '#165dff',
scope: pathname,
}),
title: '리소스 공유 채팅방', title: '리소스 공유 채팅방',
shortName: '공유채팅',
description: '리소스 공유 채팅방을 홈 화면 앱으로 바로 엽니다.',
themeColor: '#165dff', themeColor: '#165dff',
scope: pathname,
}; };
} }
return null; return null;
} }
export function applyBootstrapInstallMetadataForCurrentRoute(): BootstrapInstallMetadataResult {
if (typeof window === 'undefined') {
return null;
}
const pathname = window.location.pathname;
const metadata = resolveBootstrapInstallMetadataDefinition(pathname, window.location.search);
if (!metadata) {
return null;
}
return {
manifestObjectUrl: createCurrentRouteInstallManifestObjectUrl(metadata),
title: metadata.title,
themeColor: metadata.themeColor,
};
}