refactor(WEB): Server Component → Client Component 전면 마이그레이션
- 53개 페이지를 Server Component에서 Client Component로 변환 - Next.js 15에서 Server Component 렌더링 중 쿠키 수정 불가 이슈 해결 - 폐쇄형 ERP 시스템 특성상 SEO 불필요, Client Component 사용이 적합 주요 변경사항: - 모든 페이지에 'use client' 지시어 추가 - use(params) 훅으로 async params 처리 - useState + useEffect로 데이터 페칭 패턴 적용 - skipTokenRefresh 옵션 및 관련 코드 제거 (더 이상 필요 없음) 변환된 페이지: - Settings: 4개 (account-info, notification-settings, permissions, popup-management) - Accounting: 9개 (vendors, sales, deposits, bills, withdrawals, expected-expenses, bad-debt-collection) - Sales: 4개 (quote-management, pricing-management) - Production/Quality/Master-data: 6개 - Material/Outbound: 4개 - Construction: 22개 - Other: 4개 (payment-history, subscription, dev/test-urls) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -475,6 +475,186 @@ export async function finalizePricing(id: string): Promise<{ success: boolean; d
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 품목 목록 + 단가 목록 병합 조회
|
||||
// ============================================
|
||||
|
||||
// 품목 API 응답 타입 (GET /api/v1/items)
|
||||
interface ItemApiData {
|
||||
id: number;
|
||||
item_type: string; // FG, PT, SM, RM, CS (품목 유형)
|
||||
code: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
category_id: number | null;
|
||||
created_at: string;
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
// 단가 목록 조회용 타입
|
||||
interface PriceApiListItem {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
item_type_code: string;
|
||||
item_id: number;
|
||||
client_group_id: number | null;
|
||||
purchase_price: string | null;
|
||||
processing_cost: string | null;
|
||||
loss_rate: string | null;
|
||||
margin_rate: string | null;
|
||||
sales_price: string | null;
|
||||
rounding_rule: 'round' | 'ceil' | 'floor';
|
||||
rounding_unit: number;
|
||||
supplier: string | null;
|
||||
effective_from: string;
|
||||
effective_to: string | null;
|
||||
status: 'draft' | 'active' | 'finalized';
|
||||
is_final: boolean;
|
||||
finalized_at: string | null;
|
||||
finalized_by: number | null;
|
||||
note: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
}
|
||||
|
||||
// 목록 표시용 타입
|
||||
export interface PricingListItem {
|
||||
id: string;
|
||||
itemId: string;
|
||||
itemCode: string;
|
||||
itemName: string;
|
||||
itemType: string;
|
||||
specification?: string;
|
||||
unit: string;
|
||||
purchasePrice?: number;
|
||||
processingCost?: number;
|
||||
salesPrice?: number;
|
||||
marginRate?: number;
|
||||
effectiveDate?: string;
|
||||
status: 'draft' | 'active' | 'finalized' | 'not_registered';
|
||||
currentRevision: number;
|
||||
isFinal: boolean;
|
||||
itemTypeCode: string;
|
||||
}
|
||||
|
||||
// 품목 유형 매핑 (type_code → 프론트엔드 ItemType)
|
||||
function mapItemTypeForList(typeCode?: string): string {
|
||||
switch (typeCode) {
|
||||
case 'FG': return 'FG';
|
||||
case 'PT': return 'PT';
|
||||
case 'SM': return 'SM';
|
||||
case 'RM': return 'RM';
|
||||
case 'CS': return 'CS';
|
||||
default: return 'PT';
|
||||
}
|
||||
}
|
||||
|
||||
// API 상태 → 프론트엔드 상태 매핑
|
||||
function mapStatusForList(apiStatus: string, isFinal: boolean): 'draft' | 'active' | 'finalized' | 'not_registered' {
|
||||
if (isFinal) return 'finalized';
|
||||
switch (apiStatus) {
|
||||
case 'draft': return 'draft';
|
||||
case 'active': return 'active';
|
||||
case 'finalized': return 'finalized';
|
||||
default: return 'draft';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 목록 데이터 조회 (품목 + 단가 병합)
|
||||
*/
|
||||
export async function getPricingListData(): Promise<PricingListItem[]> {
|
||||
try {
|
||||
// 품목 목록 조회
|
||||
const { response: itemsResponse, error: itemsError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items?group_id=1&size=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (itemsError || !itemsResponse) {
|
||||
console.error('[PricingActions] Items fetch error:', itemsError?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const itemsResult = await itemsResponse.json();
|
||||
const items: ItemApiData[] = itemsResult.success ? (itemsResult.data?.data || []) : [];
|
||||
|
||||
// 단가 목록 조회
|
||||
const { response: pricingResponse, error: pricingError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing?size=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
|
||||
if (pricingError || !pricingResponse) {
|
||||
console.error('[PricingActions] Pricing fetch error:', pricingError?.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const pricingResult = await pricingResponse.json();
|
||||
const pricings: PriceApiListItem[] = pricingResult.success ? (pricingResult.data?.data || []) : [];
|
||||
|
||||
// 단가 정보를 빠르게 찾기 위한 Map 생성
|
||||
const pricingMap = new Map<string, PriceApiListItem>();
|
||||
for (const pricing of pricings) {
|
||||
const key = `${pricing.item_type_code}_${pricing.item_id}`;
|
||||
if (!pricingMap.has(key)) {
|
||||
pricingMap.set(key, pricing);
|
||||
}
|
||||
}
|
||||
|
||||
// 품목 목록을 기준으로 병합
|
||||
return items.map((item) => {
|
||||
const key = `${item.item_type}_${item.id}`;
|
||||
const pricing = pricingMap.get(key);
|
||||
|
||||
if (pricing) {
|
||||
return {
|
||||
id: String(pricing.id),
|
||||
itemId: String(item.id),
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
itemType: mapItemTypeForList(item.item_type),
|
||||
specification: undefined,
|
||||
unit: item.unit || 'EA',
|
||||
purchasePrice: pricing.purchase_price ? parseFloat(pricing.purchase_price) : undefined,
|
||||
processingCost: pricing.processing_cost ? parseFloat(pricing.processing_cost) : undefined,
|
||||
salesPrice: pricing.sales_price ? parseFloat(pricing.sales_price) : undefined,
|
||||
marginRate: pricing.margin_rate ? parseFloat(pricing.margin_rate) : undefined,
|
||||
effectiveDate: pricing.effective_from,
|
||||
status: mapStatusForList(pricing.status, pricing.is_final),
|
||||
currentRevision: 0,
|
||||
isFinal: pricing.is_final,
|
||||
itemTypeCode: item.item_type,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: `item_${item.id}`,
|
||||
itemId: String(item.id),
|
||||
itemCode: item.code,
|
||||
itemName: item.name,
|
||||
itemType: mapItemTypeForList(item.item_type),
|
||||
specification: undefined,
|
||||
unit: item.unit || 'EA',
|
||||
purchasePrice: undefined,
|
||||
processingCost: undefined,
|
||||
salesPrice: undefined,
|
||||
marginRate: undefined,
|
||||
effectiveDate: undefined,
|
||||
status: 'not_registered' as const,
|
||||
currentRevision: 0,
|
||||
isFinal: false,
|
||||
itemTypeCode: item.item_type,
|
||||
};
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[PricingActions] getPricingListData error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 이력 조회
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user