refactor(WEB): 공통코드 클라이언트 훅 전환 및 품목 탭 동적 생성

- common-codes.ts에서 'use server' 제거, 타입/유틸리티만 유지
- useCommonCodes 훅 생성 (클라이언트 /api/proxy/ 패턴, 5분 캐시)
- ItemListClient 탭/통계카드를 common_codes 기반 동적 생성으로 전환
- OrderSalesDetailEdit 서버액션 → useCommonCodes 훅 전환
- order-management actions.ts 서버액션 내 공통코드 직접 조회로 변경
This commit is contained in:
2026-01-30 20:13:46 +09:00
parent d0634bb2e7
commit fc8d29e513
6 changed files with 215 additions and 189 deletions

View File

@@ -1,7 +1,9 @@
'use server';
import type { Order, OrderStats, OrderDetail, OrderDetailFormData, OrderStatus, OrderType } from './types';
import { apiClient, getOrderStatusOptions, getOrderTypeOptions } from '@/lib/api';
import { apiClient } from '@/lib/api';
import type { CommonCode } from '@/lib/api/common-codes';
import { toCommonCodeOptions } from '@/lib/api/common-codes';
// ========================================
// 타입 변환 함수
@@ -506,7 +508,25 @@ export async function updateOrderStatus(
}
// ========================================
// 공통 코드 조회 (재사용)
// 공통 코드 조회 (서버 액션용)
// ========================================
export { getOrderStatusOptions, getOrderTypeOptions };
export async function getOrderStatusOptions() {
try {
const response = await apiClient.get<CommonCode[]>('/settings/common/order_status');
const data = Array.isArray(response) ? response : (response as { data?: CommonCode[] }).data ?? [];
return { success: true, data: toCommonCodeOptions(data) };
} catch {
return { success: false, error: '수주 상태 코드 조회 실패' };
}
}
export async function getOrderTypeOptions() {
try {
const response = await apiClient.get<CommonCode[]>('/settings/common/order_type');
const data = Array.isArray(response) ? response : (response as { data?: CommonCode[] }).data ?? [];
return { success: true, data: toCommonCodeOptions(data) };
} catch {
return { success: false, error: '수주 유형 코드 조회 실패' };
}
}

View File

@@ -8,10 +8,11 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import type { ItemMaster } from '@/types/item';
import { ITEM_TYPE_LABELS } from '@/types/item';
import { useCommonCodes } from '@/hooks/useCommonCodes';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
@@ -352,25 +353,50 @@ export default function ItemListClient() {
}
};
// 탭 옵션 (품목 유형별)
const tabs: TabOption[] = [
{ value: 'all', label: '전체', count: totalStats.totalAll, color: 'gray' },
{ value: 'FG', label: '제품', count: totalStats.totalFG, color: 'purple' },
{ value: 'PT', label: '부품', count: totalStats.totalPT, color: 'orange' },
{ value: 'SM', label: '부자재', count: totalStats.totalSM, color: 'green' },
{ value: 'RM', label: '원자재', count: totalStats.totalRM, color: 'blue' },
{ value: 'CS', label: '소모품', count: totalStats.totalCS, color: 'gray' },
];
// 품목 유형 공통코드
const { codes: itemTypeCodes } = useCommonCodes('item_type');
// 통계 카드 (전체 통계)
const stats: StatCard[] = [
{ label: '전체 품목', value: totalStats.totalAll, icon: Package, iconColor: 'text-blue-600' },
{ label: '제품', value: totalStats.totalFG, icon: Package, iconColor: 'text-purple-600' },
{ label: '부품', value: totalStats.totalPT, icon: Package, iconColor: 'text-orange-600' },
{ label: '부자재', value: totalStats.totalSM, icon: Package, iconColor: 'text-green-600' },
{ label: '원자재', value: totalStats.totalRM, icon: Package, iconColor: 'text-cyan-600' },
{ label: '소모품', value: totalStats.totalCS, icon: Package, iconColor: 'text-gray-600' },
];
// 코드별 색상 매핑
const codeColorMap: Record<string, string> = {
FG: 'purple', PT: 'orange', SM: 'green', RM: 'blue', CS: 'gray',
};
const codeIconColorMap: Record<string, string> = {
FG: 'text-purple-600', PT: 'text-orange-600', SM: 'text-green-600', RM: 'text-cyan-600', CS: 'text-gray-600',
};
// 코드별 통계 매핑
const codeCountMap: Record<string, number> = {
FG: totalStats.totalFG, PT: totalStats.totalPT, SM: totalStats.totalSM,
RM: totalStats.totalRM, CS: totalStats.totalCS,
};
// 탭 옵션 (공통코드 기반 동적 생성)
const tabs: TabOption[] = useMemo(() => {
const dynamicTabs: TabOption[] = itemTypeCodes.map((code) => ({
value: code.code,
label: code.name,
count: codeCountMap[code.code] ?? 0,
color: codeColorMap[code.code] ?? 'gray',
}));
return [
{ value: 'all', label: '전체', count: totalStats.totalAll, color: 'gray' },
...dynamicTabs,
];
}, [itemTypeCodes, totalStats]);
// 통계 카드 (공통코드 기반 동적 생성)
const stats: StatCard[] = useMemo(() => {
const dynamicStats: StatCard[] = itemTypeCodes.map((code) => ({
label: code.name,
value: codeCountMap[code.code] ?? 0,
icon: Package,
iconColor: codeIconColorMap[code.code] ?? 'text-gray-600',
}));
return [
{ label: '전체 품목', value: totalStats.totalAll, icon: Package, iconColor: 'text-blue-600' },
...dynamicStats,
];
}, [itemTypeCodes, totalStats]);
// UniversalListPage Config
const config: UniversalListConfig<ItemMaster> = {

View File

@@ -45,7 +45,7 @@ import {
updateOrder,
type OrderStatus,
} from "@/components/orders";
import { getDeliveryMethodOptions, getCommonCodeOptions } from "@/lib/api/common-codes";
import { useCommonCodes } from "@/hooks/useCommonCodes";
// 수정 폼 데이터
interface EditFormData {
@@ -131,9 +131,9 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
const [isSaving, setIsSaving] = useState(false);
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
// 공통코드 옵션
const [deliveryMethods, setDeliveryMethods] = useState<SelectOption[]>([]);
const [shippingCosts, setShippingCosts] = useState<SelectOption[]>([]);
// 공통코드 옵션 (useCommonCodes 훅)
const { options: deliveryMethods } = useCommonCodes('delivery_method');
const { options: shippingCosts } = useCommonCodes('shipping_cost');
// 제품-부품 트리 토글
const toggleProduct = (key: string) => {
@@ -259,23 +259,6 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
loadOrder();
}, [orderId, router]);
// 공통코드 옵션 로드
useEffect(() => {
async function loadCommonCodes() {
const [deliveryResult, shippingResult] = await Promise.all([
getDeliveryMethodOptions(),
getCommonCodeOptions('shipping_cost'),
]);
if (deliveryResult.success && deliveryResult.data) {
setDeliveryMethods(deliveryResult.data);
}
if (shippingResult.success && shippingResult.data) {
setShippingCosts(shippingResult.data);
}
}
loadCommonCodes();
}, []);
const handleCancel = () => {
// V2 패턴: ?mode=view로 이동