UI/UX 개선: 사이드바 레이아웃 안정화 및 메뉴 hover 효과 개선

- 사이드바 고정 너비 설정으로 메뉴 클릭 시 너비 변화 방지
- 메뉴 항목 hover 효과 일관성 개선 (고정 높이, 부드러운 색상 전환)
- 디버깅 로그 제거로 성능 최적화
- 관리자 페이지 카드 디자인 개선 (그라데이션 배경, 아이콘 색상 조정)
This commit is contained in:
leeheejin 2025-09-25 09:29:56 +09:00
parent 1a60177fe4
commit e3cd6dc3a0
13 changed files with 307 additions and 269 deletions

View File

@ -14,7 +14,7 @@ export default function CommonCodeManagementPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
@ -26,8 +26,8 @@ export default function CommonCodeManagementPage() {
<div className="flex flex-col gap-6 lg:flex-row lg:gap-8">
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
<div className="w-full lg:w-80 lg:flex-shrink-0">
<Card className="h-full">
<CardHeader>
<Card className="h-full shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">📂 </CardTitle>
</CardHeader>
<CardContent className="p-0">
@ -38,8 +38,8 @@ export default function CommonCodeManagementPage() {
{/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
<div className="min-w-0 flex-1">
<Card className="h-fit">
<CardHeader>
<Card className="h-fit shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">
📋
{selectedCategoryCode && (

View File

@ -8,7 +8,7 @@ export default function CompanyPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>

View File

@ -79,13 +79,13 @@ export default function DataFlowPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
{currentStep !== "list" && (
<Button variant="outline" onClick={goToPreviousStep} className="flex items-center">
<Button variant="outline" onClick={goToPreviousStep} className="flex items-center shadow-sm">
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
@ -97,7 +97,7 @@ export default function DataFlowPage() {
{/* 관계도 목록 단계 */}
{currentStep === "list" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
</div>
<DataFlowList onDesignDiagram={handleDesignDiagram} />
@ -107,7 +107,7 @@ export default function DataFlowPage() {
{/* 관계도 설계 단계 */}
{currentStep === "design" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
</div>
<DataFlowDesigner

View File

@ -223,7 +223,7 @@ export default function ExternalConnectionsPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
@ -231,7 +231,7 @@ export default function ExternalConnectionsPage() {
</div>
{/* 검색 및 필터 */}
<Card className="mb-6">
<Card className="mb-6 shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-col gap-4 md:flex-row md:items-center">
@ -289,7 +289,7 @@ export default function ExternalConnectionsPage() {
<div className="text-gray-500"> ...</div>
</div>
) : connections.length === 0 ? (
<Card>
<Card className="shadow-sm">
<CardContent className="pt-6">
<div className="py-8 text-center text-gray-500">
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
@ -302,7 +302,7 @@ export default function ExternalConnectionsPage() {
</CardContent>
</Card>
) : (
<Card>
<Card className="shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>

View File

@ -223,18 +223,18 @@ export default function LayoutManagementPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
<Button className="flex items-center gap-2" onClick={() => setCreateModalOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<Button className="flex items-center gap-2 shadow-sm" onClick={() => setCreateModalOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 검색 및 필터 */}
<Card className="mb-6">
<Card className="mb-6 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="flex-1">
@ -284,7 +284,7 @@ export default function LayoutManagementPage() {
{layouts.map((layout) => {
const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS];
return (
<Card key={layout.layoutCode} className="transition-shadow hover:shadow-lg">
<Card key={layout.layoutCode} className="shadow-sm transition-shadow hover:shadow-lg">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">

View File

@ -7,7 +7,7 @@ export default function MenuPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>

View File

@ -69,7 +69,7 @@ export default function ScreenManagementPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> 릿 </p>
@ -81,12 +81,12 @@ export default function ScreenManagementPage() {
{/* 화면 목록 단계 */}
{currentStep === "list" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToNextStep("design")}>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToNextStep("design")}>
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
<ScreenList
onScreenSelect={setSelectedScreen}
selectedScreen={selectedScreen}
@ -101,9 +101,9 @@ export default function ScreenManagementPage() {
{/* 화면 설계 단계 */}
{currentStep === "design" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
<Button variant="outline" onClick={() => goToStep("list")}>
<Button variant="outline" className="shadow-sm" onClick={() => goToStep("list")}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
</div>
@ -114,18 +114,18 @@ export default function ScreenManagementPage() {
{/* 템플릿 관리 단계 */}
{currentStep === "template" && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.template.title}</h2>
<div className="flex gap-2">
<Button variant="outline" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => goToStep("list")}>
</Button>
<div className="flex gap-2">
<Button variant="outline" className="shadow-sm" onClick={goToPreviousStep}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 shadow-sm" onClick={() => goToStep("list")}>
</Button>
</div>
</div>
</div>
<TemplateManager selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
</div>
)}

View File

@ -130,44 +130,44 @@ export default function WebTypesManagePage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>
</div>
<Link href="/admin/standards/new">
<Button>
<Button className="shadow-sm">
<Plus className="mr-2 h-4 w-4" />
</Button>
</Link>
</div>
{/* 필터 및 검색 */}
<Card className="mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Filter className="h-5 w-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{/* 검색 */}
<div className="relative">
<Search className="text-muted-foreground absolute top-3 left-3 h-4 w-4" />
<Input
placeholder="웹타입명, 설명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 필터 및 검색 */}
<Card className="shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2 text-lg">
<Filter className="h-5 w-5 text-gray-600" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 검색 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="웹타입명, 설명 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 카테고리 필터 */}
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger>
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
{/* 카테고리 필터 */}
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger>
<SelectValue placeholder="카테고리 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{categories.map((category) => (
@ -178,96 +178,96 @@ export default function WebTypesManagePage() {
</SelectContent>
</Select>
{/* 활성화 상태 필터 */}
<Select value={activeFilter} onValueChange={setActiveFilter}>
<SelectTrigger>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="Y"></SelectItem>
<SelectItem value="N"></SelectItem>
</SelectContent>
</Select>
{/* 활성화 상태 필터 */}
<Select value={activeFilter} onValueChange={setActiveFilter}>
<SelectTrigger>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="Y"></SelectItem>
<SelectItem value="N"></SelectItem>
</SelectContent>
</Select>
{/* 초기화 버튼 */}
<Button variant="outline" onClick={resetFilters}>
<RotateCcw className="mr-2 h-4 w-4" />
</Button>
{/* 초기화 버튼 */}
<Button variant="outline" onClick={resetFilters}>
<RotateCcw className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 결과 통계 */}
<div className="mb-4">
<p className="text-muted-foreground text-sm"> {filteredAndSortedWebTypes.length} .</p>
</div>
{/* 결과 통계 */}
<div className="bg-white rounded-lg border px-4 py-3">
<p className="text-gray-700 text-sm font-medium"> {filteredAndSortedWebTypes.length} .</p>
</div>
{/* 웹타입 목록 테이블 */}
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
<div className="flex items-center gap-2">
{sortField === "sort_order" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
<div className="flex items-center gap-2">
{sortField === "web_type" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
<div className="flex items-center gap-2">
{sortField === "type_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
<div className="flex items-center gap-2">
{sortField === "category" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead></TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
<div className="flex items-center gap-2">
{sortField === "component_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
<div className="flex items-center gap-2">
{sortField === "config_panel" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
<div className="flex items-center gap-2">
{sortField === "is_active" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
<div className="flex items-center gap-2">
{sortField === "updated_date" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="text-center"></TableHead>
{/* 웹타입 목록 테이블 */}
<Card className="shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("sort_order")}>
<div className="flex items-center gap-2">
{sortField === "sort_order" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("web_type")}>
<div className="flex items-center gap-2">
{sortField === "web_type" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("type_name")}>
<div className="flex items-center gap-2">
{sortField === "type_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("category")}>
<div className="flex items-center gap-2">
{sortField === "category" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead></TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("component_name")}>
<div className="flex items-center gap-2">
{sortField === "component_name" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("config_panel")}>
<div className="flex items-center gap-2">
{sortField === "config_panel" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("is_active")}>
<div className="flex items-center gap-2">
{sortField === "is_active" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="hover:bg-muted/50 cursor-pointer" onClick={() => handleSort("updated_date")}>
<div className="flex items-center gap-2">
{sortField === "updated_date" &&
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
</div>
</TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -310,24 +310,24 @@ export default function WebTypesManagePage() {
<TableCell className="text-muted-foreground text-sm">
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Link href={`/admin/standards/${webType.web_type}`}>
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/admin/standards/${webType.web_type}/edit`}>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
</Link>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</AlertDialogTrigger>
<TableCell>
<div className="flex items-center gap-2">
<Link href={`/admin/standards/${webType.web_type}`}>
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/admin/standards/${webType.web_type}/edit`}>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
</Link>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>

View File

@ -543,7 +543,7 @@ export default function TableManagementPage() {
return (
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
@ -593,10 +593,10 @@ export default function TableManagementPage() {
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* 테이블 목록 */}
<Card className="lg:col-span-1">
<CardHeader>
<Card className="lg:col-span-1 shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
<Database className="h-5 w-5 text-gray-600" />
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
</CardTitle>
</CardHeader>
@ -663,10 +663,10 @@ export default function TableManagementPage() {
</Card>
{/* 컬럼 타입 관리 */}
<Card className="lg:col-span-4">
<CardHeader>
<Card className="lg:col-span-4 shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5" />
<Settings className="h-5 w-5 text-gray-600" />
{selectedTable ? <> - {selectedTable}</> : "테이블 타입 관리"}
</CardTitle>
</CardHeader>

View File

@ -148,25 +148,25 @@ export default function TemplatesManagePage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">릿 </h1>
<p className="mt-2 text-gray-600"> 릿 </p>
</div>
<div className="flex space-x-2">
<Button asChild>
<Link href="/admin/templates/new">
<Plus className="mr-2 h-4 w-4" /> 릿
</Link>
</Button>
<div className="flex space-x-2">
<Button asChild className="shadow-sm">
<Link href="/admin/templates/new">
<Plus className="mr-2 h-4 w-4" /> 릿
</Link>
</Button>
</div>
</div>
</div>
{/* 필터 및 검색 */}
<Card>
<CardHeader>
<Card className="shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center">
<Filter className="mr-2 h-5 w-5" />
<Filter className="mr-2 h-5 w-5 text-gray-600" />
</CardTitle>
</CardHeader>
@ -231,8 +231,8 @@ export default function TemplatesManagePage() {
</Card>
{/* 템플릿 목록 테이블 */}
<Card>
<CardHeader>
<Card className="shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle>릿 ({filteredAndSortedTemplates.length})</CardTitle>
</CardHeader>
<CardContent>

View File

@ -11,7 +11,7 @@ export default function UserMngPage() {
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto p-6 space-y-6">
{/* 페이지 제목 */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"> </h1>
<p className="mt-2 text-gray-600"> </p>

View File

@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => {
{/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
<div className="w-[20%] border-r bg-gray-50">
<div className="p-6">
<h2 className="mb-4 text-lg font-semibold">{getUITextSync("menu.type.title")}</h2>
<div className="space-y-3">
<Card className="shadow-sm">
<CardHeader className="bg-gray-50/50 pb-3">
<CardTitle className="text-lg">{getUITextSync("menu.type.title")}</CardTitle>
</CardHeader>
<CardContent className="space-y-3 pt-4">
<Card
className={`cursor-pointer transition-all ${
selectedMenuType === "admin" ? "border-blue-500 bg-blue-50" : "hover:border-gray-300"
@ -864,21 +867,23 @@ export const MenuManagement: React.FC = () => {
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</div>
</div>
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
<div className="w-[80%] overflow-hidden">
<div className="flex h-full flex-col p-6">
<div className="mb-6 flex-shrink-0">
<h2 className="mb-2 text-xl font-semibold">
{getMenuTypeString()} {getUITextSync("menu.list.title")}
</h2>
</div>
{/* 검색 및 필터 영역 */}
<div className="mb-4 flex-shrink-0">
<Card className="flex-1 shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="text-xl">
{getMenuTypeString()} {getUITextSync("menu.list.title")}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-hidden">
{/* 검색 및 필터 영역 */}
<div className="mb-4 flex-shrink-0">
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<Label htmlFor="company">{getUITextSync("filter.company")}</Label>
@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
</div>
</div>
</div>
</div>
</div>
<div className="flex-1 overflow-hidden">
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600">
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
{getUITextSync("button.add.top.level")}
</Button>
{selectedMenus.size > 0 && (
<Button
variant="destructive"
onClick={handleDeleteSelectedMenus}
disabled={deleting}
className="min-w-[120px]"
>
{deleting ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
{getUITextSync("button.delete.processing")}
</>
) : (
getUITextSync("button.delete.selected.count", {
count: selectedMenus.size,
})
<div className="flex-1 overflow-hidden">
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600">
{getUITextSync("menu.list.total", { count: getCurrentMenus().length })}
</div>
<div className="flex space-x-2">
<Button variant="outline" onClick={() => handleAddTopLevelMenu()} className="min-w-[100px]">
{getUITextSync("button.add.top.level")}
</Button>
{selectedMenus.size > 0 && (
<Button
variant="destructive"
onClick={handleDeleteSelectedMenus}
disabled={deleting}
className="min-w-[120px]"
>
{deleting ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
{getUITextSync("button.delete.processing")}
</>
) : (
getUITextSync("button.delete.selected.count", {
count: selectedMenus.size,
})
)}
</Button>
)}
</Button>
)}
</div>
</div>
<MenuTable
menus={getCurrentMenus()}
title=""
onAddMenu={handleAddMenu}
onEditMenu={handleEditMenu}
onToggleStatus={handleToggleStatus}
selectedMenus={selectedMenus}
onMenuSelectionChange={handleMenuSelectionChange}
onSelectAllMenus={handleSelectAllMenus}
expandedMenus={expandedMenus}
onToggleExpand={handleToggleExpand}
uiTexts={uiTexts}
/>
</div>
</div>
<MenuTable
menus={getCurrentMenus()}
title=""
onAddMenu={handleAddMenu}
onEditMenu={handleEditMenu}
onToggleStatus={handleToggleStatus}
selectedMenus={selectedMenus}
onMenuSelectionChange={handleMenuSelectionChange}
onSelectAllMenus={handleSelectAllMenus}
expandedMenus={expandedMenus}
onToggleExpand={handleToggleExpand}
uiTexts={uiTexts}
/>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => {
</TabsContent>
{/* 화면 할당 탭 */}
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden">
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
<TabsContent value="screen-assignment" className="flex-1 overflow-hidden p-6">
<Card className="h-full shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle> </CardTitle>
</CardHeader>
<CardContent className="h-full overflow-hidden">
<ScreenAssignmentTab menus={[...adminMenus, ...userMenus]} />
</CardContent>
</Card>
</TabsContent>
</Tabs>

View File

@ -1,6 +1,6 @@
"use client";
import { useState, Suspense } from "react";
import { useState, Suspense, useEffect } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) {
const searchParams = useSearchParams();
const { user, logout, refreshUserData } = useAuth();
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [expandedMenus, setExpandedMenus] = useState<Set<string>>(new Set());
const [isMobile, setIsMobile] = useState(false);
// 화면 크기 감지 및 사이드바 초기 상태 설정
useEffect(() => {
const checkIsMobile = () => {
const mobile = window.innerWidth < 1024; // lg 브레이크포인트
setIsMobile(mobile);
// 모바일에서만 사이드바를 닫음
if (mobile) {
setSidebarOpen(false);
} else {
setSidebarOpen(true);
}
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
// 프로필 관련 로직
const {
@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
? `/screens/${firstScreen.screenId}?mode=admin`
: `/screens/${firstScreen.screenId}`;
console.log("🎯 메뉴에서 화면으로 이동:", {
menuName: menu.name,
screenId: firstScreen.screenId,
isAdminMode,
targetPath: screenPath,
});
router.push(screenPath);
setSidebarOpen(false);
if (isMobile) {
setSidebarOpen(false);
}
return;
}
} catch (error) {
@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
// 할당된 화면이 없고 URL이 있으면 기존 URL로 이동
if (menu.url && menu.url !== "#") {
router.push(menu.url);
setSidebarOpen(false);
if (isMobile) {
setSidebarOpen(false);
}
} else {
// URL도 없고 할당된 화면도 없으면 경고 메시지
console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu);
toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다.");
}
}
@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
await logout();
router.push("/login");
} catch (error) {
console.error("로그아웃 실패:", error);
// 로그아웃 실패 시 처리
}
};
@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
return (
<div key={menu.id}>
<div
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors hover:cursor-pointer ${
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-colors duration-200 ease-in-out h-10 ${
pathname === menu.url
? "bg-gradient-to-br from-slate-100 to-blue-100/40 text-slate-900 border-l-4 border-blue-500"
: isExpanded
@ -315,9 +330,9 @@ function AppLayoutInner({ children }: AppLayoutProps) {
} ${level > 0 ? "ml-6" : ""}`}
onClick={() => handleMenuClick(menu)}
>
<div className="flex items-center">
<div className="flex items-center min-w-0 flex-1">
{menu.icon}
<span className="ml-3">{menu.name}</span>
<span className="ml-3 truncate" title={menu.name}>{menu.name}</span>
</div>
{menu.hasChildren && (
<div className="ml-auto">
@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) {
}`}
onClick={() => handleMenuClick(child)}
>
{child.icon}
<span className="ml-3">{child.name}</span>
<div className="flex items-center min-w-0 flex-1">
{child.icon}
<span className="ml-3 truncate" title={child.name}>{child.name}</span>
</div>
</div>
))}
</div>
@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) {
{/* MainHeader 컴포넌트 사용 */}
<MainHeader
user={user}
onSidebarToggle={() => setSidebarOpen(!sidebarOpen)}
onSidebarToggle={() => {
// 모바일에서만 토글 동작
if (isMobile) {
setSidebarOpen(!sidebarOpen);
}
}}
onProfileClick={openProfileModal}
onLogout={handleLogout}
/>
<div className="flex flex-1">
{/* 모바일 사이드바 오버레이 */}
{sidebarOpen && (
{sidebarOpen && isMobile && (
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setSidebarOpen(false)} />
)}
{/* 왼쪽 사이드바 */}
<aside
className={`${
sidebarOpen ? "translate-x-0" : "-translate-x-full"
} fixed top-14 left-0 z-40 flex h-full w-64 flex-col border-r border-slate-200 bg-white transition-transform duration-300 lg:relative lg:top-0 lg:z-auto lg:h-full lg:translate-x-0 lg:transform-none`}
isMobile
? (sidebarOpen ? "translate-x-0" : "-translate-x-full") + " fixed top-14 left-0 z-40"
: "translate-x-0 relative top-0 z-auto"
} flex h-full w-72 min-w-72 max-w-72 flex-col border-r border-slate-200 bg-white transition-transform duration-300`}
>
{/* 사이드바 상단 - Admin/User 모드 전환 버튼 (관리자만) */}
{(user as ExtendedUserInfo)?.userType === "admin" && (
@ -428,7 +452,7 @@ function AppLayoutInner({ children }: AppLayoutProps) {
</aside>
{/* 가운데 컨텐츠 영역 */}
<main className="flex-1 bg-white">{children}</main>
<main className="flex-1 min-w-0 bg-white overflow-hidden">{children}</main>
</div>
{/* 프로필 수정 모달 */}