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 = { const nextConfig: NextConfig = {
reactStrictMode: false, // 🧪 TEST: Strict Mode 비활성화로 중복 요청 테스트 reactStrictMode: false, // 🧪 TEST: Strict Mode 비활성화로 중복 요청 테스트
turbopack: {}, // ✅ CRITICAL: Next.js 15 + next-intl compatibility turbopack: {}, // ✅ CRITICAL: Next.js 15 + next-intl compatibility
serverExternalPackages: ['puppeteer'], // puppeteer는 Node.js 전용 - Webpack 번들 제외
images: { images: {
remotePatterns: [ remotePatterns: [
{ {

166
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -402,7 +402,7 @@ export function LocationDetailPanel({
<SelectContent> <SelectContent>
{finishedGoods.map((fg) => ( {finishedGoods.map((fg) => (
<SelectItem key={fg.item_code} value={fg.item_code}> <SelectItem key={fg.item_code} value={fg.item_code}>
{fg.item_code} {fg.item_code} {fg.item_name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -477,8 +477,8 @@ export function LocationDetailPanel({
</div> </div>
</div> </div>
{/* 3행: 제작사이즈, 산출중량, 산출면적, 수량 */} {/* 3행: 제작사이즈, 산출중량, 산출면적, 수량, 산출하기 */}
<div className="grid grid-cols-4 gap-3 text-sm pt-2 border-t border-gray-200"> <div className="grid grid-cols-5 gap-3 text-sm pt-2 border-t border-gray-200">
<div> <div>
<span className="text-xs text-gray-500"></span> <span className="text-xs text-gray-500"></span>
<p className="font-semibold"> <p className="font-semibold">
@@ -503,6 +503,24 @@ export function LocationDetailPanel({
: "-"} : "-"}
</p> </p>
</div> </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"> <div className="flex items-end">
<Button <Button
onClick={() => onCalculateLocation?.(location.id)} onClick={() => onCalculateLocation?.(location.id)}
@@ -600,7 +618,41 @@ export function LocationDetailPanel({
<TableCell className="text-center"> <TableCell className="text-center">
<QuantityInput <QuantityInput
value={item.quantity} 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" className="w-14 h-7 text-center text-xs"
min={1} min={1}
disabled={disabled} disabled={disabled}

View File

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

View File

@@ -72,6 +72,20 @@ const MOCK_COMPANIES = [
// 알림 폴링 간격 (30초) // 알림 폴링 간격 (30초)
const NOTIFICATION_POLLING_INTERVAL = 30000; 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 { interface AuthenticatedLayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
@@ -774,9 +788,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 배지 */} {/* 배지 */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${ <span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${
notification.needs_approval BADGE_COLORS[notification.badge] || BADGE_COLORS['기타']
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
}`}> }`}>
{notification.badge} {notification.badge}
</span> </span>
@@ -1038,9 +1050,7 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
{/* 배지 */} {/* 배지 */}
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${ <span className={`inline-flex items-center justify-center px-2 py-1 text-xs font-medium rounded-md ${
notification.needs_approval BADGE_COLORS[notification.badge] || BADGE_COLORS['기타']
? 'bg-red-100 text-red-700'
: 'bg-blue-100 text-blue-700'
}`}> }`}>
{notification.badge} {notification.badge}
</span> </span>

View File

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