# 채번규칙 (Numbering Rules)

> **작성일**: 2026-02-27
> **상태**: 운영 중
> **최종 갱신**: 2026-03-17

---

## 1. 개요

### 1.1 목적

문서 번호(견적번호, 수주번호, 원자재 로트번호 등)를 규칙 기반으로 자동 생성하는 내부 서비스. 테넌트별로 채번 규칙을 설정하면, 해당 규칙에 따라 일련번호가 자동 생성된다. 규칙이 없는 경우 레거시 포맷으로 폴백.

### 1.2 핵심 원칙

- 유연한 패턴 기반 번호 생성 (6가지 세그먼트 유형)
- Atomic UPSERT로 동시성 안전 시퀀스 관리
- 기간별 자동 리셋 (일/월/년/무제한)
- 미리보기 기능 (DB 업데이트 없음)
- 규칙 미설정 시 레거시 로직 자동 폴백

---

## 2. 모델

| 모델 | 테이블 | 설명 |
|------|--------|------|
| `NumberingRule` | `numbering_rules` | 채번 규칙 정의 (document_type, pattern, reset_period) |
| — | `numbering_sequences` | 일련번호 카운터 (tenant + type + scope + period) |

**지원 문서 유형:**

| 코드 | 한글 | 사용처 |
|------|------|--------|
| `quote` | 견적 | `QuoteNumberService` |
| `order` | 수주 | `OrderService` |
| `sale` | 매출 | (예정) |
| `work_order` | 작업지시 | (예정) |
| `material_receipt` | 원자재수입검사 | `ReceivingService` |
| `stock_production` | 재고생산 | `BendingCodeService` (향후 연동) |

**리셋 주기:**

| 코드 | 설명 | period_key 예시 |
|------|------|----------------|
| `daily` | 매일 01부터 리셋 | `260317` |
| `monthly` | 매월 01부터 리셋 | `202603` |
| `yearly` | 매년 01부터 리셋 | `2026` |
| `never` | 리셋 없이 계속 증가 | `all` |

---

## 3. 패턴 구조

패턴은 JSON 배열로 세그먼트를 정의한다.

### 3.1 세그먼트 유형

| 세그먼트 | 설명 | 예시 |
|---------|------|------|
| `static` | 고정 문자열 | `{ "type": "static", "value": "KD" }` |
| `separator` | 구분자 | `{ "type": "separator", "value": "-" }` |
| `date` | 날짜 포맷 (PHP date) | `{ "type": "date", "format": "ymd" }` → `260317` |
| `sequence` | 일련번호 (자릿수는 rule 레벨) | `{ "type": "sequence" }` |
| `param` | 외부 파라미터 | `{ "type": "param", "key": "pair_code", "default": "SS" }` |
| `mapping` | 값 → 코드 매핑 | `{ "type": "mapping", "key": "category", "map": {"SCREEN": "SC"}, "default": "XX" }` |

### 3.2 date 포맷 예시

| format | 결과 | 설명 |
|--------|------|------|
| `ymd` | `260317` | 2자리 연+월+일 |
| `Ymd` | `20260317` | 4자리 연+월+일 |
| `ym` | `2603` | 2자리 연+월 |
| `Y` | `2026` | 4자리 연도 |

---

## 4. MNG 채번규칙 관리

### 4.1 접근 경로

```
MNG > 권한 관리 > 채번 규칙 관리
URL: admin.codebridge-x.com/numbering-rules
```

### 4.2 관리 화면 기능

| 기능 | 설명 |
|------|------|
| 목록 조회 | 문서유형, 상태 필터 + 규칙명 검색 |
| 새 규칙 | 패턴 세그먼트 편집기 + 실시간 미리보기 |
| 수정 | 기존 규칙의 패턴, 리셋주기, 자릿수 변경 |
| 삭제 | 규칙 삭제 (Hard Delete) |

### 4.3 신규 규칙 생성 절차

1. **+ 새 규칙** 버튼 클릭
2. **기본 정보** 입력:
   - 규칙명: 관리용 이름 (예: `원자재 로트번호`)
   - 문서유형: 드롭다운 선택 (테넌트당 유형별 1개만 가능)
   - 리셋 주기: 일별 / 월별 / 연별 / 무제한
   - 시퀀스 자릿수: 2 → `01,02` / 3 → `001,002` / 4 → `0001,0002`
3. **패턴 세그먼트** 구성:
   - `+ 세그먼트 추가` 버튼으로 세그먼트를 순서대로 추가
   - 각 세그먼트의 타입과 값 설정
4. **미리보기** 확인: 세그먼트 변경 시 실시간 미리보기 표시
5. **생성** 버튼으로 저장

> **주의**: 테넌트당 문서유형별 1개 규칙만 가능하다. 이미 등록된 유형은 선택 불가.

---

## 5. 신규 규칙 생성 템플릿

아래는 자주 사용하는 채번 패턴 템플릿이다. MNG에서 새 규칙 생성 시 참고한다.

### 5.1 원자재 로트번호 (YYMMDD-NN)

> 5130 레거시 방식 차용. 일별 시퀀스 리셋.

| 항목 | 값 |
|------|------|
| 규칙명 | `원자재 로트번호` |
| 문서유형 | `material_receipt` (원자재수입검사) |
| 리셋 주기 | `daily` (일별) |
| 시퀀스 자릿수 | `2` |

**패턴:**

```json
[
  { "type": "date", "format": "ymd" },
  { "type": "separator", "value": "-" },
  { "type": "sequence" }
]
```

**결과:** `260317-01`, `260317-02`, ... → 다음날 `260318-01`

### 5.2 견적번호 (KD-PR-YYMMDD-NN)

| 항목 | 값 |
|------|------|
| 규칙명 | `5130 견적번호` |
| 문서유형 | `quote` (견적) |
| 리셋 주기 | `daily` |
| 시퀀스 자릿수 | `2` |

**패턴:**

```json
[
  { "type": "static", "value": "KD" },
  { "type": "separator", "value": "-" },
  { "type": "static", "value": "PR" },
  { "type": "separator", "value": "-" },
  { "type": "date", "format": "ymd" },
  { "type": "separator", "value": "-" },
  { "type": "sequence" }
]
```

**결과:** `KD-PR-260317-01`

### 5.3 수주 로트번호 (KD-{pairCode}-YYMMDD-NN)

| 항목 | 값 |
|------|------|
| 규칙명 | `5130 수주 로트번호` |
| 문서유형 | `order` (수주) |
| 리셋 주기 | `daily` |
| 시퀀스 자릿수 | `2` |

**패턴:**

```json
[
  { "type": "static", "value": "KD" },
  { "type": "separator", "value": "-" },
  { "type": "param", "key": "pair_code", "default": "SS" },
  { "type": "separator", "value": "-" },
  { "type": "date", "format": "ymd" },
  { "type": "separator", "value": "-" },
  { "type": "sequence" }
]
```

**결과:** `KD-SS-260317-01`, `KD-TE-260317-01` (pairCode별 독립 시퀀스)

### 5.4 작업지시번호 (WO-YYYYMMDD-NNNN)

| 항목 | 값 |
|------|------|
| 규칙명 | `작업지시번호` |
| 문서유형 | `work_order` (작업지시) |
| 리셋 주기 | `daily` |
| 시퀀스 자릿수 | `4` |

**패턴:**

```json
[
  { "type": "static", "value": "WO" },
  { "type": "separator", "value": "-" },
  { "type": "date", "format": "Ymd" },
  { "type": "separator", "value": "-" },
  { "type": "sequence" }
]
```

**결과:** `WO-20260317-0001`

### 5.5 재공품 로트번호 ({prod}{spec}{date}-{length})

> 절곡품 LOT 번호. 제품+종류+날짜+길이 코드를 조합한 고정 번호.
> 날짜코드는 `BendingCodeService::generateDateCode()`가 생성한 4자리 압축 포맷.
> 같은 날 같은 조합은 동일 LOT 번호를 사용한다 (일련번호 없음).

| 항목 | 값 |
|------|------|
| 규칙명 | `5130 재공품 로트번호` |
| 문서유형 | `stock_production` (재고생산) |
| 리셋 주기 | — (시퀀스 미사용) |

**패턴:**

```json
[
  { "type": "param", "key": "prod_code", "default": "R" },
  { "type": "param", "key": "spec_code", "default": "M" },
  { "type": "param", "key": "date_code", "default": "6318" },
  { "type": "separator", "value": "-" },
  { "type": "param", "key": "length_code", "default": "30" }
]
```

**결과:** `GI6318-53` (연기차단재 화이바원단 W50×3000, 2026-03-18)

**호출 예시:**

```php
$numberingService->generate('stock_production', [
    'prod_code' => 'G',       // 연기차단재
    'spec_code' => 'I',       // 화이바원단(W50)
    'date_code' => '6318',    // BendingCodeService::generateDateCode()
    'length_code' => '53',    // W50×3000
]);
```

### 5.6 월별 리셋 예시 (INV-YYYYMM-NNN)

| 항목 | 값 |
|------|------|
| 규칙명 | `매출번호` |
| 문서유형 | `sale` (매출) |
| 리셋 주기 | `monthly` |
| 시퀀스 자릿수 | `3` |

**패턴:**

```json
[
  { "type": "static", "value": "INV" },
  { "type": "separator", "value": "-" },
  { "type": "date", "format": "Ym" },
  { "type": "separator", "value": "-" },
  { "type": "sequence" }
]
```

**결과:** `INV-202603-001` → 4월 리셋 → `INV-202604-001`

---

## 6. 서비스 연동

### 6.1 호출 구조

```
호출 서비스 → NumberingService.generate(documentType, params)
                ├─ 규칙 있음 → 패턴 기반 생성 (Atomic UPSERT)
                └─ 규칙 없음 → null 반환 → 호출 서비스가 레거시 로직 사용
```

### 6.2 현재 연동 서비스

| 서비스 | 문서유형 | 레거시 폴백 |
|--------|---------|------------|
| `QuoteNumberService` | `quote` | `QT{YYYYMMDD}{NNNN}` |
| `OrderService` | `order` | `ORD{YYYYMMDD}{NNNN}` |
| `ReceivingService` | `material_receipt` | `YYMMDD-NN` (DB 시퀀스) |

### 6.3 연동 코드 패턴

새로운 서비스에서 채번규칙을 사용하려면 아래 패턴을 따른다:

```php
// 1. NumberingService 호출
$numberingService = app(NumberingService::class);
$numberingService->setContext($this->tenantId(), $this->apiUserId());

// 2. 규칙 기반 생성 시도
$number = $numberingService->generate('document_type', [
    'param_key' => 'value',  // param/mapping 세그먼트용
]);

// 3. 규칙 없으면 레거시 폴백
if ($number !== null) {
    return $number;
}
return $this->generateLegacy();
```

---

## 7. 동시성 처리

```sql
INSERT INTO numbering_sequences
    (tenant_id, document_type, scope_key, period_key, last_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
    last_sequence = last_sequence + 1, updated_at = NOW()
```

MySQL의 `ON DUPLICATE KEY UPDATE`로 Atomic 연산을 보장한다. 동시 요청에서도 시퀀스 충돌 없이 안전하게 번호를 생성한다.

**scope_key 분리**: `param`, `mapping` 세그먼트 값이 **누적 결합**되어 scope_key로 저장된다. 복수 param이 있으면 조합별 독립 시퀀스를 유지한다.

예: 수주 `KD-SS-260317-01`과 `KD-TE-260317-01`은 scope_key가 `SS`, `TE`로 각각 독립 시퀀스.

---

## 8. 현재 등록 현황

### 프론트_테스트회사 (tenant_id: 287)

| # | 규칙명 | 문서유형 | 패턴 미리보기 | 리셋 | 자릿수 |
|---|--------|---------|-------------|------|:------:|
| 1 | 5130 견적번호 | quote | `KD-PR-260317-01` | 일별 | 2 |
| 2 | 5130 수주 로트번호 | order | `KD-SS-260317-01` | 일별 | 2 |
| 3 | 원자재 로트번호 | material_receipt | `260317-01` | 일별 | 2 |
| 4 | 5130 재공품 로트번호 | stock_production | `RM6318-30` | — | — |

> 신규 테넌트에 규칙이 없으면 각 서비스의 레거시 로직이 자동 적용된다.

---

## 관련 문서

- [DB 스키마 — 공통](../system/database/commons.md)
- [품목 정책](item-policy.md) — 품목 코드 체계

---

**최종 업데이트**: 2026-03-18
