diff --git a/src/components/business/construction/order-management/actions.ts b/src/components/business/construction/order-management/actions.ts index ebed98ec..d804de66 100644 --- a/src/components/business/construction/order-management/actions.ts +++ b/src/components/business/construction/order-management/actions.ts @@ -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 }; \ No newline at end of file +export async function getOrderStatusOptions() { + try { + const response = await apiClient.get('/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('/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: '수주 유형 코드 조회 실패' }; + } +} \ No newline at end of file diff --git a/src/components/items/ItemListClient.tsx b/src/components/items/ItemListClient.tsx index 342500c6..2adadd59 100644 --- a/src/components/items/ItemListClient.tsx +++ b/src/components/items/ItemListClient.tsx @@ -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 = { + FG: 'purple', PT: 'orange', SM: 'green', RM: 'blue', CS: 'gray', + }; + const codeIconColorMap: Record = { + FG: 'text-purple-600', PT: 'text-orange-600', SM: 'text-green-600', RM: 'text-cyan-600', CS: 'text-gray-600', + }; + + // 코드별 통계 매핑 + const codeCountMap: Record = { + 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 = { diff --git a/src/components/orders/OrderSalesDetailEdit.tsx b/src/components/orders/OrderSalesDetailEdit.tsx index e8cd8c70..222ac13a 100644 --- a/src/components/orders/OrderSalesDetailEdit.tsx +++ b/src/components/orders/OrderSalesDetailEdit.tsx @@ -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>(new Set()); - // 공통코드 옵션 - const [deliveryMethods, setDeliveryMethods] = useState([]); - const [shippingCosts, setShippingCosts] = useState([]); + // 공통코드 옵션 (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로 이동 diff --git a/src/hooks/useCommonCodes.ts b/src/hooks/useCommonCodes.ts new file mode 100644 index 00000000..11521c8b --- /dev/null +++ b/src/hooks/useCommonCodes.ts @@ -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(); +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 { + 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(() => getCached(group) ?? []); + const [isLoading, setIsLoading] = useState(!getCached(group)); + const [error, setError] = useState(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 }; +} \ No newline at end of file diff --git a/src/lib/api/common-codes.ts b/src/lib/api/common-codes.ts index 753fd552..11448565 100644 --- a/src/lib/api/common-codes.ts +++ b/src/lib/api/common-codes.ts @@ -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 | 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(`/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 { - 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; } \ No newline at end of file diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index b86e6b95..308d6941 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -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 클라이언트