feat(WEB): 자재/영업/품질 모듈 기능 개선 및 문서 컴포넌트 추가

- 입고관리: 상세/목록 UI 개선, actions 로직 강화
- 재고현황: 상세/목록 개선, StockAuditModal 신규 추가
- 영업주문관리: 페이지 구조 개선, OrderSalesDetailEdit 기능 강화
- 주문: OrderRegistration 개선, SalesOrderDocument 신규 추가
- 견적: QuoteTransactionModal 기능 개선
- 품질: InspectionModalV2, ImportInspectionDocument 대폭 개선
- UniversalListPage: 템플릿 기능 확장

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-28 21:15:25 +09:00
parent 79b39a3ef6
commit 1f640622e0
23 changed files with 4683 additions and 1946 deletions

View File

@@ -114,17 +114,33 @@ function transformApiToListItem(data: ItemApiData): StockItem {
const stock = data.stock;
const hasStock = !!stock;
// description 또는 attributes에서 규격 정보 추출
let specification = '';
if (data.description) {
specification = data.description;
} else if (data.attributes && typeof data.attributes === 'object') {
const attrs = data.attributes as Record<string, unknown>;
if (attrs.specification) {
specification = String(attrs.specification);
}
}
return {
id: String(data.id),
stockNumber: hasStock ? String((stock as unknown as Record<string, unknown>).stock_number ?? stock.id ?? data.id) : String(data.id),
itemCode: data.code,
itemName: data.name,
itemType: data.item_type,
specification,
unit: data.unit || 'EA',
calculatedQty: hasStock ? (parseFloat(String((stock as unknown as Record<string, unknown>).calculated_qty ?? stock.stock_qty)) || 0) : 0,
actualQty: hasStock ? (parseFloat(String((stock as unknown as Record<string, unknown>).actual_qty ?? stock.stock_qty)) || 0) : 0,
stockQty: hasStock ? (parseFloat(String(stock.stock_qty)) || 0) : 0,
safetyStock: hasStock ? (parseFloat(String(stock.safety_stock)) || 0) : 0,
lotCount: hasStock ? (stock.lot_count || 0) : 0,
lotDaysElapsed: hasStock ? (stock.days_elapsed || 0) : 0,
status: hasStock ? stock.status : null,
useStatus: data.is_active === false ? 'inactive' : 'active',
location: hasStock ? (stock.location || '-') : '-',
hasStock,
};
@@ -210,9 +226,12 @@ export async function getStocks(params?: {
search?: string;
itemType?: string;
status?: string;
useStatus?: string;
location?: string;
sortBy?: string;
sortDir?: string;
startDate?: string;
endDate?: string;
}): Promise<{
success: boolean;
data: StockItem[];
@@ -232,9 +251,14 @@ export async function getStocks(params?: {
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.useStatus && params.useStatus !== 'all') {
searchParams.set('is_active', params.useStatus === 'active' ? '1' : '0');
}
if (params?.location) searchParams.set('location', params.location);
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`;
@@ -410,3 +434,99 @@ export async function getStockById(id: string): Promise<{
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 재고 단건 수정 =====
export async function updateStock(
id: string,
data: {
actualQty: number;
safetyStock: number;
useStatus: 'active' | 'inactive';
}
): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/${id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
actual_qty: data.actualQty,
safety_stock: data.safetyStock,
is_active: data.useStatus === 'active',
}),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '재고 수정에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] updateStock error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 재고 실사 (일괄 업데이트) =====
export async function updateStockAudit(updates: { id: string; actualQty: number }[]): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/audit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: updates.map((u) => ({
item_id: u.id,
actual_qty: u.actualQty,
})),
}),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 실사 저장에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '재고 실사 저장에 실패했습니다.' };
}
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[StockActions] updateStockAudit error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}