feat(WEB): 입력 컴포넌트 공통화 및 UI 개선
- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가 - MobileCard 컴포넌트 통합 (ListMobileCard 제거) - IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈) - IntegratedDetailTemplate 타이틀 중복 수정 - 문서 시스템 컴포넌트 추가 - 헤더 벨 아이콘 포커스 스타일 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -227,13 +228,13 @@ export function BOMManagementSection({
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label>수량 *</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
<NumberInput
|
||||
value={parseFloat(quantity) || 0}
|
||||
onChange={(value) => setQuantity(String(value ?? 0))}
|
||||
placeholder="1"
|
||||
min="0"
|
||||
step="0.01"
|
||||
min={0}
|
||||
step={0.01}
|
||||
allowDecimal
|
||||
className={isSubmitted && isQuantityInvalid ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
/>
|
||||
{isSubmitted && isQuantityInvalid && (
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'use client';
|
||||
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import type { DynamicFieldRendererProps } from '../types';
|
||||
|
||||
export function NumberField({
|
||||
@@ -30,20 +30,19 @@ export function NumberField({
|
||||
{unit && ` (${unit})`}
|
||||
{field.is_required && <span className="text-red-500"> *</span>}
|
||||
</Label>
|
||||
<Input
|
||||
<NumberInput
|
||||
id={fieldKey}
|
||||
type="number"
|
||||
placeholder={field.placeholder || `${field.field_name}을(를) 입력하세요`}
|
||||
value={stringValue}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// 빈 문자열이면 null, 아니면 숫자로 변환
|
||||
onChange(newValue === '' ? null : Number(newValue));
|
||||
value={value !== null && value !== undefined ? Number(value) : undefined}
|
||||
onChange={(newValue) => {
|
||||
// undefined이면 null, 아니면 숫자로 변환
|
||||
onChange(newValue ?? null);
|
||||
}}
|
||||
disabled={disabled}
|
||||
step={step}
|
||||
min={field.validation_rules?.min}
|
||||
max={field.validation_rules?.max}
|
||||
allowDecimal={step !== 1}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
/>
|
||||
{error && (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -436,18 +437,18 @@ function BOMLineRow({
|
||||
{line.material || '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={line.quantity}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
setBomLines(
|
||||
bomLines.map((l) =>
|
||||
l.id === line.id ? { ...l, quantity: Number(e.target.value) } : l
|
||||
l.id === line.id ? { ...l, quantity: value ?? 0 } : l
|
||||
)
|
||||
);
|
||||
}}
|
||||
min="0"
|
||||
step="0.01"
|
||||
min={0}
|
||||
step={0.01}
|
||||
allowDecimal
|
||||
className="w-full"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { CurrencyInput } from '@/components/ui/currency-input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -265,18 +267,18 @@ export default function BOMSection({
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={line.quantity}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
setBomLines(
|
||||
bomLines.map((l) =>
|
||||
l.id === line.id ? { ...l, quantity: Number(e.target.value) } : l
|
||||
l.id === line.id ? { ...l, quantity: value ?? 0 } : l
|
||||
)
|
||||
);
|
||||
}}
|
||||
min="0"
|
||||
step="0.01"
|
||||
min={0}
|
||||
step={0.01}
|
||||
allowDecimal
|
||||
className="w-full"
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -284,18 +286,16 @@ export default function BOMSection({
|
||||
<Badge variant="secondary">{line.unit}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
<CurrencyInput
|
||||
value={line.unitPrice || 0}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
setBomLines(
|
||||
bomLines.map((l) =>
|
||||
l.id === line.id ? { ...l, unitPrice: Number(e.target.value) } : l
|
||||
l.id === line.id ? { ...l, unitPrice: value ?? 0 } : l
|
||||
)
|
||||
);
|
||||
}}
|
||||
min="0"
|
||||
className="w-full text-right"
|
||||
className="w-full"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { FileImage, Plus, Trash2, X, Download, Loader2 } from 'lucide-react';
|
||||
import type { BendingDetail } from '@/types/item';
|
||||
import type { UseFormSetValue } from 'react-hook-form';
|
||||
@@ -357,36 +358,34 @@ export default function BendingDiagramSection({
|
||||
<tr key={detail.id} className={detail.shaded ? 'bg-gray-100' : ''}>
|
||||
<td className="px-3 py-2 text-center border-b">{detail.no}</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={detail.input}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
const newDetails = [...bendingDetails];
|
||||
const value = e.target.value === '' ? 0 : parseFloat(e.target.value);
|
||||
newDetails[index] = {
|
||||
...detail,
|
||||
input: isNaN(value) ? 0 : value,
|
||||
input: value ?? 0,
|
||||
};
|
||||
setBendingDetails(newDetails);
|
||||
updateWidthSum(newDetails);
|
||||
}}
|
||||
allowDecimal
|
||||
className="h-8 text-center"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
value={detail.elongation}
|
||||
onChange={(e) => {
|
||||
onChange={(value) => {
|
||||
const newDetails = [...bendingDetails];
|
||||
const value = e.target.value === '' ? -1 : parseFloat(e.target.value);
|
||||
newDetails[index] = {
|
||||
...detail,
|
||||
elongation: isNaN(value) ? -1 : value,
|
||||
elongation: value ?? -1,
|
||||
};
|
||||
setBendingDetails(newDetails);
|
||||
updateWidthSum(newDetails);
|
||||
}}
|
||||
allowDecimal
|
||||
className="h-8 text-center"
|
||||
/>
|
||||
</td>
|
||||
@@ -409,17 +408,17 @@ export default function BendingDiagramSection({
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 border-b">
|
||||
<Input
|
||||
type="number"
|
||||
value={detail.aAngle || ''}
|
||||
onChange={(e) => {
|
||||
<NumberInput
|
||||
value={detail.aAngle ?? undefined}
|
||||
onChange={(value) => {
|
||||
const newDetails = [...bendingDetails];
|
||||
newDetails[index] = {
|
||||
...detail,
|
||||
aAngle: parseFloat(e.target.value) || undefined,
|
||||
aAngle: value ?? undefined,
|
||||
};
|
||||
setBendingDetails(newDetails);
|
||||
}}
|
||||
allowDecimal
|
||||
className="h-8 text-center"
|
||||
placeholder="각도"
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -206,13 +207,13 @@ export default function AssemblyPartForm({
|
||||
<Label>
|
||||
측면 규격 (가로) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
placeholder="예: 50"
|
||||
value={sideSpecWidth}
|
||||
onChange={(e) => {
|
||||
setSideSpecWidth(e.target.value);
|
||||
setValue('sideSpecWidth', e.target.value);
|
||||
value={parseFloat(sideSpecWidth) || 0}
|
||||
onChange={(value) => {
|
||||
const strValue = String(value ?? 0);
|
||||
setSideSpecWidth(strValue);
|
||||
setValue('sideSpecWidth', strValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -220,13 +221,13 @@ export default function AssemblyPartForm({
|
||||
<Label>
|
||||
측면 규격 (세로) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
placeholder="예: 100"
|
||||
value={sideSpecHeight}
|
||||
onChange={(e) => {
|
||||
setSideSpecHeight(e.target.value);
|
||||
setValue('sideSpecHeight', e.target.value);
|
||||
value={parseFloat(sideSpecHeight) || 0}
|
||||
onChange={(value) => {
|
||||
const strValue = String(value ?? 0);
|
||||
setSideSpecHeight(strValue);
|
||||
setValue('sideSpecHeight', strValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -156,15 +157,16 @@ export default function BendingPartForm({
|
||||
폭 합계 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={widthSum}
|
||||
onChange={(e) => {
|
||||
setWidthSum(e.target.value);
|
||||
setValue('length', e.target.value);
|
||||
<NumberInput
|
||||
value={parseFloat(widthSum) || 0}
|
||||
onChange={(value) => {
|
||||
const strValue = String(value ?? 0);
|
||||
setWidthSum(strValue);
|
||||
setValue('length', strValue);
|
||||
}}
|
||||
placeholder="전개도 상세를 입력해주세요"
|
||||
readOnly={bendingDetailsLength > 0}
|
||||
disabled={bendingDetailsLength > 0}
|
||||
allowDecimal
|
||||
className={`${bendingDetailsLength > 0 ? "bg-blue-50 font-medium" : ""} ${(errors as any).length ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">mm</span>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useMemo } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { NumberInput } from '@/components/ui/number-input';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -171,7 +173,7 @@ export default function PurchasedPartForm({
|
||||
<div className="md:col-span-2 grid grid-cols-2 gap-4 p-4 bg-green-50 rounded-lg border border-green-200">
|
||||
<div>
|
||||
<Label>모터 용량 (kg) *</Label>
|
||||
<Input type="number" placeholder="예: 1.5" step="0.1" />
|
||||
<NumberInput placeholder="예: 1.5" step={0.1} allowDecimal />
|
||||
</div>
|
||||
<div>
|
||||
<Label>전압 (V) *</Label>
|
||||
@@ -229,7 +231,7 @@ export default function PurchasedPartForm({
|
||||
</div>
|
||||
<div>
|
||||
<Label>길이 (링크 수) *</Label>
|
||||
<Input type="number" placeholder="예: 100" />
|
||||
<QuantityInput placeholder="예: 100" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
type TabOption,
|
||||
type StatCard,
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
|
||||
// Debounce 훅
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -221,13 +222,12 @@ export function MasterFieldDialog({
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div>
|
||||
<Label>컬럼 개수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="2"
|
||||
max="10"
|
||||
<QuantityInput
|
||||
min={2}
|
||||
max={10}
|
||||
value={newMasterFieldColumnCount}
|
||||
onChange={(e) => {
|
||||
const count = parseInt(e.target.value) || 2;
|
||||
onChange={(value) => {
|
||||
const count = value ?? 2;
|
||||
setNewMasterFieldColumnCount(count);
|
||||
// 컬럼 개수에 맞게 이름 배열 조정
|
||||
const newNames = Array.from({ length: count }, (_, i) =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -321,13 +322,12 @@ export function TemplateFieldDialog({
|
||||
<>
|
||||
<div>
|
||||
<Label>컬럼 개수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<QuantityInput
|
||||
min={2}
|
||||
max={10}
|
||||
value={templateFieldColumnCount}
|
||||
onChange={(e) => {
|
||||
const count = parseInt(e.target.value) || 2;
|
||||
onChange={(value) => {
|
||||
const count = value ?? 2;
|
||||
setTemplateFieldColumnCount(count);
|
||||
const newNames = Array.from({ length: count }, (_, i) =>
|
||||
templateFieldColumnNames[i] || `컬럼${i + 1}`
|
||||
|
||||
Reference in New Issue
Block a user