feat(WEB): 수주관리 Phase 2 타입 정의 확장 및 공정관리 개별 품목 표시 수정
- Order, OrderItem 인터페이스에 상세 페이지용 필드 추가 - OrderFormData, OrderItemFormData에 수정 페이지용 필드 추가 - 변환 함수에서 새 필드 매핑 처리 - 공정관리 개별 품목을 ID 대신 품목명으로 표시
This commit is contained in:
@@ -156,19 +156,36 @@ export interface Order {
|
|||||||
remarks?: string;
|
remarks?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
items?: OrderItem[];
|
items?: OrderItem[];
|
||||||
|
// 상세 페이지용 추가 필드
|
||||||
|
manager?: string; // 담당자
|
||||||
|
contact?: string; // 연락처 (client_contact)
|
||||||
|
deliveryRequestDate?: string; // 납품요청일
|
||||||
|
shippingCost?: string; // 운임비용
|
||||||
|
receiver?: string; // 수신자
|
||||||
|
receiverContact?: string; // 수신처 연락처
|
||||||
|
address?: string; // 수신처 주소
|
||||||
|
addressDetail?: string; // 상세주소
|
||||||
|
subtotal?: number; // 소계 (supply_amount와 동일)
|
||||||
|
discountRate?: number; // 할인율
|
||||||
|
totalAmount?: number; // 총금액 (amount와 동일하지만 명시적)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
id: string;
|
id: string;
|
||||||
itemId?: number;
|
itemId?: number;
|
||||||
|
itemCode?: string; // 품목코드
|
||||||
itemName: string;
|
itemName: string;
|
||||||
specification?: string;
|
specification?: string;
|
||||||
|
spec?: string; // specification alias
|
||||||
|
type?: string; // 층 (layer)
|
||||||
|
symbol?: string; // 부호
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
supplyAmount: number;
|
supplyAmount: number;
|
||||||
taxAmount: number;
|
taxAmount: number;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
|
amount?: number; // totalAmount alias
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,10 +208,20 @@ export interface OrderFormData {
|
|||||||
remarks?: string;
|
remarks?: string;
|
||||||
note?: string;
|
note?: string;
|
||||||
items?: OrderItemFormData[];
|
items?: OrderItemFormData[];
|
||||||
|
// 수정 페이지용 추가 필드
|
||||||
|
expectedShipDate?: string; // 출고예정일
|
||||||
|
deliveryRequestDate?: string; // 납품요청일
|
||||||
|
deliveryMethod?: string; // 배송방식 (deliveryMethodCode alias)
|
||||||
|
shippingCost?: string; // 운임비용
|
||||||
|
receiver?: string; // 수신자
|
||||||
|
receiverContact?: string; // 수신처 연락처
|
||||||
|
address?: string; // 수신처 주소
|
||||||
|
addressDetail?: string; // 상세주소
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderItemFormData {
|
export interface OrderItemFormData {
|
||||||
itemId?: number;
|
itemId?: number;
|
||||||
|
itemCode?: string; // 품목코드
|
||||||
itemName: string;
|
itemName: string;
|
||||||
specification?: string;
|
specification?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
@@ -302,7 +329,18 @@ function transformApiToFrontend(apiData: ApiOrder): Order {
|
|||||||
memo: apiData.memo ?? undefined,
|
memo: apiData.memo ?? undefined,
|
||||||
remarks: apiData.remarks ?? undefined,
|
remarks: apiData.remarks ?? undefined,
|
||||||
note: apiData.note ?? undefined,
|
note: apiData.note ?? undefined,
|
||||||
items: apiData.items?.map(transformItemApiToFrontend),
|
items: apiData.items?.map(transformItemApiToFrontend), // 상세 페이지용 추가 필드 (API에서 매핑)
|
||||||
|
manager: apiData.client?.representative ?? undefined,
|
||||||
|
contact: apiData.client_contact ?? apiData.client?.phone ?? undefined,
|
||||||
|
deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유
|
||||||
|
shippingCost: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||||
|
receiver: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||||
|
receiverContact: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||||
|
address: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||||
|
addressDetail: undefined, // API에 해당 필드 없음 - 추후 구현
|
||||||
|
subtotal: apiData.supply_amount,
|
||||||
|
discountRate: apiData.discount_rate,
|
||||||
|
totalAmount: apiData.total_amount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,14 +348,19 @@ function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem {
|
|||||||
return {
|
return {
|
||||||
id: String(apiItem.id),
|
id: String(apiItem.id),
|
||||||
itemId: apiItem.item_id ?? undefined,
|
itemId: apiItem.item_id ?? undefined,
|
||||||
|
itemCode: apiItem.item_id ? `ITEM-${apiItem.item_id}` : undefined, // 임시: 실제 item_code는 API에서 제공 필요
|
||||||
itemName: apiItem.item_name,
|
itemName: apiItem.item_name,
|
||||||
specification: apiItem.specification ?? undefined,
|
specification: apiItem.specification ?? undefined,
|
||||||
|
spec: apiItem.specification ?? undefined, // specification alias
|
||||||
|
type: undefined, // 층 - API에 해당 필드 없음
|
||||||
|
symbol: undefined, // 부호 - API에 해당 필드 없음
|
||||||
quantity: apiItem.quantity,
|
quantity: apiItem.quantity,
|
||||||
unit: apiItem.unit ?? undefined,
|
unit: apiItem.unit ?? undefined,
|
||||||
unitPrice: apiItem.unit_price,
|
unitPrice: apiItem.unit_price,
|
||||||
supplyAmount: apiItem.supply_amount,
|
supplyAmount: apiItem.supply_amount,
|
||||||
taxAmount: apiItem.tax_amount,
|
taxAmount: apiItem.tax_amount,
|
||||||
totalAmount: apiItem.total_amount,
|
totalAmount: apiItem.total_amount,
|
||||||
|
amount: apiItem.total_amount, // totalAmount alias
|
||||||
sortOrder: apiItem.sort_order,
|
sortOrder: apiItem.sort_order,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,39 +178,36 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
|
|||||||
<CardTitle className="text-base flex items-center gap-2">
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
<Package className="h-4 w-4" />
|
<Package className="h-4 w-4" />
|
||||||
개별 품목
|
개별 품목
|
||||||
|
{individualItems.length > 0 && individualItems[0].items && (
|
||||||
|
<Badge variant="secondary" className="ml-2">
|
||||||
|
{individualItems[0].items.length}개
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
{individualItems.length === 0 ? (
|
{individualItems.length === 0 || !individualItems[0].items?.length ? (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
<Package className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
<Package className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||||
<p className="text-sm">등록된 개별 품목이 없습니다</p>
|
<p className="text-sm">등록된 개별 품목이 없습니다</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="max-h-80 overflow-y-auto">
|
||||||
{individualItems.map((rule) => (
|
<div className="space-y-2">
|
||||||
<div
|
{individualItems[0].items.map((item) => (
|
||||||
key={rule.id}
|
<div
|
||||||
className="flex items-center justify-between p-4 border rounded-lg"
|
key={item.id}
|
||||||
>
|
className="flex items-center justify-between p-3 border rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors"
|
||||||
<div className="flex items-center gap-4">
|
>
|
||||||
<Badge variant={rule.isActive ? 'default' : 'secondary'}>
|
<div className="flex items-center gap-3">
|
||||||
{rule.isActive ? '활성' : '비활성'}
|
<Badge variant="outline" className="font-mono text-xs">
|
||||||
</Badge>
|
{item.code}
|
||||||
<div>
|
</Badge>
|
||||||
<div className="font-medium">
|
<span className="font-medium">{item.name}</span>
|
||||||
{rule.conditionValue}
|
|
||||||
</div>
|
|
||||||
{rule.description && (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{rule.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline">우선순위: {rule.priority}</Badge>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||||
import type { Process, ProcessFormData, ClassificationRule } from '@/types/process';
|
import type { Process, ProcessFormData, ClassificationRule, IndividualItem } from '@/types/process';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// API 타입 정의
|
// API 타입 정의
|
||||||
@@ -107,13 +107,22 @@ function transformApiToFrontend(apiData: ApiProcess): Process {
|
|||||||
function transformProcessItemsToRules(processItems: ApiProcessItem[]): ClassificationRule[] {
|
function transformProcessItemsToRules(processItems: ApiProcessItem[]): ClassificationRule[] {
|
||||||
if (processItems.length === 0) return [];
|
if (processItems.length === 0) return [];
|
||||||
|
|
||||||
|
const activeItems = processItems.filter(pi => pi.is_active);
|
||||||
|
if (activeItems.length === 0) return [];
|
||||||
|
|
||||||
// 모든 품목 ID를 쉼표로 구분하여 하나의 규칙으로 통합
|
// 모든 품목 ID를 쉼표로 구분하여 하나의 규칙으로 통합
|
||||||
const itemIds = processItems
|
const itemIds = activeItems
|
||||||
.filter(pi => pi.is_active)
|
|
||||||
.map(pi => String(pi.item_id))
|
.map(pi => String(pi.item_id))
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
if (!itemIds) return [];
|
// 품목 상세 정보 추출 (code, name 포함)
|
||||||
|
const items: IndividualItem[] = activeItems
|
||||||
|
.filter(pi => pi.item) // item 정보가 있는 것만
|
||||||
|
.map(pi => ({
|
||||||
|
id: String(pi.item!.id),
|
||||||
|
code: pi.item!.code,
|
||||||
|
name: pi.item!.name,
|
||||||
|
}));
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
id: `individual-${Date.now()}`,
|
id: `individual-${Date.now()}`,
|
||||||
@@ -122,9 +131,10 @@ function transformProcessItemsToRules(processItems: ApiProcessItem[]): Classific
|
|||||||
matchingType: 'equals',
|
matchingType: 'equals',
|
||||||
conditionValue: itemIds,
|
conditionValue: itemIds,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
description: `개별 품목 ${processItems.length}개`,
|
description: `개별 품목 ${activeItems.length}개`,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
items, // 품목 상세 정보 추가
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ export type RuleType = '품목코드' | '품목명' | '품목구분';
|
|||||||
// 매칭 방식
|
// 매칭 방식
|
||||||
export type MatchingType = 'startsWith' | 'endsWith' | 'contains' | 'equals';
|
export type MatchingType = 'startsWith' | 'endsWith' | 'contains' | 'equals';
|
||||||
|
|
||||||
|
// 개별 품목 정보
|
||||||
|
export interface IndividualItem {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 자동 분류 규칙
|
// 자동 분류 규칙
|
||||||
export interface ClassificationRule {
|
export interface ClassificationRule {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,6 +35,7 @@ export interface ClassificationRule {
|
|||||||
description?: string;
|
description?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
items?: IndividualItem[]; // 개별 품목인 경우 품목 정보
|
||||||
}
|
}
|
||||||
|
|
||||||
// 자동 분류 규칙 입력용 (id, createdAt 제외)
|
// 자동 분류 규칙 입력용 (id, createdAt 제외)
|
||||||
|
|||||||
Reference in New Issue
Block a user