fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가
## 품목관리 수정 버그 수정 - FG(제품) 수정 시 품목명 반영 안되는 문제 해결 - productName → name 필드 매핑 추가 - FG 품목코드 = 품목명 동기화 로직 추가 - Materials(SM, RM, CS) 수정페이지 진입 오류 해결 - UNIQUE 제약조건 위반 오류 해결 ## Sales 페이지 - 거래처관리 (client-management-sales-admin) 페이지 구현 - 견적관리 (quote-management) 페이지 구현 - 관련 컴포넌트 및 훅 추가 ## 기타 - 회원가입 페이지 차단 처리 - 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
185
src/components/molecules/FormField.tsx
Normal file
185
src/components/molecules/FormField.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 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";
|
||||
|
||||
export type FormFieldType = 'text' | 'number' | 'date' | 'select' | 'textarea' | 'custom' | 'password';
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
export function FormField({
|
||||
label,
|
||||
required = false,
|
||||
type = 'text',
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
error,
|
||||
helpText,
|
||||
options = [],
|
||||
selectPlaceholder = "선택하세요",
|
||||
children,
|
||||
className = "",
|
||||
inputClassName = "",
|
||||
rows = 3,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
htmlFor,
|
||||
}: 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 (
|
||||
<Input
|
||||
id={htmlFor}
|
||||
type="date"
|
||||
value={value as string}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
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 '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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user