feat(WEB): 컴포넌트 레지스트리 UI 개선 및 middleware 업데이트
- ComponentRegistryClient 기능 확장 및 UI 개선 - middleware 라우팅 로직 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -258,14 +258,16 @@ function CategorySection({
|
||||
tier,
|
||||
expandedCard,
|
||||
onCardToggle,
|
||||
defaultExpanded = true,
|
||||
}: {
|
||||
category: string;
|
||||
components: ComponentEntry[];
|
||||
tier: string;
|
||||
expandedCard: string | null;
|
||||
onCardToggle: (filePath: string) => void;
|
||||
defaultExpanded?: boolean;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
// Group by subcategory
|
||||
const groups = useMemo(() => {
|
||||
@@ -330,12 +332,35 @@ function CategorySection({
|
||||
export default function ComponentRegistryClient({ registry }: ComponentRegistryClientProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeTier, setActiveTier] = useState('전체');
|
||||
const [activeDomainCategory, setActiveDomainCategory] = useState('전체');
|
||||
const [expandedCard, setExpandedCard] = useState<string | null>(null);
|
||||
const [isAllExpanded, setIsAllExpanded] = useState(true);
|
||||
const [globalExpandKey, setGlobalExpandKey] = useState(0);
|
||||
|
||||
const handleCardToggle = useCallback((filePath: string) => {
|
||||
setExpandedCard((prev) => (prev === filePath ? null : filePath));
|
||||
}, []);
|
||||
|
||||
const handleTierChange = useCallback((tier: string) => {
|
||||
setActiveTier(tier);
|
||||
if (tier !== 'domain') setActiveDomainCategory('전체');
|
||||
setIsAllExpanded(tier !== 'domain');
|
||||
setGlobalExpandKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const handleDomainCategoryChange = useCallback((cat: string) => {
|
||||
setActiveDomainCategory(cat);
|
||||
if (cat !== '전체') {
|
||||
setIsAllExpanded(true);
|
||||
setGlobalExpandKey(prev => prev + 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleAll = useCallback(() => {
|
||||
setIsAllExpanded(prev => !prev);
|
||||
setGlobalExpandKey(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let comps = registry.components;
|
||||
|
||||
@@ -343,6 +368,10 @@ export default function ComponentRegistryClient({ registry }: ComponentRegistryC
|
||||
comps = comps.filter((c) => c.tier === activeTier);
|
||||
}
|
||||
|
||||
if (activeTier === 'domain' && activeDomainCategory !== '전체') {
|
||||
comps = comps.filter((c) => c.category === activeDomainCategory);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
const q = searchTerm.toLowerCase();
|
||||
comps = comps.filter(
|
||||
@@ -355,7 +384,7 @@ export default function ComponentRegistryClient({ registry }: ComponentRegistryC
|
||||
}
|
||||
|
||||
return comps;
|
||||
}, [registry.components, activeTier, searchTerm]);
|
||||
}, [registry.components, activeTier, activeDomainCategory, searchTerm]);
|
||||
|
||||
// Group filtered components by category
|
||||
const groupedByCategory = useMemo(() => {
|
||||
@@ -382,6 +411,19 @@ export default function ComponentRegistryClient({ registry }: ComponentRegistryC
|
||||
return counts;
|
||||
}, [registry.components]);
|
||||
|
||||
// Domain sub-categories for sub-filter chips
|
||||
const domainCategories = useMemo(() => {
|
||||
const cats = new Map<string, number>();
|
||||
for (const comp of registry.components) {
|
||||
if (comp.tier === 'domain') {
|
||||
cats.set(comp.category, (cats.get(comp.category) || 0) + 1);
|
||||
}
|
||||
}
|
||||
return [...cats.entries()]
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([name, count]) => ({ name, count }));
|
||||
}, [registry.components]);
|
||||
|
||||
// Count of previewable components (all tiers)
|
||||
const previewCount = useMemo(() => {
|
||||
return registry.components.filter(
|
||||
@@ -441,7 +483,7 @@ export default function ComponentRegistryClient({ registry }: ComponentRegistryC
|
||||
return (
|
||||
<button
|
||||
key={tier}
|
||||
onClick={() => setActiveTier(tier)}
|
||||
onClick={() => handleTierChange(tier)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-500 text-white'
|
||||
@@ -457,23 +499,69 @@ export default function ComponentRegistryClient({ registry }: ComponentRegistryC
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Results count */}
|
||||
{(searchTerm || activeTier !== '전체') && (
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
{filtered.length}개 결과
|
||||
</p>
|
||||
{/* Domain Sub-filter Chips */}
|
||||
{activeTier === 'domain' && domainCategories.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-4">
|
||||
<button
|
||||
onClick={() => handleDomainCategoryChange('전체')}
|
||||
className={`px-2.5 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||
activeDomainCategory === '전체'
|
||||
? 'bg-gray-700 text-white'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
전체
|
||||
<span className={`ml-1 ${activeDomainCategory === '전체' ? 'text-gray-300' : 'text-gray-400'}`}>
|
||||
{tierCounts['domain'] || 0}
|
||||
</span>
|
||||
</button>
|
||||
{domainCategories.map(({ name, count }) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => handleDomainCategoryChange(name)}
|
||||
className={`px-2.5 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||
activeDomainCategory === name
|
||||
? 'bg-gray-700 text-white'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
<span className={`ml-1 ${activeDomainCategory === name ? 'text-gray-300' : 'text-gray-400'}`}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results count & Expand/Collapse All */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{(searchTerm || activeTier !== '전체') ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
{filtered.length}개 결과
|
||||
</p>
|
||||
) : <div />}
|
||||
{groupedByCategory.length > 1 && (
|
||||
<button
|
||||
onClick={handleToggleAll}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
{isAllExpanded ? '전체 접기' : '전체 펼치기'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Component List */}
|
||||
<div className="space-y-2">
|
||||
{groupedByCategory.map(([category, { tier, components }]) => (
|
||||
<CategorySection
|
||||
key={category}
|
||||
key={`${category}-${globalExpandKey}`}
|
||||
category={category}
|
||||
tier={tier}
|
||||
components={components}
|
||||
expandedCard={expandedCard}
|
||||
onCardToggle={handleCardToggle}
|
||||
defaultExpanded={isAllExpanded}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -218,6 +218,13 @@ export async function middleware(request: NextRequest) {
|
||||
// 1️⃣ 로케일 제거
|
||||
const pathnameWithoutLocale = getPathnameWithoutLocale(pathname);
|
||||
|
||||
// 1.5️⃣ 프로덕션 환경에서 /dev/ 경로 차단
|
||||
if (process.env.NODE_ENV === 'production' && (
|
||||
pathnameWithoutLocale.startsWith('/dev/') || pathnameWithoutLocale === '/dev'
|
||||
)) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
// 2️⃣ Bot Detection (기존 로직)
|
||||
const isBotRequest = isBot(userAgent);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user