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:
byeongcheolryu
2025-12-04 20:52:42 +09:00
parent 42f80e2b16
commit 751e65f59b
52 changed files with 8869 additions and 1088 deletions

View 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>
);
}