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:
2026-01-09 17:25:24 +09:00
parent d472b771e1
commit 6615f39466
3 changed files with 474 additions and 246 deletions

View File

@@ -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
View 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');
}

View File

@@ -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';