Sidebar.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use client';
  2. import { useState, useEffect } from 'react';
  3. import Link from 'next/link';
  4. import Image from 'next/image';
  5. import { usePathname } from 'next/navigation';
  6. import {
  7. ArrowLeft,
  8. ChevronsUpDown,
  9. CircleAlert,
  10. Settings,
  11. User,
  12. } from 'lucide-react';
  13. import {
  14. Sidebar as SidebarRoot,
  15. SidebarContent,
  16. SidebarHeader,
  17. SidebarMenu,
  18. SidebarMenuButton,
  19. SidebarMenuItem,
  20. SidebarGroup,
  21. SidebarGroupContent,
  22. useSidebar,
  23. } from '@/components/ui/sidebar';
  24. import {
  25. DropdownMenu,
  26. DropdownMenuContent,
  27. DropdownMenuItem,
  28. DropdownMenuLabel,
  29. DropdownMenuSeparator,
  30. DropdownMenuTrigger,
  31. } from '@/components/ui/dropdown-menu';
  32. import { fetchApi } from '@/lib/utils/client';
  33. import type { StudioSettingsResponse } from '@/types/response/studio/settings';
  34. export default function Sidebar()
  35. {
  36. const pathname = usePathname();
  37. const { isMobile } = useSidebar();
  38. const [channel, setChannel] = useState<Pick<StudioSettingsResponse, 'isYouTubeConnected'|'channelName'|'handle'|'thumbnailUrl'>|null>(null);
  39. const isActive = (href: string) => pathname === href || pathname.startsWith(href + '/');
  40. useEffect(() => {
  41. fetchApi<StudioSettingsResponse>('/api/studio/settings')
  42. .then(res => {
  43. if (res.data) {
  44. setChannel(res.data);
  45. }
  46. })
  47. .catch(() => {});
  48. }, []);
  49. const isConnected = channel?.isYouTubeConnected === true;
  50. return (
  51. <SidebarRoot collapsible="icon">
  52. <SidebarHeader>
  53. {/* 상단 Studio 라벨 + 돌아가기 버튼 (전체 영역 클릭 가능) */}
  54. <Link
  55. href="/"
  56. aria-label="메인으로 돌아가기"
  57. title="메인으로 돌아가기"
  58. className="flex items-center gap-1.5 rounded px-2 py-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring group-data-[collapsible=icon]:hidden"
  59. >
  60. <ArrowLeft className="size-3.5" />
  61. <span className="text-xs font-semibold">Studio</span>
  62. </Link>
  63. {/* YouTube 채널 카드 */}
  64. <SidebarMenu>
  65. <SidebarMenuItem>
  66. <DropdownMenu>
  67. <DropdownMenuTrigger asChild>
  68. <SidebarMenuButton
  69. size="lg"
  70. className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
  71. tooltip={channel?.channelName ?? 'YouTube 채널'}
  72. >
  73. {/* 아이콘 (collapse 시 이것만 표시) */}
  74. <div className={`relative flex size-8 shrink-0 items-center justify-center rounded-lg ${!channel?.thumbnailUrl ? 'bg-sidebar-primary' : 'border'} text-sidebar-primary-foreground`}>
  75. {channel?.thumbnailUrl ? (
  76. <Image
  77. src={channel.thumbnailUrl}
  78. alt={channel.channelName ?? 'YouTube'}
  79. width={32}
  80. height={32}
  81. className="size-full object-cover"
  82. />
  83. ) : (
  84. <span className="text-xs font-bold">
  85. <User />
  86. </span>
  87. )}
  88. {/* 미연동 경고 뱃지 */}
  89. {channel !== null && !isConnected && (
  90. <span className="absolute -right-1 -bottom-1 flex size-3.5 items-center justify-center rounded-full bg-background">
  91. <CircleAlert className="size-2.5 text-amber-500" />
  92. </span>
  93. )}
  94. </div>
  95. {/* 텍스트 (collapse 시 hidden) */}
  96. <div className="grid flex-1 text-left text-sm leading-tight">
  97. <span className="truncate font-semibold">
  98. {isConnected
  99. ? (channel?.channelName ?? '채널 이름')
  100. : '채널 미연동'}
  101. </span>
  102. <span className="truncate text-xs text-sidebar-foreground/60">
  103. {isConnected
  104. ? (channel?.handle ?? '')
  105. : '채널을 연결해 주세요'}
  106. </span>
  107. </div>
  108. <ChevronsUpDown className="ml-auto size-4 shrink-0" />
  109. </SidebarMenuButton>
  110. </DropdownMenuTrigger>
  111. <DropdownMenuContent
  112. className="w-[--anchor-width] min-w-56 rounded-lg"
  113. align="start"
  114. side={isMobile ? 'bottom' : 'right'}
  115. sideOffset={4}
  116. >
  117. {isConnected ? (
  118. <>
  119. <DropdownMenuLabel className="text-xs text-muted-foreground">
  120. {channel?.channelName}
  121. </DropdownMenuLabel>
  122. <DropdownMenuSeparator />
  123. <DropdownMenuItem asChild>
  124. <Link href="/studio/settings">채널 정보 보기</Link>
  125. </DropdownMenuItem>
  126. </>
  127. ) : (
  128. <>
  129. <DropdownMenuLabel className="text-xs text-muted-foreground">
  130. YouTube 채널이 연동되지 않았습니다
  131. </DropdownMenuLabel>
  132. <DropdownMenuSeparator />
  133. <DropdownMenuItem asChild>
  134. <Link href="/studio/settings">YouTube 연동하기</Link>
  135. </DropdownMenuItem>
  136. </>
  137. )}
  138. </DropdownMenuContent>
  139. </DropdownMenu>
  140. </SidebarMenuItem>
  141. </SidebarMenu>
  142. </SidebarHeader>
  143. <SidebarContent>
  144. <SidebarGroup>
  145. <SidebarGroupContent>
  146. <SidebarMenu>
  147. {/* 채널 설정 */}
  148. <SidebarMenuItem>
  149. <SidebarMenuButton asChild isActive={isActive('/studio/settings')} tooltip="채널 설정">
  150. <Link href="/studio/settings">
  151. <Settings />
  152. <span>채널 설정</span>
  153. </Link>
  154. </SidebarMenuButton>
  155. </SidebarMenuItem>
  156. </SidebarMenu>
  157. </SidebarGroupContent>
  158. </SidebarGroup>
  159. </SidebarContent>
  160. </SidebarRoot>
  161. );
  162. }