feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
@extends('layouts.app')
|
|
|
|
|
|
|
|
|
|
|
|
@section('title', '수식 시뮬레이터')
|
|
|
|
|
|
|
|
|
|
|
|
@section('content')
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div class="container mx-auto max-w-7xl">
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
<!-- 헤더 -->
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div class="flex justify-between items-center mb-4">
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
<div>
|
|
|
|
|
|
<h1 class="text-2xl font-bold text-gray-800">수식 시뮬레이터</h1>
|
|
|
|
|
|
<p class="text-sm text-gray-500 mt-1">입력값을 넣어 전체 수식 실행 결과를 테스트합니다.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a href="{{ route('quote-formulas.index') }}"
|
|
|
|
|
|
class="text-gray-600 hover:text-gray-800 text-sm font-medium">
|
|
|
|
|
|
← 수식 목록
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 입력 영역 (상단 가로 배치) -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
|
|
|
|
|
<!-- 로딩 -->
|
|
|
|
|
|
<div id="inputLoading" class="text-center py-4">
|
|
|
|
|
|
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
|
|
|
|
|
<p class="text-gray-500 mt-2 text-sm">입력 변수를 불러오는 중...</p>
|
|
|
|
|
|
</div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 입력 폼 -->
|
|
|
|
|
|
<form id="simulatorForm" class="hidden">
|
|
|
|
|
|
<!-- 동적으로 생성될 입력 필드 (가로 배치) -->
|
|
|
|
|
|
<div id="inputFields" class="flex flex-wrap gap-3 items-end"></div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 에러 메시지 -->
|
|
|
|
|
|
<div id="errorMessage" class="hidden mt-3 bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700"></div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 실행 버튼 -->
|
|
|
|
|
|
<div class="mt-3 flex justify-end">
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
<button type="submit" id="runButton"
|
2025-12-23 23:41:37 +09:00
|
|
|
|
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2">
|
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
|
</svg>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
<span>수식 실행</span>
|
|
|
|
|
|
<svg id="runSpinner" class="hidden animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 입력 변수 없음 -->
|
|
|
|
|
|
<div id="noInputs" class="hidden text-center py-4 text-gray-500">
|
|
|
|
|
|
<p>입력 변수가 없습니다.</p>
|
|
|
|
|
|
<a href="{{ route('quote-formulas.create') }}" class="text-blue-600 hover:text-blue-700 text-sm mt-2 inline-block">
|
|
|
|
|
|
수식 추가하기 →
|
|
|
|
|
|
</a>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 수식 실행 순서 (입력 영역 내부) -->
|
|
|
|
|
|
<div id="categoryOrderSection" class="hidden mt-3 pt-3 border-t border-gray-100">
|
|
|
|
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
|
|
|
|
<span class="text-xs font-medium text-gray-500">실행순서:</span>
|
|
|
|
|
|
<div id="categoryOrder" class="flex flex-wrap gap-1.5"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<!-- 모드 선택 탭 -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm mb-4">
|
|
|
|
|
|
<div class="flex border-b">
|
|
|
|
|
|
<button type="button" id="modeFormula" class="flex-1 px-4 py-3 text-sm font-medium text-blue-600 border-b-2 border-blue-600 bg-blue-50 mode-tab" data-mode="formula">
|
|
|
|
|
|
📊 수식 시뮬레이션
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" id="modeBom" class="flex-1 px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700 mode-tab" data-mode="bom">
|
|
|
|
|
|
🔧 BOM 디버깅
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- BOM 시뮬레이션 섹션 (숨김 기본) -->
|
|
|
|
|
|
<div id="bomSection" class="hidden">
|
|
|
|
|
|
<!-- BOM 입력 영역 -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">완제품 BOM 시뮬레이션</h3>
|
|
|
|
|
|
<form id="bomSimulatorForm">
|
|
|
|
|
|
<div class="flex flex-wrap gap-3 items-end">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<!-- 제품 카테고리 (PC) -->
|
|
|
|
|
|
<div class="w-28">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">카테고리</label>
|
|
|
|
|
|
<select id="productCategory" name="PC" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
<option value="">전체</option>
|
|
|
|
|
|
<option value="SCREEN">스크린</option>
|
|
|
|
|
|
<option value="STEEL">철재</option>
|
|
|
|
|
|
<option value="BENDING">절곡</option>
|
|
|
|
|
|
<option value="ALUMINUM">알루미늄</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 제품명 (완제품 FG) -->
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<div class="w-48">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">제품명</label>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<select id="fgCodeSelect" name="finished_goods_code" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<option value="">선택...</option>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 폭 (W0) -->
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<div class="w-20">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">W0</label>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<input type="number" name="W0" value="2000" min="100" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 높이 (H0) -->
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<div class="w-20">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">H0</label>
|
|
|
|
|
|
<input type="number" name="H0" value="2500" min="100" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 가이드레일 설치유형 (GT) -->
|
|
|
|
|
|
<div class="w-28">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">가이드레일</label>
|
|
|
|
|
|
<select id="guideRailType" name="GT" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
<option value="wall">벽부착</option>
|
|
|
|
|
|
<option value="ceiling">천장매립</option>
|
|
|
|
|
|
<option value="floor">바닥매립</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 모터 전원 (MP) -->
|
|
|
|
|
|
<div class="w-24">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">모터전원</label>
|
|
|
|
|
|
<select id="motorPower" name="MP" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
<option value="single">단상</option>
|
|
|
|
|
|
<option value="three">삼상</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 연동제어기 (CT) -->
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<div class="w-24">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">제어기</label>
|
|
|
|
|
|
<select id="controller" name="CT" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
<option value="basic">기본</option>
|
|
|
|
|
|
<option value="smart">스마트</option>
|
|
|
|
|
|
<option value="premium">프리미엄</option>
|
|
|
|
|
|
</select>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
</div>
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<!-- 마구리 날개치수 (WS) -->
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<div class="w-20">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">마구리</label>
|
|
|
|
|
|
<input type="number" id="wingSize" name="WS" value="50" min="0" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 검사비 (INSP) -->
|
|
|
|
|
|
<div class="w-24">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">검사비</label>
|
|
|
|
|
|
<input type="number" id="inspectionFee" name="INSP" value="50000" min="0" step="1000" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 수량 (QTY) -->
|
|
|
|
|
|
<div class="w-16">
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">수량</label>
|
|
|
|
|
|
<input type="number" name="QTY" value="1" min="1" class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 실행 버튼 -->
|
|
|
|
|
|
<button type="submit" id="runBomButton" class="bg-green-600 hover:bg-green-700 text-white px-4 py-1.5 rounded font-medium text-sm transition-colors flex items-center gap-2">
|
2025-12-30 17:27:01 +09:00
|
|
|
|
<span>실행</span>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
<svg id="bomSpinner" class="hidden animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- BOM 결과 영역 -->
|
|
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
|
|
|
|
|
<!-- 디버깅 패널 (10단계) -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
|
|
|
|
|
🔍 계산 과정 (10단계)
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div id="debugSteps" class="space-y-2 text-xs max-h-[60vh] overflow-y-auto">
|
|
|
|
|
|
<p class="text-gray-400 text-center py-4">BOM 계산을 실행하세요</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 공정별 품목 그룹 -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
|
|
|
|
|
🏭 공정별 품목
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div id="processGroups" class="space-y-3 max-h-[60vh] overflow-y-auto">
|
|
|
|
|
|
<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 소계 및 합계 -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
|
|
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
|
|
|
|
|
💰 원가 요약
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div id="costSummary" class="space-y-2 text-sm">
|
|
|
|
|
|
<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 수식 시뮬레이션 섹션 -->
|
|
|
|
|
|
<div id="formulaSection">
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 결과 영역 (2열 그리드) -->
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
|
|
|
|
<!-- 좌측: 실행 결과 (계산된 변수) -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
|
|
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-3 pb-2 border-b flex items-center gap-2">
|
|
|
|
|
|
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
계산된 변수
|
|
|
|
|
|
</h2>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
|
|
|
|
|
<!-- 초기 상태 -->
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div id="resultEmpty" class="text-center text-gray-400 py-8">
|
|
|
|
|
|
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
|
|
|
|
</svg>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<p class="text-sm">입력값을 넣고 수식을 실행하세요</p>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 로딩 -->
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div id="resultLoading" class="hidden text-center py-8">
|
|
|
|
|
|
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 mx-auto"></div>
|
|
|
|
|
|
<p class="text-gray-500 mt-3 text-sm">수식을 실행하는 중...</p>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 계산된 변수 결과 -->
|
|
|
|
|
|
<div id="calculatedVariables" class="hidden space-y-2 max-h-[calc(100vh-400px)] overflow-y-auto"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 에러가 있을 경우 -->
|
|
|
|
|
|
<div id="resultErrors" class="hidden mt-4">
|
|
|
|
|
|
<h3 class="text-sm font-medium text-red-700 mb-2">오류</h3>
|
|
|
|
|
|
<div id="errorList" class="space-y-2"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 우측: 품목 목록 -->
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
|
|
|
|
<!-- 탭 헤더 -->
|
|
|
|
|
|
<div class="flex items-center gap-4 mb-3 pb-2 border-b">
|
|
|
|
|
|
<button type="button" id="tabGenerated" class="flex items-center gap-2 text-lg font-semibold text-gray-400 hover:text-gray-600 transition-colors tab-btn" data-tab="generated">
|
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>생성된 품목</span>
|
|
|
|
|
|
<span id="generatedCount" class="hidden px-1.5 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">0</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" id="tabAllItems" class="flex items-center gap-2 text-lg font-semibold text-green-600 hover:text-green-700 transition-colors tab-btn active" data-tab="all">
|
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>전체 품목</span>
|
|
|
|
|
|
<span id="allItemsCount" class="px-1.5 py-0.5 bg-gray-100 text-gray-600 text-xs rounded-full">0</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 전체 품목 탭 -->
|
|
|
|
|
|
<div id="allItemsTab">
|
|
|
|
|
|
<!-- 검색/필터 -->
|
|
|
|
|
|
<div class="flex gap-2 mb-3">
|
|
|
|
|
|
<input type="text" id="itemSearch" placeholder="코드 또는 품명 검색..."
|
|
|
|
|
|
class="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-green-500">
|
|
|
|
|
|
<select id="itemTypeFilter" class="px-3 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-green-500">
|
|
|
|
|
|
<option value="">전체 유형</option>
|
|
|
|
|
|
<option value="FG">완제품 (FG)</option>
|
|
|
|
|
|
<option value="PT">부품 (PT)</option>
|
|
|
|
|
|
<option value="SM">부자재 (SM)</option>
|
|
|
|
|
|
<option value="RM">원자재 (RM)</option>
|
|
|
|
|
|
<option value="CS">소모품 (CS)</option>
|
|
|
|
|
|
</select>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 품목 유형 통계 -->
|
|
|
|
|
|
<div id="itemStats" class="flex flex-wrap gap-1.5 mb-3"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 로딩 -->
|
|
|
|
|
|
<div id="itemsLoading" class="text-center py-8">
|
|
|
|
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600 mx-auto"></div>
|
|
|
|
|
|
<p class="text-gray-500 mt-2 text-sm">품목 목록을 불러오는 중...</p>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
|
|
|
|
|
|
<!-- 품목 목록 -->
|
|
|
|
|
|
<div id="allItemsList" class="hidden space-y-1.5 max-h-[calc(100vh-480px)] overflow-y-auto"></div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<!-- 생성된 품목 탭 -->
|
|
|
|
|
|
<div id="generatedItemsTab" class="hidden">
|
|
|
|
|
|
<!-- 초기 상태 -->
|
|
|
|
|
|
<div id="itemsEmpty" class="text-center text-gray-400 py-8">
|
|
|
|
|
|
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<p class="text-sm">수식 실행 후 생성된 품목이 표시됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 생성된 품목 결과 -->
|
|
|
|
|
|
<div id="generatedItems" class="hidden space-y-2 max-h-[calc(100vh-400px)] overflow-y-auto"></div>
|
|
|
|
|
|
</div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-24 15:45:54 +09:00
|
|
|
|
</div> <!-- /formulaSection -->
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
@endsection
|
|
|
|
|
|
|
|
|
|
|
|
@push('scripts')
|
|
|
|
|
|
<script>
|
|
|
|
|
|
let inputVariables = [];
|
|
|
|
|
|
let categories = [];
|
2025-12-23 23:41:37 +09:00
|
|
|
|
let allItems = [];
|
|
|
|
|
|
let filteredItems = [];
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
|
|
|
|
|
// 초기화
|
|
|
|
|
|
async function init() {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
loadInputVariables(),
|
2025-12-23 23:41:37 +09:00
|
|
|
|
loadCategories(),
|
|
|
|
|
|
loadAllItems()
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
]);
|
2025-12-23 23:41:37 +09:00
|
|
|
|
|
|
|
|
|
|
// 탭 이벤트 바인딩
|
|
|
|
|
|
setupTabs();
|
|
|
|
|
|
|
|
|
|
|
|
// 검색/필터 이벤트 바인딩
|
|
|
|
|
|
setupItemFilters();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 전체 품목 로드
|
|
|
|
|
|
async function loadAllItems() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/formulas/items', {
|
|
|
|
|
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('itemsLoading').classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
allItems = result.data.items || [];
|
|
|
|
|
|
filteredItems = [...allItems];
|
|
|
|
|
|
|
|
|
|
|
|
// 통계 표시
|
|
|
|
|
|
renderItemStats(result.data.stats || {});
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 목록 표시
|
|
|
|
|
|
renderAllItems();
|
|
|
|
|
|
|
|
|
|
|
|
// 전체 개수 표시
|
|
|
|
|
|
document.getElementById('allItemsCount').textContent = result.data.total || 0;
|
|
|
|
|
|
document.getElementById('allItemsList').classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('품목 목록 로드 실패:', err);
|
|
|
|
|
|
document.getElementById('itemsLoading').classList.add('hidden');
|
|
|
|
|
|
document.getElementById('allItemsList').innerHTML = '<p class="text-sm text-red-500 text-center py-4">품목을 불러오는데 실패했습니다.</p>';
|
|
|
|
|
|
document.getElementById('allItemsList').classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 유형 통계 렌더링
|
|
|
|
|
|
function renderItemStats(stats) {
|
|
|
|
|
|
const container = document.getElementById('itemStats');
|
|
|
|
|
|
const typeLabels = {
|
|
|
|
|
|
'FG': '완제품',
|
|
|
|
|
|
'PT': '부품',
|
|
|
|
|
|
'SM': '부자재',
|
|
|
|
|
|
'RM': '원자재',
|
|
|
|
|
|
'CS': '소모품'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const statHtml = Object.entries(stats).map(([type, count]) => {
|
|
|
|
|
|
const color = itemTypeBadgeColors[type] || 'bg-gray-100 text-gray-600';
|
|
|
|
|
|
const label = typeLabels[type] || type;
|
|
|
|
|
|
return `<span class="px-2 py-0.5 ${color} text-xs rounded cursor-pointer hover:opacity-80" onclick="filterByType('${type}')">${label}: ${count}</span>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = statHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 전체 품목 목록 렌더링
|
|
|
|
|
|
function renderAllItems() {
|
|
|
|
|
|
const container = document.getElementById('allItemsList');
|
|
|
|
|
|
|
|
|
|
|
|
if (filteredItems.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<p class="text-sm text-gray-500 text-center py-4">표시할 품목이 없습니다.</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = filteredItems.map(item => {
|
|
|
|
|
|
const badgeColor = itemTypeBadgeColors[item.item_type] || 'bg-gray-100 text-gray-600';
|
|
|
|
|
|
const bomBadge = item.has_bom ? `<span class="text-[10px] text-orange-500">BOM ${item.bom_count}</span>` : '';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="flex items-center gap-2 p-2 bg-gray-50 hover:bg-gray-100 rounded text-sm transition-colors">
|
|
|
|
|
|
<span class="px-1.5 py-0.5 ${badgeColor} text-xs font-medium rounded flex-shrink-0">
|
|
|
|
|
|
${item.item_type_label || item.item_type}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="font-mono text-xs text-gray-400 flex-shrink-0">${item.code}</span>
|
|
|
|
|
|
<span class="text-gray-700 truncate flex-1" title="${item.name}">${item.name}</span>
|
|
|
|
|
|
${bomBadge}
|
|
|
|
|
|
<span class="text-xs text-gray-400 flex-shrink-0">${item.unit || 'EA'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 유형으로 필터
|
|
|
|
|
|
function filterByType(type) {
|
|
|
|
|
|
document.getElementById('itemTypeFilter').value = type;
|
|
|
|
|
|
applyItemFilters();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 검색/필터 이벤트 설정
|
|
|
|
|
|
function setupItemFilters() {
|
|
|
|
|
|
const searchInput = document.getElementById('itemSearch');
|
|
|
|
|
|
const typeFilter = document.getElementById('itemTypeFilter');
|
|
|
|
|
|
|
|
|
|
|
|
let debounceTimer;
|
|
|
|
|
|
searchInput.addEventListener('input', () => {
|
|
|
|
|
|
clearTimeout(debounceTimer);
|
|
|
|
|
|
debounceTimer = setTimeout(applyItemFilters, 300);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
typeFilter.addEventListener('change', applyItemFilters);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 적용
|
|
|
|
|
|
function applyItemFilters() {
|
|
|
|
|
|
const search = document.getElementById('itemSearch').value.toLowerCase().trim();
|
|
|
|
|
|
const itemType = document.getElementById('itemTypeFilter').value;
|
|
|
|
|
|
|
|
|
|
|
|
filteredItems = allItems.filter(item => {
|
|
|
|
|
|
const matchesSearch = !search ||
|
|
|
|
|
|
item.code.toLowerCase().includes(search) ||
|
|
|
|
|
|
item.name.toLowerCase().includes(search);
|
|
|
|
|
|
|
|
|
|
|
|
const matchesType = !itemType || item.item_type === itemType;
|
|
|
|
|
|
|
|
|
|
|
|
return matchesSearch && matchesType;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
renderAllItems();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 탭 설정
|
|
|
|
|
|
function setupTabs() {
|
|
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', function() {
|
|
|
|
|
|
const tab = this.dataset.tab;
|
|
|
|
|
|
switchTab(tab);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 탭 전환
|
|
|
|
|
|
function switchTab(tab) {
|
|
|
|
|
|
const allTab = document.getElementById('allItemsTab');
|
|
|
|
|
|
const generatedTab = document.getElementById('generatedItemsTab');
|
|
|
|
|
|
const allBtn = document.getElementById('tabAllItems');
|
|
|
|
|
|
const generatedBtn = document.getElementById('tabGenerated');
|
|
|
|
|
|
|
|
|
|
|
|
if (tab === 'all') {
|
|
|
|
|
|
allTab.classList.remove('hidden');
|
|
|
|
|
|
generatedTab.classList.add('hidden');
|
|
|
|
|
|
allBtn.classList.add('text-green-600');
|
|
|
|
|
|
allBtn.classList.remove('text-gray-400');
|
|
|
|
|
|
generatedBtn.classList.add('text-gray-400');
|
|
|
|
|
|
generatedBtn.classList.remove('text-green-600');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
allTab.classList.add('hidden');
|
|
|
|
|
|
generatedTab.classList.remove('hidden');
|
|
|
|
|
|
allBtn.classList.remove('text-green-600');
|
|
|
|
|
|
allBtn.classList.add('text-gray-400');
|
|
|
|
|
|
generatedBtn.classList.remove('text-gray-400');
|
|
|
|
|
|
generatedBtn.classList.add('text-green-600');
|
|
|
|
|
|
}
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 입력 변수 로드
|
|
|
|
|
|
async function loadInputVariables() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/formulas/variables', {
|
|
|
|
|
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('inputLoading').classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
// type이 'input'인 변수만 필터링
|
|
|
|
|
|
inputVariables = result.data.filter(v => v.type === 'input');
|
|
|
|
|
|
|
|
|
|
|
|
if (inputVariables.length === 0) {
|
|
|
|
|
|
document.getElementById('noInputs').classList.remove('hidden');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderInputFields();
|
|
|
|
|
|
document.getElementById('simulatorForm').classList.remove('hidden');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById('noInputs').classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('입력 변수 로드 실패:', err);
|
|
|
|
|
|
document.getElementById('inputLoading').classList.add('hidden');
|
|
|
|
|
|
document.getElementById('noInputs').classList.remove('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 순서 로드
|
|
|
|
|
|
async function loadCategories() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/categories/dropdown', {
|
|
|
|
|
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
categories = result.data;
|
|
|
|
|
|
renderCategoryOrder();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('카테고리 로드 실패:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
// 디자인 페이지와 동일한 셀렉트 옵션 정의
|
|
|
|
|
|
const selectOptions = {
|
|
|
|
|
|
'PC': [
|
|
|
|
|
|
{ value: 'screen', label: '스크린' },
|
|
|
|
|
|
{ value: 'slat', label: '슬랫' },
|
|
|
|
|
|
{ value: 'steel', label: '철재' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'PRODUCT_ID': [], // 제품카테고리에 따라 동적으로 변경
|
|
|
|
|
|
'GT': [
|
|
|
|
|
|
{ value: 'back', label: '백면' },
|
|
|
|
|
|
{ value: 'side', label: '측면' },
|
|
|
|
|
|
{ value: 'both', label: '양측' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'MP': [
|
|
|
|
|
|
{ value: '220V', label: '220V' },
|
|
|
|
|
|
{ value: '380V', label: '380V' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'CT': [
|
|
|
|
|
|
{ value: 'none', label: '없음' },
|
|
|
|
|
|
{ value: 'single', label: '단독제어' },
|
|
|
|
|
|
{ value: 'linked', label: '연동제어' },
|
|
|
|
|
|
{ value: 'central', label: '중앙제어' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'CONTROLLER_TYPE': [
|
|
|
|
|
|
{ value: '매립형', label: '매립형' },
|
|
|
|
|
|
{ value: '노출형', label: '노출형' },
|
|
|
|
|
|
{ value: '일체형', label: '일체형' }
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 제품카테고리별 제품 목록
|
|
|
|
|
|
const productsByCategory = {
|
|
|
|
|
|
'screen': [
|
|
|
|
|
|
{ value: 'screen_standard', label: '스크린 셔터 (표준형)' },
|
|
|
|
|
|
{ value: 'screen_premium', label: '스크린 셔터 (프리미엄)' },
|
|
|
|
|
|
{ value: 'screen_large', label: '스크린 셔터 (대형)' },
|
|
|
|
|
|
{ value: 'screen_corner', label: '스크린 셔터 (코너형)' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'slat': [
|
|
|
|
|
|
{ value: 'slat_steel', label: '철재 슬랫 셔터' },
|
|
|
|
|
|
{ value: 'slat_aluminum', label: '알루미늄 슬랫 셔터' }
|
|
|
|
|
|
],
|
|
|
|
|
|
'steel': [
|
|
|
|
|
|
{ value: 'steel_door', label: '철재문' },
|
|
|
|
|
|
{ value: 'steel_shutter', label: '철재 셔터' }
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 입력 필드 렌더링 (가로 배치)
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
function renderInputFields() {
|
|
|
|
|
|
const container = document.getElementById('inputFields');
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
let html = '';
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
inputVariables.forEach(v => {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
const defaultValue = v.default_value || '';
|
|
|
|
|
|
let inputHtml = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 셀렉트 옵션이 정의된 변수인 경우
|
|
|
|
|
|
if (selectOptions[v.variable]) {
|
|
|
|
|
|
const options = selectOptions[v.variable];
|
|
|
|
|
|
const optionsHtml = options.map(opt =>
|
|
|
|
|
|
`<option value="${opt.value}">${opt.label}</option>`
|
|
|
|
|
|
).join('');
|
|
|
|
|
|
|
|
|
|
|
|
// 제품명은 비활성화 상태로 시작 (카테고리 선택 후 활성화)
|
|
|
|
|
|
const isDisabled = v.variable === 'PRODUCT_ID' ? 'disabled' : '';
|
|
|
|
|
|
|
|
|
|
|
|
inputHtml = `
|
|
|
|
|
|
<select name="${v.variable}" id="input_${v.variable}"
|
|
|
|
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${isDisabled ? 'bg-gray-100' : ''}"
|
|
|
|
|
|
${isDisabled}>
|
|
|
|
|
|
<option value="">선택</option>
|
|
|
|
|
|
${optionsHtml}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 수량(QTY)은 숫자 입력
|
|
|
|
|
|
else if (v.variable === 'QTY') {
|
|
|
|
|
|
inputHtml = `
|
|
|
|
|
|
<input type="number"
|
|
|
|
|
|
name="${v.variable}"
|
|
|
|
|
|
id="input_${v.variable}"
|
|
|
|
|
|
value="1"
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 기타 숫자 입력 (W0, H0 등)
|
|
|
|
|
|
else {
|
|
|
|
|
|
inputHtml = `
|
|
|
|
|
|
<input type="number"
|
|
|
|
|
|
name="${v.variable}"
|
|
|
|
|
|
id="input_${v.variable}"
|
|
|
|
|
|
value="${defaultValue}"
|
|
|
|
|
|
step="any"
|
|
|
|
|
|
class="w-full px-2 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
|
placeholder="${v.variable}">
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 너비 클래스 결정 (셀렉트는 조금 더 넓게)
|
|
|
|
|
|
const widthClass = selectOptions[v.variable] ? 'w-32' : 'w-24';
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
|
|
|
|
|
html += `
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div class="${widthClass}">
|
|
|
|
|
|
<label class="block text-xs font-medium text-gray-600 mb-1 truncate" title="${v.description || v.name || v.variable}">
|
|
|
|
|
|
${v.variable}
|
|
|
|
|
|
${v.name ? `<span class="text-gray-400">(${v.name})</span>` : ''}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
${inputHtml}
|
|
|
|
|
|
</div>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
`;
|
2025-12-23 23:41:37 +09:00
|
|
|
|
});
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
|
|
|
|
|
|
// 제품카테고리 변경 시 제품명 옵션 업데이트
|
|
|
|
|
|
const pcSelect = document.getElementById('input_PC');
|
|
|
|
|
|
const productSelect = document.getElementById('input_PRODUCT_ID');
|
|
|
|
|
|
|
|
|
|
|
|
if (pcSelect && productSelect) {
|
|
|
|
|
|
pcSelect.addEventListener('change', function() {
|
|
|
|
|
|
const category = this.value;
|
|
|
|
|
|
const products = productsByCategory[category] || [];
|
|
|
|
|
|
|
|
|
|
|
|
productSelect.innerHTML = '<option value="">선택</option>';
|
|
|
|
|
|
products.forEach(p => {
|
|
|
|
|
|
productSelect.innerHTML += `<option value="${p.value}">${p.label}</option>`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 선택 시 제품명 활성화
|
|
|
|
|
|
if (category) {
|
|
|
|
|
|
productSelect.disabled = false;
|
|
|
|
|
|
productSelect.classList.remove('bg-gray-100');
|
2025-12-04 16:23:27 +09:00
|
|
|
|
} else {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
productSelect.disabled = true;
|
|
|
|
|
|
productSelect.classList.add('bg-gray-100');
|
2025-12-04 16:23:27 +09:00
|
|
|
|
}
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 순서 렌더링
|
|
|
|
|
|
function renderCategoryOrder() {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
const section = document.getElementById('categoryOrderSection');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
const container = document.getElementById('categoryOrder');
|
|
|
|
|
|
|
|
|
|
|
|
if (categories.length === 0) {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
section.classList.add('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = categories.map((cat, index) => `
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<span class="inline-flex items-center gap-0.5 px-1.5 py-0.5 bg-gray-100 border border-gray-200 rounded text-xs text-gray-600">
|
|
|
|
|
|
<span class="w-3.5 h-3.5 flex items-center justify-center bg-gray-300 rounded text-xs font-medium">${index + 1}</span>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
${cat.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
`).join('');
|
2025-12-23 23:41:37 +09:00
|
|
|
|
|
|
|
|
|
|
section.classList.remove('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 폼 제출 (수식 실행)
|
|
|
|
|
|
document.getElementById('simulatorForm').addEventListener('submit', async function(e) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const errorDiv = document.getElementById('errorMessage');
|
|
|
|
|
|
errorDiv.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 수집
|
|
|
|
|
|
const formData = new FormData(this);
|
|
|
|
|
|
const inputs = {};
|
|
|
|
|
|
for (const [key, value] of formData.entries()) {
|
|
|
|
|
|
if (value !== '') {
|
2025-12-04 16:23:27 +09:00
|
|
|
|
// 숫자로 변환 가능하면 숫자로, 아니면 문자열로 유지
|
|
|
|
|
|
const numValue = parseFloat(value);
|
|
|
|
|
|
inputs[key] = isNaN(numValue) ? value : numValue;
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UI 상태 변경
|
|
|
|
|
|
document.getElementById('runButton').disabled = true;
|
|
|
|
|
|
document.getElementById('runSpinner').classList.remove('hidden');
|
|
|
|
|
|
document.getElementById('resultEmpty').classList.add('hidden');
|
2025-12-23 23:41:37 +09:00
|
|
|
|
document.getElementById('itemsEmpty').classList.add('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
document.getElementById('resultLoading').classList.remove('hidden');
|
2025-12-23 23:41:37 +09:00
|
|
|
|
document.getElementById('calculatedVariables').classList.add('hidden');
|
|
|
|
|
|
document.getElementById('generatedItems').classList.add('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/formulas/simulate', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
|
|
|
|
},
|
2025-12-04 15:23:59 +09:00
|
|
|
|
body: JSON.stringify({ input_variables: inputs })
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('resultLoading').classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
if (response.ok && result.success) {
|
2025-12-04 15:23:59 +09:00
|
|
|
|
renderResults(result.data, result.has_errors);
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
errorDiv.textContent = result.message || '수식 실행에 실패했습니다.';
|
|
|
|
|
|
errorDiv.classList.remove('hidden');
|
|
|
|
|
|
document.getElementById('resultEmpty').classList.remove('hidden');
|
2025-12-23 23:41:37 +09:00
|
|
|
|
document.getElementById('itemsEmpty').classList.remove('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('수식 실행 오류:', err);
|
|
|
|
|
|
document.getElementById('resultLoading').classList.add('hidden');
|
|
|
|
|
|
errorDiv.textContent = '서버 오류가 발생했습니다.';
|
|
|
|
|
|
errorDiv.classList.remove('hidden');
|
|
|
|
|
|
document.getElementById('resultEmpty').classList.remove('hidden');
|
2025-12-23 23:41:37 +09:00
|
|
|
|
document.getElementById('itemsEmpty').classList.remove('hidden');
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
} finally {
|
|
|
|
|
|
document.getElementById('runButton').disabled = false;
|
|
|
|
|
|
document.getElementById('runSpinner').classList.add('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 결과 렌더링
|
2025-12-04 15:23:59 +09:00
|
|
|
|
function renderResults(data, hasErrors = false) {
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
// 계산된 변수
|
|
|
|
|
|
const variablesContainer = document.getElementById('calculatedVariables');
|
|
|
|
|
|
const variables = data.variables || {};
|
|
|
|
|
|
const variableKeys = Object.keys(variables);
|
|
|
|
|
|
|
|
|
|
|
|
if (variableKeys.length === 0) {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
variablesContainer.innerHTML = '<p class="text-sm text-gray-500 text-center py-4">계산된 변수가 없습니다.</p>';
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
} else {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
// 경고 배너 (일부 오류가 있는 경우)
|
|
|
|
|
|
let warningHtml = '';
|
|
|
|
|
|
if (hasErrors) {
|
|
|
|
|
|
warningHtml = `
|
|
|
|
|
|
<div class="mb-3 p-2 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-700">
|
|
|
|
|
|
⚠️ 일부 수식에서 오류가 발생했습니다.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
variablesContainer.innerHTML = warningHtml + variableKeys.map(key => {
|
2025-12-04 15:23:59 +09:00
|
|
|
|
const varInfo = variables[key];
|
|
|
|
|
|
// varInfo가 객체면 value 추출, 아니면 직접 사용
|
|
|
|
|
|
let rawValue = varInfo && typeof varInfo === 'object' ? varInfo.value : varInfo;
|
|
|
|
|
|
const varName = varInfo && typeof varInfo === 'object' ? varInfo.name : '';
|
|
|
|
|
|
|
|
|
|
|
|
// JSON 문자열인 경우 파싱 시도
|
|
|
|
|
|
if (typeof rawValue === 'string' && rawValue.startsWith('{')) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
rawValue = JSON.parse(rawValue);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 파싱 실패 시 원본 유지
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 값 포맷팅
|
|
|
|
|
|
let formattedValue;
|
|
|
|
|
|
if (rawValue === null || rawValue === undefined) {
|
|
|
|
|
|
formattedValue = '<span class="text-gray-400">null</span>';
|
|
|
|
|
|
} else if (typeof rawValue === 'object') {
|
|
|
|
|
|
// Range 결과 등 객체인 경우 - value 필드 우선, 없으면 전체 표시
|
|
|
|
|
|
if (rawValue.value) {
|
|
|
|
|
|
formattedValue = `<span title="${rawValue.note || ''}">${rawValue.value}</span>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
formattedValue = JSON.stringify(rawValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (typeof rawValue === 'number') {
|
|
|
|
|
|
formattedValue = rawValue.toLocaleString();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
formattedValue = rawValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
return `
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div class="flex justify-between items-center py-1.5 px-2 bg-gray-50 rounded text-sm">
|
|
|
|
|
|
<div class="truncate mr-2">
|
2025-12-04 15:23:59 +09:00
|
|
|
|
<span class="font-mono text-gray-700">${key}</span>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
${varName ? `<span class="text-gray-400 text-xs ml-1">${varName}</span>` : ''}
|
2025-12-04 15:23:59 +09:00
|
|
|
|
</div>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<span class="font-semibold text-blue-600 whitespace-nowrap">${formattedValue}</span>
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
variablesContainer.classList.remove('hidden');
|
|
|
|
|
|
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
// 생성된 품목
|
|
|
|
|
|
const itemsContainer = document.getElementById('generatedItems');
|
|
|
|
|
|
const items = data.items || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (items.length === 0) {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
itemsContainer.innerHTML = '<p class="text-sm text-gray-500 text-center py-4">생성된 품목이 없습니다.</p>';
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
} else {
|
2025-12-23 23:41:37 +09:00
|
|
|
|
itemsContainer.innerHTML = items.map(item => renderItemCard(item)).join('');
|
|
|
|
|
|
|
|
|
|
|
|
// 트리 토글 이벤트 바인딩
|
|
|
|
|
|
itemsContainer.querySelectorAll('.bom-toggle').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', function(e) {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const targetId = this.dataset.target;
|
|
|
|
|
|
const targetEl = document.getElementById(targetId);
|
|
|
|
|
|
const icon = this.querySelector('svg');
|
|
|
|
|
|
|
|
|
|
|
|
if (targetEl.classList.contains('hidden')) {
|
|
|
|
|
|
targetEl.classList.remove('hidden');
|
|
|
|
|
|
icon.classList.add('rotate-90');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
targetEl.classList.add('hidden');
|
|
|
|
|
|
icon.classList.remove('rotate-90');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
itemsContainer.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
// 생성된 품목 개수 표시 및 탭 전환
|
|
|
|
|
|
const generatedCount = document.getElementById('generatedCount');
|
|
|
|
|
|
generatedCount.textContent = items.length;
|
|
|
|
|
|
generatedCount.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
// 생성된 품목 탭으로 전환
|
|
|
|
|
|
switchTab('generated');
|
|
|
|
|
|
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
// 오류 처리
|
|
|
|
|
|
const errors = data.errors || [];
|
|
|
|
|
|
const errorsContainer = document.getElementById('resultErrors');
|
|
|
|
|
|
const errorList = document.getElementById('errorList');
|
|
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
|
errorList.innerHTML = errors.map(err => `
|
2025-12-23 23:41:37 +09:00
|
|
|
|
<div class="py-1.5 px-2 bg-red-50 rounded text-sm text-red-700">
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
${err}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
errorsContainer.classList.remove('hidden');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
errorsContainer.classList.add('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 23:41:37 +09:00
|
|
|
|
// 품목 유형별 배지 색상
|
|
|
|
|
|
const itemTypeBadgeColors = {
|
|
|
|
|
|
'FG': 'bg-purple-100 text-purple-700', // 완제품
|
|
|
|
|
|
'PT': 'bg-blue-100 text-blue-700', // 부품
|
|
|
|
|
|
'SM': 'bg-green-100 text-green-700', // 부자재
|
|
|
|
|
|
'RM': 'bg-yellow-100 text-yellow-700', // 원자재
|
|
|
|
|
|
'CS': 'bg-gray-100 text-gray-600' // 소모품
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let itemIdCounter = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 카드 렌더링
|
|
|
|
|
|
function renderItemCard(item) {
|
|
|
|
|
|
const itemId = `item_${itemIdCounter++}`;
|
|
|
|
|
|
const hasBom = item.has_bom && item.bom_children && item.bom_children.length > 0;
|
|
|
|
|
|
const badgeColor = itemTypeBadgeColors[item.item_type] || 'bg-gray-100 text-gray-600';
|
|
|
|
|
|
|
|
|
|
|
|
// 가격 정보 포맷
|
|
|
|
|
|
const unitPrice = item.unit_price ? item.unit_price.toLocaleString() : '-';
|
|
|
|
|
|
const totalPrice = item.total_price ? item.total_price.toLocaleString() : '-';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
|
|
|
<!-- 품목 헤더 -->
|
|
|
|
|
|
<div class="p-2.5 bg-gradient-to-r from-green-50 to-white">
|
|
|
|
|
|
<div class="flex items-start justify-between">
|
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
|
<div class="flex items-center gap-2 mb-1">
|
|
|
|
|
|
${hasBom ? `
|
|
|
|
|
|
<button type="button" class="bom-toggle p-0.5 hover:bg-gray-200 rounded transition-colors" data-target="${itemId}_bom">
|
|
|
|
|
|
<svg class="w-4 h-4 text-gray-500 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : '<span class="w-5"></span>'}
|
|
|
|
|
|
<span class="px-1.5 py-0.5 ${badgeColor} text-xs font-medium rounded">
|
|
|
|
|
|
${item.item_type_label || item.item_type || '미등록'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="font-mono text-xs text-gray-500">${item.item_code}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ml-5 text-sm font-medium text-gray-800 truncate" title="${item.item_name}">
|
|
|
|
|
|
${item.item_name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${item.description ? `<div class="ml-5 text-xs text-gray-500 truncate mt-0.5" title="${item.description}">${item.description}</div>` : ''}
|
|
|
|
|
|
${item.specification ? `<div class="ml-5 text-xs text-gray-400 mt-0.5">${item.specification}</div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-right flex-shrink-0 ml-3">
|
|
|
|
|
|
<div class="text-sm font-semibold text-green-600">
|
|
|
|
|
|
${item.quantity || 0} <span class="text-gray-500 font-normal">${item.unit || 'EA'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs text-gray-500 mt-0.5">
|
|
|
|
|
|
@${unitPrice}원
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-xs font-medium text-gray-700">
|
|
|
|
|
|
= ${totalPrice}원
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- BOM 트리 (있는 경우) -->
|
|
|
|
|
|
${hasBom ? `
|
|
|
|
|
|
<div id="${itemId}_bom" class="hidden border-t border-gray-100 bg-gray-50/50">
|
|
|
|
|
|
<div class="p-2 pl-6">
|
|
|
|
|
|
<div class="text-xs font-medium text-gray-500 mb-1.5 flex items-center gap-1">
|
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
BOM 구성 (${item.bom_children.length}개 하위 품목)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${renderBomTree(item.bom_children, item.quantity || 1, 0)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 트리 재귀 렌더링
|
|
|
|
|
|
function renderBomTree(children, parentQty = 1, depth = 0) {
|
|
|
|
|
|
if (!children || children.length === 0) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const maxDepth = 5; // 최대 깊이 제한
|
|
|
|
|
|
if (depth >= maxDepth) return '<div class="text-xs text-gray-400 ml-4">...</div>';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="space-y-1 ${depth > 0 ? 'ml-4 pl-2 border-l border-gray-200' : ''}">
|
|
|
|
|
|
${children.map(child => {
|
|
|
|
|
|
const childBadgeColor = itemTypeBadgeColors[child.item_type] || 'bg-gray-100 text-gray-600';
|
|
|
|
|
|
const calculatedQty = (child.quantity || 1) * parentQty;
|
|
|
|
|
|
const hasChildren = child.has_bom && child.children && child.children.length > 0;
|
|
|
|
|
|
const childId = `bom_${itemIdCounter++}`;
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="text-xs">
|
|
|
|
|
|
<div class="flex items-center gap-1.5 py-1 px-1.5 bg-white rounded hover:bg-gray-50">
|
|
|
|
|
|
${hasChildren ? `
|
|
|
|
|
|
<button type="button" class="bom-toggle p-0.5 hover:bg-gray-200 rounded" data-target="${childId}">
|
|
|
|
|
|
<svg class="w-3 h-3 text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
` : '<span class="w-4"></span>'}
|
|
|
|
|
|
<span class="px-1 py-0.5 ${childBadgeColor} text-[10px] rounded">
|
|
|
|
|
|
${child.item_type_label || child.item_type || '-'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="font-mono text-gray-400">${child.code}</span>
|
|
|
|
|
|
<span class="text-gray-700 truncate flex-1">${child.name}</span>
|
|
|
|
|
|
<span class="text-gray-500 whitespace-nowrap">
|
|
|
|
|
|
${child.quantity || 1} × ${parentQty} =
|
|
|
|
|
|
<span class="font-medium text-gray-700">${calculatedQty}</span>
|
|
|
|
|
|
${child.unit || 'EA'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${hasChildren ? `
|
|
|
|
|
|
<div id="${childId}" class="hidden">
|
|
|
|
|
|
${renderBomTree(child.children, calculatedQty, depth + 1)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 15:45:54 +09:00
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// BOM 시뮬레이션 기능
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// 모드 전환 설정
|
|
|
|
|
|
function setupModeTabs() {
|
|
|
|
|
|
document.querySelectorAll('.mode-tab').forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', function() {
|
|
|
|
|
|
const mode = this.dataset.mode;
|
|
|
|
|
|
switchMode(mode);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 모드 전환
|
|
|
|
|
|
function switchMode(mode) {
|
|
|
|
|
|
const formulaSection = document.getElementById('formulaSection');
|
|
|
|
|
|
const bomSection = document.getElementById('bomSection');
|
|
|
|
|
|
const modeFormula = document.getElementById('modeFormula');
|
|
|
|
|
|
const modeBom = document.getElementById('modeBom');
|
|
|
|
|
|
|
|
|
|
|
|
if (mode === 'formula') {
|
|
|
|
|
|
formulaSection.classList.remove('hidden');
|
|
|
|
|
|
bomSection.classList.add('hidden');
|
|
|
|
|
|
modeFormula.classList.add('text-blue-600', 'border-b-2', 'border-blue-600', 'bg-blue-50');
|
|
|
|
|
|
modeFormula.classList.remove('text-gray-500');
|
|
|
|
|
|
modeBom.classList.remove('text-green-600', 'border-b-2', 'border-green-600', 'bg-green-50');
|
|
|
|
|
|
modeBom.classList.add('text-gray-500');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
formulaSection.classList.add('hidden');
|
|
|
|
|
|
bomSection.classList.remove('hidden');
|
|
|
|
|
|
modeBom.classList.add('text-green-600', 'border-b-2', 'border-green-600', 'bg-green-50');
|
|
|
|
|
|
modeBom.classList.remove('text-gray-500');
|
|
|
|
|
|
modeFormula.classList.remove('text-blue-600', 'border-b-2', 'border-blue-600', 'bg-blue-50');
|
|
|
|
|
|
modeFormula.classList.add('text-gray-500');
|
|
|
|
|
|
|
2025-12-30 17:27:01 +09:00
|
|
|
|
// FG 목록 로드 및 카테고리 필터 설정
|
2025-12-24 15:45:54 +09:00
|
|
|
|
loadFinishedGoods();
|
2025-12-30 17:27:01 +09:00
|
|
|
|
setupCategoryFilter();
|
2025-12-24 15:45:54 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 17:27:01 +09:00
|
|
|
|
// 완제품 목록 저장 (카테고리 필터링용)
|
|
|
|
|
|
let allFinishedGoods = [];
|
|
|
|
|
|
|
2025-12-24 15:45:54 +09:00
|
|
|
|
// 완제품 목록 로드
|
|
|
|
|
|
async function loadFinishedGoods() {
|
|
|
|
|
|
const select = document.getElementById('fgCodeSelect');
|
2025-12-30 17:27:01 +09:00
|
|
|
|
if (allFinishedGoods.length > 0) {
|
|
|
|
|
|
// 이미 로드됨 - 필터링만 적용
|
|
|
|
|
|
filterFinishedGoods();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-24 15:45:54 +09:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/formulas/items?item_type=FG', {
|
|
|
|
|
|
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data && result.data.items) {
|
2025-12-30 17:27:01 +09:00
|
|
|
|
allFinishedGoods = result.data.items;
|
|
|
|
|
|
filterFinishedGoods();
|
2025-12-24 15:45:54 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('완제품 목록 로드 실패:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 17:27:01 +09:00
|
|
|
|
// 카테고리별 완제품 필터링
|
|
|
|
|
|
function filterFinishedGoods() {
|
|
|
|
|
|
const select = document.getElementById('fgCodeSelect');
|
|
|
|
|
|
const categorySelect = document.getElementById('productCategory');
|
|
|
|
|
|
const selectedCategory = categorySelect ? categorySelect.value : '';
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 옵션 초기화
|
|
|
|
|
|
select.innerHTML = '<option value="">제품을 선택하세요...</option>';
|
|
|
|
|
|
|
|
|
|
|
|
// 필터링
|
|
|
|
|
|
const filtered = selectedCategory
|
|
|
|
|
|
? allFinishedGoods.filter(item => {
|
|
|
|
|
|
const itemCategory = (item.item_category || '').toUpperCase();
|
|
|
|
|
|
return itemCategory === selectedCategory.toUpperCase();
|
|
|
|
|
|
})
|
|
|
|
|
|
: allFinishedGoods;
|
|
|
|
|
|
|
|
|
|
|
|
// 옵션 추가
|
|
|
|
|
|
filtered.forEach(item => {
|
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
|
option.value = item.code;
|
|
|
|
|
|
option.dataset.category = item.item_category || '';
|
|
|
|
|
|
option.textContent = item.name; // 제품명만 표시
|
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 변경 이벤트
|
|
|
|
|
|
function setupCategoryFilter() {
|
|
|
|
|
|
const categorySelect = document.getElementById('productCategory');
|
|
|
|
|
|
if (categorySelect) {
|
|
|
|
|
|
categorySelect.addEventListener('change', filterFinishedGoods);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 15:45:54 +09:00
|
|
|
|
// BOM 시뮬레이션 폼 제출
|
|
|
|
|
|
document.getElementById('bomSimulatorForm').addEventListener('submit', async function(e) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
const formData = new FormData(this);
|
|
|
|
|
|
const fgCode = formData.get('finished_goods_code');
|
|
|
|
|
|
|
|
|
|
|
|
if (!fgCode) {
|
|
|
|
|
|
alert('완제품을 선택하세요.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const inputVars = {
|
|
|
|
|
|
W0: parseFloat(formData.get('W0')) || 2000,
|
2025-12-30 17:27:01 +09:00
|
|
|
|
H0: parseFloat(formData.get('H0')) || 2500,
|
|
|
|
|
|
QTY: parseInt(formData.get('QTY')) || 1,
|
|
|
|
|
|
// 새 변수들 (React 동기화)
|
|
|
|
|
|
PC: formData.get('PC') || '',
|
|
|
|
|
|
GT: formData.get('GT') || 'wall',
|
|
|
|
|
|
MP: formData.get('MP') || 'single',
|
|
|
|
|
|
CT: formData.get('CT') || 'basic',
|
|
|
|
|
|
WS: parseInt(formData.get('WS')) || 50,
|
|
|
|
|
|
INSP: parseInt(formData.get('INSP')) || 50000
|
2025-12-24 15:45:54 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// UI 상태 변경
|
|
|
|
|
|
document.getElementById('runBomButton').disabled = true;
|
|
|
|
|
|
document.getElementById('bomSpinner').classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/admin/quote-formulas/formulas/simulate-bom', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
finished_goods_code: fgCode,
|
|
|
|
|
|
input_variables: inputVars
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
renderBomResults(result.data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
alert(result.message || 'BOM 계산에 실패했습니다.');
|
|
|
|
|
|
resetBomResults();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('BOM 계산 오류:', err);
|
|
|
|
|
|
alert('서버 오류가 발생했습니다.');
|
|
|
|
|
|
resetBomResults();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
document.getElementById('runBomButton').disabled = false;
|
|
|
|
|
|
document.getElementById('bomSpinner').classList.add('hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 결과 초기화
|
|
|
|
|
|
function resetBomResults() {
|
|
|
|
|
|
document.getElementById('debugSteps').innerHTML = '<p class="text-gray-400 text-center py-4">BOM 계산을 실행하세요</p>';
|
|
|
|
|
|
document.getElementById('processGroups').innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>';
|
|
|
|
|
|
document.getElementById('costSummary').innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">BOM 계산을 실행하세요</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BOM 결과 렌더링
|
|
|
|
|
|
function renderBomResults(data) {
|
|
|
|
|
|
renderDebugSteps(data.debug_steps || []);
|
|
|
|
|
|
renderProcessGroups(data.grouped_items || {});
|
|
|
|
|
|
renderCostSummary(data.subtotals || {}, data.grand_total || 0, data.finished_goods || {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 디버그 스텝 아이콘
|
|
|
|
|
|
const stepIcons = {
|
|
|
|
|
|
1: '📥', 2: '🔢', 3: '📦', 4: '🌳', 5: '💵',
|
|
|
|
|
|
6: '✖️', 7: '💰', 8: '🏭', 9: '📊', 10: '🎯'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 디버깅 패널 렌더링
|
|
|
|
|
|
function renderDebugSteps(steps) {
|
|
|
|
|
|
const container = document.getElementById('debugSteps');
|
|
|
|
|
|
|
|
|
|
|
|
if (!steps || steps.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4">디버깅 정보가 없습니다.</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = steps.map(step => {
|
|
|
|
|
|
const icon = stepIcons[step.step] || '•';
|
|
|
|
|
|
const statusColor = step.status === 'success' ? 'text-green-600' : step.status === 'error' ? 'text-red-600' : 'text-blue-600';
|
|
|
|
|
|
const bgColor = step.status === 'success' ? 'bg-green-50 border-green-200' : step.status === 'error' ? 'bg-red-50 border-red-200' : 'bg-blue-50 border-blue-200';
|
|
|
|
|
|
|
|
|
|
|
|
// 상세 정보 렌더링
|
|
|
|
|
|
let detailHtml = '';
|
|
|
|
|
|
if (step.details) {
|
|
|
|
|
|
if (typeof step.details === 'object') {
|
|
|
|
|
|
const entries = Object.entries(step.details);
|
|
|
|
|
|
if (entries.length > 0) {
|
|
|
|
|
|
detailHtml = `
|
|
|
|
|
|
<div class="mt-1 pl-6 text-[10px] text-gray-500">
|
|
|
|
|
|
${entries.slice(0, 5).map(([k, v]) => {
|
|
|
|
|
|
const val = typeof v === 'object' ? JSON.stringify(v) : v;
|
|
|
|
|
|
return `<div><span class="text-gray-400">${k}:</span> ${val}</div>`;
|
|
|
|
|
|
}).join('')}
|
|
|
|
|
|
${entries.length > 5 ? `<div class="text-gray-400">... +${entries.length - 5}개</div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
detailHtml = `<div class="mt-1 pl-6 text-[10px] text-gray-500">${step.details}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="p-2 rounded border ${bgColor}">
|
|
|
|
|
|
<div class="flex items-start gap-2">
|
|
|
|
|
|
<span class="text-sm">${icon}</span>
|
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<span class="font-medium ${statusColor}">Step ${step.step}</span>
|
|
|
|
|
|
<span class="text-gray-600">${step.name || ''}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-gray-500 mt-0.5">${step.message || ''}</div>
|
|
|
|
|
|
${detailHtml}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 공정명 한글화
|
|
|
|
|
|
const processLabels = {
|
|
|
|
|
|
'screen': '🎬 스크린 공정',
|
|
|
|
|
|
'bending': '🔧 절곡 공정',
|
|
|
|
|
|
'steel': '🔩 철재 공정',
|
|
|
|
|
|
'electric': '⚡ 전기 공정',
|
|
|
|
|
|
'assembly': '🔨 조립 공정',
|
|
|
|
|
|
'unknown': '❓ 기타'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 공정별 그룹 렌더링
|
|
|
|
|
|
function renderProcessGroups(groupedItems) {
|
|
|
|
|
|
const container = document.getElementById('processGroups');
|
|
|
|
|
|
|
|
|
|
|
|
const processKeys = Object.keys(groupedItems);
|
|
|
|
|
|
if (processKeys.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4 text-xs">그룹화된 품목이 없습니다.</p>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = processKeys.map(processType => {
|
|
|
|
|
|
const group = groupedItems[processType];
|
|
|
|
|
|
const items = group.items || [];
|
|
|
|
|
|
const subtotal = group.subtotal || 0;
|
|
|
|
|
|
const label = processLabels[processType] || processType;
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
|
|
|
<div class="px-3 py-2 bg-gray-100 flex items-center justify-between">
|
|
|
|
|
|
<span class="font-medium text-gray-700 text-xs">${label}</span>
|
|
|
|
|
|
<span class="text-xs text-gray-500">${items.length}개 품목</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="p-2 space-y-1 text-xs">
|
|
|
|
|
|
${items.slice(0, 8).map(item => renderProcessItem(item)).join('')}
|
|
|
|
|
|
${items.length > 8 ? `<div class="text-gray-400 text-center py-1">... +${items.length - 8}개 더</div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="px-3 py-1.5 bg-gray-50 text-right border-t">
|
|
|
|
|
|
<span class="text-xs text-gray-500">소계:</span>
|
|
|
|
|
|
<span class="font-medium text-gray-700 ml-1">${subtotal.toLocaleString()}원</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 공정별 품목 아이템 렌더링
|
|
|
|
|
|
function renderProcessItem(item) {
|
2025-12-24 16:22:17 +09:00
|
|
|
|
const price = item.total_price || item.amount || 0;
|
2025-12-24 15:45:54 +09:00
|
|
|
|
const qty = item.quantity || 0;
|
|
|
|
|
|
const categoryNote = item.calculation_note || '';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="flex items-center gap-2 py-1 px-2 bg-white rounded hover:bg-gray-50">
|
|
|
|
|
|
<span class="font-mono text-gray-400 truncate w-20">${item.item_code || ''}</span>
|
|
|
|
|
|
<span class="text-gray-700 truncate flex-1">${item.item_name || ''}</span>
|
|
|
|
|
|
<span class="text-gray-500 whitespace-nowrap">${qty} ${item.unit || 'EA'}</span>
|
|
|
|
|
|
<span class="text-green-600 font-medium whitespace-nowrap">${price.toLocaleString()}원</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 원가 요약 렌더링
|
|
|
|
|
|
function renderCostSummary(subtotals, grandTotal, finishedGoods) {
|
|
|
|
|
|
const container = document.getElementById('costSummary');
|
|
|
|
|
|
|
|
|
|
|
|
// 완제품 정보
|
|
|
|
|
|
let fgHtml = '';
|
|
|
|
|
|
if (finishedGoods && finishedGoods.code) {
|
|
|
|
|
|
fgHtml = `
|
|
|
|
|
|
<div class="p-3 bg-purple-50 rounded-lg border border-purple-200 mb-3">
|
|
|
|
|
|
<div class="text-xs text-purple-600 font-medium">완제품</div>
|
|
|
|
|
|
<div class="font-medium text-gray-800">${finishedGoods.code}</div>
|
|
|
|
|
|
<div class="text-sm text-gray-600">${finishedGoods.name || ''}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 공정별 소계
|
|
|
|
|
|
const subtotalEntries = Object.entries(subtotals);
|
|
|
|
|
|
let subtotalsHtml = '';
|
|
|
|
|
|
if (subtotalEntries.length > 0) {
|
|
|
|
|
|
subtotalsHtml = `
|
|
|
|
|
|
<div class="space-y-2 mb-4">
|
|
|
|
|
|
<div class="text-xs font-medium text-gray-500 uppercase">공정별 소계</div>
|
2025-12-24 16:22:17 +09:00
|
|
|
|
${subtotalEntries.map(([process, data]) => {
|
2025-12-24 15:45:54 +09:00
|
|
|
|
const label = processLabels[process] || process;
|
2025-12-24 16:22:17 +09:00
|
|
|
|
// data는 객체 {name, count, subtotal} 또는 숫자일 수 있음
|
|
|
|
|
|
const amount = typeof data === 'object' ? (data.subtotal || 0) : data;
|
2025-12-24 15:45:54 +09:00
|
|
|
|
return `
|
|
|
|
|
|
<div class="flex justify-between items-center py-1.5 px-2 bg-gray-50 rounded">
|
|
|
|
|
|
<span class="text-gray-600">${label}</span>
|
|
|
|
|
|
<span class="font-medium text-gray-700">${amount.toLocaleString()}원</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 총합계
|
|
|
|
|
|
const totalHtml = `
|
|
|
|
|
|
<div class="p-3 bg-green-50 rounded-lg border border-green-200">
|
|
|
|
|
|
<div class="flex justify-between items-center">
|
|
|
|
|
|
<span class="font-medium text-gray-700">총 원가</span>
|
|
|
|
|
|
<span class="text-xl font-bold text-green-600">${grandTotal.toLocaleString()}원</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = fgHtml + subtotalsHtml + totalHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
// 초기화 실행
|
|
|
|
|
|
init();
|
2025-12-24 15:45:54 +09:00
|
|
|
|
setupModeTabs();
|
feat(quote-formulas): 견적수식 관리 기능 구현
## 구현 내용
### 모델 (5개)
- QuoteFormulaCategory: 수식 카테고리
- QuoteFormula: 수식 정의 (input/calculation/range/mapping)
- QuoteFormulaRange: 범위별 값 정의
- QuoteFormulaMapping: 매핑 테이블
- QuoteFormulaItem: 수식-품목 연결
### 서비스 (3개)
- QuoteFormulaCategoryService: 카테고리 CRUD
- QuoteFormulaService: 수식 CRUD, 복제, 재정렬
- FormulaEvaluatorService: 수식 계산 엔진
- 지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF, AND, OR, NOT
### API Controller (2개)
- QuoteFormulaCategoryController: 카테고리 API (11개 엔드포인트)
- QuoteFormulaController: 수식 API (16개 엔드포인트)
### FormRequest (4개)
- Store/Update QuoteFormulaCategoryRequest
- Store/Update QuoteFormulaRequest
### Blade Views (8개)
- 수식 목록/추가/수정/시뮬레이터
- 카테고리 목록/추가/수정
- HTMX 테이블 partial
### 라우트
- API: 27개 엔드포인트
- Web: 7개 라우트
2025-12-04 14:00:24 +09:00
|
|
|
|
</script>
|
2025-12-23 23:41:37 +09:00
|
|
|
|
@endpush
|