fix(WEB): 절곡 작업일지 레거시 일치 수정

- 셔터박스 테이블 구성요소 기준 정렬 (파트→길이 순서)
- 마구리 무게: 원본 box 크기로 계산 (레거시 일치)
- LOT 접두사 제거: 4개 섹션 모두 "-" 표시
- 가이드레일 하부BASE 치수 표시 수정
This commit is contained in:
2026-02-20 23:52:12 +09:00
parent ceeeeb1ef4
commit 4c4f0678d2
6 changed files with 94 additions and 101 deletions

View File

@@ -55,9 +55,7 @@ export function BottomBarSection({ bendingInfo, mapping }: BottomBarSectionProps
<td className="border border-gray-400 px-1 py-0.5 text-center">{row.material}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.length)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.quantity)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">
{row.lotPrefix}-
</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">-</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmtWeight(row.weight)}</td>
</tr>
))}

View File

@@ -60,9 +60,7 @@ function PartTable({ title, rows, imageUrl, lotNo, baseSize }: {
{row.partName === '하부BASE' ? (baseSize || '-') : fmt(row.length)}
</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.quantity)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">
{row.lotPrefix}-
</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">-</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmtWeight(row.weight)}</td>
</tr>
))}
@@ -79,11 +77,11 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo }: GuideRailSecti
const productCode = bendingInfo.productCode;
const wallRows = wall
? buildWallGuideRailRows(wall.lengthData, wall.baseSize, mapping)
? buildWallGuideRailRows(wall.lengthData, wall.baseDimension || '135*80', mapping)
: [];
const sideRows = side
? buildSideGuideRailRows(side.lengthData, mapping)
? buildSideGuideRailRows(side.lengthData, side.baseDimension || '135*130', mapping)
: [];
if (wallRows.length === 0 && sideRows.length === 0) return null;
@@ -100,7 +98,7 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo }: GuideRailSecti
rows={wallRows}
imageUrl={getBendingImageUrl('guiderail', productCode, 'wall')}
lotNo={lotNo}
baseSize={wall?.baseSize}
baseSize={wall?.baseDimension || wall?.baseSize}
/>
)}
@@ -110,7 +108,7 @@ export function GuideRailSection({ bendingInfo, mapping, lotNo }: GuideRailSecti
rows={sideRows}
imageUrl={getBendingImageUrl('guiderail', productCode, 'side')}
lotNo={lotNo}
baseSize="135*130"
baseSize={side?.baseDimension || '135*130'}
/>
)}
</div>

View File

@@ -70,9 +70,7 @@ function ShutterBoxSubSection({ box, index }: { box: ShutterBoxData; index: numb
<td className="border border-gray-400 px-1 py-0.5 text-center">{row.material}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{row.dimension}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.quantity)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">
{row.lotPrefix}-
</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">-</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmtWeight(row.weight)}</td>
</tr>
))}

View File

@@ -55,7 +55,7 @@ export function SmokeBarrierSection({ bendingInfo }: SmokeBarrierSectionProps) {
<td className="border border-gray-400 px-1 py-0.5 text-center">{row.material}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.length)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmt(row.quantity)}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{row.lotCode}</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">-</td>
<td className="border border-gray-400 px-1 py-0.5 text-center">{fmtWeight(row.weight)}</td>
</tr>
))}

View File

@@ -14,7 +14,8 @@ export interface LengthQuantity {
// 가이드레일 타입별 데이터
export interface GuideRailTypeData {
lengthData: LengthQuantity[];
baseSize: string; // "135*80" 또는 "135*130"
baseSize: string; // BOM 프로파일 사이즈 "130*75" (섹션 제목용)
baseDimension?: string; // 실제 하부BASE 물리 치수 "135*130" (작업일지 표시/무게계산용)
}
// 셔터박스 데이터

View File

@@ -27,9 +27,8 @@ const EGI_DENSITY = 7.85; // g/cm3
// 가이드레일
const WALL_PART_WIDTH = 412; // mm - 벽면형 파트 폭
const SIDE_PART_WIDTH = 462; // mm - 측면형 파트 폭
const WALL_BASE_HEIGHT_MIXED = 80; // 혼합형 벽면 하부BASE 높이
const WALL_BASE_HEIGHT_ONLY = 130; // 면형 단독 하부BASE 높이
const SIDE_BASE_HEIGHT = 130; // 측면형 하부BASE 높이
const WALL_BASE_HEIGHT = 80; // 벽면 하부BASE 높이 (legacy: wall_basesize 135*80)
const SIDE_BASE_HEIGHT = 130; // 면형 하부BASE 높이 (legacy: side_basesize 135*130)
const BASE_WIDTH = 135; // 하부BASE 폭
// 하단마감재
@@ -48,6 +47,21 @@ const GUIDE_RAIL_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300];
const SHUTTER_BOX_LENGTH_BUCKETS = [1219, 2438, 3000, 3500, 4000, 4150];
const SMOKE_W50_LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300];
// ============================================================
// 하부BASE 치수 파싱 헬퍼
// ============================================================
/** baseDimension 문자열("135*130")에서 width/height 파싱. 없으면 기본값 사용 */
function parseBaseDimension(baseDimension: string | undefined, fallbackHeight: number): { width: number; height: number } {
if (baseDimension) {
const parts = baseDimension.split('*').map(Number);
if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) {
return { width: parts[0], height: parts[1] };
}
}
return { width: BASE_WIDTH, height: fallbackHeight };
}
// ============================================================
// 핵심 함수: calWeight (PHP Lines 27-55)
// ============================================================
@@ -165,12 +179,10 @@ export function getSLengthCode(length: number, category: string): string | null
*/
export function buildWallGuideRailRows(
lengthData: LengthQuantity[],
baseSize: string,
baseDimension: string,
mapping: MaterialMapping,
): GuideRailPartRow[] {
const rows: GuideRailPartRow[] = [];
const baseHeight = baseSize === '135*80' ? WALL_BASE_HEIGHT_MIXED : WALL_BASE_HEIGHT_ONLY;
for (const ld of lengthData) {
if (ld.quantity <= 0) continue;
@@ -211,9 +223,11 @@ export function buildWallGuideRailRows(
}
// 하부BASE (길이 데이터와 무관하게 1행)
// baseDimension: "135*130" (KQTS01/KTE01) 또는 "135*80" (기타)
const totalQty = lengthData.reduce((sum, ld) => sum + ld.quantity, 0);
if (totalQty > 0) {
const baseW = calcWeight('EGI 1.55T', BASE_WIDTH, baseHeight);
const { width: bw, height: bh } = parseBaseDimension(baseDimension, WALL_BASE_HEIGHT);
const baseW = calcWeight('EGI 1.55T', bw, bh);
rows.push({
partName: '하부BASE', lotPrefix: 'XX', material: 'EGI 1.55T',
length: 0, quantity: totalQty, weight: Math.round(baseW.weight * totalQty * 100) / 100,
@@ -228,6 +242,7 @@ export function buildWallGuideRailRows(
*/
export function buildSideGuideRailRows(
lengthData: LengthQuantity[],
baseDimension: string,
mapping: MaterialMapping,
): GuideRailPartRow[] {
const rows: GuideRailPartRow[] = [];
@@ -259,9 +274,11 @@ export function buildSideGuideRailRows(
}
// 하부BASE
// baseDimension: "135*130" (측면형은 항상 135*130)
const totalQty = lengthData.reduce((sum, ld) => sum + ld.quantity, 0);
if (totalQty > 0) {
const baseW = calcWeight('EGI 1.55T', BASE_WIDTH, SIDE_BASE_HEIGHT);
const { width: bw, height: bh } = parseBaseDimension(baseDimension, SIDE_BASE_HEIGHT);
const baseW = calcWeight('EGI 1.55T', bw, bh);
rows.push({
partName: '하부BASE', lotPrefix: 'XX', material: 'EGI 1.55T',
length: 0, quantity: totalQty, weight: Math.round(baseW.weight * totalQty * 100) / 100,
@@ -341,93 +358,74 @@ export function buildShutterBoxRows(box: ShutterBoxData): ShutterBoxPartRow[] {
const { width: boxWidth, height: boxHeight } = parseBoxSize(box.size);
const isStandard = box.size === '500*380';
for (const ld of box.lengthData) {
if (ld.quantity <= 0) continue;
// 방향별 파트 정의
let parts: { name: string; prefix: string; dim: number }[];
if (isStandard) {
// 표준 500*380 구성
const parts = [
{ name: '①전면부', prefix: 'CF', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③⑤점검구', prefix: 'CP', dim: boxWidth - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: 170 },
];
for (const p of parts) {
const w = calcWeight(BOX_FINISH_MATERIAL, p.dim, ld.length);
rows.push({
partName: p.name, lotPrefix: p.prefix, material: BOX_FINISH_MATERIAL,
dimension: `${ld.length}`, quantity: ld.quantity,
weight: Math.round(w.weight * ld.quantity * 100) / 100,
});
}
} else if (box.direction === '양면') {
const parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③점검구', prefix: 'XX', dim: boxWidth - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: 170 },
{ name: '⑤점검구', prefix: 'XX', dim: boxHeight - 100 },
];
for (const p of parts) {
const w = calcWeight(BOX_FINISH_MATERIAL, p.dim, ld.length);
rows.push({
partName: p.name, lotPrefix: p.prefix, material: BOX_FINISH_MATERIAL,
dimension: `${ld.length}`, quantity: ld.quantity,
weight: Math.round(w.weight * ld.quantity * 100) / 100,
});
}
} else if (box.direction === '밑면') {
const parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③점검구', prefix: 'XX', dim: boxWidth - 200 },
{ name: '④후면부', prefix: 'CB', dim: boxHeight + 85 * 2 },
];
for (const p of parts) {
const w = calcWeight(BOX_FINISH_MATERIAL, p.dim, ld.length);
rows.push({
partName: p.name, lotPrefix: p.prefix, material: BOX_FINISH_MATERIAL,
dimension: `${ld.length}`, quantity: ld.quantity,
weight: Math.round(w.weight * ld.quantity * 100) / 100,
});
}
} else if (box.direction === '후면') {
const parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth + 85 * 2 },
{ name: '③점검구', prefix: 'XX', dim: boxHeight - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: boxHeight + 85 * 2 },
];
for (const p of parts) {
const w = calcWeight(BOX_FINISH_MATERIAL, p.dim, ld.length);
rows.push({
partName: p.name, lotPrefix: p.prefix, material: BOX_FINISH_MATERIAL,
dimension: `${ld.length}`, quantity: ld.quantity,
weight: Math.round(w.weight * ld.quantity * 100) / 100,
});
}
if (isStandard) {
parts = [
{ name: '①전면부', prefix: 'CF', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③⑤점검구', prefix: 'CP', dim: boxWidth - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: 170 },
];
} else if (box.direction === '양면') {
parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③점검구', prefix: 'XX', dim: boxWidth - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: 170 },
{ name: '⑤점검구', prefix: 'XX', dim: boxHeight - 100 },
];
} else if (box.direction === '밑면') {
parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth - 330 },
{ name: '③점검구', prefix: 'XX', dim: boxWidth - 200 },
{ name: '④후면부', prefix: 'CB', dim: boxHeight + 85 * 2 },
];
} else if (box.direction === '후면') {
parts = [
{ name: '①전면부', prefix: 'XX', dim: boxHeight + 122 },
{ name: '②린텔부', prefix: 'CL', dim: boxWidth + 85 * 2 },
{ name: '③점검구', prefix: 'XX', dim: boxHeight - 200 },
{ name: '④후면코너부', prefix: 'CB', dim: boxHeight + 85 * 2 },
];
} else {
parts = [];
}
// 구성요소 기준 정렬: 파트 → 길이 순서
for (const p of parts) {
for (const ld of box.lengthData) {
if (ld.quantity <= 0) continue;
const w = calcWeight(BOX_FINISH_MATERIAL, p.dim, ld.length);
rows.push({
partName: p.name, lotPrefix: p.prefix, material: BOX_FINISH_MATERIAL,
dimension: `${ld.length}`, quantity: ld.quantity,
weight: Math.round(w.weight * ld.quantity * 100) / 100,
});
}
}
// 상부덮개 (비표준일 때)
if (!isStandard && box.coverQty > 0) {
// 상부덮개
if (box.coverQty > 0) {
const coverWidth = boxWidth - 111;
const w = calcWeight(BOX_FINISH_MATERIAL, coverWidth, BOX_COVER_LENGTH);
rows.push({
partName: '상부덮개', lotPrefix: 'XX', material: BOX_FINISH_MATERIAL,
partName: isStandard ? '상부덮개' : (box.direction === '양면' ? '⑥상부덮개' : '⑤상부덮개'),
lotPrefix: 'XX', material: BOX_FINISH_MATERIAL,
dimension: `1219 * ${coverWidth}`, quantity: box.coverQty,
weight: Math.round(w.weight * box.coverQty * 100) / 100,
});
}
// 마구리 (비표준일 때)
if (!isStandard && box.finCoverQty > 0) {
const finWidth = boxWidth + 5;
const finHeight = boxHeight + 5;
const w = calcWeight(BOX_FINISH_MATERIAL, finWidth, finHeight);
// 마구리 (레거시: 무게는 원본 box 크기로 계산, 표시 치수만 +5)
if (box.finCoverQty > 0) {
const w = calcWeight(BOX_FINISH_MATERIAL, boxWidth, boxHeight);
rows.push({
partName: '측면부(마구리)', lotPrefix: 'XX', material: BOX_FINISH_MATERIAL,
dimension: `${finWidth} * ${finHeight}`, quantity: box.finCoverQty,
partName: isStandard ? '측면부(마구리)' : (box.direction === '양면' ? '⑦측면부(마구리)' : '⑥측면부(마구리)'),
lotPrefix: 'XX', material: BOX_FINISH_MATERIAL,
dimension: `${boxWidth + 5} * ${boxHeight + 5}`, quantity: box.finCoverQty,
weight: Math.round(w.weight * box.finCoverQty * 100) / 100,
});
}
@@ -493,8 +491,6 @@ export function calculateProductionSummary(
// 가이드레일 - 벽면형
if (bendingInfo.guideRail.wall) {
const baseHeight = bendingInfo.guideRail.wall.baseSize === '135*80'
? WALL_BASE_HEIGHT_MIXED : WALL_BASE_HEIGHT_ONLY;
for (const ld of bendingInfo.guideRail.wall.lengthData) {
if (ld.quantity <= 0) continue;
addWeight(mapping.guideRailFinish, WALL_PART_WIDTH, ld.length, ld.quantity);
@@ -504,7 +500,8 @@ export function calculateProductionSummary(
}
}
const totalWallQty = bendingInfo.guideRail.wall.lengthData.reduce((s, l) => s + l.quantity, 0);
addWeight('EGI 1.55T', BASE_WIDTH, baseHeight, totalWallQty);
const wallBase = parseBaseDimension(bendingInfo.guideRail.wall.baseDimension, WALL_BASE_HEIGHT);
addWeight('EGI 1.55T', wallBase.width, wallBase.height, totalWallQty);
}
// 가이드레일 - 측면형
@@ -515,7 +512,8 @@ export function calculateProductionSummary(
addWeight(mapping.bodyMaterial, SIDE_PART_WIDTH, ld.length, ld.quantity * 3);
}
const totalSideQty = bendingInfo.guideRail.side.lengthData.reduce((s, l) => s + l.quantity, 0);
addWeight('EGI 1.55T', BASE_WIDTH, SIDE_BASE_HEIGHT, totalSideQty);
const sideBase = parseBaseDimension(bendingInfo.guideRail.side.baseDimension, SIDE_BASE_HEIGHT);
addWeight('EGI 1.55T', sideBase.width, sideBase.height, totalSideQty);
}
// 하단마감재