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 <button
onClick={onClick} onClick={onClick}
className={` 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 isActiveState
? "border-primary bg-primary text-white shadow-sm" ? "border-primary bg-primary text-white shadow-sm font-medium"
: "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50" : "border-gray-200 bg-white hover:border-gray-300 hover:bg-gray-50 text-gray-600"
} }
${className} ${className}
`} `}
> >
<span {label}
className={`text-sm ${
isActiveState ? "text-white font-medium" : "text-gray-600 font-normal"
}`}
>
{label}
</span>
{count !== undefined && ( {count !== undefined && (
<span <span
className={`text-sm font-semibold ${ className={`font-semibold ${
isActiveState ? "text-white" : "text-gray-900" isActiveState ? "text-white" : "text-gray-900"
}`} }`}
> >

View File

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

View File

@@ -29,9 +29,9 @@ export const InfoField = memo(function InfoField({
className = '', className = '',
}: InfoFieldProps) { }: InfoFieldProps) {
return ( 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> <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> </div>
); );
}); });
@@ -194,7 +194,7 @@ export function MobileCard({
<div <div
key={`${detail.label}-${index}`} key={`${detail.label}-${index}`}
className={cn( className={cn(
'flex items-center gap-1', 'flex items-center gap-1 min-w-0',
detail.colSpan === 2 && 'col-span-2' detail.colSpan === 2 && 'col-span-2'
)} )}
> >
@@ -264,7 +264,7 @@ export function MobileCard({
return ( return (
<div <div
className={cn( 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', handleClick && 'cursor-pointer',
isSelected isSelected
? 'border-blue-500 bg-blue-50/50' ? 'border-blue-500 bg-blue-50/50'

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ function SelectTrigger({
function SelectContent({ function SelectContent({
className, className,
children, children,
position = "item-aligned", position = "popper",
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) { }: React.ComponentProps<typeof SelectPrimitive.Content>) {
return ( return (
@@ -66,7 +66,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( 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" && 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", "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, className,