Initial import

This commit is contained in:
how2ice
2026-04-21 03:33:23 +09:00
commit 9e4b70f1f1
495 changed files with 94680 additions and 0 deletions

14
src/features/layout/README.md Executable file
View File

@@ -0,0 +1,14 @@
# Layout Feature
프로젝트 종속적인 레이아웃은 `src/features/layout` 아래에서 관리합니다.
## 포함 항목
- 컴포넌트 샘플 레이아웃
- 위젯 샘플 레이아웃
- Markdown preview 리스트 레이아웃
## 규칙
- 공통 레이아웃 시스템이 아니라 현재 프로젝트 화면에 종속되면 `features/layout`에 배치
- 공통 재사용 가능성이 높으면 `components` 또는 `widgets`로 승격 검토

View File

@@ -0,0 +1,178 @@
import { Anchor, Card, Empty, Flex, Space, Tag, Typography } from 'antd';
import { createElement, useEffect, useMemo, useState } from 'react';
import type { LoadedSampleEntry, SampleEntry } from '../../../samples/registry';
import { resolveSampleEntries } from '../../../samples/registry';
const { Paragraph, Text, Title } = Typography;
export type ComponentSamplesLayoutProps = {
entries: SampleEntry[];
pathFilter?: string;
excludeComponentIds?: string[];
includeComponentIds?: string[];
};
export function ComponentSamplesLayout({
entries,
pathFilter = '/components/',
excludeComponentIds = [],
includeComponentIds = [],
}: ComponentSamplesLayoutProps) {
const [sampleEntries, setSampleEntries] = useState<LoadedSampleEntry[]>([]);
useEffect(() => {
let mounted = true;
void resolveSampleEntries(entries, pathFilter).then((loadedEntries) => {
if (mounted) {
setSampleEntries(loadedEntries);
}
});
return () => {
mounted = false;
};
}, [entries, pathFilter]);
const groupedComponents = useMemo(() => {
const excludedComponentIdSet = new Set(excludeComponentIds);
const includedComponentIdSet = includeComponentIds.length ? new Set(includeComponentIds) : null;
const componentMap = new Map<
string,
{
componentId: string;
title: string;
description: string;
category: string;
baseSample?: (typeof sampleEntries)[number];
pluginSamples: (typeof sampleEntries)[number][];
featureSamples: (typeof sampleEntries)[number][];
}
>();
sampleEntries.forEach((entry) => {
if (excludedComponentIdSet.has(entry.sampleMeta.componentId)) {
return;
}
if (includedComponentIdSet && !includedComponentIdSet.has(entry.sampleMeta.componentId)) {
return;
}
const existingGroup = componentMap.get(entry.sampleMeta.componentId) ?? {
componentId: entry.sampleMeta.componentId,
title: entry.sampleMeta.title,
description: entry.sampleMeta.description,
category: entry.sampleMeta.category,
pluginSamples: [],
featureSamples: [],
};
if (entry.sampleMeta.kind === 'base') {
existingGroup.baseSample = entry;
existingGroup.title = entry.sampleMeta.title;
existingGroup.description = entry.sampleMeta.description;
existingGroup.category = entry.sampleMeta.category;
} else if (entry.sampleMeta.kind === 'feature') {
existingGroup.featureSamples.push(entry);
} else {
existingGroup.pluginSamples.push(entry);
}
componentMap.set(entry.sampleMeta.componentId, existingGroup);
});
return Array.from(componentMap.values());
}, [excludeComponentIds, includeComponentIds, sampleEntries]);
if (groupedComponents.length === 0) {
return <Empty description="표시할 컴포넌트 샘플이 없습니다." />;
}
return (
<div className="component-samples-layout">
<aside className="component-samples-layout__sidebar">
<Card title="Components" className="component-samples-layout__nav-card">
<Anchor
affix={false}
items={groupedComponents.map((componentGroup) => ({
key: componentGroup.componentId,
href: `#component-sample-${componentGroup.componentId}`,
title: componentGroup.title,
}))}
/>
</Card>
</aside>
<div className="component-samples-layout__content">
<Flex vertical gap={20} className="component-samples-layout__stack">
{groupedComponents.map((componentGroup) => (
<Card
key={componentGroup.componentId}
id={`component-sample-${componentGroup.componentId}`}
data-focus-id={`component:${componentGroup.componentId}`}
title={componentGroup.title}
extra={<Tag color="blue">{componentGroup.category}</Tag>}
className="component-samples-layout__card"
>
<Space direction="vertical" size={16} className="component-samples-layout__section">
<div>
<Paragraph>{componentGroup.description}</Paragraph>
<Text type="secondary" code>
{componentGroup.componentId}
</Text>
</div>
{componentGroup.baseSample ? (
<div className="component-samples-layout__sample-block">
<Title level={5}>Base Sample</Title>
<div>{createElement(componentGroup.baseSample.Sample)}</div>
</div>
) : null}
{componentGroup.pluginSamples.length > 0 ? (
<div className="component-samples-layout__sample-block">
<Title level={5}>Plugin Samples</Title>
<Flex vertical gap={16}>
{componentGroup.pluginSamples.map(({ modulePath, Sample, sampleMeta }) => (
<Card
key={modulePath}
size="small"
data-focus-id={`component:${componentGroup.componentId}:${sampleMeta.id}`}
title={sampleMeta.title}
extra={<Text code>{sampleMeta.variantLabel ?? sampleMeta.id}</Text>}
>
<Paragraph>{sampleMeta.description}</Paragraph>
{createElement(Sample)}
</Card>
))}
</Flex>
</div>
) : null}
{componentGroup.featureSamples.length > 0 ? (
<div className="component-samples-layout__sample-block">
<Title level={5}>Feature Samples</Title>
<Flex vertical gap={16}>
{componentGroup.featureSamples.map(({ modulePath, Sample, sampleMeta }) => (
<Card
key={modulePath}
size="small"
data-focus-id={`component:${componentGroup.componentId}:${sampleMeta.id}`}
title={sampleMeta.title}
extra={<Text code>{sampleMeta.variantLabel ?? sampleMeta.id}</Text>}
>
<Paragraph>{sampleMeta.description}</Paragraph>
{createElement(Sample)}
</Card>
))}
</Flex>
</div>
) : null}
</Space>
</Card>
))}
</Flex>
</div>
</div>
);
}

View File

@@ -0,0 +1,2 @@
export { ComponentSamplesLayout } from './ComponentSamplesLayout';
export type { ComponentSamplesLayoutProps } from './ComponentSamplesLayout';

View File

@@ -0,0 +1,12 @@
import { Flex } from 'antd';
import { TmsDashboardFeatureSamples } from '../../dashboard/TmsDashboardFeatureSamples';
import { WmsDashboardFeatureSamples } from '../../dashboard/WmsDashboardFeatureSamples';
export function DashboardFeatureGalleryLayout() {
return (
<Flex vertical gap={20} className="feature-dashboard-gallery">
<WmsDashboardFeatureSamples />
<TmsDashboardFeatureSamples />
</Flex>
);
}

View File

@@ -0,0 +1 @@
export { DashboardFeatureGalleryLayout } from './DashboardFeatureGalleryLayout';

View File

@@ -0,0 +1,39 @@
import { Empty, Flex } from 'antd';
import { createElement, useEffect, useState } from 'react';
import { widgetSampleEntries } from '../../../app/manifests/samples.manifest';
import type { LoadedSampleEntry } from '../../../samples/registry';
import { resolveSampleEntries } from '../../../samples/registry';
export function DashboardReportGalleryLayout() {
const [dashboardSamples, setDashboardSamples] = useState<LoadedSampleEntry[]>([]);
useEffect(() => {
let mounted = true;
void resolveSampleEntries(widgetSampleEntries, '/widgets/').then((loadedEntries) => {
if (mounted) {
setDashboardSamples(
loadedEntries.filter((entry) => entry.sampleMeta.componentId === 'dashboard-report-card'),
);
}
});
return () => {
mounted = false;
};
}, []);
if (dashboardSamples.length === 0) {
return <Empty description="표시할 대시보드 카드 샘플이 없습니다." />;
}
return (
<Flex gap={20} wrap className="dashboard-widget-grid">
{dashboardSamples.map(({ modulePath, Sample }) => (
<div key={modulePath} className="dashboard-widget-grid__item">
{createElement(Sample)}
</div>
))}
</Flex>
);
}

View File

@@ -0,0 +1 @@
export { DashboardReportGalleryLayout } from './DashboardReportGalleryLayout';

View File

@@ -0,0 +1,87 @@
import { Card, Empty, Flex, Typography } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import { docsMarkdownEntries } from '../../../app/manifests/docs.manifest';
import { FolderTreeNav } from '../../../components/navigation';
import {
MarkdownPreviewCard,
resolveMarkdownDocuments,
type MarkdownDocument,
} from '../../../components/markdownPreview';
const { Text } = Typography;
export function DocsMarkdownPreviewLayout() {
const [documents, setDocuments] = useState<MarkdownDocument[]>([]);
useEffect(() => {
let mounted = true;
void resolveMarkdownDocuments(docsMarkdownEntries, '/docs/').then((loadedDocuments) => {
if (mounted) {
setDocuments(loadedDocuments);
}
});
return () => {
mounted = false;
};
}, []);
const grouped = useMemo(() => {
const folderMap = new Map<string, typeof documents>();
documents.forEach((document) => {
const bucket = folderMap.get(document.folder) ?? [];
bucket.push(document);
folderMap.set(document.folder, bucket);
});
return Array.from(folderMap.entries()).sort((left, right) => left[0].localeCompare(right[0]));
}, [documents]);
if (documents.length === 0) {
return <Empty description="표시할 docs markdown 문서가 없습니다." />;
}
return (
<div className="docs-markdown-layout">
<aside className="docs-markdown-layout__sidebar">
<Card title="Docs Folders" className="component-samples-layout__nav-card">
<FolderTreeNav
title="Docs Tree"
groups={grouped.map(([folder, folderDocuments]) => ({
id: `docs-folder-${folder}`,
label: folder,
items: folderDocuments.map((document) => ({
id: document.id,
label: document.title,
href: `#doc-item-${document.id}`,
})),
}))}
/>
</Card>
</aside>
<div className="docs-markdown-layout__content">
<Flex vertical gap={20}>
{grouped.map(([folder, folderDocuments]) => (
<section key={folder} id={`docs-folder-${folder}`}>
<Card title={folder} extra={<Text code>{folderDocuments.length} docs</Text>}>
<Flex vertical gap={16}>
{folderDocuments.map((document) => (
<div
key={document.id}
id={`doc-item-${document.id}`}
className="feature-markdown-list__item"
>
<MarkdownPreviewCard document={document} />
</div>
))}
</Flex>
</Card>
</section>
))}
</Flex>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { DocsMarkdownPreviewLayout } from './DocsMarkdownPreviewLayout';

View File

@@ -0,0 +1,12 @@
import { featureMarkdownEntries } from '../../../app/manifests/docs.manifest';
import { MarkdownPreviewList } from '../../../components/markdownPreview';
export function FeatureMarkdownPreviewListLayout() {
return (
<MarkdownPreviewList
entries={featureMarkdownEntries}
basePath="/features/"
emptyDescription="표시할 feature markdown 문서가 없습니다."
/>
);
}

View File

@@ -0,0 +1 @@
export { FeatureMarkdownPreviewListLayout } from './FeatureMarkdownPreviewListLayout';

View File

@@ -0,0 +1,33 @@
import { Card, Empty, Flex, List, Tag, Typography } from 'antd';
import { registeredWidgets } from '../../../widgets/registry';
import { resolveWidgetFeatures } from '../../../widgets/core';
const { Paragraph, Text } = Typography;
export function WidgetRegistryLayout() {
if (registeredWidgets.length === 0) {
return <Empty description="등록된 위젯이 없습니다." />;
}
return (
<Flex gap={20} wrap className="sample-widgets-layout">
{registeredWidgets.map((widget) => (
<div key={widget.id} className="sample-widgets-layout__item">
<Card title={widget.title} extra={<Text code>{widget.id}</Text>}>
<Paragraph>{widget.description}</Paragraph>
<List
size="small"
dataSource={resolveWidgetFeatures(widget.features)}
renderItem={(feature) => (
<List.Item>
<Tag color="blue">{feature.label}</Tag>
<span>{feature.description}</span>
</List.Item>
)}
/>
</Card>
</div>
))}
</Flex>
);
}

View File

@@ -0,0 +1 @@
export { WidgetRegistryLayout } from './WidgetRegistryLayout';

View File

@@ -0,0 +1,56 @@
import { Empty, Flex } from 'antd';
import { createElement, useEffect, useState } from 'react';
import type { LoadedSampleEntry, SampleEntry } from '../../../samples/registry';
import { resolveSampleEntries } from '../../../samples/registry';
export type SampleWidgetsLayoutProps = {
entries: SampleEntry[];
pathFilter?: string;
includeComponentIds?: string[];
};
export function SampleWidgetsLayout({
entries,
pathFilter = '/widgets/',
includeComponentIds = [],
}: SampleWidgetsLayoutProps) {
const [sampleEntries, setSampleEntries] = useState<LoadedSampleEntry[]>([]);
useEffect(() => {
let mounted = true;
void resolveSampleEntries(entries, pathFilter).then((loadedEntries) => {
if (mounted) {
setSampleEntries(loadedEntries);
}
});
return () => {
mounted = false;
};
}, [entries, pathFilter]);
const visibleEntries =
includeComponentIds.length > 0
? sampleEntries.filter((entry) => includeComponentIds.includes(entry.sampleMeta.componentId))
: sampleEntries;
if (visibleEntries.length === 0) {
return <Empty description="표시할 위젯 샘플이 없습니다." />;
}
return (
<Flex gap={20} wrap className="sample-widgets-layout">
{visibleEntries.map(({ modulePath, Sample, sampleMeta }) => (
<div
key={modulePath}
id={`widget-sample-${sampleMeta.componentId}`}
className="sample-widgets-layout__item"
data-focus-id={`widget:${sampleMeta.componentId}`}
>
<div className="sample-widgets-layout__item">{createElement(Sample)}</div>
</div>
))}
</Flex>
);
}

View File

@@ -0,0 +1,2 @@
export { SampleWidgetsLayout } from './SampleWidgetsLayout';
export type { SampleWidgetsLayoutProps } from './SampleWidgetsLayout';