From 2b8a19b4af89bd33487ad9acb03b43f434b04cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Thu, 12 Feb 2026 14:59:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A0=88=EC=A7=80=EC=8A=A4=ED=8A=B8=EB=A6=AC=20UI?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20middleware=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ComponentRegistryClient 기능 확장 및 UI 개선 - middleware 라우팅 로직 수정 Co-Authored-By: Claude Opus 4.6 --- .../ComponentRegistryClient.tsx | 106 ++++++++++++++++-- src/middleware.ts | 7 ++ 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx b/src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx index 2d76ee40..0a773fa6 100644 --- a/src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx +++ b/src/app/[locale]/(protected)/dev/component-registry/ComponentRegistryClient.tsx @@ -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(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(); + 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 ( + {domainCategories.map(({ name, count }) => ( + + ))} + )} + {/* Results count & Expand/Collapse All */} +
+ {(searchTerm || activeTier !== '전체') ? ( +

+ {filtered.length}개 결과 +

+ ) :
} + {groupedByCategory.length > 1 && ( + + )} +
+ {/* Component List */}
{groupedByCategory.map(([category, { tier, components }]) => ( ))}
diff --git a/src/middleware.ts b/src/middleware.ts index 821d5ee3..b8affb2d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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);