diff --git a/app/Services/FlowTester/ApiLogCapturer.php b/app/Services/FlowTester/ApiLogCapturer.php index 6335cc30..0161b2ae 100644 --- a/app/Services/FlowTester/ApiLogCapturer.php +++ b/app/Services/FlowTester/ApiLogCapturer.php @@ -192,4 +192,4 @@ private function decodeUnicodeEscapes(string $str): string return $str; } -} \ No newline at end of file +} diff --git a/docs/QUOTE_FORMULA_SEED_PLAN.md b/docs/QUOTE_FORMULA_SEED_PLAN.md new file mode 100644 index 00000000..578d98da --- /dev/null +++ b/docs/QUOTE_FORMULA_SEED_PLAN.md @@ -0,0 +1,253 @@ +# 견적수식 시드 데이터 구현 계획 + +## 📋 개요 + +`design/src/components/utils/formulaSampleData.ts`에 정의된 샘플 데이터를 +MNG의 `quote_formulas` 테이블에 시드(Seed)하여 관리할 수 있도록 구현합니다. + +--- + +## 🔍 분석 결과 + +### 소스 데이터 구조 (formulaSampleData.ts) + +| 데이터 종류 | 개수 | 설명 | +|------------|------|------| +| **itemMasters** | 21개 | 품목 마스터 (제품, 가이드레일, 케이스, 모터, 제어기 등) | +| **pricings** | 20개 | 품목별 판매/구매 단가 | +| **formulaRules** | 26개 | 수식 규칙 (계산식, 범위, 매핑) | +| **categoryGroups** | 11개 | 수식 카테고리 | + +### 수식 규칙 상세 (26개) + +| 카테고리 | 규칙 수 | 유형 | 설명 | +|----------|---------|------|------| +| 오픈사이즈 | 2 | formula | W0, H0 입력값 | +| 제작사이즈 | 4 | formula | W1, H1 (스크린/철재별) | +| 면적 | 1 | formula | W1 * H1 / 1000000 | +| 중량 | 2 | formula | 스크린/철재별 중량 계산 | +| 가이드레일 | 5 | formula/range | 길이, 자재선택, 설치유형별 수량 | +| 케이스 | 3 | formula/range | 사이즈, 자재 자동선택 | +| 모터 | 1 | range | 중량 기반 자동선택 | +| 제어기 | 1 | mapping | 유형별 자동선택 | +| 마구리 | 1 | formula | 날개 수량 계산 | +| 검사 | 1 | formula | 검사비 고정 | +| 단가수식 | 5 | formula | 품목별 단가 계산 | + +### 카테고리 그룹 (11개) + +``` +1. 오픈사이즈 6. 케이스 +2. 제작사이즈 7. 모터 +3. 면적 8. 제어기 +4. 중량 9. 마구리 +5. 가이드레일 10. 검사 + 11. 단가수식 +``` + +--- + +## 📊 데이터 매핑 + +### formulaSampleData → quote_formula_categories + +``` +categoryGroups → quote_formula_categories +├── id → id +├── name → name +├── name (uppercase) → code +├── description → description +├── order → sort_order +└── is_active: true +``` + +### formulaSampleData → quote_formulas + +``` +formulaRules → quote_formulas +├── ruleCode → variable +├── ruleName → name +├── category → category_id (FK) +├── ruleType → type (input/calculation/range/mapping) +├── inputVariable → (참조용 description에 포함) +├── formula/outputFormula → formula +├── unit → (metadata JSON) +├── description → description +├── status → is_active +└── ranges[] → quote_formula_ranges +``` + +### formulaSampleData → quote_formula_ranges + +``` +ranges[] → quote_formula_ranges +├── minValue → min_value +├── maxValue → max_value +├── result → result_value +├── itemCode → item_code +├── quantity → quantity +└── description → description +``` + +--- + +## 🛠️ 구현 계획 + +### Phase 1: Seeder 생성 (api/) + +**⚠️ 중요: MNG_CRITICAL_RULES에 따라 api/에서 Seeder 생성** + +| 순서 | 작업 | 예상 시간 | +|------|------|----------| +| 1.1 | QuoteFormulaCategorySeeder 생성 | 10분 | +| 1.2 | QuoteFormulaSeeder 생성 | 30분 | +| 1.3 | QuoteFormulaRangeSeeder 생성 | 15분 | +| 1.4 | DatabaseSeeder에 등록 | 5분 | + +### Phase 2: MNG에서 Seeder 참조 및 실행 + +| 순서 | 작업 | 예상 시간 | +|------|------|----------| +| 2.1 | mng에서 api Seeder 실행 방법 구현 | 15분 | +| 2.2 | 데이터 검증 및 테스트 | 10분 | + +### Phase 3: MNG 관리 UI 개선 (선택) + +| 순서 | 작업 | 예상 시간 | +|------|------|----------| +| 3.1 | 범위(Range) 관리 UI 추가 | 40분 | +| 3.2 | 매핑(Mapping) 관리 UI 추가 | 40분 | +| 3.3 | 시뮬레이터 연동 테스트 | 20분 | + +--- + +## 📝 상세 구현 + +### 1.1 QuoteFormulaCategorySeeder + +```php +// api/database/seeders/QuoteFormulaCategorySeeder.php + +$categories = [ + ['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'description' => '제품의 설치 오픈 사이즈 (W0, H0)', 'sort_order' => 1], + ['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'description' => '실제 제작 사이즈 (W1, H1)', 'sort_order' => 2], + ['code' => 'AREA', 'name' => '면적', 'description' => '제품 면적 계산 (㎡)', 'sort_order' => 3], + ['code' => 'WEIGHT', 'name' => '중량', 'description' => '제품 중량 계산 (kg)', 'sort_order' => 4], + ['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'description' => '가이드레일 자동 선택 및 수량 계산', 'sort_order' => 5], + ['code' => 'CASE', 'name' => '케이스', 'description' => '케이스(셔터박스) 자동 선택', 'sort_order' => 6], + ['code' => 'MOTOR', 'name' => '모터', 'description' => '개폐전동기 자동 선택', 'sort_order' => 7], + ['code' => 'CONTROLLER', 'name' => '제어기', 'description' => '연동제어기 자동 선택', 'sort_order' => 8], + ['code' => 'EDGE_WING', 'name' => '마구리', 'description' => '마구리 날개 수량 계산', 'sort_order' => 9], + ['code' => 'INSPECTION', 'name' => '검사', 'description' => '제품 검사비', 'sort_order' => 10], + ['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'description' => '품목별 단가 계산 수식', 'sort_order' => 11], +]; +``` + +### 1.2 QuoteFormulaSeeder (일부 예시) + +```php +// api/database/seeders/QuoteFormulaSeeder.php + +$formulas = [ + // 오픈사이즈 + [ + 'category_code' => 'OPEN_SIZE', + 'variable' => 'W0', + 'name' => '오픈사이즈 W0 (가로)', + 'type' => 'input', + 'formula' => null, + 'description' => '자동 견적 산출 섹션의 오픈사이즈 W0 입력값', + 'metadata' => ['unit' => 'mm'], + ], + [ + 'category_code' => 'OPEN_SIZE', + 'variable' => 'H0', + 'name' => '오픈사이즈 H0 (세로)', + 'type' => 'input', + 'formula' => null, + 'description' => '자동 견적 산출 섹션의 오픈사이즈 H0 입력값', + 'metadata' => ['unit' => 'mm'], + ], + + // 제작사이즈 + [ + 'category_code' => 'MAKE_SIZE', + 'variable' => 'W1_SCREEN', + 'name' => '제작사이즈 W1 (스크린)', + 'type' => 'calculation', + 'formula' => 'W0 + 140', + 'description' => '스크린 제작 가로 = 오픈 가로 + 140', + 'metadata' => ['unit' => 'mm', 'product_type' => 'screen'], + ], + + // 범위 타입 예시 + [ + 'category_code' => 'GUIDE_RAIL', + 'variable' => 'GR_AUTO_SELECT', + 'name' => '가이드레일 자재 자동 선택', + 'type' => 'range', + 'formula' => null, + 'description' => '가이드레일 길이 및 수량 자동 산출 (기본 2개)', + 'metadata' => ['unit' => 'EA'], + 'ranges' => [ + ['min' => 1219, 'max' => 2438, 'result' => '2438 2개', 'quantity' => 2], + ['min' => 2438, 'max' => 3000, 'result' => '3000 2개', 'quantity' => 2], + ], + ], +]; +``` + +--- + +## 📋 실행 순서 + +```bash +# 1. api/ 디렉토리에서 Seeder 생성 +cd ../api +php artisan make:seeder QuoteFormulaCategorySeeder +php artisan make:seeder QuoteFormulaSeeder + +# 2. Seeder 코드 작성 후 실행 +php artisan db:seed --class=QuoteFormulaCategorySeeder +php artisan db:seed --class=QuoteFormulaSeeder + +# 3. mng/에서 데이터 확인 +cd ../mng +php artisan tinker +>>> \App\Models\Quote\QuoteFormulaCategory::count() +>>> \App\Models\Quote\QuoteFormula::count() +``` + +--- + +## ⚠️ 주의사항 + +1. **tenant_id**: Seeder 실행 시 기본 tenant_id 설정 필요 +2. **중복 방지**: 이미 데이터가 있으면 건너뛰도록 처리 +3. **외래키**: category_id는 카테고리 먼저 생성 후 참조 +4. **트랜잭션**: 전체 Seeder를 트랜잭션으로 감싸기 + +--- + +## 📊 예상 결과 + +| 테이블 | 레코드 수 | +|--------|----------| +| quote_formula_categories | 11 | +| quote_formulas | 26 | +| quote_formula_ranges | ~10 | +| quote_formula_mappings | ~5 | + +--- + +## 🔄 향후 확장 + +1. **Excel Import**: 사용자가 Excel로 수식 일괄 등록 +2. **버전 관리**: 수식 변경 이력 추적 +3. **테스트 케이스**: 시뮬레이터에서 자동 테스트 +4. **API 연동**: React 프론트엔드에서 수식 사용 + +--- + +**작성일**: 2025-12-04 +**예상 소요 시간**: 2-3시간 (Phase 1-2) diff --git a/resources/views/archived-records/restore-check.blade.php b/resources/views/archived-records/restore-check.blade.php index 3a654461..d4927979 100644 --- a/resources/views/archived-records/restore-check.blade.php +++ b/resources/views/archived-records/restore-check.blade.php @@ -156,10 +156,10 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-g 취소 @if($restoreCheck['can_restore']) -
@endif -@endsection \ No newline at end of file +@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/boards/edit.blade.php b/resources/views/boards/edit.blade.php index e7afa620..54a7a591 100644 --- a/resources/views/boards/edit.blade.php +++ b/resources/views/boards/edit.blade.php @@ -345,7 +345,7 @@ class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg"> const data = await response.json(); if (response.ok && data.success) { - alert('게시판이 수정되었습니다.'); + showToast('게시판이 수정되었습니다.', 'success'); window.location.reload(); } else { errorDiv.textContent = data.message || '게시판 수정에 실패했습니다.'; @@ -415,7 +415,7 @@ function removeFieldRow(button) { if (container.querySelectorAll('.field-row').length > 1) { row.remove(); } else { - alert('최소 1개의 필드는 필요합니다.'); + showToast('최소 1개의 필드는 필요합니다.', 'warning'); } } @@ -457,7 +457,7 @@ function closeFieldModal() { } } } catch (error) { - alert('필드 정보를 불러오는데 실패했습니다.'); + showToast('필드 정보를 불러오는데 실패했습니다.', 'error'); } } @@ -468,27 +468,28 @@ function closeFieldEditModal() { // 필드 삭제 async function deleteField(fieldId, fieldName) { - if (!confirm(`"${fieldName}" 필드를 삭제하시겠습니까?`)) return; + showDeleteConfirm(fieldName + ' 필드', async () => { + try { + const response = await fetch(`/api/admin/boards/${boardId}/fields/${fieldId}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json' + } + }); - try { - const response = await fetch(`/api/admin/boards/${boardId}/fields/${fieldId}`, { - method: 'DELETE', - headers: { - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - 'Accept': 'application/json' + const data = await response.json(); + + if (data.success) { + showToast('필드가 삭제되었습니다.', 'success'); + window.location.reload(); + } else { + showToast(data.message || '필드 삭제에 실패했습니다.', 'error'); } - }); - - const data = await response.json(); - - if (data.success) { - window.location.reload(); - } else { - alert(data.message || '필드 삭제에 실패했습니다.'); + } catch (error) { + showToast('서버 오류가 발생했습니다.', 'error'); } - } catch (error) { - alert('서버 오류가 발생했습니다.'); - } + }); } // 필드 추가 폼 제출 (다중) @@ -515,7 +516,7 @@ function closeFieldEditModal() { }); if (fields.length === 0) { - alert('최소 1개의 필드를 입력해주세요.'); + showToast('최소 1개의 필드를 입력해주세요.', 'warning'); return; } @@ -548,7 +549,7 @@ function closeFieldEditModal() { } if (errorMessages.length > 0) { - alert(`${successCount}개 저장 완료, ${errorMessages.length}개 실패\n\n${errorMessages.join('\n')}`); + showToast(`${successCount}개 저장 완료, ${errorMessages.length}개 실패`, 'warning'); } if (successCount > 0) { @@ -583,13 +584,14 @@ function closeFieldEditModal() { const data = await response.json(); if (data.success) { + showToast('필드가 수정되었습니다.', 'success'); closeFieldEditModal(); window.location.reload(); } else { - alert(data.message || '필드 저장에 실패했습니다.'); + showToast(data.message || '필드 저장에 실패했습니다.', 'error'); } } catch (error) { - alert('서버 오류가 발생했습니다.'); + showToast('서버 오류가 발생했습니다.', 'error'); } }); diff --git a/resources/views/boards/index.blade.php b/resources/views/boards/index.blade.php index 88b50c09..cb30b705 100644 --- a/resources/views/boards/index.blade.php +++ b/resources/views/boards/index.blade.php @@ -88,7 +88,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> // 삭제 확인 window.confirmDelete = function(id, name) { - if (confirm(`"${name}" 게시판을 삭제하시겠습니까?`)) { + showDeleteConfirm(name, () => { htmx.ajax('DELETE', `/api/admin/boards/${id}`, { target: '#board-table', swap: 'none', @@ -98,12 +98,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#board-table', 'filterSubmit'); }); - } + }); }; // 복원 확인 window.confirmRestore = function(id, name) { - if (confirm(`"${name}" 게시판을 복원하시겠습니까?`)) { + showConfirm(`"${name}" 게시판을 복원하시겠습니까?`, () => { htmx.ajax('POST', `/api/admin/boards/${id}/restore`, { target: '#board-table', swap: 'none', @@ -113,12 +113,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#board-table', 'filterSubmit'); }); - } + }, { title: '복원 확인', icon: 'question' }); }; // 영구삭제 확인 window.confirmForceDelete = function(id, name) { - if (confirm(`"${name}" 게시판을 영구 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다!`)) { + showPermanentDeleteConfirm(name, () => { htmx.ajax('DELETE', `/api/admin/boards/${id}/force`, { target: '#board-table', swap: 'none', @@ -128,7 +128,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#board-table', 'filterSubmit'); }); - } + }); }; // 활성/비활성 토글 diff --git a/resources/views/daily-logs/index.blade.php b/resources/views/daily-logs/index.blade.php index 82fbec01..30b61fc4 100644 --- a/resources/views/daily-logs/index.blade.php +++ b/resources/views/daily-logs/index.blade.php @@ -565,18 +565,18 @@ function reindexEntries() { closeModal(); htmx.trigger('#log-table', 'filterSubmit'); } else { - alert(result.message || '오류가 발생했습니다.'); + showToast(result.message || '오류가 발생했습니다.', 'error'); } }) .catch(err => { console.error(err); - alert('오류가 발생했습니다.'); + showToast('오류가 발생했습니다.', 'error'); }); }); // 삭제 확인 function confirmDelete(id, date) { - if (confirm(`"${date}" 일일 로그를 삭제하시겠습니까?`)) { + showDeleteConfirm(`"${date}" 일일 로그`, () => { fetch(`/api/admin/daily-logs/${id}`, { method: 'DELETE', headers: { @@ -589,12 +589,12 @@ function confirmDelete(id, date) { htmx.trigger('#log-table', 'filterSubmit'); } }); - } + }); } // 복원 확인 function confirmRestore(id, date) { - if (confirm(`"${date}" 일일 로그를 복원하시겠습니까?`)) { + showConfirm(`"${date}" 일일 로그를 복원하시겠습니까?`, () => { fetch(`/api/admin/daily-logs/${id}/restore`, { method: 'POST', headers: { @@ -607,12 +607,12 @@ function confirmRestore(id, date) { htmx.trigger('#log-table', 'filterSubmit'); } }); - } + }, { title: '복원 확인', icon: 'question' }); } // 영구삭제 확인 function confirmForceDelete(id, date) { - if (confirm(`"${date}" 일일 로그를 영구 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다!`)) { + showPermanentDeleteConfirm(`"${date}" 일일 로그`, () => { fetch(`/api/admin/daily-logs/${id}/force`, { method: 'DELETE', headers: { @@ -625,7 +625,7 @@ function confirmForceDelete(id, date) { htmx.trigger('#log-table', 'filterSubmit'); } }); - } + }); } // ======================================== @@ -656,7 +656,7 @@ function scrollToTableRow(logId) { }, 300); } else { // 테이블에 해당 로그가 없는 경우 (필터링 등으로 인해) - alert('해당 로그가 현재 목록에 표시되지 않습니다.\n필터를 확인해주세요.'); + showToast('해당 로그가 현재 목록에 표시되지 않습니다. 필터를 확인해주세요.', 'warning'); } } @@ -817,7 +817,7 @@ function updateTableEntryStatus(logId, entryId, status) { // 테이블 항목 삭제 function deleteTableEntry(logId, entryId) { - if (confirm('이 항목을 삭제하시겠습니까?')) { + showConfirm('이 항목을 삭제하시겠습니까?', () => { fetch(`/api/admin/daily-logs/entries/${entryId}`, { method: 'DELETE', headers: { @@ -830,7 +830,7 @@ function deleteTableEntry(logId, entryId) { loadTableAccordionContent(logId); } }); - } + }, { title: '항목 삭제', icon: 'warning' }); } // 테이블 빠른 항목 추가 - 미완료 항목 수정 모달 사용 @@ -1128,7 +1128,7 @@ function submitPendingEditEntries(event) { const inputField = document.getElementById('pendingEditAssigneeNameInput'); assigneeName = inputField.value.trim(); if (!assigneeName) { - alert('담당자 이름을 입력해주세요.'); + showToast('담당자 이름을 입력해주세요.', 'warning'); inputField.focus(); return; } @@ -1206,7 +1206,7 @@ function submitPendingEditEntries(event) { }) .catch(err => { console.error(err); - alert('저장 중 오류가 발생했습니다.'); + showToast('저장 중 오류가 발생했습니다.', 'error'); }) .finally(() => { submitBtn.disabled = false; diff --git a/resources/views/daily-logs/partials/table.blade.php b/resources/views/daily-logs/partials/table.blade.php index db4e19aa..ec7c3ebc 100644 --- a/resources/views/daily-logs/partials/table.blade.php +++ b/resources/views/daily-logs/partials/table.blade.php @@ -331,7 +331,7 @@ function updateCardEntryStatus(logId, entryId, status) { } function deleteCardEntry(logId, entryId) { - if (confirm('이 항목을 삭제하시겠습니까?')) { + showConfirm('이 항목을 삭제하시겠습니까?', () => { fetch(`/api/admin/daily-logs/entries/${entryId}`, { method: 'DELETE', headers: { @@ -344,7 +344,7 @@ function deleteCardEntry(logId, entryId) { loadCardAccordionContent(logId); } }); - } + }, { title: '항목 삭제', icon: 'warning' }); } function openQuickAddCardEntry(logId) { @@ -352,7 +352,7 @@ function openQuickAddCardEntry(logId) { if (typeof openQuickAddModal === 'function') { openQuickAddModal(logId); } else { - alert('모달을 열 수 없습니다. 페이지를 새로고침해주세요.'); + showToast('모달을 열 수 없습니다. 페이지를 새로고침해주세요.', 'warning'); } } diff --git a/resources/views/daily-logs/show.blade.php b/resources/views/daily-logs/show.blade.php index f802effe..97eebdc7 100644 --- a/resources/views/daily-logs/show.blade.php +++ b/resources/views/daily-logs/show.blade.php @@ -241,7 +241,7 @@ function updateStatus(entryId, status) { // 항목 삭제 function confirmDeleteEntry(entryId) { - if (confirm('이 항목을 삭제하시겠습니까?')) { + showConfirm('이 항목을 삭제하시겠습니까?', () => { fetch(`/api/admin/daily-logs/entries/${entryId}`, { method: 'DELETE', headers: { @@ -254,7 +254,7 @@ function confirmDeleteEntry(entryId) { location.reload(); } }); - } + }, { title: '항목 삭제', icon: 'warning' }); } // 항목 추가 모달 @@ -298,7 +298,7 @@ function updateNewAssigneeOptions() { if (result.success) { location.reload(); } else { - alert(result.message || '오류가 발생했습니다.'); + showToast(result.message || '오류가 발생했습니다.', 'error'); } }); }); diff --git a/resources/views/departments/create.blade.php b/resources/views/departments/create.blade.php index 53c256ba..7b148fe1 100644 --- a/resources/views/departments/create.blade.php +++ b/resources/views/departments/create.blade.php @@ -109,10 +109,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" if (event.detail.target.id === 'departmentForm') { const response = JSON.parse(event.detail.xhr.response); if (response.success) { - alert(response.message); + showToast(response.message, 'success'); window.location.href = response.redirect; } else { - alert('오류: ' + (response.message || '부서 생성에 실패했습니다.')); + showToast(response.message || '부서 생성에 실패했습니다.', 'error'); } } }); @@ -121,13 +121,13 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" document.body.addEventListener('htmx:responseError', function(event) { if (event.detail.xhr.status === 422) { const errors = JSON.parse(event.detail.xhr.response).errors; - let errorMsg = '입력 오류:\n'; + let errorMsg = '입력 오류: '; for (let field in errors) { - errorMsg += '- ' + errors[field].join('\n') + '\n'; + errorMsg += errors[field].join(', ') + ' '; } - alert(errorMsg); + showToast(errorMsg, 'error'); } else { - alert('서버 오류가 발생했습니다.'); + showToast('서버 오류가 발생했습니다.', 'error'); } }); diff --git a/resources/views/departments/edit.blade.php b/resources/views/departments/edit.blade.php index 28ee66a5..f96e26a6 100644 --- a/resources/views/departments/edit.blade.php +++ b/resources/views/departments/edit.blade.php @@ -141,10 +141,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" if (event.detail.target.id === 'departmentForm') { const response = JSON.parse(event.detail.xhr.response); if (response.success) { - alert(response.message); + showToast(response.message, 'success'); window.location.href = response.redirect; } else { - alert('오류: ' + (response.message || '부서 수정에 실패했습니다.')); + showToast(response.message || '부서 수정에 실패했습니다.', 'error'); } } }); @@ -153,13 +153,13 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition" document.body.addEventListener('htmx:responseError', function(event) { if (event.detail.xhr.status === 422) { const errors = JSON.parse(event.detail.xhr.response).errors; - let errorMsg = '입력 오류:\n'; + let errorMsg = '입력 오류: '; for (let field in errors) { - errorMsg += '- ' + errors[field].join('\n') + '\n'; + errorMsg += errors[field].join(', ') + ' '; } - alert(errorMsg); + showToast(errorMsg, 'error'); } else { - alert('서버 오류가 발생했습니다.'); + showToast('서버 오류가 발생했습니다.', 'error'); } }); diff --git a/resources/views/departments/index.blade.php b/resources/views/departments/index.blade.php index 36fe03c1..c23bc1c7 100644 --- a/resources/views/departments/index.blade.php +++ b/resources/views/departments/index.blade.php @@ -82,7 +82,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> // 삭제 확인 window.confirmDelete = function(id, name) { - if (confirm(`"${name}" 부서를 삭제하시겠습니까?\n\n하위 부서가 있으면 삭제할 수 없습니다.`)) { + showDeleteConfirm(name, () => { htmx.ajax('DELETE', `/api/admin/departments/${id}`, { target: '#department-table', swap: 'none', @@ -92,12 +92,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#department-table', 'filterSubmit'); }); - } + }); }; // 복원 확인 window.confirmRestore = function(id, name) { - if (confirm(`"${name}" 부서를 복원하시겠습니까?`)) { + showConfirm(`"${name}" 부서를 복원하시겠습니까?`, () => { htmx.ajax('POST', `/api/admin/departments/${id}/restore`, { target: '#department-table', swap: 'none', @@ -107,12 +107,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#department-table', 'filterSubmit'); }); - } + }, { title: '복원 확인', icon: 'question' }); }; // 영구 삭제 확인 window.confirmForceDelete = function(id, name) { - if (confirm(`"${name}" 부서를 영구 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다!`)) { + showPermanentDeleteConfirm(name, () => { htmx.ajax('DELETE', `/api/admin/departments/${id}/force`, { target: '#department-table', swap: 'none', @@ -122,7 +122,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden"> }).then(() => { htmx.trigger('#department-table', 'filterSubmit'); }); - } + }); }; @endpush \ No newline at end of file diff --git a/resources/views/dev-tools/flow-tester/create.blade.php b/resources/views/dev-tools/flow-tester/create.blade.php index e43d0633..2f0133c6 100644 --- a/resources/views/dev-tools/flow-tester/create.blade.php +++ b/resources/views/dev-tools/flow-tester/create.blade.php @@ -311,13 +311,13 @@ function showError(errors) { if (!isValidated) { e.preventDefault(); - alert('먼저 JSON을 검증해주세요.'); + showToast('먼저 JSON을 검증해주세요.', 'warning'); return; } if (!nameInput.value.trim()) { e.preventDefault(); - alert('이름을 입력해주세요.'); + showToast('이름을 입력해주세요.', 'warning'); nameInput.focus(); return; } diff --git a/resources/views/dev-tools/flow-tester/edit.blade.php b/resources/views/dev-tools/flow-tester/edit.blade.php index 6ab5260a..3e8b3674 100644 --- a/resources/views/dev-tools/flow-tester/edit.blade.php +++ b/resources/views/dev-tools/flow-tester/edit.blade.php @@ -177,7 +177,7 @@ function formatJson() { const json = JSON.parse(textarea.value); textarea.value = JSON.stringify(json, null, 2); } catch (e) { - alert('JSON 파싱 오류: ' + e.message); + showToast('JSON 파싱 오류: ' + e.message, 'error'); } } @@ -212,32 +212,32 @@ function validateJson() { } }) .catch(error => { - alert('검증 오류: ' + error.message); + showToast('검증 오류: ' + error.message, 'error'); }); } function runFlow(id) { - if (!confirm('이 플로우를 실행하시겠습니까?')) return; - - fetch(`/dev-tools/flow-tester/${id}/run`, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - 'Content-Type': 'application/json', - }, - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - alert(data.message); - location.reload(); - } else { - alert('실행 실패: ' + (data.message || '알 수 없는 오류')); - } - }) - .catch(error => { - alert('오류 발생: ' + error.message); - }); + showConfirm('이 플로우를 실행하시겠습니까?', () => { + fetch(`/dev-tools/flow-tester/${id}/run`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + }, + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(data.message, 'success'); + location.reload(); + } else { + showToast('실행 실패: ' + (data.message || '알 수 없는 오류'), 'error'); + } + }) + .catch(error => { + showToast('오류 발생: ' + error.message, 'error'); + }); + }, { title: '플로우 실행', icon: 'question' }); } @endpush diff --git a/resources/views/dev-tools/flow-tester/index.blade.php b/resources/views/dev-tools/flow-tester/index.blade.php index cbbb0ca9..38a76209 100644 --- a/resources/views/dev-tools/flow-tester/index.blade.php +++ b/resources/views/dev-tools/flow-tester/index.blade.php @@ -451,8 +451,12 @@ function toggleFlowDetail(id, event) { } function runFlow(id) { - if (!confirm('이 플로우를 실행하시겠습니까?')) return; + showConfirm('이 플로우를 실행하시겠습니까?', () => { + executeRunFlow(id); + }, { title: '플로우 실행', icon: 'question' }); + } + function executeRunFlow(id) { // 실행 버튼을 로딩 상태로 변경 const btn = document.querySelector(`button[onclick="runFlow(${id})"]`); const originalHtml = btn.innerHTML; @@ -481,7 +485,7 @@ function runFlow(id) { .catch(error => { btn.disabled = false; btn.innerHTML = originalHtml; - alert('오류 발생: ' + error.message); + showToast('오류 발생: ' + error.message, 'error'); }); } @@ -632,26 +636,26 @@ function getApiLogSummary(apiLogs) { } function confirmDelete(id, name) { - if (!confirm(`"${name}" 플로우를 삭제하시겠습니까?`)) return; - - fetch(`/dev-tools/flow-tester/${id}`, { - method: 'DELETE', - headers: { - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - 'Accept': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - location.reload(); - } else { - alert(data.message || '삭제 실패'); - } - }) - .catch(error => { - alert('오류 발생: ' + error.message); + showDeleteConfirm(`"${name}" 플로우`, () => { + fetch(`/dev-tools/flow-tester/${id}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + }, + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } else { + showToast(data.message || '삭제 실패', 'error'); + } + }) + .catch(error => { + showToast('오류 발생: ' + error.message, 'error'); + }); }); } diff --git a/resources/views/dev-tools/flow-tester/partials/example-flows.blade.php b/resources/views/dev-tools/flow-tester/partials/example-flows.blade.php index 97c8abb2..c45150b8 100644 --- a/resources/views/dev-tools/flow-tester/partials/example-flows.blade.php +++ b/resources/views/dev-tools/flow-tester/partials/example-flows.blade.php @@ -866,7 +866,7 @@ class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition- copy(exampleId) { const json = JSON.stringify(ExampleFlows[exampleId], null, 2); navigator.clipboard.writeText(json).then(() => { - alert('JSON이 클립보드에 복사되었습니다.'); + showToast('JSON이 클립보드에 복사되었습니다.', 'success'); }).catch(err => { console.error('복사 실패:', err); }); diff --git a/resources/views/dev-tools/flow-tester/partials/guide-modal.blade.php b/resources/views/dev-tools/flow-tester/partials/guide-modal.blade.php index 9e6c87a9..7a88b891 100644 --- a/resources/views/dev-tools/flow-tester/partials/guide-modal.blade.php +++ b/resources/views/dev-tools/flow-tester/partials/guide-modal.blade.php @@ -532,7 +532,7 @@ class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition- function copyPromptTemplate() { const text = document.getElementById('prompt-template').textContent; navigator.clipboard.writeText(text).then(() => { - alert('프롬프트 템플릿이 복사되었습니다.'); + showToast('프롬프트 템플릿이 복사되었습니다.', 'success'); }).catch(err => { console.error('복사 실패:', err); }); @@ -542,7 +542,7 @@ function copyPromptTemplate() { function copyExamplePrompt() { const text = document.getElementById('example-prompt').textContent; navigator.clipboard.writeText(text).then(() => { - alert('예시 프롬프트가 복사되었습니다.'); + showToast('예시 프롬프트가 복사되었습니다.', 'success'); }).catch(err => { console.error('복사 실패:', err); }); diff --git a/resources/views/dev-tools/flow-tester/run-detail.blade.php b/resources/views/dev-tools/flow-tester/run-detail.blade.php index 423ce2d6..cbf2c8a4 100644 --- a/resources/views/dev-tools/flow-tester/run-detail.blade.php +++ b/resources/views/dev-tools/flow-tester/run-detail.blade.php @@ -487,45 +487,45 @@ function copyErrorForAI() { btn.classList.add('bg-purple-600', 'hover:bg-purple-700'); }, 2000); }).catch(err => { - alert('복사 실패: ' + err.message); + showToast('복사 실패: ' + err.message, 'error'); }); } function runFlow(id) { - if (!confirm('이 플로우를 다시 실행하시겠습니까?')) return; + showConfirm('이 플로우를 다시 실행하시겠습니까?', () => { + const btn = document.getElementById('run-btn'); + const originalHtml = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ` 실행 중...`; - const btn = document.getElementById('run-btn'); - const originalHtml = btn.innerHTML; - btn.disabled = true; - btn.innerHTML = ` 실행 중...`; - - fetch(`/dev-tools/flow-tester/${id}/run`, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': '{{ csrf_token() }}', - 'Content-Type': 'application/json', - }, - }) - .then(response => response.json()) - .then(data => { - // 새 실행 결과 페이지로 이동 - if (data.run_id) { - window.location.href = `/dev-tools/flow-tester/runs/${data.run_id}`; - } else { + fetch(`/dev-tools/flow-tester/${id}/run`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + }, + }) + .then(response => response.json()) + .then(data => { + // 새 실행 결과 페이지로 이동 + if (data.run_id) { + window.location.href = `/dev-tools/flow-tester/runs/${data.run_id}`; + } else { + btn.disabled = false; + btn.innerHTML = originalHtml; + showToast(data.message || '실행 완료', 'success'); + location.reload(); + } + }) + .catch(error => { btn.disabled = false; btn.innerHTML = originalHtml; - alert(data.message || '실행 완료'); - location.reload(); - } - }) - .catch(error => { - btn.disabled = false; - btn.innerHTML = originalHtml; - alert('오류 발생: ' + error.message); - }); + showToast('오류 발생: ' + error.message, 'error'); + }); + }, { title: '플로우 재실행', icon: 'question' }); } @endpush diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index bb1d3a6b..ddd28bd7 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -7,6 +7,8 @@