feat(WEB): 수주관리 Phase 2 타입 정의 확장 및 공정관리 개별 품목 표시 수정

- Order, OrderItem 인터페이스에 상세 페이지용 필드 추가
- OrderFormData, OrderItemFormData에 수정 페이지용 필드 추가
- 변환 함수에서 새 필드 매핑 처리
- 공정관리 개별 품목을 ID 대신 품목명으로 표시
This commit is contained in:
2026-01-08 20:57:49 +09:00
parent ba36c0ec19
commit fde8726e14
4 changed files with 87 additions and 29 deletions

View File

@@ -156,19 +156,36 @@ export interface Order {
remarks?: string;
note?: string;
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 {
id: string;
itemId?: number;
itemCode?: string; // 품목코드
itemName: string;
specification?: string;
spec?: string; // specification alias
type?: string; // 층 (layer)
symbol?: string; // 부호
quantity: number;
unit?: string;
unitPrice: number;
supplyAmount: number;
taxAmount: number;
totalAmount: number;
amount?: number; // totalAmount alias
sortOrder: number;
}
@@ -191,10 +208,20 @@ export interface OrderFormData {
remarks?: string;
note?: string;
items?: OrderItemFormData[];
// 수정 페이지용 추가 필드
expectedShipDate?: string; // 출고예정일
deliveryRequestDate?: string; // 납품요청일
deliveryMethod?: string; // 배송방식 (deliveryMethodCode alias)
shippingCost?: string; // 운임비용
receiver?: string; // 수신자
receiverContact?: string; // 수신처 연락처
address?: string; // 수신처 주소
addressDetail?: string; // 상세주소
}
export interface OrderItemFormData {
itemId?: number;
itemCode?: string; // 품목코드
itemName: string;
specification?: string;
quantity: number;
@@ -302,7 +329,18 @@ function transformApiToFrontend(apiData: ApiOrder): Order {
memo: apiData.memo ?? undefined,
remarks: apiData.remarks ?? 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 {
id: String(apiItem.id),
itemId: apiItem.item_id ?? undefined,
itemCode: apiItem.item_id ? `ITEM-${apiItem.item_id}` : undefined, // 임시: 실제 item_code는 API에서 제공 필요
itemName: apiItem.item_name,
specification: apiItem.specification ?? undefined,
spec: apiItem.specification ?? undefined, // specification alias
type: undefined, // 층 - API에 해당 필드 없음
symbol: undefined, // 부호 - API에 해당 필드 없음
quantity: apiItem.quantity,
unit: apiItem.unit ?? undefined,
unitPrice: apiItem.unit_price,
supplyAmount: apiItem.supply_amount,
taxAmount: apiItem.tax_amount,
totalAmount: apiItem.total_amount,
amount: apiItem.total_amount, // totalAmount alias
sortOrder: apiItem.sort_order,
};
}

View File

@@ -178,39 +178,36 @@ export function ProcessDetail({ process }: ProcessDetailProps) {
<CardTitle className="text-base flex items-center gap-2">
<Package className="h-4 w-4" />
{individualItems.length > 0 && individualItems[0].items && (
<Badge variant="secondary" className="ml-2">
{individualItems[0].items.length}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="pt-6">
{individualItems.length === 0 ? (
{individualItems.length === 0 || !individualItems[0].items?.length ? (
<div className="text-center py-8 text-muted-foreground">
<Package className="h-10 w-10 mx-auto mb-3 opacity-30" />
<p className="text-sm"> </p>
</div>
) : (
<div className="space-y-3">
{individualItems.map((rule) => (
<div
key={rule.id}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-4">
<Badge variant={rule.isActive ? 'default' : 'secondary'}>
{rule.isActive ? '활성' : '비활성'}
</Badge>
<div>
<div className="font-medium">
{rule.conditionValue}
</div>
{rule.description && (
<div className="text-sm text-muted-foreground">
{rule.description}
</div>
)}
<div className="max-h-80 overflow-y-auto">
<div className="space-y-2">
{individualItems[0].items.map((item) => (
<div
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-3">
<Badge variant="outline" className="font-mono text-xs">
{item.code}
</Badge>
<span className="font-medium">{item.name}</span>
</div>
</div>
<Badge variant="outline">: {rule.priority}</Badge>
</div>
))}
))}
</div>
</div>
)}
</CardContent>

View File

@@ -3,7 +3,7 @@
import { isNextRedirectError } from '@/lib/utils/redirect-error';
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 타입 정의
@@ -107,13 +107,22 @@ function transformApiToFrontend(apiData: ApiProcess): Process {
function transformProcessItemsToRules(processItems: ApiProcessItem[]): ClassificationRule[] {
if (processItems.length === 0) return [];
const activeItems = processItems.filter(pi => pi.is_active);
if (activeItems.length === 0) return [];
// 모든 품목 ID를 쉼표로 구분하여 하나의 규칙으로 통합
const itemIds = processItems
.filter(pi => pi.is_active)
const itemIds = activeItems
.map(pi => String(pi.item_id))
.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 [{
id: `individual-${Date.now()}`,
@@ -122,9 +131,10 @@ function transformProcessItemsToRules(processItems: ApiProcessItem[]): Classific
matchingType: 'equals',
conditionValue: itemIds,
priority: 0,
description: `개별 품목 ${processItems.length}`,
description: `개별 품목 ${activeItems.length}`,
isActive: true,
createdAt: new Date().toISOString(),
items, // 품목 상세 정보 추가
}];
}

View File

@@ -17,6 +17,13 @@ export type RuleType = '품목코드' | '품목명' | '품목구분';
// 매칭 방식
export type MatchingType = 'startsWith' | 'endsWith' | 'contains' | 'equals';
// 개별 품목 정보
export interface IndividualItem {
id: string;
code: string;
name: string;
}
// 자동 분류 규칙
export interface ClassificationRule {
id: string;
@@ -28,6 +35,7 @@ export interface ClassificationRule {
description?: string;
isActive: boolean;
createdAt: string;
items?: IndividualItem[]; // 개별 품목인 경우 품목 정보
}
// 자동 분류 규칙 입력용 (id, createdAt 제외)