refactor(WEB): Server Action 공통화 및 보안 강화
- executeServerAction 공통 유틸 도입으로 actions.ts 대폭 간소화 (50+개 파일) - sanitize 유틸 추가 (XSS 방지) - middleware CSP 헤더 추가 및 Open Redirect 방지 - 프록시 라우트 로깅 개발환경 한정으로 변경 - 프로덕션 불필요 console.log 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,7 @@
|
||||
'use server';
|
||||
|
||||
|
||||
import { isNextRedirectError } from '@/lib/utils/redirect-error';
|
||||
import { serverFetch } from '@/lib/api/fetch-wrapper';
|
||||
import { executeServerAction } from '@/lib/api/execute-server-action';
|
||||
import type { PricingData, ItemInfo } from './types';
|
||||
|
||||
// API 응답 타입
|
||||
@@ -160,319 +159,80 @@ function transformFrontendToApi(data: PricingData): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 상세 조회
|
||||
*/
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
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;
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/pricing/${id}`,
|
||||
transform: (data: PriceApiData) => transformApiToFrontend(data),
|
||||
errorMessage: '단가 조회에 실패했습니다.',
|
||||
});
|
||||
return result.success ? result.data || null : 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;
|
||||
}
|
||||
interface ItemApiItem { id: number; code: string; name: string; item_type: string; specification?: string; unit?: string }
|
||||
const result = await executeServerAction<ItemApiItem>({
|
||||
url: `${API_URL}/api/v1/items/${itemId}`,
|
||||
errorMessage: '품목 조회에 실패했습니다.',
|
||||
});
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 등록
|
||||
* 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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const apiData = transformFrontendToApi(data);
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/pricing`,
|
||||
method: 'POST',
|
||||
body: apiData,
|
||||
transform: (d: PriceApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '단가 등록에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
/**
|
||||
* 단가 수정
|
||||
*/
|
||||
export async function updatePricing(
|
||||
id: string,
|
||||
data: PricingData,
|
||||
changeReason?: string
|
||||
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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const apiData = { ...transformFrontendToApi(data), change_reason: changeReason || null };
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/pricing/${id}`,
|
||||
method: 'PUT',
|
||||
body: apiData,
|
||||
transform: (d: PriceApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '단가 수정에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/pricing/${id}`,
|
||||
method: 'DELETE',
|
||||
errorMessage: '단가 삭제에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, error: result.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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
const result = await executeServerAction({
|
||||
url: `${API_URL}/api/v1/pricing/${id}/finalize`,
|
||||
method: 'POST',
|
||||
transform: (d: PriceApiData) => transformApiToFrontend(d),
|
||||
errorMessage: '단가 확정에 실패했습니다.',
|
||||
});
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
return { success: result.success, data: result.data, error: result.error };
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -565,94 +325,57 @@ function mapStatusForList(apiStatus: string, isFinal: boolean): 'draft' | 'activ
|
||||
* 단가 목록 데이터 조회 (품목 + 단가 병합)
|
||||
*/
|
||||
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' }
|
||||
);
|
||||
interface ItemsPaginatedResponse { data: ItemApiData[]; current_page: number; last_page: number; per_page: number; total: number }
|
||||
interface PricingPaginatedResponse { data: PriceApiListItem[]; current_page: number; last_page: number; per_page: number; total: number }
|
||||
|
||||
if (itemsError || !itemsResponse) {
|
||||
console.error('[PricingActions] Items fetch error:', itemsError?.message);
|
||||
return [];
|
||||
}
|
||||
const [itemsResult, pricingResult] = await Promise.all([
|
||||
executeServerAction<ItemsPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/items?group_id=1&size=100`,
|
||||
errorMessage: '품목 목록 조회에 실패했습니다.',
|
||||
}),
|
||||
executeServerAction<PricingPaginatedResponse>({
|
||||
url: `${API_URL}/api/v1/pricing?size=100`,
|
||||
errorMessage: '단가 목록 조회에 실패했습니다.',
|
||||
}),
|
||||
]);
|
||||
|
||||
const itemsResult = await itemsResponse.json();
|
||||
const items: ItemApiData[] = itemsResult.success ? (itemsResult.data?.data || []) : [];
|
||||
const items: ItemApiData[] = itemsResult.success ? (itemsResult.data?.data || []) : [];
|
||||
const pricings: PriceApiListItem[] = pricingResult.success ? (pricingResult.data?.data || []) : [];
|
||||
|
||||
// 단가 목록 조회
|
||||
const { response: pricingResponse, error: pricingError } = await serverFetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing?size=100`,
|
||||
{ method: 'GET' }
|
||||
);
|
||||
if (items.length === 0) return [];
|
||||
|
||||
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 [];
|
||||
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,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -661,77 +384,35 @@ export async function getPricingListData(): Promise<PricingListItem[]> {
|
||||
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>;
|
||||
revisionNumber: number; revisionDate: string; revisionBy: string; revisionReason?: string;
|
||||
beforeSnapshot: Record<string, unknown> | null; afterSnapshot: Record<string, unknown>;
|
||||
}>;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
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: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
interface RevisionApiData {
|
||||
data?: Array<{
|
||||
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 };
|
||||
}>;
|
||||
}
|
||||
|
||||
const result = await executeServerAction<RevisionApiData>({
|
||||
url: `${API_URL}/api/v1/pricing/${priceId}/revisions`,
|
||||
errorMessage: '이력 조회에 실패했습니다.',
|
||||
});
|
||||
|
||||
if (result.__authError) return { success: false, __authError: true };
|
||||
if (!result.success || !result.data) return { success: false, error: result.error };
|
||||
|
||||
const revisions = (result.data.data || []).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,
|
||||
beforeSnapshot: rev.before_snapshot,
|
||||
afterSnapshot: rev.after_snapshot,
|
||||
}));
|
||||
|
||||
return { success: true, data: revisions };
|
||||
}
|
||||
Reference in New Issue
Block a user