Files
sam-react-prod/src/components/molecules/FormField.tsx
유병철 c2ed71540f feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화:
- date-picker.tsx 공통 컴포넌트 신규 추가
- 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일)
- DateRangeSelector 개선

공정관리:
- RuleModal 대폭 리팩토링 (-592줄 → 간소화)
- ProcessForm, StepForm 개선
- ProcessDetail 수정, actions 확장

작업자화면:
- WorkerScreen 기능 대폭 확장 (+543줄)
- WorkItemCard 개선
- types 확장

회계/인사/영업/품질:
- BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용
- EmployeeForm, VacationDialog 등 DatePicker 적용
- OrderRegistration, QuoteRegistration DatePicker 적용
- InspectionCreate, InspectionDetail DatePicker 적용

공사관리/CEO대시보드:
- BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용
- ScheduleDetailModal, TodayIssueSection 개선

기타:
- WorkOrderCreate/Edit/Detail/List 개선
- ShipmentCreate/Edit, ReceivingDetail 개선
- calendar, calendarEvents 수정
- datepicker 마이그레이션 체크리스트 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 15:48:00 +09:00

296 lines
7.7 KiB
TypeScript

/**
* FormField - 통합 폼 필드 컴포넌트
*/
import { ReactNode } from "react";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import { Textarea } from "../ui/textarea";
import { AlertCircle } from "lucide-react";
import { PhoneInput } from "../ui/phone-input";
import { BusinessNumberInput } from "../ui/business-number-input";
import { PersonalNumberInput } from "../ui/personal-number-input";
import { NumberInput } from "../ui/number-input";
import { CurrencyInput } from "../ui/currency-input";
import { QuantityInput } from "../ui/quantity-input";
import { DatePicker } from "../ui/date-picker";
export type FormFieldType =
| 'text'
| 'number'
| 'date'
| 'select'
| 'textarea'
| 'custom'
| 'password'
// 새 입력 타입
| 'phone' // 전화번호 (자동 하이픈)
| 'businessNumber' // 사업자번호 (000-00-00000)
| 'personalNumber' // 주민번호 (000000-0000000)
| 'currency' // 금액 (천단위 콤마, ₩)
| 'quantity'; // 수량 (정수, 최소 0)
export interface SelectOption {
value: string;
label: string;
disabled?: boolean;
}
export interface FormFieldProps {
label: string;
required?: boolean;
type?: FormFieldType;
value?: string | number;
onChange?: (value: string) => void;
/** number 타입 전용 onChange (currency, quantity 타입에서 사용) */
onChangeNumber?: (value: number | undefined) => void;
placeholder?: string;
disabled?: boolean;
error?: string;
helpText?: string;
options?: SelectOption[];
selectPlaceholder?: string;
children?: ReactNode;
className?: string;
inputClassName?: string;
rows?: number;
min?: number;
max?: number;
step?: number;
htmlFor?: string;
// 새 입력 타입 전용 옵션
/** 사업자번호 유효성 검사 표시 */
showValidation?: boolean;
/** 주민번호 뒷자리 마스킹 */
maskBack?: boolean;
/** 소수점 허용 (NumberInput) */
allowDecimal?: boolean;
/** 소수점 자릿수 (NumberInput) */
decimalPlaces?: number;
/** 천단위 콤마 (NumberInput) */
useComma?: boolean;
/** 접미사 (원, 개, % 등) */
suffix?: string;
/** 수량 +/- 버튼 표시 */
showButtons?: boolean;
}
export function FormField({
label,
required = false,
type = 'text',
value,
onChange,
onChangeNumber,
placeholder,
disabled = false,
error,
helpText,
options = [],
selectPlaceholder = "선택하세요",
children,
className = "",
inputClassName = "",
rows = 3,
min,
max,
step,
htmlFor,
// 새 입력 타입 전용 옵션
showValidation,
maskBack,
allowDecimal,
decimalPlaces,
useComma,
suffix,
showButtons,
}: FormFieldProps) {
const renderInput = () => {
switch (type) {
case 'select':
return (
<Select
value={value as string}
onValueChange={onChange}
disabled={disabled}
>
<SelectTrigger className={`${error ? 'border-red-500' : ''} ${inputClassName}`}>
<SelectValue placeholder={selectPlaceholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem
key={option.value}
value={option.value}
disabled={option.disabled}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
case 'textarea':
return (
<Textarea
id={htmlFor}
value={value as string}
onChange={(e) => onChange?.(e.target.value)}
placeholder={placeholder}
disabled={disabled}
rows={rows}
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
/>
);
case 'custom':
return children;
case 'number':
return (
<Input
id={htmlFor}
type="number"
value={value}
onChange={(e) => onChange?.(e.target.value)}
placeholder={placeholder}
disabled={disabled}
min={min}
max={max}
step={step}
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
/>
);
case 'date':
return (
<DatePicker
value={(value as string) || ''}
onChange={(date) => onChange?.(date)}
disabled={disabled}
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
/>
);
case 'password':
return (
<Input
id={htmlFor}
type="password"
value={value as string}
onChange={(e) => onChange?.(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
/>
);
case 'phone':
return (
<PhoneInput
value={(value as string) || ''}
onChange={(v) => onChange?.(v)}
placeholder={placeholder}
disabled={disabled}
error={!!error}
className={inputClassName}
/>
);
case 'businessNumber':
return (
<BusinessNumberInput
value={(value as string) || ''}
onChange={(v) => onChange?.(v)}
placeholder={placeholder}
disabled={disabled}
error={!!error}
showValidation={showValidation}
className={inputClassName}
/>
);
case 'personalNumber':
return (
<PersonalNumberInput
value={(value as string) || ''}
onChange={(v) => onChange?.(v)}
placeholder={placeholder}
disabled={disabled}
error={!!error}
maskBack={maskBack}
className={inputClassName}
/>
);
case 'currency':
return (
<CurrencyInput
value={value}
onChange={(v) => onChangeNumber?.(v)}
placeholder={placeholder}
disabled={disabled}
error={!!error}
className={inputClassName}
/>
);
case 'quantity':
return (
<QuantityInput
value={value}
onChange={(v) => onChangeNumber?.(v)}
placeholder={placeholder}
disabled={disabled}
error={!!error}
min={min}
max={max}
step={step}
showButtons={showButtons}
suffix={suffix}
className={inputClassName}
/>
);
case 'text':
default:
return (
<Input
id={htmlFor}
type="text"
value={value as string}
onChange={(e) => onChange?.(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
/>
);
}
};
return (
<div className={className}>
<Label htmlFor={htmlFor}>
{label} {required && <span className="text-red-500">*</span>}
</Label>
<div className="mt-1">
{renderInput()}
</div>
{error && (
<div className="flex items-center gap-1 mt-1 text-sm text-red-500">
<AlertCircle className="h-3 w-3" />
<span>{error}</span>
</div>
)}
{helpText && !error && (
<p className="text-xs text-muted-foreground mt-1">{helpText}</p>
)}
</div>
);
}