chore: test deploy snapshot
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
|||||||
hasPendingAttentionVerificationRequest,
|
hasPendingAttentionVerificationRequest,
|
||||||
} from '../services/chat-room-service.js';
|
} from '../services/chat-room-service.js';
|
||||||
import {
|
import {
|
||||||
|
archiveChatShareTokenRoomMap,
|
||||||
ensureDefaultChatShareTokenRoomMap,
|
ensureDefaultChatShareTokenRoomMap,
|
||||||
getChatShareTokenRoomMap,
|
getChatShareTokenRoomMap,
|
||||||
resolveChatShareTokenRoomSessionIds,
|
resolveChatShareTokenRoomSessionIds,
|
||||||
@@ -1142,12 +1143,13 @@ async function buildChatShareSnapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const childRequestIdsByParentRequestId = buildChildRequestIdsByParentRequestId(requests);
|
const childRequestIdsByParentRequestId = buildChildRequestIdsByParentRequestId(requests);
|
||||||
const isManagedShareRoomPlaceholder =
|
const isManagedShareRoomSession =
|
||||||
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX) && !targetRequestFromStore;
|
tokenPayload.kind === 'request-bundle' && normalizedSessionId.startsWith(MANAGED_CHAT_SHARE_SESSION_PREFIX);
|
||||||
|
const isManagedShareRoomPlaceholder = isManagedShareRoomSession && !targetRequestFromStore;
|
||||||
const rootRequestId = isManagedShareRoomPlaceholder
|
const rootRequestId = isManagedShareRoomPlaceholder
|
||||||
? targetRequestId
|
? targetRequestId
|
||||||
: resolveShareRootRequestId(targetRequestId, requestMap, tokenPayload.kind);
|
: resolveShareRootRequestId(targetRequestId, requestMap, tokenPayload.kind);
|
||||||
const scopeRequestIds = isManagedShareRoomPlaceholder
|
const scopeRequestIds = isManagedShareRoomSession
|
||||||
? requests.map((request) => request.requestId.trim()).filter(Boolean)
|
? requests.map((request) => request.requestId.trim()).filter(Boolean)
|
||||||
: collectShareScopeRequestIds(rootRequestId, childRequestIdsByParentRequestId);
|
: collectShareScopeRequestIds(rootRequestId, childRequestIdsByParentRequestId);
|
||||||
const scopeRequestIdSet = new Set(scopeRequestIds);
|
const scopeRequestIdSet = new Set(scopeRequestIds);
|
||||||
@@ -2524,6 +2526,101 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.delete(`${CHAT_SHARE_ROUTE_PREFIX}/:token/rooms/:sessionId`, async (request, reply) => {
|
||||||
|
const params = z.object({
|
||||||
|
token: z.string().trim().min(1).max(16000),
|
||||||
|
sessionId: z.string().trim().min(1).max(120),
|
||||||
|
}).parse(request.params ?? {});
|
||||||
|
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||||
|
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||||
|
|
||||||
|
if (!tokenPayload) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 링크가 유효하지 않습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenPayload.kind === 'prompt') {
|
||||||
|
return reply.code(400).send({
|
||||||
|
message: 'prompt 공유 링크에서는 채팅방을 삭제할 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const unavailableMessage = resolveManagedShareUnavailableMessage(managedContext.managedResource);
|
||||||
|
|
||||||
|
if (unavailableMessage) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: unavailableMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await ensureManagedShareAccessPin(request, reply, managedContext.sharePath))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!managedContext.managedResource || managedContext.managedResource.token.resourceType !== 'chat-share') {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '공유 채팅방 정보를 찾을 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!managedContext.managedResource.token.permissions.includes('manage')) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에는 채팅방을 삭제할 권한이 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasManagedShareAllowedApp(managedContext.managedResource, 'chat-room-settings')) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에는 채팅방 설정 앱 권한이 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRoomContext = await resolveActiveManagedShareRoom({
|
||||||
|
managedResource: managedContext.managedResource,
|
||||||
|
tokenPayload,
|
||||||
|
requestedSessionId: params.sessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resolvedRoomContext.requestedRoomMatched || !resolvedRoomContext.activeRoom) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
message: '이 공유 링크에서 접근할 수 없는 채팅방입니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedRoomContext.rooms.length <= 1) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
message: '마지막 채팅방은 삭제할 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveChatService()?.forgetSession(params.sessionId);
|
||||||
|
chatRuntimeService.clearSession(params.sessionId);
|
||||||
|
const deleted = await deleteChatConversation(params.sessionId);
|
||||||
|
const archived = await archiveChatShareTokenRoomMap(managedContext.managedResource.token.id, params.sessionId);
|
||||||
|
|
||||||
|
if (!deleted || !archived.archived) {
|
||||||
|
return reply.code(404).send({
|
||||||
|
message: '삭제할 채팅방을 찾을 수 없습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managedContext.managedResource) {
|
||||||
|
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
||||||
|
actorLabel: 'share-manager',
|
||||||
|
summary: '공유 채팅방을 삭제했습니다.',
|
||||||
|
detail: params.sessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
deleted: true,
|
||||||
|
deletedSessionId: params.sessionId,
|
||||||
|
nextRoomSessionId: archived.nextDefaultRoom?.sessionId ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/messages`, async (request, reply) => {
|
app.post(`${CHAT_SHARE_ROUTE_PREFIX}/:token/messages`, async (request, reply) => {
|
||||||
const params = z.object({
|
const params = z.object({
|
||||||
token: z.string().trim().min(1).max(16000),
|
token: z.string().trim().min(1).max(16000),
|
||||||
|
|||||||
@@ -453,6 +453,63 @@ test('buildAgenticCodexPrompt keeps the chat type label provided by the client c
|
|||||||
assert.doesNotMatch(prompt, /- label: 코드 수정/);
|
assert.doesNotMatch(prompt, /- label: 코드 수정/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildAgenticCodexPrompt pins the explicitly referenced answer ahead of recent history', () => {
|
||||||
|
const prompt = buildAgenticCodexPrompt(
|
||||||
|
{
|
||||||
|
pageId: null,
|
||||||
|
pageTitle: 'Codex Live',
|
||||||
|
topMenu: 'chat',
|
||||||
|
focusedComponentId: null,
|
||||||
|
pageUrl: 'https://preview.sm-home.cloud/chat/live',
|
||||||
|
chatTypeLabel: '일반 요청',
|
||||||
|
chatTypeDescription: '일반 요청 설명',
|
||||||
|
},
|
||||||
|
'지금 무슨 답변을 참조했나요?',
|
||||||
|
'session-reference',
|
||||||
|
{
|
||||||
|
recentHistoryLines: [
|
||||||
|
'[user] 예전 질문',
|
||||||
|
'[codex] 다른 답변',
|
||||||
|
],
|
||||||
|
referencedRequest: {
|
||||||
|
sessionId: 'session-reference',
|
||||||
|
requestId: 'request-123',
|
||||||
|
requesterClientId: null,
|
||||||
|
chatTypeId: 'general-request',
|
||||||
|
chatTypeLabel: '일반 요청',
|
||||||
|
requestOrigin: 'composer',
|
||||||
|
sharedResourceTokenId: null,
|
||||||
|
parentRequestId: null,
|
||||||
|
promptContextRef: null,
|
||||||
|
status: 'completed',
|
||||||
|
statusMessage: '요청 처리 완료',
|
||||||
|
retryCount: 0,
|
||||||
|
userMessageId: 1,
|
||||||
|
userText: 'preview서버에서 test서버 채팅이 안열리는데 test서버 배포 해야돼?',
|
||||||
|
responseMessageId: 2,
|
||||||
|
responseText: '배포 누락이 아니라 iframe 차단입니다.',
|
||||||
|
usageSnapshot: null,
|
||||||
|
totalTokens: null,
|
||||||
|
hasResponse: true,
|
||||||
|
canDelete: false,
|
||||||
|
manualPromptCompletedAt: null,
|
||||||
|
manualVerificationCompletedAt: null,
|
||||||
|
createdAt: '2026-05-27T14:50:23.000Z',
|
||||||
|
updatedAt: '2026-05-27T14:51:00.000Z',
|
||||||
|
answeredAt: '2026-05-27T14:51:00.000Z',
|
||||||
|
terminalAt: '2026-05-27T14:51:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(prompt, /## 답변 참조/);
|
||||||
|
assert.match(prompt, /참조 requestId: request-123/);
|
||||||
|
assert.match(prompt, /참조 사용자 요청: preview서버에서 test서버 채팅이 안열리는데 test서버 배포 해야돼\?/);
|
||||||
|
assert.match(prompt, /참조 답변 본문: 배포 누락이 아니라 iframe 차단입니다\./);
|
||||||
|
assert.match(prompt, /다른 최근 답변을 임의로 섞지 마세요\./);
|
||||||
|
assert.ok(prompt.indexOf('## 답변 참조') < prompt.indexOf('최근 대화 문맥(보조 참조)'));
|
||||||
|
});
|
||||||
|
|
||||||
test('resolveCodexParticipantsForExecution expands moderator into opening and closing turns', () => {
|
test('resolveCodexParticipantsForExecution expands moderator into opening and closing turns', () => {
|
||||||
const participants = resolveCodexParticipantsForExecution({
|
const participants = resolveCodexParticipantsForExecution({
|
||||||
pageId: null,
|
pageId: null,
|
||||||
|
|||||||
@@ -2804,6 +2804,30 @@ function buildChatSessionReferenceInstructionBlock(referenceContent?: string | n
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildReferencedRequestInstructionBlock(referencedRequest?: ChatConversationRequestItem | null) {
|
||||||
|
if (!referencedRequest) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = referencedRequest.requestId.trim();
|
||||||
|
const referencedQuestion = normalizeChatPromptHistoryText(referencedRequest.userText || '');
|
||||||
|
const referencedAnswer = normalizeChatPromptHistoryText(
|
||||||
|
referencedRequest.responseText || referencedRequest.statusMessage || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'## 답변 참조',
|
||||||
|
'- 현재 요청은 아래 이전 답변을 명시적으로 선택해 이어가는 후속 요청입니다.',
|
||||||
|
'- 최근 대화 일반 문맥보다 아래 참조 답변을 우선해서 해석하세요.',
|
||||||
|
requestId ? `- 참조 requestId: ${requestId}` : null,
|
||||||
|
referencedQuestion ? `- 참조 사용자 요청: ${normalizePromptContextText(referencedQuestion, 1200)}` : null,
|
||||||
|
referencedAnswer
|
||||||
|
? `- 참조 답변 본문: ${normalizePromptContextText(referencedAnswer, 4000)}`
|
||||||
|
: '- 참조 답변 본문: 아직 저장된 답변 본문이 없어 상위 요청만 참조합니다.',
|
||||||
|
'- 사용자가 "방금 어떤 답변을 참조했냐"처럼 물으면 위 requestId와 참조 본문을 기준으로 답하고, 다른 최근 답변을 임의로 섞지 마세요.',
|
||||||
|
].filter((line): line is string => Boolean(line));
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAgenticCodexPrompt(
|
export function buildAgenticCodexPrompt(
|
||||||
context: ChatContext | null,
|
context: ChatContext | null,
|
||||||
input: string,
|
input: string,
|
||||||
@@ -2814,6 +2838,7 @@ export function buildAgenticCodexPrompt(
|
|||||||
omittedHistoryCount?: number;
|
omittedHistoryCount?: number;
|
||||||
sessionReferenceResourcePath?: string;
|
sessionReferenceResourcePath?: string;
|
||||||
sessionReferenceContent?: string;
|
sessionReferenceContent?: string;
|
||||||
|
referencedRequest?: ChatConversationRequestItem | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const repoPath = promptContext?.repoPath?.trim() || resolveMainProjectRoot();
|
const repoPath = promptContext?.repoPath?.trim() || resolveMainProjectRoot();
|
||||||
@@ -2847,6 +2872,7 @@ export function buildAgenticCodexPrompt(
|
|||||||
...buildChatSessionReferenceInstructionBlock(promptContext?.sessionReferenceContent),
|
...buildChatSessionReferenceInstructionBlock(promptContext?.sessionReferenceContent),
|
||||||
'',
|
'',
|
||||||
...buildChatTypeInstructionBlock(context),
|
...buildChatTypeInstructionBlock(context),
|
||||||
|
...(promptContext?.referencedRequest ? ['', ...buildReferencedRequestInstructionBlock(promptContext.referencedRequest)] : []),
|
||||||
'',
|
'',
|
||||||
'응답 규칙:',
|
'응답 규칙:',
|
||||||
'- 사실성보다 추측을 우선하지 마세요. 오늘/최신/건수 질문은 직접 확인하세요.',
|
'- 사실성보다 추측을 우선하지 마세요. 오늘/최신/건수 질문은 직접 확인하세요.',
|
||||||
@@ -3024,6 +3050,7 @@ async function runAgenticCodexReply(
|
|||||||
options?: {
|
options?: {
|
||||||
omitPromptHistory?: boolean;
|
omitPromptHistory?: boolean;
|
||||||
requestedAt?: Date | null;
|
requestedAt?: Date | null;
|
||||||
|
parentRequestId?: string | null;
|
||||||
},
|
},
|
||||||
onProgress?: (text: string) => void,
|
onProgress?: (text: string) => void,
|
||||||
onActivity?: (line: string) => void,
|
onActivity?: (line: string) => void,
|
||||||
@@ -3032,6 +3059,9 @@ async function runAgenticCodexReply(
|
|||||||
const repoPath = resolveMainProjectRoot();
|
const repoPath = resolveMainProjectRoot();
|
||||||
await validateAgenticCodexRuntime(repoPath, env.PLAN_CODEX_BIN);
|
await validateAgenticCodexRuntime(repoPath, env.PLAN_CODEX_BIN);
|
||||||
const resolvedContext = await resolveCodexLiveChatContext(context, sessionId);
|
const resolvedContext = await resolveCodexLiveChatContext(context, sessionId);
|
||||||
|
const referencedRequest = options?.parentRequestId?.trim()
|
||||||
|
? await getChatConversationRequest(sessionId, options.parentRequestId.trim())
|
||||||
|
: null;
|
||||||
const appConfig = await getAppConfigSnapshot();
|
const appConfig = await getAppConfigSnapshot();
|
||||||
const recentHistory = await buildRecentChatPromptHistory(sessionId, requestId, {
|
const recentHistory = await buildRecentChatPromptHistory(sessionId, requestId, {
|
||||||
maxMessages: appConfig.chat?.maxContextMessages,
|
maxMessages: appConfig.chat?.maxContextMessages,
|
||||||
@@ -3090,6 +3120,7 @@ async function runAgenticCodexReply(
|
|||||||
omittedHistoryCount: recentHistory.omittedCount,
|
omittedHistoryCount: recentHistory.omittedCount,
|
||||||
sessionReferenceResourcePath,
|
sessionReferenceResourcePath,
|
||||||
sessionReferenceContent,
|
sessionReferenceContent,
|
||||||
|
referencedRequest,
|
||||||
});
|
});
|
||||||
let streamedOutput = '';
|
let streamedOutput = '';
|
||||||
let stdoutTail = '';
|
let stdoutTail = '';
|
||||||
@@ -3840,6 +3871,7 @@ async function buildCodexReply(
|
|||||||
options?: {
|
options?: {
|
||||||
omitPromptHistory?: boolean;
|
omitPromptHistory?: boolean;
|
||||||
requestedAt?: Date | null;
|
requestedAt?: Date | null;
|
||||||
|
parentRequestId?: string | null;
|
||||||
},
|
},
|
||||||
onProgress?: (text: string) => void,
|
onProgress?: (text: string) => void,
|
||||||
onActivity?: (line: string) => void,
|
onActivity?: (line: string) => void,
|
||||||
@@ -6143,6 +6175,7 @@ export class ChatService {
|
|||||||
{
|
{
|
||||||
omitPromptHistory: request.omitPromptHistory === true,
|
omitPromptHistory: request.omitPromptHistory === true,
|
||||||
requestedAt: new Date(request.requestedAtMs),
|
requestedAt: new Date(request.requestedAtMs),
|
||||||
|
parentRequestId: request.parentRequestId,
|
||||||
},
|
},
|
||||||
(partialReply) => {
|
(partialReply) => {
|
||||||
stopProgressTimer();
|
stopProgressTimer();
|
||||||
|
|||||||
@@ -310,3 +310,120 @@ export async function resolveChatShareTokenRoomSessionIds(tokenId: string) {
|
|||||||
const rooms = await listChatShareTokenRoomMaps(tokenId);
|
const rooms = await listChatShareTokenRoomMaps(tokenId);
|
||||||
return rooms.map((item) => item.sessionId).filter(Boolean);
|
return rooms.map((item) => item.sessionId).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function archiveChatShareTokenRoomMap(tokenId: string, sessionId: string) {
|
||||||
|
const normalizedTokenId = tokenId.trim();
|
||||||
|
const normalizedSessionId = sessionId.trim();
|
||||||
|
|
||||||
|
if (!normalizedTokenId || !normalizedSessionId) {
|
||||||
|
return {
|
||||||
|
archived: false,
|
||||||
|
archivedRoom: null,
|
||||||
|
nextDefaultRoom: null,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureChatShareTokenRoomMapTable();
|
||||||
|
|
||||||
|
return db.transaction(async (trx) => {
|
||||||
|
const current = await trx(`${CHAT_SHARE_TOKEN_ROOM_MAP_TABLE} as room_map`)
|
||||||
|
.leftJoin(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'room_map.session_id')
|
||||||
|
.select(
|
||||||
|
'room_map.shared_resource_token_id',
|
||||||
|
'room_map.session_id',
|
||||||
|
'room_map.root_request_id',
|
||||||
|
'room_map.is_default',
|
||||||
|
'room_map.sort_order',
|
||||||
|
'room_map.created_by_client_id',
|
||||||
|
'room_map.created_at',
|
||||||
|
'room_map.updated_at',
|
||||||
|
'conversation.title',
|
||||||
|
'conversation.request_badge_label',
|
||||||
|
'conversation.chat_type_id',
|
||||||
|
'conversation.last_chat_type_id',
|
||||||
|
'conversation.context_label',
|
||||||
|
'conversation.context_description',
|
||||||
|
'conversation.notify_offline',
|
||||||
|
'conversation.updated_at as conversation_updated_at',
|
||||||
|
)
|
||||||
|
.where({
|
||||||
|
'room_map.shared_resource_token_id': normalizedTokenId,
|
||||||
|
'room_map.session_id': normalizedSessionId,
|
||||||
|
})
|
||||||
|
.whereNull('room_map.archived_at')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return {
|
||||||
|
archived: false,
|
||||||
|
archivedRoom: null,
|
||||||
|
nextDefaultRoom: null,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({
|
||||||
|
shared_resource_token_id: normalizedTokenId,
|
||||||
|
session_id: normalizedSessionId,
|
||||||
|
})
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.update({
|
||||||
|
archived_at: db.fn.now(),
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let nextDefaultRoom: ChatShareTokenRoomMapItem | null = null;
|
||||||
|
|
||||||
|
if (current.is_default) {
|
||||||
|
const nextDefaultRow = await trx(`${CHAT_SHARE_TOKEN_ROOM_MAP_TABLE} as room_map`)
|
||||||
|
.leftJoin(`${CHAT_CONVERSATION_TABLE} as conversation`, 'conversation.session_id', 'room_map.session_id')
|
||||||
|
.select(
|
||||||
|
'room_map.shared_resource_token_id',
|
||||||
|
'room_map.session_id',
|
||||||
|
'room_map.root_request_id',
|
||||||
|
'room_map.is_default',
|
||||||
|
'room_map.sort_order',
|
||||||
|
'room_map.created_by_client_id',
|
||||||
|
'room_map.created_at',
|
||||||
|
'room_map.updated_at',
|
||||||
|
'conversation.title',
|
||||||
|
'conversation.request_badge_label',
|
||||||
|
'conversation.chat_type_id',
|
||||||
|
'conversation.last_chat_type_id',
|
||||||
|
'conversation.context_label',
|
||||||
|
'conversation.context_description',
|
||||||
|
'conversation.notify_offline',
|
||||||
|
'conversation.updated_at as conversation_updated_at',
|
||||||
|
)
|
||||||
|
.where({ 'room_map.shared_resource_token_id': normalizedTokenId })
|
||||||
|
.whereNull('room_map.archived_at')
|
||||||
|
.orderBy('room_map.sort_order', 'asc')
|
||||||
|
.orderBy('room_map.created_at', 'asc')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (nextDefaultRow) {
|
||||||
|
await trx(CHAT_SHARE_TOKEN_ROOM_MAP_TABLE)
|
||||||
|
.where({
|
||||||
|
shared_resource_token_id: normalizedTokenId,
|
||||||
|
session_id: nextDefaultRow.session_id,
|
||||||
|
})
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.update({
|
||||||
|
is_default: true,
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
nextDefaultRoom = mapChatShareTokenRoomRow({
|
||||||
|
...nextDefaultRow,
|
||||||
|
is_default: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
archived: true,
|
||||||
|
archivedRoom: mapChatShareTokenRoomRow(current),
|
||||||
|
nextDefaultRoom,
|
||||||
|
} as const;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -2371,6 +2371,29 @@ export async function createChatShareRoom(
|
|||||||
} satisfies ChatShareRoomSummary;
|
} satisfies ChatShareRoomSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteChatShareRoom(token: string, sessionId: string) {
|
||||||
|
const response = await requestChatApi<{
|
||||||
|
ok: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
deletedSessionId: string;
|
||||||
|
nextRoomSessionId?: string | null;
|
||||||
|
}>(
|
||||||
|
`/shares/${encodeURIComponent(token)}/rooms/${encodeURIComponent(sessionId)}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
allowUnauthenticated: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleted: response.deleted === true,
|
||||||
|
deletedSessionId: normalizeRequiredText(response.deletedSessionId),
|
||||||
|
nextRoomSessionId: normalizeOptionalText(response.nextRoomSessionId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteChatConversationRequest(sessionId: string, requestId: string) {
|
export async function deleteChatConversationRequest(sessionId: string, requestId: string) {
|
||||||
const response = await requestChatApi<{ ok: boolean; deleted: boolean; sessionId: string; requestId: string }>(
|
const response = await requestChatApi<{ ok: boolean; deleted: boolean; sessionId: string; requestId: string }>(
|
||||||
`/conversations/${encodeURIComponent(sessionId)}/requests/${encodeURIComponent(requestId)}`,
|
`/conversations/${encodeURIComponent(sessionId)}/requests/${encodeURIComponent(requestId)}`,
|
||||||
|
|||||||
@@ -211,21 +211,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-list-panel {
|
.chat-share-page__room-list-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: rgba(248, 250, 252, 0.94);
|
background: rgba(248, 250, 252, 0.94);
|
||||||
box-shadow: inset 0 0 0 1px rgba(219, 226, 236, 0.82);
|
box-shadow: inset 0 0 0 1px rgba(219, 226, 236, 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-filter-input.ant-input-affix-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-color: rgba(191, 204, 220, 0.9);
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(241, 245, 249, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__room-list {
|
.chat-share-page__room-list {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-swipe {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 14px;
|
||||||
|
isolation: isolate;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-delete-action {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 96px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, #ef4444 0%, #dc2626 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-swipe.is-swiped .chat-share-page__room-delete-action,
|
||||||
|
.chat-share-page__room-swipe.is-dragging .chat-share-page__room-delete-action {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-swipe.is-delete-locked .chat-share-page__room-delete-action {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-card {
|
.chat-share-page__room-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -235,6 +289,11 @@
|
|||||||
0 6px 18px rgba(148, 163, 184, 0.08);
|
0 6px 18px rgba(148, 163, 184, 0.08);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s ease, background 0.2s ease, transform 0.16s ease;
|
||||||
|
touch-action: pan-y;
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-share-page__room-card--active {
|
.chat-share-page__room-card--active {
|
||||||
@@ -244,6 +303,26 @@
|
|||||||
0 10px 24px rgba(59, 130, 246, 0.16);
|
0 10px 24px rgba(59, 130, 246, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-swipe.is-dragging .chat-share-page__room-card {
|
||||||
|
transition: box-shadow 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card--default {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, #f4fbff 0%, #e3f4ff 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(34, 211, 238, 0.22),
|
||||||
|
0 8px 20px rgba(14, 165, 233, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-card--default.chat-share-page__room-card--active {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, #dff7ff 0%, #d2f0ff 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(8, 145, 178, 0.34),
|
||||||
|
0 10px 24px rgba(14, 165, 233, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__room-card-head {
|
.chat-share-page__room-card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -265,6 +344,17 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__room-list-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 84px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(226, 232, 240, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__create-room-form {
|
.chat-share-page__create-room-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1515,6 +1605,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.chat-share-page__room-list-panel {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__program-app-shell--system-chat-room {
|
.chat-share-page__program-app-shell--system-chat-room {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -2258,12 +2352,38 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__previous-question-modal-dialog {
|
||||||
|
top: max(72px, calc(env(safe-area-inset-top, 0px) + 56px));
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__previous-question-modal-dialog .ant-modal-content {
|
||||||
|
max-height: min(
|
||||||
|
calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 96px),
|
||||||
|
720px
|
||||||
|
);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-share-page__previous-question-modal-dialog .ant-modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__previous-question-modal-section {
|
.chat-share-page__previous-question-modal-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-share-page__previous-question-modal-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-share-page__previous-question-modal-section + .chat-share-page__previous-question-modal-section {
|
.chat-share-page__previous-question-modal-section + .chat-share-page__previous-question-modal-section {
|
||||||
border-top: 1px solid #e5eaf1;
|
border-top: 1px solid #e5eaf1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
clearChatShareConversationRoom,
|
clearChatShareConversationRoom,
|
||||||
completeChatShareManualBadge,
|
completeChatShareManualBadge,
|
||||||
createChatShareRoom,
|
createChatShareRoom,
|
||||||
|
deleteChatShareRoom,
|
||||||
fetchChatShareRuntimeSnapshot,
|
fetchChatShareRuntimeSnapshot,
|
||||||
fetchChatShareSnapshot,
|
fetchChatShareSnapshot,
|
||||||
getStoredChatShareAccessPin,
|
getStoredChatShareAccessPin,
|
||||||
@@ -97,6 +98,12 @@ const SHARE_TOKEN_USAGE_CLOCK_INTERVAL_MS = 1000;
|
|||||||
const SHARE_EXPIRY_CLOCK_INTERVAL_MS = 60 * 1000;
|
const SHARE_EXPIRY_CLOCK_INTERVAL_MS = 60 * 1000;
|
||||||
const SHARE_IMMEDIATE_SEND_PINNED_STORAGE_KEY = 'codex-live-share-immediate-send-pinned-by-token';
|
const SHARE_IMMEDIATE_SEND_PINNED_STORAGE_KEY = 'codex-live-share-immediate-send-pinned-by-token';
|
||||||
const SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS = 1000;
|
const SHARE_IMMEDIATE_SEND_TOGGLE_HOLD_MS = 1000;
|
||||||
|
const SHARE_ROOM_SWIPE_DELETE_LIMIT_PX = 108;
|
||||||
|
const SHARE_ROOM_SWIPE_DELETE_THRESHOLD_PX = 72;
|
||||||
|
const SHARE_ROOM_TOUCH_TAP_SLOP_PX = 14;
|
||||||
|
const SHARE_ROOM_TOUCH_SCROLL_CANCEL_PX = 18;
|
||||||
|
const SHARE_ROOM_SWIPE_ACTIVATION_PX = 18;
|
||||||
|
const SHARE_ROOM_SWIPE_START_EDGE_PX = 56;
|
||||||
const SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS = [
|
const SHARE_ACCESS_PIN_PROMPT_TTL_OPTIONS = [
|
||||||
{ value: 'always', label: '매번 묻기', minutes: 0 },
|
{ value: 'always', label: '매번 묻기', minutes: 0 },
|
||||||
{ value: '5', label: '5분 유지', minutes: 5 },
|
{ value: '5', label: '5분 유지', minutes: 5 },
|
||||||
@@ -249,6 +256,14 @@ function parseAccessPinPromptTtlOptionValue(value: string) {
|
|||||||
return matchedOption?.minutes ?? null;
|
return matchedOption?.minutes ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canDeleteShareRoom(room: ChatShareRoomSummary, rooms: ChatShareRoomSummary[]) {
|
||||||
|
if (room.isDefault) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rooms.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
function readStoredShareImmediateSendPinnedByToken() {
|
function readStoredShareImmediateSendPinnedByToken() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {} as Record<string, boolean>;
|
return {} as Record<string, boolean>;
|
||||||
@@ -1551,6 +1566,18 @@ function buildRequestAnswerText(
|
|||||||
return stripHiddenPreviewTags(String(request.responseText ?? '').replace(DIFF_CODE_BLOCK_PATTERN, '')).trim();
|
return stripHiddenPreviewTags(String(request.responseText ?? '').replace(DIFF_CODE_BLOCK_PATTERN, '')).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveShareRequestFallbackAnswerText(request: ChatConversationRequest) {
|
||||||
|
if (request.status === 'queued') {
|
||||||
|
return '요청 대기 등록 하였습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.status === 'accepted' || request.status === 'started') {
|
||||||
|
return '요청 처리 중 입니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.statusMessage?.trim() || '아직 답변이 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
function replaceChatShareSnapshotRequest(snapshot: ChatShareSnapshot, nextRequest: ChatConversationRequest): ChatShareSnapshot {
|
function replaceChatShareSnapshotRequest(snapshot: ChatShareSnapshot, nextRequest: ChatConversationRequest): ChatShareSnapshot {
|
||||||
return {
|
return {
|
||||||
...snapshot,
|
...snapshot,
|
||||||
@@ -1580,6 +1607,41 @@ function resolveShareConversationParentRequest(
|
|||||||
return parentRequestId ? requestById.get(parentRequestId) ?? null : null;
|
return parentRequestId ? requestById.get(parentRequestId) ?? null : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveShareRequestLineage(
|
||||||
|
request: ChatConversationRequest | null | undefined,
|
||||||
|
requestById: ReadonlyMap<string, ChatConversationRequest>,
|
||||||
|
) {
|
||||||
|
const directParentRequest = resolveShareConversationParentRequest(request, requestById);
|
||||||
|
|
||||||
|
if (!directParentRequest) {
|
||||||
|
return {
|
||||||
|
directParentRequest: null,
|
||||||
|
topParentRequest: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentRequest: ChatConversationRequest | null = directParentRequest;
|
||||||
|
let topParentRequest: ChatConversationRequest | null = directParentRequest;
|
||||||
|
const visitedRequestIds = new Set<string>();
|
||||||
|
|
||||||
|
while (currentRequest) {
|
||||||
|
const currentRequestId = currentRequest.requestId.trim();
|
||||||
|
|
||||||
|
if (!currentRequestId || visitedRequestIds.has(currentRequestId)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitedRequestIds.add(currentRequestId);
|
||||||
|
topParentRequest = currentRequest;
|
||||||
|
currentRequest = resolveShareConversationParentRequest(currentRequest, requestById);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
directParentRequest,
|
||||||
|
topParentRequest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildSharePreviewItemsFromText(text: string, shareToken: string) {
|
function buildSharePreviewItemsFromText(text: string, shareToken: string) {
|
||||||
if (!shareToken) {
|
if (!shareToken) {
|
||||||
return [];
|
return [];
|
||||||
@@ -2936,7 +2998,11 @@ function ShareRequestCard({
|
|||||||
() => buildSharePreviewItemsFromText(request.userText, shareToken),
|
() => buildSharePreviewItemsFromText(request.userText, shareToken),
|
||||||
[request.userText, shareToken],
|
[request.userText, shareToken],
|
||||||
);
|
);
|
||||||
const resolvedAnswerText = answerText.trim() || request.statusMessage?.trim() || '아직 답변이 없습니다.';
|
const { directParentRequest, topParentRequest } = useMemo(
|
||||||
|
() => resolveShareRequestLineage(request, requestById),
|
||||||
|
[request, requestById],
|
||||||
|
);
|
||||||
|
const resolvedAnswerText = answerText.trim() || resolveShareRequestFallbackAnswerText(request);
|
||||||
const shouldRenderQuestion = mode !== 'answer-only';
|
const shouldRenderQuestion = mode !== 'answer-only';
|
||||||
const shouldRenderFullAnswer = mode === 'full' || mode === 'answer-only';
|
const shouldRenderFullAnswer = mode === 'full' || mode === 'answer-only';
|
||||||
const isRequestStillRunning = isRequestInFlight(request.status);
|
const isRequestStillRunning = isRequestInFlight(request.status);
|
||||||
@@ -2975,8 +3041,22 @@ function ShareRequestCard({
|
|||||||
&& !request.hasResponse
|
&& !request.hasResponse
|
||||||
&& request.status === 'queued';
|
&& request.status === 'queued';
|
||||||
const retryCount = Math.max(0, Number(request.retryCount ?? 0) || 0);
|
const retryCount = Math.max(0, Number(request.retryCount ?? 0) || 0);
|
||||||
|
const hasQuestionLineage = Boolean(directParentRequest || topParentRequest);
|
||||||
const questionActions = (
|
const questionActions = (
|
||||||
<>
|
<>
|
||||||
|
{hasQuestionLineage && onOpenPreviousQuestion ? (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="chat-share-page__message-action-button"
|
||||||
|
icon={<CommentOutlined />}
|
||||||
|
aria-label="부모 질의 보기"
|
||||||
|
title="부모 질의 보기"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenPreviousQuestion(request.requestId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{canCancelDisconnectedRequest ? (
|
{canCancelDisconnectedRequest ? (
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -3239,7 +3319,7 @@ export function ChatSharePage() {
|
|||||||
const [pendingVerificationCompletionRequestIds, setPendingVerificationCompletionRequestIds] = useState<string[]>([]);
|
const [pendingVerificationCompletionRequestIds, setPendingVerificationCompletionRequestIds] = useState<string[]>([]);
|
||||||
const [pendingRequestCancellationIds, setPendingRequestCancellationIds] = useState<string[]>([]);
|
const [pendingRequestCancellationIds, setPendingRequestCancellationIds] = useState<string[]>([]);
|
||||||
const [pendingRequestRetryIds, setPendingRequestRetryIds] = useState<string[]>([]);
|
const [pendingRequestRetryIds, setPendingRequestRetryIds] = useState<string[]>([]);
|
||||||
const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(true);
|
const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(false);
|
||||||
const [isRoomSwitching, setIsRoomSwitching] = useState(false);
|
const [isRoomSwitching, setIsRoomSwitching] = useState(false);
|
||||||
const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
|
const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
|
||||||
const [previousQuestionModalRequestId, setPreviousQuestionModalRequestId] = useState('');
|
const [previousQuestionModalRequestId, setPreviousQuestionModalRequestId] = useState('');
|
||||||
@@ -3283,6 +3363,12 @@ export function ChatSharePage() {
|
|||||||
const [optimisticShareRooms, setOptimisticShareRooms] = useState<ChatShareRoomSummary[]>([]);
|
const [optimisticShareRooms, setOptimisticShareRooms] = useState<ChatShareRoomSummary[]>([]);
|
||||||
const [isRefreshingRoomNotificationStatus, setIsRefreshingRoomNotificationStatus] = useState(false);
|
const [isRefreshingRoomNotificationStatus, setIsRefreshingRoomNotificationStatus] = useState(false);
|
||||||
const [isSendingRoomNotificationTest, setIsSendingRoomNotificationTest] = useState(false);
|
const [isSendingRoomNotificationTest, setIsSendingRoomNotificationTest] = useState(false);
|
||||||
|
const [isDeletingRoom, setIsDeletingRoom] = useState(false);
|
||||||
|
const [pendingDeleteRoomSessionId, setPendingDeleteRoomSessionId] = useState('');
|
||||||
|
const [swipedRoomSessionId, setSwipedRoomSessionId] = useState('');
|
||||||
|
const [draggingRoomSessionId, setDraggingRoomSessionId] = useState('');
|
||||||
|
const [draggingRoomOffsetX, setDraggingRoomOffsetX] = useState(0);
|
||||||
|
const [shareRoomFilterKeyword, setShareRoomFilterKeyword] = useState('');
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
const [searchPanelMode, setSearchPanelMode] = useState<ShareSearchPanelMode>('all');
|
const [searchPanelMode, setSearchPanelMode] = useState<ShareSearchPanelMode>('all');
|
||||||
const [selectedAppEnvironment, setSelectedAppEnvironment] = useState<ShareAppEnvironment>(() =>
|
const [selectedAppEnvironment, setSelectedAppEnvironment] = useState<ShareAppEnvironment>(() =>
|
||||||
@@ -3291,6 +3377,7 @@ export function ChatSharePage() {
|
|||||||
const [programTarget, setProgramTarget] = useState<ShareProgramTarget | null>(null);
|
const [programTarget, setProgramTarget] = useState<ShareProgramTarget | null>(null);
|
||||||
const [programMinimizedTarget, setProgramMinimizedTarget] = useState<ShareProgramTarget | null>(null);
|
const [programMinimizedTarget, setProgramMinimizedTarget] = useState<ShareProgramTarget | null>(null);
|
||||||
const [isProgramMinimized, setIsProgramMinimized] = useState(false);
|
const [isProgramMinimized, setIsProgramMinimized] = useState(false);
|
||||||
|
const [programReloadKey, setProgramReloadKey] = useState(0);
|
||||||
const programMinimizedCardRef = useRef<HTMLDivElement | null>(null);
|
const programMinimizedCardRef = useRef<HTMLDivElement | null>(null);
|
||||||
const programMinimizedDragStateRef = useRef<{
|
const programMinimizedDragStateRef = useRef<{
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
@@ -3299,6 +3386,14 @@ export function ChatSharePage() {
|
|||||||
captureTarget: HTMLDivElement;
|
captureTarget: HTMLDivElement;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const programMinimizedMovedRef = useRef(false);
|
const programMinimizedMovedRef = useRef(false);
|
||||||
|
const roomSwipeStartXRef = useRef<number | null>(null);
|
||||||
|
const roomSwipeStartYRef = useRef<number | null>(null);
|
||||||
|
const roomSwipeLockedSessionIdRef = useRef('');
|
||||||
|
const roomSwipeStartEligibleRef = useRef(false);
|
||||||
|
const roomSwipeMovedRef = useRef(false);
|
||||||
|
const roomTouchScrollDetectedRef = useRef(false);
|
||||||
|
const suppressRoomClickRef = useRef(false);
|
||||||
|
const skipNextRoomClickSessionIdRef = useRef('');
|
||||||
const programMinimizedPositionRef = useRef(getDefaultProgramMinimizedPosition());
|
const programMinimizedPositionRef = useRef(getDefaultProgramMinimizedPosition());
|
||||||
const [programMinimizedPosition, setProgramMinimizedPosition] = useState(() => programMinimizedPositionRef.current);
|
const [programMinimizedPosition, setProgramMinimizedPosition] = useState(() => programMinimizedPositionRef.current);
|
||||||
const composerRef = useRef<TextAreaRef | null>(null);
|
const composerRef = useRef<TextAreaRef | null>(null);
|
||||||
@@ -3348,6 +3443,30 @@ export function ChatSharePage() {
|
|||||||
() => shareRooms.find((item) => item.sessionId === selectedShareRoomSessionId) ?? null,
|
() => shareRooms.find((item) => item.sessionId === selectedShareRoomSessionId) ?? null,
|
||||||
[selectedShareRoomSessionId, shareRooms],
|
[selectedShareRoomSessionId, shareRooms],
|
||||||
);
|
);
|
||||||
|
const filteredShareRooms = useMemo(() => {
|
||||||
|
const keyword = shareRoomFilterKeyword.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return shareRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shareRooms.filter((room) => {
|
||||||
|
const searchIndex = [
|
||||||
|
room.title,
|
||||||
|
room.contextLabel,
|
||||||
|
room.requestBadgeLabel,
|
||||||
|
room.sessionId,
|
||||||
|
]
|
||||||
|
.map((value) => value?.trim().toLowerCase() ?? '')
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return searchIndex.includes(keyword);
|
||||||
|
});
|
||||||
|
}, [shareRoomFilterKeyword, shareRooms]);
|
||||||
|
const pendingDeleteRoom = useMemo(
|
||||||
|
() => shareRooms.find((item) => item.sessionId === pendingDeleteRoomSessionId) ?? null,
|
||||||
|
[pendingDeleteRoomSessionId, shareRooms],
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (optimisticShareRooms.length === 0 || !snapshot?.rooms?.length) {
|
if (optimisticShareRooms.length === 0 || !snapshot?.rooms?.length) {
|
||||||
return;
|
return;
|
||||||
@@ -3831,6 +3950,111 @@ export function ChatSharePage() {
|
|||||||
setCreatingRoomSeedMessage('이 방에서 이어갈 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.');
|
setCreatingRoomSeedMessage('이 방에서 이어갈 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.');
|
||||||
setIsCreateRoomOpen(true);
|
setIsCreateRoomOpen(true);
|
||||||
}, [currentSharedChatTypeId, enabledChatTypes]);
|
}, [currentSharedChatTypeId, enabledChatTypes]);
|
||||||
|
const resetRoomSwipeState = useCallback(() => {
|
||||||
|
roomSwipeStartXRef.current = null;
|
||||||
|
roomSwipeStartYRef.current = null;
|
||||||
|
roomSwipeLockedSessionIdRef.current = '';
|
||||||
|
roomSwipeStartEligibleRef.current = false;
|
||||||
|
roomSwipeMovedRef.current = false;
|
||||||
|
roomTouchScrollDetectedRef.current = false;
|
||||||
|
setDraggingRoomSessionId('');
|
||||||
|
setDraggingRoomOffsetX(0);
|
||||||
|
}, []);
|
||||||
|
const handleRoomSwipeStart = useCallback((
|
||||||
|
sessionId: string,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number,
|
||||||
|
swipeStartAllowed: boolean,
|
||||||
|
) => {
|
||||||
|
const targetRoom = shareRooms.find((room) => room.sessionId === sessionId);
|
||||||
|
|
||||||
|
if (isDeletingRoom || !targetRoom || !canDeleteShareRoom(targetRoom, shareRooms)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
roomSwipeStartXRef.current = clientX;
|
||||||
|
roomSwipeStartYRef.current = clientY;
|
||||||
|
roomSwipeLockedSessionIdRef.current = sessionId;
|
||||||
|
roomSwipeStartEligibleRef.current = swipeStartAllowed;
|
||||||
|
roomSwipeMovedRef.current = false;
|
||||||
|
roomTouchScrollDetectedRef.current = false;
|
||||||
|
setDraggingRoomSessionId(sessionId);
|
||||||
|
setDraggingRoomOffsetX(0);
|
||||||
|
setSwipedRoomSessionId((current) => (current === sessionId ? sessionId : ''));
|
||||||
|
}, [isDeletingRoom, shareRooms]);
|
||||||
|
const handleRoomSwipeMove = useCallback((sessionId: string, clientX: number, clientY: number) => {
|
||||||
|
if (
|
||||||
|
roomSwipeLockedSessionIdRef.current !== sessionId
|
||||||
|
|| roomSwipeStartXRef.current == null
|
||||||
|
|| roomSwipeStartYRef.current == null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = clientX - roomSwipeStartXRef.current;
|
||||||
|
const deltaY = clientY - roomSwipeStartYRef.current;
|
||||||
|
const absDeltaX = Math.abs(deltaX);
|
||||||
|
const absDeltaY = Math.abs(deltaY);
|
||||||
|
|
||||||
|
if (absDeltaY <= SHARE_ROOM_TOUCH_TAP_SLOP_PX && absDeltaX <= SHARE_ROOM_TOUCH_TAP_SLOP_PX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absDeltaY >= SHARE_ROOM_TOUCH_SCROLL_CANCEL_PX && absDeltaY > absDeltaX) {
|
||||||
|
roomTouchScrollDetectedRef.current = true;
|
||||||
|
setSwipedRoomSessionId('');
|
||||||
|
setDraggingRoomSessionId('');
|
||||||
|
setDraggingRoomOffsetX(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absDeltaX < SHARE_ROOM_SWIPE_ACTIVATION_PX || absDeltaX <= absDeltaY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roomSwipeStartEligibleRef.current) {
|
||||||
|
setDraggingRoomSessionId(sessionId);
|
||||||
|
setDraggingRoomOffsetX(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX >= 0) {
|
||||||
|
setDraggingRoomSessionId(sessionId);
|
||||||
|
setDraggingRoomOffsetX(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextOffset = Math.max(deltaX, -SHARE_ROOM_SWIPE_DELETE_LIMIT_PX);
|
||||||
|
if (Math.abs(nextOffset) >= SHARE_ROOM_SWIPE_ACTIVATION_PX) {
|
||||||
|
roomSwipeMovedRef.current = true;
|
||||||
|
suppressRoomClickRef.current = true;
|
||||||
|
}
|
||||||
|
setDraggingRoomSessionId(sessionId);
|
||||||
|
setDraggingRoomOffsetX(nextOffset);
|
||||||
|
}, []);
|
||||||
|
const handleRoomSwipeEnd = useCallback((sessionId: string) => {
|
||||||
|
if (roomSwipeLockedSessionIdRef.current !== sessionId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomTouchScrollDetectedRef.current) {
|
||||||
|
resetRoomSwipeState();
|
||||||
|
setSwipedRoomSessionId('');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
suppressRoomClickRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRevealDelete = draggingRoomOffsetX <= -SHARE_ROOM_SWIPE_DELETE_THRESHOLD_PX;
|
||||||
|
const wasSwipeGesture = roomSwipeMovedRef.current;
|
||||||
|
resetRoomSwipeState();
|
||||||
|
setSwipedRoomSessionId(shouldRevealDelete ? sessionId : '');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
suppressRoomClickRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
return !wasSwipeGesture && !shouldRevealDelete;
|
||||||
|
}, [draggingRoomOffsetX, resetRoomSwipeState]);
|
||||||
const refreshShareRuntime = useCallback(async (options?: { silent?: boolean }) => {
|
const refreshShareRuntime = useCallback(async (options?: { silent?: boolean }) => {
|
||||||
if (!normalizedToken || !selectedShareRoomSessionId) {
|
if (!normalizedToken || !selectedShareRoomSessionId) {
|
||||||
setShareRuntimeSnapshot(null);
|
setShareRuntimeSnapshot(null);
|
||||||
@@ -3957,6 +4181,74 @@ export function ChatSharePage() {
|
|||||||
snapshotRefreshPromiseRef.current = refreshTask;
|
snapshotRefreshPromiseRef.current = refreshTask;
|
||||||
return refreshTask;
|
return refreshTask;
|
||||||
}, [normalizedToken]);
|
}, [normalizedToken]);
|
||||||
|
const handleDeleteShareRoom = useCallback(async (room: ChatShareRoomSummary) => {
|
||||||
|
if (!normalizedToken || !room.sessionId || isDeletingRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDeleteShareRoom(room, shareRooms)) {
|
||||||
|
message.warning(room.isDefault ? '기본 채팅방은 삭제할 수 없습니다.' : '마지막 채팅방은 삭제할 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeletingRoom(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await deleteChatShareRoom(normalizedToken, room.sessionId);
|
||||||
|
const fallbackSessionId =
|
||||||
|
result.nextRoomSessionId
|
||||||
|
|| shareRooms.find((item) => item.sessionId !== room.sessionId)?.sessionId
|
||||||
|
|| '';
|
||||||
|
|
||||||
|
setOptimisticShareRooms((current) => current.filter((item) => item.sessionId !== room.sessionId));
|
||||||
|
setSnapshot((current) => {
|
||||||
|
if (!current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRooms = current.rooms.filter((item) => item.sessionId !== room.sessionId);
|
||||||
|
const nextActiveSessionId =
|
||||||
|
current.activeSessionId === room.sessionId
|
||||||
|
? (result.nextRoomSessionId || nextRooms.find((item) => item.isDefault)?.sessionId || nextRooms[0]?.sessionId || '')
|
||||||
|
: current.activeSessionId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
rooms: nextRooms,
|
||||||
|
activeSessionId: nextActiveSessionId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setPendingDeleteRoomSessionId('');
|
||||||
|
setSwipedRoomSessionId('');
|
||||||
|
requestedRoomSessionIdRef.current =
|
||||||
|
requestedRoomSessionIdRef.current === room.sessionId ? fallbackSessionId : requestedRoomSessionIdRef.current;
|
||||||
|
setRequestedRoomSessionId((current) => (current === room.sessionId ? fallbackSessionId : current));
|
||||||
|
|
||||||
|
if (selectedShareRoomSessionId === room.sessionId) {
|
||||||
|
setDraftText('');
|
||||||
|
setComposerAttachments([]);
|
||||||
|
setReplyReferenceRequestId('');
|
||||||
|
setLatestRequestId('');
|
||||||
|
setExpandMode('pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSnapshot({ silent: true });
|
||||||
|
message.success(`"${room.title}" 채팅방을 삭제했습니다.`);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '공유 채팅방 삭제 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
resetRoomSwipeState();
|
||||||
|
setIsDeletingRoom(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isDeletingRoom,
|
||||||
|
message,
|
||||||
|
normalizedToken,
|
||||||
|
refreshSnapshot,
|
||||||
|
resetRoomSwipeState,
|
||||||
|
selectedShareRoomSessionId,
|
||||||
|
shareRooms,
|
||||||
|
]);
|
||||||
const handleCancelShareRuntimeRequest = useCallback((item: ChatRuntimeJobItem) => {
|
const handleCancelShareRuntimeRequest = useCallback((item: ChatRuntimeJobItem) => {
|
||||||
const actionLabel = item.status === 'queued' ? '대기 요청' : '실행 중 요청';
|
const actionLabel = item.status === 'queued' ? '대기 요청' : '실행 중 요청';
|
||||||
|
|
||||||
@@ -4448,6 +4740,10 @@ export function ChatSharePage() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleReloadProgram = useCallback(() => {
|
||||||
|
setProgramReloadKey((current) => current + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccessPinInput('');
|
setAccessPinInput('');
|
||||||
setAccessPinSubmitError('');
|
setAccessPinSubmitError('');
|
||||||
@@ -4743,6 +5039,16 @@ export function ChatSharePage() {
|
|||||||
|
|
||||||
setRequestedRoomSessionId('');
|
setRequestedRoomSessionId('');
|
||||||
}, [requestedRoomSessionId, shareRooms]);
|
}, [requestedRoomSessionId, shareRooms]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDeletingRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shareRooms.some((room) => room.sessionId === swipedRoomSessionId)) {
|
||||||
|
setSwipedRoomSessionId('');
|
||||||
|
resetRoomSwipeState();
|
||||||
|
}
|
||||||
|
}, [isDeletingRoom, resetRoomSwipeState, shareRooms, swipedRoomSessionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -5228,13 +5534,30 @@ export function ChatSharePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectShareRoom = useCallback((sessionId: string) => {
|
const handleSelectShareRoom = useCallback((sessionId: string, options?: { bypassSuppression?: boolean }) => {
|
||||||
const normalizedSessionId = sessionId.trim();
|
const normalizedSessionId = sessionId.trim();
|
||||||
|
const shouldBypassSuppression = options?.bypassSuppression === true;
|
||||||
|
|
||||||
|
if (!shouldBypassSuppression && skipNextRoomClickSessionIdRef.current === normalizedSessionId) {
|
||||||
|
skipNextRoomClickSessionIdRef.current = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldBypassSuppression && (suppressRoomClickRef.current || roomSwipeMovedRef.current)) {
|
||||||
|
suppressRoomClickRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!normalizedSessionId) {
|
if (!normalizedSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (swipedRoomSessionId === normalizedSessionId || draggingRoomSessionId === normalizedSessionId) {
|
||||||
|
setSwipedRoomSessionId('');
|
||||||
|
resetRoomSwipeState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (normalizedSessionId === selectedShareRoomSessionId) {
|
if (normalizedSessionId === selectedShareRoomSessionId) {
|
||||||
setIsShareRoomListVisible(false);
|
setIsShareRoomListVisible(false);
|
||||||
return;
|
return;
|
||||||
@@ -5244,7 +5567,7 @@ export function ChatSharePage() {
|
|||||||
setIsRoomSwitching(true);
|
setIsRoomSwitching(true);
|
||||||
setRequestedRoomSessionId(normalizedSessionId);
|
setRequestedRoomSessionId(normalizedSessionId);
|
||||||
setIsShareRoomListVisible(false);
|
setIsShareRoomListVisible(false);
|
||||||
}, [selectedShareRoomSessionId]);
|
}, [draggingRoomSessionId, resetRoomSwipeState, selectedShareRoomSessionId, swipedRoomSessionId]);
|
||||||
|
|
||||||
const handleCreateShareRoom = useCallback(async () => {
|
const handleCreateShareRoom = useCallback(async () => {
|
||||||
if (!normalizedToken || isCreatingRoom) {
|
if (!normalizedToken || isCreatingRoom) {
|
||||||
@@ -6013,17 +6336,35 @@ export function ChatSharePage() {
|
|||||||
|
|
||||||
return summarizeShareReplyReferenceText(answerText || replyReferenceRequest.userText || '선택한 답변');
|
return summarizeShareReplyReferenceText(answerText || replyReferenceRequest.userText || '선택한 답변');
|
||||||
}, [replyReferenceRequest, requestAnswerTextById]);
|
}, [replyReferenceRequest, requestAnswerTextById]);
|
||||||
const previousQuestionModalRequest = useMemo(
|
const previousQuestionModalTargetRequest = useMemo(
|
||||||
() => (previousQuestionModalRequestId.trim() ? requestById.get(previousQuestionModalRequestId.trim()) ?? null : null),
|
() => (previousQuestionModalRequestId.trim() ? requestById.get(previousQuestionModalRequestId.trim()) ?? null : null),
|
||||||
[previousQuestionModalRequestId, requestById],
|
[previousQuestionModalRequestId, requestById],
|
||||||
);
|
);
|
||||||
const previousQuestionModalText = useMemo(
|
const previousQuestionModalLineage = useMemo(
|
||||||
() => resolveShareRequestQuestionText(previousQuestionModalRequest),
|
() => resolveShareRequestLineage(previousQuestionModalTargetRequest, requestById),
|
||||||
[previousQuestionModalRequest],
|
[previousQuestionModalTargetRequest, requestById],
|
||||||
);
|
);
|
||||||
const previousQuestionModalPreviewItems = useMemo(
|
const previousQuestionModalDirectParent = previousQuestionModalLineage.directParentRequest;
|
||||||
() => buildSharePreviewItemsFromText(previousQuestionModalRequest?.userText ?? '', normalizedToken),
|
const previousQuestionModalTopParent =
|
||||||
[normalizedToken, previousQuestionModalRequest?.userText],
|
previousQuestionModalLineage.topParentRequest
|
||||||
|
&& previousQuestionModalLineage.topParentRequest.requestId.trim() !== previousQuestionModalLineage.directParentRequest?.requestId.trim()
|
||||||
|
? previousQuestionModalLineage.topParentRequest
|
||||||
|
: null;
|
||||||
|
const previousQuestionModalDirectParentText = useMemo(
|
||||||
|
() => resolveShareRequestQuestionText(previousQuestionModalDirectParent),
|
||||||
|
[previousQuestionModalDirectParent],
|
||||||
|
);
|
||||||
|
const previousQuestionModalTopParentText = useMemo(
|
||||||
|
() => resolveShareRequestQuestionText(previousQuestionModalTopParent),
|
||||||
|
[previousQuestionModalTopParent],
|
||||||
|
);
|
||||||
|
const previousQuestionModalDirectParentPreviewItems = useMemo(
|
||||||
|
() => buildSharePreviewItemsFromText(previousQuestionModalDirectParent?.userText ?? '', normalizedToken),
|
||||||
|
[normalizedToken, previousQuestionModalDirectParent?.userText],
|
||||||
|
);
|
||||||
|
const previousQuestionModalTopParentPreviewItems = useMemo(
|
||||||
|
() => buildSharePreviewItemsFromText(previousQuestionModalTopParent?.userText ?? '', normalizedToken),
|
||||||
|
[normalizedToken, previousQuestionModalTopParent?.userText],
|
||||||
);
|
);
|
||||||
const activityLogByRequestId = useMemo(
|
const activityLogByRequestId = useMemo(
|
||||||
() => new Map((snapshot?.activityLogs ?? []).map((item) => [item.requestId.trim(), item])),
|
() => new Map((snapshot?.activityLogs ?? []).map((item) => [item.requestId.trim(), item])),
|
||||||
@@ -6164,6 +6505,7 @@ export function ChatSharePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recordShareAppLaunch(target.appId);
|
recordShareAppLaunch(target.appId);
|
||||||
|
setProgramReloadKey(0);
|
||||||
setProgramTarget(target);
|
setProgramTarget(target);
|
||||||
setProgramMinimizedTarget(target);
|
setProgramMinimizedTarget(target);
|
||||||
setIsProgramMinimized(false);
|
setIsProgramMinimized(false);
|
||||||
@@ -7206,6 +7548,17 @@ export function ChatSharePage() {
|
|||||||
<div className={contentLayoutClassName}>
|
<div className={contentLayoutClassName}>
|
||||||
{canToggleShareRoomList && isShareRoomListVisible ? (
|
{canToggleShareRoomList && isShareRoomListVisible ? (
|
||||||
<section className="chat-share-page__panel chat-share-page__room-list-panel">
|
<section className="chat-share-page__panel chat-share-page__room-list-panel">
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
value={shareRoomFilterKeyword}
|
||||||
|
onChange={(event) => {
|
||||||
|
setShareRoomFilterKeyword(event.target.value);
|
||||||
|
}}
|
||||||
|
className="chat-share-page__room-filter-input"
|
||||||
|
placeholder="채팅방 필터"
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
aria-label="공유채팅 채팅방 필터"
|
||||||
|
/>
|
||||||
<div className="chat-share-page__section-head chat-share-page__section-head--compact">
|
<div className="chat-share-page__section-head chat-share-page__section-head--compact">
|
||||||
<div className="chat-share-page__section-copy">
|
<div className="chat-share-page__section-copy">
|
||||||
<Title level={5}>채팅방</Title>
|
<Title level={5}>채팅방</Title>
|
||||||
@@ -7220,28 +7573,97 @@ export function ChatSharePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-share-page__room-list">
|
<div className="chat-share-page__room-list">
|
||||||
{shareRooms.map((room) => {
|
{filteredShareRooms.map((room) => {
|
||||||
const isActive = room.sessionId === selectedShareRoomSessionId;
|
const isActive = room.sessionId === selectedShareRoomSessionId;
|
||||||
|
const canDeleteRoom = canDeleteShareRoom(room, shareRooms);
|
||||||
|
const isDeletingTarget = isDeletingRoom && pendingDeleteRoomSessionId === room.sessionId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={room.sessionId}
|
key={room.sessionId}
|
||||||
type="button"
|
className={`chat-share-page__room-swipe ${
|
||||||
className={`chat-share-page__room-card${isActive ? ' chat-share-page__room-card--active' : ''}`}
|
swipedRoomSessionId === room.sessionId ? 'is-swiped' : ''
|
||||||
onClick={() => {
|
} ${draggingRoomSessionId === room.sessionId ? 'is-dragging' : ''} ${
|
||||||
handleSelectShareRoom(room.sessionId);
|
canDeleteRoom ? '' : 'is-delete-locked'
|
||||||
}}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="chat-share-page__room-card-head">
|
{canDeleteRoom ? (
|
||||||
<span className="chat-share-page__room-card-title">{room.title}</span>
|
<button
|
||||||
{room.isDefault ? <Tag color="blue">기본</Tag> : null}
|
type="button"
|
||||||
</span>
|
className="chat-share-page__room-delete-action"
|
||||||
<span className="chat-share-page__room-card-meta">
|
aria-label={`${room.title} 채팅방 삭제`}
|
||||||
{room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '공유 채팅방'}
|
title={`${room.title} 채팅방 삭제`}
|
||||||
</span>
|
disabled={isDeletingTarget}
|
||||||
</button>
|
onClick={() => {
|
||||||
|
setPendingDeleteRoomSessionId(room.sessionId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`chat-share-page__room-card${isActive ? ' chat-share-page__room-card--active' : ''}${
|
||||||
|
room.isDefault ? ' chat-share-page__room-card--default' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
transform:
|
||||||
|
draggingRoomSessionId === room.sessionId
|
||||||
|
? `translate3d(${draggingRoomOffsetX}px, 0, 0)`
|
||||||
|
: canDeleteRoom && swipedRoomSessionId === room.sessionId
|
||||||
|
? `translate3d(-${SHARE_ROOM_SWIPE_DELETE_THRESHOLD_PX}px, 0, 0)`
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
onTouchStart={(event) => {
|
||||||
|
const touch = event.changedTouches[0];
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
handleRoomSwipeStart(
|
||||||
|
room.sessionId,
|
||||||
|
touch?.clientX ?? 0,
|
||||||
|
touch?.clientY ?? 0,
|
||||||
|
(touch?.clientX ?? 0) >= rect.right - SHARE_ROOM_SWIPE_START_EDGE_PX,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onTouchMove={(event) => {
|
||||||
|
handleRoomSwipeMove(
|
||||||
|
room.sessionId,
|
||||||
|
event.changedTouches[0]?.clientX ?? 0,
|
||||||
|
event.changedTouches[0]?.clientY ?? 0,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onTouchEnd={() => {
|
||||||
|
const shouldSelect = handleRoomSwipeEnd(room.sessionId);
|
||||||
|
|
||||||
|
if (shouldSelect) {
|
||||||
|
skipNextRoomClickSessionIdRef.current = room.sessionId;
|
||||||
|
handleSelectShareRoom(room.sessionId, { bypassSuppression: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onTouchCancel={() => {
|
||||||
|
handleRoomSwipeEnd(room.sessionId);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleSelectShareRoom(room.sessionId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="chat-share-page__room-card-head">
|
||||||
|
<span className="chat-share-page__room-card-title">{room.title}</span>
|
||||||
|
{room.isDefault ? <Tag color="cyan">기본 채팅</Tag> : null}
|
||||||
|
</span>
|
||||||
|
<span className="chat-share-page__room-card-meta">
|
||||||
|
{room.isDefault
|
||||||
|
? (room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '삭제할 수 없는 기본 채팅방')
|
||||||
|
: (room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '공유 채팅방')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{filteredShareRooms.length === 0 ? (
|
||||||
|
<div className="chat-share-page__room-list-empty">
|
||||||
|
<Text type="secondary">조건에 맞는 채팅방이 없습니다.</Text>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -7644,36 +8066,96 @@ export function ChatSharePage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<Modal
|
<Modal
|
||||||
open={Boolean(previousQuestionModalRequest)}
|
open={Boolean(pendingDeleteRoom)}
|
||||||
title="부모 요청"
|
title="공유 채팅방을 삭제할까요?"
|
||||||
footer={null}
|
okText="삭제"
|
||||||
|
cancelText="취소"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
centered
|
centered
|
||||||
|
onCancel={() => {
|
||||||
|
setPendingDeleteRoomSessionId('');
|
||||||
|
}}
|
||||||
|
onOk={async () => {
|
||||||
|
if (!pendingDeleteRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleDeleteShareRoom(pendingDeleteRoom);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{pendingDeleteRoom
|
||||||
|
? `"${pendingDeleteRoom.title}" 채팅방과 이 방의 요청·메시지 기록이 삭제됩니다.`
|
||||||
|
: '선택한 공유 채팅방과 이 방의 요청·메시지 기록이 삭제됩니다.'}
|
||||||
|
</Text>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
open={Boolean(previousQuestionModalDirectParent || previousQuestionModalTopParent)}
|
||||||
|
title="부모 질의"
|
||||||
|
footer={null}
|
||||||
|
className="chat-share-page__previous-question-modal-dialog"
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setPreviousQuestionModalRequestId('');
|
setPreviousQuestionModalRequestId('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="chat-share-page__previous-question-modal">
|
<div className="chat-share-page__previous-question-modal">
|
||||||
<div className="chat-share-page__previous-question-modal-section">
|
{previousQuestionModalDirectParent ? (
|
||||||
<Text strong>부모 요청</Text>
|
<div className="chat-share-page__previous-question-modal-section">
|
||||||
<Text type="secondary">
|
<div className="chat-share-page__previous-question-modal-head">
|
||||||
{previousQuestionModalRequest ? formatTimeLabel(previousQuestionModalRequest.createdAt) || '요청 시각 없음' : ''}
|
<Text strong>바로 상위 부모</Text>
|
||||||
</Text>
|
<Text type="secondary">
|
||||||
<Paragraph className="chat-share-page__previous-question-modal-text">
|
{formatTimeLabel(previousQuestionModalDirectParent.createdAt) || '요청 시각 없음'}
|
||||||
{previousQuestionModalText || '부모 요청 내용을 찾지 못했습니다.'}
|
</Text>
|
||||||
</Paragraph>
|
|
||||||
{previousQuestionModalPreviewItems.length > 0 ? (
|
|
||||||
<div className="chat-share-page__resource-list">
|
|
||||||
{previousQuestionModalPreviewItems.map((item) => (
|
|
||||||
<ShareResourcePreviewCard
|
|
||||||
key={`previous-question-${item.id}`}
|
|
||||||
item={item}
|
|
||||||
shareToken={normalizedToken}
|
|
||||||
onOpenProgram={openProgramTarget}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
<Paragraph className="chat-share-page__previous-question-modal-text">
|
||||||
</div>
|
{previousQuestionModalDirectParentText || '부모 질의 내용을 찾지 못했습니다.'}
|
||||||
|
</Paragraph>
|
||||||
|
{previousQuestionModalDirectParentPreviewItems.length > 0 ? (
|
||||||
|
<div className="chat-share-page__resource-list">
|
||||||
|
{previousQuestionModalDirectParentPreviewItems.map((item) => (
|
||||||
|
<ShareResourcePreviewCard
|
||||||
|
key={`previous-question-direct-${item.id}`}
|
||||||
|
item={item}
|
||||||
|
shareToken={normalizedToken}
|
||||||
|
onOpenProgram={openProgramTarget}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{previousQuestionModalTopParent ? (
|
||||||
|
<div className="chat-share-page__previous-question-modal-section">
|
||||||
|
<div className="chat-share-page__previous-question-modal-head">
|
||||||
|
<Text strong>연결된 최상위 부모</Text>
|
||||||
|
<Text type="secondary">
|
||||||
|
{formatTimeLabel(previousQuestionModalTopParent.createdAt) || '요청 시각 없음'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Paragraph className="chat-share-page__previous-question-modal-text">
|
||||||
|
{previousQuestionModalTopParentText || '최상위 부모 질의 내용을 찾지 못했습니다.'}
|
||||||
|
</Paragraph>
|
||||||
|
{previousQuestionModalTopParentPreviewItems.length > 0 ? (
|
||||||
|
<div className="chat-share-page__resource-list">
|
||||||
|
{previousQuestionModalTopParentPreviewItems.map((item) => (
|
||||||
|
<ShareResourcePreviewCard
|
||||||
|
key={`previous-question-top-${item.id}`}
|
||||||
|
item={item}
|
||||||
|
shareToken={normalizedToken}
|
||||||
|
onOpenProgram={openProgramTarget}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!previousQuestionModalDirectParent && !previousQuestionModalTopParent ? (
|
||||||
|
<div className="chat-share-page__previous-question-modal-section">
|
||||||
|
<Paragraph className="chat-share-page__previous-question-modal-text">
|
||||||
|
부모 질의 내용을 찾지 못했습니다.
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
@@ -8400,6 +8882,18 @@ export function ChatSharePage() {
|
|||||||
open={Boolean(programTarget) && !isProgramMinimized}
|
open={Boolean(programTarget) && !isProgramMinimized}
|
||||||
title={programTarget?.label ?? '공유 프로그램'}
|
title={programTarget?.label ?? '공유 프로그램'}
|
||||||
meta={programTarget?.meta ?? '공유 토큰 실행'}
|
meta={programTarget?.meta ?? '공유 토큰 실행'}
|
||||||
|
actions={
|
||||||
|
programTarget ? (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
className="fullscreen-preview-modal__icon-button"
|
||||||
|
aria-label="앱 새로고침"
|
||||||
|
title="앱 새로고침"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={handleReloadProgram}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
zIndex={SHARE_PROGRAM_MODAL_Z_INDEX}
|
zIndex={SHARE_PROGRAM_MODAL_Z_INDEX}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
hideHeader={false}
|
hideHeader={false}
|
||||||
@@ -8413,23 +8907,28 @@ export function ChatSharePage() {
|
|||||||
>
|
>
|
||||||
{programTarget ? (
|
{programTarget ? (
|
||||||
embeddedPlayAppContent ? (
|
embeddedPlayAppContent ? (
|
||||||
<div className="chat-share-page__program-app-shell">
|
<div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
|
||||||
{embeddedPlayAppContent}
|
{embeddedPlayAppContent}
|
||||||
</div>
|
</div>
|
||||||
) : programTarget.appId && findReadyPlayAppEntryById(programTarget.appId) ? (
|
) : programTarget.appId && findReadyPlayAppEntryById(programTarget.appId) ? (
|
||||||
<iframe
|
<iframe
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
title={programTarget.label}
|
title={programTarget.label}
|
||||||
src={programTarget.url}
|
src={programTarget.url}
|
||||||
className="app-chat-panel__preview-frame"
|
className="app-chat-panel__preview-frame"
|
||||||
/>
|
/>
|
||||||
) : programTarget.appId === SHARE_CURRENT_CHAT_APP_ID ? (
|
) : programTarget.appId === SHARE_CURRENT_CHAT_APP_ID ? (
|
||||||
<iframe
|
<iframe
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
title={programTarget.label}
|
title={programTarget.label}
|
||||||
src={programTarget.url}
|
src={programTarget.url}
|
||||||
className="app-chat-panel__preview-frame"
|
className="app-chat-panel__preview-frame"
|
||||||
/>
|
/>
|
||||||
) : programTarget.appId === 'text-memo-widget' ? (
|
) : programTarget.appId === 'text-memo-widget' ? (
|
||||||
<div className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface">
|
<div
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
|
className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
|
||||||
|
>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={(
|
fallback={(
|
||||||
<div className="chat-share-page__program-app-loading" role="status" aria-live="polite">
|
<div className="chat-share-page__program-app-loading" role="status" aria-live="polite">
|
||||||
@@ -8441,7 +8940,10 @@ export function ChatSharePage() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
) : programTarget.appId === 'token-setting' ? (
|
) : programTarget.appId === 'token-setting' ? (
|
||||||
<div className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface">
|
<div
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
|
className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
|
||||||
|
>
|
||||||
<TokenSettingManagementPage
|
<TokenSettingManagementPage
|
||||||
sharedPreviewTokenSetting={shareTokenSetting}
|
sharedPreviewTokenSetting={shareTokenSetting}
|
||||||
sharedAccess={
|
sharedAccess={
|
||||||
@@ -8455,7 +8957,10 @@ export function ChatSharePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : programTarget.appId === 'shared-resource' ? (
|
) : programTarget.appId === 'shared-resource' ? (
|
||||||
<div className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface">
|
<div
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
|
className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
|
||||||
|
>
|
||||||
<SharedResourceManagementPage
|
<SharedResourceManagementPage
|
||||||
disableInstallMetadata
|
disableInstallMetadata
|
||||||
sharedPreview={{
|
sharedPreview={{
|
||||||
@@ -8475,26 +8980,34 @@ export function ChatSharePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : programTarget.appId === 'app-settings' ? (
|
) : programTarget.appId === 'app-settings' ? (
|
||||||
<div className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface">
|
<div
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
|
className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
|
||||||
|
>
|
||||||
<SharedAppSettingsPage shareToken={normalizedToken} />
|
<SharedAppSettingsPage shareToken={normalizedToken} />
|
||||||
</div>
|
</div>
|
||||||
) : programTarget.appId === 'server-command' ? (
|
) : programTarget.appId === 'server-command' ? (
|
||||||
<div className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface">
|
<div
|
||||||
|
key={`${programTarget.key}:${programReloadKey}`}
|
||||||
|
className="chat-share-page__program-app-shell chat-share-page__program-app-shell--surface"
|
||||||
|
>
|
||||||
<ServerCommandPage sharedAccess={sharedServerCommandAccess} />
|
<ServerCommandPage sharedAccess={sharedServerCommandAccess} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ChatPreviewBody
|
<div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
|
||||||
target={{
|
<ChatPreviewBody
|
||||||
label: programTarget.label,
|
target={{
|
||||||
url: programTarget.url,
|
label: programTarget.label,
|
||||||
kind: programTarget.kind,
|
url: programTarget.url,
|
||||||
}}
|
kind: programTarget.kind,
|
||||||
previewText=""
|
}}
|
||||||
isPreviewLoading={false}
|
previewText=""
|
||||||
previewError=""
|
isPreviewLoading={false}
|
||||||
renderHtmlAsFrame
|
previewError=""
|
||||||
fullscreen
|
renderHtmlAsFrame
|
||||||
/>
|
fullscreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</FullscreenPreviewModal>
|
</FullscreenPreviewModal>
|
||||||
|
|||||||
Reference in New Issue
Block a user