feat(WEB): 글로벌 검색, 토큰 갱신 개선, 템플릿 기능 확장

- CommandMenuSearch 컴포넌트 추가 (Cmd+K 글로벌 메뉴 검색)
- AuthenticatedLayout: 검색 통합, 모바일/데스크톱 스켈레톤 분리
- middleware: 토큰 갱신 후 리다이렉트 방식으로 변경 (race condition 방지)
- IntegratedDetailTemplate: stickyButtons 옵션 추가 (하단 고정 버튼)
- UniversalListPage: 컬럼 정렬 기능 추가 (sortBy, sortOrder)
- Sidebar: 축소 모드 패딩/간격 최적화
- 각종 컴포넌트 버그 수정 및 경로 정규화

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-26 15:07:10 +09:00
parent cd060ec562
commit a15132d75d
38 changed files with 927 additions and 443 deletions

View File

@@ -13,7 +13,9 @@ import {
XCircle,
RotateCcw,
Loader2,
Plus,
} from 'lucide-react';
import { useMenuStore } from '@/store/menuStore';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -125,6 +127,7 @@ const PERMISSION_LABELS_MAP: Record<PermissionType, string> = {
export function PermissionDetailClient({ permissionId, isNew = false, mode = 'view' }: PermissionDetailClientProps) {
const router = useRouter();
const sidebarCollapsed = useMenuStore((state) => state.sidebarCollapsed);
// 역할 데이터
const [role, setRole] = useState<Role | null>(null);
@@ -481,50 +484,9 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
title={isNew ? '역할 등록' : mode === 'edit' ? '역할 수정' : '역할 상세'}
description={isNew ? '새 역할을 등록합니다' : mode === 'edit' ? '역할 정보를 수정합니다' : '역할 정보와 권한을 관리합니다'}
icon={Shield}
actions={
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
}
/>
{/* 저장/삭제 버튼 */}
<div className="flex justify-end gap-2">
{isNew ? (
<Button onClick={handleSaveNew} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
) : (
<>
<Button variant="outline" onClick={handleUpdateRole} disabled={isSaving}>
{isSaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
<Button variant="destructive" onClick={handleDelete}>
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</>
)}
</div>
<div className="space-y-6 pb-24">
{/* 기본 정보 */}
<Card>
<CardContent className="p-6">
@@ -658,6 +620,53 @@ export function PermissionDetailClient({ permissionId, isNew = false, mode = 'vi
</CardContent>
</Card>
)}
</div>
{/* 하단 액션 버튼 (sticky) */}
<div className={`fixed bottom-6 ${sidebarCollapsed ? 'left-[156px]' : 'left-[316px]'} right-[48px] px-6 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 flex items-center justify-between`}>
<Button variant="outline" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
<div className="flex items-center gap-2">
{isNew ? (
<Button onClick={handleSaveNew} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
</>
)}
</Button>
) : (
<>
<Button variant="outline" onClick={handleUpdateRole} disabled={isSaving}>
{isSaving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Save className="h-4 w-4 mr-2" />
</>
)}
</Button>
<Button
variant="outline"
onClick={handleDelete}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
>
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</>
)}
</div>
</div>
{/* 삭제 확인 다이얼로그 */}
{!isNew && role && (