# 비즈니스 로직 분석

> **분석 대상:** 5130 레거시 견적 시스템 비즈니스 로직
> **분석 일자:** 2025-12-19

---

## 비즈니스 프로세스 개요

### 견적 생성 플로우
```
┌─────────────────────────────────────────────────────────────┐
│  1. 견적 시작                                                │
│     └─ 신규 / 복사 / 수주연계                                │
├─────────────────────────────────────────────────────────────┤
│  2. 기본 정보 입력                                           │
│     └─ 현장명, 발주처, 담당자                                │
├─────────────────────────────────────────────────────────────┤
│  3. 제품 선택                                                │
│     ├─ 대분류: 스크린 / 철재                                 │
│     ├─ 모델: KSS01, KFS01 등                                 │
│     └─ 규격: 폭, 높이, 마구리윙                              │
├─────────────────────────────────────────────────────────────┤
│  4. 옵션 선택                                                │
│     └─ 절곡, 모터, 보증, 슬랫, 부자재                        │
├─────────────────────────────────────────────────────────────┤
│  5. 상세 항목 입력                                           │
│     ├─ 각 행별 위치, 폭, 높이, 수량 입력                     │
│     └─ 자동 계산 트리거                                      │
├─────────────────────────────────────────────────────────────┤
│  6. 금액 계산 (자동)                                         │
│     ├─ AJAX → get_screen_amount / get_slat_amount            │
│     ├─ 18개 항목별 단가 조회 및 계산                          │
│     └─ 합계 산출                                             │
├─────────────────────────────────────────────────────────────┤
│  7. 할인 적용                                                │
│     └─ 할인율 / 할인액 입력 → 최종금액 계산                  │
├─────────────────────────────────────────────────────────────┤
│  8. 저장                                                     │
│     ├─ 견적번호 생성 (KD-PR-YYMMDD-NN)                       │
│     └─ DB 저장 (estimate 테이블)                             │
└─────────────────────────────────────────────────────────────┘
```

---

## 1. 견적 유형별 처리

### 스크린 견적 (Screen)
| 특성 | 값 |
|------|-----|
| 대분류 | 스크린 |
| 주자재 소재 | 실리카, 와이어 |
| 면적 계산 | (높이 + 550) × 폭 / 1,000,000 m² |
| 기본 제작폭 | 160mm |
| 전용 항목 | L바, 보강평철, 무게평철, 환봉 |

### 슬랫 견적 (Slat/철재)
| 특성 | 값 |
|------|-----|
| 대분류 | 철재 |
| 주자재 소재 | 방화슬랫 |
| 면적 계산 | (높이 + 50) × 폭 / 1,000,000 m² |
| 기본 제작폭 | 110mm |
| 전용 항목 | 조인트바 |

---

## 2. 옵션 체크박스 로직

### 옵션별 영향 항목

| 옵션 | 변수 | 영향받는 항목 |
|------|------|---------------|
| 절곡 | `steel` | 케이스, 케이스용 연기차단재, 마구리, 가이드레일, 레일용 연기차단재, 하장바, L바(스크린), 보강평철(스크린) |
| 모터 | `motor` | 모터 가격 포함/미포함 |
| 보증 | `warranty` | 보증기간 표시 (인정) |
| 슬랫 | `slatcheck` | 주자재(슬랫), 조인트바 |
| 부자재 | `partscheck` | 감기샤프트, 각파이프, 앵글 |

### 조건부 계산 로직
```php
// 절곡 옵션 체크 시
if ($steel == '1') {
    // 케이스, 연기차단재, 레일, 하장바 등 계산
    $caseAmount = calculateCase($width, $caseType, $itemList);
    $smokebanAmount = calculateSmokeban($width, $itemList);
    // ...
} else {
    // 해당 항목 0원 처리
    $caseAmount = 0;
    $smokebanAmount = 0;
}

// 모터 옵션 체크 시
if ($motor == '1') {
    $motorAmount = getMotorPrice($motorCapacity);
} else {
    $motorAmount = 0;
}
```

---

## 3. 단가 조회 로직

### 조회 우선순위
1. **BDmodels 테이블**: 모델별 품목 단가
2. **price_* 테이블**: 품목별 세부 단가
3. **기본값**: 조회 실패 시 기본 단가 적용

### 단가 조회 함수
```php
// fetch_unitprice.php

// 1. 모터 단가 조회
function getMotorPrice($capacity) {
    // price_motor 테이블에서 용량별 단가 조회
    $sql = "SELECT unit_price FROM price_motor WHERE capacity = ?";
    // ...
}

// 2. 샤프트 단가 조회
function getShaftPrice($length) {
    // price_shaft 테이블에서 길이별 단가 조회
    $sql = "SELECT unit_price FROM price_shaft WHERE ? BETWEEN min_length AND max_length";
    // ...
}

// 3. BDmodels에서 품목 단가 조회
function getBDModelPrice($modelname, $itemname, $size) {
    $sql = "SELECT itemList FROM BDmodels WHERE modelname = ? AND itemname = ?";
    // JSON 파싱 후 사이즈에 맞는 가격 반환
}
```

---

## 4. 금액 계산 플로우

### 스크린 금액 계산 (get_screen_amount.php)

```
┌─────────────────────────────────────────────────────────────┐
│  입력값                                                      │
│  - 폭(col2), 높이(col3), 수량(col4), 소재(col5)              │
│  - 케이스타입(col6), 레일타입(col7), 설치방식(col8)          │
│  - 체크박스옵션 (steel, motor, warranty, slatcheck, partscheck)│
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  1. 면적 계산                                                │
│     area = (height + 550) × width / 1,000,000                │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  2. 중량 계산 (모터 용량 결정용)                             │
│     weight = area × 소재별_단위중량                          │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  3. 모터 용량 결정                                           │
│     motorCapacity = searchBracketSize(weight, inch)          │
│     → 150K / 300K / 500K / 800K / 1000K                      │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  4. 18개 항목별 금액 계산                                    │
│     ┌─────────────────────────────────────────────────────┐ │
│     │ 1. 검사비: inspectionFee (고정)                     │ │
│     │ 2. 주자재: area × 소재단가                          │ │
│     │ 3. 모터: getMotorPrice(capacity) × motor체크        │ │
│     │ 4. 제어기: getControllerPrice(type) × motor체크     │ │
│     │ 5. 케이스: (width+제작폭) × m당단가 × steel체크     │ │
│     │ 6. 케이스연기차단재: width × m당단가 × steel체크    │ │
│     │ 7. 마구리: 2개 × 개당단가 × steel체크               │ │
│     │ 8. 앵글: 규격별단가 × steel체크                     │ │
│     │ 9. 가이드레일: (height+레일여유) × m당단가 × steel  │ │
│     │ 10. 레일연기차단재: height × m당단가 × steel        │ │
│     │ 11. 하장바: width × m당단가 × steel체크             │ │
│     │ 12. L바: width × m당단가 × steel체크                │ │
│     │ 13. 보강평철: width × m당단가 × steel체크           │ │
│     │ 14. 샤프트: getShaftPrice(width) × partscheck       │ │
│     │ 15. 무게평철: weight계산 × 단가                     │ │
│     │ 16. 환봉: 길이계산 × m당단가                        │ │
│     │ 17. 각파이프: 길이계산 × m당단가 × partscheck       │ │
│     │ 18. 앵글: 길이계산 × m당단가 × partscheck           │ │
│     └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  5. 행 합계                                                  │
│     rowTotal = Σ(항목별금액) × 수량                          │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│  6. 전체 합계                                                │
│     estimateTotal = Σ(모든행 rowTotal)                       │
│     EstimateFinalSum = estimateTotal - 할인액                │
└─────────────────────────────────────────────────────────────┘
```

---

## 5. 모터 용량 결정 로직

### 중량 + 인치 기반 판단

```php
function searchBracketSize($motorWeight, $bracketInch = null) {
    $weight = floatval($motorWeight);
    $inch = intval($bracketInch);

    // 인치별 중량 기준
    if ($inch > 0) {
        // 4인치 기준
        if ($inch == 4 && $weight <= 300) → 300K
        if ($inch == 4 && $weight <= 400) → 400K

        // 5인치 기준
        if ($inch == 5 && $weight <= 246) → 300K
        if ($inch == 5 && $weight <= 327) → 400K
        if ($inch == 5 && $weight <= 500) → 500K
        if ($inch == 5 && $weight <= 600) → 600K

        // 6인치 기준
        if ($inch == 6 && $weight <= 208) → 300K
        if ($inch == 6 && $weight <= 277) → 400K
        if ($inch == 6 && $weight <= 424) → 500K
        if ($inch == 6 && $weight <= 508) → 600K
        if ($inch == 6 && $weight <= 800) → 800K
        if ($inch == 6 && $weight <= 1000) → 1000K

        // 8인치 기준
        if ($inch == 8 && $weight <= 324) → 500K
        if ($inch == 8 && $weight <= 388) → 600K
        if ($inch == 8 && $weight <= 611) → 800K
        if ($inch == 8 && $weight <= 1000) → 1000K
    } else {
        // 인치 없이 중량만으로 판단
        if ($weight <= 300) → 300K
        if ($weight <= 400) → 400K
        if ($weight <= 500) → 500K
        if ($weight <= 600) → 600K
        if ($weight <= 800) → 800K
        if ($weight <= 1000) → 1000K
    }
}
```

### 브라켓 사이즈 매핑

| 모터 용량 | 브라켓 사이즈 |
|-----------|---------------|
| 300K, 400K | 530×320 |
| 500K, 600K | 600×350 |
| 800K, 1000K | 690×390 |

---

## 6. 견적번호 생성

### 형식
```
KD-PR-YYMMDD-NN

KD: 경동 (회사코드)
PR: 프로젝트
YYMMDD: 날짜 (년월일 6자리)
NN: 일련번호 (01~99, 당일 기준)
```

### 생성 로직
```php
// generate_serial_pjnum.php
function generatePjnum($pdo) {
    $today = date('ymd');
    $prefix = "KD-PR-{$today}-";

    // 오늘 날짜의 마지막 번호 조회
    $sql = "SELECT pjnum FROM estimate
            WHERE pjnum LIKE ?
            ORDER BY pjnum DESC LIMIT 1";
    $stmh = $pdo->prepare($sql);
    $stmh->execute([$prefix . '%']);
    $row = $stmh->fetch();

    if ($row) {
        // 마지막 번호 추출 후 +1
        $lastNum = intval(substr($row['pjnum'], -2));
        $nextNum = str_pad($lastNum + 1, 2, '0', STR_PAD_LEFT);
    } else {
        $nextNum = '01';
    }

    return $prefix . $nextNum;
}
```

---

## 7. 금액 처리 규칙

### 단위 변환
| 항목 | 입력 단위 | 계산 단위 | 비고 |
|------|----------|----------|------|
| 폭/높이 | mm | m | /1000 변환 |
| 면적 | - | m² | 폭×높이/1,000,000 |
| 중량 | - | kg | 면적×단위중량 |
| 금액 | - | 원 | 천원 단위 반올림 |

### 반올림 규칙
```javascript
// calculation.js
// 금액은 천원 단위에서 반올림
roundedAreaPrice = Math.round(areaPrice / 1000) * 1000;

// 면적은 소수점 2자리
area = Math.round(area * 100) / 100;
```

### 수동 편집 처리
```javascript
// 수동 편집된 셀은 배경색 변경
$('.manually-edited').css('background-color', '#f8d7da');

// 자동 계산 시 수동 편집 값 유지 옵션
if (!isManuallyEdited) {
    cell.val(calculatedValue);
}
```

---

## 8. 데이터 저장 규칙

### 저장 전 검증
1. 필수값 확인: 현장명, 발주처, 담당자
2. 수치 변환: 콤마 제거, 정수 변환
3. 권한 확인: 레벨 5 이하

### 저장 데이터
```php
// insert.php
$data = [
    'pjnum' => generatePjnum(),
    'indate' => date('Y-m-d'),
    'orderman' => $_SESSION['name'],
    'outworkplace' => $outworkplace,
    'major_category' => $major_category,
    'model_name' => $model_name,
    'makeWidth' => intval(str_replace(',', '', $makeWidth)),
    'makeHeight' => intval(str_replace(',', '', $makeHeight)),
    'maguriWing' => $maguriWing,
    'inspectionFee' => intval(str_replace(',', '', $inspectionFee)),
    'estimateList' => json_encode($estimateList),
    'estimateList_auto' => json_encode($estimateList_auto),
    'estimateSlatList' => json_encode($estimateSlatList),
    'estimateSlatList_auto' => json_encode($estimateSlatList_auto),
    'estimateTotal' => intval(str_replace(',', '', $estimateTotal)),
    'steel' => $steel,
    'motor' => $motor,
    'warranty' => $warranty,
    'slatcheck' => $slatcheck,
    'partscheck' => $partscheck
];
```

---

## 9. SAM 이관 시 로직 변경

### Service 클래스 분리
```php
// app/Services/QuotationService.php
class QuotationService
{
    // 1. 견적 생성
    public function createQuote(array $data): Quote { }

    // 2. 금액 계산
    public function calculateAmount(Quote $quote): array { }

    // 3. 스크린 계산
    protected function calculateScreenAmount(array $details): array { }

    // 4. 슬랫 계산
    protected function calculateSlatAmount(array $details): array { }

    // 5. 모터 용량 결정
    protected function determineMotorCapacity(float $weight, ?int $inch): int { }

    // 6. 단가 조회
    protected function getUnitPrice(string $itemCode, array $params): float { }
}
```

### 계산 로직 캡슐화
```php
// app/ValueObjects/QuoteDimension.php
class QuoteDimension
{
    public function __construct(
        public readonly int $width,
        public readonly int $height,
        public readonly int $wing = 50
    ) {}

    public function getAreaForScreen(): float
    {
        return ($this->height + 550) * $this->width / 1000000;
    }

    public function getAreaForSlat(): float
    {
        return ($this->height + 50) * $this->width / 1000000;
    }
}
```

### 옵션 처리
```php
// app/ValueObjects/QuoteOptions.php
class QuoteOptions
{
    public function __construct(
        public readonly bool $steel = false,
        public readonly bool $motor = false,
        public readonly bool $warranty = false,
        public readonly bool $slat = false,
        public readonly bool $parts = false
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            steel: $data['steel'] ?? false,
            motor: $data['motor'] ?? false,
            warranty: $data['warranty'] ?? false,
            slat: $data['slat'] ?? false,
            parts: $data['parts'] ?? false
        );
    }

    public function toJson(): string
    {
        return json_encode([
            'steel' => $this->steel,
            'motor' => $this->motor,
            'warranty' => $this->warranty,
            'slat' => $this->slat,
            'parts' => $this->parts
        ]);
    }
}
```

---

## 참조 파일

- `5130/estimate/get_screen_amount.php` - 스크린 계산 로직
- `5130/estimate/get_slat_amount.php` - 슬랫 계산 로직
- `5130/estimate/fetch_unitprice.php` - 단가 조회 함수
- `5130/estimate/insert.php` - 저장 로직
- `5130/estimate/generate_serial_pjnum.php` - 번호 생성
