feat(order-management): Mock → API 연동 및 common-codes 유틸리티 추가
- common-codes.ts 신규 생성 (공용 코드 조회 유틸리티) - getCommonCodes(), getCommonCodeOptions() 기본 함수 - getOrderStatusOptions(), getOrderTypeOptions() 등 편의 함수 - order-management/actions.ts Mock 데이터 → 실제 API 연동 - 상태 변환 함수 (Frontend ↔ Backend 매핑) - getOrderList(), getOrderStats(), createOrder(), updateOrder() 등 구현 - lib/api/index.ts에 common-codes 모듈 export 추가
This commit is contained in:
@@ -1,131 +1,189 @@
|
||||
'use server';
|
||||
|
||||
import type { Order, OrderStats, OrderType, OrderDetail, OrderDetailFormData } from './types';
|
||||
import { MOCK_ORDER_DETAIL } from './types';
|
||||
import { format, addDays, subDays, subMonths } from 'date-fns';
|
||||
import type { Order, OrderStats, OrderDetail, OrderDetailFormData, OrderStatus, OrderType } from './types';
|
||||
import { apiClient, getOrderStatusOptions, getOrderTypeOptions } from '@/lib/api';
|
||||
|
||||
// ========================================
|
||||
// 타입 변환 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 목업 발주 데이터 생성 (고정 데이터)
|
||||
* - types.ts의 MOCK 옵션들과 정확히 일치해야 필터가 동작함
|
||||
* - Math.random() 제거 → index 기반 deterministic 데이터
|
||||
* Backend status_code → Frontend OrderStatus 변환
|
||||
* DRAFT → waiting, CONFIRMED → order_complete, IN_PROGRESS → delivery_scheduled, COMPLETED → delivery_complete
|
||||
*/
|
||||
function generateMockOrders(): Order[] {
|
||||
// types.ts MOCK_PARTNERS와 일치
|
||||
const partners = [
|
||||
{ id: '1', name: '(주)대한건설' },
|
||||
{ id: '2', name: '삼성물산' },
|
||||
{ id: '3', name: '현대건설' },
|
||||
{ id: '4', name: 'GS건설' },
|
||||
{ id: '5', name: '대림산업' },
|
||||
];
|
||||
|
||||
// types.ts MOCK_SITES와 일치
|
||||
const sites = [
|
||||
'강남 오피스빌딩 신축',
|
||||
'판교 데이터센터',
|
||||
'송도 물류센터',
|
||||
'인천공항 터미널',
|
||||
'부산항 창고',
|
||||
];
|
||||
|
||||
const names = [
|
||||
'철근 HD13',
|
||||
'철근 HD16',
|
||||
'철근 HD19',
|
||||
'철근 HD22',
|
||||
'H빔 300x300',
|
||||
'H빔 200x200',
|
||||
'콘크리트 25-21-12',
|
||||
'레미콘 배합',
|
||||
];
|
||||
|
||||
const items = [
|
||||
'철근 HD13',
|
||||
'철근 HD16',
|
||||
'철근 HD19',
|
||||
'H빔',
|
||||
'레미콘',
|
||||
'앵커볼트',
|
||||
'데크플레이트',
|
||||
'용접봉',
|
||||
];
|
||||
|
||||
// types.ts MOCK_CONSTRUCTION_PM과 일치
|
||||
const constructionPMs = ['홍길동', '김철수', '이영희', '박민수'];
|
||||
// types.ts MOCK_ORDER_MANAGERS와 일치
|
||||
const orderManagers = ['김담당', '이담당', '박담당', '최담당'];
|
||||
// types.ts MOCK_ORDER_COMPANIES와 일치
|
||||
const orderCompanies = ['A건설', 'B철강', 'C자재', 'D산업'];
|
||||
// types.ts MOCK_WORK_TEAM_LEADERS와 일치
|
||||
const workTeamLeaders = ['이반장', '김반장', '박반장', '최반장'];
|
||||
const orderTypes: OrderType[] = ['steel_bar', 'material', 'outsourcing'];
|
||||
const statuses: Order['status'][] = ['waiting', 'order_complete', 'delivery_scheduled', 'delivery_complete'];
|
||||
|
||||
const orders: Order[] = [];
|
||||
// 고정 기준일 (2026-01-06)
|
||||
const baseDate = new Date(2026, 0, 6);
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
// index 기반 deterministic 선택 (랜덤 제거)
|
||||
const partner = partners[i % partners.length];
|
||||
const site = sites[i % sites.length];
|
||||
const status = statuses[i % statuses.length];
|
||||
const orderType = orderTypes[i % orderTypes.length];
|
||||
|
||||
// 날짜도 index 기반으로 고정
|
||||
const monthOffset = i % 3; // 0, 1, 2개월 전
|
||||
const dayOffset = (i * 3) % 30; // 0~29일 분산
|
||||
const periodStart = subMonths(addDays(baseDate, -dayOffset), monthOffset);
|
||||
const periodEnd = addDays(periodStart, 10 + (i % 20)); // 10~29일 기간
|
||||
const orderDate = subDays(periodStart, i % 5);
|
||||
const constructionStartDate = addDays(periodStart, i % 5);
|
||||
const plannedDelivery = addDays(orderDate, 3 + (i % 14));
|
||||
const actualDelivery = status === 'delivery_complete'
|
||||
? format(addDays(plannedDelivery, (i % 5) - 2), 'yyyy-MM-dd')
|
||||
: null;
|
||||
|
||||
orders.push({
|
||||
id: `order-${i + 1}`,
|
||||
contractNumber: `CT-${2026}-${String(i + 1).padStart(4, '0')}`,
|
||||
partnerId: partner.id,
|
||||
partnerName: partner.name,
|
||||
siteName: site,
|
||||
name: names[i % names.length],
|
||||
constructionPM: constructionPMs[i % constructionPMs.length],
|
||||
orderManager: orderManagers[i % orderManagers.length],
|
||||
orderNumber: `ORD-${2026}-${String(i + 1).padStart(4, '0')}`,
|
||||
orderCompany: orderCompanies[i % orderCompanies.length],
|
||||
workTeamLeader: workTeamLeaders[i % workTeamLeaders.length],
|
||||
constructionStartDate: format(constructionStartDate, 'yyyy-MM-dd'),
|
||||
orderType,
|
||||
item: items[i % items.length],
|
||||
quantity: 10 + (i * 7) % 90, // 10~99 고정 패턴
|
||||
orderDate: format(orderDate, 'yyyy-MM-dd'),
|
||||
plannedDeliveryDate: format(plannedDelivery, 'yyyy-MM-dd'),
|
||||
actualDeliveryDate: actualDelivery,
|
||||
status,
|
||||
periodStart: format(periodStart, 'yyyy-MM-dd'),
|
||||
periodEnd: format(periodEnd, 'yyyy-MM-dd'),
|
||||
createdAt: format(subDays(periodStart, i % 10), 'yyyy-MM-dd\'T\'HH:mm:ss'),
|
||||
updatedAt: format(baseDate, 'yyyy-MM-dd\'T\'HH:mm:ss'),
|
||||
});
|
||||
}
|
||||
|
||||
return orders;
|
||||
function transformStatus(backendStatus: string | null | undefined): OrderStatus {
|
||||
const statusMap: Record<string, OrderStatus> = {
|
||||
DRAFT: 'waiting',
|
||||
CONFIRMED: 'order_complete',
|
||||
IN_PROGRESS: 'delivery_scheduled',
|
||||
COMPLETED: 'delivery_complete',
|
||||
CANCELLED: 'waiting', // 취소는 대기로 표시
|
||||
};
|
||||
return statusMap[backendStatus?.toUpperCase() || ''] || 'waiting';
|
||||
}
|
||||
|
||||
// 캐시된 목업 데이터
|
||||
let cachedOrders: Order[] | null = null;
|
||||
|
||||
function getMockOrders(): Order[] {
|
||||
if (!cachedOrders) {
|
||||
cachedOrders = generateMockOrders();
|
||||
}
|
||||
return cachedOrders;
|
||||
/**
|
||||
* Frontend OrderStatus → Backend status_code 변환
|
||||
*/
|
||||
function transformToBackendStatus(frontendStatus: OrderStatus): string {
|
||||
const statusMap: Record<OrderStatus, string> = {
|
||||
waiting: 'DRAFT',
|
||||
order_complete: 'CONFIRMED',
|
||||
delivery_scheduled: 'IN_PROGRESS',
|
||||
delivery_complete: 'COMPLETED',
|
||||
};
|
||||
return statusMap[frontendStatus] || 'DRAFT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend order_type_code → Frontend OrderType 변환
|
||||
*/
|
||||
function transformOrderType(backendType: string | null | undefined): OrderType {
|
||||
// Backend: ORDER, PURCHASE
|
||||
// Frontend: steel_bar, material, outsourcing
|
||||
// 현재 Backend는 ORDER/PURCHASE만 있으므로 options에서 가져오거나 기본값 사용
|
||||
const typeMap: Record<string, OrderType> = {
|
||||
ORDER: 'steel_bar',
|
||||
PURCHASE: 'material',
|
||||
};
|
||||
return typeMap[backendType?.toUpperCase() || ''] || 'steel_bar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend OrderType → Backend order_type_code 변환
|
||||
*/
|
||||
function transformToBackendOrderType(frontendType: OrderType): string {
|
||||
const typeMap: Record<OrderType, string> = {
|
||||
steel_bar: 'ORDER',
|
||||
material: 'PURCHASE',
|
||||
outsourcing: 'ORDER',
|
||||
};
|
||||
return typeMap[frontendType] || 'ORDER';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// API 응답 타입
|
||||
// ========================================
|
||||
|
||||
interface ApiOrder {
|
||||
id: number;
|
||||
order_no: string;
|
||||
client_id: number | null;
|
||||
client_name: string | null;
|
||||
client?: { id: number; name: string } | null;
|
||||
site_name: string | null;
|
||||
status_code: string;
|
||||
order_type_code: string;
|
||||
received_at: string | null;
|
||||
delivery_date: string | null;
|
||||
actual_delivery_date: string | null;
|
||||
total_amount: number | null;
|
||||
supply_amount: number | null;
|
||||
tax_amount: number | null;
|
||||
memo: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
items?: ApiOrderItem[];
|
||||
quote?: { id: number; quote_no: string; site_name: string } | null;
|
||||
}
|
||||
|
||||
interface ApiOrderItem {
|
||||
id: number;
|
||||
item_id: number | null;
|
||||
item_name: string;
|
||||
specification: string | null;
|
||||
quantity: number;
|
||||
unit: string | null;
|
||||
unit_price: number;
|
||||
supply_amount: number;
|
||||
tax_amount: number;
|
||||
total_amount: number;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
interface ApiOrderStats {
|
||||
total: number;
|
||||
draft: number;
|
||||
confirmed: number;
|
||||
in_progress: number;
|
||||
completed: number;
|
||||
cancelled: number;
|
||||
total_amount: number;
|
||||
confirmed_amount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → Order 타입 변환
|
||||
*/
|
||||
function transformOrder(apiOrder: ApiOrder): Order {
|
||||
return {
|
||||
id: String(apiOrder.id),
|
||||
contractNumber: apiOrder.quote?.quote_no || '',
|
||||
partnerId: apiOrder.client_id ? String(apiOrder.client_id) : '',
|
||||
partnerName: apiOrder.client?.name || apiOrder.client_name || '',
|
||||
siteName: apiOrder.site_name || '',
|
||||
name: apiOrder.items?.[0]?.item_name || '',
|
||||
constructionPM: '', // Backend에 없음 - options로 확장 가능
|
||||
orderManager: '', // Backend에 없음 - options로 확장 가능
|
||||
orderNumber: apiOrder.order_no,
|
||||
orderCompany: '', // Backend에 없음 - options로 확장 가능
|
||||
workTeamLeader: '', // Backend에 없음 - options로 확장 가능
|
||||
constructionStartDate: apiOrder.received_at || '',
|
||||
orderType: transformOrderType(apiOrder.order_type_code),
|
||||
item: apiOrder.items?.[0]?.item_name || '',
|
||||
quantity: apiOrder.items?.[0]?.quantity || 0,
|
||||
orderDate: apiOrder.received_at || '',
|
||||
plannedDeliveryDate: apiOrder.delivery_date || '',
|
||||
actualDeliveryDate: apiOrder.actual_delivery_date || null,
|
||||
status: transformStatus(apiOrder.status_code),
|
||||
periodStart: apiOrder.received_at || '',
|
||||
periodEnd: apiOrder.delivery_date || '',
|
||||
createdAt: apiOrder.created_at,
|
||||
updatedAt: apiOrder.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 → OrderDetail 타입 변환
|
||||
*/
|
||||
function transformOrderDetail(apiOrder: ApiOrder): OrderDetail {
|
||||
const baseOrder = transformOrder(apiOrder);
|
||||
|
||||
return {
|
||||
...baseOrder,
|
||||
orderCompanyId: '', // Backend에 없음
|
||||
deliveryLocationType: 'site',
|
||||
deliveryAddress: '', // Backend에 없음
|
||||
deliveryMemo: apiOrder.memo || '',
|
||||
totalAmount: apiOrder.total_amount || 0,
|
||||
supplyAmount: apiOrder.supply_amount || 0,
|
||||
taxAmount: apiOrder.tax_amount || 0,
|
||||
categories: [], // Backend 구조와 다름 - items를 카테고리로 그룹화 필요
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderDetailFormData → API 요청 데이터 변환
|
||||
*/
|
||||
function transformOrderToApi(data: OrderDetailFormData): Record<string, unknown> {
|
||||
return {
|
||||
client_id: data.partnerId ? parseInt(data.partnerId, 10) : null,
|
||||
site_name: data.siteName,
|
||||
status_code: transformToBackendStatus(data.status),
|
||||
order_type_code: transformToBackendOrderType(data.orderType),
|
||||
delivery_date: data.deliveryAddress ? undefined : undefined, // 필드 매핑 필요
|
||||
memo: data.deliveryMemo,
|
||||
// items 변환은 별도 처리 필요
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// API 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 발주 목록 조회
|
||||
* GET /api/v1/orders
|
||||
*/
|
||||
export async function getOrderList(params?: {
|
||||
size?: number;
|
||||
@@ -141,61 +199,67 @@ export async function getOrderList(params?: {
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업 데이터
|
||||
let orders = getMockOrders();
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
// 날짜 필터
|
||||
if (params?.startDate && params?.endDate) {
|
||||
orders = orders.filter((order) => {
|
||||
return order.periodStart >= params.startDate! && order.periodEnd <= params.endDate!;
|
||||
});
|
||||
}
|
||||
// 페이지네이션
|
||||
if (params?.page) queryParams.page = String(params.page);
|
||||
if (params?.size) queryParams.size = String(params.size);
|
||||
|
||||
// 상태 필터
|
||||
// 검색
|
||||
if (params?.search) queryParams.q = params.search;
|
||||
|
||||
// 상태 필터 (Frontend → Backend 변환)
|
||||
if (params?.status && params.status !== 'all') {
|
||||
orders = orders.filter((order) => order.status === params.status);
|
||||
queryParams.status = transformToBackendStatus(params.status as OrderStatus);
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
if (params?.partnerId && params.partnerId !== 'all') {
|
||||
orders = orders.filter((order) => order.partnerId === params.partnerId);
|
||||
queryParams.client_id = params.partnerId;
|
||||
}
|
||||
|
||||
// 검색
|
||||
if (params?.search) {
|
||||
const search = params.search.toLowerCase();
|
||||
orders = orders.filter(
|
||||
(order) =>
|
||||
order.orderNumber.toLowerCase().includes(search) ||
|
||||
order.partnerName.toLowerCase().includes(search) ||
|
||||
order.siteName.toLowerCase().includes(search) ||
|
||||
order.orderManager.toLowerCase().includes(search)
|
||||
);
|
||||
// 날짜 범위 필터
|
||||
if (params?.startDate) {
|
||||
queryParams.date_from = params.startDate;
|
||||
}
|
||||
if (params?.endDate) {
|
||||
queryParams.date_to = params.endDate;
|
||||
}
|
||||
|
||||
// 페이지네이션
|
||||
const page = params?.page || 1;
|
||||
const size = params?.size || 1000;
|
||||
const start = (page - 1) * size;
|
||||
const paginatedOrders = orders.slice(start, start + size);
|
||||
const response = await apiClient.get<{
|
||||
data: ApiOrder[];
|
||||
meta?: { total: number; current_page: number; per_page: number };
|
||||
total?: number;
|
||||
current_page?: number;
|
||||
per_page?: number;
|
||||
}>('/orders', { params: queryParams });
|
||||
|
||||
// API 응답 구조 처리
|
||||
const orders = Array.isArray(response.data) ? response.data : (response.data as unknown as ApiOrder[]);
|
||||
const meta = response.meta || {
|
||||
total: response.total || orders.length,
|
||||
current_page: response.current_page || params?.page || 1,
|
||||
per_page: response.per_page || params?.size || 20,
|
||||
};
|
||||
|
||||
const transformedOrders = orders.map(transformOrder);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: paginatedOrders,
|
||||
total: orders.length,
|
||||
items: transformedOrders,
|
||||
total: meta.total,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 목록 조회에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('발주 목록 조회 오류:', error);
|
||||
return { success: false, error: '발주 목록을 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 통계 조회
|
||||
* GET /api/v1/orders/stats
|
||||
*/
|
||||
export async function getOrderStats(): Promise<{
|
||||
success: boolean;
|
||||
@@ -203,52 +267,44 @@ export async function getOrderStats(): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const orders = getMockOrders();
|
||||
|
||||
const stats: OrderStats = {
|
||||
total: orders.length,
|
||||
waiting: orders.filter((o) => o.status === 'waiting').length,
|
||||
orderComplete: orders.filter((o) => o.status === 'order_complete').length,
|
||||
deliveryScheduled: orders.filter((o) => o.status === 'delivery_scheduled').length,
|
||||
deliveryComplete: orders.filter((o) => o.status === 'delivery_complete').length,
|
||||
};
|
||||
const response = await apiClient.get<ApiOrderStats>('/orders/stats');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: stats,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 통계 조회에 실패했습니다.',
|
||||
data: {
|
||||
total: response.total,
|
||||
waiting: response.draft,
|
||||
orderComplete: response.confirmed,
|
||||
deliveryScheduled: response.in_progress,
|
||||
deliveryComplete: response.completed,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('발주 통계 조회 오류:', error);
|
||||
return { success: false, error: '발주 통계를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 삭제
|
||||
* DELETE /api/v1/orders/{id}
|
||||
*/
|
||||
export async function deleteOrder(id: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업: 실제로는 API 호출
|
||||
if (cachedOrders) {
|
||||
cachedOrders = cachedOrders.filter((o) => o.id !== id);
|
||||
}
|
||||
|
||||
await apiClient.delete(`/orders/${id}`);
|
||||
return { success: true };
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 삭제에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('발주 삭제 오류:', error);
|
||||
return { success: false, error: '발주 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 일괄 삭제
|
||||
* Backend에 batch API가 없으므로 개별 삭제 반복
|
||||
*/
|
||||
export async function deleteOrders(ids: string[]): Promise<{
|
||||
success: boolean;
|
||||
@@ -256,32 +312,28 @@ export async function deleteOrders(ids: string[]): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업: 실제로는 API 호출
|
||||
if (cachedOrders) {
|
||||
const beforeCount = cachedOrders.length;
|
||||
cachedOrders = cachedOrders.filter((o) => !ids.includes(o.id));
|
||||
const deletedCount = beforeCount - cachedOrders.length;
|
||||
let deletedCount = 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount,
|
||||
};
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await apiClient.delete(`/orders/${id}`);
|
||||
deletedCount++;
|
||||
} catch {
|
||||
// 개별 삭제 실패는 무시하고 계속 진행
|
||||
console.warn(`발주 삭제 실패: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: ids.length,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 일괄 삭제에 실패했습니다.',
|
||||
};
|
||||
return { success: true, deletedCount };
|
||||
} catch (error) {
|
||||
console.error('발주 일괄 삭제 오류:', error);
|
||||
return { success: false, error: '발주 일괄 삭제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 상세 조회
|
||||
* GET /api/v1/orders/{id}
|
||||
*/
|
||||
export async function getOrderDetail(id: string): Promise<{
|
||||
success: boolean;
|
||||
@@ -289,30 +341,17 @@ export async function getOrderDetail(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const orders = getMockOrders();
|
||||
const order = orders.find((o) => o.id === id);
|
||||
|
||||
if (!order) {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주를 찾을 수 없습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: order,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 상세 조회에 실패했습니다.',
|
||||
};
|
||||
const response = await apiClient.get<ApiOrder>(`/orders/${id}`);
|
||||
return { success: true, data: transformOrder(response) };
|
||||
} catch (error) {
|
||||
console.error('발주 상세 조회 오류:', error);
|
||||
return { success: false, error: '발주를 찾을 수 없습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 상세 조회 (전체 정보)
|
||||
* GET /api/v1/orders/{id}
|
||||
*/
|
||||
export async function getOrderDetailFull(id: string): Promise<{
|
||||
success: boolean;
|
||||
@@ -320,27 +359,17 @@ export async function getOrderDetailFull(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업: 실제로는 API 호출
|
||||
// 임시로 MOCK_ORDER_DETAIL 반환
|
||||
const mockDetail: OrderDetail = {
|
||||
...MOCK_ORDER_DETAIL,
|
||||
id,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mockDetail,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 상세 조회에 실패했습니다.',
|
||||
};
|
||||
const response = await apiClient.get<ApiOrder>(`/orders/${id}`);
|
||||
return { success: true, data: transformOrderDetail(response) };
|
||||
} catch (error) {
|
||||
console.error('발주 상세 조회 오류:', error);
|
||||
return { success: false, error: '발주 상세 조회에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 수정
|
||||
* PUT /api/v1/orders/{id}
|
||||
*/
|
||||
export async function updateOrder(
|
||||
id: string,
|
||||
@@ -350,20 +379,18 @@ export async function updateOrder(
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업: 실제로는 API 호출
|
||||
console.log('Updating order:', id, data);
|
||||
|
||||
const apiData = transformOrderToApi(data);
|
||||
await apiClient.put(`/orders/${id}`, apiData);
|
||||
return { success: true };
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 수정에 실패했습니다.',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('발주 수정 오류:', error);
|
||||
return { success: false, error: '발주 수정에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 복제
|
||||
* 기존 발주를 조회 후 새로 생성
|
||||
*/
|
||||
export async function duplicateOrder(id: string): Promise<{
|
||||
success: boolean;
|
||||
@@ -371,18 +398,83 @@ export async function duplicateOrder(id: string): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 목업: 실제로는 API 호출
|
||||
const newId = `order-${Date.now()}`;
|
||||
console.log('Duplicating order:', id, '-> new id:', newId);
|
||||
// 1. 기존 발주 조회
|
||||
const existingOrder = await apiClient.get<ApiOrder>(`/orders/${id}`);
|
||||
|
||||
// 2. 새 발주 생성 (order_no는 자동 생성됨)
|
||||
const newOrderData = {
|
||||
client_id: existingOrder.client_id,
|
||||
site_name: existingOrder.site_name,
|
||||
status_code: 'DRAFT', // 복제된 발주는 항상 임시저장
|
||||
order_type_code: existingOrder.order_type_code,
|
||||
delivery_date: existingOrder.delivery_date,
|
||||
memo: existingOrder.memo ? `[복제] ${existingOrder.memo}` : '[복제됨]',
|
||||
items: existingOrder.items?.map((item) => ({
|
||||
item_id: item.item_id,
|
||||
item_name: item.item_name,
|
||||
specification: item.specification,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
unit_price: item.unit_price,
|
||||
})),
|
||||
};
|
||||
|
||||
const response = await apiClient.post<{ id: number }>('/orders', newOrderData);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newId,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: '발주 복제에 실패했습니다.',
|
||||
newId: String(response.id),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('발주 복제 오류:', error);
|
||||
return { success: false, error: '발주 복제에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 생성
|
||||
* POST /api/v1/orders
|
||||
*/
|
||||
export async function createOrder(data: OrderDetailFormData): Promise<{
|
||||
success: boolean;
|
||||
data?: { id: string };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const apiData = transformOrderToApi(data);
|
||||
const response = await apiClient.post<{ id: number }>('/orders', apiData);
|
||||
|
||||
return { success: true, data: { id: String(response.id) } };
|
||||
} catch (error) {
|
||||
console.error('발주 생성 오류:', error);
|
||||
return { success: false, error: '발주 생성에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 상태 변경
|
||||
* PATCH /api/v1/orders/{id}/status
|
||||
*/
|
||||
export async function updateOrderStatus(
|
||||
id: string,
|
||||
status: OrderStatus
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
await apiClient.patch(`/orders/${id}/status`, {
|
||||
status: transformToBackendStatus(status),
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('발주 상태 변경 오류:', error);
|
||||
return { success: false, error: '발주 상태 변경에 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 조회 (재사용)
|
||||
// ========================================
|
||||
|
||||
export { getOrderStatusOptions, getOrderTypeOptions };
|
||||
121
src/lib/api/common-codes.ts
Normal file
121
src/lib/api/common-codes.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
'use server';
|
||||
|
||||
import { apiClient } from './index';
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 타입
|
||||
// ========================================
|
||||
|
||||
export interface CommonCode {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
sort_order: number;
|
||||
attributes: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 조회 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 특정 그룹의 공통 코드 목록 조회
|
||||
* GET /api/v1/settings/common/{group}
|
||||
*/
|
||||
export async function getCommonCodes(group: string): Promise<{
|
||||
success: boolean;
|
||||
data?: CommonCode[];
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await apiClient.get<CommonCode[]>(`/settings/common/${group}`);
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
console.error(`공통코드 조회 오류 (${group}):`, error);
|
||||
return { success: false, error: '공통코드를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 옵션 형태로 변환
|
||||
* Select/ComboBox 등에서 사용
|
||||
*/
|
||||
export async function getCommonCodeOptions(group: string): Promise<{
|
||||
success: boolean;
|
||||
data?: { value: string; label: string }[];
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await getCommonCodes(group);
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
|
||||
const options = result.data.map((code) => ({
|
||||
value: code.code,
|
||||
label: code.name,
|
||||
}));
|
||||
|
||||
return { success: true, data: options };
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 자주 사용하는 코드 그룹 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 수주 상태 코드 조회
|
||||
*/
|
||||
export async function getOrderStatusCodes() {
|
||||
return getCommonCodes('order_status');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 상태 옵션 조회
|
||||
*/
|
||||
export async function getOrderStatusOptions() {
|
||||
return getCommonCodeOptions('order_status');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 유형 코드 조회
|
||||
*/
|
||||
export async function getOrderTypeCodes() {
|
||||
return getCommonCodes('order_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 유형 옵션 조회
|
||||
*/
|
||||
export async function getOrderTypeOptions() {
|
||||
return getCommonCodeOptions('order_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 유형 코드 조회
|
||||
*/
|
||||
export async function getClientTypeCodes() {
|
||||
return getCommonCodes('client_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 유형 옵션 조회
|
||||
*/
|
||||
export async function getClientTypeOptions() {
|
||||
return getCommonCodeOptions('client_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 유형 코드 조회
|
||||
*/
|
||||
export async function getItemTypeCodes() {
|
||||
return getCommonCodes('item_type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 유형 옵션 조회
|
||||
*/
|
||||
export async function getItemTypeOptions() {
|
||||
return getCommonCodeOptions('item_type');
|
||||
}
|
||||
@@ -5,6 +5,21 @@ export { ApiClient, withTokenRefresh } from './client';
|
||||
export { serverFetch } from './fetch-wrapper';
|
||||
export { AUTH_CONFIG } from './auth/auth-config';
|
||||
|
||||
// 공통 코드 유틸리티
|
||||
export {
|
||||
getCommonCodes,
|
||||
getCommonCodeOptions,
|
||||
getOrderStatusCodes,
|
||||
getOrderStatusOptions,
|
||||
getOrderTypeCodes,
|
||||
getOrderTypeOptions,
|
||||
getClientTypeCodes,
|
||||
getClientTypeOptions,
|
||||
getItemTypeCodes,
|
||||
getItemTypeOptions,
|
||||
type CommonCode,
|
||||
} from './common-codes';
|
||||
|
||||
// Server-side API 클라이언트 인스턴스
|
||||
// 서버 액션에서 사용
|
||||
import { ApiClient } from './client';
|
||||
|
||||
Reference in New Issue
Block a user