Merge pull request 'feature/lucy-component' (#6) from feature/lucy-component into develop
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -6,17 +6,19 @@ import { MENU_ITEMS } from '~/shared/constants/menu';
|
|||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex h-screen w-screen">
|
||||||
<aside className="bg-primary-tertiary01 z-10 h-full w-[260px] flex-none shadow-[4px_0_5px_0_rgba(0,0,0,0.1)]">
|
<aside className="bg-primary-tertiary01 z-10 h-full w-[260px] flex-none flex flex-col shadow-[4px_0_5px_0_rgba(0,0,0,0.1)]">
|
||||||
<div className="flex flex-col items-center bg-white pt-6 pb-4">
|
<div className="flex flex-col items-center bg-white pt-6 pb-4">
|
||||||
<div className="flex items-center justify-center pb-7">
|
<div className="flex items-center justify-center pb-7">
|
||||||
<span className="text-xl font-bold text-primary">DABEEO</span>
|
<span className="text-xl font-bold text-primary">DABEEO</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600">사용자 정보</div>
|
<div className="text-sm text-gray-600">사용자 정보</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-primary flex h-12 items-center px-5 font-bold text-white">
|
<div className="bg-primary flex h-12 flex-none items-center px-5 font-bold text-white">
|
||||||
변화탐지관리
|
변화탐지관리
|
||||||
</div>
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<LayoutMenu items={MENU_ITEMS} />
|
<LayoutMenu items={MENU_ITEMS} />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="bg-primary-tertiary02 flex flex-1 min-w-0 flex-col">
|
<main className="bg-primary-tertiary02 flex flex-1 min-w-0 flex-col">
|
||||||
<div className="bg-primary w-full h-4" />
|
<div className="bg-primary w-full h-4" />
|
||||||
|
|||||||
147
web-app/app/shared/components/button/Button.tsx
Normal file
147
web-app/app/shared/components/button/Button.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import type { ButtonProps as AriaButtonProps } from 'react-aria-components';
|
||||||
|
import { Button as AriaButton, composeRenderProps } from 'react-aria-components';
|
||||||
|
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
import { LoadingSpinnerIcon } from '../icons/LoadingSpinner';
|
||||||
|
|
||||||
|
export interface ButtonProps extends AriaButtonProps {
|
||||||
|
color?: 'primary' | 'light' | 'green' | 'lightGreen' | 'black' | 'gray' | 'orange' | 'navy' | 'lightNavy';
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
stopPropagation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = tv({
|
||||||
|
base: 'relative inline-flex items-center justify-center gap-2 border cursor-pointer box-border data-focus-visible:ring-2 ring-offset-2',
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
small: 'h-6 px-3 text-xs font-medium',
|
||||||
|
medium: 'h-9 px-3 text-xs font-medium',
|
||||||
|
large: 'min-w-30 h-13 px-5 text-sm font-semibold',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: 'text-white bg-primary border-primary disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-primary',
|
||||||
|
light:
|
||||||
|
'text-primary border-primary bg-primary-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-primary',
|
||||||
|
green:
|
||||||
|
'text-white bg-kc-green-main border-kc-green-main disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-green-main',
|
||||||
|
lightGreen:
|
||||||
|
'text-kc-green-main border-kc-green-main bg-kc-green-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-green-main',
|
||||||
|
black:
|
||||||
|
'text-white bg-kc-black-34 border-kc-black-34 disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-black-34',
|
||||||
|
gray: 'text-kc-black-34 bg-kc-gray-eb border-kc-black-34 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-black-34',
|
||||||
|
orange:
|
||||||
|
'text-kc-orange-main bg-kc-orange-tertiary01 border-kc-orange-main disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-orange-main',
|
||||||
|
navy: 'text-white bg-kc-navy-main border-kc-navy-main disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-navy-main',
|
||||||
|
lightNavy:
|
||||||
|
'text-kc-navy-main border-kc-navy-main bg-kc-navy-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-navy-main',
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: 'cursor-not-allowed',
|
||||||
|
},
|
||||||
|
isPending: {
|
||||||
|
true: 'text-transparent cursor-progress',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
color: 'primary',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:border-primary-tertiary hover:bg-primary-tertiary active:bg-primary-secondary active:border-primary-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'light',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-primary-tertiary01 active:text-primary-secondary active:bg-primary-tertiary01 active:border-primary-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'green',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-kc-green-tertiary hover:border-kc-green-tertiary active:bg-kc-green-secondary active:border-kc-green-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'lightGreen',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-kc-green-tertiary01 active:text-kc-green-secondary active:bg-kc-green-tertiary01 active:border-kc-green-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'black',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className: 'hover:bg-kc-black-47 hover:border-kc-black-47 active:bg-kc-black-22 active:border-kc-black-22',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'gray',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-kc-gray-da hover:border-kc-black-47 active:text-kc-black-22 active:bg-kc-gray-da active:border-kc-black-22',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'orange',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-kc-orange-tertiary02 active:text-kc-orange-secondary active:bg-kc-orange-tertiary02 active:border-kc-orange-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'navy',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:border-kc-navy-tertiary hover:bg-kc-navy-tertiary active:bg-kc-navy-secondary active:border-kc-navy-secondary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'lightNavy',
|
||||||
|
isDisabled: false,
|
||||||
|
isPending: false,
|
||||||
|
className:
|
||||||
|
'hover:bg-kc-navy-tertiary01 active:text-kc-navy-secondary active:bg-kc-navy-tertiary01 active:border-kc-navy-secondary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Button = (props: ButtonProps) => {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
color = 'primary',
|
||||||
|
size = 'medium',
|
||||||
|
children,
|
||||||
|
stopPropagation = false,
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaButton
|
||||||
|
className={composeRenderProps(className, (className, renderProps) => {
|
||||||
|
return button({ ...renderProps, color, size, className });
|
||||||
|
})}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (stopPropagation) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
onClick?.(e);
|
||||||
|
}}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{composeRenderProps(children, (children, { isPending }) => (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
{isPending && (
|
||||||
|
<span aria-hidden className="flex absolute inset-0 justify-center items-center">
|
||||||
|
<LoadingSpinnerIcon className="w-4 h-4 text-white animate-spin" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</AriaButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
182
web-app/app/shared/components/calendar/Calendar.tsx
Normal file
182
web-app/app/shared/components/calendar/Calendar.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import {
|
||||||
|
Button as AriaButton,
|
||||||
|
Calendar as AriaCalendar,
|
||||||
|
CalendarGridHeader as AriaCalendarGridHeader,
|
||||||
|
CalendarCell,
|
||||||
|
CalendarGrid,
|
||||||
|
CalendarGridBody,
|
||||||
|
CalendarHeaderCell,
|
||||||
|
Heading,
|
||||||
|
} from 'react-aria-components';
|
||||||
|
|
||||||
|
import { getLocalTimeZone, today } from '@internationalized/date';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from '@/components/icons';
|
||||||
|
|
||||||
|
import { calendarDateToDate, dateToCalendarDate } from './utils';
|
||||||
|
|
||||||
|
const cellStyles = tv({
|
||||||
|
base: 'flex h-[30px] w-[30px] cursor-default items-center justify-center rounded-full text-xs outline-none',
|
||||||
|
variants: {
|
||||||
|
isSelected: {
|
||||||
|
true: 'bg-primary font-bold text-white',
|
||||||
|
false: '',
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: 'cursor-default text-kc-gray-be',
|
||||||
|
},
|
||||||
|
isUnavailable: {
|
||||||
|
true: 'cursor-default text-kc-gray-be line-through',
|
||||||
|
},
|
||||||
|
isOutsideMonth: {
|
||||||
|
true: 'text-kc-gray-be',
|
||||||
|
},
|
||||||
|
isSunday: {
|
||||||
|
true: 'text-[#e48686]',
|
||||||
|
},
|
||||||
|
isSaturday: {
|
||||||
|
true: 'text-[#7b8cc8]',
|
||||||
|
},
|
||||||
|
isToday: {
|
||||||
|
true: 'font-bold text-primary',
|
||||||
|
},
|
||||||
|
isHovered: {
|
||||||
|
true: 'bg-primary-tertiary01',
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: 'ring-2 ring-offset-2 ring-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
isSelected: true,
|
||||||
|
isSunday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isSelected: true,
|
||||||
|
isSaturday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isSelected: true,
|
||||||
|
isToday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isSelected: true,
|
||||||
|
isHovered: true,
|
||||||
|
className: 'bg-primary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface CalendarProps {
|
||||||
|
value?: Date | null;
|
||||||
|
defaultValue?: Date | null;
|
||||||
|
onChange?: (date: Date | null) => void;
|
||||||
|
maxValue?: Date | null;
|
||||||
|
minValue?: Date | null;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Calendar({ value, defaultValue, onChange, maxValue, minValue, isDisabled, className }: CalendarProps) {
|
||||||
|
const todayDate = today(getLocalTimeZone());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaCalendar
|
||||||
|
aria-label="날짜 선택"
|
||||||
|
value={value !== undefined ? dateToCalendarDate(value ?? null) : undefined}
|
||||||
|
defaultValue={defaultValue !== undefined ? dateToCalendarDate(defaultValue ?? null) : undefined}
|
||||||
|
onChange={(val) => onChange?.(calendarDateToDate(val))}
|
||||||
|
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
||||||
|
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
firstDayOfWeek="sun"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<CalendarHeader />
|
||||||
|
<CalendarGrid className="border-spacing-0">
|
||||||
|
<CalendarGridHeader />
|
||||||
|
<CalendarGridBody>
|
||||||
|
{(date) => {
|
||||||
|
const dayOfWeek = date.toDate('UTC').getDay();
|
||||||
|
const isSunday = dayOfWeek === 0;
|
||||||
|
const isSaturday = dayOfWeek === 6;
|
||||||
|
const isToday = date.compare(todayDate) === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CalendarCell
|
||||||
|
date={date}
|
||||||
|
className={({
|
||||||
|
isSelected,
|
||||||
|
isDisabled: isCellDisabled,
|
||||||
|
isUnavailable,
|
||||||
|
isOutsideMonth,
|
||||||
|
isHovered,
|
||||||
|
isFocusVisible,
|
||||||
|
}) =>
|
||||||
|
cellStyles({
|
||||||
|
isSelected,
|
||||||
|
isDisabled: isCellDisabled,
|
||||||
|
isUnavailable,
|
||||||
|
isOutsideMonth,
|
||||||
|
isSunday: !isSelected && !isDisabled && !isOutsideMonth ? isSunday : false,
|
||||||
|
isSaturday: !isSelected && !isDisabled && !isOutsideMonth ? isSaturday : false,
|
||||||
|
isToday: !isSelected ? isToday : false,
|
||||||
|
isHovered: !isSelected ? isHovered : false,
|
||||||
|
isFocusVisible,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</AriaCalendar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const navButton = tv({
|
||||||
|
base: 'flex h-7 w-7 items-center justify-center rounded-sm border-none bg-transparent outline-none',
|
||||||
|
variants: {
|
||||||
|
isDisabled: {
|
||||||
|
true: 'cursor-default text-kc-gray-be',
|
||||||
|
false: 'cursor-pointer text-kc-black-34 hover:bg-kc-gray-eb',
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: 'ring-2 ring-offset-2 ring-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function CalendarHeader() {
|
||||||
|
return (
|
||||||
|
<header className="flex items-center gap-1 pb-3">
|
||||||
|
<AriaButton slot="previous" className={(renderProps) => navButton(renderProps)}>
|
||||||
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
|
</AriaButton>
|
||||||
|
<Heading className="mx-2 flex-1 text-center text-sm font-semibold text-kc-black-34" />
|
||||||
|
<AriaButton slot="next" className={(renderProps) => navButton(renderProps)}>
|
||||||
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
|
</AriaButton>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CalendarGridHeader() {
|
||||||
|
return (
|
||||||
|
<AriaCalendarGridHeader>
|
||||||
|
{(day) => (
|
||||||
|
<CalendarHeaderCell className="h-[30px] w-[30px] text-xs font-semibold text-kc-gray-99 first:text-[#e48686] last:text-[#7b8cc8]">
|
||||||
|
{day}
|
||||||
|
</CalendarHeaderCell>
|
||||||
|
)}
|
||||||
|
</AriaCalendarGridHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
195
web-app/app/shared/components/calendar/RangeCalendar.tsx
Normal file
195
web-app/app/shared/components/calendar/RangeCalendar.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import {
|
||||||
|
RangeCalendar as AriaRangeCalendar,
|
||||||
|
CalendarCell,
|
||||||
|
CalendarGrid,
|
||||||
|
CalendarGridBody,
|
||||||
|
type DateValue,
|
||||||
|
} from 'react-aria-components';
|
||||||
|
|
||||||
|
import { getLocalTimeZone, today } from '@internationalized/date';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import { CalendarGridHeader, CalendarHeader } from './Calendar';
|
||||||
|
import { calendarDateToDate, dateToCalendarDate } from './utils';
|
||||||
|
|
||||||
|
const rangeCell = tv({
|
||||||
|
base: 'flex h-full w-full items-center justify-center rounded-full text-xs',
|
||||||
|
variants: {
|
||||||
|
selectionState: {
|
||||||
|
none: '',
|
||||||
|
middle: '',
|
||||||
|
cap: 'bg-primary font-bold text-white',
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: 'text-kc-gray-be',
|
||||||
|
},
|
||||||
|
isOutsideMonth: {
|
||||||
|
true: 'text-kc-gray-be',
|
||||||
|
},
|
||||||
|
isSunday: {
|
||||||
|
true: 'text-[#e48686]',
|
||||||
|
},
|
||||||
|
isSaturday: {
|
||||||
|
true: 'text-[#7b8cc8]',
|
||||||
|
},
|
||||||
|
isToday: {
|
||||||
|
true: 'font-bold text-primary',
|
||||||
|
},
|
||||||
|
isHovered: {
|
||||||
|
true: '',
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: 'ring-2 ring-offset-2 ring-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
selectionState: 'cap',
|
||||||
|
isSunday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectionState: 'cap',
|
||||||
|
isSaturday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectionState: 'cap',
|
||||||
|
isToday: true,
|
||||||
|
className: 'text-white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectionState: 'none',
|
||||||
|
isHovered: true,
|
||||||
|
className: 'bg-primary-tertiary01 rounded-full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectionState: 'middle',
|
||||||
|
isHovered: true,
|
||||||
|
className: 'bg-primary-tertiary01',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface RangeCalendarProps {
|
||||||
|
value?: { start: Date; end: Date } | null;
|
||||||
|
defaultValue?: { start: Date; end: Date } | null;
|
||||||
|
onChange?: (value: { start: Date; end: Date } | null) => void;
|
||||||
|
maxValue?: Date | null;
|
||||||
|
minValue?: Date | null;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RangeCalendar({
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
maxValue,
|
||||||
|
minValue,
|
||||||
|
isDisabled,
|
||||||
|
className,
|
||||||
|
}: RangeCalendarProps) {
|
||||||
|
const todayDate = today(getLocalTimeZone());
|
||||||
|
|
||||||
|
const ariaValue =
|
||||||
|
value !== undefined
|
||||||
|
? value
|
||||||
|
? { start: dateToCalendarDate(value.start)!, end: dateToCalendarDate(value.end)! }
|
||||||
|
: null
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const ariaDefaultValue =
|
||||||
|
defaultValue !== undefined
|
||||||
|
? defaultValue
|
||||||
|
? { start: dateToCalendarDate(defaultValue.start)!, end: dateToCalendarDate(defaultValue.end)! }
|
||||||
|
: null
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaRangeCalendar<DateValue>
|
||||||
|
aria-label="날짜 범위 선택"
|
||||||
|
value={ariaValue}
|
||||||
|
defaultValue={ariaDefaultValue}
|
||||||
|
onChange={(range) => {
|
||||||
|
if (!range) {
|
||||||
|
onChange?.(null);
|
||||||
|
} else {
|
||||||
|
onChange?.({
|
||||||
|
start: calendarDateToDate(range.start)!,
|
||||||
|
end: calendarDateToDate(range.end)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
||||||
|
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
firstDayOfWeek="sun"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<CalendarHeader />
|
||||||
|
<CalendarGrid className="border-spacing-0 [&_td]:px-0 [&_td]:py-px">
|
||||||
|
<CalendarGridHeader />
|
||||||
|
<CalendarGridBody>
|
||||||
|
{(date) => {
|
||||||
|
const dayOfWeek = date.toDate('UTC').getDay();
|
||||||
|
const isSunday = dayOfWeek === 0;
|
||||||
|
const isSaturday = dayOfWeek === 6;
|
||||||
|
const isToday = date.compare(todayDate) === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CalendarCell
|
||||||
|
date={date}
|
||||||
|
className={({ isSelected, isSelectionStart, isSelectionEnd }) => {
|
||||||
|
const classes = ['group h-[30px] w-[30px] cursor-default text-sm outline-none'];
|
||||||
|
if (isSelected) {
|
||||||
|
classes.push('bg-primary/10');
|
||||||
|
if (isSelectionStart) classes.push('rounded-s-full');
|
||||||
|
if (isSelectionEnd) classes.push('rounded-e-full');
|
||||||
|
}
|
||||||
|
return classes.join(' ');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
formattedDate,
|
||||||
|
isSelected,
|
||||||
|
isSelectionStart,
|
||||||
|
isSelectionEnd,
|
||||||
|
isDisabled: isCellDisabled,
|
||||||
|
isOutsideMonth,
|
||||||
|
isHovered,
|
||||||
|
isFocusVisible,
|
||||||
|
}) => {
|
||||||
|
const selectionState =
|
||||||
|
isSelected && (isSelectionStart || isSelectionEnd)
|
||||||
|
? ('cap' as const)
|
||||||
|
: isSelected
|
||||||
|
? ('middle' as const)
|
||||||
|
: ('none' as const);
|
||||||
|
const showDayColor = selectionState !== 'cap' && !isCellDisabled && !isOutsideMonth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={rangeCell({
|
||||||
|
selectionState,
|
||||||
|
isDisabled: isCellDisabled,
|
||||||
|
isOutsideMonth,
|
||||||
|
isSunday: showDayColor ? isSunday : false,
|
||||||
|
isSaturday: showDayColor ? isSaturday : false,
|
||||||
|
isToday: selectionState !== 'cap' ? isToday : false,
|
||||||
|
isHovered: !isSelected ? isHovered : false,
|
||||||
|
isFocusVisible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formattedDate}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</CalendarCell>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</AriaRangeCalendar>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
web-app/app/shared/components/calendar/index.ts
Normal file
4
web-app/app/shared/components/calendar/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { Calendar } from './Calendar';
|
||||||
|
export type { CalendarProps } from './Calendar';
|
||||||
|
export { RangeCalendar } from './RangeCalendar';
|
||||||
|
export type { RangeCalendarProps } from './RangeCalendar';
|
||||||
13
web-app/app/shared/components/calendar/utils.ts
Normal file
13
web-app/app/shared/components/calendar/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { CalendarDate, type DateValue, fromDate, toCalendarDate } from '@internationalized/date';
|
||||||
|
|
||||||
|
const TIMEZONE = 'Asia/Seoul';
|
||||||
|
|
||||||
|
export function dateToCalendarDate(date: Date | null): CalendarDate | null {
|
||||||
|
if (!date) return null;
|
||||||
|
return toCalendarDate(fromDate(date, TIMEZONE));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calendarDateToDate(value: DateValue | null): Date | null {
|
||||||
|
if (!value) return null;
|
||||||
|
return value.toDate(TIMEZONE);
|
||||||
|
}
|
||||||
79
web-app/app/shared/components/datePicker/DatePicker.tsx
Normal file
79
web-app/app/shared/components/datePicker/DatePicker.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { DatePicker as AriaDatePicker, Button, Dialog, Group, OverlayArrow, Popover } from 'react-aria-components';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import { Calendar } from '../calendar';
|
||||||
|
import { calendarDateToDate, dateToCalendarDate } from '../calendar/utils';
|
||||||
|
import { CalendarIcon } from '../icons';
|
||||||
|
|
||||||
|
const trigger = tv({
|
||||||
|
base: 'flex h-9 w-[156px] cursor-pointer items-center border border-kc-gray-be bg-white text-left outline-none transition',
|
||||||
|
variants: {
|
||||||
|
isHovered: {
|
||||||
|
true: 'border-kc-black-34',
|
||||||
|
},
|
||||||
|
isFocused: {
|
||||||
|
true: 'border-kc-black-34',
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: 'cursor-default border-kc-gray-99 bg-kc-gray-eb',
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: 'ring-2 ring-offset-2 ring-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DatePickerProps = {
|
||||||
|
value: Date | null;
|
||||||
|
onChange: (date: Date | null) => void;
|
||||||
|
maxValue?: Date | null;
|
||||||
|
minValue?: Date | null;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DatePicker({ value, onChange, maxValue, minValue, isDisabled, className, name }: DatePickerProps) {
|
||||||
|
return (
|
||||||
|
<AriaDatePicker
|
||||||
|
aria-label="날짜 선택"
|
||||||
|
value={dateToCalendarDate(value)}
|
||||||
|
onChange={(val) => onChange(calendarDateToDate(val))}
|
||||||
|
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
||||||
|
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
granularity="day"
|
||||||
|
firstDayOfWeek="sun"
|
||||||
|
className={className}
|
||||||
|
name={name}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<Button className={(renderProps) => trigger(renderProps)}>
|
||||||
|
<span className={`flex-1 px-3 text-sm ${value ? 'text-kc-black-34' : 'text-kc-gray-99'}`}>
|
||||||
|
{value ? dayjs(value).format('YYYY.MM.DD') : 'YYYY.MM.DD'}
|
||||||
|
</span>
|
||||||
|
<span className="flex h-full w-8 shrink-0 items-center justify-center text-kc-gray-99">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Popover className="bg-white border border-kc-gray-be p-3 px-6 pb-6 drop-shadow-modal outline-none" offset={12}>
|
||||||
|
<OverlayArrow className="group/arrow">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 12 8"
|
||||||
|
className="block fill-white stroke-kc-gray-be group-data-[placement=bottom]/arrow:rotate-180"
|
||||||
|
>
|
||||||
|
<path d="M0 0 L6 8 L12 0" />
|
||||||
|
</svg>
|
||||||
|
</OverlayArrow>
|
||||||
|
<Dialog className="outline-none">
|
||||||
|
<Calendar />
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
</AriaDatePicker>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
web-app/app/shared/components/datePicker/DateRangePicker.tsx
Normal file
113
web-app/app/shared/components/datePicker/DateRangePicker.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
DateRangePicker as AriaDateRangePicker,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Group,
|
||||||
|
OverlayArrow,
|
||||||
|
Popover,
|
||||||
|
} from 'react-aria-components';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import { CalendarIcon } from '@/components/icons';
|
||||||
|
|
||||||
|
import { RangeCalendar } from '../calendar/RangeCalendar';
|
||||||
|
import { calendarDateToDate, dateToCalendarDate } from '../calendar/utils';
|
||||||
|
|
||||||
|
const trigger = tv({
|
||||||
|
base: 'flex h-9 w-[260px] cursor-pointer items-center border border-kc-gray-be bg-white text-left outline-none transition',
|
||||||
|
variants: {
|
||||||
|
isHovered: {
|
||||||
|
true: 'border-kc-black-34',
|
||||||
|
},
|
||||||
|
isFocused: {
|
||||||
|
true: 'border-kc-black-34',
|
||||||
|
},
|
||||||
|
isDisabled: {
|
||||||
|
true: 'cursor-default border-kc-gray-99 bg-kc-gray-eb',
|
||||||
|
},
|
||||||
|
isFocusVisible: {
|
||||||
|
true: 'ring-2 ring-offset-2 ring-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DateRangePickerProps = {
|
||||||
|
value: { start: Date; end: Date } | null;
|
||||||
|
onChange: (value: { start: Date; end: Date } | null) => void;
|
||||||
|
maxValue?: Date | null;
|
||||||
|
minValue?: Date | null;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
startName?: string;
|
||||||
|
endName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DateRangePicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
maxValue,
|
||||||
|
minValue,
|
||||||
|
isDisabled,
|
||||||
|
className,
|
||||||
|
startName,
|
||||||
|
endName,
|
||||||
|
}: DateRangePickerProps) {
|
||||||
|
const ariaValue = value ? { start: dateToCalendarDate(value.start)!, end: dateToCalendarDate(value.end)! } : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AriaDateRangePicker
|
||||||
|
aria-label="날짜 범위 선택"
|
||||||
|
value={ariaValue}
|
||||||
|
onChange={(range) => {
|
||||||
|
if (!range) {
|
||||||
|
onChange(null);
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
start: calendarDateToDate(range.start)!,
|
||||||
|
end: calendarDateToDate(range.end)!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxValue={maxValue !== undefined ? dateToCalendarDate(maxValue) : undefined}
|
||||||
|
minValue={minValue !== undefined ? dateToCalendarDate(minValue) : undefined}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
granularity="day"
|
||||||
|
firstDayOfWeek="sun"
|
||||||
|
className={className}
|
||||||
|
startName={startName}
|
||||||
|
endName={endName}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<Button className={(renderProps) => trigger(renderProps)}>
|
||||||
|
<span className={`tabular-nums px-3 text-sm ${value?.start ? 'text-kc-black-34' : 'text-kc-gray-99'}`}>
|
||||||
|
{value?.start ? dayjs(value.start).format('YYYY. MM. DD') : 'YYYY.MM.DD'}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-kc-gray-99">~</span>
|
||||||
|
<span className={`tabular-nums flex-1 px-3 text-sm ${value?.end ? 'text-kc-black-34' : 'text-kc-gray-99'}`}>
|
||||||
|
{value?.end ? dayjs(value.end).format('YYYY. MM. DD') : 'YYYY.MM.DD'}
|
||||||
|
</span>
|
||||||
|
<span className="flex h-full w-8 shrink-0 items-center justify-center text-kc-gray-99">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Popover className="bg-white border border-kc-gray-be p-3 px-6 pb-6 drop-shadow-modal outline-none" offset={12}>
|
||||||
|
<OverlayArrow className="group/arrow">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 12 8"
|
||||||
|
className="block fill-white stroke-kc-gray-be group-data-[placement=bottom]/arrow:rotate-180"
|
||||||
|
>
|
||||||
|
<path d="M0 0 L6 8 L12 0" />
|
||||||
|
</svg>
|
||||||
|
</OverlayArrow>
|
||||||
|
<Dialog className="outline-none">
|
||||||
|
<RangeCalendar />
|
||||||
|
</Dialog>
|
||||||
|
</Popover>
|
||||||
|
</AriaDateRangePicker>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
web-app/app/shared/components/datePicker/index.ts
Normal file
4
web-app/app/shared/components/datePicker/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { DatePicker } from './DatePicker';
|
||||||
|
export type { DatePickerProps } from './DatePicker';
|
||||||
|
export { DateRangePicker } from './DateRangePicker';
|
||||||
|
export type { DateRangePickerProps } from './DateRangePicker';
|
||||||
23
web-app/app/shared/components/icons/Calendar.tsx
Normal file
23
web-app/app/shared/components/icons/Calendar.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function CalendarIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 2v4" />
|
||||||
|
<path d="M16 2v4" />
|
||||||
|
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||||
|
<path d="M3 10h18" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
web-app/app/shared/components/icons/ChevronLeft.tsx
Normal file
20
web-app/app/shared/components/icons/ChevronLeft.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function ChevronLeftIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="m15 18-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
web-app/app/shared/components/icons/ChevronRight.tsx
Normal file
20
web-app/app/shared/components/icons/ChevronRight.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function ChevronRightIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="m9 18 6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
web-app/app/shared/components/icons/LoadingSpinner.tsx
Normal file
20
web-app/app/shared/components/icons/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { type SVGProps } from 'react';
|
||||||
|
|
||||||
|
export function LoadingSpinnerIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" {...props}>
|
||||||
|
<circle cx="12" cy="12" r="10" strokeWidth="4" fill="none" className="opacity-25" />
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
strokeWidth="4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
fill="none"
|
||||||
|
pathLength="100"
|
||||||
|
strokeDasharray="60 140"
|
||||||
|
strokeDashoffset="0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
web-app/app/shared/components/icons/index.ts
Normal file
5
web-app/app/shared/components/icons/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './Calendar';
|
||||||
|
export * from './ChevronLeft';
|
||||||
|
export * from './ChevronRight';
|
||||||
|
export * from './LoadingSpinner';
|
||||||
|
|
||||||
16
web-app/app/shared/components/inputGroup/Input.tsx
Normal file
16
web-app/app/shared/components/inputGroup/Input.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Ref } from 'react';
|
||||||
|
import type { InputProps as AriaInputProps } from 'react-aria-components';
|
||||||
|
import { Input as AriaInput } from 'react-aria-components';
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import style from './style';
|
||||||
|
|
||||||
|
export interface InputProps extends AriaInputProps {
|
||||||
|
className?: string;
|
||||||
|
ref?: Ref<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
export default function Input(props: InputProps) {
|
||||||
|
const { className, ...restProps } = props;
|
||||||
|
return <AriaInput className={twMerge(style().input(), className)} {...restProps} />;
|
||||||
|
}
|
||||||
51
web-app/app/shared/components/inputGroup/InputGroup.tsx
Normal file
51
web-app/app/shared/components/inputGroup/InputGroup.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export interface InputGroupProps extends PropsWithChildren {
|
||||||
|
className?: string;
|
||||||
|
innerClassName?: string;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
}
|
||||||
|
const style = tv({
|
||||||
|
slots: {
|
||||||
|
container: 'relative flex flex-col justify-center items-start bg-white',
|
||||||
|
base: [
|
||||||
|
'h-9',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'text-sm',
|
||||||
|
'text-kc-black-34',
|
||||||
|
'leading-[18px]',
|
||||||
|
'border',
|
||||||
|
'border-kc-gray-be',
|
||||||
|
'has-data-focused:border-kc-black-34',
|
||||||
|
'has-data-hovered:border-kc-black-34',
|
||||||
|
'has-data-disabled:bg-kc-gray-eb',
|
||||||
|
'has-data-disabled:text-kc-gray-99',
|
||||||
|
'has-data-disabled:border-kc-gray-be',
|
||||||
|
'has-data-invalid:border-kc-red',
|
||||||
|
'has-data-invalid:has-data-focused:border-kc-red',
|
||||||
|
'has-data-invalid:has-data-hovered:border-kc-red',
|
||||||
|
'w-full',
|
||||||
|
'has-[[type=number]]:leading-6',
|
||||||
|
],
|
||||||
|
input: ['w-full', 'px-3', 'outline-0', '[&[type=number]]:pr-1.5'],
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isReadOnly: {
|
||||||
|
true: {
|
||||||
|
base: 'bg-kc-yellow-secondary has-data-hovered:border-kc-gray-be has-data-focused:border-kc-gray-be',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const InputGroup = ({ className, innerClassName, isReadOnly = false, children }: InputGroupProps) => {
|
||||||
|
const styles = style({ isReadOnly });
|
||||||
|
return (
|
||||||
|
<div className={twMerge(styles.container(), className)}>
|
||||||
|
<div className={twMerge(styles.base(), innerClassName)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
6
web-app/app/shared/components/inputGroup/index.ts
Normal file
6
web-app/app/shared/components/inputGroup/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import InternalInput from './Input';
|
||||||
|
import { InputGroup as InternalInputGroup } from './InputGroup';
|
||||||
|
|
||||||
|
export const InputGroup = Object.assign(InternalInputGroup, {
|
||||||
|
Input: InternalInput,
|
||||||
|
});
|
||||||
121
web-app/app/shared/components/pagination/Pagination.tsx
Normal file
121
web-app/app/shared/components/pagination/Pagination.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { Button as AriaButton } from 'react-aria-components';
|
||||||
|
|
||||||
|
type PaginationProps = {
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
pageCount?: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://design-system.w3.org/components/pagination.html
|
||||||
|
export const Pagination = ({ totalPages, currentPage, pageCount = 10, onPageChange }: PaginationProps) => {
|
||||||
|
const start = useMemo(() => {
|
||||||
|
return Math.floor(currentPage / pageCount) * pageCount;
|
||||||
|
}, [currentPage, pageCount]);
|
||||||
|
|
||||||
|
const handlePageClick = useCallback(
|
||||||
|
(page: number) => () => {
|
||||||
|
if (typeof onPageChange === 'function' && page >= 0 && page < totalPages) {
|
||||||
|
onPageChange(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[totalPages, onPageChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageArray = useMemo(() => {
|
||||||
|
const arr: number[] = [];
|
||||||
|
|
||||||
|
// totalPage가 0이면 아무것도 안나와서 최소 1이 되도록 수정
|
||||||
|
const _totalPages = totalPages > 0 ? totalPages : 1;
|
||||||
|
if (Number.isNaN(start)) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
const pageNumber = start + i;
|
||||||
|
|
||||||
|
if (pageNumber >= _totalPages) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push(pageNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, [start, pageCount, totalPages]);
|
||||||
|
|
||||||
|
const noPrev = start === 0;
|
||||||
|
const noNext = start + pageCount >= totalPages;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="text-kc-gray-99 text-sm" aria-label="pagination">
|
||||||
|
<ul className="flex items-center justify-center list-none">
|
||||||
|
<li className="flex">
|
||||||
|
<AriaButton
|
||||||
|
className="cursor-pointer data-[disabled=true]:cursor-default"
|
||||||
|
isDisabled={noPrev}
|
||||||
|
aria-label="첫 페이지"
|
||||||
|
onClick={handlePageClick(0)}
|
||||||
|
>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="15.8571" y="15" width="1.5" height="10" fill="currentColor" />
|
||||||
|
<path d="M24.1429 15.3571L19.1429 19.8571L24.1429 24.3571" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</AriaButton>
|
||||||
|
</li>
|
||||||
|
<li className="mr-2.5 flex">
|
||||||
|
<AriaButton
|
||||||
|
className="cursor-pointer data-[disabled=true]:cursor-default"
|
||||||
|
isDisabled={noPrev}
|
||||||
|
aria-label="이전 페이지"
|
||||||
|
onClick={handlePageClick(start - 1)}
|
||||||
|
>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22.5 15.5L17.5 20L22.5 24.5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</AriaButton>
|
||||||
|
</li>
|
||||||
|
{pageArray.map((pageNumber) => (
|
||||||
|
<li key={pageNumber}>
|
||||||
|
<AriaButton
|
||||||
|
isDisabled={currentPage === pageNumber}
|
||||||
|
className="w-10 h-10 flex items-center justify-center tabular-nums not-disabled:cursor-pointer aria-[current=page]:text-sm aria-[current=page]:text-primary aria-[current=page]:font-bold"
|
||||||
|
onClick={handlePageClick(pageNumber)}
|
||||||
|
aria-current={currentPage === pageNumber ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
{pageNumber + 1}
|
||||||
|
</AriaButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li className="ml-2.5 flex">
|
||||||
|
<AriaButton
|
||||||
|
isDisabled={noNext}
|
||||||
|
className="cursor-pointer data-[disabled=true]:cursor-default"
|
||||||
|
// className="text-[#CCCCCC]"
|
||||||
|
aria-label="다음 페이지"
|
||||||
|
onClick={handlePageClick(start + pageCount)}
|
||||||
|
>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.5 15.5L22.5 20L17.5 24.5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
</svg>
|
||||||
|
</AriaButton>
|
||||||
|
</li>
|
||||||
|
<li className="flex">
|
||||||
|
<AriaButton
|
||||||
|
isDisabled={noNext}
|
||||||
|
className="cursor-pointer data-[disabled=true]:cursor-default"
|
||||||
|
// className="text-[#CCCCCC]"
|
||||||
|
aria-label="마지막 페이지"
|
||||||
|
onClick={handlePageClick(totalPages - 1)}
|
||||||
|
>
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.75 15.5L20.75 20L15.75 24.5" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path d="M24.25 15H22.75V25H24.25V15Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</AriaButton>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
web-app/app/shared/components/section/Section.tsx
Normal file
23
web-app/app/shared/components/section/Section.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
type SectionVariant = 'base' | 'card' | 'list' | 'searchFilterGray';
|
||||||
|
|
||||||
|
const variantClassMap: Record<SectionVariant, string> = {
|
||||||
|
base: 'flex-center w-full bg-white shadow-card p-6',
|
||||||
|
card: 'flex-center bg-white shadow-card p-6',
|
||||||
|
list: 'flex flex-col h-full min-h-0 gap-4 bg-white shadow-card p-6',
|
||||||
|
searchFilterGray: 'flex items-center bg-gray-100 py-3 px-7 text-kc-black-2a text-sm font-medium gap-3',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SectionProps = {
|
||||||
|
variant: SectionVariant;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Section = ({ variant, children, className }: SectionProps) => {
|
||||||
|
const baseClassName = variantClassMap[variant];
|
||||||
|
const mergedClassName = className ? `${baseClassName} ${className}` : baseClassName;
|
||||||
|
|
||||||
|
return <section className={mergedClassName}>{children}</section>;
|
||||||
|
};
|
||||||
357
web-app/app/shared/components/table/Table.tsx
Normal file
357
web-app/app/shared/components/table/Table.tsx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { createContext, useContext, type ReactNode, type HTMLAttributes, type TdHTMLAttributes, type ThHTMLAttributes } from 'react';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Styles
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const tableStyles = tv({
|
||||||
|
slots: {
|
||||||
|
wrapper: 'flex flex-col gap-2 w-full',
|
||||||
|
scrollContainer: 'relative flex-1 overflow-y-auto overflow-x-auto min-h-0',
|
||||||
|
table: 'w-full border-collapse',
|
||||||
|
thead: 'sticky top-0 z-1 bg-primary-tertiary02',
|
||||||
|
th: 'px-2 h-[42px] align-middle whitespace-pre-line text-sm font-bold text-primary-secondary',
|
||||||
|
thGrouped: 'h-[38px] border-l border-r border-white text-sm font-bold text-primary-secondary',
|
||||||
|
thGroupedBottom: 'h-[38px] border-l border-r border-b border-white text-sm font-bold text-primary-secondary',
|
||||||
|
tr: 'cursor-default transition-colors duration-200',
|
||||||
|
trEmpty: 'text-center align-middle text-sm text-dabeeo-gray-44',
|
||||||
|
td: 'border-b border-dabeeo-gray-eb py-2.5 px-2 text-sm text-dabeeo-gray-44 align-middle whitespace-nowrap overflow-hidden text-ellipsis font-medium',
|
||||||
|
caption: 'flex justify-between text-sm',
|
||||||
|
captionLeft: 'flex items-end gap-6',
|
||||||
|
total: 'font-medium text-dabeeo-gray-44',
|
||||||
|
totalCount: 'font-bold text-primary',
|
||||||
|
loading: 'flex py-2 gap-2 w-full items-center justify-center h-16',
|
||||||
|
spinner: 'w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isLayoutFixed: {
|
||||||
|
true: { table: 'table-fixed' },
|
||||||
|
},
|
||||||
|
isFullHeight: {
|
||||||
|
true: { wrapper: 'h-full' },
|
||||||
|
},
|
||||||
|
isHoverable: {
|
||||||
|
true: { tr: 'hover:bg-dabeeo-gray-eb' },
|
||||||
|
},
|
||||||
|
isClickable: {
|
||||||
|
true: { tr: 'cursor-pointer' },
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
true: { tr: 'bg-primary-tertiary01' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Context
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type TableContextValue = {
|
||||||
|
isLayoutFixed: boolean;
|
||||||
|
isFullHeight: boolean;
|
||||||
|
isHoverable: boolean;
|
||||||
|
isClickable: boolean;
|
||||||
|
columnCount: number;
|
||||||
|
setColumnCount: (count: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableContext = createContext<TableContextValue | null>(null);
|
||||||
|
|
||||||
|
function useTableContext() {
|
||||||
|
const context = useContext(TableContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Table compound components must be used within a Table component');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Table (Root)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type TableRootProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
isLayoutFixed?: boolean;
|
||||||
|
isFullHeight?: boolean;
|
||||||
|
isHoverable?: boolean;
|
||||||
|
isClickable?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TableRoot({
|
||||||
|
children,
|
||||||
|
isLayoutFixed = true,
|
||||||
|
isFullHeight = true,
|
||||||
|
isHoverable = true,
|
||||||
|
isClickable = true,
|
||||||
|
className,
|
||||||
|
}: TableRootProps) {
|
||||||
|
const [columnCount, setColumnCount] = useState(0);
|
||||||
|
const styles = tableStyles({ isLayoutFixed, isFullHeight });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContext.Provider
|
||||||
|
value={{ isLayoutFixed, isFullHeight, isHoverable, isClickable, columnCount, setColumnCount }}
|
||||||
|
>
|
||||||
|
<div className={styles.wrapper({ className })}>{children}</div>
|
||||||
|
</TableContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Caption
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type CaptionProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Caption({ children, className }: CaptionProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return <div className={styles.caption({ className })}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptionLeftProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CaptionLeft({ children, className }: CaptionLeftProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return <div className={styles.captionLeft({ className })}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptionRightProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function CaptionRight({ children, className }: CaptionRightProps) {
|
||||||
|
return <div className={className}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TotalProps = {
|
||||||
|
count: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Total({ count, className }: TotalProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return (
|
||||||
|
<div className={styles.total({ className })}>
|
||||||
|
Total <span className={styles.totalCount()}>{count.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Container (scroll wrapper + table)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type ContainerProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Container({ children, className }: ContainerProps) {
|
||||||
|
const { isLayoutFixed } = useTableContext();
|
||||||
|
const styles = tableStyles({ isLayoutFixed });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.scrollContainer({ className })}>
|
||||||
|
<table className={styles.table()}>{children}</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Colgroup
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type ColgroupProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Colgroup({ children }: ColgroupProps) {
|
||||||
|
return <colgroup>{children}</colgroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColProps = {
|
||||||
|
width?: number | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Col({ width }: ColProps) {
|
||||||
|
const widthStyle = typeof width === 'number' ? `${width}px` : width || 'auto';
|
||||||
|
return <col style={{ width: widthStyle }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Header
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Header({ children, className }: HeaderProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return <thead className={styles.thead({ className })}>{children}</thead>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderRowProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function HeaderRow({ children, className }: HeaderRowProps) {
|
||||||
|
return <tr className={className}>{children}</tr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderCellProps = ThHTMLAttributes<HTMLTableCellElement> & {
|
||||||
|
children?: ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
isGroupTitle?: boolean;
|
||||||
|
isGroupChild?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function HeaderCell({
|
||||||
|
children,
|
||||||
|
align = 'left',
|
||||||
|
isGroupTitle = false,
|
||||||
|
isGroupChild = false,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: HeaderCellProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
|
||||||
|
let cellClass = styles.th();
|
||||||
|
if (isGroupTitle) {
|
||||||
|
cellClass = styles.thGroupedBottom();
|
||||||
|
} else if (isGroupChild) {
|
||||||
|
cellClass = styles.thGrouped();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th align={align} className={`${cellClass} ${className || ''}`} style={style} {...props}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Body
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type BodyProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Body({ children, className }: BodyProps) {
|
||||||
|
return <tbody className={className}>{children}</tbody>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowProps = HTMLAttributes<HTMLTableRowElement> & {
|
||||||
|
children: ReactNode;
|
||||||
|
isSelected?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Row({ children, isSelected = false, className, ...props }: RowProps) {
|
||||||
|
const { isHoverable, isClickable } = useTableContext();
|
||||||
|
const styles = tableStyles({ isHoverable, isClickable, isSelected });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={styles.tr({ className })} {...props}>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CellProps = TdHTMLAttributes<HTMLTableCellElement> & {
|
||||||
|
children?: ReactNode;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Cell({ children, align = 'left', className, style, ...props }: CellProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td className={styles.td({ className })} style={{ textAlign: align, ...style }} {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Empty & Loading
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
type EmptyProps = {
|
||||||
|
colSpan: number;
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Empty({ colSpan, children = '데이터가 없습니다.', className }: EmptyProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return (
|
||||||
|
<tr className={styles.trEmpty({ className })}>
|
||||||
|
<td colSpan={colSpan} className="py-2.5">
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoadingProps = {
|
||||||
|
colSpan: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Loading({ colSpan, className }: LoadingProps) {
|
||||||
|
const styles = tableStyles();
|
||||||
|
return (
|
||||||
|
<tr className={styles.trEmpty({ className })}>
|
||||||
|
<td colSpan={colSpan} className="py-2.5">
|
||||||
|
<div className={styles.loading()}>
|
||||||
|
<div className={styles.spinner()} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Import useState
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Export
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const Table = Object.assign(TableRoot, {
|
||||||
|
Caption,
|
||||||
|
CaptionLeft,
|
||||||
|
CaptionRight,
|
||||||
|
Total,
|
||||||
|
Container,
|
||||||
|
Colgroup,
|
||||||
|
Col,
|
||||||
|
Header,
|
||||||
|
HeaderRow,
|
||||||
|
HeaderCell,
|
||||||
|
Body,
|
||||||
|
Row,
|
||||||
|
Cell,
|
||||||
|
Empty,
|
||||||
|
Loading,
|
||||||
|
});
|
||||||
1
web-app/app/shared/components/table/index.ts
Normal file
1
web-app/app/shared/components/table/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Table } from './Table';
|
||||||
@@ -13,41 +13,56 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
{
|
{
|
||||||
id: 'inference',
|
id: 'inference',
|
||||||
name: '추론관리',
|
name: '추론관리',
|
||||||
children: [{ id: 'inference-list', name: '추론 목록', menuUrl: '/inference' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'model',
|
|
||||||
name: '모델관리',
|
|
||||||
children: [{ id: 'model-list', name: '모델 목록', menuUrl: '/model' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'labeling',
|
|
||||||
name: '라벨링',
|
|
||||||
children: [
|
children: [
|
||||||
{ id: 'label', name: '라벨링 작업', menuUrl: '/labeling/label' },
|
{ id: 'terrain', name: '지형변화탐지', menuUrl: '/terrain' },
|
||||||
{ id: 'review', name: '라벨링 검수', menuUrl: '/labeling/review' },
|
{ id: 'object', name: '객체탐지', menuUrl: '/object' },
|
||||||
|
{ id: 'schedule', name: '스케줄관리', menuUrl: '/schedule' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'log',
|
id: 'labeling-management',
|
||||||
name: '로그',
|
name: '라벨링관리',
|
||||||
children: [
|
children: [
|
||||||
{ id: 'audit', name: '감사 로그', menuUrl: '/log/audit' },
|
{ id: 'change-labeling', name: '변화탐지 라벨링관리', menuUrl: '/labeling-management/change' },
|
||||||
{ id: 'system', name: '시스템 로그', menuUrl: '/log/system' },
|
{ id: 'object-labeling', name: '객체탐지 라벨링관리', menuUrl: '/labeling-management/object' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'schedule',
|
id: 'change-model',
|
||||||
name: '스케줄관리',
|
name: '변화모델관리',
|
||||||
children: [{ id: 'schedule-list', name: '스케줄 목록', menuUrl: '/schedule' }],
|
children: [
|
||||||
|
{ id: 'model-g1', name: 'G1 모델관리', menuUrl: '/change-model/g1' },
|
||||||
|
{ id: 'model-g2', name: 'G2 모델관리', menuUrl: '/change-model/g2' },
|
||||||
|
{ id: 'model-g3', name: 'G3 모델관리', menuUrl: '/change-model/g3' },
|
||||||
|
{ id: 'model-classification', name: '분류모델관리', menuUrl: '/change-model/classification' },
|
||||||
|
{ id: 'parameter-change', name: '파라미터관리', menuUrl: '/change-model/parameter' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'detection-model',
|
||||||
|
name: '탐지모델관리',
|
||||||
|
children: [
|
||||||
|
{ id: 'model-g1', name: 'G1 모델관리', menuUrl: '/detection-model/g1' },
|
||||||
|
{ id: 'model-g2', name: 'G2 모델관리', menuUrl: '/detection-model/g2' },
|
||||||
|
{ id: 'model-g3', name: 'G3 모델관리', menuUrl: '/detection-model/g3' },
|
||||||
|
{ id: 'model-classification', name: '분류모델관리', menuUrl: '/detection-model/classification' },
|
||||||
|
{ id: 'parameter-change', name: '파라미터관리', menuUrl: '/detection-model/parameter' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'system',
|
id: 'system',
|
||||||
name: '시스템관리',
|
name: '시스템관리',
|
||||||
children: [
|
children: [
|
||||||
{ id: 'code', name: '공통코드 관리', menuUrl: '/code' },
|
{ id: 'code', name: '공통코드 관리', menuUrl: '/code' },
|
||||||
{ id: 'hyper-parameter', name: '하이퍼파라미터 설정', menuUrl: '/hyper-parameter' },
|
|
||||||
{ id: 'user', name: '사용자 관리', menuUrl: '/user' },
|
{ id: 'user', name: '사용자 관리', menuUrl: '/user' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'log',
|
||||||
|
name: '로그관리',
|
||||||
|
children: [
|
||||||
|
{ id: 'audit', name: '감사 로그', menuUrl: '/log/audit' },
|
||||||
|
{ id: 'error', name: '에러 로그', menuUrl: '/log/error' },
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.72.1",
|
"react-hook-form": "^7.72.1",
|
||||||
"react-router": "7.14.0",
|
"react-router": "7.14.0",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
54
web-app/pnpm-lock.yaml
generated
54
web-app/pnpm-lock.yaml
generated
@@ -41,6 +41,12 @@ importers:
|
|||||||
react-router:
|
react-router:
|
||||||
specifier: 7.14.0
|
specifier: 7.14.0
|
||||||
version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0
|
||||||
|
tailwind-variants:
|
||||||
|
specifier: ^3.2.2
|
||||||
|
version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2)
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.6
|
specifier: ^4.3.6
|
||||||
version: 4.3.6
|
version: 4.3.6
|
||||||
@@ -1170,42 +1176,36 @@ packages:
|
|||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
|
resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
|
resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
|
resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
|
resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
|
resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
|
||||||
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
|
resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
|
||||||
@@ -1267,79 +1267,66 @@ packages:
|
|||||||
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
|
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
||||||
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
|
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
|
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
|
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
|
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
|
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
|
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
|
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
|
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
|
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
|
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
||||||
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
|
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.60.1':
|
'@rollup/rollup-linux-x64-musl@4.60.1':
|
||||||
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
|
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.60.1':
|
'@rollup/rollup-openbsd-x64@4.60.1':
|
||||||
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
|
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
|
||||||
@@ -1418,28 +1405,24 @@ packages:
|
|||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
|
'@tailwindcss/oxide-linux-arm64-musl@4.2.2':
|
||||||
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
|
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
|
'@tailwindcss/oxide-linux-x64-gnu@4.2.2':
|
||||||
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
|
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
|
'@tailwindcss/oxide-linux-x64-musl@4.2.2':
|
||||||
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
|
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
|
'@tailwindcss/oxide-wasm32-wasi@4.2.2':
|
||||||
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
|
resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
|
||||||
@@ -2076,28 +2059,24 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.32.0:
|
lightningcss-linux-arm64-musl@1.32.0:
|
||||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.32.0:
|
lightningcss-linux-x64-gnu@1.32.0:
|
||||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.32.0:
|
lightningcss-linux-x64-musl@1.32.0:
|
||||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.32.0:
|
lightningcss-win32-arm64-msvc@1.32.0:
|
||||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||||
@@ -2464,6 +2443,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
tailwind-merge@3.5.0:
|
||||||
|
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
|
||||||
|
|
||||||
|
tailwind-variants@3.2.2:
|
||||||
|
resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==}
|
||||||
|
engines: {node: '>=16.x', pnpm: '>=7.x'}
|
||||||
|
peerDependencies:
|
||||||
|
tailwind-merge: '>=3.0.0'
|
||||||
|
tailwindcss: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
tailwind-merge:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tailwindcss@4.2.2:
|
tailwindcss@4.2.2:
|
||||||
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
|
||||||
|
|
||||||
@@ -5570,6 +5562,14 @@ snapshots:
|
|||||||
|
|
||||||
statuses@2.0.2: {}
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
tailwind-merge@3.5.0: {}
|
||||||
|
|
||||||
|
tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2):
|
||||||
|
dependencies:
|
||||||
|
tailwindcss: 4.2.2
|
||||||
|
optionalDependencies:
|
||||||
|
tailwind-merge: 3.5.0
|
||||||
|
|
||||||
tailwindcss@4.2.2: {}
|
tailwindcss@4.2.2: {}
|
||||||
|
|
||||||
tapable@2.3.2: {}
|
tapable@2.3.2: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user