From 2906825c330bae5e2e0d1bfaf51747a94cdcec03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 6 Mar 2026 14:34:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[sidebar]=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=A9=94=EB=89=B4=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MenuFavorite 모델 생성 (menu_favorites 테이블) - SidebarMenuService에 즐겨찾기 CRUD 메서드 추가 - MenuFavoriteController 생성 (toggle/reorder API) - 사이드바 상단에 즐겨찾기 섹션 표시 - 메뉴 아이템에 별 아이콘 추가 (hover 시 표시, 토글) - 최대 10개 제한, 리프 메뉴만 대상 --- .../Api/MenuFavoriteController.php | 44 +++++++++ app/Models/Commons/MenuFavorite.php | 36 +++++++ app/Providers/AppServiceProvider.php | 2 + app/Services/SidebarMenuService.php | 95 +++++++++++++++++++ .../sidebar/favorites-section.blade.php | 65 +++++++++++++ .../components/sidebar/menu-item.blade.php | 15 ++- resources/views/partials/sidebar.blade.php | 34 +++++++ routes/web.php | 6 ++ 8 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/MenuFavoriteController.php create mode 100644 app/Models/Commons/MenuFavorite.php create mode 100644 resources/views/components/sidebar/favorites-section.blade.php diff --git a/app/Http/Controllers/Api/MenuFavoriteController.php b/app/Http/Controllers/Api/MenuFavoriteController.php new file mode 100644 index 00000000..c9d1074c --- /dev/null +++ b/app/Http/Controllers/Api/MenuFavoriteController.php @@ -0,0 +1,44 @@ +validate([ + 'menu_id' => 'required|integer|exists:menus,id', + ]); + + $result = $this->sidebarMenuService->toggleFavorite( + auth()->id(), + $request->integer('menu_id') + ); + + return response()->json($result); + } + + public function reorder(Request $request): JsonResponse + { + $request->validate([ + 'menu_ids' => 'required|array', + 'menu_ids.*' => 'integer', + ]); + + $this->sidebarMenuService->reorderFavorites( + auth()->id(), + $request->input('menu_ids') + ); + + return response()->json(['success' => true]); + } +} diff --git a/app/Models/Commons/MenuFavorite.php b/app/Models/Commons/MenuFavorite.php new file mode 100644 index 00000000..1c5f148a --- /dev/null +++ b/app/Models/Commons/MenuFavorite.php @@ -0,0 +1,36 @@ + 'integer', + ]; + + public function menu(): BelongsTo + { + return $this->belongsTo(Menu::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function scopeForUser($query, int $userId) + { + return $query->where('user_id', $userId)->orderBy('sort_order'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b9d84580..2d148c76 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -60,6 +60,8 @@ public function boot(): void 'mainMenus' => $menusBySection['main'], 'toolsMenus' => $menusBySection['tools'], 'labsMenus' => $menusBySection['labs'], + 'favoriteMenus' => $menuService->getFavoriteMenus(), + 'favoriteMenuIds' => $menuService->getFavoriteMenuIds(), ]); }); } diff --git a/app/Services/SidebarMenuService.php b/app/Services/SidebarMenuService.php index 8c7362f0..0b51a208 100644 --- a/app/Services/SidebarMenuService.php +++ b/app/Services/SidebarMenuService.php @@ -4,6 +4,7 @@ use App\Models\Boards\Board; use App\Models\Commons\Menu; +use App\Models\Commons\MenuFavorite; use App\Models\Tenants\Department; use App\Models\User; use Illuminate\Support\Collection; @@ -297,6 +298,100 @@ private static function hasMoreSpecificPrefixMenu(string $currentPath, string $m return $result; } + // ─── 즐겨찾기 기능 ─── + + private const MAX_FAVORITES = 10; + + /** + * 사용자의 즐겨찾기 메뉴 목록 조회 + */ + public function getFavoriteMenus(?int $userId = null): Collection + { + $userId = $userId ?? auth()->id(); + if (! $userId) { + return collect(); + } + + $tenantId = auth()->user()?->tenant_id ?? 1; + + return MenuFavorite::where('tenant_id', $tenantId) + ->forUser($userId) + ->with(['menu' => fn ($q) => $q->withoutGlobalScopes()]) + ->get() + ->filter(fn ($fav) => $fav->menu && $fav->menu->is_active) + ->values(); + } + + /** + * 즐겨찾기 메뉴 ID 배열 (별 아이콘 활성 판단용) + */ + public function getFavoriteMenuIds(?int $userId = null): array + { + $userId = $userId ?? auth()->id(); + if (! $userId) { + return []; + } + + $tenantId = auth()->user()?->tenant_id ?? 1; + + return MenuFavorite::where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->pluck('menu_id') + ->toArray(); + } + + /** + * 즐겨찾기 토글 (추가/제거) + */ + public function toggleFavorite(int $userId, int $menuId): array + { + $tenantId = auth()->user()?->tenant_id ?? 1; + + $existing = MenuFavorite::where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->where('menu_id', $menuId) + ->first(); + + if ($existing) { + $existing->delete(); + + return ['action' => 'removed']; + } + + // 최대 개수 체크 + $count = MenuFavorite::where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->count(); + + if ($count >= self::MAX_FAVORITES) { + return ['action' => 'max_reached', 'max' => self::MAX_FAVORITES]; + } + + MenuFavorite::create([ + 'tenant_id' => $tenantId, + 'user_id' => $userId, + 'menu_id' => $menuId, + 'sort_order' => $count, + ]); + + return ['action' => 'added']; + } + + /** + * 즐겨찾기 순서 변경 + */ + public function reorderFavorites(int $userId, array $menuIds): void + { + $tenantId = auth()->user()?->tenant_id ?? 1; + + foreach ($menuIds as $order => $menuId) { + MenuFavorite::where('tenant_id', $tenantId) + ->where('user_id', $userId) + ->where('menu_id', $menuId) + ->update(['sort_order' => $order]); + } + } + /** * 메뉴 또는 자식 메뉴가 활성 상태인지 확인 */ diff --git a/resources/views/components/sidebar/favorites-section.blade.php b/resources/views/components/sidebar/favorites-section.blade.php new file mode 100644 index 00000000..881f53eb --- /dev/null +++ b/resources/views/components/sidebar/favorites-section.blade.php @@ -0,0 +1,65 @@ +@props(['favorites' => collect()]) + +@if($favorites->isNotEmpty()) + +@endif diff --git a/resources/views/components/sidebar/menu-item.blade.php b/resources/views/components/sidebar/menu-item.blade.php index 7d366d08..0fe8d37f 100644 --- a/resources/views/components/sidebar/menu-item.blade.php +++ b/resources/views/components/sidebar/menu-item.blade.php @@ -22,6 +22,9 @@ $target = $menu->is_external ? '_blank' : '_self'; + // 즐겨찾기 여부 + $isFavorited = isset($favoriteMenuIds) && in_array($menu->id, $favoriteMenuIds); + // 메뉴 뱃지 확인 (라우트명 또는 URL 기준) $badgeCount = 0; $badgeColor = '#ef4444'; @@ -42,7 +45,7 @@ } @endphp -
  • +
  • is_external) @endif + {{-- 즐겨찾기 별 아이콘 --}} +
  • diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 98b524cc..cad424a9 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -117,6 +117,9 @@ class="w-full border-gray-300 rounded-lg text-sm focus:ring-primary focus:border