Files
sam-docs/plans/mng-quote-formula-development-plan.md
hskwon 4d64beab78 docs: MNG 견적수식 관리 개발 계획 문서 추가
- Phase 1-4 개발 계획 (범위/매핑/품목 관리 UI, 5130 연동)
- MNG vs API 시스템 비교 분석
- 코딩 컨벤션 및 예시 코드 (Controller, Service, Blade View)
- DB 스키마 DDL 및 API 응답 포맷
- 새 세션용 체크리스트 포함 (v1.1)
2025-12-22 16:37:45 +09:00

29 KiB

MNG 견적수식 관리 개발 계획

작성일: 2025-12-22 상태: 계획 수립 대상: mng.sam.kr/quote-formulas


1. 현황 분석

1.1 MNG 프로젝트 현재 상태

구현된 기능 (mng)

기능 상태 설명
수식 목록 완료 페이지네이션, 필터링, HTMX 테이블
수식 생성 완료 카테고리, 유형, 변수명, 수식 입력
수식 수정 완료 편집 폼, API 연동
수식 삭제 완료 Soft Delete, 복원, 영구삭제
수식 복제 완료 수식 복사 기능
활성/비활성 완료 토글 기능
카테고리 관리 완료 CRUD 구현
시뮬레이터 완료 입력값 → 계산 결과 미리보기
변수 참조 완료 사용 가능한 변수 목록 표시
수식 검증 완료 문법 검증 API

미구현/미완성 기능 (mng)

기능 상태 설명
범위(Range) 관리 UI 미구현 범위별 결과 설정 화면 없음
매핑(Mapping) 관리 UI 미구현 매핑 규칙 설정 화면 없음
품목(Item) 관리 UI 미구현 출력 품목 설정 화면 없음
5130 데이터 연동 미구현 레거시 가격 데이터 동기화

1.2 API 프로젝트 현재 상태

모델 구조 (api)

QuoteFormulaCategory (카테고리)
└── QuoteFormula (수식)
    ├── QuoteFormulaRange (범위 조건)
    ├── QuoteFormulaMapping (매핑 규칙)
    └── QuoteFormulaItem (출력 품목)

시더 데이터 (api)

시더 데이터 수 설명
QuoteFormulaCategorySeeder 11개 카테고리 (오픈사이즈~단가수식)
QuoteFormulaSeeder 30개 수식, 18개 범위 스크린 계산 수식
QuoteFormulaItemSeeder 25개 품목 마스터 (5130 가격 적용)

서비스 (api)

서비스 역할
QuoteCalculationService 자동산출 실행 엔진
FormulaEvaluatorService 수식 평가, 범위/매핑 처리
QuoteService 견적 CRUD, 상태 관리
QuoteNumberService 견적번호 생성
QuoteDocumentService PDF/이메일/카카오 발송 (TODO)

2. MNG vs API 비교 분석

2.1 데이터 구조 비교

항목 MNG API 일치
quote_formula_categories
quote_formulas
quote_formula_ranges
quote_formula_mappings
quote_formula_items

결론: 모델 구조는 동일함 (같은 DB 사용)

2.2 기능 비교

기능 MNG API 비고
수식 CRUD 동일
카테고리 CRUD 동일
범위 관리 UI (시더) MNG에 UI 필요
매핑 관리 UI (시더) MNG에 UI 필요
품목 관리 UI (시더) MNG에 UI 필요
시뮬레이터 동일
자동산출 API - API 전용

2.3 핵심 차이점

MNG (관리 UI)
├── 수식 기본 정보 관리 ✅
├── 카테고리 관리 ✅
├── 시뮬레이터 ✅
└── 범위/매핑/품목 관리 ❌ ← 개발 필요

API (자동산출 엔진)
├── 시더로 데이터 주입 ✅
├── 자동산출 서비스 ✅
└── 견적 생성 API ✅

3. 개발 계획

3.1 목표

MNG에서 범위(Range), 매핑(Mapping), 품목(Item) 관리 UI를 추가하여:

  1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능
  2. 5130 레거시 데이터를 참조하여 가격 설정 가능
  3. 실시간 시뮬레이션으로 설정 검증 가능

3.2 개발 범위

Phase 1: 범위(Range) 관리 UI

우선순위: 높음 이유: 모터, 가이드레일, 케이스 자동 선택에 필수

기능 목록:

  1. 수식 상세 페이지에 범위 관리 탭 추가
  2. 범위 목록 표시 (min ~ max → 결과)
  3. 범위 추가/수정/삭제
  4. 드래그앤드롭 순서 변경
  5. item_code 연결 (품목 선택)

화면 설계:

[수식 수정] 페이지
├── [기본 정보] 탭 (기존)
├── [범위 설정] 탭 ← 추가
│   ├── 조건 변수: [K (중량)] ▼
│   ├── 범위 목록
│   │   ┌─────────────────────────────────────────────────┐
│   │   │ # │ 최소값 │ 최대값 │ 결과값      │ 품목코드    │
│   │   ├─────────────────────────────────────────────────┤
│   │   │ 1 │ 0      │ 150    │ 150K        │ PT-MOTOR-150│
│   │   │ 2 │ 150    │ 300    │ 300K        │ PT-MOTOR-300│
│   │   │ 3 │ 300    │ 400    │ 400K        │ PT-MOTOR-400│
│   │   └─────────────────────────────────────────────────┘
│   └── [+ 범위 추가]
├── [매핑 설정] 탭
└── [품목 설정] 탭

API 엔드포인트 (MNG 내부):

GET    /api/admin/quote-formulas/formulas/{id}/ranges
POST   /api/admin/quote-formulas/formulas/{id}/ranges
PUT    /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId}
DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId}
POST   /api/admin/quote-formulas/formulas/{id}/ranges/reorder

Phase 2: 매핑(Mapping) 관리 UI

우선순위: 중간 이유: 제어기 유형 등 코드 매핑에 사용

기능 목록:

  1. 수식 상세 페이지에 매핑 관리 탭 추가
  2. 매핑 목록 표시 (소스값 → 결과값)
  3. 매핑 추가/수정/삭제

화면 설계:

[매핑 설정] 탭
├── 소스 변수: [CONTROL_TYPE] ▼
├── 매핑 목록
│   ┌──────────────────────────────────────────────────┐
│   │ # │ 소스값  │ 결과값     │ 품목코드        │
│   ├──────────────────────────────────────────────────┤
│   │ 1 │ EMB     │ 매립형     │ PT-CTRL-EMB     │
│   │ 2 │ EXP     │ 노출형     │ PT-CTRL-EXP     │
│   │ 3 │ BOX_1P  │ 콘트롤박스 │ PT-CTRL-BOX-1P  │
│   └──────────────────────────────────────────────────┘
└── [+ 매핑 추가]

Phase 3: 품목(Item) 관리 UI

우선순위: 중간 이유: 수식 결과로 생성되는 품목 정의

기능 목록:

  1. 수식 상세 페이지에 품목 관리 탭 추가
  2. 품목 목록 표시
  3. 품목 추가/수정/삭제
  4. 수량/단가 수식 입력
  5. 5130 품목 검색 및 가격 참조

화면 설계:

[품목 설정] 탭
├── 품목 목록
│   ┌───────────────────────────────────────────────────────────┐
│   │ 품목코드     │ 품목명          │ 규격    │ 수량식 │ 단가식│
│   ├───────────────────────────────────────────────────────────┤
│   │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1      │ 285000│
│   │ PT-GR-3000   │ 가이드레일 3000 │ 3000mm  │ 2      │ 42000 │
│   └───────────────────────────────────────────────────────────┘
├── [+ 품목 추가]
└── [5130에서 가져오기] ← 레거시 연동

Phase 4: 5130 데이터 연동

우선순위: 높음 이유: 실제 가격 데이터 필요

기능 목록:

  1. 5130 DB 가격 테이블 조회 API
  2. 품목 추가 시 5130 검색 모달
  3. 가격 동기화 기능
  4. 가격 변경 이력 관리

5130 테이블 참조:

-- chandj.price_motor: 모터 가격
-- chandj.price_*: 기타 품목 가격

3.3 파일 수정/추가 목록

Routes (mng/routes/web.php)

// 기존 라우트에 추가
Route::prefix('quote-formulas')->group(function () {
    // ... 기존 라우트
    Route::get('/{id}/ranges', [QuoteFormulaController::class, 'ranges']);
    Route::get('/{id}/mappings', [QuoteFormulaController::class, 'mappings']);
    Route::get('/{id}/items', [QuoteFormulaController::class, 'items']);
});

API Routes (mng/routes/api.php)

// 범위/매핑/품목 CRUD API
Route::prefix('quote-formulas/formulas/{formulaId}')->group(function () {
    Route::apiResource('ranges', QuoteFormulaRangeController::class);
    Route::post('ranges/reorder', [QuoteFormulaRangeController::class, 'reorder']);

    Route::apiResource('mappings', QuoteFormulaMappingController::class);

    Route::apiResource('items', QuoteFormulaItemController::class);
});

// 5130 연동
Route::prefix('legacy')->group(function () {
    Route::get('items/search', [LegacyController::class, 'searchItems']);
    Route::get('prices/{itemCode}', [LegacyController::class, 'getPrice']);
});

Controllers

app/Http/Controllers/
├── QuoteFormulaController.php (수정: 탭 추가)
└── Api/Admin/Quote/
    ├── QuoteFormulaRangeController.php (신규)
    ├── QuoteFormulaMappingController.php (신규)
    ├── QuoteFormulaItemController.php (신규)
    └── LegacyController.php (신규: 5130 연동)

Services

app/Services/
├── Quote/
│   ├── QuoteFormulaRangeService.php (신규)
│   ├── QuoteFormulaMappingService.php (신규)
│   └── QuoteFormulaItemService.php (신규)
└── Legacy/
    └── LegacyPriceService.php (신규: 5130 가격 조회)

Views

resources/views/quote-formulas/
├── edit.blade.php (수정: 탭 구조로 변경)
├── partials/
│   ├── ranges-tab.blade.php (신규)
│   ├── mappings-tab.blade.php (신규)
│   └── items-tab.blade.php (신규)
└── modals/
    ├── range-form.blade.php (신규)
    ├── mapping-form.blade.php (신규)
    ├── item-form.blade.php (신규)
    └── legacy-item-search.blade.php (신규)

3.4 개발 순서

Phase 1: 범위 관리 (1주)
├── Day 1-2: API 엔드포인트 구현
├── Day 3-4: UI 컴포넌트 구현
└── Day 5: 테스트 및 검증

Phase 2: 매핑 관리 (0.5주)
├── Day 1: API 구현
└── Day 2-3: UI 구현

Phase 3: 품목 관리 (0.5주)
├── Day 1: API 구현
└── Day 2-3: UI 구현

Phase 4: 5130 연동 (1주)
├── Day 1-2: 레거시 DB 조회 서비스
├── Day 3-4: 검색 모달 UI
└── Day 5: 가격 동기화 기능

통합 테스트 (0.5주)
├── 시뮬레이터 연동 테스트
└── 전체 플로우 검증

4. 기술 스택

4.1 Frontend (MNG)

  • Framework: Laravel Blade + Alpine.js
  • Styling: Tailwind CSS + DaisyUI
  • AJAX: HTMX (hx-get, hx-post, hx-delete)
  • Modal: DaisyUI modal 컴포넌트

4.2 Backend (MNG)

  • Framework: Laravel 12
  • ORM: Eloquent
  • DB: MySQL (samdb + chandj)
  • Auth: Session 기반

4.3 API 연동

  • MNG 내부 API (/api/admin/quote-formulas/*)
  • 5130 DB 직접 조회 (chandj 데이터베이스)

5. 데이터 마이그레이션

5.1 현재 상태

  • API 시더로 30개 수식, 18개 범위, 25개 품목 등록됨
  • 5130 실제 가격 데이터 반영 완료

5.2 마이그레이션 계획

  1. Phase 1 완료 후: 시더 데이터를 MNG UI로 확인 가능
  2. Phase 4 완료 후: 5130 데이터 자동 동기화

6. 검증 계획

6.1 시뮬레이터 테스트

입력: W0=3000, H0=2500
예상 결과:
  - CASE: PT-CASE-3600 (S=3270)
  - GR: PT-GR-3000 (H1=2770)
  - MOTOR: PT-MOTOR-150 (K=41.21kg)

6.2 CRUD 테스트

  • 범위 추가/수정/삭제 후 시뮬레이터 결과 확인
  • 품목 가격 변경 후 합계 확인

7. 참고 자료

7.1 기존 파일 위치 (MNG)

mng/
├── app/Http/Controllers/
│   ├── QuoteFormulaController.php
│   └── Api/Admin/Quote/QuoteFormulaController.php
├── app/Services/Quote/
│   └── QuoteFormulaService.php
├── app/Models/Quote/
│   ├── QuoteFormula.php
│   ├── QuoteFormulaCategory.php
│   ├── QuoteFormulaRange.php
│   ├── QuoteFormulaMapping.php
│   └── QuoteFormulaItem.php
└── resources/views/quote-formulas/
    ├── index.blade.php
    ├── create.blade.php
    ├── edit.blade.php
    └── simulator.blade.php

7.2 API 시더 위치

api/database/seeders/
├── QuoteFormulaCategorySeeder.php
├── QuoteFormulaSeeder.php
└── QuoteFormulaItemSeeder.php

7.3 5130 가격 테이블

chandj.price_motor     -- 모터 가격
chandj.price_*         -- 기타 품목 가격 (확인 필요)

8. 리스크 및 대응

리스크 영향 대응
5130 DB 스키마 변경 JSON 파싱 로직 유연하게 구현
MNG-API 데이터 불일치 동일 DB 사용으로 해결됨
시뮬레이터 성능 저하 수식 캐싱 적용

9. 다음 단계

  1. 승인 요청: 이 계획에 대한 검토 및 승인
  2. Phase 1 착수: 범위 관리 UI 개발 시작
  3. 주간 리뷰: 진행 상황 점검

10. 코딩 컨벤션 및 예시 코드

10.1 API Controller 패턴 (MNG)

<?php
// 파일: app/Http/Controllers/Api/Admin/Quote/QuoteFormulaRangeController.php

namespace App\Http\Controllers\Api\Admin\Quote;

use App\Http\Controllers\Controller;
use App\Services\Quote\QuoteFormulaRangeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class QuoteFormulaRangeController extends Controller
{
    public function __construct(
        private readonly QuoteFormulaRangeService $rangeService
    ) {}

    /**
     * 범위 목록 조회
     */
    public function index(int $formulaId): JsonResponse
    {
        $ranges = $this->rangeService->getRangesByFormula($formulaId);

        return response()->json([
            'success' => true,
            'data' => $ranges,
        ]);
    }

    /**
     * 범위 생성
     */
    public function store(Request $request, int $formulaId): JsonResponse
    {
        $validated = $request->validate([
            'min_value' => 'nullable|numeric',
            'max_value' => 'nullable|numeric',
            'condition_variable' => 'required|string|max:50',
            'result_value' => 'required|string',
            'result_type' => 'in:fixed,formula',
            'sort_order' => 'nullable|integer',
        ]);

        $range = $this->rangeService->createRange($formulaId, $validated);

        return response()->json([
            'success' => true,
            'message' => '범위가 추가되었습니다.',
            'data' => $range,
        ]);
    }

    /**
     * 범위 수정
     */
    public function update(Request $request, int $formulaId, int $rangeId): JsonResponse
    {
        $validated = $request->validate([
            'min_value' => 'nullable|numeric',
            'max_value' => 'nullable|numeric',
            'result_value' => 'required|string',
            'result_type' => 'in:fixed,formula',
        ]);

        $this->rangeService->updateRange($rangeId, $validated);

        return response()->json([
            'success' => true,
            'message' => '범위가 수정되었습니다.',
        ]);
    }

    /**
     * 범위 삭제
     */
    public function destroy(int $formulaId, int $rangeId): JsonResponse
    {
        $this->rangeService->deleteRange($rangeId);

        return response()->json([
            'success' => true,
            'message' => '범위가 삭제되었습니다.',
        ]);
    }

    /**
     * 순서 변경
     */
    public function reorder(Request $request, int $formulaId): JsonResponse
    {
        $validated = $request->validate([
            'range_ids' => 'required|array',
            'range_ids.*' => 'integer',
        ]);

        $this->rangeService->reorder($validated['range_ids']);

        return response()->json([
            'success' => true,
            'message' => '순서가 변경되었습니다.',
        ]);
    }
}

10.2 Service 패턴 (MNG)

<?php
// 파일: app/Services/Quote/QuoteFormulaRangeService.php

namespace App\Services\Quote;

use App\Models\Quote\QuoteFormulaRange;
use Illuminate\Support\Collection;

class QuoteFormulaRangeService
{
    /**
     * 수식별 범위 조회
     */
    public function getRangesByFormula(int $formulaId): Collection
    {
        return QuoteFormulaRange::where('formula_id', $formulaId)
            ->orderBy('sort_order')
            ->get();
    }

    /**
     * 범위 생성
     */
    public function createRange(int $formulaId, array $data): QuoteFormulaRange
    {
        $data['formula_id'] = $formulaId;

        // 순서 자동 설정
        if (!isset($data['sort_order'])) {
            $maxOrder = QuoteFormulaRange::where('formula_id', $formulaId)->max('sort_order') ?? 0;
            $data['sort_order'] = $maxOrder + 1;
        }

        return QuoteFormulaRange::create($data);
    }

    /**
     * 범위 수정
     */
    public function updateRange(int $rangeId, array $data): QuoteFormulaRange
    {
        $range = QuoteFormulaRange::findOrFail($rangeId);
        $range->update($data);

        return $range->fresh();
    }

    /**
     * 범위 삭제
     */
    public function deleteRange(int $rangeId): void
    {
        QuoteFormulaRange::destroy($rangeId);
    }

    /**
     * 순서 변경
     */
    public function reorder(array $rangeIds): void
    {
        foreach ($rangeIds as $order => $id) {
            QuoteFormulaRange::where('id', $id)->update(['sort_order' => $order + 1]);
        }
    }
}

10.3 Model 구조 (참조용)

<?php
// 파일: app/Models/Quote/QuoteFormulaRange.php (기존)

class QuoteFormulaRange extends Model
{
    protected $table = 'quote_formula_ranges';

    protected $fillable = [
        'formula_id',
        'min_value',
        'max_value',
        'condition_variable',
        'result_value',      // JSON 저장 가능: {"value": "150K", "item_code": "PT-MOTOR-150"}
        'result_type',       // 'fixed' | 'formula'
        'sort_order',
    ];

    protected $casts = [
        'min_value' => 'decimal:4',
        'max_value' => 'decimal:4',
        'sort_order' => 'integer',
    ];

    public function formula(): BelongsTo
    {
        return $this->belongsTo(QuoteFormula::class, 'formula_id');
    }

    public function isInRange($value): bool
    {
        // min < value <= max 체크
    }
}

10.4 Blade View 패턴 (HTMX + Alpine.js)

{{-- 파일: resources/views/quote-formulas/partials/ranges-tab.blade.php --}}

<div x-data="rangesManager()" x-init="loadRanges()">
    {{-- 헤더 --}}
    <div class="flex justify-between items-center mb-4">
        <h3 class="text-lg font-medium">범위 설정</h3>
        <button @click="openAddModal()" class="btn btn-primary btn-sm">
            + 범위 추가
        </button>
    </div>

    {{-- 조건 변수 표시 --}}
    <div class="mb-4">
        <span class="text-sm text-gray-600">조건 변수:</span>
        <span class="font-mono bg-gray-100 px-2 py-1 rounded" x-text="conditionVariable || '-'"></span>
    </div>

    {{-- 범위 목록 테이블 --}}
    <div class="overflow-x-auto">
        <table class="table table-zebra w-full">
            <thead>
                <tr>
                    <th class="w-12">#</th>
                    <th>최소값</th>
                    <th>최대값</th>
                    <th>결과값</th>
                    <th>품목코드</th>
                    <th class="w-24">액션</th>
                </tr>
            </thead>
            <tbody>
                <template x-for="(range, index) in ranges" :key="range.id">
                    <tr>
                        <td x-text="index + 1"></td>
                        <td x-text="range.min_value ?? '-'"></td>
                        <td x-text="range.max_value ?? '-'"></td>
                        <td>
                            <span class="font-mono" x-text="getResultDisplay(range)"></span>
                        </td>
                        <td>
                            <span class="badge badge-ghost" x-text="getItemCode(range) || '-'"></span>
                        </td>
                        <td>
                            <button @click="editRange(range)" class="btn btn-ghost btn-xs">수정</button>
                            <button @click="deleteRange(range.id)" class="btn btn-ghost btn-xs text-red-500">삭제</button>
                        </td>
                    </tr>
                </template>
            </tbody>
        </table>
    </div>

    {{-- 빈 상태 --}}
    <div x-show="ranges.length === 0" class="text-center py-8 text-gray-500">
        설정된 범위가 없습니다.
    </div>

    {{-- 범위 추가/수정 모달 --}}
    <dialog id="rangeModal" class="modal">
        <div class="modal-box">
            <h3 class="font-bold text-lg" x-text="editingRange ? '범위 수정' : '범위 추가'"></h3>
            <form @submit.prevent="saveRange()">
                <div class="grid grid-cols-2 gap-4 mt-4">
                    <div>
                        <label class="label">최소값</label>
                        <input type="number" step="0.0001" x-model="form.min_value"
                               class="input input-bordered w-full">
                    </div>
                    <div>
                        <label class="label">최대값</label>
                        <input type="number" step="0.0001" x-model="form.max_value"
                               class="input input-bordered w-full">
                    </div>
                </div>
                <div class="mt-4">
                    <label class="label">결과값 (JSON)</label>
                    <textarea x-model="form.result_value" rows="3"
                              class="textarea textarea-bordered w-full font-mono"
                              placeholder='{"value": "150K", "item_code": "PT-MOTOR-150"}'></textarea>
                </div>
                <div class="modal-action">
                    <button type="button" @click="closeModal()" class="btn">취소</button>
                    <button type="submit" class="btn btn-primary">저장</button>
                </div>
            </form>
        </div>
    </dialog>
</div>

<script>
function rangesManager() {
    return {
        ranges: [],
        conditionVariable: '',
        editingRange: null,
        form: {
            min_value: null,
            max_value: null,
            result_value: '',
            result_type: 'fixed',
        },

        async loadRanges() {
            const formulaId = {{ $formula->id }};
            const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges`);
            const data = await res.json();
            if (data.success) {
                this.ranges = data.data;
                if (this.ranges.length > 0) {
                    this.conditionVariable = this.ranges[0].condition_variable;
                }
            }
        },

        getResultDisplay(range) {
            try {
                const parsed = JSON.parse(range.result_value);
                return parsed.value || range.result_value;
            } catch {
                return range.result_value;
            }
        },

        getItemCode(range) {
            try {
                const parsed = JSON.parse(range.result_value);
                return parsed.item_code;
            } catch {
                return null;
            }
        },

        openAddModal() {
            this.editingRange = null;
            this.form = { min_value: null, max_value: null, result_value: '', result_type: 'fixed' };
            document.getElementById('rangeModal').showModal();
        },

        editRange(range) {
            this.editingRange = range;
            this.form = { ...range };
            document.getElementById('rangeModal').showModal();
        },

        closeModal() {
            document.getElementById('rangeModal').close();
        },

        async saveRange() {
            const formulaId = {{ $formula->id }};
            const url = this.editingRange
                ? `/api/admin/quote-formulas/formulas/${formulaId}/ranges/${this.editingRange.id}`
                : `/api/admin/quote-formulas/formulas/${formulaId}/ranges`;
            const method = this.editingRange ? 'PUT' : 'POST';

            const res = await fetch(url, {
                method,
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': '{{ csrf_token() }}',
                },
                body: JSON.stringify(this.form),
            });

            const data = await res.json();
            if (data.success) {
                this.closeModal();
                await this.loadRanges();
                showToast(data.message, 'success');
            } else {
                showToast(data.message, 'error');
            }
        },

        async deleteRange(rangeId) {
            if (!confirm('이 범위를 삭제하시겠습니까?')) return;

            const formulaId = {{ $formula->id }};
            const res = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}/ranges/${rangeId}`, {
                method: 'DELETE',
                headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
            });

            const data = await res.json();
            if (data.success) {
                await this.loadRanges();
                showToast(data.message, 'success');
            }
        },
    };
}
</script>

10.5 DB 스키마 (참조용)

-- quote_formula_ranges 테이블 (기존)
CREATE TABLE `quote_formula_ranges` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `formula_id` BIGINT UNSIGNED NOT NULL,
    `min_value` DECIMAL(15,4) NULL COMMENT '최소값 (NULL=제한없음)',
    `max_value` DECIMAL(15,4) NULL COMMENT '최대값 (NULL=제한없음)',
    `condition_variable` VARCHAR(50) NOT NULL COMMENT '조건 변수 (K, H1, S 등)',
    `result_value` TEXT NOT NULL COMMENT '결과값 (JSON 또는 문자열)',
    `result_type` ENUM('fixed','formula') DEFAULT 'fixed' COMMENT '결과 유형',
    `sort_order` INT DEFAULT 0,
    `created_at` TIMESTAMP NULL,
    `updated_at` TIMESTAMP NULL,

    INDEX `idx_formula_id` (`formula_id`),
    CONSTRAINT `fk_ranges_formula` FOREIGN KEY (`formula_id`)
        REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
);

-- quote_formula_items 테이블 (기존)
CREATE TABLE `quote_formula_items` (
    `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `formula_id` BIGINT UNSIGNED NOT NULL,
    `item_code` VARCHAR(50) NOT NULL COMMENT '품목 코드',
    `item_name` VARCHAR(200) NOT NULL COMMENT '품목명',
    `specification` VARCHAR(200) NULL COMMENT '규격',
    `unit` VARCHAR(20) NOT NULL COMMENT '단위',
    `quantity_formula` VARCHAR(500) NOT NULL COMMENT '수량 계산식',
    `unit_price_formula` VARCHAR(500) NULL COMMENT '단가 계산식 (NULL=마스터 참조)',
    `sort_order` INT DEFAULT 0,
    `created_at` TIMESTAMP NULL,
    `updated_at` TIMESTAMP NULL,

    INDEX `idx_formula_id` (`formula_id`),
    INDEX `idx_item_code` (`item_code`),
    CONSTRAINT `fk_items_formula` FOREIGN KEY (`formula_id`)
        REFERENCES `quote_formulas`(`id`) ON DELETE CASCADE
);

10.6 API 응답 형식

// 성공 응답
{
    "success": true,
    "message": "범위가 추가되었습니다.",
    "data": { ... }
}

// 실패 응답
{
    "success": false,
    "message": "이미 사용 중인 변수명입니다."
}

// 목록 응답
{
    "success": true,
    "data": [
        {
            "id": 1,
            "formula_id": 5,
            "min_value": "0.0000",
            "max_value": "150.0000",
            "condition_variable": "K",
            "result_value": "{\"value\":\"150K\",\"item_code\":\"PT-MOTOR-150\"}",
            "result_type": "fixed",
            "sort_order": 1
        }
    ]
}

11. 체크리스트 (새 세션용)

개발 시작 전 확인

  • mng 프로젝트 디렉토리 확인: /Users/hskwon/Works/@KD_SAM/SAM/mng
  • 기존 Controller 패턴 확인: app/Http/Controllers/Api/Admin/Quote/QuoteFormulaController.php
  • 기존 Model 확인: app/Models/Quote/QuoteFormulaRange.php
  • 기존 View 확인: resources/views/quote-formulas/edit.blade.php
  • DB 테이블 확인: quote_formula_ranges, quote_formula_items

Phase 1 (범위 관리) 작업 순서

  1. QuoteFormulaRangeController.php 생성
  2. QuoteFormulaRangeService.php 생성
  3. routes/api.php에 라우트 추가
  4. edit.blade.php 탭 구조로 수정
  5. partials/ranges-tab.blade.php 생성
  6. HTMX/Alpine.js 연동 테스트
  7. 시뮬레이터와 연동 확인

문서 버전: 1.1 작성자: Claude Code 검토자: - 업데이트: 코딩 컨벤션 및 예시 코드 추가 (2025-12-22)