feat: 견적서 목업 데이터 → API 연동 전환
- EstimateDetailTableSection: 하드코딩된 셀렉트 옵션 → API 데이터 연동 - 재료/도장/모터/제어기/시공비: getCommonCodeOptions() 사용 - 공과 품목: getExpenseItemOptions() 사용 - EstimateListClient: 거래처/견적자 필터 API 연동 - MOCK_PARTNERS → getClientOptions() - MOCK_ESTIMATORS → getUserOptions() - actions.ts: 공통코드/거래처/사용자/공과품목 API 함수 추가 - constants.ts: MOCK_MATERIALS 제거 - EstimateDetailForm: MOCK_MATERIALS import 제거
This commit is contained in:
@@ -20,7 +20,7 @@ import type {
|
||||
import { getEmptyEstimateDetailFormData, estimateDetailToFormData } from './types';
|
||||
import { ElectronicApprovalModal } from './modals/ElectronicApprovalModal';
|
||||
import { EstimateDocumentModal } from './modals/EstimateDocumentModal';
|
||||
import { MOCK_MATERIALS } from './utils';
|
||||
// MOCK_MATERIALS 제거됨 - API 데이터 사용
|
||||
import {
|
||||
EstimateInfoSection,
|
||||
EstimateSummarySection,
|
||||
@@ -95,6 +95,11 @@ export default function EstimateDetailForm({
|
||||
// ===== 저장 핸들러 (IntegratedDetailTemplate용) =====
|
||||
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
// 🔍 디버깅: 저장 전 formData 확인 (브라우저 콘솔)
|
||||
console.log('🔍 [handleConfirmSave] formData.detailItems:', formData.detailItems?.length, '개');
|
||||
console.log('🔍 [handleConfirmSave] formData.priceAdjustmentData:', formData.priceAdjustmentData);
|
||||
console.log('🔍 [handleConfirmSave] formData 전체:', formData);
|
||||
|
||||
// 현재 사용자 이름을 견적자로 설정하여 저장
|
||||
const result = await updateEstimate(estimateId, {
|
||||
...formData,
|
||||
@@ -295,7 +300,7 @@ export default function EstimateDetailForm({
|
||||
id: String(Date.now() + Math.random() + i),
|
||||
no: currentLength + i + 1,
|
||||
name: '',
|
||||
material: MOCK_MATERIALS[0]?.value || '',
|
||||
material: 'screen', // 기본값: 스크린 (API 옵션 첫번째 값)
|
||||
width: 0,
|
||||
height: 0,
|
||||
quantity: 1,
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
STATUS_STYLES,
|
||||
STATUS_LABELS,
|
||||
} from './types';
|
||||
import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates } from './actions';
|
||||
import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions';
|
||||
import type { ClientOption, UserOption } from './actions';
|
||||
|
||||
// 테이블 컬럼 정의
|
||||
const tableColumns = [
|
||||
@@ -48,19 +49,6 @@ const tableColumns = [
|
||||
{ key: 'actions', label: '작업', className: 'w-[80px] text-center' },
|
||||
];
|
||||
|
||||
// 거래처/견적자 옵션 (다중선택용)
|
||||
const MOCK_PARTNERS = [
|
||||
{ value: '1', label: '회사명' },
|
||||
{ value: '2', label: '야사 대림아파트' },
|
||||
{ value: '3', label: '여의 현장아파트' },
|
||||
];
|
||||
|
||||
const MOCK_ESTIMATORS = [
|
||||
{ value: 'hong', label: '홍길동' },
|
||||
{ value: 'kim', label: '김철수' },
|
||||
{ value: 'lee', label: '이영희' },
|
||||
];
|
||||
|
||||
// 금액 포맷팅
|
||||
function formatAmount(amount: number): string {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
@@ -82,6 +70,9 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
const [endDate, setEndDate] = useState('');
|
||||
// Stats 데이터
|
||||
const [stats, setStats] = useState<EstimateStats | null>(initialStats || null);
|
||||
// 필터 옵션 데이터
|
||||
const [partnerOptions, setPartnerOptions] = useState<ClientOption[]>([]);
|
||||
const [estimatorOptions, setEstimatorOptions] = useState<UserOption[]>([]);
|
||||
|
||||
// Stats 로드
|
||||
useEffect(() => {
|
||||
@@ -94,6 +85,22 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
}
|
||||
}, [initialStats]);
|
||||
|
||||
// 거래처/견적자 옵션 로드
|
||||
useEffect(() => {
|
||||
// 거래처 옵션 로드
|
||||
getClientOptions().then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setPartnerOptions(result.data);
|
||||
}
|
||||
});
|
||||
// 견적자(사용자) 옵션 로드
|
||||
getUserOptions().then((result) => {
|
||||
if (result.success && result.data) {
|
||||
setEstimatorOptions(result.data);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ===== 핸들러 =====
|
||||
const handleRowClick = useCallback(
|
||||
(item: Estimate) => {
|
||||
@@ -172,13 +179,13 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
key: 'partner',
|
||||
label: '거래처',
|
||||
type: 'multi',
|
||||
options: MOCK_PARTNERS,
|
||||
options: partnerOptions,
|
||||
},
|
||||
{
|
||||
key: 'estimator',
|
||||
label: '견적자',
|
||||
type: 'multi',
|
||||
options: MOCK_ESTIMATORS,
|
||||
options: estimatorOptions,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
@@ -382,7 +389,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit]
|
||||
[startDate, endDate, activeStatTab, stats, partnerOptions, estimatorOptions, handleRowClick, handleEdit]
|
||||
);
|
||||
|
||||
return <UniversalListPage config={config} initialData={initialData} />;
|
||||
|
||||
@@ -940,4 +940,202 @@ export async function getExpenseItemOptions(): Promise<{
|
||||
console.error('공과 품목 목록 조회 오류:', error);
|
||||
return { success: false, error: '공과 품목 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 공통코드/거래처/사용자 API ====================
|
||||
|
||||
/**
|
||||
* 공통코드 옵션 타입
|
||||
*/
|
||||
export interface CommonCodeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
code: string;
|
||||
price?: number;
|
||||
attributes?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 그룹의 공통코드 목록 조회
|
||||
* GET /api/v1/settings/common/{group}
|
||||
*/
|
||||
export async function getCommonCodes(group: string): Promise<{
|
||||
success: boolean;
|
||||
data?: CommonCodeOption[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: Array<{
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
attributes: string | null;
|
||||
sort_order: number;
|
||||
}>;
|
||||
}>(`/settings/common/${group}`);
|
||||
|
||||
const items = Array.isArray(response.data) ? response.data : [];
|
||||
const options: CommonCodeOption[] = items.map((item) => {
|
||||
const attrs = item.attributes ? JSON.parse(item.attributes) : {};
|
||||
return {
|
||||
value: attrs.price !== undefined ? String(attrs.price) : item.code,
|
||||
label: item.name,
|
||||
code: item.code,
|
||||
price: attrs.price,
|
||||
attributes: attrs,
|
||||
};
|
||||
});
|
||||
|
||||
return { success: true, data: options };
|
||||
} catch (error) {
|
||||
console.error(`공통코드(${group}) 목록 조회 오류:`, error);
|
||||
return { success: false, error: `공통코드(${group}) 목록을 불러오는데 실패했습니다.` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 옵션 타입
|
||||
*/
|
||||
export interface ClientOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 목록 조회 (셀렉트 옵션용)
|
||||
* GET /api/v1/clients
|
||||
*/
|
||||
export async function getClientOptions(): Promise<{
|
||||
success: boolean;
|
||||
data?: ClientOption[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
code?: string;
|
||||
}>;
|
||||
};
|
||||
}>('/clients', {
|
||||
params: {
|
||||
active: '1',
|
||||
size: '100',
|
||||
},
|
||||
});
|
||||
|
||||
const paginatedData = response.data;
|
||||
const items = Array.isArray(paginatedData.data) ? paginatedData.data : [];
|
||||
const options: ClientOption[] = items.map((item) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
}));
|
||||
|
||||
return { success: true, data: options };
|
||||
} catch (error) {
|
||||
console.error('거래처 목록 조회 오류:', error);
|
||||
return { success: false, error: '거래처 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 옵션 타입
|
||||
*/
|
||||
export interface UserOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 목록 조회 (견적자 셀렉트 옵션용)
|
||||
* GET /api/v1/users
|
||||
*/
|
||||
export async function getUserOptions(): Promise<{
|
||||
success: boolean;
|
||||
data?: UserOption[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
email?: string;
|
||||
}>;
|
||||
};
|
||||
}>('/users', {
|
||||
params: {
|
||||
active: '1',
|
||||
size: '100',
|
||||
},
|
||||
});
|
||||
|
||||
const paginatedData = response.data;
|
||||
const items = Array.isArray(paginatedData.data) ? paginatedData.data : [];
|
||||
const options: UserOption[] = items.map((item) => ({
|
||||
value: String(item.id),
|
||||
label: item.name,
|
||||
}));
|
||||
|
||||
return { success: true, data: options };
|
||||
} catch (error) {
|
||||
console.error('사용자 목록 조회 오류:', error);
|
||||
return { success: false, error: '사용자 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적서 옵션 데이터 일괄 조회
|
||||
* 제품, 도장, 모터, 제어기, 시공비 등 모든 셀렉트 옵션
|
||||
*/
|
||||
export interface EstimateOptionsData {
|
||||
materials: CommonCodeOption[];
|
||||
paintings: CommonCodeOption[];
|
||||
motors: CommonCodeOption[];
|
||||
controllers: CommonCodeOption[];
|
||||
widthConstructions: CommonCodeOption[];
|
||||
heightConstructions: CommonCodeOption[];
|
||||
}
|
||||
|
||||
export async function getEstimateOptions(): Promise<{
|
||||
success: boolean;
|
||||
data?: EstimateOptionsData;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const [materials, paintings, motors, controllers, widthConstructions, heightConstructions] =
|
||||
await Promise.all([
|
||||
getCommonCodes('material_type'),
|
||||
getCommonCodes('painting_type'),
|
||||
getCommonCodes('motor_type'),
|
||||
getCommonCodes('controller_type'),
|
||||
getCommonCodes('width_construction_cost'),
|
||||
getCommonCodes('height_construction_cost'),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
materials: materials.data || [],
|
||||
paintings: paintings.data || [],
|
||||
motors: motors.data || [],
|
||||
controllers: controllers.data || [],
|
||||
widthConstructions: widthConstructions.data || [],
|
||||
heightConstructions: heightConstructions.data || [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('견적서 옵션 일괄 조회 오류:', error);
|
||||
return { success: false, error: '견적서 옵션을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { EstimateDetailItem } from '../types';
|
||||
import { formatAmount, MOCK_MATERIALS } from '../utils';
|
||||
import { formatAmount } from '../utils';
|
||||
import { calculateItemValuesWithApplied, calculateTotalsWithApplied } from '../hooks/useEstimateCalculations';
|
||||
import type { CommonCodeOption } from '../actions';
|
||||
|
||||
// 계산식 정보
|
||||
const FORMULA_INFO: Record<string, string> = {
|
||||
@@ -85,10 +86,21 @@ export interface AppliedPrices {
|
||||
controller: number;
|
||||
}
|
||||
|
||||
// 옵션 데이터 타입
|
||||
export interface EstimateDetailOptions {
|
||||
materials: CommonCodeOption[];
|
||||
paintings: CommonCodeOption[];
|
||||
motors: CommonCodeOption[];
|
||||
controllers: CommonCodeOption[];
|
||||
widthConstructions: CommonCodeOption[];
|
||||
heightConstructions: CommonCodeOption[];
|
||||
}
|
||||
|
||||
interface EstimateDetailTableSectionProps {
|
||||
detailItems: EstimateDetailItem[];
|
||||
appliedPrices: AppliedPrices | null;
|
||||
isViewMode: boolean;
|
||||
options?: EstimateDetailOptions;
|
||||
onAddItems: (count: number) => void;
|
||||
onRemoveItem: (id: string) => void;
|
||||
onRemoveSelected: () => void;
|
||||
@@ -99,10 +111,45 @@ interface EstimateDetailTableSectionProps {
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
// API 데이터 로드 전 기본 옵션 (폴백용)
|
||||
const DEFAULT_OPTIONS: EstimateDetailOptions = {
|
||||
materials: [
|
||||
{ value: 'screen', label: '스크린', code: 'screen' },
|
||||
{ value: 'slat', label: '슬랫', code: 'slat' },
|
||||
{ value: 'bending', label: '벤딩', code: 'bending' },
|
||||
{ value: 'jointbar', label: '조인트바', code: 'jointbar' },
|
||||
],
|
||||
paintings: [
|
||||
{ value: '0', label: '직접입력', code: '0', price: 0 },
|
||||
{ value: '50000', label: '도장A', code: 'painting_a', price: 50000 },
|
||||
{ value: '80000', label: '도장B', code: 'painting_b', price: 80000 },
|
||||
],
|
||||
motors: [
|
||||
{ value: '300000', label: '모터 300,000', code: 'motor_300k', price: 300000 },
|
||||
{ value: '500000', label: '모터 500,000', code: 'motor_500k', price: 500000 },
|
||||
],
|
||||
controllers: [
|
||||
{ value: '150000', label: '제어기 150,000', code: 'ctrl_150k', price: 150000 },
|
||||
{ value: '250000', label: '제어기 250,000', code: 'ctrl_250k', price: 250000 },
|
||||
],
|
||||
widthConstructions: [
|
||||
{ value: '300000', label: '3.01~4.0M', code: 'w_3_4m', price: 300000 },
|
||||
{ value: '400000', label: '4.01~5.0M', code: 'w_4_5m', price: 400000 },
|
||||
{ value: '500000', label: '5.01~6.0M', code: 'w_5_6m', price: 500000 },
|
||||
{ value: '600000', label: '6.01~7.0M', code: 'w_6_7m', price: 600000 },
|
||||
],
|
||||
heightConstructions: [
|
||||
{ value: '5000', label: '3.51~4.5M', code: 'h_3_4m', price: 5000 },
|
||||
{ value: '8000', label: '4.51~5.5M', code: 'h_4_5m', price: 8000 },
|
||||
{ value: '10000', label: '5.51~6.5M', code: 'h_5_6m', price: 10000 },
|
||||
],
|
||||
};
|
||||
|
||||
export function EstimateDetailTableSection({
|
||||
detailItems,
|
||||
appliedPrices,
|
||||
isViewMode,
|
||||
options,
|
||||
onAddItems,
|
||||
onRemoveItem,
|
||||
onRemoveSelected,
|
||||
@@ -112,6 +159,8 @@ export function EstimateDetailTableSection({
|
||||
onApplyAdjustedPrice,
|
||||
onReset,
|
||||
}: EstimateDetailTableSectionProps) {
|
||||
// API 옵션이 없으면 기본 옵션 사용
|
||||
const opts = options || DEFAULT_OPTIONS;
|
||||
const selectedCount = detailItems.filter((item) => (item as unknown as { selected?: boolean }).selected).length;
|
||||
const allSelected = detailItems.length > 0 && detailItems.every((item) => (item as unknown as { selected?: boolean }).selected);
|
||||
const totals = calculateTotalsWithApplied(detailItems, appliedPrices);
|
||||
@@ -279,7 +328,7 @@ export function EstimateDetailTableSection({
|
||||
{/* 01: 명칭 */}
|
||||
<TableCell>
|
||||
<Input
|
||||
value={item.name}
|
||||
value={item.name ?? ''}
|
||||
onChange={(e) => onItemChange(item.id, 'name', e.target.value)}
|
||||
disabled={isViewMode}
|
||||
className={`w-full min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||||
@@ -288,7 +337,7 @@ export function EstimateDetailTableSection({
|
||||
{/* 02: 제품 */}
|
||||
<TableCell>
|
||||
<Select
|
||||
value={item.material}
|
||||
value={item.material ?? ''}
|
||||
onValueChange={(val) => onItemChange(item.id, 'material', val)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
@@ -296,7 +345,7 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOCK_MATERIALS.map((option) => (
|
||||
{opts.materials.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
@@ -309,7 +358,7 @@ export function EstimateDetailTableSection({
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={item.width}
|
||||
value={item.width ?? 0}
|
||||
onChange={(e) => onItemChange(item.id, 'width', Number(e.target.value))}
|
||||
disabled={isViewMode}
|
||||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||||
@@ -320,7 +369,7 @@ export function EstimateDetailTableSection({
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={item.height}
|
||||
value={item.height ?? 0}
|
||||
onChange={(e) => onItemChange(item.id, 'height', Number(e.target.value))}
|
||||
disabled={isViewMode}
|
||||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||||
@@ -419,9 +468,11 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">직접입력</SelectItem>
|
||||
<SelectItem value="50000">도장A</SelectItem>
|
||||
<SelectItem value="80000">도장B</SelectItem>
|
||||
{opts.paintings.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
@@ -436,8 +487,11 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="300000">모터 300,000</SelectItem>
|
||||
<SelectItem value="500000">모터 500,000</SelectItem>
|
||||
{opts.motors.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
@@ -452,8 +506,11 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="150000">제어기 150,000</SelectItem>
|
||||
<SelectItem value="250000">제어기 250,000</SelectItem>
|
||||
{opts.controllers.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
@@ -468,10 +525,11 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="300000">3.01~4.0M</SelectItem>
|
||||
<SelectItem value="400000">4.01~5.0M</SelectItem>
|
||||
<SelectItem value="500000">5.01~6.0M</SelectItem>
|
||||
<SelectItem value="600000">6.01~7.0M</SelectItem>
|
||||
{opts.widthConstructions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
@@ -486,9 +544,11 @@ export function EstimateDetailTableSection({
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5000">3.51~4.5M</SelectItem>
|
||||
<SelectItem value="8000">4.51~5.5M</SelectItem>
|
||||
<SelectItem value="10000">5.51~6.5M</SelectItem>
|
||||
{opts.heightConstructions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
@@ -507,7 +567,7 @@ export function EstimateDetailTableSection({
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={item.expense}
|
||||
value={item.expense ?? 0}
|
||||
onChange={(e) => onItemChange(item.id, 'expense', Number(e.target.value))}
|
||||
disabled={isViewMode}
|
||||
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||||
@@ -528,7 +588,7 @@ export function EstimateDetailTableSection({
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={item.quantity}
|
||||
value={item.quantity ?? 0}
|
||||
onChange={(e) => onItemChange(item.id, 'quantity', Number(e.target.value))}
|
||||
disabled={isViewMode}
|
||||
className={`text-right min-w-[40px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// 목업 재료 목록
|
||||
export const MOCK_MATERIALS = [
|
||||
{ value: 'screen', label: '스크린' },
|
||||
{ value: 'slat', label: '슬랫' },
|
||||
{ value: 'bending', label: '벤딩' },
|
||||
{ value: 'jointbar', label: '조인트바' },
|
||||
];
|
||||
|
||||
// 공과 품목은 Items API (type=RM)에서 조회
|
||||
// MOCK_EXPENSES 제거됨 - getExpenseItemOptions() 사용
|
||||
// 견적서 상수 정의
|
||||
// 모든 MOCK 데이터는 API로 대체됨:
|
||||
// - 재료(material_type): getCommonCodeOptions('material_type')
|
||||
// - 공과 품목: getExpenseItemOptions()
|
||||
// - 도장/모터/제어기/시공비: getCommonCodeOptions('{group}')
|
||||
// - 거래처/견적자: getClientOptions(), getUserOptions()
|
||||
Reference in New Issue
Block a user