feat: 수주/견적 기능 개선 및 PDF 생성 업데이트

- 수주 상세 뷰/수정 컴포넌트 개선
- 견적 위치 패널 업데이트
- PDF 생성 API 수정
- 레이아웃 및 공통코드 API 업데이트
- 패키지 의존성 업데이트
This commit is contained in:
2026-01-29 01:12:58 +09:00
parent d2a39de576
commit 6bcd298995
12 changed files with 272 additions and 81 deletions

View File

@@ -6,6 +6,7 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const nextConfig: NextConfig = {
reactStrictMode: false, // 🧪 TEST: Strict Mode 비활성화로 중복 요청 테스트
turbopack: {}, // ✅ CRITICAL: Next.js 15 + next-intl compatibility
serverExternalPackages: ['puppeteer'], // puppeteer는 Node.js 전용 - Webpack 번들 제외
images: {
remotePatterns: [
{

166
package-lock.json generated
View File

@@ -47,7 +47,7 @@
"lucide-react": "^0.552.0",
"next": "^15.5.9",
"next-intl": "^4.4.0",
"puppeteer": "^24.36.0",
"puppeteer": "^23.11.1",
"react": "^19.2.3",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.3",
@@ -1535,17 +1535,18 @@
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.1.tgz",
"integrity": "sha512-YmhAxs7XPuxN0j7LJloHpfD1ylhDuFmmwMvfy/+6nBSrETT2ycL53LrhgPtR+f+GcPSybQVuQ5inWWu5MrWCpA==",
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz",
"integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"debug": "^4.4.0",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.3",
"tar-fs": "^3.1.1",
"semver": "^7.6.3",
"tar-fs": "^3.0.6",
"unbzip2-stream": "^1.4.3",
"yargs": "^17.7.2"
},
"bin": {
@@ -4944,6 +4945,26 @@
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
@@ -4976,6 +4997,30 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -5115,22 +5160,22 @@
}
},
"node_modules/chromium-bidi": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz",
"integrity": "sha512-c+RLxH0Vg2x2syS9wPw378oJgiJNXtYXUvnVAldUlt5uaHekn0CCU7gPksNgHjrH1qFhmjVXQj4esvuthuC7OQ==",
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz",
"integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
"mitt": "3.0.1",
"zod": "3.23.8"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/chromium-bidi/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
@@ -5623,9 +5668,9 @@
"license": "MIT"
},
"node_modules/devtools-protocol": {
"version": "0.0.1551306",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz",
"integrity": "sha512-CFx8QdSim8iIv+2ZcEOclBKTQY6BI1IEDa7Tm9YkwAXzEWFndTEzpTo5jAUhSnq24IC7xaDw0wvGcm96+Y3PEg==",
"version": "0.0.1367902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
"license": "BSD-3-Clause"
},
"node_modules/doctrine": {
@@ -7001,6 +7046,26 @@
"node": ">= 14"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -8312,6 +8377,17 @@
}
}
},
"node_modules/next-intl/node_modules/@swc/helpers": {
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -9043,17 +9119,18 @@
}
},
"node_modules/puppeteer": {
"version": "24.36.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.36.0.tgz",
"integrity": "sha512-BD/VCyV/Uezvd6o7Fd1DmEJSxTzofAKplzDy6T9d4WbLTQ5A+06zY7VwO91ZlNU22vYE8sidVEsTpTrKc+EEnQ==",
"version": "23.11.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz",
"integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==",
"deprecated": "< 24.15.0 is no longer supported",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.11.1",
"chromium-bidi": "13.0.1",
"@puppeteer/browsers": "2.6.1",
"chromium-bidi": "0.11.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1551306",
"puppeteer-core": "24.36.0",
"devtools-protocol": "0.0.1367902",
"puppeteer-core": "23.11.1",
"typed-query-selector": "^2.12.0"
},
"bin": {
@@ -9064,18 +9141,17 @@
}
},
"node_modules/puppeteer-core": {
"version": "24.36.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.36.0.tgz",
"integrity": "sha512-P3Ou0MAFDCQ0dK1d9F9+8jTrg6JvXjUacgG0YkJQP4kbEnUOGokSDEMmMId5ZhXD5HwsHM202E9VwEpEjWfwxg==",
"version": "23.11.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz",
"integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.11.1",
"chromium-bidi": "13.0.1",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1551306",
"@puppeteer/browsers": "2.6.1",
"chromium-bidi": "0.11.0",
"debug": "^4.4.0",
"devtools-protocol": "0.0.1367902",
"typed-query-selector": "^2.12.0",
"webdriver-bidi-protocol": "0.4.0",
"ws": "^8.19.0"
"ws": "^8.18.0"
},
"engines": {
"node": ">=18"
@@ -10169,6 +10245,12 @@
"utrie": "^1.0.2"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -10403,6 +10485,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"license": "MIT",
"dependencies": {
"buffer": "^5.2.1",
"through": "^2.3.8"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -10571,12 +10663,6 @@
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz",
"integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==",
"license": "Apache-2.0"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -53,7 +53,7 @@
"lucide-react": "^0.552.0",
"next": "^15.5.9",
"next-intl": "^4.4.0",
"puppeteer": "^24.36.0",
"puppeteer": "^23.11.1",
"react": "^19.2.3",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.3",

View File

@@ -522,15 +522,15 @@ function OrderListContent() {
<TableCell>{order.expectedShipDate || "-"}</TableCell>
<TableCell>{order.orderDate || "-"}</TableCell>
<TableCell>{order.client || "-"}</TableCell>
<TableCell>{(order as any).productName || "-"}</TableCell>
<TableCell>{(order as any).receiver || "-"}</TableCell>
<TableCell className="max-w-[150px] truncate">{(order as any).receiverAddress || "-"}</TableCell>
<TableCell>{(order as any).receiverPlace || "-"}</TableCell>
<TableCell>{order.productName || "-"}</TableCell>
<TableCell>{order.receiver || "-"}</TableCell>
<TableCell className="max-w-[150px] truncate">{order.receiverAddress || "-"}</TableCell>
<TableCell>{order.receiverPlace || "-"}</TableCell>
<TableCell>{order.deliveryMethodLabel || "-"}</TableCell>
<TableCell>{(order as any).manager || "-"}</TableCell>
<TableCell className="text-center">{(order as any).frameCount || "-"}</TableCell>
<TableCell>{order.manager || "-"}</TableCell>
<TableCell className="text-center">{order.frameCount || "-"}</TableCell>
<TableCell>{getOrderStatusBadge(order.status)}</TableCell>
<TableCell className="max-w-[100px] truncate">{(order as any).remarks || "-"}</TableCell>
<TableCell className="max-w-[100px] truncate">{order.remarks || "-"}</TableCell>
</TableRow>
);
};

View File

@@ -35,14 +35,16 @@ export async function POST(request: NextRequest) {
);
}
// Puppeteer 브라우저 실행
// Puppeteer 브라우저 실행 (Docker Alpine에서는 시스템 Chromium 사용)
const browser = await puppeteer.launch({
headless: true,
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-software-rasterizer',
],
});

View File

@@ -45,6 +45,7 @@ import {
updateOrder,
type OrderStatus,
} from "@/components/orders";
import { getDeliveryMethodOptions, getCommonCodeOptions } from "@/lib/api/common-codes";
// 수정 폼 데이터
interface EditFormData {
@@ -88,22 +89,11 @@ interface EditFormData {
}>;
}
// 배송방식 옵션
const DELIVERY_METHODS = [
{ value: "direct", label: "직접배차" },
{ value: "pickup", label: "상차" },
{ value: "courier", label: "택배" },
{ value: "self", label: "직접수령" },
{ value: "freight", label: "화물" },
];
// 운임비용 옵션
const SHIPPING_COSTS = [
{ value: "free", label: "무료" },
{ value: "prepaid", label: "선불" },
{ value: "collect", label: "착불" },
{ value: "negotiable", label: "협의" },
];
// 옵션 타입 정의
interface SelectOption {
value: string;
label: string;
}
// 상태 뱃지 헬퍼
@@ -141,6 +131,10 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
const [isSaving, setIsSaving] = useState(false);
const [expandedProducts, setExpandedProducts] = useState<Set<string>>(new Set());
// 공통코드 옵션
const [deliveryMethods, setDeliveryMethods] = useState<SelectOption[]>([]);
const [shippingCosts, setShippingCosts] = useState<SelectOption[]>([]);
// 제품-부품 트리 토글
const toggleProduct = (key: string) => {
setExpandedProducts((prev) => {
@@ -265,6 +259,24 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
loadOrder();
}, [orderId, router]);
// 공통코드 옵션 로드
useEffect(() => {
async function loadCommonCodes() {
const [deliveryResult, shippingResult] = await Promise.all([
getDeliveryMethodOptions(),
getCommonCodeOptions('shipping_cost'),
]);
if (deliveryResult.success && deliveryResult.data) {
setDeliveryMethods(deliveryResult.data);
}
if (shippingResult.success && shippingResult.data) {
setShippingCosts(shippingResult.data);
}
}
loadCommonCodes();
}, []);
const handleCancel = () => {
// V2 패턴: ?mode=view로 이동
router.push(`/sales/order-management-sales/${orderId}?mode=view`);
@@ -453,7 +465,7 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{DELIVERY_METHODS.map((method) => (
{deliveryMethods.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
@@ -476,7 +488,7 @@ export function OrderSalesDetailEdit({ orderId }: OrderSalesDetailEditProps) {
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{SHIPPING_COSTS.map((cost) => (
{shippingCosts.map((cost) => (
<SelectItem key={cost.value} value={cost.value}>
{cost.label}
</SelectItem>

View File

@@ -354,7 +354,7 @@ export function OrderSalesDetailView({ orderId }: OrderSalesDetailViewProps) {
<InfoItem label="출고예정일" value={order.expectedShipDate || "미정"} />
<InfoItem label="납품요청일" value={order.deliveryRequestDate} />
<InfoItem label="배송방식" value={order.deliveryMethodLabel} />
<InfoItem label="운임비용" value={order.shippingCost} />
<InfoItem label="운임비용" value={order.shippingCostLabel} />
<InfoItem label="수신(반장/업체)" value={order.receiver} />
<InfoItem label="수신처 연락처" value={order.receiverContact} />
<InfoItem label="수신처 주소" value={order.address} />

View File

@@ -27,6 +27,7 @@ interface ApiOrder {
delivery_date: string | null;
delivery_method_code: string | null;
delivery_method_label?: string; // API에서 조회한 배송방식 라벨
shipping_cost_label?: string; // API에서 조회한 운임비용 라벨
received_at: string | null;
memo: string | null;
remarks: string | null;
@@ -231,11 +232,17 @@ export interface Order {
remarks?: string;
note?: string;
items?: OrderItem[];
// 목록 페이지용 추가 필드
productName?: string; // 제품명 (첫 번째 품목명)
receiverAddress?: string; // 수신주소
receiverPlace?: string; // 수신처 (전화번호)
frameCount?: number; // 틀수 (수량)
// 상세 페이지용 추가 필드
manager?: string; // 담당자
contact?: string; // 연락처 (client_contact)
deliveryRequestDate?: string; // 납품요청일
shippingCost?: string; // 운임비용
shippingCost?: string; // 운임비용 (코드)
shippingCostLabel?: string; // 운임비용 (라벨)
receiver?: string; // 수신자
receiverContact?: string; // 수신처 연락처
address?: string; // 수신처 주소
@@ -457,12 +464,19 @@ function transformApiToFrontend(apiData: ApiOrder): Order {
memo: apiData.memo ?? undefined,
remarks: apiData.remarks ?? undefined,
note: apiData.note ?? undefined,
items: apiData.items?.map(transformItemApiToFrontend) || [], // 상세 페이지용 추가 필드 (API에서 매핑)
items: apiData.items?.map(transformItemApiToFrontend) || [],
// 목록 페이지용 추가 필드
productName: apiData.items?.[0]?.item_name ?? undefined,
receiverAddress: apiData.options?.shipping_address ?? undefined,
receiverPlace: apiData.options?.receiver_contact ?? undefined,
frameCount: apiData.quantity ?? undefined,
// 상세 페이지용 추가 필드 (API에서 매핑)
manager: apiData.client?.manager_name ?? undefined,
contact: apiData.client_contact ?? apiData.client?.phone ?? undefined,
deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유
// options JSON에서 추출
shippingCost: apiData.options?.shipping_cost_code ?? undefined,
shippingCostLabel: apiData.shipping_cost_label ?? undefined,
receiver: apiData.options?.receiver ?? undefined,
receiverContact: apiData.options?.receiver_contact ?? undefined,
address: apiData.options?.shipping_address ?? undefined,

View File

@@ -402,7 +402,7 @@ export function LocationDetailPanel({
<SelectContent>
{finishedGoods.map((fg) => (
<SelectItem key={fg.item_code} value={fg.item_code}>
{fg.item_code}
{fg.item_code} {fg.item_name}
</SelectItem>
))}
</SelectContent>
@@ -477,8 +477,8 @@ export function LocationDetailPanel({
</div>
</div>
{/* 3행: 제작사이즈, 산출중량, 산출면적, 수량 */}
<div className="grid grid-cols-4 gap-3 text-sm pt-2 border-t border-gray-200">
{/* 3행: 제작사이즈, 산출중량, 산출면적, 수량, 산출하기 */}
<div className="grid grid-cols-5 gap-3 text-sm pt-2 border-t border-gray-200">
<div>
<span className="text-xs text-gray-500"></span>
<p className="font-semibold">
@@ -503,6 +503,24 @@ export function LocationDetailPanel({
: "-"}
</p>
</div>
<div>
<span className="text-xs text-gray-500"> (QTY)</span>
<QuantityInput
value={location.quantity}
onChange={(newQty) => {
if (!location || disabled) return;
// 수량 변경 시 totalPrice 재계산
const unitPrice = location.unitPrice || 0;
onUpdateLocation(location.id, {
quantity: newQty,
totalPrice: unitPrice * newQty,
});
}}
className="h-8 text-sm font-semibold"
min={1}
disabled={disabled}
/>
</div>
<div className="flex items-end">
<Button
onClick={() => onCalculateLocation?.(location.id)}
@@ -600,7 +618,41 @@ export function LocationDetailPanel({
<TableCell className="text-center">
<QuantityInput
value={item.quantity}
onChange={() => {}}
onChange={(newQty) => {
if (!location || disabled) return;
const existingBomResult = location.bomResult;
if (!existingBomResult) return;
// 해당 아이템 찾아서 수량 및 금액 업데이트
const updatedItems = (existingBomResult.items || []).map((bomItem: any, i: number) => {
if (bomItemsByTab[tab.value]?.[index] === bomItem) {
const newTotalPrice = (bomItem.unit_price || 0) * newQty;
return {
...bomItem,
quantity: newQty,
total_price: newTotalPrice,
};
}
return bomItem;
});
// grand_total 재계산
const newGrandTotal = updatedItems.reduce(
(sum: number, item: any) => sum + (item.total_price || 0),
0
);
// location 업데이트 (unitPrice, totalPrice 포함)
onUpdateLocation(location.id, {
unitPrice: newGrandTotal,
totalPrice: newGrandTotal * location.quantity,
bomResult: {
...existingBomResult,
items: updatedItems,
grand_total: newGrandTotal,
},
});
}}
className="w-14 h-7 text-center text-xs"
min={1}
disabled={disabled}

View File

@@ -324,7 +324,7 @@ export function LocationListPanel({
<SelectContent>
{finishedGoods.map((fg) => (
<SelectItem key={fg.item_code} value={fg.item_code}>
{fg.item_code}
{fg.item_code} {fg.item_name}
</SelectItem>
))}
</SelectContent>

View File

@@ -72,6 +72,20 @@ const MOCK_COMPANIES = [
// 알림 폴링 간격 (30초)
const NOTIFICATION_POLLING_INTERVAL = 30000;
// 뱃지 색상 매핑 (TodayIssueSection과 동기화)
const BADGE_COLORS: Record<string, string> = {
'수주등록': 'bg-blue-100 text-blue-700',
'추심이슈': 'bg-purple-100 text-purple-700',
'안전재고': 'bg-orange-100 text-orange-700',
'지출승인': 'bg-green-100 text-green-700',
'세금신고': 'bg-red-100 text-red-700',
'결재요청': 'bg-yellow-100 text-yellow-700',
'신규업체': 'bg-emerald-100 text-emerald-700',
'입금': 'bg-cyan-100 text-cyan-700',
'출금': 'bg-pink-100 text-pink-700',
'기타': 'bg-gray-100 text-gray-700',
};
interface AuthenticatedLayoutProps {
children: React.ReactNode;
}
@@ -774,9 +788,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 배지 */}
<div className="flex-shrink-0">
<span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${
notification.needs_approval
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
BADGE_COLORS[notification.badge] || BADGE_COLORS['기타']
}`}>
{notification.badge}
</span>
@@ -1038,9 +1050,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 배지 */}
<div className="flex-shrink-0">
<span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${
notification.needs_approval
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
BADGE_COLORS[notification.badge] || BADGE_COLORS['기타']
}`}>
{notification.badge}
</span>

View File

@@ -134,6 +134,20 @@ export async function getDeliveryMethodOptions() {
return getCommonCodeOptions('delivery_method');
}
/**
* 운임비용 코드 조회
*/
export async function getShippingCostCodes() {
return getCommonCodes('shipping_cost');
}
/**
* 운임비용 옵션 조회
*/
export async function getShippingCostOptions() {
return getCommonCodeOptions('shipping_cost');
}
/**
* 코드값으로 라벨 조회 (code → name 매핑)
*/