feat(WEB): Vercel 배포 대응 및 타입 안정성 개선
- puppeteer → puppeteer-core + @sparticuz/chromium 전환 (Vercel 서버리스 호환) - PDF 생성 API 로컬/Vercel 환경 분기 처리 - next.config.ts: ignoreBuildErrors false로 전환 - WorkOrder items에 orderNodeId/orderNodeName 필드 추가 - 결재선 데이터에 name/dept 필드 추가 - OrderSalesDetailView 타입 캐스팅 안전하게 수정 - vercel.json 설정 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -210,9 +210,9 @@ const createQmsMockWorkOrder = (subType?: string): WorkOrder => ({
|
||||
shutterCount: 5,
|
||||
department: '생산부',
|
||||
items: [
|
||||
{ id: '1', no: 1, status: 'completed', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '3000×2500', quantity: 2, unit: 'EA' },
|
||||
{ id: '2', no: 2, status: 'in_progress', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '3000×2500', quantity: 3, unit: 'EA' },
|
||||
{ id: '3', no: 3, status: 'waiting', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12000×4500', quantity: 1, unit: 'EA' },
|
||||
{ id: '1', no: 1, status: 'completed', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '3000×2500', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '2', no: 2, status: 'in_progress', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '3000×2500', quantity: 3, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '3', no: 3, status: 'waiting', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12000×4500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
],
|
||||
currentStep: 2,
|
||||
issues: [],
|
||||
@@ -314,7 +314,7 @@ export const InspectionModalV2 = ({
|
||||
// 저장된 측정값을 initialValues로 변환
|
||||
const docData = result.resolveData?.document?.data;
|
||||
if (docData && docData.length > 0) {
|
||||
const values = parseSavedDataToInitialValues(tmpl, docData);
|
||||
const values = parseSavedDataToInitialValues(tmpl, docData.map((d: { field_key: string; field_value?: string | null }) => ({ field_key: d.field_key, field_value: d.field_value ?? null })));
|
||||
setImportInitialValues(values);
|
||||
} else {
|
||||
setImportInitialValues(undefined);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const MOCK_WORK_ORDER: WorkOrder = {
|
||||
projectName: '강남 아파트 단지',
|
||||
assignees: ['김작업', '이생산'],
|
||||
quantity: 5,
|
||||
shutterCount: 3,
|
||||
dueDate: '2024-10-05',
|
||||
priority: 1,
|
||||
status: 'inProgress',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import puppeteer from 'puppeteer';
|
||||
import puppeteer from 'puppeteer-core';
|
||||
import chromium from '@sparticuz/chromium';
|
||||
|
||||
/**
|
||||
* PDF 생성 API
|
||||
@@ -35,17 +36,20 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Puppeteer 브라우저 실행 (Docker Alpine에서는 시스템 Chromium 사용)
|
||||
// 로컬 개발 vs Vercel 환경 분기
|
||||
const isVercel = process.env.VERCEL === '1';
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
||||
args: [
|
||||
args: isVercel ? chromium.args : [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--disable-software-rasterizer',
|
||||
],
|
||||
executablePath: isVercel
|
||||
? await chromium.executablePath()
|
||||
: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable',
|
||||
headless: true,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
@@ -1325,10 +1325,10 @@ export async function getInspectionTemplate(params: {
|
||||
},
|
||||
// 결재선 데이터
|
||||
approvalLines: [
|
||||
{ id: 1, role: '작성', sortOrder: 1 },
|
||||
{ id: 2, role: '검토', sortOrder: 2 },
|
||||
{ id: 3, role: '승인', sortOrder: 3 },
|
||||
{ id: 4, role: '승인', sortOrder: 4 },
|
||||
{ id: 1, name: '', dept: '', role: '작성', sortOrder: 1 },
|
||||
{ id: 2, name: '', dept: '', role: '검토', sortOrder: 2 },
|
||||
{ id: 3, name: '', dept: '', role: '승인', sortOrder: 3 },
|
||||
{ id: 4, name: '', dept: '', role: '승인', sortOrder: 4 },
|
||||
],
|
||||
},
|
||||
inspectionItems: [
|
||||
|
||||
@@ -133,16 +133,16 @@ function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number })
|
||||
)}
|
||||
<MapPin className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-semibold text-sm">{node.name}</span>
|
||||
{options.product_name && (
|
||||
{options.product_name ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({options.product_name as string})
|
||||
({String(options.product_name)})
|
||||
</span>
|
||||
)}
|
||||
{(options.open_width || options.open_height) && (
|
||||
) : null}
|
||||
{(options.open_width || options.open_height) ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{options.open_width as string}x{options.open_height as string}mm
|
||||
{String(options.open_width ?? '')}x{String(options.open_height ?? '')}mm
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<BadgeSm className={statusConfig.className}>
|
||||
|
||||
@@ -231,6 +231,8 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
specification: '8,260 X 8,350 mm',
|
||||
quantity: 500,
|
||||
unit: 'm',
|
||||
orderNodeId: null,
|
||||
orderNodeName: '',
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
@@ -241,6 +243,8 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
specification: '1,200 X 2,400 mm',
|
||||
quantity: 100,
|
||||
unit: 'EA',
|
||||
orderNodeId: null,
|
||||
orderNodeName: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -164,6 +164,8 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
specification: '8,260 X 8,350 mm',
|
||||
quantity: 500,
|
||||
unit: 'm',
|
||||
orderNodeId: null,
|
||||
orderNodeName: '',
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
@@ -174,6 +176,8 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
specification: '1,200 X 2,400 mm',
|
||||
quantity: 100,
|
||||
unit: 'EA',
|
||||
orderNodeId: null,
|
||||
orderNodeName: '',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
|
||||
@@ -96,9 +96,9 @@ export function InspectionReportModal({
|
||||
shutterCount: 12,
|
||||
department: '생산부',
|
||||
items: [
|
||||
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' },
|
||||
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' },
|
||||
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' },
|
||||
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
],
|
||||
currentStep: 2,
|
||||
issues: [],
|
||||
|
||||
@@ -60,9 +60,9 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W
|
||||
shutterCount: 12,
|
||||
department: '생산부',
|
||||
items: [
|
||||
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' },
|
||||
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' },
|
||||
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' },
|
||||
{ id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
{ id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' },
|
||||
],
|
||||
currentStep: 2,
|
||||
issues: [],
|
||||
|
||||
@@ -663,6 +663,7 @@ export default function WorkerScreen() {
|
||||
projectName: '-',
|
||||
assignees: [],
|
||||
quantity: mockItem.quantity,
|
||||
shutterCount: 0,
|
||||
dueDate: '',
|
||||
priority: 5,
|
||||
status: 'waiting',
|
||||
@@ -789,6 +790,7 @@ export default function WorkerScreen() {
|
||||
projectName: '-',
|
||||
assignees: [],
|
||||
quantity: mockItem.quantity,
|
||||
shutterCount: 0,
|
||||
dueDate: '',
|
||||
priority: 5,
|
||||
status: 'waiting',
|
||||
@@ -814,6 +816,7 @@ export default function WorkerScreen() {
|
||||
projectName: '-',
|
||||
assignees: [],
|
||||
quantity: item.quantity,
|
||||
shutterCount: 0,
|
||||
dueDate: '',
|
||||
priority: 5,
|
||||
status: 'waiting',
|
||||
|
||||
@@ -154,9 +154,10 @@ export function LocationDetailPanel({
|
||||
|
||||
Object.entries(subtotals).forEach(([key, value]) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const obj = value as { name?: string };
|
||||
tabs.push({
|
||||
value: key,
|
||||
label: value.name || key,
|
||||
label: obj.name || key,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import type { QuoteFormDataV2 } from './QuoteRegistrationV2';
|
||||
import type { BomCalculationResultItem } from './types';
|
||||
|
||||
// 양식 타입
|
||||
type TemplateType = 'vendor' | 'calculation';
|
||||
@@ -327,7 +328,7 @@ export function QuotePreviewContent({
|
||||
|
||||
{/* BOM 품목 상세 */}
|
||||
{bomItems.length > 0 ? (
|
||||
bomItems.map((item, itemIndex) => (
|
||||
bomItems.map((item: BomCalculationResultItem, itemIndex: number) => (
|
||||
<tr key={`${loc.id}-${itemIndex}`} className="border-b border-gray-200">
|
||||
<td className="border-r border-gray-300 px-2 py-1 text-center text-gray-500">
|
||||
{itemIndex === 0 ? locationSymbol : ''}
|
||||
|
||||
@@ -108,10 +108,11 @@ export function QuoteSummaryPanel({
|
||||
unitPrice: item.unit_price || 0,
|
||||
totalPrice: item.total_price || 0,
|
||||
}));
|
||||
const obj = value as { name?: string; count?: number; subtotal?: number };
|
||||
result.push({
|
||||
label: value.name || key,
|
||||
count: value.count || 0,
|
||||
amount: value.subtotal || 0,
|
||||
label: obj.name || key,
|
||||
count: obj.count || 0,
|
||||
amount: obj.subtotal || 0,
|
||||
items: groupItems,
|
||||
});
|
||||
} else if (typeof value === "number") {
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
BomCalculationResultItem,
|
||||
BomCalculationResult,
|
||||
} from './types';
|
||||
export type { BomCalculationResult, BomCalculationResultItem };
|
||||
import { transformApiToFrontend, transformFrontendToApi } from './types';
|
||||
|
||||
// ===== 페이지네이션 타입 =====
|
||||
|
||||
Reference in New Issue
Block a user