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:
@@ -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: '수주 유형 코드 조회 실패' };
|
||||
}
|
||||
}
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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로 이동
|
||||
|
||||
124
src/hooks/useCommonCodes.ts
Normal file
124
src/hooks/useCommonCodes.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { CommonCode, CommonCodeOption } from '@/lib/api/common-codes';
|
||||
import { toCommonCodeOptions } from '@/lib/api/common-codes';
|
||||
|
||||
// ========================================
|
||||
// 메모리 캐시 (앱 전체 공유)
|
||||
// ========================================
|
||||
|
||||
const cache = new Map<string, { data: CommonCode[]; timestamp: number }>();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5분
|
||||
|
||||
function getCached(group: string): CommonCode[] | null {
|
||||
const entry = cache.get(group);
|
||||
if (!entry) return null;
|
||||
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
||||
cache.delete(group);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
function setCache(group: string, data: CommonCode[]) {
|
||||
cache.set(group, { data, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 fetch (클라이언트 사이드)
|
||||
// ========================================
|
||||
|
||||
async function fetchCommonCodes(group: string): Promise<CommonCode[]> {
|
||||
const response = await fetch(`/api/proxy/settings/common/${group}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`공통코드 조회 실패 (${group}): ${response.status}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
// API 응답: { success: true, data: [...] } 또는 직접 배열
|
||||
return json.data ?? json;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// useCommonCodes 훅
|
||||
// ========================================
|
||||
|
||||
interface UseCommonCodesResult {
|
||||
/** 공통 코드 배열 */
|
||||
codes: CommonCode[];
|
||||
/** Select/ComboBox용 옵션 배열 */
|
||||
options: CommonCodeOption[];
|
||||
/** 로딩 상태 */
|
||||
isLoading: boolean;
|
||||
/** 에러 메시지 */
|
||||
error: string | null;
|
||||
/** 캐시 무시하고 재조회 */
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 조회 훅
|
||||
*
|
||||
* @param group - 코드 그룹명 (예: 'item_type', 'order_status')
|
||||
* @returns codes, options, isLoading, error, refetch
|
||||
*
|
||||
* @example
|
||||
* const { codes, options, isLoading } = useCommonCodes('item_type');
|
||||
* // codes: [{ id, code, name, ... }, ...]
|
||||
* // options: [{ value: 'FG', label: '완제품' }, ...]
|
||||
*/
|
||||
export function useCommonCodes(group: string): UseCommonCodesResult {
|
||||
const [codes, setCodes] = useState<CommonCode[]>(() => getCached(group) ?? []);
|
||||
const [isLoading, setIsLoading] = useState(!getCached(group));
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => { mountedRef.current = false; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 캐시 히트 시 fetch 스킵 (refreshKey=0일 때만)
|
||||
const cached = getCached(group);
|
||||
if (cached && refreshKey === 0) {
|
||||
setCodes(cached);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setIsLoading(true);
|
||||
|
||||
fetchCommonCodes(group)
|
||||
.then((data) => {
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
setCache(group, data);
|
||||
setCodes(data);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
console.error(`공통코드 조회 오류 (${group}):`, err);
|
||||
setError(err.message || '공통코드를 불러오는데 실패했습니다.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled && mountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [group, refreshKey]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
cache.delete(group);
|
||||
setRefreshKey((k) => k + 1);
|
||||
}, [group]);
|
||||
|
||||
const options = toCommonCodeOptions(codes);
|
||||
|
||||
return { codes, options, isLoading, error, refetch };
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
'use server';
|
||||
|
||||
import { apiClient } from './index';
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 타입
|
||||
// 공통 코드 타입 및 유틸리티
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 공통 코드 타입
|
||||
*/
|
||||
export interface CommonCode {
|
||||
id: number;
|
||||
code: string;
|
||||
@@ -15,147 +14,28 @@ export interface CommonCode {
|
||||
attributes: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 공통 코드 조회 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 특정 그룹의 공통 코드 목록 조회
|
||||
* GET /api/v1/settings/common/{group}
|
||||
* 공통 코드 옵션 (Select/ComboBox용)
|
||||
*/
|
||||
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: '공통코드를 불러오는데 실패했습니다.' };
|
||||
}
|
||||
export interface CommonCodeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 옵션 형태로 변환
|
||||
* Select/ComboBox 등에서 사용
|
||||
* CommonCode[] → 옵션 배열로 변환
|
||||
*/
|
||||
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) => ({
|
||||
export function toCommonCodeOptions(codes: CommonCode[]): CommonCodeOption[] {
|
||||
return codes.map((code) => ({
|
||||
value: code.code,
|
||||
label: code.name,
|
||||
}));
|
||||
|
||||
return { success: true, data: options };
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 자주 사용하는 코드 그룹 함수
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 수주 상태 코드 조회
|
||||
*/
|
||||
export async function getOrderStatusCodes() {
|
||||
return getCommonCodes('order_status');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 상태 옵션 조회
|
||||
* CommonCode[]에서 code로 name 조회
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 코드 조회
|
||||
*/
|
||||
export async function getDeliveryMethodCodes() {
|
||||
return getCommonCodes('delivery_method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 배송방식 옵션 조회
|
||||
*/
|
||||
export async function getDeliveryMethodOptions() {
|
||||
return getCommonCodeOptions('delivery_method');
|
||||
}
|
||||
|
||||
/**
|
||||
* 운임비용 코드 조회
|
||||
*/
|
||||
export async function getShippingCostCodes() {
|
||||
return getCommonCodes('shipping_cost');
|
||||
}
|
||||
|
||||
/**
|
||||
* 운임비용 옵션 조회
|
||||
*/
|
||||
export async function getShippingCostOptions() {
|
||||
return getCommonCodeOptions('shipping_cost');
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드값으로 라벨 조회 (code → name 매핑)
|
||||
*/
|
||||
export async function getCodeLabel(group: string, code: string): Promise<string> {
|
||||
const result = await getCommonCodes(group);
|
||||
if (result.success && result.data) {
|
||||
const found = result.data.find((item) => item.code === code);
|
||||
return found?.name || code;
|
||||
}
|
||||
return code;
|
||||
export function getCodeLabel(codes: CommonCode[], code: string): string {
|
||||
const found = codes.find((item) => item.code === code);
|
||||
return found?.name || code;
|
||||
}
|
||||
@@ -5,19 +5,12 @@ 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,
|
||||
toCommonCodeOptions,
|
||||
getCodeLabel,
|
||||
type CommonCode,
|
||||
type CommonCodeOption,
|
||||
} from './common-codes';
|
||||
|
||||
// Server-side API 클라이언트
|
||||
|
||||
Reference in New Issue
Block a user