- 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>
737 lines
20 KiB
TypeScript
737 lines
20 KiB
TypeScript
/**
|
|
* 단가관리 서버 액션
|
|
*
|
|
* API Endpoints:
|
|
* - GET /api/v1/pricing - 목록 조회
|
|
* - GET /api/v1/pricing/{id} - 상세 조회
|
|
* - POST /api/v1/pricing - 등록
|
|
* - PUT /api/v1/pricing/{id} - 수정
|
|
* - DELETE /api/v1/pricing/{id} - 삭제
|
|
* - POST /api/v1/pricing/{id}/finalize - 확정
|
|
* - GET /api/v1/pricing/{id}/revisions - 이력 조회
|
|
*/
|
|
|
|
'use server';
|
|
|
|
|
|
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
|
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
|
import type { PricingData, ItemInfo } from './types';
|
|
|
|
// API 응답 타입
|
|
interface ApiResponse<T> {
|
|
success: boolean;
|
|
data: T;
|
|
message: string;
|
|
}
|
|
|
|
// 단가 API 응답 데이터 타입
|
|
interface PriceApiData {
|
|
id: number;
|
|
tenant_id: number;
|
|
item_type_code: string; // FG, PT, SM, RM, CS (items.item_type과 동일)
|
|
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;
|
|
client_group?: {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
product?: {
|
|
id: number;
|
|
product_code: string;
|
|
product_name: string;
|
|
specification: string | null;
|
|
unit: string;
|
|
product_type: string;
|
|
};
|
|
material?: {
|
|
id: number;
|
|
item_code: string;
|
|
item_name: string;
|
|
specification: string | null;
|
|
unit: string;
|
|
product_type: string;
|
|
};
|
|
revisions?: Array<{
|
|
id: number;
|
|
revision_number: number;
|
|
changed_at: string;
|
|
changed_by: number;
|
|
change_reason: string | null;
|
|
before_snapshot: Record<string, unknown> | null;
|
|
after_snapshot: Record<string, unknown>;
|
|
changed_by_user?: {
|
|
id: number;
|
|
name: string;
|
|
};
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* API 데이터 → 프론트엔드 타입 변환
|
|
*/
|
|
function transformApiToFrontend(apiData: PriceApiData): PricingData {
|
|
const product = apiData.product;
|
|
const material = apiData.material;
|
|
|
|
const itemCode = product?.product_code || material?.item_code || `ITEM-${apiData.item_id}`;
|
|
const itemName = product?.product_name || material?.item_name || '품목명 없음';
|
|
const specification = product?.specification || material?.specification || undefined;
|
|
const unit = product?.unit || material?.unit || 'EA';
|
|
const itemType = product?.product_type || material?.product_type || 'PT';
|
|
|
|
// 리비전 변환
|
|
const revisions = apiData.revisions?.map((rev) => ({
|
|
revisionNumber: rev.revision_number,
|
|
revisionDate: rev.changed_at,
|
|
revisionBy: rev.changed_by_user?.name || `User ${rev.changed_by}`,
|
|
revisionReason: rev.change_reason || undefined,
|
|
previousData: rev.before_snapshot as unknown as PricingData,
|
|
})) || [];
|
|
|
|
return {
|
|
id: String(apiData.id),
|
|
itemId: String(apiData.item_id),
|
|
itemCode,
|
|
itemName,
|
|
itemType,
|
|
specification,
|
|
unit,
|
|
effectiveDate: apiData.effective_from,
|
|
purchasePrice: apiData.purchase_price ? parseFloat(apiData.purchase_price) : undefined,
|
|
processingCost: apiData.processing_cost ? parseFloat(apiData.processing_cost) : undefined,
|
|
loss: apiData.loss_rate ? parseFloat(apiData.loss_rate) : undefined,
|
|
roundingRule: apiData.rounding_rule || 'round',
|
|
roundingUnit: apiData.rounding_unit || 1,
|
|
marginRate: apiData.margin_rate ? parseFloat(apiData.margin_rate) : undefined,
|
|
salesPrice: apiData.sales_price ? parseFloat(apiData.sales_price) : undefined,
|
|
supplier: apiData.supplier || undefined,
|
|
note: apiData.note || undefined,
|
|
currentRevision: revisions.length,
|
|
isFinal: apiData.is_final,
|
|
revisions,
|
|
finalizedDate: apiData.finalized_at || undefined,
|
|
status: apiData.status,
|
|
createdAt: apiData.created_at,
|
|
createdBy: '관리자',
|
|
updatedAt: apiData.updated_at,
|
|
updatedBy: '관리자',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 프론트엔드 데이터 → API 요청 형식 변환
|
|
* item_type_code는 품목 정보(data.itemType)에서 가져옴 (FG, PT, SM, RM, CS 등)
|
|
*/
|
|
function transformFrontendToApi(data: PricingData): Record<string, unknown> {
|
|
return {
|
|
item_type_code: data.itemType, // 품목에서 가져온 실제 item_type 값 사용
|
|
item_id: parseInt(data.itemId),
|
|
purchase_price: data.purchasePrice || null,
|
|
processing_cost: data.processingCost || null,
|
|
loss_rate: data.loss || null,
|
|
margin_rate: data.marginRate || null,
|
|
sales_price: data.salesPrice || null,
|
|
rounding_rule: data.roundingRule || 'round',
|
|
rounding_unit: data.roundingUnit || 1,
|
|
supplier: data.supplier || null,
|
|
effective_from: data.effectiveDate,
|
|
effective_to: null,
|
|
note: data.note || null,
|
|
status: data.status || 'draft',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 단가 상세 조회
|
|
*/
|
|
export async function getPricingById(id: string): Promise<PricingData | null> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
|
{
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
console.error('[PricingActions] GET pricing error:', error.message);
|
|
return null;
|
|
}
|
|
|
|
if (!response) {
|
|
console.error('[PricingActions] GET pricing: 응답이 없습니다.');
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.error('[PricingActions] GET pricing error:', response.status);
|
|
return null;
|
|
}
|
|
|
|
const result: ApiResponse<PriceApiData> = await response.json();
|
|
console.log('[PricingActions] GET pricing response:', result);
|
|
|
|
if (!result.success || !result.data) {
|
|
return null;
|
|
}
|
|
|
|
return transformApiToFrontend(result.data);
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] getPricingById error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 품목 정보 조회 (통합 품목 API)
|
|
*
|
|
* GET /api/v1/items/{id}
|
|
*/
|
|
export async function getItemInfo(itemId: string): Promise<ItemInfo | null> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items/${itemId}`,
|
|
{
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
console.error('[PricingActions] getItemInfo error:', error.message);
|
|
return null;
|
|
}
|
|
|
|
if (!response) {
|
|
console.error('[PricingActions] getItemInfo: 응답이 없습니다.');
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.error('[PricingActions] Item not found:', itemId);
|
|
return null;
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (!result.success || !result.data) {
|
|
return null;
|
|
}
|
|
|
|
const item = result.data;
|
|
return {
|
|
id: String(item.id),
|
|
itemCode: item.code,
|
|
itemName: item.name,
|
|
itemType: item.item_type || 'PT',
|
|
specification: item.specification || undefined,
|
|
unit: item.unit || 'EA',
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] getItemInfo error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단가 등록
|
|
* item_type_code는 data.itemType에서 자동으로 가져옴
|
|
*/
|
|
export async function createPricing(
|
|
data: PricingData
|
|
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const apiData = transformFrontendToApi(data);
|
|
|
|
console.log('[PricingActions] POST pricing request:', apiData);
|
|
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(apiData),
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '단가 등록에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[PricingActions] POST pricing response:', result);
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '단가 등록에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] createPricing error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단가 수정
|
|
*/
|
|
export async function updatePricing(
|
|
id: string,
|
|
data: PricingData,
|
|
changeReason?: string
|
|
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const apiData = {
|
|
...transformFrontendToApi(data),
|
|
change_reason: changeReason || null,
|
|
} as Record<string, unknown>;
|
|
|
|
console.log('[PricingActions] PUT pricing request:', apiData);
|
|
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify(apiData),
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '단가 수정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[PricingActions] PUT pricing response:', result);
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '단가 수정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] updatePricing error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단가 삭제
|
|
*/
|
|
export async function deletePricing(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
|
|
{
|
|
method: 'DELETE',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '단가 삭제에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[PricingActions] DELETE pricing response:', result);
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '단가 삭제에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] deletePricing error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단가 확정
|
|
*/
|
|
export async function finalizePricing(id: string): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}/finalize`,
|
|
{
|
|
method: 'POST',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '단가 확정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[PricingActions] POST finalize response:', result);
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '단가 확정에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: transformApiToFrontend(result.data),
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] finalizePricing error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// 품목 목록 + 단가 목록 병합 조회
|
|
// ============================================
|
|
|
|
// 품목 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 [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 단가 이력 조회
|
|
*/
|
|
export async function getPricingRevisions(priceId: string): Promise<{
|
|
success: boolean;
|
|
data?: Array<{
|
|
revisionNumber: number;
|
|
revisionDate: string;
|
|
revisionBy: string;
|
|
revisionReason?: string;
|
|
beforeSnapshot: Record<string, unknown> | null;
|
|
afterSnapshot: Record<string, unknown>;
|
|
}>;
|
|
error?: string;
|
|
__authError?: boolean;
|
|
}> {
|
|
try {
|
|
const { response, error } = await serverFetch(
|
|
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${priceId}/revisions`,
|
|
{
|
|
method: 'GET',
|
|
cache: 'no-store',
|
|
}
|
|
);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: error.message,
|
|
__authError: error.code === 'UNAUTHORIZED',
|
|
};
|
|
}
|
|
|
|
if (!response) {
|
|
return {
|
|
success: false,
|
|
error: '이력 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[PricingActions] GET revisions response:', result);
|
|
|
|
if (!response.ok || !result.success) {
|
|
return {
|
|
success: false,
|
|
error: result.message || '이력 조회에 실패했습니다.',
|
|
};
|
|
}
|
|
|
|
const revisions = result.data.data?.map((rev: {
|
|
revision_number: number;
|
|
changed_at: string;
|
|
changed_by: number;
|
|
change_reason: string | null;
|
|
before_snapshot: Record<string, unknown> | null;
|
|
after_snapshot: Record<string, unknown>;
|
|
changed_by_user?: { name: string };
|
|
}) => ({
|
|
revisionNumber: rev.revision_number,
|
|
revisionDate: rev.changed_at,
|
|
revisionBy: rev.changed_by_user?.name || `User ${rev.changed_by}`,
|
|
revisionReason: rev.change_reason || undefined,
|
|
beforeSnapshot: rev.before_snapshot,
|
|
afterSnapshot: rev.after_snapshot,
|
|
})) || [];
|
|
|
|
return {
|
|
success: true,
|
|
data: revisions,
|
|
};
|
|
} catch (error) {
|
|
if (isNextRedirectError(error)) throw error;
|
|
console.error('[PricingActions] getPricingRevisions error:', error);
|
|
return {
|
|
success: false,
|
|
error: '서버 오류가 발생했습니다.',
|
|
};
|
|
}
|
|
} |