feat(WEB): 입력 컴포넌트 공통화 및 UI 개선

- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가
- MobileCard 컴포넌트 통합 (ListMobileCard 제거)
- IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈)
- IntegratedDetailTemplate 타이틀 중복 수정
- 문서 시스템 컴포넌트 추가
- 헤더 벨 아이콘 포커스 스타일 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-21 20:56:17 +09:00
parent cfa72fe19b
commit 835c06ce94
190 changed files with 8575 additions and 2354 deletions

View File

@@ -13,7 +13,11 @@ import { Plus, X, FileText, Receipt, CreditCard, Upload, Download, Trash2 } from
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { PhoneInput } from '@/components/ui/phone-input';
import { BusinessNumberInput } from '@/components/ui/business-number-input';
import { Textarea } from '@/components/ui/textarea';
import { CurrencyInput } from '@/components/ui/currency-input';
import { NumberInput } from '@/components/ui/number-input';
import { Switch } from '@/components/ui/switch';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
@@ -377,25 +381,58 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
value: string | number,
options?: {
required?: boolean;
type?: 'text' | 'tel' | 'email' | 'number';
type?: 'text' | 'tel' | 'email' | 'number' | 'phone' | 'businessNumber';
placeholder?: string;
disabled?: boolean;
}
) => {
const { required, type = 'text', placeholder, disabled } = options || {};
const isDisabled = isViewMode || disabled;
const stringValue = value !== null && value !== undefined ? String(value) : '';
const renderInput = () => {
switch (type) {
case 'phone':
return (
<PhoneInput
value={stringValue}
onChange={(v) => handleChange(field, v)}
placeholder={placeholder}
disabled={isDisabled}
className="bg-white"
/>
);
case 'businessNumber':
return (
<BusinessNumberInput
value={stringValue}
onChange={(v) => handleChange(field, v)}
placeholder={placeholder}
disabled={isDisabled}
showValidation
className="bg-white"
/>
);
default:
return (
<Input
type={type === 'tel' ? 'tel' : type}
value={value}
onChange={(e) => handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)}
placeholder={placeholder}
disabled={isDisabled}
className="bg-white"
/>
);
}
};
return (
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700">
{label} {required && <span className="text-red-500">*</span>}
</Label>
<Input
type={type}
value={value}
onChange={(e) => handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)}
placeholder={placeholder}
disabled={isViewMode || disabled}
className="bg-white"
/>
{renderInput()}
</div>
);
};
@@ -409,7 +446,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
<CardTitle className="text-lg"> </CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderField('사업자등록번호', 'businessNumber', formData.businessNumber, { required: true, placeholder: '000-00-00000', disabled: true })}
{renderField('사업자등록번호', 'businessNumber', formData.businessNumber, { required: true, type: 'businessNumber', placeholder: '000-00-00000', disabled: true })}
{renderField('거래처 코드', 'vendorCode', formData.vendorCode, { disabled: true })}
{renderField('거래처명', 'vendorName', formData.vendorName, { required: true })}
{renderField('대표자명', 'representativeName', formData.representativeName)}
@@ -492,9 +529,9 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderField('전화번호', 'phone', formData.phone, { type: 'tel', placeholder: '02-0000-0000' })}
{renderField('모바일', 'mobile', formData.mobile, { type: 'tel', placeholder: '010-0000-0000' })}
{renderField('팩스', 'fax', formData.fax, { type: 'tel', placeholder: '02-0000-0000' })}
{renderField('전화번호', 'phone', formData.phone, { type: 'phone', placeholder: '02-0000-0000' })}
{renderField('모바일', 'mobile', formData.mobile, { type: 'phone', placeholder: '010-0000-0000' })}
{renderField('팩스', 'fax', formData.fax, { type: 'phone', placeholder: '02-0000-0000' })}
{renderField('이메일', 'email', formData.email, { type: 'email' })}
</div>
</CardContent>
@@ -507,7 +544,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-4">
{renderField('담당자명', 'contactName', formData.contactName)}
{renderField('담당자 전화', 'contactPhone', formData.contactPhone, { type: 'tel' })}
{renderField('담당자 전화', 'contactPhone', formData.contactPhone, { type: 'phone' })}
{renderField('시스템 관리자', 'systemManager', formData.systemManager, { disabled: true })}
</CardContent>
</Card>
@@ -799,10 +836,9 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
{/* 미수금 */}
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700"></Label>
<Input
type="number"
<CurrencyInput
value={formData.debtAmount}
onChange={(e) => handleChange('debtAmount', Number(e.target.value))}
onChange={(value) => handleChange('debtAmount', value ?? 0)}
disabled={isViewMode}
className="bg-white"
/>
@@ -831,12 +867,12 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
<div className="space-y-2">
<Label className="text-sm font-medium text-gray-700"></Label>
<div className="flex items-center gap-2">
<Input
type="number"
<NumberInput
value={formData.overdueDays}
onChange={(e) => handleChange('overdueDays', Number(e.target.value))}
onChange={(value) => handleChange('overdueDays', value ?? 0)}
disabled={isViewMode}
className="bg-white w-[100px]"
min={0}
/>
<span className="text-sm text-gray-500"></span>
</div>

View File

@@ -28,7 +28,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { MobileCard } from '@/components/molecules/MobileCard';
import { MobileCard } from '@/components/organisms/MobileCard';
import {
UniversalListPage,
type UniversalListConfig,