feat: [공통] UI 컴포넌트 개선 (TabChip, FormField, Select 등)

- TabChip, FormField, MobileCard, Select 컴포넌트 개선
- IntegratedListTemplateV2, UniversalListPage 타입 보강
- LoginPage UI 개선

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-25 22:32:52 +09:00
parent 27a7773d95
commit 81a016ada9
6 changed files with 18 additions and 19 deletions

View File

@@ -42,25 +42,19 @@ export function TabChip({
<button
onClick={onClick}
className={`
flex items-center gap-2 px-4 py-2.5 rounded-full border transition-all
inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg border text-sm whitespace-nowrap transition-all
${
isActiveState
? "border-primary bg-primary text-white shadow-sm"
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50"
? "border-primary bg-primary text-white shadow-sm font-medium"
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 text-gray-600"
}
${className}
`}
>
<span
className={`text-sm ${
isActiveState ? "text-white font-medium" : "text-gray-600 font-normal"
}`}
>
{label}
</span>
{label}
{count !== undefined && (
<span
className={`text-sm font-semibold ${
className={`font-semibold ${
isActiveState ? "text-white" : "text-gray-900"
}`}
>

View File

@@ -76,6 +76,8 @@ export interface FormFieldProps {
showButtons?: boolean;
/** 텍스트 입력 최대 길이 */
maxLength?: number;
/** SelectContent에 전달할 className (예: max-h-60) */
selectContentClassName?: string;
}
export function FormField({
@@ -108,6 +110,7 @@ export function FormField({
suffix,
showButtons,
maxLength,
selectContentClassName,
}: FormFieldProps) {
const renderInput = () => {
@@ -122,7 +125,7 @@ export function FormField({
<SelectTrigger className={`${error ? 'border-red-500' : ''} ${inputClassName}`}>
<SelectValue placeholder={selectPlaceholder} />
</SelectTrigger>
<SelectContent>
<SelectContent className={selectContentClassName}>
{options.map((option) => (
<SelectItem
key={option.value}

View File

@@ -29,9 +29,9 @@ export const InfoField = memo(function InfoField({
className = '',
}: InfoFieldProps) {
return (
<div className={cn('space-y-0.5', className)}>
<div className={cn('space-y-0.5 min-w-0', className)}>
<p className="text-xs text-muted-foreground">{label}</p>
<div className={cn('text-sm font-medium', valueClassName)}>{value}</div>
<div className={cn('text-sm font-medium break-words', valueClassName)}>{value}</div>
</div>
);
});
@@ -194,7 +194,7 @@ export function MobileCard({
<div
key={`${detail.label}-${index}`}
className={cn(
'flex items-center gap-1',
'flex items-center gap-1 min-w-0',
detail.colSpan === 2 && 'col-span-2'
)}
>
@@ -264,7 +264,7 @@ export function MobileCard({
return (
<div
className={cn(
'border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all',
'border rounded-lg p-5 space-y-4 bg-white dark:bg-card transition-all overflow-hidden',
handleClick && 'cursor-pointer',
isSelected
? 'border-blue-500 bg-blue-50/50'

View File

@@ -42,6 +42,7 @@ import { formatNumber } from '@/lib/utils/amount';
export interface TabOption {
value: string;
label: string;
mobileLabel?: string;
count: number;
color?: string; // 모바일 탭 색상
}
@@ -805,7 +806,7 @@ export function IntegratedListTemplateV2<T = any>({
{tabs.map((tab) => (
<TabChip
key={tab.value}
label={tab.label}
label={tab.mobileLabel || tab.label}
count={tab.count}
active={activeTab === tab.value}
onClick={() => onTabChange?.(tab.value)}

View File

@@ -16,6 +16,7 @@ export type { FilterFieldConfig, FilterValues };
export interface TabOption {
value: string;
label: string;
mobileLabel?: string;
count: number;
color?: string;
}

View File

@@ -58,7 +58,7 @@ function SelectTrigger({
function SelectContent({
className,
children,
position = "item-aligned",
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
@@ -66,7 +66,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-60 min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,