407 lines
17 KiB
Markdown
407 lines
17 KiB
Markdown
|
|
# MNG 채번 규칙 관리 UI 계획
|
||
|
|
|
||
|
|
> **작성일**: 2026-02-07
|
||
|
|
> **목적**: MNG 관리자 패널에서 테넌트별 채번 규칙(견적번호, 수주로트번호 등)을 CRUD 관리하는 UI 구현
|
||
|
|
> **기준 문서**: `docs/plans/tenant-numbering-system-plan.md` (API 채번 시스템)
|
||
|
|
> **상태**: 대기
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 개요
|
||
|
|
|
||
|
|
### 1.1 배경
|
||
|
|
- API에 채번 규칙 시스템(`numbering_rules`, `numbering_sequences` 테이블)이 이미 구현됨
|
||
|
|
- 현재는 Seeder로만 규칙 등록 가능 → MNG에서 관리 UI가 필요
|
||
|
|
- 테넌트별로 견적, 수주, 원자재수입검사 등 문서유형별 채번 패턴을 설정/수정/삭제할 수 있어야 함
|
||
|
|
|
||
|
|
### 1.2 기준 원칙
|
||
|
|
```
|
||
|
|
- MNG 독립 모델 사용 (API 테이블 참조, 마이그레이션 생성 금지)
|
||
|
|
- MNG 기존 패턴 준수: Controller(Blade) + Api Controller(HTMX/JSON) + Service + FormRequest
|
||
|
|
- HTMX + Alpine.js로 SPA 유사 UX 제공
|
||
|
|
- JSON 패턴 편집을 위한 동적 폼 (세그먼트 추가/삭제/정렬)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 변경 승인 정책
|
||
|
|
|
||
|
|
| 분류 | 예시 | 승인 |
|
||
|
|
|------|------|------|
|
||
|
|
| 즉시 가능 | MNG 모델/서비스/컨트롤러/뷰 생성 | 불필요 |
|
||
|
|
| 컨펌 필요 | routes/web.php 수정, 사이드바 메뉴 추가 | **필수** |
|
||
|
|
| 금지 | mng/database/migrations/ 파일 생성, API 테이블 구조 변경 | 별도 협의 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 기술 스택 & 패턴
|
||
|
|
|
||
|
|
### 2.1 MNG 프로젝트 스택
|
||
|
|
| 항목 | 기술 |
|
||
|
|
|------|------|
|
||
|
|
| Backend | Laravel 12, PHP 8.4+ |
|
||
|
|
| Template | Blade (Plain Laravel, React/Vue 없음) |
|
||
|
|
| CSS | Tailwind CSS |
|
||
|
|
| 비동기 | HTMX 1.9 (페이지 새로고침 없이 테이블/폼 업데이트) |
|
||
|
|
| JS 프레임워크 | Alpine.js (동적 폼, 탭, 모달) |
|
||
|
|
| 인증 | Session 기반 (middleware: auth, tenant) |
|
||
|
|
| Multi-tenant | `session('selected_tenant_id')` 기반 |
|
||
|
|
|
||
|
|
### 2.2 참고 패턴 (부서관리 CRUD)
|
||
|
|
```
|
||
|
|
mng/app/Http/Controllers/DepartmentController.php ← Blade 렌더링만
|
||
|
|
mng/app/Http/Controllers/Api/Admin/DepartmentController.php ← CRUD 로직 (HTMX/JSON)
|
||
|
|
mng/app/Services/DepartmentService.php ← 비즈니스 로직
|
||
|
|
mng/app/Http/Requests/StoreDepartmentRequest.php ← 검증
|
||
|
|
mng/resources/views/departments/index.blade.php ← 목록 (HTMX 테이블)
|
||
|
|
mng/resources/views/departments/create.blade.php ← 생성 폼
|
||
|
|
mng/resources/views/departments/edit.blade.php ← 수정 폼
|
||
|
|
mng/resources/views/departments/partials/table.blade.php ← HTMX 파셜
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 대상 범위
|
||
|
|
|
||
|
|
### 3.1 Phase 1: 백엔드 (Model + Service + Controller + FormRequest + Route)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 1.1 | NumberingRule 모델 생성 | ⏳ | API 테이블 참조, BelongsToTenant |
|
||
|
|
| 1.2 | NumberingRuleService 생성 | ⏳ | CRUD + 미리보기 |
|
||
|
|
| 1.3 | NumberingRuleController (페이지) 생성 | ⏳ | Blade 렌더링 |
|
||
|
|
| 1.4 | Api/Admin/NumberingRuleController 생성 | ⏳ | HTMX/JSON CRUD |
|
||
|
|
| 1.5 | FormRequest 생성 | ⏳ | JSON 패턴 검증 |
|
||
|
|
| 1.6 | routes/web.php 라우트 추가 | ⏳ | ⚠️ 컨펌 필요 |
|
||
|
|
|
||
|
|
### 3.2 Phase 2: 프론트엔드 (Blade Views)
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 2.1 | index.blade.php (목록) | ⏳ | HTMX 테이블, 필터 |
|
||
|
|
| 2.2 | partials/table.blade.php | ⏳ | HTMX 파셜 |
|
||
|
|
| 2.3 | create.blade.php (생성) | ⏳ | Alpine.js 동적 세그먼트 폼 |
|
||
|
|
| 2.4 | edit.blade.php (수정) | ⏳ | 기존 패턴 로드 + 편집 |
|
||
|
|
| 2.5 | partials/segment-form.blade.php | ⏳ | 세그먼트 편집 컴포넌트 |
|
||
|
|
| 2.6 | partials/preview.blade.php | ⏳ | 실시간 미리보기 |
|
||
|
|
|
||
|
|
### 3.3 Phase 3: 통합 & 검증
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 3.1 | 사이드바 메뉴 추가 | ⏳ | ⚠️ 컨펌 필요 |
|
||
|
|
| 3.2 | 기능 테스트 | ⏳ | CRUD + 미리보기 |
|
||
|
|
| 3.3 | 기존 시더 데이터 확인 | ⏳ | tenant_id=287 규칙 편집 가능 확인 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 상세 설계
|
||
|
|
|
||
|
|
### 4.1 파일 구조 (생성할 파일 목록)
|
||
|
|
|
||
|
|
```
|
||
|
|
mng/
|
||
|
|
├── app/
|
||
|
|
│ ├── Models/
|
||
|
|
│ │ └── NumberingRule.php ← NEW
|
||
|
|
│ ├── Services/
|
||
|
|
│ │ └── NumberingRuleService.php ← NEW
|
||
|
|
│ ├── Http/
|
||
|
|
│ │ ├── Controllers/
|
||
|
|
│ │ │ ├── NumberingRuleController.php ← NEW (Blade)
|
||
|
|
│ │ │ └── Api/Admin/
|
||
|
|
│ │ │ └── NumberingRuleController.php ← NEW (HTMX/JSON)
|
||
|
|
│ │ └── Requests/
|
||
|
|
│ │ ├── StoreNumberingRuleRequest.php ← NEW
|
||
|
|
│ │ └── UpdateNumberingRuleRequest.php ← NEW
|
||
|
|
├── resources/views/
|
||
|
|
│ └── numbering/
|
||
|
|
│ ├── index.blade.php ← NEW
|
||
|
|
│ ├── create.blade.php ← NEW
|
||
|
|
│ ├── edit.blade.php ← NEW
|
||
|
|
│ └── partials/
|
||
|
|
│ ├── table.blade.php ← NEW
|
||
|
|
│ ├── segment-form.blade.php ← NEW
|
||
|
|
│ └── preview.blade.php ← NEW
|
||
|
|
└── routes/
|
||
|
|
└── web.php ← MODIFY (라우트 추가)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 DB 스키마 (이미 존재, 참조용)
|
||
|
|
|
||
|
|
```sql
|
||
|
|
-- numbering_rules (API에서 생성 완료)
|
||
|
|
id, tenant_id, document_type(50), rule_name(100),
|
||
|
|
pattern(JSON), reset_period(20), sequence_padding(INT),
|
||
|
|
is_active(BOOL), created_by, updated_by, timestamps
|
||
|
|
UNIQUE(tenant_id, document_type)
|
||
|
|
|
||
|
|
-- numbering_sequences (API에서 생성 완료, 조회 전용)
|
||
|
|
id, tenant_id, document_type(50), scope_key(100),
|
||
|
|
period_key(20), last_sequence(INT), timestamps
|
||
|
|
UNIQUE(tenant_id, document_type, scope_key, period_key)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 JSON 패턴 세그먼트 타입
|
||
|
|
|
||
|
|
| 타입 | 필드 | 예시 | 설명 |
|
||
|
|
|------|------|------|------|
|
||
|
|
| `static` | `value` | `{"type":"static","value":"KD"}` | 고정 문자열 |
|
||
|
|
| `separator` | `value` | `{"type":"separator","value":"-"}` | 구분자 |
|
||
|
|
| `date` | `format` | `{"type":"date","format":"ymd"}` | PHP date format |
|
||
|
|
| `param` | `key`, `default` | `{"type":"param","key":"pair_code","default":"SS"}` | 외부 파라미터 |
|
||
|
|
| `mapping` | `key`, `map`, `default` | `{"type":"mapping","key":"product_category","map":{"screen":"SC","steel":"ST"},"default":"SC"}` | 값 매핑 |
|
||
|
|
| `sequence` | (없음) | `{"type":"sequence"}` | 자동 순번 |
|
||
|
|
|
||
|
|
### 4.4 UI 설계
|
||
|
|
|
||
|
|
#### 목록 페이지 (`index.blade.php`)
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────────────────────────┐
|
||
|
|
│ 채번 규칙 관리 [+ 새 규칙] │
|
||
|
|
├──────────────────────────────────────────────────────────┤
|
||
|
|
│ [문서유형 ▼] [상태 ▼] [검색...] [검색 버튼] │
|
||
|
|
├──────────────────────────────────────────────────────────┤
|
||
|
|
│ # │ 규칙명 │ 문서유형 │ 패턴 미리보기 │ 상태 │ 작업 │
|
||
|
|
│ 1 │ 5130 견적번호 │ quote │ KD-PR-260207-01 │ 활성 │ 수정/삭제│
|
||
|
|
│ 2 │ 5130 수주 로트 │ order │ KD-SS-260207-01 │ 활성 │ 수정/삭제│
|
||
|
|
└──────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 생성/수정 페이지 (`create.blade.php` / `edit.blade.php`)
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────────────────────────┐
|
||
|
|
│ 채번 규칙 생성 ← 목록으로 │
|
||
|
|
├──────────────────────────────────────────────────────────┤
|
||
|
|
│ ┌─ 기본 정보 ──────────────────────────────────────────┐ │
|
||
|
|
│ │ 규칙명: [________] 문서유형: [quote ▼] │ │
|
||
|
|
│ │ 리셋주기: [daily ▼] 시퀀스 자릿수: [2] │ │
|
||
|
|
│ │ 활성: [✓] │ │
|
||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ┌─ 패턴 세그먼트 ─────────────────────────────────────┐ │
|
||
|
|
│ │ ① [static ▼] value: [KD] [✕] [↕] │ │
|
||
|
|
│ │ ② [separator ▼] value: [-] [✕] [↕] │ │
|
||
|
|
│ │ ③ [static ▼] value: [PR] [✕] [↕] │ │
|
||
|
|
│ │ ④ [separator ▼] value: [-] [✕] [↕] │ │
|
||
|
|
│ │ ⑤ [date ▼] format: [ymd] [✕] [↕] │ │
|
||
|
|
│ │ ⑥ [separator ▼] value: [-] [✕] [↕] │ │
|
||
|
|
│ │ ⑦ [sequence ▼] [✕] [↕] │ │
|
||
|
|
│ │ [+ 세그먼트 추가] │ │
|
||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ┌─ 미리보기 ──────────────────────────────────────────┐ │
|
||
|
|
│ │ 생성 예시: KD-PR-260207-01 │ │
|
||
|
|
│ │ KD-PR-260207-02 │ │
|
||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ [취소] [저장] │
|
||
|
|
└──────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.5 핵심 구현 코드 (Blueprint)
|
||
|
|
|
||
|
|
#### Model (`mng/app/Models/NumberingRule.php`)
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
namespace App\Models;
|
||
|
|
|
||
|
|
use App\Traits\BelongsToTenant;
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
|
||
|
|
class NumberingRule extends Model
|
||
|
|
{
|
||
|
|
use BelongsToTenant;
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'tenant_id', 'document_type', 'rule_name', 'pattern',
|
||
|
|
'reset_period', 'sequence_padding', 'is_active',
|
||
|
|
'created_by', 'updated_by',
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'pattern' => 'array',
|
||
|
|
'is_active' => 'boolean',
|
||
|
|
'sequence_padding' => 'integer',
|
||
|
|
];
|
||
|
|
|
||
|
|
// 문서유형 상수
|
||
|
|
const DOC_QUOTE = 'quote';
|
||
|
|
const DOC_ORDER = 'order';
|
||
|
|
const DOC_SALE = 'sale';
|
||
|
|
const DOC_WORK_ORDER = 'work_order';
|
||
|
|
const DOC_MATERIAL_RECEIPT = 'material_receipt';
|
||
|
|
|
||
|
|
public static function documentTypes(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
self::DOC_QUOTE => '견적',
|
||
|
|
self::DOC_ORDER => '수주',
|
||
|
|
self::DOC_SALE => '매출',
|
||
|
|
self::DOC_WORK_ORDER => '작업지시',
|
||
|
|
self::DOC_MATERIAL_RECEIPT => '원자재수입검사',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function resetPeriods(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'daily' => '일별',
|
||
|
|
'monthly' => '월별',
|
||
|
|
'yearly' => '연별',
|
||
|
|
'never' => '리셋안함',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 패턴 미리보기 문자열 생성 (실제 시퀀스 없이)
|
||
|
|
*/
|
||
|
|
public function getPreviewAttribute(): string
|
||
|
|
{
|
||
|
|
$result = '';
|
||
|
|
foreach ($this->pattern as $segment) {
|
||
|
|
$result .= match ($segment['type']) {
|
||
|
|
'static' => $segment['value'],
|
||
|
|
'separator' => $segment['value'],
|
||
|
|
'date' => now()->format($segment['format']),
|
||
|
|
'param' => $segment['default'] ?? '{' . $segment['key'] . '}',
|
||
|
|
'mapping' => $segment['default'] ?? '{' . $segment['key'] . '}',
|
||
|
|
'sequence' => str_pad('1', $this->sequence_padding, '0', STR_PAD_LEFT),
|
||
|
|
default => '',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Service (`mng/app/Services/NumberingRuleService.php`)
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\NumberingRule;
|
||
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||
|
|
|
||
|
|
class NumberingRuleService
|
||
|
|
{
|
||
|
|
public function getRules(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$tenantId = session('selected_tenant_id');
|
||
|
|
$query = NumberingRule::query();
|
||
|
|
|
||
|
|
if ($tenantId) {
|
||
|
|
$query->where('tenant_id', $tenantId);
|
||
|
|
}
|
||
|
|
if (!empty($filters['document_type'])) {
|
||
|
|
$query->where('document_type', $filters['document_type']);
|
||
|
|
}
|
||
|
|
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
|
||
|
|
$query->where('is_active', (bool) $filters['is_active']);
|
||
|
|
}
|
||
|
|
if (!empty($filters['search'])) {
|
||
|
|
$query->where('rule_name', 'like', "%{$filters['search']}%");
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->orderBy('document_type')->paginate($perPage);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getRule(int $id): ?NumberingRule { ... }
|
||
|
|
public function createRule(array $data): NumberingRule { ... }
|
||
|
|
public function updateRule(int $id, array $data): bool { ... }
|
||
|
|
public function deleteRule(int $id): bool { ... }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 세그먼트 동적 폼 (Alpine.js)
|
||
|
|
```javascript
|
||
|
|
// create.blade.php 내 Alpine.js 컴포넌트
|
||
|
|
Alpine.data('patternEditor', () => ({
|
||
|
|
segments: [],
|
||
|
|
segmentTypes: [
|
||
|
|
{ value: 'static', label: '고정 문자열' },
|
||
|
|
{ value: 'separator', label: '구분자' },
|
||
|
|
{ value: 'date', label: '날짜' },
|
||
|
|
{ value: 'param', label: '외부 파라미터' },
|
||
|
|
{ value: 'mapping', label: '값 매핑' },
|
||
|
|
{ value: 'sequence', label: '자동 순번' },
|
||
|
|
],
|
||
|
|
dateFormats: [
|
||
|
|
{ value: 'ymd', label: 'YYMMDD (260207)' },
|
||
|
|
{ value: 'Ymd', label: 'YYYYMMDD (20260207)' },
|
||
|
|
{ value: 'Ym', label: 'YYYYMM (202602)' },
|
||
|
|
{ value: 'Y', label: 'YYYY (2026)' },
|
||
|
|
],
|
||
|
|
|
||
|
|
addSegment() {
|
||
|
|
this.segments.push({ type: 'static', value: '' });
|
||
|
|
},
|
||
|
|
removeSegment(index) {
|
||
|
|
this.segments.splice(index, 1);
|
||
|
|
},
|
||
|
|
moveSegment(from, to) { ... },
|
||
|
|
|
||
|
|
get preview() {
|
||
|
|
return this.segments.map(seg => {
|
||
|
|
switch(seg.type) {
|
||
|
|
case 'static': return seg.value || '?';
|
||
|
|
case 'separator': return seg.value || '-';
|
||
|
|
case 'date': return formatDate(seg.format || 'ymd');
|
||
|
|
case 'param': return seg.default || `{${seg.key || '?'}}`;
|
||
|
|
case 'mapping': return seg.default || `{${seg.key || '?'}}`;
|
||
|
|
case 'sequence': return '01';
|
||
|
|
default: return '';
|
||
|
|
}
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 구현 순서 & 예상 작업량
|
||
|
|
|
||
|
|
| Phase | 작업 | 파일 수 | 예상 |
|
||
|
|
|-------|------|--------|------|
|
||
|
|
| 1 | 백엔드 (Model, Service, Controller, FormRequest, Route) | 6개 생성 + 1개 수정 | 중 |
|
||
|
|
| 2 | 프론트엔드 (Blade Views 6개) | 6개 생성 | 대 (Alpine.js 동적 폼) |
|
||
|
|
| 3 | 통합 & 검증 (메뉴, 테스트) | 1개 수정 | 소 |
|
||
|
|
|
||
|
|
**핵심 난이도**: Phase 2의 세그먼트 동적 폼 (Alpine.js로 JSON 배열 편집 + 실시간 미리보기)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 검증 결과
|
||
|
|
|
||
|
|
### 6.1 테스트 시나리오
|
||
|
|
|
||
|
|
| 입력 | 예상 결과 | 상태 |
|
||
|
|
|------|----------|:----:|
|
||
|
|
| 목록 진입 | tenant_id=287 규칙 2건 표시 | ⏳ |
|
||
|
|
| 견적 규칙 수정 → 저장 | pattern JSON 업데이트, 미리보기 변경 | ⏳ |
|
||
|
|
| 새 규칙 생성 (material_receipt) | 규칙 3건으로 증가 | ⏳ |
|
||
|
|
| 세그먼트 추가/삭제/순서변경 | Alpine.js 동적 폼 동작 | ⏳ |
|
||
|
|
| 미리보기 버튼 | 실시간 번호 예시 표시 | ⏳ |
|
||
|
|
| 규칙 삭제 | soft delete 또는 hard delete | ⏳ |
|
||
|
|
| 중복 document_type 생성 시도 | 유니크 제약 에러 표시 | ⏳ |
|
||
|
|
|
||
|
|
### 6.2 성공 기준
|
||
|
|
|
||
|
|
| 기준 | 달성 | 비고 |
|
||
|
|
|------|------|------|
|
||
|
|
| 규칙 CRUD 정상 동작 | ⏳ | 생성/조회/수정/삭제 |
|
||
|
|
| 세그먼트 동적 편집 | ⏳ | 추가/삭제/순서변경 |
|
||
|
|
| 실시간 미리보기 | ⏳ | 패턴 변경 시 즉시 반영 |
|
||
|
|
| 기존 API 채번 로직과 호환 | ⏳ | MNG에서 수정한 규칙이 API에서 정상 작동 |
|
||
|
|
| MNG 기존 패턴 준수 | ⏳ | HTMX + Alpine.js + Tailwind |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 참고 문서
|
||
|
|
|
||
|
|
- **채번 시스템 설계**: `docs/plans/tenant-numbering-system-plan.md`
|
||
|
|
- **MNG CRUD 패턴**: `mng/app/Http/Controllers/DepartmentController.php` + `Api/Admin/DepartmentController.php`
|
||
|
|
- **Alpine.js 동적 폼 참고**: `mng/resources/views/quote-formulas/edit.blade.php` (탭 + 동적 아이템)
|
||
|
|
- **HTMX 테이블 참고**: `mng/resources/views/departments/partials/table.blade.php`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|