Skip to content
20 changes: 20 additions & 0 deletions src/components/ui/badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { cn } from '@/lib/utils/cn';
import { ReactNode } from 'react';

interface BadgeProps {
children: ReactNode;
className?: string;
}

export default function Badge({ children, className }: BadgeProps) {
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2.5 py-1.5 text-sm font-medium',
className
)}
>
{children}
</span>
);
}
37 changes: 37 additions & 0 deletions src/components/ui/badge/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button } from '@/components/ui/button';
import Badge from './Badge';

export type StatusType = 'pending' | 'approved' | 'rejected';

interface StatusBadgeProps {
status: StatusType;
variant: 'employer' | 'employee';
onApprove: () => void;
onReject: () => void;
}

export default function StatusBadge({ status, variant, onApprove, onReject }: StatusBadgeProps) {
if (status === 'pending' && variant === 'employer') {
return (
<div className='flex w-1/2 flex-col gap-2 md:flex-row'>
<Button variant='reject' size='md' className='whitespace-nowrap' onClick={onReject}>
거절하기
</Button>
<Button variant='approve' size='md' className='whitespace-nowrap' onClick={onApprove}>
승인하기
</Button>
</div>
);
}

const BADGE_CLASS =
status === 'pending'
? 'bg-green-100 text-green-200'
: status === 'approved'
? 'bg-blue-100 text-blue-200'
: 'bg-red-100 text-red-400';

const BADGE_TEXT = status === 'pending' ? '대기중' : status === 'approved' ? '승인 완료' : '거절';

return <Badge className={BADGE_CLASS}>{BADGE_TEXT}</Badge>;
}
2 changes: 2 additions & 0 deletions src/components/ui/badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Badge } from '@/components/ui/badge/Badge';
export { default as StatusBadge } from '@/components/ui/badge/StatusBadge';
46 changes: 46 additions & 0 deletions src/components/ui/badge/statusbadge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { default as StatusBadge } from './StatusBadge';

const meta: Meta<typeof StatusBadge> = {
title: 'UI/StatusBadge',
component: StatusBadge,
tags: ['autodocs'],
parameters: { layout: 'centered' },
};
export default meta;

type Story = StoryObj<typeof StatusBadge>;

// Approve 승인 완료 뱃지
export const Approve: Story = {
args: {
status: 'approved',
variant: 'employer',
},
};

// Reject 거절 뱃지
export const Reject: Story = {
args: {
status: 'rejected',
variant: 'employer',
},
};

// Pending 대기중 employee 뱃지
export const PendingEmployee: Story = {
args: {
status: 'pending',
variant: 'employee',
},
};

// Pending 대기중 employer 버튼
export const PendingEmployer: Story = {
args: {
status: 'pending',
variant: 'employer',
onApprove: () => alert('승인!'),
onReject: () => alert('거절!'),
},
};
17 changes: 9 additions & 8 deletions src/components/ui/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function Calendar({ value, onSelect }: CalendarProps) {
};

return (
<div className='relative mt-3 aspect-square rounded-xl border bg-white p-4'>
<div className='relative mt-3 rounded-xl border bg-white p-4'>
<CalendarHeader
selectMode={selectMode}
currentMonth={currentMonth}
Expand Down Expand Up @@ -84,13 +84,14 @@ export default function Calendar({ value, onSelect }: CalendarProps) {
}}
/>
)}

<button
onClick={handleToday}
className='absolute bottom-5 right-5 text-sm text-blue-200 hover:underline'
>
오늘로 이동
</button>
<div className='pt-7'>
<button
onClick={handleToday}
className='absolute bottom-4 right-5 text-sm text-blue-200 hover:underline'
>
오늘로 이동
</button>
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/ui/calendar/DayViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV
})}
</div>

<div className={`${DAY_CALENDAR_CLASS} gap-1`}>
<div className={`${DAY_CALENDAR_CLASS} min-h-[300px] gap-1`}>
{DAYS.map((dayObj, i) => {
const { date, isCurrentMonth } = dayObj;

Expand All @@ -48,7 +48,7 @@ export default function DayViewMode({ currentMonth, currentDay, onSelect }: DayV
onClick={() => !isDisabled && onSelect(date)}
disabled={isDisabled}
className={cn(
'h-[3rem] w-[3rem] rounded-lg py-1.5 transition',
'aspect-square w-full rounded-lg transition',
isSelected
? 'bg-blue-200 font-semibold text-white'
: !isDisabled
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/calendar/MonthViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { MonthViewProps } from '@/types/calendar';

export default function MonthViewMode({ onSelect: onSelectMonth }: MonthViewProps) {
return (
<div className='grid grid-cols-4 gap-2 text-center'>
<div className='grid max-h-[340px] grid-cols-2 justify-items-center gap-2.5 text-center'>
{Array.from({ length: 12 }).map((_, i) => (
<button
key={i}
onClick={() => onSelectMonth(i)}
className='aspect-square rounded-lg py-2 hover:bg-blue-100'
className='h-[3rem] w-[4rem] rounded-lg hover:bg-blue-100'
>
{i + 1}월
</button>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/calendar/YearViewMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ export default function YearViewMode({ currentMonth, onSelect }: YearViewProps)
const START_YEAR = Math.floor(currentMonth.getFullYear() / 10) * 10;

return (
<div className='grid grid-cols-2 gap-4 text-center'>
<div className='grid max-h-[340px] grid-cols-2 justify-items-center gap-6 text-center'>
{Array.from({ length: 10 }).map((_, i) => {
const YEAR = START_YEAR + i;
return (
<button
key={YEAR}
onClick={() => onSelect(YEAR)}
className='h-[3rem] rounded-lg py-2 hover:bg-blue-100'
className='h-[3rem] w-[5rem] rounded-lg py-2 hover:bg-blue-100'
>
{YEAR}
</button>
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Badge, StatusBadge } from '@/components/ui/badge';
export {
Calendar,
CalendarHeader,
Expand Down
5 changes: 3 additions & 2 deletions src/components/ui/input/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ export default function DateInput({
(date: Date) => {
setSelectedDate(date);
setInputValue(formatDate(date));
setClose();
onChange?.(date);
},
[onChange]
[onChange, setClose]
);

// 날짜 선택
Expand Down Expand Up @@ -124,7 +125,7 @@ export default function DateInput({
/>

{isOpen && (
<div className='z-1 absolute w-full'>
<div className='absolute z-10 w-full'>
<Calendar onSelect={handleDateSelect} value={selectedDate ?? new Date()} />
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as Pagination } from './pagination';
export { default as Pagination } from '@/components/ui/pagination/pagination';
34 changes: 26 additions & 8 deletions src/components/ui/table/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { fetchTableData } from './testApi';
const meta: Meta<typeof Table> = {
title: 'UI/Table',
component: Table,
argTypes: {
userType: {
control: { type: 'radio' },
options: ['employer', 'employee'],
},
},
tags: ['autodocs'],
parameters: { layout: 'fullscreen' },
};

export default meta;
Expand All @@ -23,6 +19,8 @@ type Story = StoryObj<typeof Table>;
function TableWithTestApi({ userType }: { userType: UserType }) {
const [headers, setHeaders] = useState<string[]>([]);
const [data, setData] = useState<TableRowProps[]>([]);
const [offset, setOffset] = useState(0);
const limit = 5;

useEffect(() => {
const getData = async () => {
Expand All @@ -33,12 +31,32 @@ function TableWithTestApi({ userType }: { userType: UserType }) {
getData();
}, [userType]);

return <Table headers={headers} data={data} userType={userType} />;
const count = data.length;
const paginatedData = data.slice(offset, offset + limit);

return (
<Table
headers={headers}
data={paginatedData}
userType={userType}
total={count}
limit={limit}
offset={offset}
onPageChange={setOffset}
/>
);
}

export const TableExample: Story = {
export const EmployerTable: Story = {
args: {
userType: 'employer',
},
render: args => <TableWithTestApi userType={args.userType as UserType} />,
};

export const EmployeeTable: Story = {
args: {
userType: 'employee',
},
render: args => <TableWithTestApi userType={args.userType as UserType} />,
};
73 changes: 53 additions & 20 deletions src/components/ui/table/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,68 @@
import TableRow from '@/components/ui/table/TableRow';
import { Pagination } from '@/components/ui';
import { TableRowProps } from '@/components/ui/table/TableRowProps';
import { cn } from '@/lib/utils/cn';
import { UserType } from '@/types/user';
import TableRow from './TableRow';

interface TableProps {
data: TableRowProps[];
headers: string[];
userType: UserType;
total: number;
limit: number;
offset: number;
onPageChange: (newOffset: number) => void;
}

// <Table headers={headers} data={data} userType={type} /> type은 확인이 좀 더 필요합니다

export default function Table({ data, headers, userType }: TableProps) {
export default function Table({
data,
headers,
userType,
total,
limit,
offset,
onPageChange,
}: TableProps) {
return (
<div className='overflow-x-auto rounded-lg border border-gray-200'>
<table className='min-w-full'>
<thead>
<tr className='bg-red-100'>
{headers.map((header, index) => (
<th key={index} className='px-2 py-3 text-left text-sm font-normal'>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, index) => (
<TableRow key={index} rowData={row} variant={userType} />
))}
</tbody>
</table>
<div className='flex justify-center px-3 py-2'>페이지네이션</div>
<div className='py-[60px]'>
<div className='px-8 text-xl font-bold md:px-10 lg:mx-auto lg:max-w-[1000px] lg:px-0'>
{userType === 'employer' ? '신청자 목록' : '신청 목록'}
</div>
<div className='m-7 overflow-hidden rounded-lg border bg-white lg:mx-auto lg:max-w-[1000px]'>
<div className='scroll-bar overflow-x-auto'>
<table className='w-full table-fixed border-collapse'>
<thead>
<tr>
{headers.map((header, index) => (
<th
key={index}
className={cn(
'md:borde-r-0 bg-red-100 px-2 py-3 text-left text-xs font-normal',
index < headers.length - 1 && 'border-r md:border-r-0',
index === 0 && 'sticky left-0 z-10 w-[200px]',
index > 0 && index < headers.length - 1 && 'w-[245px]',
index === headers.length - 1 && 'w-[220px] md:w-[230px]'
)}
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{data.map(row => (
<TableRow key={row.id} rowData={row} variant={userType} />
))}
</tbody>
</table>
</div>

<div className='flex justify-center px-3 py-2'>
<Pagination total={total} limit={limit} offset={offset} onPageChange={onPageChange} />
</div>
</div>
</div>
);
}
Loading