Files
sam-docs/plans/archive/bending-worklog-reimplementation-plan.md
권혁성 28b69e5449 docs: archive 37개 + COMPLETED 3개 복원 - 향후 docs/ 정식 문서화 시 참조용
- 완료 문서의 상세 내용은 추후 docs/ 구조화 시 정식 문서에 반영 예정
- HISTORY.md는 요약 인덱스로 유지, 개별 파일은 상세 참조용 보관

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 22:32:20 +09:00

40 KiB
Raw Blame History

절곡 작업일지 완전 재구현 계획

작성일: 2026-02-19 목적: PHP viewBendingWork_slat.php와 동일한 구조로 React BendingWorkLogContent.tsx 완전 재구현 기준 문서: 5130/output/viewBendingWork_slat.php (~1400줄) 상태: 구현 완료 (커밋: 59b9b1b)


📍 현재 진행 상태

항목 내용
마지막 완료 작업 Phase 1~5 전체 구현 완료 + 슬랫 입고 LOT NO 개소별 표시 버그 수정
다음 작업 실 데이터 테스트 (bending_info가 채워진 작업지시로 화면 확인)
진행률 15/15 (100%)
마지막 업데이트 2026-02-19
Git 커밋 59b9b1b feat(WEB): 절곡 작업일지 완전 재구현 + 슬랫 입고 LOT NO 개소별 표시 수정

1. 개요

1.1 배경

현재 React BendingWorkLogContent.tsx빈 껍데기 상태로, 단순 테이블에 item.productName, item.specification, item.quantity만 평면 나열함. PHP 원본(viewBendingWork_slat.php)의 4개 카테고리 구조를 전혀 지원하지 않음.

현재 React 컴포넌트 상태:

  • 헤더 + 결재란 (ConstructionApprovalTable 사용)
  • 신청업체 / 신청내용 테이블
  • 제품 정보 테이블 (빈 칸) 데이터 바인딩 없음
  • 작업내역 (유형명/세부품명/재질/LOT/길이/수량) 단순 flat 리스트
  • 생산량 합계 [kg] SUS/EGI 빈 칸
  • 4개 카테고리 섹션 완전 부재

PHP 원본 구조 (구현 목표):

  • 가이드레일: 벽면형/측면형 분류, 이미지 + 세부품명별 길이/수량/LOT NO/무게 계산
  • 하단마감재: 3000/4000mm 길이별 수량, 별도마감재
  • 셔터박스: 동적 이미지 + 구성요소(전면부/린텔부/점검구/후면코너부/상부덮개/측면부)
  • 연기차단재: W50 레일용, W80 케이스용
  • 생산량 합계: SUS(7.93g/cm3) / EGI(7.85g/cm3) 무게 자동 계산

1.2 데이터 흐름 (전체 파이프라인)

[수주 시스템]
order_nodes.options.bending_info (JSON)
    │
    ▼ WorkOrderService.php (Line 276)
    │ $nodeOptions['bending_info'] ?? null
    │
    ▼
work_order_items.options (JSON)
    │ { floor, code, width, height, bending_info, slat_info, cutting_info, wip_info }
    │
    ▼ API GET /work-orders/{id} → items[].options.bending_info
    │
    ▼ Frontend getWorkOrderById() → WorkOrder.items
    │
    ▼ WorkLogModal.tsx (Line 207-213)
    │ <BendingWorkLogContent data={order} />
    │ ※ materialLots 미전달 (bending은 slat과 다르게 LOT를 별도로 안 받음)
    │
    ▼ BendingWorkLogContent.tsx (재작성 대상)

핵심: bending_infowork_order_items.options JSON 안에 저장되며, 현재 프론트엔드 WorkOrderItem 타입에는 bendingInfo 필드가 없음 (slatInfo처럼 추가 필요).

1.3 현재 bending_info 구조 (SAM에 정의된 것)

// react/src/components/production/WorkerScreen/types.ts (Lines 91-107)
export interface BendingInfo {
  drawingUrl?: string;
  common: BendingCommonInfo;
  detailParts: BendingDetailPart[];
}

export interface BendingCommonInfo {
  kind: string;           // "혼합형 120X70"
  type: string;           // "혼합형" | "벽면형" | "측면형"
  lengthQuantities: { length: number; quantity: number }[];
}

export interface BendingDetailPart {
  partName: string;       // "엘바", "하장바"
  material: string;       // "EGI 1.6T"
  barcyInfo: string;      // "16 I 75"
}

1.4 현재 WorkOrderItem 타입 (types.ts Lines 106-120)

// react/src/components/production/WorkOrders/types.ts
export interface WorkOrderItem {
  id: string;
  no: number;
  status: ItemStatus;
  productName: string;
  floorCode: string;
  specification: string;
  width?: number;
  height?: number;
  quantity: number;
  unit: string;
  orderNodeId: number | null;
  orderNodeName: string;
  slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number };
  // ❌ bendingInfo 없음 → 추가 필요
}

transform 함수 (types.ts Lines 457-474): slatInfoitem.options.slat_info에서 파싱하지만, bending_info는 아직 매핑하지 않음.

1.5 PHP col → SAM 매핑 (완전 테이블)

PHP에서 데이터는 estimateSlatList JSON의 각 아이템에 col{N} 키로 저장됨.

PHP 컬럼 의미 SAM bending_info 필드 상태
col4 제품코드 (KQTS01, KTE01 등) productCode ⚠️ item_code로 별도 존재, bending_info에도 추가
col6 가이드레일 유형 common.type 존재
col7 마감유형 (SUS마감/EGI마감) finishMaterial 추가 필요
col24 유효 길이 (mm) common.lengthQuantities 존재
col32 연기차단재 W50 수량 - 2438mm smokeBarrier.w50[].quantity 추가 필요
col33 연기차단재 W50 수량 - 3000mm 상동
col34 연기차단재 W50 수량 - 3500mm 상동
col35 연기차단재 W50 수량 - 4000mm 상동
col36 연기차단재 W50 수량 - 4300mm 상동
col37 셔터박스 크기 (500*380 등) shutterBox[].size 추가 필요
col37_custom 셔터박스 커스텀 크기 shutterBox[].size (custom일 때)
col37_railwidth 셔터박스 레일 폭 shutterBox[].railWidth
col37_frontbottom 셔터박스 전면 하단 치수 shutterBox[].frontBottom
col37_boxdirection 셔터박스 방향 (양면/밑면/후면) shutterBox[].direction
col39 셔터박스 수량 - 1219mm shutterBox[].lengthData
col40 셔터박스 수량 - 2438mm 상동
col41 셔터박스 수량 - 3000mm 상동
col42 셔터박스 수량 - 3500mm 상동
col43 셔터박스 수량 - 4000mm 상동
col44 셔터박스 수량 - 4150mm 상동
col45 상부덮개 수량 shutterBox[].coverQty
col47 마구리 수량 shutterBox[].finCoverQty
col48 연기차단재 W80 수량 smokeBarrier.w80Qty
col50 하단마감재 3000mm 수량 bottomBar.length3000Qty
col51 하단마감재 4000mm 수량 bottomBar.length4000Qty

1.6 기준 원칙

┌─────────────────────────────────────────────────────────────────┐
│  🎯 핵심 원칙                                                    │
├─────────────────────────────────────────────────────────────────┤
│  - options JSON 확장 (컬럼 추가 금지 - 멀티테넌시 원칙)           │
│  - PHP 원본과 동일한 계산 로직 (calWeight, 길이 버킷팅)           │
│  - 이미지는 정적 파일로 서빙 (셔터박스만 SVG/Canvas 대체)         │
│  - 카테고리별 독립 컴포넌트 (가이드레일/하단마감/셔터박스/연기차단재)│
│  - 현재 WorkOrderItem에 bendingInfo 필드 추가 (slatInfo 패턴)    │
└─────────────────────────────────────────────────────────────────┘

1.7 변경 승인 정책

분류 예시 승인
즉시 가능 React 컴포넌트 추가/수정, 타입 정의 추가, 이미지 복사 불필요
⚠️ 컨펌 필요 bending_info JSON 스키마 변경, API 응답 구조 변경, 계산 로직 변경 필수
🔴 금지 work_order_items 테이블 컬럼 추가, 기존 API 삭제 별도 협의

2. 대상 범위

2.1 Phase 1: 데이터 스키마 확장 (백엔드)

# 작업 항목 상태 비고
1.1 bending_info JSON 스키마 확장 설계 BendingInfoExtended 타입 정의 완료
1.2 WorkOrderService.php - options 매핑 확인/수정 Line 277에서 bending_info 정상 전달 확인
1.3 API 응답에 확장된 bending_info 포함 확인 transform 함수에 bendingInfo 매핑 추가 완료

2.2 Phase 2: 이미지 서빙

# 작업 항목 상태 비고
2.1 5130/img/ → api/public/images/bending/ 복사 guiderail(12) + bottombar(6) + part(1) + box source(3) = 22개
2.2 이미지 URL 빌더 유틸 (프론트) bending/utils.ts getBendingImageUrl()

2.3 Phase 3: 프론트엔드 타입 & 유틸리티

# 작업 항목 상태 비고
3.1 BendingWorkLog 타입 정의 확장 bending/types.ts + WorkOrderItem.bendingInfo 추가
3.2 무게 계산 유틸리티 (calcWeight) bending/utils.ts (calcWeight, getMaterialMapping 등 11개 함수)
3.3 WorkOrderItem transform에 bendingInfo 매핑 추가 item.options.bending_info → bendingInfo

2.4 Phase 4: 프론트엔드 컴포넌트 구현

# 작업 항목 상태 비고
4.1 GuideRailSection 컴포넌트 벽면형/측면형 분류, 이미지+파트테이블
4.2 BottomBarSection 컴포넌트 하단마감재 + 별도마감재
4.3 ShutterBoxSection 컴포넌트 방향별(양면/밑면/후면) 구성요소, source 이미지
4.4 SmokeBarrierSection 컴포넌트 W50 레일용 + W80 케이스용
4.5 ProductionSummarySection 컴포넌트 SUS/EGI/합계 표시
4.6 BendingWorkLogContent 통합 헤더 + 신청업체/내용 + 제품정보 + 4섹션 + 합계 + 비고

2.5 Phase 5: 검증 & 정리

# 작업 항목 상태 비고
5.1 PHP 원본과 출력 비교 검증 TypeScript 타입 체크 통과, 실 데이터 테스트 대기

3. 작업 절차

3.1 단계별 절차

Phase 1: 데이터 스키마 확장 (백엔드)
├── 1.1 bending_info 확장 스키마 설계
│   ├── guideRail: { wall, side } (길이 버킷팅 + 수량 + baseSize)
│   ├── bottomBar: { material, extraFinish, length3000Qty, length4000Qty }
│   ├── shutterBox: [{ size, direction, railWidth, frontBottom, coverQty, finCoverQty, lengthData }]
│   └── smokeBarrier: { w50: [...], w80Qty }
├── 1.2 WorkOrderService.php 매핑 확인 (Line 276)
└── 1.3 API 응답 검증 (curl로 직접 확인)

Phase 2: 이미지 서빙
├── 2.1 정적 이미지 복사 (guiderail 12jpg + bottombar 6jpg + part 1jpg = 19개)
└── 2.2 이미지 URL 헬퍼 유틸

Phase 3: 프론트엔드 타입 & 유틸
├── 3.1 타입 정의 (bending/types.ts 신규 + WorkOrderItem.bendingInfo 추가)
├── 3.2 calcWeight + getMaterialMapping 유틸 (bending/utils.ts)
└── 3.3 transform 함수에 bendingInfo 매핑 추가 (slatInfo 패턴 동일)

Phase 4: 컴포넌트 구현
├── 4.1 GuideRailSection (가장 복잡 - 벽면/측면 분리, 파트 구성, 무게 계산)
├── 4.2 BottomBarSection (3000/4000 수량, 별도마감)
├── 4.3 ShutterBoxSection (방향별 구성요소, SVG 다이어그램)
├── 4.4 SmokeBarrierSection (W50 길이별 + W80 고정)
├── 4.5 ProductionSummarySection (SUS/EGI 누적 합계)
└── 4.6 BendingWorkLogContent 통합 (헤더+신청+4섹션+합계 조립)

Phase 5: 검증
└── 5.1 PHP 원본과 비교 (num=24822)

4. 상세 작업 내용 (PHP 로직 완전 인라인)

4.1 Phase 1: bending_info 확장 스키마

1.1 확장된 bending_info JSON 구조

interface BendingInfoExtended {
  // === 기존 필드 (유지) ===
  drawingUrl?: string;
  common: BendingCommonInfo;      // { kind, type, lengthQuantities }
  detailParts: BendingDetailPart[];  // [{ partName, material, barcyInfo }]

  // === 신규 필드 ===
  productCode: string;        // "KTE01", "KQTS01", "KSE01", "KSS01", "KWE01"
  finishMaterial: string;     // "EGI마감", "SUS마감"

  guideRail: {
    wall: {
      lengthData: { length: number; quantity: number }[];
      baseSize: string;  // "135*80" 또는 "135*130"
    } | null;
    side: {
      lengthData: { length: number; quantity: number }[];
      baseSize: string;  // "135*130"
    } | null;
  };

  bottomBar: {
    material: string;          // "EGI 1.55T" 또는 "SUS 1.5T"
    extraFinish: string;       // "SUS 1.2T" 또는 "없음"
    length3000Qty: number;
    length4000Qty: number;
  };

  shutterBox: {
    size: string;              // "500*380" 등
    direction: string;         // "양면" | "밑면" | "후면"
    railWidth: number;
    frontBottom: number;
    coverQty: number;          // 상부덮개 수량
    finCoverQty: number;       // 마구리 수량
    lengthData: { length: number; quantity: number }[];
  }[];  // 배열 (여러 사이즈 가능)

  smokeBarrier: {
    w50: { length: number; quantity: number }[];  // 레일용 W50
    w80Qty: number;                                // 케이스용 W80 수량
  };
}

1.2 calWeight 함수 (PHP 원본 Lines 27-55 → TypeScript 구현)

// PHP 원본:
// $volume_cm3 = ($thickness * $calWidth * $calHeight) / 1000;
// $weight_kg = ($volume_cm3 * $density) / 1000;
// SUS → $SUS_total += $weight_kg, EGI → $EGI_total += $weight_kg

function calcWeight(
  material: string,   // "SUS 1.2T", "EGI 1.55T", "EGI 0.8T" 등
  width: number,      // mm
  height: number      // mm (= 길이)
): { weight: number; type: 'SUS' | 'EGI' } {
  const thickness = parseFloat(material.match(/\d+(\.\d+)?/)?.[0] || '0');
  const isSUS = material.includes('SUS');
  const density = isSUS ? 7.93 : 7.85;  // g/cm3
  const volume_cm3 = (thickness * width * height) / 1000;
  const weight_kg = (volume_cm3 * density) / 1000;
  return {
    weight: Math.round(weight_kg * 100) / 100,
    type: isSUS ? 'SUS' : 'EGI',
  };
}

1.3 제품코드별 재질 매핑 (PHP Lines 330-366)

function getMaterialMapping(productCode: string, finishMaterial: string) {
  // Group 1: KQTS01
  if (productCode === 'KQTS01') {
    return {
      guideRailFinish: 'SUS 1.2T',     // ①②마감재
      bodyMaterial: 'EGI 1.55T',        // ③본체, ④C형, ⑤D형
      guideRailExtraFinish: '',         // 별도마감 없음
      bottomBarFinish: 'SUS 1.5T',      // 하단마감재
      bottomBarExtraFinish: '없음',     // 별도마감 없음
    };
  }
  // Group 2: KTE01
  if (productCode === 'KTE01') {
    const isSUS = finishMaterial === 'SUS마감';
    return {
      guideRailFinish: 'EGI 1.55T',
      bodyMaterial: 'EGI 1.55T',
      guideRailExtraFinish: isSUS ? 'SUS 1.2T' : '',
      bottomBarFinish: 'EGI 1.55T',
      bottomBarExtraFinish: isSUS ? 'SUS 1.2T' : '없음',
    };
  }
  // 기타 제품코드 (KSE01, KSS01, KWE01 등) - KTE01 + EGI마감과 동일 패턴
  return {
    guideRailFinish: 'EGI 1.55T',
    bodyMaterial: 'EGI 1.55T',
    guideRailExtraFinish: '',
    bottomBarFinish: 'EGI 1.55T',
    bottomBarExtraFinish: '없음',
  };
}

1.4 가이드레일 길이 버킷팅 알고리즘 (PHP Lines 384-413)

// 고정 버킷: [2438, 3000, 3500, 4000, 4300]
// 각 아이템의 col24(유효길이)를 "첫 번째로 수용 가능한 버킷"에 넣음 (first-fit)

const LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300];

function bucketGuideRails(items: Array<{ validLength: number; railType: string }>) {
  const buckets = LENGTH_BUCKETS.map(len => ({
    length: len, wallSum: 0, sideSum: 0,
    wallBaseSize: null as string | null, sideBaseSize: null as string | null,
  }));

  for (const item of items) {
    for (const bucket of buckets) {
      if (item.validLength <= bucket.length) {
        if (item.railType === '혼합형(130*75)(130*125)') {
          bucket.wallSum += 1;
          bucket.sideSum += 1;
          bucket.wallBaseSize = '135*80';
          bucket.sideBaseSize = '135*130';
        } else if (item.railType === '벽면형(130*75)') {
          bucket.wallSum += 2;
          bucket.wallBaseSize = '135*130';
        } else if (item.railType === '측면형(130*125)') {
          bucket.sideSum += 2;
          bucket.sideBaseSize = '135*130';
        }
        break;  // first-fit: 한 버킷에 넣으면 다음 아이템으로
      }
    }
  }
  return buckets.filter(b => b.wallSum > 0 || b.sideSum > 0);
}

1.5 가이드레일 세부품명 + LOT 접두사 + 무게 계산 폭

벽면형 [130*75] 파트 구성:

세부품명 LOT 접두사 재질 무게 계산 폭 (mm)
①②마감재 XX guideRailFinish 412
③본체 RT bodyMaterial 412
④C형 RC bodyMaterial 412
⑤D형 RD bodyMaterial 412
⑥별도마감 (SUS마감 시만) RS guideRailExtraFinish 412
하부BASE XX EGI 1.55T (고정) 135 (높이=80)

무게: calcWeight(재질, 412, 길이) / 하부BASE: calcWeight('EGI 1.55T', 135, 80) baseSize는 135*80 (혼합형) 또는 135*130 (벽면형 단독)

측면형 [130*125] 파트 구성:

세부품명 LOT 접두사 재질 무게 계산 폭 (mm)
①②마감재 SS guideRailFinish 462
③본체 ST bodyMaterial 462
④C형 SC bodyMaterial 462
⑤D형 SD bodyMaterial 462
하부BASE XX EGI 1.55T (고정) 135 (높이=130)

무게: calcWeight(재질, 462, 길이) / 하부BASE: calcWeight('EGI 1.55T', 135, 130)

1.6 하단마감재 세부품명

세부품명 LOT 접두사 재질 무게 계산 폭 (mm) 길이 옵션
①하단마감재 TE(EGI)/TS(SUS) bottomBarFinish 184 3000, 4000
④별도마감재 TE/TS bottomBarExtraFinish 238 3000, 4000

별도마감재는 bottomBarExtraFinish !== '없음'일 때만 표시.

1.7 셔터박스 구성요소 (방향별 - PHP Lines 819-1190)

셔터박스 재질: 항상 EGI 1.55T (= $BoxFinish)

표준 사이즈 (500*380) 구성:

구성요소 LOT 접두사 치수 공식
①전면부 CF boxHeight + 122
②린텔부 CL boxWidth - 330
③⑤점검구 CP boxWidth - 200
④후면코너부 CB 170 (고정)

비표준 사이즈 - 양면 구성:

구성요소 LOT 접두사 치수 공식
①전면부 XX boxHeight + 122
②린텔부 CL boxWidth - 330
③점검구 XX boxWidth - 200
④후면코너부 CB 170 (고정)
⑤점검구 XX boxHeight - 100
⑥상부덮개 XX 1219 * (boxWidth - 111)
⑦측면부(마구리) XX (boxWidthFin+5) * (boxHeightFin+5) 표시

비표준 사이즈 - 밑면 구성:

구성요소 LOT 접두사 치수 공식
①전면부 XX boxHeight + 122
②린텔부 CL boxWidth - 330
③점검구 XX boxWidth - 200
④후면부 CB boxHeight + 85*2
⑤상부덮개 XX 1219 * (boxWidth - 111)
⑥측면부(마구리) XX (boxWidthFin+5) * (boxHeightFin+5) 표시

비표준 사이즈 - 후면 구성:

구성요소 LOT 접두사 치수 공식
①전면부 XX boxHeight + 122
②린텔부 CL boxWidth + 85*2
③점검구 XX boxHeight - 200
④후면코너부 CB boxHeight + 85*2
⑤상부덮개 XX 1219 * (boxWidth - 111)
⑥측면부(마구리) XX (boxWidthFin+5) * (boxHeightFin+5) 표시

공통 사항:

  • 상부덮개 무게: calcWeight('EGI 1.55T', boxWidth - 111, 1219) × coverQty
  • 마구리 무게: calcWeight('EGI 1.55T', boxWidthFin, boxHeightFin) × finCoverQty
  • 셔터박스 길이 버킷: [1219, 2438, 3000, 3500, 4000, 4150]

1.8 연기차단재 (PHP Lines 1195-1321)

파트 재질 무게 계산 폭 (mm) 길이 버킷
레일용 [W50] EGI 0.8T 26 2438, 3000, 3500, 4000, 4300
케이스용 [W80] EGI 0.8T 26 3000 (고정)

LOT 접두사: 모두 GI LOT 코드 생성: GI-{getSLengthCode(length, category)}

1.9 getSLengthCode 함수 (PHP Lines 56-100)

function getSLengthCode(length: number, category: string): string | null {
  if (category === '연기차단재50') {
    return length === 3000 ? '53' : length === 4000 ? '54' : null;
  }
  if (category === '연기차단재80') {
    return length === 3000 ? '83' : length === 4000 ? '84' : null;
  }
  // category === '기타' (일반)
  const map: Record<number, string> = {
    1219: '12', 2438: '24', 3000: '30', 3500: '35',
    4000: '40', 4150: '41', 4200: '42', 4300: '43',
  };
  return map[length] || null;
}

4.2 Phase 2: 이미지 서빙

복사 대상 (총 19개 JPG 파일)

가이드레일 (12개):

5130/img/guiderail/ → api/public/images/bending/guiderail/
├── guiderail_KQTS01_wall_130x75.jpg
├── guiderail_KQTS01_side_130x125.jpg
├── guiderail_KTE01_wall_130x75.jpg
├── guiderail_KTE01_side_130x125.jpg
├── guiderail_KSE01_wall_120x70.jpg
├── guiderail_KSE01_side_120x120.jpg
├── guiderail_KSS01_wall_120x70.jpg
├── guiderail_KSS01_side_120x120.jpg
├── guiderail_KSS02_wall_120x70.jpg
├── guiderail_KSS02_side_120x120.jpg
├── guiderail_KWE01_wall_120x70.jpg
└── guiderail_KWE01_side_120x120.jpg

하단마감재 (6개):

5130/img/bottombar/ → api/public/images/bending/bottombar/
├── bottombar_KQTS01.jpg
├── bottombar_KTE01.jpg
├── bottombar_KSE01.jpg
├── bottombar_KSS01.jpg
├── bottombar_KSS02.jpg
└── bottombar_KWE01.jpg

연기차단재 (1개):

5130/img/part/ → api/public/images/bending/part/
└── smokeban.jpg

셔터박스 이미지: PHP에서 GD 라이브러리로 동적 생성 → React에서는 SVG/Canvas로 대체

  • 소스 이미지: 5130/img/box/source/box_{both|bottom|rear}.jpg
  • 치수 텍스트를 오버레이하는 구조 → SVG 컴포넌트로 재구현

이미지 URL 패턴

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.sam.kr';

function getBendingImageUrl(category: string, productCode: string, type?: string): string {
  switch (category) {
    case 'guiderail': {
      // PHP: guiderail_{prodCode}_{wall|side}_{size}.jpg
      // KQTS01, KTE01 → 130x75 (wall) / 130x125 (side)
      // KSE01, KSS01, KSS02, KWE01 → 120x70 (wall) / 120x120 (side)
      const size = ['KQTS01', 'KTE01'].includes(productCode)
        ? (type === 'wall' ? '130x75' : '130x125')
        : (type === 'wall' ? '120x70' : '120x120');
      return `${API_BASE}/images/bending/guiderail/guiderail_${productCode}_${type}_${size}.jpg`;
    }
    case 'bottombar':
      return `${API_BASE}/images/bending/bottombar/bottombar_${productCode}.jpg`;
    case 'smokebarrier':
      return `${API_BASE}/images/bending/part/smokeban.jpg`;
    default:
      return '';
  }
}

4.3 Phase 3: 프론트엔드 타입 & 유틸리티

파일 구조

react/src/components/production/WorkOrders/documents/
├── BendingWorkLogContent.tsx      ← 기존 파일 (재작성)
├── bending/
│   ├── types.ts                   ← 절곡 작업일지 전용 타입
│   ├── utils.ts                   ← calcWeight, getMaterialMapping, getBendingImageUrl, getSLengthCode
│   ├── GuideRailSection.tsx       ← 가이드레일 섹션
│   ├── BottomBarSection.tsx       ← 하단마감재 섹션
│   ├── ShutterBoxSection.tsx      ← 셔터박스 섹션
│   ├── SmokeBarrierSection.tsx    ← 연기차단재 섹션
│   └── ProductionSummarySection.tsx ← 생산량 합계

WorkOrderItem.bendingInfo 추가 (slatInfo 패턴 참고)

// types.ts에 추가
export interface WorkOrderItem {
  // ... 기존 필드 ...
  slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number };
  bendingInfo?: BendingInfoExtended;  // ← 신규 추가
}

// transform 함수에 추가 (slatInfo 패턴 동일)
bendingInfo: item.options?.bending_info
  ? (item.options.bending_info as BendingInfoExtended)
  : undefined,

4.4 Phase 4: 컴포넌트 구현 상세

4.1 GuideRailSection 레이아웃

┌──────────────────────────────────────────────────────────────────────────────┐
│ 1.1 벽면형 [130*75]                                                          │
│ ┌─────────────────────┐  ┌──────────────────────────────────────────────────┐│
│ │ [guiderail 이미지]    │  │ 세부품명  │ 재질      │ 길이 │ 수량 │ LOT NO │ 무게 ││
│ │                      │  │──────────┼──────────┼──────┼──────┼────────┼──────││
│ │                      │  │ ①②마감재 │ SUS 1.2T │ 4000 │  6  │ ____  │ XX.X ││
│ │ 입고&생산 LOT NO:    │  │ ③본체    │ EGI 1.55T│ 4000 │  6  │ ____  │ XX.X ││
│ │ ___________          │  │ ④C형     │ EGI 1.55T│ 4000 │  6  │ ____  │ XX.X ││
│ └─────────────────────┘  │ ⑤D형     │ EGI 1.55T│ 4000 │  6  │ ____  │ XX.X ││
│                           │ ⑥별도마감 │ SUS 1.2T │ 4000 │  6  │ ____  │ XX.X ││
│                           │ 하부BASE  │ EGI 1.55T│135*80│  N  │ ____  │ XX.X ││
│                           └──────────────────────────────────────────────────┘│
├──────────────────────────────────────────────────────────────────────────────┤
│ 1.2 측면형 [130*125] (동일 구조, 폭=462mm, baseSize=135*130)                  │
└──────────────────────────────────────────────────────────────────────────────┘

각 길이 버킷(2438/3000/3500/4000/4300)별로 수량이 있는 행만 표시. 각 파트의 무게는 calcWeight(재질, 폭, 길이) × 수량으로 계산.

4.2 BottomBarSection 레이아웃

┌──────────────────────────────────────────────────────────────────────────────┐
│ 2. 하단마감재                                                                 │
│ ┌─────────────────────┐  ┌──────────────────────────────────────────────────┐│
│ │ [bottombar 이미지]    │  │ 세부품명     │ 재질      │ 길이 │ 수량 │ LOT  │ 무게 ││
│ │                      │  │─────────────┼──────────┼──────┼──────┼──────┼──────││
│ │                      │  │ ①하단마감재  │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│ └─────────────────────┘  │ ①하단마감재  │ EGI 1.55T│ 4000 │  N  │ ____ │ XX.X ││
│                           │ ④별도마감재  │ SUS 1.2T │ 3000 │  N  │ ____ │ XX.X ││
│                           │ ④별도마감재  │ SUS 1.2T │ 4000 │  N  │ ____ │ XX.X ││
│                           └──────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────────┘

4.3 ShutterBoxSection 레이아웃

┌──────────────────────────────────────────────────────────────────────────────┐
│ 3. 셔터박스 [500*380] 양면                                                    │
│ ┌─────────────────────┐  ┌──────────────────────────────────────────────────┐│
│ │ [SVG 다이어그램]      │  │ 구성요소    │ 재질      │ 길이 │ 수량 │ LOT  │ 무게 ││
│ │  (치수 텍스트 포함)   │  │────────────┼──────────┼──────┼──────┼──────┼──────││
│ │  boxHeight+122       │  │ ①전면부    │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│ │  boxWidth-330        │  │ ②린텔부    │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│ │  boxWidth-200        │  │ ③점검구    │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│ └─────────────────────┘  │ ④후면코너부 │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│                           │ ⑤점검구    │ EGI 1.55T│ 3000 │  N  │ ____ │ XX.X ││
│                           │ ⑥상부덮개  │ EGI 1.55T│ 1219 │  N  │ ____ │ XX.X ││
│                           │ ⑦마구리    │ EGI 1.55T│  -   │  N  │ ____ │ XX.X ││
│                           └──────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────────┘

4.4 SmokeBarrierSection 레이아웃

┌──────────────────────────────────────────────────────────────────────────────┐
│ 4. 연기차단재                                                                 │
│ ┌─────────────────────┐  ┌──────────────────────────────────────────────────┐│
│ │ [smokeban.jpg]       │  │ 파트           │ 재질     │ 길이 │ 수량 │ LOT  │ 무게 ││
│ │                      │  │───────────────┼─────────┼──────┼──────┼──────┼──────││
│ └─────────────────────┘  │ 레일용 [W50]  │EGI 0.8T │ 3000 │  N  │ ____ │ XX.X ││
│                           │ 레일용 [W50]  │EGI 0.8T │ 4000 │  N  │ ____ │ XX.X ││
│                           │ 케이스용 [W80]│EGI 0.8T │ 3000 │  N  │ ____ │ XX.X ││
│                           └──────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────────┘

4.5 ProductionSummarySection 레이아웃

┌──────────────────────────────────────────────────────┐
│ 생산량 합계(KG) │    SUS    │    EGI    │    합계    │
│                 │  XX.XX kg │  XX.XX kg │  XX.XX kg  │
└──────────────────────────────────────────────────────┘

SUS_total과 EGI_total은 4개 섹션의 모든 calcWeight 호출에서 누적.


5. 모든 하드코딩 상수 (PHP 원본 기준)

상수 용도
SUS 밀도 7.93 g/cm3 calWeight
EGI 밀도 7.85 g/cm3 calWeight
벽면형 파트 폭 412 mm 가이드레일 무게 계산
측면형 파트 폭 462 mm 가이드레일 무게 계산
벽면형 하부BASE 135 × 80 mm 가이드레일
측면형 하부BASE 135 × 130 mm 가이드레일
하단마감재 폭 184 mm 하단마감재 무게
별도마감재 폭 238 mm 별도마감재 무게
연기차단재 폭 (W50/W80) 26 mm 연기차단재 무게
상부덮개 길이 1219 mm (고정) 셔터박스
상부덮개 폭 boxWidth - 111 셔터박스
전면부 치수 boxHeight + 122 셔터박스
린텔부 치수 boxWidth - 330 셔터박스
점검구 치수 boxWidth - 200 셔터박스
후면코너부 치수 (표준/양면) 170 셔터박스
가이드레일 길이 버킷 [2438, 3000, 3500, 4000, 4300] 길이 분류
셔터박스 길이 버킷 [1219, 2438, 3000, 3500, 4000, 4150] 길이 분류
하단마감재 길이 [3000, 4000] 길이 분류
연기차단재 W50 길이 버킷 [2438, 3000, 3500, 4000, 4300] 길이 분류
케이스용 W80 길이 3000 (고정) 연기차단재
마구리 표시 크기 보정 +5 mm (양쪽) 셔터박스

6. 컨펌 대기 목록

# 항목 변경 내용 영향 범위 상태
1 bending_info 스키마 확장 guideRail, bottomBar, shutterBox, smokeBarrier 필드 추가 api options JSON ⚠️ 컨펌 필요
2 이미지 파일 복사 5130/img/ → api/public/images/bending/ (19개 JPG) api 서버 ⚠️ 컨펌 필요
3 셔터박스 이미지 처리 SVG 컴포넌트로 클라이언트 렌더링 (PHP GD 대체) react ⚠️ 컨펌 필요

7. 변경 이력

날짜 항목 변경 내용 파일 승인
2026-02-19 - 문서 초안 작성 - -
2026-02-19 - 자기완결성 보완 (PHP 로직 완전 인라인, 이미지 목록, 상수 테이블, 데이터 흐름) - -

8. 참고 문서 & 핵심 파일 경로

수정 대상 파일

파일 역할 작업
react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx 메인 컴포넌트 재작성
react/src/components/production/WorkOrders/types.ts WorkOrderItem 타입 bendingInfo 필드 추가 + transform 함수 수정
react/src/components/production/WorkOrders/documents/bending/ 신규 디렉토리 6개 파일 생성 (types, utils, 4개 섹션 + 합계)

참조 파일 (읽기 전용)

파일 역할
5130/output/viewBendingWork_slat.php PHP 원본 (~1400줄)
react/src/components/production/WorkerScreen/types.ts BendingInfo 인터페이스 (Lines 91-107)
react/src/components/production/WorkerScreen/WorkLogModal.tsx 작업일지 모달 - BendingWorkLogContent 호출 (Lines 207-213)
api/app/Services/WorkOrderService.php options에 bending_info 저장 (Line 276)
react/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx 슬랫 작업일지 참고 (유사 패턴)
react/src/components/production/WorkOrders/documents/index.ts export 파일 (BendingWorkLogContent 등록됨)

이미지 원본 경로

소스 대상 파일 수
5130/img/guiderail/*.jpg api/public/images/bending/guiderail/ 12개
5130/img/bottombar/*.jpg api/public/images/bending/bottombar/ 6개
5130/img/part/smokeban.jpg api/public/images/bending/part/ 1개

참고: api/public/images/bending/ 디렉토리는 아직 존재하지 않음 → 생성 필요.


9. 세션 관리

Serena 메모리 ID

  • bending-worklog-state: 진행 상태
  • bending-worklog-snapshot: 스냅샷
  • bending-worklog-active-symbols: 수정 중 파일

10. 검증 결과

10.1 성공 기준

기준 달성 비고
4개 카테고리 섹션이 PHP와 동일한 레이아웃으로 렌더링
SUS/EGI 무게 계산이 PHP calWeight와 동일한 결과 calcWeight(SUS 1.2T, 412, 4000) 등으로 검증
생산량 합계(KG)가 SUS/EGI 별도 + 합산으로 표시
가이드레일/하단마감재/연기차단재 이미지가 정상 표시
셔터박스 SVG 다이어그램에 치수 텍스트 표시
제품코드/마감유형에 따라 세부품명 동적 변경 KQTS01 vs KTE01+SUS vs KTE01+EGI
가이드레일 길이 버킷팅이 PHP first-fit과 동일
빌드 에러 없음

10.2 검증 방법

  • PHP 원본: 5130/output/viewBendingWork_slat.php?num=24822 출력과 비교
  • 무게 계산 단위 테스트: calcWeight('SUS 1.2T', 412, 4000) → 예상값과 비교
    • thickness=1.2, width=412, height=4000, density=7.93
    • volume_cm3 = (1.2 * 412 * 4000) / 1000 = 1977.6
    • weight_kg = (1977.6 * 7.93) / 1000 = 15.68

11. 자기완결성 점검 결과

# 검증 항목 상태 비고
1 작업 목적이 명확한가? PHP 동일 구조 재구현
2 성공 기준이 정의되어 있는가? 섹션 10.1 (8개 기준)
3 작업 범위가 구체적인가? 5 Phase, 15개 작업 항목
4 의존성이 명시되어 있는가? Phase 순서 = 의존성, 데이터 흐름 섹션 1.2
5 참고 파일 경로가 정확한가? 섹션 8 (수정 대상 + 참조 파일 분리)
6 단계별 절차가 실행 가능한가? PHP 로직 완전 인라인 (섹션 4)
7 검증 방법이 명시되어 있는가? PHP num=24822 비교 + 단위 테스트 예시
8 모호한 표현이 없는가? 모든 상수/공식/조건 구체적으로 명시

새 세션 시뮬레이션 테스트

질문 답변 가능 참조 섹션
Q1. 이 작업의 목적은 무엇인가? 1.1 배경
Q2. 데이터가 어디서 어떻게 오는가? 1.2 데이터 흐름
Q3. 어디서부터 시작해야 하는가? 3.1 단계별 절차
Q4. 어떤 파일을 수정/생성해야 하는가? 8 핵심 파일 경로
Q5. PHP 원본의 계산 로직은? 4.1 (calWeight, 버킷팅, 재질매핑 전부 인라인)
Q6. 이미지 파일은 어디에 있는가? 4.2 (19개 파일 목록 + URL 패턴)
Q7. 모든 하드코딩 상수 값은? 섹션 5 (완전 테이블)
Q8. 작업 완료 확인 방법은? 10.1 성공 기준 + 10.2 검증 방법
Q9. 막혔을 때 참고 문서는? 8 참고 문서

결과: 9/9 통과 → 자기완결성 확보


이 문서는 /sc:plan 스킬로 생성되었습니다.