chore: test deploy snapshot

This commit is contained in:
2026-05-27 16:35:12 +09:00
parent e8a628ac34
commit 10805d242e
7 changed files with 1032 additions and 72 deletions

View File

@@ -42,6 +42,7 @@ import {
clearChatShareConversationRoom,
completeChatShareManualBadge,
createChatShareRoom,
deleteChatShareRoom,
fetchChatShareRuntimeSnapshot,
fetchChatShareSnapshot,
getStoredChatShareAccessPin,
@@ -97,6 +98,12 @@ const SHARE_TOKEN_USAGE_CLOCK_INTERVAL_MS = 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_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 = [
{ value: 'always', label: '매번 묻기', minutes: 0 },
{ value: '5', label: '5분 유지', minutes: 5 },
@@ -249,6 +256,14 @@ function parseAccessPinPromptTtlOptionValue(value: string) {
return matchedOption?.minutes ?? null;
}
function canDeleteShareRoom(room: ChatShareRoomSummary, rooms: ChatShareRoomSummary[]) {
if (room.isDefault) {
return false;
}
return rooms.length > 1;
}
function readStoredShareImmediateSendPinnedByToken() {
if (typeof window === 'undefined') {
return {} as Record<string, boolean>;
@@ -1551,6 +1566,18 @@ function buildRequestAnswerText(
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 {
return {
...snapshot,
@@ -1580,6 +1607,41 @@ function resolveShareConversationParentRequest(
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) {
if (!shareToken) {
return [];
@@ -2936,7 +2998,11 @@ function ShareRequestCard({
() => buildSharePreviewItemsFromText(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 shouldRenderFullAnswer = mode === 'full' || mode === 'answer-only';
const isRequestStillRunning = isRequestInFlight(request.status);
@@ -2975,8 +3041,22 @@ function ShareRequestCard({
&& !request.hasResponse
&& request.status === 'queued';
const retryCount = Math.max(0, Number(request.retryCount ?? 0) || 0);
const hasQuestionLineage = Boolean(directParentRequest || topParentRequest);
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 ? (
<Button
type="text"
@@ -3239,7 +3319,7 @@ export function ChatSharePage() {
const [pendingVerificationCompletionRequestIds, setPendingVerificationCompletionRequestIds] = useState<string[]>([]);
const [pendingRequestCancellationIds, setPendingRequestCancellationIds] = useState<string[]>([]);
const [pendingRequestRetryIds, setPendingRequestRetryIds] = useState<string[]>([]);
const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(true);
const [isShareRoomListVisible, setIsShareRoomListVisible] = useState(false);
const [isRoomSwitching, setIsRoomSwitching] = useState(false);
const [replyReferenceRequestId, setReplyReferenceRequestId] = useState('');
const [previousQuestionModalRequestId, setPreviousQuestionModalRequestId] = useState('');
@@ -3283,6 +3363,12 @@ export function ChatSharePage() {
const [optimisticShareRooms, setOptimisticShareRooms] = useState<ChatShareRoomSummary[]>([]);
const [isRefreshingRoomNotificationStatus, setIsRefreshingRoomNotificationStatus] = 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 [searchPanelMode, setSearchPanelMode] = useState<ShareSearchPanelMode>('all');
const [selectedAppEnvironment, setSelectedAppEnvironment] = useState<ShareAppEnvironment>(() =>
@@ -3291,6 +3377,7 @@ export function ChatSharePage() {
const [programTarget, setProgramTarget] = useState<ShareProgramTarget | null>(null);
const [programMinimizedTarget, setProgramMinimizedTarget] = useState<ShareProgramTarget | null>(null);
const [isProgramMinimized, setIsProgramMinimized] = useState(false);
const [programReloadKey, setProgramReloadKey] = useState(0);
const programMinimizedCardRef = useRef<HTMLDivElement | null>(null);
const programMinimizedDragStateRef = useRef<{
pointerId: number;
@@ -3299,6 +3386,14 @@ export function ChatSharePage() {
captureTarget: HTMLDivElement;
} | null>(null);
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 [programMinimizedPosition, setProgramMinimizedPosition] = useState(() => programMinimizedPositionRef.current);
const composerRef = useRef<TextAreaRef | null>(null);
@@ -3348,6 +3443,30 @@ export function ChatSharePage() {
() => shareRooms.find((item) => item.sessionId === selectedShareRoomSessionId) ?? null,
[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(() => {
if (optimisticShareRooms.length === 0 || !snapshot?.rooms?.length) {
return;
@@ -3831,6 +3950,111 @@ export function ChatSharePage() {
setCreatingRoomSeedMessage('이 방에서 이어갈 작업 내용을 남겨 주세요. 필요한 파일이나 참고 문맥이 있으면 함께 적어 주세요.');
setIsCreateRoomOpen(true);
}, [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 }) => {
if (!normalizedToken || !selectedShareRoomSessionId) {
setShareRuntimeSnapshot(null);
@@ -3957,6 +4181,74 @@ export function ChatSharePage() {
snapshotRefreshPromiseRef.current = refreshTask;
return refreshTask;
}, [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 actionLabel = item.status === 'queued' ? '대기 요청' : '실행 중 요청';
@@ -4448,6 +4740,10 @@ export function ChatSharePage() {
window.location.reload();
}, []);
const handleReloadProgram = useCallback(() => {
setProgramReloadKey((current) => current + 1);
}, []);
useEffect(() => {
setAccessPinInput('');
setAccessPinSubmitError('');
@@ -4743,6 +5039,16 @@ export function ChatSharePage() {
setRequestedRoomSessionId('');
}, [requestedRoomSessionId, shareRooms]);
useEffect(() => {
if (isDeletingRoom) {
return;
}
if (!shareRooms.some((room) => room.sessionId === swipedRoomSessionId)) {
setSwipedRoomSessionId('');
resetRoomSwipeState();
}
}, [isDeletingRoom, resetRoomSwipeState, shareRooms, swipedRoomSessionId]);
useEffect(() => {
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 shouldBypassSuppression = options?.bypassSuppression === true;
if (!shouldBypassSuppression && skipNextRoomClickSessionIdRef.current === normalizedSessionId) {
skipNextRoomClickSessionIdRef.current = '';
return;
}
if (!shouldBypassSuppression && (suppressRoomClickRef.current || roomSwipeMovedRef.current)) {
suppressRoomClickRef.current = false;
return;
}
if (!normalizedSessionId) {
return;
}
if (swipedRoomSessionId === normalizedSessionId || draggingRoomSessionId === normalizedSessionId) {
setSwipedRoomSessionId('');
resetRoomSwipeState();
return;
}
if (normalizedSessionId === selectedShareRoomSessionId) {
setIsShareRoomListVisible(false);
return;
@@ -5244,7 +5567,7 @@ export function ChatSharePage() {
setIsRoomSwitching(true);
setRequestedRoomSessionId(normalizedSessionId);
setIsShareRoomListVisible(false);
}, [selectedShareRoomSessionId]);
}, [draggingRoomSessionId, resetRoomSwipeState, selectedShareRoomSessionId, swipedRoomSessionId]);
const handleCreateShareRoom = useCallback(async () => {
if (!normalizedToken || isCreatingRoom) {
@@ -6013,17 +6336,35 @@ export function ChatSharePage() {
return summarizeShareReplyReferenceText(answerText || replyReferenceRequest.userText || '선택한 답변');
}, [replyReferenceRequest, requestAnswerTextById]);
const previousQuestionModalRequest = useMemo(
const previousQuestionModalTargetRequest = useMemo(
() => (previousQuestionModalRequestId.trim() ? requestById.get(previousQuestionModalRequestId.trim()) ?? null : null),
[previousQuestionModalRequestId, requestById],
);
const previousQuestionModalText = useMemo(
() => resolveShareRequestQuestionText(previousQuestionModalRequest),
[previousQuestionModalRequest],
const previousQuestionModalLineage = useMemo(
() => resolveShareRequestLineage(previousQuestionModalTargetRequest, requestById),
[previousQuestionModalTargetRequest, requestById],
);
const previousQuestionModalPreviewItems = useMemo(
() => buildSharePreviewItemsFromText(previousQuestionModalRequest?.userText ?? '', normalizedToken),
[normalizedToken, previousQuestionModalRequest?.userText],
const previousQuestionModalDirectParent = previousQuestionModalLineage.directParentRequest;
const previousQuestionModalTopParent =
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(
() => new Map((snapshot?.activityLogs ?? []).map((item) => [item.requestId.trim(), item])),
@@ -6164,6 +6505,7 @@ export function ChatSharePage() {
}
recordShareAppLaunch(target.appId);
setProgramReloadKey(0);
setProgramTarget(target);
setProgramMinimizedTarget(target);
setIsProgramMinimized(false);
@@ -7206,6 +7548,17 @@ export function ChatSharePage() {
<div className={contentLayoutClassName}>
{canToggleShareRoomList && isShareRoomListVisible ? (
<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-copy">
<Title level={5}></Title>
@@ -7220,28 +7573,97 @@ export function ChatSharePage() {
) : null}
</div>
<div className="chat-share-page__room-list">
{shareRooms.map((room) => {
{filteredShareRooms.map((room) => {
const isActive = room.sessionId === selectedShareRoomSessionId;
const canDeleteRoom = canDeleteShareRoom(room, shareRooms);
const isDeletingTarget = isDeletingRoom && pendingDeleteRoomSessionId === room.sessionId;
return (
<button
<div
key={room.sessionId}
type="button"
className={`chat-share-page__room-card${isActive ? ' chat-share-page__room-card--active' : ''}`}
onClick={() => {
handleSelectShareRoom(room.sessionId);
}}
className={`chat-share-page__room-swipe ${
swipedRoomSessionId === room.sessionId ? 'is-swiped' : ''
} ${draggingRoomSessionId === room.sessionId ? 'is-dragging' : ''} ${
canDeleteRoom ? '' : 'is-delete-locked'
}`}
>
<span className="chat-share-page__room-card-head">
<span className="chat-share-page__room-card-title">{room.title}</span>
{room.isDefault ? <Tag color="blue"></Tag> : null}
</span>
<span className="chat-share-page__room-card-meta">
{room.contextLabel?.trim() || room.requestBadgeLabel?.trim() || '공유 채팅방'}
</span>
</button>
{canDeleteRoom ? (
<button
type="button"
className="chat-share-page__room-delete-action"
aria-label={`${room.title} 채팅방 삭제`}
title={`${room.title} 채팅방 삭제`}
disabled={isDeletingTarget}
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>
</section>
) : null}
@@ -7644,36 +8066,96 @@ export function ChatSharePage() {
</div>
) : null}
<Modal
open={Boolean(previousQuestionModalRequest)}
title="부모 요청"
footer={null}
open={Boolean(pendingDeleteRoom)}
title="공유 채팅방을 삭제할까요?"
okText="삭제"
cancelText="취소"
okButtonProps={{ danger: true }}
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={() => {
setPreviousQuestionModalRequestId('');
}}
>
<div className="chat-share-page__previous-question-modal">
<div className="chat-share-page__previous-question-modal-section">
<Text strong> </Text>
<Text type="secondary">
{previousQuestionModalRequest ? formatTimeLabel(previousQuestionModalRequest.createdAt) || '요청 시각 없음' : ''}
</Text>
<Paragraph className="chat-share-page__previous-question-modal-text">
{previousQuestionModalText || '부모 요청 내용을 찾지 못했습니다.'}
</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}
/>
))}
{previousQuestionModalDirectParent ? (
<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(previousQuestionModalDirectParent.createdAt) || '요청 시각 없음'}
</Text>
</div>
) : null}
</div>
<Paragraph className="chat-share-page__previous-question-modal-text">
{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>
</Modal>
<Modal
@@ -8400,6 +8882,18 @@ export function ChatSharePage() {
open={Boolean(programTarget) && !isProgramMinimized}
title={programTarget?.label ?? '공유 프로그램'}
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}
maskClosable={false}
hideHeader={false}
@@ -8413,23 +8907,28 @@ export function ChatSharePage() {
>
{programTarget ? (
embeddedPlayAppContent ? (
<div className="chat-share-page__program-app-shell">
<div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
{embeddedPlayAppContent}
</div>
) : programTarget.appId && findReadyPlayAppEntryById(programTarget.appId) ? (
<iframe
key={`${programTarget.key}:${programReloadKey}`}
title={programTarget.label}
src={programTarget.url}
className="app-chat-panel__preview-frame"
/>
) : programTarget.appId === SHARE_CURRENT_CHAT_APP_ID ? (
<iframe
key={`${programTarget.key}:${programReloadKey}`}
title={programTarget.label}
src={programTarget.url}
className="app-chat-panel__preview-frame"
/>
) : 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
fallback={(
<div className="chat-share-page__program-app-loading" role="status" aria-live="polite">
@@ -8441,7 +8940,10 @@ export function ChatSharePage() {
</Suspense>
</div>
) : 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
sharedPreviewTokenSetting={shareTokenSetting}
sharedAccess={
@@ -8455,7 +8957,10 @@ export function ChatSharePage() {
/>
</div>
) : 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
disableInstallMetadata
sharedPreview={{
@@ -8475,26 +8980,34 @@ export function ChatSharePage() {
/>
</div>
) : 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} />
</div>
) : 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} />
</div>
) : (
<ChatPreviewBody
target={{
label: programTarget.label,
url: programTarget.url,
kind: programTarget.kind,
}}
previewText=""
isPreviewLoading={false}
previewError=""
renderHtmlAsFrame
fullscreen
/>
<div key={`${programTarget.key}:${programReloadKey}`} className="chat-share-page__program-app-shell">
<ChatPreviewBody
target={{
label: programTarget.label,
url: programTarget.url,
kind: programTarget.kind,
}}
previewText=""
isPreviewLoading={false}
previewError=""
renderHtmlAsFrame
fullscreen
/>
</div>
)
) : null}
</FullscreenPreviewModal>