507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
import { AppstoreOutlined, FileMarkdownOutlined, MessageOutlined, ProfileOutlined } from '@ant-design/icons';
|
|
import { Badge } from 'antd';
|
|
import type { MenuProps } from 'antd';
|
|
import type { ReactNode } from 'react';
|
|
import type { PlanFilterStatus } from '../../features/planBoard';
|
|
|
|
export type TopMenuKey = 'docs' | 'apis' | 'plans' | 'chat' | 'play';
|
|
export type HeaderTopMenuKey = 'docs' | 'plans' | 'play';
|
|
export type ApiSectionKey = 'components' | 'widgets';
|
|
export type PlanSectionKey =
|
|
| PlanFilterStatus
|
|
| 'release'
|
|
| 'release-review'
|
|
| 'board'
|
|
| 'charts'
|
|
| 'schedule'
|
|
| 'history'
|
|
| 'automation-type'
|
|
| 'automation-context'
|
|
| 'token-setting'
|
|
| 'shared-resource'
|
|
| 'server-command';
|
|
export type ChatSectionKey = 'live' | 'rooms' | 'changes' | 'resources' | 'errors' | 'manage' | 'manage-defaults' | 'manage-share';
|
|
export type PlaySectionKey = 'layout' | 'draw' | 'apps' | 'test' | 'cbt';
|
|
export type PlaySidebarKey = PlaySectionKey | `layout-record:${string}`;
|
|
export const CHAT_SECTION_GROUP_LABELS: Record<ChatSectionKey, string> = {
|
|
live: 'Codex Live',
|
|
rooms: '시스템 채팅',
|
|
changes: 'Codex Live',
|
|
resources: '리소스 관리',
|
|
errors: '앱로그',
|
|
manage: '채팅 관리',
|
|
'manage-defaults': '채팅 관리',
|
|
'manage-share': '채팅 관리',
|
|
};
|
|
export const CHAT_SECTION_LABELS: Record<ChatSectionKey, string> = {
|
|
live: 'Codex Live',
|
|
rooms: '시스템 채팅',
|
|
changes: '변경 이력',
|
|
resources: '리소스 관리',
|
|
errors: '에러 로그',
|
|
manage: '유형 권한 관리',
|
|
'manage-defaults': '공통 문맥 관리',
|
|
'manage-share': '공유채팅 생성',
|
|
};
|
|
|
|
export const DOCS_DEFAULT_FOLDER = 'project';
|
|
export const PLAY_LAYOUT_RECORD_PREFIX = 'layout-record:' as const;
|
|
export const PLAN_MENU_STATUS_ORDER: PlanFilterStatus[] = ['all', 'in-progress', 'error', 'done'];
|
|
export const PLAN_GROUP_LABEL = '작업';
|
|
|
|
export const DOCS_FOLDER_LABELS: Record<string, string> = {
|
|
project: '프로젝트 구조',
|
|
};
|
|
|
|
export const PLAN_FILTER_LABELS: Record<PlanFilterStatus, string> = {
|
|
all: '자동화 현황',
|
|
'in-progress': '실행 중 (0)',
|
|
done: '완료',
|
|
error: '실패 (0)',
|
|
};
|
|
|
|
export const PLAN_SIDEBAR_LABELS: Record<PlanSectionKey, string> = {
|
|
...PLAN_FILTER_LABELS,
|
|
release: 'release (0)',
|
|
'release-review': 'release 검수',
|
|
board: '작업 요청',
|
|
charts: '차트',
|
|
schedule: '스케줄',
|
|
history: '이력',
|
|
'automation-type': '자동화 유형',
|
|
'automation-context': 'Context 유형',
|
|
'token-setting': '설정',
|
|
'shared-resource': '공유 리소스 관리',
|
|
'server-command': 'Command',
|
|
};
|
|
|
|
export const PLAY_SIDEBAR_LABELS: Record<PlaySectionKey, string> = {
|
|
layout: 'Layout Editor',
|
|
draw: 'Layout Draw',
|
|
apps: 'Apps',
|
|
test: 'Test App',
|
|
cbt: 'CBT',
|
|
};
|
|
|
|
export const PLAN_MENU_ANCHOR_IDS: Partial<Record<PlanSectionKey, string>> = {
|
|
all: 'plan-menu-all',
|
|
'in-progress': 'plan-menu-in-progress',
|
|
release: 'plan-menu-release',
|
|
'release-review': 'plan-menu-release-review',
|
|
done: 'plan-menu-done',
|
|
error: 'plan-menu-error',
|
|
board: 'plan-menu-board',
|
|
charts: 'plan-menu-charts',
|
|
schedule: 'plan-menu-schedule',
|
|
history: 'plan-menu-history',
|
|
'automation-type': 'plan-menu-automation-type',
|
|
'automation-context': 'plan-menu-automation-context',
|
|
'token-setting': 'plan-menu-token-setting',
|
|
'shared-resource': 'plan-menu-shared-resource',
|
|
'server-command': 'plan-menu-server-command',
|
|
};
|
|
|
|
export function getDocsSectionLabel(section: string) {
|
|
return DOCS_FOLDER_LABELS[section] ?? section;
|
|
}
|
|
|
|
export function resolveSavedLayoutMenuKey(layoutId: string): PlaySidebarKey {
|
|
return `${PLAY_LAYOUT_RECORD_PREFIX}${layoutId}`;
|
|
}
|
|
|
|
export function resolveSavedLayoutIdFromMenuKey(key: PlaySidebarKey) {
|
|
return key.startsWith(PLAY_LAYOUT_RECORD_PREFIX) ? key.slice(PLAY_LAYOUT_RECORD_PREFIX.length) : null;
|
|
}
|
|
|
|
export function resolvePlaySidebarLabel(selectedPlayMenu: PlaySidebarKey, savedLayouts: Array<{ id: string; name: string }>) {
|
|
const savedLayoutId = resolveSavedLayoutIdFromMenuKey(selectedPlayMenu);
|
|
|
|
if (
|
|
selectedPlayMenu === 'layout' ||
|
|
selectedPlayMenu === 'draw' ||
|
|
selectedPlayMenu === 'apps' ||
|
|
selectedPlayMenu === 'test' ||
|
|
selectedPlayMenu === 'cbt'
|
|
) {
|
|
return PLAY_SIDEBAR_LABELS[selectedPlayMenu];
|
|
}
|
|
|
|
return savedLayouts.find((record) => record.id === savedLayoutId)?.name ?? 'Saved Layout';
|
|
}
|
|
|
|
export function renderPlanMenuLabel(menu: PlanSectionKey, label: string) {
|
|
const anchorId = PLAN_MENU_ANCHOR_IDS[menu];
|
|
|
|
if (!anchorId) {
|
|
return label;
|
|
}
|
|
|
|
return <span id={anchorId}>{label}</span>;
|
|
}
|
|
|
|
export function buildDocsPath(folder = DOCS_DEFAULT_FOLDER) {
|
|
return `/docs/${folder}`;
|
|
}
|
|
|
|
export function buildApisPath(section: ApiSectionKey = 'components') {
|
|
return `/apis/${section}`;
|
|
}
|
|
|
|
export function buildPlansPath(section: PlanSectionKey = 'all') {
|
|
return `/plans/${section}`;
|
|
}
|
|
|
|
export function buildChatPath(section: ChatSectionKey = 'live') {
|
|
return `/chat/${section}`;
|
|
}
|
|
|
|
export function buildPlayPath(section: PlaySectionKey = 'layout') {
|
|
return `/play/${section}`;
|
|
}
|
|
|
|
function normalizePlayAppReturnToPath(returnTo: string | null | undefined) {
|
|
if (!returnTo || !returnTo.startsWith('/') || returnTo.startsWith('//')) {
|
|
return null;
|
|
}
|
|
|
|
return returnTo;
|
|
}
|
|
|
|
export function buildPlayAppPath(
|
|
appId: string,
|
|
launchContext: 'direct' | 'embedded' = 'direct',
|
|
returnTo?: string | null,
|
|
) {
|
|
const searchParams = new URLSearchParams({
|
|
app: appId,
|
|
});
|
|
|
|
if (launchContext === 'embedded') {
|
|
searchParams.set('launchContext', launchContext);
|
|
}
|
|
|
|
const normalizedReturnTo = normalizePlayAppReturnToPath(returnTo);
|
|
|
|
if (normalizedReturnTo) {
|
|
searchParams.set('returnTo', normalizedReturnTo);
|
|
}
|
|
|
|
return `/play/apps?${searchParams.toString()}`;
|
|
}
|
|
|
|
export function buildSavedLayoutPath(layoutId: string) {
|
|
return `/play/layout-record/${layoutId}`;
|
|
}
|
|
|
|
type RenderMenuLabelParams = {
|
|
key: string;
|
|
label: string;
|
|
};
|
|
|
|
function renderMenuLabel({ key, label }: RenderMenuLabelParams) {
|
|
return <span id={key}>{label}</span>;
|
|
}
|
|
|
|
export function buildDocsMenuItems(docFolders: string[]): MenuProps['items'] {
|
|
return [
|
|
{
|
|
key: 'docs-group',
|
|
icon: <FileMarkdownOutlined />,
|
|
label: 'Docs',
|
|
children: docFolders.map((folder) => ({
|
|
key: folder,
|
|
label: DOCS_FOLDER_LABELS[folder] ?? folder,
|
|
})),
|
|
},
|
|
];
|
|
}
|
|
|
|
export function buildApiMenuItems(): MenuProps['items'] {
|
|
return [
|
|
{
|
|
key: 'api-group',
|
|
icon: <AppstoreOutlined />,
|
|
label: 'APIs',
|
|
children: [
|
|
{ key: 'components', label: 'Components' },
|
|
{ key: 'widgets', label: 'Widgets' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
export function buildPlanMenuItems(hasAccess = true): MenuProps['items'] {
|
|
if (!hasAccess) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{
|
|
key: 'plan-group',
|
|
icon: <ProfileOutlined />,
|
|
label: PLAN_GROUP_LABEL,
|
|
children: [
|
|
{
|
|
key: 'all',
|
|
label: renderPlanMenuLabel('all', PLAN_FILTER_LABELS.all),
|
|
},
|
|
{
|
|
key: 'board',
|
|
label: renderPlanMenuLabel('board', PLAN_SIDEBAR_LABELS.board),
|
|
},
|
|
{
|
|
key: 'release-review',
|
|
label: renderPlanMenuLabel('release-review', PLAN_SIDEBAR_LABELS['release-review']),
|
|
},
|
|
{
|
|
key: 'charts',
|
|
label: renderPlanMenuLabel('charts', PLAN_SIDEBAR_LABELS.charts),
|
|
},
|
|
{
|
|
key: 'schedule',
|
|
label: renderPlanMenuLabel('schedule', PLAN_SIDEBAR_LABELS.schedule),
|
|
},
|
|
{
|
|
key: 'history',
|
|
label: renderPlanMenuLabel('history', PLAN_SIDEBAR_LABELS.history),
|
|
},
|
|
{
|
|
key: 'automation-type',
|
|
label: renderPlanMenuLabel('automation-type', PLAN_SIDEBAR_LABELS['automation-type']),
|
|
},
|
|
{
|
|
key: 'automation-context',
|
|
label: renderPlanMenuLabel('automation-context', PLAN_SIDEBAR_LABELS['automation-context']),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'token-management-group',
|
|
icon: <ProfileOutlined />,
|
|
label: '토큰관리',
|
|
children: [
|
|
{
|
|
key: 'token-setting',
|
|
label: renderPlanMenuLabel('token-setting', PLAN_SIDEBAR_LABELS['token-setting']),
|
|
},
|
|
{
|
|
key: 'shared-resource',
|
|
label: renderPlanMenuLabel('shared-resource', PLAN_SIDEBAR_LABELS['shared-resource']),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'server-group',
|
|
icon: <ProfileOutlined />,
|
|
label: 'Servers',
|
|
children: [
|
|
{
|
|
key: 'server-command',
|
|
label: renderPlanMenuLabel('server-command', PLAN_SIDEBAR_LABELS['server-command']),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
function renderChatUnreadLabel(label: string, unreadCount: number) {
|
|
if (unreadCount <= 0) {
|
|
return label;
|
|
}
|
|
|
|
return (
|
|
<Badge count={unreadCount} size="small" offset={[10, 0]}>
|
|
<span>{label}</span>
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
export function buildChatMenuItems(_hasAccess = true, unreadCount = 0): MenuProps['items'] {
|
|
return [
|
|
{
|
|
key: 'chat-group',
|
|
icon: <MessageOutlined />,
|
|
label: renderChatUnreadLabel('채팅', unreadCount),
|
|
children: [
|
|
{ key: 'live', label: 'Codex Live' },
|
|
{ key: 'rooms', label: '시스템 채팅' },
|
|
{ key: 'changes', label: '변경 이력' },
|
|
{ key: 'resources', label: '리소스 관리' },
|
|
],
|
|
},
|
|
{
|
|
key: 'app-log-group',
|
|
icon: <MessageOutlined />,
|
|
label: '앱로그',
|
|
children: [{ key: 'errors', label: '에러 로그' }],
|
|
},
|
|
{
|
|
key: 'chat-manage-group',
|
|
icon: <MessageOutlined />,
|
|
label: '채팅 관리',
|
|
children: [
|
|
{ key: 'manage', label: '유형 권한 관리' },
|
|
{ key: 'manage-defaults', label: '공통 문맥 관리' },
|
|
{ key: 'manage-share', label: '공유채팅 생성' },
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
export function buildPlayMenuItems(savedLayouts: Array<{ id: string; name: string }>): MenuProps['items'] {
|
|
return [
|
|
{
|
|
key: 'play-group',
|
|
icon: <AppstoreOutlined />,
|
|
label: 'Play',
|
|
children: [
|
|
{
|
|
key: 'play-layout-group',
|
|
label: 'Layout',
|
|
children: [
|
|
{ key: 'layout', label: 'Layout Editor' },
|
|
{ key: 'draw', label: 'Layout Draw' },
|
|
...(savedLayouts.length
|
|
? savedLayouts.map((record) => ({
|
|
key: resolveSavedLayoutMenuKey(record.id),
|
|
label: record.name,
|
|
}))
|
|
: [{ key: 'saved-layout-empty', label: '저장된 레이아웃 없음', disabled: true }]),
|
|
],
|
|
},
|
|
{
|
|
key: 'play-apps-group',
|
|
label: 'Apps',
|
|
children: [
|
|
{ key: 'apps', label: 'Apps' },
|
|
{ key: 'test', label: 'Test App' },
|
|
{
|
|
key: 'play-apps-general-group',
|
|
label: '일반',
|
|
children: [{ key: 'cbt', label: 'CBT' }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
export function resolvePlanOpenKeys() {
|
|
return ['plan-group', 'token-management-group', 'server-group', 'chat-group', 'app-log-group', 'chat-manage-group'];
|
|
}
|
|
|
|
export function resolvePlayOpenKeys() {
|
|
return ['play-group', 'play-layout-group', 'play-apps-group', 'play-apps-general-group'];
|
|
}
|
|
|
|
export function resolvePlanQuickFilterMenu(filter: 'working' | 'release-pending-main' | 'automation-failed') {
|
|
if (filter === 'working') {
|
|
return 'in-progress' as const;
|
|
}
|
|
|
|
return filter === 'release-pending-main' ? ('release' as const) : ('error' as const);
|
|
}
|
|
|
|
export function renderSidebarIntro(activeTopMenu: TopMenuKey) {
|
|
const isDocsGroup = activeTopMenu === 'docs' || activeTopMenu === 'apis';
|
|
|
|
return {
|
|
color: isDocsGroup ? 'gold' : activeTopMenu === 'play' ? 'cyan' : 'green',
|
|
tag: isDocsGroup ? 'Docs' : activeTopMenu === 'play' ? 'Play' : '자동화',
|
|
description: isDocsGroup
|
|
? '사이드바에서 Docs와 APIs를 함께 탐색합니다.'
|
|
: activeTopMenu === 'play'
|
|
? '사이드바에서 Play 화면을 탐색합니다.'
|
|
: '사이드바에서 작업, Codex Live, 앱로그를 함께 전환합니다.',
|
|
};
|
|
}
|
|
|
|
export function resolveCurrentPageDescriptor(params: {
|
|
topMenu: TopMenuKey;
|
|
docsMenu: string;
|
|
apiMenu: ApiSectionKey;
|
|
planMenu: PlanSectionKey;
|
|
chatMenu: ChatSectionKey;
|
|
playMenu: PlaySidebarKey;
|
|
savedLayouts: Array<{ id: string; name: string }>;
|
|
}) {
|
|
const { topMenu, docsMenu, apiMenu, planMenu, chatMenu, playMenu, savedLayouts } = params;
|
|
|
|
if (topMenu === 'docs') {
|
|
return {
|
|
id: `docs:${docsMenu}`,
|
|
title: `Docs / ${getDocsSectionLabel(docsMenu)}`,
|
|
topMenu,
|
|
section: docsMenu,
|
|
};
|
|
}
|
|
|
|
if (topMenu === 'apis') {
|
|
return {
|
|
id: `apis:${apiMenu}`,
|
|
title: apiMenu === 'components' ? 'APIs / Components' : 'APIs / Widgets',
|
|
topMenu,
|
|
section: apiMenu,
|
|
};
|
|
}
|
|
|
|
if (topMenu === 'plans') {
|
|
const title =
|
|
planMenu === 'server-command'
|
|
? `Servers / ${PLAN_SIDEBAR_LABELS[planMenu]}`
|
|
: planMenu === 'token-setting' || planMenu === 'shared-resource'
|
|
? `토큰관리 / ${PLAN_SIDEBAR_LABELS[planMenu]}`
|
|
: `${PLAN_GROUP_LABEL} / ${PLAN_SIDEBAR_LABELS[planMenu]}`;
|
|
|
|
return {
|
|
id: `plans:${planMenu}`,
|
|
title,
|
|
topMenu,
|
|
section: planMenu,
|
|
};
|
|
}
|
|
|
|
if (topMenu === 'chat') {
|
|
const title = `${CHAT_SECTION_GROUP_LABELS[chatMenu]} / ${CHAT_SECTION_LABELS[chatMenu]}`;
|
|
|
|
return {
|
|
id: `app-log:${chatMenu}`,
|
|
title,
|
|
topMenu,
|
|
section: chatMenu,
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: `play:${playMenu}`,
|
|
title: `Play / ${resolvePlaySidebarLabel(playMenu, savedLayouts)}`,
|
|
topMenu,
|
|
section: playMenu,
|
|
};
|
|
}
|
|
|
|
export function resolveTopMenuPath(menu: HeaderTopMenuKey, currentDocsFolder: string) {
|
|
if (menu === 'docs') {
|
|
return buildDocsPath(currentDocsFolder);
|
|
}
|
|
|
|
if (menu === 'plans') {
|
|
return buildPlansPath('all');
|
|
}
|
|
|
|
if (menu === 'play') {
|
|
return buildPlayPath('layout');
|
|
}
|
|
|
|
return buildPlansPath('all');
|
|
}
|
|
|
|
export function createPageWindowId(topMenu: TopMenuKey, section: string) {
|
|
return `page:${topMenu}:${section}`;
|
|
}
|
|
|
|
export function renderMenuAnchor(label: string, anchorId: string): ReactNode {
|
|
return renderMenuLabel({ key: anchorId, label });
|
|
}
|