feat: expand live chat and work server tools
This commit is contained in:
310
src/views/play/LayoutPreviewWidgets.tsx
Normal file
310
src/views/play/LayoutPreviewWidgets.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
LeftOutlined,
|
||||
PlusOutlined,
|
||||
RightOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Empty, Input, Tag, Typography } from 'antd';
|
||||
import type { SyntheticEvent } from 'react';
|
||||
import { InputUI } from '../../components/inputs/primitives/input';
|
||||
import { SelectUI, type SelectOptionItem } from '../../components/inputs/select';
|
||||
import type {
|
||||
LayoutPreviewBaseInputState,
|
||||
LayoutPreviewEmptyPaneState,
|
||||
LayoutPreviewMemoState,
|
||||
LayoutPreviewSelectState,
|
||||
} from './layoutPreviewRuntime';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function stopPreviewEvent(event: SyntheticEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function formatMemoTimestamp(value: string) {
|
||||
return new Intl.DateTimeFormat('ko-KR', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function getMemoPreview(body: string) {
|
||||
return body.replace(/\s+/g, ' ').trim() || '새 메모';
|
||||
}
|
||||
|
||||
function formatEmptyPaneTimestamp(value: string | null) {
|
||||
if (!value) {
|
||||
return '아직 메모가 없습니다.';
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('ko-KR', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
const EMPTY_PANE_READINESS_META: Record<
|
||||
LayoutPreviewEmptyPaneState['readiness'],
|
||||
{ label: string; tone: 'default' | 'processing' | 'success' }
|
||||
> = {
|
||||
unassigned: { label: '컴포넌트 대기', tone: 'default' },
|
||||
drafting: { label: '요구 정리 중', tone: 'processing' },
|
||||
ready: { label: '준비 완료', tone: 'success' },
|
||||
};
|
||||
|
||||
export function LayoutPreviewTextMemoPane({
|
||||
state,
|
||||
onStartDraft,
|
||||
onToggleList,
|
||||
onDeleteSelection,
|
||||
onSaveDraft,
|
||||
onSelectNote,
|
||||
onMoveSelection,
|
||||
onDraftChange,
|
||||
}: {
|
||||
state: LayoutPreviewMemoState;
|
||||
onStartDraft: () => void;
|
||||
onToggleList: () => void;
|
||||
onDeleteSelection: () => void;
|
||||
onSaveDraft: () => void;
|
||||
onSelectNote: (noteId: string) => void;
|
||||
onMoveSelection: (direction: -1 | 1) => void;
|
||||
onDraftChange: (nextValue: string) => void;
|
||||
}) {
|
||||
const selectedIndex = state.selectedId ? state.notes.findIndex((note) => note.id === state.selectedId) : -1;
|
||||
const selectedNote = selectedIndex >= 0 ? state.notes[selectedIndex] : null;
|
||||
const hasDraft = state.draftBody.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="layout-playground__memo-widget-preview" onClick={stopPreviewEvent}>
|
||||
<div className="layout-playground__memo-widget-preview-toolbar" role="toolbar" aria-label="메모 도구">
|
||||
<div className="layout-playground__memo-widget-preview-toolbar-group">
|
||||
<Button type="text" shape="circle" htmlType="button" icon={<PlusOutlined />} aria-label="새 메모" onClick={onStartDraft} />
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
htmlType="button"
|
||||
icon={<DeleteOutlined />}
|
||||
aria-label="메모 삭제"
|
||||
disabled={!selectedNote && !hasDraft}
|
||||
onClick={onDeleteSelection}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
htmlType="button"
|
||||
icon={<UnorderedListOutlined />}
|
||||
aria-label="메모 목록"
|
||||
onClick={onToggleList}
|
||||
/>
|
||||
</div>
|
||||
<div className="layout-playground__memo-widget-preview-toolbar-group">
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
htmlType="button"
|
||||
icon={<LeftOutlined />}
|
||||
disabled={selectedIndex <= 0}
|
||||
onClick={() => {
|
||||
onMoveSelection(-1);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
htmlType="button"
|
||||
icon={<RightOutlined />}
|
||||
disabled={selectedIndex < 0 || selectedIndex >= state.notes.length - 1}
|
||||
onClick={() => {
|
||||
onMoveSelection(1);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
htmlType="button"
|
||||
icon={<CheckOutlined />}
|
||||
aria-label="저장"
|
||||
disabled={!hasDraft && !selectedNote}
|
||||
onClick={onSaveDraft}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`layout-playground__memo-widget-preview-body${state.isListOpen ? ' layout-playground__memo-widget-preview-body--list' : ''}`}>
|
||||
{state.isLoading ? (
|
||||
<div className="layout-playground__memo-widget-preview-empty">
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="메모를 준비하는 중입니다." />
|
||||
</div>
|
||||
) : state.isListOpen ? (
|
||||
<div className="layout-playground__memo-widget-preview-sheet">
|
||||
{state.notes.length ? (
|
||||
<div className="layout-playground__memo-widget-preview-list">
|
||||
{state.notes.map((note) => (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
className={`layout-playground__memo-widget-preview-item${note.id === state.selectedId ? ' layout-playground__memo-widget-preview-item--active' : ''}`}
|
||||
onClick={() => {
|
||||
onSelectNote(note.id);
|
||||
}}
|
||||
>
|
||||
<span className="layout-playground__memo-widget-preview-item-time">
|
||||
{formatMemoTimestamp(note.updatedAt)}
|
||||
</span>
|
||||
<span className="layout-playground__memo-widget-preview-item-copy">{getMemoPreview(note.body)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="layout-playground__memo-widget-preview-empty">
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="저장된 메모가 없습니다." />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="layout-playground__memo-widget-preview-editor">
|
||||
<div className="layout-playground__memo-widget-preview-meta">
|
||||
<span>{selectedNote ? formatMemoTimestamp(selectedNote.updatedAt) : '새 메모'}</span>
|
||||
<span>{state.draftBody.length}/1200</span>
|
||||
</div>
|
||||
<Input.TextArea
|
||||
className="layout-playground__memo-widget-preview-input"
|
||||
value={state.draftBody}
|
||||
placeholder="본문 메모를 입력하세요."
|
||||
variant="borderless"
|
||||
maxLength={1200}
|
||||
autoSize={false}
|
||||
onChange={(event) => {
|
||||
onDraftChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutPreviewBaseInputPane({
|
||||
state,
|
||||
fillPane = false,
|
||||
placeholder = '입력 후 Enter 또는 blur',
|
||||
}: {
|
||||
state: LayoutPreviewBaseInputState;
|
||||
fillPane?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`layout-playground__base-input-preview${fillPane ? ' layout-playground__base-input-preview--fill' : ''}`}
|
||||
onClick={stopPreviewEvent}
|
||||
>
|
||||
<InputUI
|
||||
className="layout-playground__base-input-preview-field"
|
||||
value={state.value}
|
||||
placeholder={placeholder}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutPreviewSelectPane({
|
||||
state,
|
||||
data,
|
||||
onChange,
|
||||
}: {
|
||||
state: LayoutPreviewSelectState;
|
||||
data: SelectOptionItem[];
|
||||
onChange: (nextCode?: string, item?: SelectOptionItem) => void;
|
||||
}) {
|
||||
const resolvedSelectedCode =
|
||||
state.selectedCode && data.some((item) => item.code === state.selectedCode)
|
||||
? state.selectedCode
|
||||
: data[0]?.code;
|
||||
const formatComboLabel = (item: SelectOptionItem) => item.value;
|
||||
|
||||
return (
|
||||
<div className="layout-playground__select-preview" onClick={stopPreviewEvent}>
|
||||
<SelectUI
|
||||
data={data}
|
||||
value={resolvedSelectedCode}
|
||||
allowClear
|
||||
placeholder="콤보 값을 선택하세요"
|
||||
formatLabel={formatComboLabel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutPreviewEmptyPane({
|
||||
paneLabel,
|
||||
paneDescription,
|
||||
sizeSummary,
|
||||
state,
|
||||
onReadinessChange,
|
||||
onNoteChange,
|
||||
}: {
|
||||
paneLabel: string;
|
||||
paneDescription: string;
|
||||
sizeSummary: string;
|
||||
state: LayoutPreviewEmptyPaneState;
|
||||
onReadinessChange: (nextValue: LayoutPreviewEmptyPaneState['readiness']) => void;
|
||||
onNoteChange: (nextValue: string) => void;
|
||||
}) {
|
||||
const readinessMeta = EMPTY_PANE_READINESS_META[state.readiness];
|
||||
|
||||
return (
|
||||
<div className="layout-playground__empty-pane-preview" onClick={stopPreviewEvent}>
|
||||
<div className="layout-playground__empty-pane-preview-head">
|
||||
<div className="layout-playground__empty-pane-preview-copy">
|
||||
<Text strong>{paneLabel}</Text>
|
||||
<Text type="secondary">{paneDescription}</Text>
|
||||
</div>
|
||||
<Tag color={readinessMeta.tone}>{readinessMeta.label}</Tag>
|
||||
</div>
|
||||
|
||||
<div className="layout-playground__empty-pane-preview-meta">
|
||||
<div className="layout-playground__empty-pane-preview-meta-item">
|
||||
<span>배치 규칙</span>
|
||||
<strong>{sizeSummary}</strong>
|
||||
</div>
|
||||
<div className="layout-playground__empty-pane-preview-meta-item">
|
||||
<span>최근 변경</span>
|
||||
<strong>{formatEmptyPaneTimestamp(state.updatedAt)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="layout-playground__empty-pane-preview-actions">
|
||||
<Button type={state.readiness === 'unassigned' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('unassigned')}>
|
||||
대기
|
||||
</Button>
|
||||
<Button type={state.readiness === 'drafting' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('drafting')}>
|
||||
정리 중
|
||||
</Button>
|
||||
<Button type={state.readiness === 'ready' ? 'primary' : 'default'} size="small" onClick={() => onReadinessChange('ready')}>
|
||||
준비 완료
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Input.TextArea
|
||||
className="layout-playground__empty-pane-preview-note"
|
||||
value={state.note}
|
||||
placeholder="이 section에 들어갈 역할이나 메모를 간단히 정리하세요."
|
||||
autoSize={{ minRows: 4, maxRows: 8 }}
|
||||
maxLength={400}
|
||||
onChange={(event) => {
|
||||
onNoteChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user