Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 142 additions & 52 deletions src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,161 @@
import * as React from 'react'
import { ko } from 'date-fns/locale'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { DayPicker } from 'react-day-picker'
import { ko } from "date-fns/locale";
import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile'
import { buttonVariants } from '@/components/ui/button'

export type CalendarProps = React.ComponentProps<typeof DayPicker>

export interface ResponsiveCalendarProps {
className?: string
classNames?: any
containerClassName?: string
numberOfMonths?: number
mode?: 'default' | 'range' | 'multiple' | 'single'
showOutsideDays?: boolean
selected?: any
onSelect?: any
disabled?: any
defaultMonth?: Date
showMultipleMonths?: boolean
[key: string]: any
}

// 아이콘 컴포넌트
const LeftIcon = ({ isDesktop }: { isDesktop: boolean }) => {
return <ChevronLeft className={isDesktop ? 'h-4 w-4' : 'h-3 w-3'} />
}

const RightIcon = ({ isDesktop }: { isDesktop: boolean }) => {
return <ChevronRight className={isDesktop ? 'h-4 w-4' : 'h-3 w-3'} />
}

// 반응형 스타일 유틸리티
const getResponsiveStyles = (
isDesktop: boolean,
numberOfMonths: number,
customClassNames?: Record<string, string>,
mode?: 'default' | 'range' | 'multiple' | 'single'
) => {
// 날짜와 요일 셀 크기 통일 - 크기 증가
const cellSize = isDesktop ? 'w-11' : 'w-8'

const baseStyles = {
months: cn(
'flex flex-col space-y-4',
isDesktop && numberOfMonths > 1 && 'md:flex-row md:space-x-4 md:space-y-0'
),
month: 'space-y-3', // 간격 확대
caption: 'flex justify-center pt-1 relative items-center mb-2', // 하단 여백 추가
caption_label: cn('font-medium', isDesktop ? 'text-sm' : 'text-xs'),
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'bg-transparent p-0 opacity-50 hover:opacity-100',
isDesktop ? 'h-7 w-7' : 'h-6 w-6'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse',
head_row: 'flex justify-between w-full px-1 mb-1', // 간격 조절 및 하단 여백 추가
head_cell: cn(
'text-muted-foreground text-center font-normal',
cellSize,
isDesktop ? 'text-[0.8rem]' : 'text-[0.65rem]'
),
row: 'flex w-full justify-between px-1 mt-2', // 간격 조절 및 상단 여백 추가
cell: cn(
`relative p-0 text-center ${isDesktop ? 'text-sm' : 'text-xs'} focus-within:relative focus-within:z-20`,
'[&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50',
mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md'
),
day: cn(
buttonVariants({ variant: 'ghost' }),
cellSize,
isDesktop ? 'h-9' : 'h-7',
'p-0 font-normal aria-selected:opacity-100'
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...customClassNames,
}

return baseStyles
}

function Calendar({
className,
classNames,
containerClassName,
numberOfMonths,
mode,
showOutsideDays = true,
showMultipleMonths = false,
...props
}: CalendarProps) {
}: ResponsiveCalendarProps) {
const isMobile = useIsMobile()
const isDesktop = !isMobile

const [isLargeDesktop, setIsLargeDesktop] = React.useState(false)

React.useEffect(() => {
const checkLargeScreen = () => {
setIsLargeDesktop(window.innerWidth >= 1024)
}

checkLargeScreen()
window.addEventListener('resize', checkLargeScreen)

return () => window.removeEventListener('resize', checkLargeScreen)
}, [])

const responsiveMonths = React.useMemo(() => {
if (numberOfMonths !== undefined) return numberOfMonths
if (showMultipleMonths && isLargeDesktop) return 2
return 1
}, [isLargeDesktop, numberOfMonths, showMultipleMonths])

return (
<DayPicker
locale={ko}
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex gap-3 justify-center',
head_cell:
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
: '[&:has([aria-selected])]:rounded-md'
),
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-11 p-0 font-normal aria-selected:opacity-100'
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames,
}}
components={{
IconLeft: () => <ChevronLeft className='h-4 w-4' />,
IconRight: () => <ChevronRight className='h-4 w-4' />,
}}
{...props}
/>
<div className={cn('w-full rounded-lg', containerClassName)}>
<DayPicker
locale={ko}
showOutsideDays={showOutsideDays}
numberOfMonths={responsiveMonths}
className={cn(
'w-full max-w-full',
isDesktop ? 'p-3' : 'p-2',
className
)}
classNames={getResponsiveStyles(
isDesktop,
responsiveMonths,
classNames,
mode
)}
components={{
IconLeft: () => <LeftIcon isDesktop={isDesktop} />,
IconRight: () => <RightIcon isDesktop={isDesktop} />,
}}
mode={mode}
{...props}
/>
</div>
)
}

Calendar.displayName = 'Calendar'

export { Calendar }
50 changes: 36 additions & 14 deletions src/features/users/components/StudentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ReactNode } from '@tanstack/react-router'
import { Pencil, Save, X } from 'lucide-react'
import { useIsMobile } from '@/hooks/use-mobile'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
Expand Down Expand Up @@ -58,7 +59,13 @@ const componentsMap: Record<DetailType, ValueItemsType> = {
},
employment: {
label: '취업',
component: (data) => <Employment datas={data.employment_companies.filter((item: UserDetailType['employment_companies'][0]) => item.is_working)} />,
component: (data) => (
<Employment
datas={data.employment_companies.filter(
(item: UserDetailType['employment_companies'][0]) => item.is_working
)}
/>
),
},
university: {
label: '대학교 진학',
Expand All @@ -77,6 +84,7 @@ const componentsMap: Record<DetailType, ValueItemsType> = {
}

export function StudentDetail({ student_id }: { student_id: string }) {
const isMobile = useIsMobile()
const { editingSection, setEditingSection, editData, setEditData } =
useEditUser()
const { data, isLoading, refetch, isFetching } =
Expand Down Expand Up @@ -123,9 +131,13 @@ export function StudentDetail({ student_id }: { student_id: string }) {
return (
<div className='h-full space-y-6 overflow-auto p-1'>
<Card>
<CardHeader className='pb-3'>
<div className='flex items-center justify-between'>
<CardTitle className='text-2xl'>{data?.name}</CardTitle>
<CardHeader className={isMobile ? 'px-3 pb-2 pt-3' : 'pb-3'}>
<div
className={`flex ${isMobile ? 'flex-col gap-2' : 'items-center justify-between'}`}
>
<CardTitle className={isMobile ? 'text-xl' : 'text-2xl'}>
{data?.name}
</CardTitle>
<div className='flex items-center gap-2'>
<Badge variant='outline'>
{data?.departments.department_name}
Expand All @@ -134,29 +146,37 @@ export function StudentDetail({ student_id }: { student_id: string }) {
</div>
</CardHeader>

<CardContent className='space-y-5'>
<CardContent className={isMobile ? 'space-y-4 px-3 py-2' : 'space-y-5'}>
{(Object.keys(componentsMap) as DetailType[]).map((key) => {
return (
<div key={key}>
<div className='mb-2 flex items-center justify-between'>
<h3 className='font-semibold'>{componentsMap[key].label}</h3>
<div
className={`mb-2 ${isMobile ? 'flex-col gap-2' : 'flex items-center justify-between'}`}
>
<h3 className={`font-semibold ${isMobile ? 'mb-2' : ''}`}>
{componentsMap[key].label}
</h3>
{editingSection === key ? (
<div className='flex gap-2'>
<Button
variant='outline'
size='sm'
onClick={cancelEditing}
className='flex items-center gap-1 text-muted-foreground transition-colors hover:text-destructive'
className={`flex items-center gap-1 text-muted-foreground transition-colors hover:text-destructive ${
isMobile ? 'h-7 px-2 text-xs' : ''
}`}
>
<X size={14} />
<X size={isMobile ? 12 : 14} />
<span>취소</span>
</Button>
<Button
size='sm'
onClick={saveEditing}
className='flex items-center gap-1 bg-gradient-to-r from-primary to-primary/90'
className={`flex items-center gap-1 bg-gradient-to-r from-primary to-primary/90 ${
isMobile ? 'h-7 px-2 text-xs' : ''
}`}
>
<Save size={14} />
<Save size={isMobile ? 12 : 14} />
<span>저장</span>
</Button>
</div>
Expand All @@ -165,16 +185,18 @@ export function StudentDetail({ student_id }: { student_id: string }) {
variant='ghost'
size='sm'
onClick={() => setEditingSection(key)}
className='flex items-center gap-1 text-muted-foreground transition-colors hover:bg-primary/10 hover:text-primary'
className={`flex items-center gap-1 text-muted-foreground transition-colors hover:bg-primary/10 hover:text-primary ${
isMobile ? 'h-7 px-2 text-xs' : ''
}`}
>
<Pencil size={14} />
<Pencil size={isMobile ? 12 : 14} />
<span>수정</span>
</Button>
) : null}
</div>

{data ? (
<div className='mb-5'>
<div className={isMobile ? 'mb-3' : 'mb-5'}>
{componentsMap[key].component(data)}
</div>
) : (
Expand Down
Loading