diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2601f6a7..0c16bbd8 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,8 +2,11 @@ "permissions": { "allow": [ "Bash(gh pr:*)", + "Bash(gh run:*)", "Bash(git push:*)", - "Bash(git fetch:*)" + "Bash(git fetch:*)", + "mcp__figma__get_design_context", + "mcp__figma__get_screenshot" ] } -} +} \ No newline at end of file diff --git a/src/app/(private)/admin/board/page.tsx b/src/app/(private)/admin/board/page.tsx new file mode 100644 index 00000000..ebb42c5a --- /dev/null +++ b/src/app/(private)/admin/board/page.tsx @@ -0,0 +1,3 @@ +export default function BoardPage() { + return
BoardPage
; +} diff --git a/src/app/(private)/admin/club-info/page.tsx b/src/app/(private)/admin/club-info/page.tsx new file mode 100644 index 00000000..332d0448 --- /dev/null +++ b/src/app/(private)/admin/club-info/page.tsx @@ -0,0 +1,3 @@ +export default function ClubInfoPage() { + return
ClubInfoPage
; +} diff --git a/src/app/(private)/admin/schedule/page.tsx b/src/app/(private)/admin/schedule/page.tsx new file mode 100644 index 00000000..bc28067f --- /dev/null +++ b/src/app/(private)/admin/schedule/page.tsx @@ -0,0 +1,3 @@ +export default function SchedulePage() { + return
SchedulePage
; +} diff --git a/src/app/globals.css b/src/app/globals.css index 0928be80..f21819a4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -216,8 +216,8 @@ --caption2-line-height: 16px; --button1-size: 14px; --button1-line-height: 24px; - --button2-size: 13px; - --button2-line-height: 16px; + --button2-size: 14px; + --button2-line-height: 20px; } .dark { diff --git a/src/assets/icons/admin/ic_admin_attendance.svg b/src/assets/icons/admin/ic_admin_attendance.svg index 837e944e..87f99d3b 100644 --- a/src/assets/icons/admin/ic_admin_attendance.svg +++ b/src/assets/icons/admin/ic_admin_attendance.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/assets/icons/admin/ic_admin_calendar.svg b/src/assets/icons/admin/ic_admin_calendar.svg new file mode 100644 index 00000000..6af182a3 --- /dev/null +++ b/src/assets/icons/admin/ic_admin_calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/admin/ic_admin_due.svg b/src/assets/icons/admin/ic_admin_due.svg index 82bf7460..40ac4d2f 100644 --- a/src/assets/icons/admin/ic_admin_due.svg +++ b/src/assets/icons/admin/ic_admin_due.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/admin/ic_admin_fileout.svg b/src/assets/icons/admin/ic_admin_fileout.svg new file mode 100644 index 00000000..e0ba79bd --- /dev/null +++ b/src/assets/icons/admin/ic_admin_fileout.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/admin/ic_admin_forum.svg b/src/assets/icons/admin/ic_admin_forum.svg new file mode 100644 index 00000000..69078333 --- /dev/null +++ b/src/assets/icons/admin/ic_admin_forum.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/admin/ic_admin_light.svg b/src/assets/icons/admin/ic_admin_light.svg new file mode 100644 index 00000000..0a083e46 --- /dev/null +++ b/src/assets/icons/admin/ic_admin_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/admin/ic_admin_manual.svg b/src/assets/icons/admin/ic_admin_manual.svg index a02c3432..5e2fd0be 100644 --- a/src/assets/icons/admin/ic_admin_manual.svg +++ b/src/assets/icons/admin/ic_admin_manual.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/assets/icons/admin/ic_admin_penalty.svg b/src/assets/icons/admin/ic_admin_penalty.svg index 265f55f3..69ccb9e5 100644 --- a/src/assets/icons/admin/ic_admin_penalty.svg +++ b/src/assets/icons/admin/ic_admin_penalty.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/admin/ic_admin_service_transfer.svg b/src/assets/icons/admin/ic_admin_service_transfer.svg index 137d4a4f..da7f8b42 100644 --- a/src/assets/icons/admin/ic_admin_service_transfer.svg +++ b/src/assets/icons/admin/ic_admin_service_transfer.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/admin/ic_admin_setting.svg b/src/assets/icons/admin/ic_admin_setting.svg new file mode 100644 index 00000000..09cd128d --- /dev/null +++ b/src/assets/icons/admin/ic_admin_setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/admin/ic_admin_user.svg b/src/assets/icons/admin/ic_admin_user.svg index 7f44dd9a..2db41f01 100644 --- a/src/assets/icons/admin/ic_admin_user.svg +++ b/src/assets/icons/admin/ic_admin_user.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/admin/index.ts b/src/assets/icons/admin/index.ts index f36180b1..b5987668 100644 --- a/src/assets/icons/admin/index.ts +++ b/src/assets/icons/admin/index.ts @@ -10,3 +10,8 @@ export { default as AdminCheckIcon } from './ic_admin_check.svg'; export { default as AdminUncheckboxIcon } from './ic_admin_uncheckbox.svg'; export { default as AdminUserIcon } from './ic_admin_user.svg'; export { default as AdminCloseIcon } from './ic_admin_close.svg'; +export { default as AdminSettingIcon } from './ic_admin_setting.svg'; +export { default as AdminFileoutIcon } from './ic_admin_fileout.svg'; +export { default as AdminLightIcon } from './ic_admin_light.svg'; +export { default as AdminForumIcon } from './ic_admin_forum.svg'; +export { default as AdminCalendarIcon } from './ic_admin_calendar.svg'; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index e5df5a6c..0edac0be 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -48,3 +48,5 @@ export { default as TooltipIcon } from './tooltip.svg'; export { default as CopyIcon } from './copy.svg'; export { default as BasicAvatarIcon } from './basic_avatar.svg'; export { default as QuestionMarkIcon } from './question_mark.svg'; + +export { default as NavToggleIcon } from './nav_toggle.svg'; diff --git a/src/assets/icons/nav_toggle.svg b/src/assets/icons/nav_toggle.svg new file mode 100644 index 00000000..56808d15 --- /dev/null +++ b/src/assets/icons/nav_toggle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/admin/layout/CollapsedDivider.tsx b/src/components/admin/layout/CollapsedDivider.tsx new file mode 100644 index 00000000..56d6661b --- /dev/null +++ b/src/components/admin/layout/CollapsedDivider.tsx @@ -0,0 +1,11 @@ +interface CollapsedDividerProps { + collapsed?: boolean; +} + +function CollapsedDivider({ collapsed }: CollapsedDividerProps) { + if (!collapsed) return null; + + return
; +} + +export { CollapsedDivider, type CollapsedDividerProps }; diff --git a/src/components/admin/layout/LNB.tsx b/src/components/admin/layout/LNB.tsx index ce247be4..1fc74baf 100644 --- a/src/components/admin/layout/LNB.tsx +++ b/src/components/admin/layout/LNB.tsx @@ -1,136 +1,118 @@ 'use client'; -import Link from 'next/link'; -import type { StaticImageData } from 'next/image'; +import { useState } from 'react'; import { usePathname } from 'next/navigation'; +import { + AdminForumIcon, + AdminCalendarIcon, + AdminSettingIcon, + AdminFileoutIcon, +} from '@/assets/icons/admin'; +import { CheckRoundIcon, ExitIcon, PeopleIcon } from '@/assets/icons'; -import logoIcon from '@/assets/icons/logo/logo_initial_Origin.svg'; -import userIcon from '@/assets/icons/admin/ic_admin_user.svg'; -import checkIcon from '@/assets/icons/admin/ic_admin_attendance.svg'; -//import penaltyIcon from '@/assets/icons/admin/ic_admin_penalty.svg'; -// import dueIcon from '@/assets/icons/admin/ic_admin_due.svg'; -import arrowIcon from '@/assets/icons/admin/ic_admin_service_transfer.svg'; -import manualIcon from '@/assets/icons/admin/ic_admin_manual.svg'; - +import { TooltipProvider } from '@/components/ui'; import { cn } from '@/lib/cn'; +import { LNBHeader } from '@/components/admin/layout/LNBHeader'; +import { LNBClubInfo } from '@/components/admin/layout/LNBClubInfo'; +import { NavSection } from '@/components/admin/layout/NavSection'; +import { NavItem } from '@/components/admin/layout/NavItem'; +import { CollapsedDivider } from '@/components/admin/layout/CollapsedDivider'; +import { ThemeModeSelector } from '@/components/admin/layout/ThemeModeSelector'; + +const managementNavItems = [ + { id: 'member', icon: PeopleIcon, label: '멤버 관리', path: '/admin/member' }, + { id: 'schedule', icon: AdminCalendarIcon, label: '일정 관리', path: '/admin/schedule' }, + { id: 'attendance', icon: CheckRoundIcon, label: '출석 관리', path: '/admin/attendance' }, + { id: 'board', icon: AdminForumIcon, label: '게시판 관리', path: '/admin/board' }, +]; -const mainNavItems = [ - { id: 'member', icon: userIcon, label: '멤버 관리', path: '/admin/member' }, - { id: 'attendance', icon: checkIcon, label: '출석 관리', path: '/admin/attendance' }, - // { id: 'penalty', icon: penaltyIcon, label: '페널티 관리', path: '/admin/penalty' }, - // { id: 'dues', icon: dueIcon, label: '회비 관리', path: '/admin/dues' }, +const infoNavItems = [ + { id: 'club-info', icon: AdminSettingIcon, label: '동아리 정보', path: '/admin/club-info' }, ]; const moveNavItems = [ - { id: 'service', icon: arrowIcon, label: '서비스로 이동', path: 'https://weeth.kr' }, + { + id: 'service', + icon: ExitIcon, + label: '서비스로 이동', + path: 'https://weeth.kr', + external: true, + }, { id: 'manual', - icon: manualIcon, + icon: AdminFileoutIcon, label: '관리자 매뉴얼', path: 'https://weeth-develop-2.s3.ap-northeast-2.amazonaws.com/Weeth_%E1%84%80%E1%85%AA%E1%86%AB%E1%84%85%E1%85%B5%E1%84%8C%E1%85%A1_%E1%84%86%E1%85%A6%E1%84%82%E1%85%B2%E1%84%8B%E1%85%A5%E1%86%AF_v3.pdf', + external: true, + openInWindow: true, }, ]; -function NavIcon({ src, isActive }: { src: StaticImageData | string; isActive: boolean }) { - const url = typeof src === 'string' ? src : (src as StaticImageData).src; - return ( - - ); -} - -const navItemClass = - 'typo-sub1 flex h-12 items-center gap-300 px-300 transition-colors text-text-alternative hover:bg-container-neutral-interaction'; - -export function LNB() { +function LNB() { const pathname = usePathname(); + const [collapsed, setCollapsed] = useState(false); return ( - + ); } + +export { LNB }; diff --git a/src/components/admin/layout/LNBClubInfo.tsx b/src/components/admin/layout/LNBClubInfo.tsx new file mode 100644 index 00000000..aa611e26 --- /dev/null +++ b/src/components/admin/layout/LNBClubInfo.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui'; +import { cn } from '@/lib/cn'; +import { useAdminClubQuery } from '@/hooks/queries/admin/useAdminClubQuery'; + +interface LNBClubInfoProps { + collapsed: boolean; +} + +function LNBClubInfo({ collapsed }: LNBClubInfoProps) { + const { data: club } = useAdminClubQuery(); + + return ( +
+ + {club?.profileImageUrl && } + {club?.name?.charAt(0)} + + {!collapsed && ( +
+ {club?.schoolName} + {club?.name} +
+ )} +
+ ); +} + +export { LNBClubInfo, type LNBClubInfoProps }; diff --git a/src/components/admin/layout/LNBHeader.tsx b/src/components/admin/layout/LNBHeader.tsx new file mode 100644 index 00000000..ee3c1f3e --- /dev/null +++ b/src/components/admin/layout/LNBHeader.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icon, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'; +import { NavToggleIcon } from '@/assets/icons'; +import { cn } from '@/lib/cn'; + +interface LNBHeaderProps { + collapsed: boolean; + onToggle: () => void; +} + +function LNBHeader({ collapsed, onToggle }: LNBHeaderProps) { + return ( +
+ + + + + + {collapsed ? '사이드바 열기' : '사이드바 닫기'} + + + {!collapsed && Weeth admin} +
+ ); +} + +export { LNBHeader, type LNBHeaderProps }; diff --git a/src/components/admin/layout/NavItem.tsx b/src/components/admin/layout/NavItem.tsx new file mode 100644 index 00000000..ee9839aa --- /dev/null +++ b/src/components/admin/layout/NavItem.tsx @@ -0,0 +1,85 @@ +'use client'; + +import Link from 'next/link'; + +import { Icon, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'; +import { cn } from '@/lib/cn'; +import { PeopleIcon } from '@/assets/icons'; + +const baseClass = + 'flex h-12 items-center transition-colors text-text-normal hover:bg-container-neutral-interaction'; + +interface NavItemProps { + icon: typeof PeopleIcon; + label: string; + path: string; + isActive?: boolean; + collapsed?: boolean; + external?: boolean; + openInWindow?: boolean; +} + +function NavItem({ + icon, + label, + path, + isActive = false, + collapsed = false, + external = false, + openInWindow = false, +}: NavItemProps) { + const iconEl = ( + + ); + + const cls = cn( + baseClass, + collapsed ? 'justify-center px-400' : 'gap-300 px-400', + isActive && 'bg-container-neutral-interaction text-text-strong', + ); + + let el: React.ReactNode; + + if (openInWindow) { + el = ( + + ); + } else if (external) { + el = ( + + {iconEl} + {!collapsed && {label}} + + ); + } else { + el = ( + + {iconEl} + {!collapsed && {label}} + + ); + } + + if (collapsed) { + return ( + + {el} + {label} + + ); + } + + return el; +} + +export { NavItem, type NavItemProps }; diff --git a/src/components/admin/layout/NavSection.tsx b/src/components/admin/layout/NavSection.tsx new file mode 100644 index 00000000..22f024ab --- /dev/null +++ b/src/components/admin/layout/NavSection.tsx @@ -0,0 +1,18 @@ +interface NavSectionProps { + label?: string; + collapsed?: boolean; + children: React.ReactNode; +} + +function NavSection({ label, collapsed, children }: NavSectionProps) { + return ( +
+ {label && !collapsed && ( + {label} + )} + {children} +
+ ); +} + +export { NavSection, type NavSectionProps }; diff --git a/src/components/admin/layout/ThemeModeSelector.tsx b/src/components/admin/layout/ThemeModeSelector.tsx new file mode 100644 index 00000000..ca5ccf73 --- /dev/null +++ b/src/components/admin/layout/ThemeModeSelector.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { useState } from 'react'; +import { ChevronDown, Moon, Sun, SunMoon } from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui'; +import { cn } from '@/lib/cn'; +import { useThemeStore } from '@/stores/theme-store'; + +type ThemeMode = 'auto' | 'light' | 'dark'; + +const THEME_OPTIONS: { value: ThemeMode; label: string; icon: typeof Sun }[] = [ + { value: 'auto', label: '자동', icon: SunMoon }, + { value: 'light', label: '라이트', icon: Sun }, + { value: 'dark', label: '다크', icon: Moon }, +]; + +const TRIGGER_LABELS: Record = { + auto: '자동 모드', + light: '라이트 모드', + dark: '다크 모드', +}; + +interface ThemeModeSelectorProps { + collapsed?: boolean; +} + +function ThemeModeSelector({ collapsed }: ThemeModeSelectorProps) { + const setDark = useThemeStore((state) => state.setDark); + + const [mode, setMode] = useState(() => { + if (typeof window === 'undefined') return 'light'; + return useThemeStore.getState().isDark ? 'dark' : 'light'; + }); + + const handleSelect = (value: ThemeMode) => { + setMode(value); + + if (value === 'light') { + setDark(false); + } else if (value === 'dark') { + setDark(true); + } else { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setDark(prefersDark); + } + }; + + const currentOption = THEME_OPTIONS.find((o) => o.value === mode)!; + const TriggerIcon = currentOption.icon; + + const trigger = ( + + + + ); + + return ( + + {collapsed ? ( + + {trigger} + {TRIGGER_LABELS[mode]} + + ) : ( + trigger + )} + + + {THEME_OPTIONS.map(({ value, label }) => ( + handleSelect(value)}> + {label} + + ))} + + + ); +} + +export { ThemeModeSelector }; diff --git a/src/components/auth/hub/ClubSearchDropdown.tsx b/src/components/auth/hub/ClubSearchDropdown.tsx index a9507c1c..44187896 100644 --- a/src/components/auth/hub/ClubSearchDropdown.tsx +++ b/src/components/auth/hub/ClubSearchDropdown.tsx @@ -30,8 +30,8 @@ function ClubSearchDropdown({ clubs, onSelect, className }: ClubSearchDropdownPr className="border-line bg-container-neutral flex w-full cursor-pointer items-center gap-400 rounded-[10px] border px-200 py-200 transition-colors" > - {club.logoUrl && ( - + {club.profileImageUrl && ( + )} {club.name.charAt(0)} diff --git a/src/components/auth/hub/ClubSelectedCard.tsx b/src/components/auth/hub/ClubSelectedCard.tsx index 3eb42f78..4436cd91 100644 --- a/src/components/auth/hub/ClubSelectedCard.tsx +++ b/src/components/auth/hub/ClubSelectedCard.tsx @@ -16,8 +16,8 @@ function ClubSelectedCard({ club, onRemove }: ClubSelectedCardProps) { type="square" className="border-line h-10 w-10 shrink-0 rounded-lg border" > - {club.logoUrl && ( - + {club.profileImageUrl && ( + )} {club.name.charAt(0)} diff --git a/src/components/auth/invite/ClubAccessPage.tsx b/src/components/auth/invite/ClubAccessPage.tsx index f2729084..56d7d4f4 100644 --- a/src/components/auth/invite/ClubAccessPage.tsx +++ b/src/components/auth/invite/ClubAccessPage.tsx @@ -14,8 +14,8 @@ function ClubAccessPage({ club }: ClubAccessPageProps) {

이 사이트는 동아리 회원만 이용할 수 있어요.

- {club.logoUrl && ( - + {club.profileImageUrl && ( + )} {club.name.charAt(0)} diff --git a/src/components/auth/invite/ClubConfirmCard.tsx b/src/components/auth/invite/ClubConfirmCard.tsx index 81e28f8c..a09ea828 100644 --- a/src/components/auth/invite/ClubConfirmCard.tsx +++ b/src/components/auth/invite/ClubConfirmCard.tsx @@ -14,8 +14,8 @@ function ClubConfirmCard({ club, confirmHref }: ClubConfirmCardProps) {
가입하려는 동아리가 맞나요? - {club.logoUrl && ( - + {club.profileImageUrl && ( + )} {club.name.charAt(0)} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index e27e6ea2..58a31489 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -14,25 +14,33 @@ const avatarVariants = cva('group/avatar relative flex shrink-0 overflow-hidden size: { 128: 'size-32', 64: 'size-16', + 40: 'size-10', 24: 'size-6', }, + colorScheme: { + default: '', + primary: '', + secondary: '', + }, }, defaultVariants: { type: 'round', size: 64, + colorScheme: 'default', }, }); interface AvatarProps extends React.ComponentProps, VariantProps {} -function Avatar({ className, type, size, ...props }: AvatarProps) { +function Avatar({ className, type, size, colorScheme, ...props }: AvatarProps) { return ( ); @@ -56,10 +64,14 @@ function AvatarFallback({ adminClubApi.getDetail(clubId!).then((res) => res.data.data), + enabled: !!clubId, + staleTime: 30 * 60 * 1000, + gcTime: 60 * 60 * 1000, + }); +} diff --git a/src/lib/apis/adminClub.ts b/src/lib/apis/adminClub.ts new file mode 100644 index 00000000..7ee1279f --- /dev/null +++ b/src/lib/apis/adminClub.ts @@ -0,0 +1,7 @@ +import { apiClient } from '@/lib/apis/client'; +import type { Club } from '@/types/club'; +import type { ApiResponse } from '@/types/common'; + +export const adminClubApi = { + getDetail: (clubId: string) => apiClient.get>(`/admin/clubs/${clubId}`), +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index f076aaf6..89037d11 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -12,3 +12,4 @@ export { mypageApi } from './mypage'; export { adminMemberApi } from './adminMember'; export { cardinalApi } from './cardinal'; export { inquiryApi } from './inquiry'; +export { adminClubApi } from './adminClub'; diff --git a/src/proxy.ts b/src/proxy.ts index 66849ab9..171b972f 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -4,7 +4,7 @@ import { ACCESS_TOKEN_KEY } from '@/lib/apis/cookies'; const PUBLIC_PATHS = ['/', '/login', '/terms', '/landing']; // TODO: 런칭 후 PRE_LAUNCH 플래그 및 관련 분기 제거 -const PRE_LAUNCH = true; +const PRE_LAUNCH = false; export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; diff --git a/src/types/club.ts b/src/types/club.ts index 2794b705..8e1c3aad 100644 --- a/src/types/club.ts +++ b/src/types/club.ts @@ -1,8 +1,12 @@ -// club 관련 타입 정의 - export interface Club { id: string; name: string; + code: string; + schoolName: string; description: string; - logoUrl?: string; + contactEmail: string; + contactPhoneNumber: string; + primaryContact: 'PHONE' | 'EMAIL'; + profileImageUrl: string; + backgroundImageUrl: string; }