브라우저 alert/confirm을 SweetAlert2로 전환

- layouts/app.blade.php에 SweetAlert2 CDN 및 전역 헬퍼 함수 추가
  - showToast(): 토스트 알림 (success, error, warning, info)
  - showConfirm(): 확인 대화상자
  - showDeleteConfirm(): 삭제 확인 (경고 아이콘)
  - showPermanentDeleteConfirm(): 영구 삭제 확인 (빨간색 경고)
  - showSuccess(), showError(): 성공/에러 알림

- 변환된 파일 목록 (48개 Blade 파일):
  - menus/* (6개), boards/* (2개), posts/* (3개)
  - daily-logs/* (3개), project-management/* (6개)
  - dev-tools/flow-tester/* (6개)
  - quote-formulas/* (4개), permission-analyze/* (1개)
  - archived-records/* (1개), profile/* (1개)
  - roles/* (3개), permissions/* (3개)
  - departments/* (3개), tenants/* (3개), users/* (3개)

- 주요 개선사항:
  - Tailwind CSS 테마와 일관된 디자인
  - 비동기 콜백 패턴으로 리팩토링
  - 삭제/복원/영구삭제 각각 다른 스타일 적용
This commit is contained in:
2025-12-05 09:49:56 +09:00
parent f28a51bdf9
commit 5c892c1ed9
50 changed files with 891 additions and 446 deletions

View File

@@ -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)

View File

@@ -156,10 +156,10 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-g
취소
</a>
@if($restoreCheck['can_restore'])
<form action="{{ route('archived-records.restore', $batchInfo['batch_id']) }}" method="POST"
onsubmit="return confirm('정말로 이 데이터를 복원하시겠습니까?\n\n복원된 데이터는 새로운 ID가 할당되며, 아카이브 레코드는 삭제됩니다.');">
<form id="restoreForm" action="{{ route('archived-records.restore', $batchInfo['batch_id']) }}" method="POST">
@csrf
<button type="submit"
<button type="button"
onclick="confirmRestore()"
class="inline-flex items-center px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-medium text-sm rounded-lg transition">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -170,3 +170,13 @@ class="inline-flex items-center px-6 py-2 bg-green-600 hover:bg-green-700 text-w
@endif
</div>
@endsection
@push('scripts')
<script>
function confirmRestore() {
showConfirm('정말로 이 데이터를 복원하시겠습니까?\n\n복원된 데이터는 새로운 ID가 할당되며, 아카이브 레코드는 삭제됩니다.', () => {
document.getElementById('restoreForm').submit();
}, { title: '데이터 복원', icon: 'question' });
}
</script>
@endpush

View File

@@ -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');
}
});

View File

@@ -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');
});
}
});
};
// 활성/비활성 토글

View File

@@ -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;

View File

@@ -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');
}
}

View File

@@ -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');
}
});
});

View File

@@ -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');
}
});
</script>

View File

@@ -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');
}
});
</script>

View File

@@ -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');
});
}
});
};
</script>
@endpush

View File

@@ -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;
}

View File

@@ -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' });
}
</script>
@endpush

View File

@@ -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');
});
});
}
</script>

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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 = `<svg class="w-4 h-4 animate-spin" 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> 실행 중...`;
const btn = document.getElementById('run-btn');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<svg class="w-4 h-4 animate-spin" 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> 실행 중...`;
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' });
}
</script>
@endpush

View File

@@ -7,6 +7,8 @@
<title>@yield('title', 'Dashboard') - {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
@stack('styles')
</head>
<body class="bg-gray-100">
@@ -46,6 +48,171 @@
<script src="{{ asset('js/context-menu.js') }}"></script>
<script src="{{ asset('js/tenant-modal.js') }}"></script>
<script src="{{ asset('js/user-modal.js') }}"></script>
<!-- SweetAlert2 공통 함수 (Tailwind 테마) -->
<script>
// Tailwind 커스텀 클래스
const SwalTailwind = Swal.mixin({
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-gray-900 font-semibold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-blue-600 hover:bg-blue-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-blue-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
denyButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
actions: 'gap-3',
},
buttonsStyling: false,
});
// Toast (스낵바) - 우상단 알림
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
customClass: {
popup: 'rounded-lg shadow-lg',
},
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
/**
* 토스트 알림 표시
* @param {string} message - 메시지
* @param {string} type - 'success' | 'error' | 'warning' | 'info'
* @param {number} timer - 표시 시간 (ms), 기본 3000
*/
function showToast(message, type = 'info', timer = 3000) {
Toast.fire({
icon: type,
title: message,
timer: timer,
});
}
/**
* 확인 모달 표시
* @param {string} message - 확인 메시지
* @param {Function} onConfirm - 확인 시 콜백
* @param {Object} options - 추가 옵션
*/
function showConfirm(message, onConfirm, options = {}) {
const defaultOptions = {
title: options.title || '확인',
icon: options.icon || 'question',
confirmButtonText: options.confirmText || '확인',
cancelButtonText: options.cancelText || '취소',
showCancelButton: true,
reverseButtons: true,
};
SwalTailwind.fire({
...defaultOptions,
html: message,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 삭제 확인 모달 (위험 스타일)
* @param {string} itemName - 삭제할 항목명
* @param {Function} onConfirm - 확인 시 콜백
*/
function showDeleteConfirm(itemName, onConfirm) {
SwalTailwind.fire({
title: '삭제 확인',
html: `<span class="text-red-600 font-medium">"${itemName}"</span>을(를) 삭제하시겠습니까?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: '삭제',
cancelButtonText: '취소',
reverseButtons: true,
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-gray-900 font-semibold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
actions: 'gap-3',
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 영구 삭제 확인 모달 (매우 위험)
* @param {string} itemName - 삭제할 항목명
* @param {Function} onConfirm - 확인 시 콜백
*/
function showPermanentDeleteConfirm(itemName, onConfirm) {
SwalTailwind.fire({
title: '⚠️ 영구 삭제',
html: `<span class="text-red-600 font-bold">"${itemName}"</span>을(를) 영구 삭제하시겠습니까?<br><br><span class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다!</span>`,
icon: 'error',
showCancelButton: true,
confirmButtonText: '영구 삭제',
cancelButtonText: '취소',
reverseButtons: true,
customClass: {
popup: 'rounded-xl shadow-2xl border-0',
title: 'text-red-600 font-bold',
htmlContainer: 'text-gray-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-red-300',
cancelButton: 'bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium px-5 py-2.5 rounded-lg transition-colors focus:ring-4 focus:ring-gray-100',
actions: 'gap-3',
},
buttonsStyling: false,
}).then((result) => {
if (result.isConfirmed && typeof onConfirm === 'function') {
onConfirm();
}
});
}
/**
* 성공 알림 모달
* @param {string} message - 메시지
* @param {Function} onClose - 닫기 후 콜백 (선택)
*/
function showSuccess(message, onClose = null) {
SwalTailwind.fire({
title: '완료',
text: message,
icon: 'success',
confirmButtonText: '확인',
}).then(() => {
if (typeof onClose === 'function') {
onClose();
}
});
}
/**
* 에러 알림 모달
* @param {string} message - 에러 메시지
*/
function showError(message) {
SwalTailwind.fire({
title: '오류',
text: message,
icon: 'error',
confirmButtonText: '확인',
});
}
</script>
@stack('scripts')
</body>
</html>

View File

@@ -157,13 +157,13 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = result.redirect;
} else {
alert(result.message);
showToast(result.message, 'error');
}
} catch (error) {
alert('메뉴 생성 중 오류가 발생했습니다.');
showToast('메뉴 생성 중 오류가 발생했습니다.', 'error');
console.error(error);
}
});

View File

@@ -170,13 +170,13 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = result.redirect;
} else {
alert(result.message);
showToast(result.message, 'error');
}
} catch (error) {
alert('메뉴 수정 중 오류가 발생했습니다.');
showToast('메뉴 수정 중 오류가 발생했습니다.', 'error');
console.error(error);
}
});

View File

@@ -160,13 +160,13 @@ class="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transit
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = result.redirect;
} else {
alert(result.message);
showToast(result.message, 'error');
}
} catch (error) {
alert('글로벌 메뉴 생성 중 오류가 발생했습니다.');
showToast('글로벌 메뉴 생성 중 오류가 발생했습니다.', 'error');
console.error(error);
}
});

View File

@@ -173,13 +173,13 @@ class="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transit
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = result.redirect;
} else {
alert(result.message);
showToast(result.message, 'error');
}
} catch (error) {
alert('글로벌 메뉴 수정 중 오류가 발생했습니다.');
showToast('글로벌 메뉴 수정 중 오류가 발생했습니다.', 'error');
console.error(error);
}
});

View File

@@ -166,20 +166,20 @@ function saveGlobalMenuOrder(items) {
if (data.success) {
htmx.trigger('#menu-table', 'filterSubmit');
} else {
alert('순서 변경 실패: ' + (data.message || ''));
showToast('순서 변경 실패: ' + (data.message || ''), 'error');
htmx.trigger('#menu-table', 'filterSubmit');
}
})
.catch(error => {
console.error('Error:', error);
alert('순서 변경 중 오류 발생');
showToast('순서 변경 중 오류 발생', 'error');
htmx.trigger('#menu-table', 'filterSubmit');
});
}
// 삭제 확인
window.confirmDelete = function(id, name) {
if (confirm(`"${name}" 기본 메뉴를 삭제하시겠습니까?`)) {
showDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/global-menus/${id}`, {
target: '#menu-table',
swap: 'none',
@@ -189,12 +189,12 @@ function saveGlobalMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 기본 메뉴를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 기본 메뉴를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/global-menus/${id}/restore`, {
target: '#menu-table',
swap: 'none',
@@ -204,12 +204,12 @@ function saveGlobalMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`⚠️ 경고: "${name}" 기본 메뉴를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/global-menus/${id}/force`, {
target: '#menu-table',
swap: 'none',
@@ -219,7 +219,7 @@ function saveGlobalMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
});
};
// 활성 토글

View File

@@ -522,13 +522,13 @@ function moveMenu(menuId, newParentId, sortOrder) {
htmx.trigger('#menu-table', 'filterSubmit');
} else {
console.error('메뉴 이동 실패:', data.message);
alert('메뉴 이동 실패: ' + (data.message || ''));
showToast('메뉴 이동 실패: ' + (data.message || ''), 'error');
htmx.trigger('#menu-table', 'filterSubmit');
}
})
.catch(error => {
console.error('moveMenu API Error:', error);
alert('메뉴 이동 중 오류 발생');
showToast('메뉴 이동 중 오류 발생', 'error');
htmx.trigger('#menu-table', 'filterSubmit');
});
}
@@ -549,20 +549,20 @@ function saveMenuOrder(items) {
if (data.success) {
htmx.trigger('#menu-table', 'filterSubmit');
} else {
alert('순서 변경 실패: ' + (data.message || ''));
showToast('순서 변경 실패: ' + (data.message || ''), 'error');
htmx.trigger('#menu-table', 'filterSubmit');
}
})
.catch(error => {
console.error('Error:', error);
alert('순서 변경 중 오류 발생');
showToast('순서 변경 중 오류 발생', 'error');
htmx.trigger('#menu-table', 'filterSubmit');
});
}
// 삭제 확인
window.confirmDelete = function(id, name) {
if (confirm(`"${name}" 메뉴를 삭제하시겠습니까?`)) {
showDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/menus/${id}`, {
target: '#menu-table',
swap: 'none',
@@ -572,12 +572,12 @@ function saveMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 메뉴를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 메뉴를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/menus/${id}/restore`, {
target: '#menu-table',
swap: 'none',
@@ -587,12 +587,12 @@ function saveMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`⚠️ 경고: "${name}" 메뉴를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/menus/${id}/force`, {
target: '#menu-table',
swap: 'none',
@@ -602,7 +602,7 @@ function saveMenuOrder(items) {
}).then(() => {
htmx.trigger('#menu-table', 'filterSubmit');
});
}
});
};
// 활성 토글
@@ -750,36 +750,34 @@ function saveMenuOrder(items) {
const menuIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
if (menuIds.length === 0) {
alert('가져올 메뉴를 선택해주세요.');
showToast('가져올 메뉴를 선택해주세요.', 'warning');
return;
}
if (!confirm(`선택한 ${menuIds.length}개 메뉴를 가져오시겠습니까?`)) {
return;
}
fetch('/api/admin/menus/copy-from-global', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ menu_ids: menuIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.copied}개 메뉴가 복사되었습니다.`);
htmx.trigger('#menu-table', 'filterSubmit');
} else {
alert('가져오기 실패: ' + (data.message || ''));
}
})
.catch(error => {
console.error('Error:', error);
alert('가져오기 중 오류 발생');
});
showConfirm(`선택한 ${menuIds.length}개 메뉴를 가져오시겠습니까?`, () => {
fetch('/api/admin/menus/copy-from-global', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify({ menu_ids: menuIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(`${data.copied}개 메뉴가 복사되었습니다.`, 'success');
htmx.trigger('#menu-table', 'filterSubmit');
} else {
showToast('가져오기 실패: ' + (data.message || ''), 'error');
}
})
.catch(error => {
console.error('Error:', error);
showToast('가져오기 중 오류 발생', 'error');
});
}, { title: '메뉴 가져오기', icon: 'question' });
};
</script>

View File

@@ -12,7 +12,7 @@
class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-post="/api/admin/permission-analyze/recalculate"
hx-swap="none"
onclick="alert('권한이 재계산되었습니다.')"
onclick="showToast('권한이 재계산되었습니다.', 'success')"
>
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -278,7 +278,7 @@ function exportCsv() {
const permissionType = document.getElementById('permissionType').value;
if (!menuId) {
alert('메뉴를 선택해주세요.');
showToast('메뉴를 선택해주세요.', 'warning');
return;
}

View File

@@ -95,14 +95,14 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
showToast(data.message, 'success');
window.location.href = '{{ route("permissions.index") }}';
} else {
alert(data.message || '권한 생성에 실패했습니다.');
showToast(data.message || '권한 생성에 실패했습니다.', 'error');
}
})
.catch(error => {
alert('권한 생성 중 오류가 발생했습니다.');
showToast('권한 생성 중 오류가 발생했습니다.', 'error');
});
});
</script>

View File

@@ -115,12 +115,12 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
document.getElementById('loadingState').style.display = 'none';
document.getElementById('formContainer').style.display = 'block';
} else {
alert('권한 정보를 불러올 수 없습니다.');
showToast('권한 정보를 불러올 수 없습니다.', 'error');
window.location.href = '{{ route("permissions.index") }}';
}
})
.catch(error => {
alert('권한 정보 로드 중 오류가 발생했습니다.');
showToast('권한 정보 로드 중 오류가 발생했습니다.', 'error');
window.location.href = '{{ route("permissions.index") }}';
});
@@ -143,14 +143,14 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
showToast(data.message, 'success');
window.location.href = '{{ route("permissions.index") }}';
} else {
alert(data.message || '권한 수정에 실패했습니다.');
showToast(data.message || '권한 수정에 실패했습니다.', 'error');
}
})
.catch(error => {
alert('권한 수정 중 오류가 발생했습니다.');
showToast('권한 수정 중 오류가 발생했습니다.', 'error');
});
});
</script>

View File

@@ -63,7 +63,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
// 권한 삭제 확인
function confirmDelete(id, name) {
if (confirm(`"${name}" 권한을 삭제하시겠습니까?\n\n이 권한이 할당된 역할이 있는 경우 삭제할 수 없습니다.`)) {
showDeleteConfirm(name, () => {
fetch(`/api/admin/permissions/${id}`, {
method: 'DELETE',
headers: {
@@ -75,15 +75,15 @@ function confirmDelete(id, name) {
.then(data => {
if (data.success) {
htmx.trigger('#permission-table', 'filterSubmit');
alert(data.message);
showToast(data.message, 'success');
} else {
alert(data.message);
showToast(data.message, 'error');
}
})
.catch(error => {
alert('권한 삭제 중 오류가 발생했습니다.');
showToast('권한 삭제 중 오류가 발생했습니다.', 'error');
});
}
});
}
</script>
@endpush

View File

@@ -206,14 +206,14 @@ function displayFiles(files) {
fileList.innerHTML = '';
if (files.length > maxFiles) {
alert(`최대 ${maxFiles}개의 파일만 첨부할 수 있습니다.`);
showToast(`최대 ${maxFiles}개의 파일만 첨부할 수 있습니다.`, 'warning');
fileInput.value = '';
return;
}
Array.from(files).forEach((file, index) => {
if (file.size > maxSize) {
alert(`${file.name}: 파일 크기가 너무 큽니다.`);
showToast(`${file.name}: 파일 크기가 너무 큽니다.`, 'error');
return;
}

View File

@@ -248,14 +248,14 @@ function displayFiles(files) {
const currentExisting = document.querySelectorAll('#existing-files > div').length;
if (files.length + currentExisting > maxFiles) {
alert(`최대 ${maxFiles}개의 파일만 첨부할 수 있습니다. (기존 ${currentExisting}개 포함)`);
showToast(`최대 ${maxFiles}개의 파일만 첨부할 수 있습니다. (기존 ${currentExisting}개 포함)`, 'warning');
fileInput.value = '';
return;
}
Array.from(files).forEach((file, index) => {
if (file.size > maxSize) {
alert(`${file.name}: 파일 크기가 너무 큽니다.`);
showToast(`${file.name}: 파일 크기가 너무 큽니다.`, 'error');
return;
}
@@ -335,38 +335,39 @@ function formatFileSize(bytes) {
});
function deleteFile(fileId) {
if (!confirm('이 파일을 삭제하시겠습니까?')) return;
showConfirm('이 파일을 삭제하시겠습니까?', () => {
const url = '{{ route("boards.posts.files.delete", [$board, $post, ":fileId"]) }}'.replace(':fileId', fileId);
const url = '{{ route("boards.posts.files.delete", [$board, $post, ":fileId"]) }}'.replace(':fileId', fileId);
fetch(url, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const fileElement = document.querySelector(`[data-file-id="${fileId}"]`);
if (fileElement) {
fileElement.remove();
fetch(url, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
// 남은 파일이 없으면 섹션 숨김
const existingFiles = document.getElementById('existing-files');
if (existingFiles && existingFiles.children.length === 0) {
existingFiles.closest('.mb-6').remove();
})
.then(response => response.json())
.then(data => {
if (data.success) {
const fileElement = document.querySelector(`[data-file-id="${fileId}"]`);
if (fileElement) {
fileElement.remove();
}
// 남은 파일이 없으면 섹션 숨김
const existingFiles = document.getElementById('existing-files');
if (existingFiles && existingFiles.children.length === 0) {
existingFiles.closest('.mb-6').remove();
}
showToast('파일이 삭제되었습니다.', 'success');
} else {
showToast(data.message || '파일 삭제에 실패했습니다.', 'error');
}
} else {
alert(data.message || '파일 삭제에 실패했습니다.');
}
})
.catch(error => {
console.error('Error:', error);
alert('파일 삭제 중 오류가 발생했습니다.');
});
})
.catch(error => {
console.error('Error:', error);
showToast('파일 삭제 중 오류가 발생했습니다.', 'error');
});
}, { title: '파일 삭제', icon: 'warning' });
}
</script>
@endpush

View File

@@ -51,15 +51,21 @@
class="px-3 py-1 text-sm text-blue-600 hover:text-blue-800 border border-blue-300 rounded hover:bg-blue-50 transition">
수정
</a>
<form action="{{ route('boards.posts.destroy', [$board, $post]) }}" method="POST"
onsubmit="return confirm('정말 삭제하시겠습니까?');">
<form id="deletePostForm" action="{{ route('boards.posts.destroy', [$board, $post]) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit"
<button type="button" onclick="confirmDeletePost()"
class="px-3 py-1 text-sm text-red-600 hover:text-red-800 border border-red-300 rounded hover:bg-red-50 transition">
삭제
</button>
</form>
<script>
function confirmDeletePost() {
showDeleteConfirm('이 게시글', () => {
document.getElementById('deletePostForm').submit();
});
}
</script>
</div>
@endif
</div>

View File

@@ -176,21 +176,21 @@ class="px-6 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transit
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
showToast(data.message, 'success');
} else {
alert('오류: ' + (data.message || '프로필 수정에 실패했습니다.'));
showToast(data.message || '프로필 수정에 실패했습니다.', 'error');
}
})
.catch(error => {
console.error(error);
if (error.errors) {
let errorMsg = '입력 오류:\n';
let errorMsg = '입력 오류: ';
for (let field in error.errors) {
errorMsg += '- ' + error.errors[field].join('\n') + '\n';
errorMsg += error.errors[field].join(', ') + ' ';
}
alert(errorMsg);
showToast(errorMsg, 'error');
} else {
alert('서버 오류가 발생했습니다.');
showToast('서버 오류가 발생했습니다.', 'error');
}
})
.finally(() => {
@@ -223,22 +223,22 @@ class="px-6 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transit
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
showToast(data.message, 'success');
this.reset(); // 폼 초기화
} else {
alert('오류: ' + (data.message || '비밀번호 변경에 실패했습니다.'));
showToast(data.message || '비밀번호 변경에 실패했습니다.', 'error');
}
})
.catch(error => {
console.error(error);
if (error.errors) {
let errorMsg = '입력 오류:\n';
let errorMsg = '입력 오류: ';
for (let field in error.errors) {
errorMsg += '- ' + error.errors[field].join('\n') + '\n';
errorMsg += error.errors[field].join(', ') + ' ';
}
alert(errorMsg);
showToast(errorMsg, 'error');
} else {
alert('서버 오류가 발생했습니다.');
showToast('서버 오류가 발생했습니다.', 'error');
}
})
.finally(() => {

View File

@@ -865,7 +865,7 @@ function closeEditEntryModal() {
} catch (error) {
console.error('Error:', error);
alert(error.message || '오류가 발생했습니다. 다시 시도해주세요.');
showToast(error.message || '오류가 발생했습니다. 다시 시도해주세요.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '저장';
@@ -890,40 +890,38 @@ function closeEditEntryModal() {
if (response.ok && data.success) {
window.location.reload();
} else {
alert(data.message || '상태 변경에 실패했습니다.');
showToast(data.message || '상태 변경에 실패했습니다.', 'error');
}
} catch (error) {
console.error('Error:', error);
alert('오류가 발생했습니다.');
showToast('오류가 발생했습니다.', 'error');
}
}
// 빠른 삭제 (모달 없이 바로 삭제)
async function quickDeleteEntry(entryId) {
if (!confirm('이 항목을 삭제하시겠습니까?')) {
return;
}
showConfirm('이 항목을 삭제하시겠습니까?', async () => {
try {
const response = await fetch(`/api/admin/daily-logs/entries/${entryId}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
}
});
try {
const response = await fetch(`/api/admin/daily-logs/entries/${entryId}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
const data = await response.json();
if (response.ok && data.success) {
window.location.reload();
} else {
showToast(data.message || '항목 삭제에 실패했습니다.', 'error');
}
});
const data = await response.json();
if (response.ok && data.success) {
window.location.reload();
} else {
alert(data.message || '항목 삭제에 실패했습니다.');
} catch (error) {
console.error('Error:', error);
showToast('오류가 발생했습니다.', 'error');
}
} catch (error) {
console.error('Error:', error);
alert('오류가 발생했습니다.');
}
}, { title: '항목 삭제', icon: 'warning' });
}
</script>
@endpush

View File

@@ -114,14 +114,14 @@ class="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg transiti
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = '{{ route('pm.projects.index') }}';
} else {
alert(result.message || '저장에 실패했습니다.');
showToast(result.message || '저장에 실패했습니다.', 'error');
}
} catch (error) {
console.error('Error:', error);
alert('저장 중 오류가 발생했습니다.');
showToast('저장 중 오류가 발생했습니다.', 'error');
}
});
</script>

View File

@@ -115,14 +115,14 @@ class="bg-gray-300 hover:bg-gray-400 text-gray-700 px-6 py-2 rounded-lg transiti
const result = await response.json();
if (result.success) {
alert(result.message);
showToast(result.message, 'success');
window.location.href = '{{ route('pm.projects.show', $project->id) }}';
} else {
alert(result.message || '저장에 실패했습니다.');
showToast(result.message || '저장에 실패했습니다.', 'error');
}
} catch (error) {
console.error('Error:', error);
alert('저장 중 오류가 발생했습니다.');
showToast('저장 중 오류가 발생했습니다.', 'error');
}
});
</script>

View File

@@ -94,7 +94,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/pm/projects/${id}`, {
target: '#project-table',
swap: 'none',
@@ -102,12 +102,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#project-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 프로젝트를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 프로젝트를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/pm/projects/${id}/restore`, {
target: '#project-table',
swap: 'none',
@@ -115,12 +115,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#project-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`"${name}" 프로젝트를 영구 삭제하시겠습니까?\n\n⚠ 이 작업은 되돌릴 수 없으며, 관련된 모든 작업과 이슈도 삭제됩니다!`)) {
showPermanentDeleteConfirm(`"${name}" 프로젝트`, () => {
htmx.ajax('DELETE', `/api/admin/pm/projects/${id}/force`, {
target: '#project-table',
swap: 'none',
@@ -128,7 +128,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#project-table', 'filterSubmit');
});
}
});
};
// 복제 확인

View File

@@ -1012,7 +1012,7 @@ function closeTaskModal() {
closeTaskModal();
loadTasks();
} else {
alert(result.message || '저장에 실패했습니다.');
showToast(result.message || '저장에 실패했습니다.', 'error');
}
});
@@ -1084,7 +1084,7 @@ function openIssueModalForTask(taskId) {
loadIssues();
loadTasks(); // 작업 탭 아코디언도 업데이트
} else {
alert(result.message || '저장에 실패했습니다.');
showToast(result.message || '저장에 실패했습니다.', 'error');
}
});
@@ -1104,15 +1104,21 @@ function getSelectedIssueIds() {
if (!action || ids.length === 0) {
select.value = '';
if (action) alert('선택된 작업이 없습니다.');
if (action) showToast('선택된 작업이 없습니다.', 'warning');
return;
}
if (action === 'delete' && !confirm(`${ids.length}개 작업을 삭제하시겠습니까?`)) {
select.value = '';
if (action === 'delete') {
showConfirm(`${ids.length}개 작업을 삭제하시겠습니까?`, async () => {
await executeBulkTaskAction(select, ids, action, value);
}, { title: '일괄 삭제', icon: 'warning' });
return;
}
await executeBulkTaskAction(select, ids, action, value);
}
async function executeBulkTaskAction(select, ids, action, value) {
await fetch('/api/admin/pm/tasks/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
@@ -1130,15 +1136,21 @@ function getSelectedIssueIds() {
if (!action || ids.length === 0) {
select.value = '';
if (action) alert('선택된 이슈가 없습니다.');
if (action) showToast('선택된 이슈가 없습니다.', 'warning');
return;
}
if (action === 'delete' && !confirm(`${ids.length}개 이슈를 삭제하시겠습니까?`)) {
select.value = '';
if (action === 'delete') {
showConfirm(`${ids.length}개 이슈를 삭제하시겠습니까?`, async () => {
await executeBulkIssueAction(select, ids, action, value);
}, { title: '일괄 삭제', icon: 'warning' });
return;
}
await executeBulkIssueAction(select, ids, action, value);
}
async function executeBulkIssueAction(select, ids, action, value) {
await fetch('/api/admin/pm/issues/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
@@ -1154,22 +1166,20 @@ function getSelectedIssueIds() {
const task = tasksData.find(t => t.id === taskId);
const taskName = task ? task.title : '이 작업';
if (!confirm(`"${taskName}"을(를) 삭제하시겠습니까?\n연결된 이슈의 작업 연결이 해제됩니다.`)) {
return;
}
showDeleteConfirm(`"${taskName}"`, async () => {
const response = await fetch(`/api/admin/pm/tasks/${taskId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
});
const result = await response.json();
const response = await fetch(`/api/admin/pm/tasks/${taskId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
if (result.success) {
loadTasks();
loadIssues(); // 연결된 이슈도 업데이트
} else {
showToast(result.message || '삭제에 실패했습니다.', 'error');
}
});
const result = await response.json();
if (result.success) {
loadTasks();
loadIssues(); // 연결된 이슈도 업데이트
} else {
alert(result.message || '삭제에 실패했습니다.');
}
}
// 이슈 삭제
@@ -1177,22 +1187,20 @@ function getSelectedIssueIds() {
const issue = issuesData.find(i => i.id === issueId);
const issueName = issue ? issue.title : '이 이슈';
if (!confirm(`"${issueName}"을(를) 삭제하시겠습니까?`)) {
return;
}
showDeleteConfirm(`"${issueName}"`, async () => {
const response = await fetch(`/api/admin/pm/issues/${issueId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
});
const result = await response.json();
const response = await fetch(`/api/admin/pm/issues/${issueId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }
if (result.success) {
loadTasks(); // 작업 탭 아코디언 업데이트
loadIssues(); // 이슈 탭 업데이트
} else {
showToast(result.message || '삭제에 실패했습니다.', 'error');
}
});
const result = await response.json();
if (result.success) {
loadTasks(); // 작업 탭 아코디언 업데이트
loadIssues(); // 이슈 탭 업데이트
} else {
alert(result.message || '삭제에 실패했습니다.');
}
}
</script>
@endpush

View File

@@ -121,11 +121,11 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
document.getElementById('loading').classList.add('hidden');
document.getElementById('formContainer').classList.remove('hidden');
} else {
alert(result.message || '데이터를 불러오는데 실패했습니다.');
showToast(result.message || '데이터를 불러오는데 실패했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.categories.index") }}';
}
} catch (err) {
alert('서버 오류가 발생했습니다.');
showToast('서버 오류가 발생했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.categories.index") }}';
}
}

View File

@@ -84,7 +84,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
// 삭제 확인
window.confirmDelete = function(id, name) {
if (confirm(`"${name}" 카테고리를 삭제하시겠습니까?\n\n해당 카테고리의 수식들은 삭제되지 않습니다.`)) {
showConfirm(`"${name}" 카테고리를 삭제하시겠습니까?\n\n해당 카테고리의 수식들은 삭제되지 않습니다.`, () => {
htmx.ajax('DELETE', `/api/admin/quote-formulas/categories/${id}`, {
target: '#category-table',
swap: 'none',
@@ -94,12 +94,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#category-table', 'filterSubmit');
});
}
}, { title: '삭제 확인', icon: 'warning' });
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 카테고리를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 카테고리를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/quote-formulas/categories/${id}/restore`, {
target: '#category-table',
swap: 'none',
@@ -109,12 +109,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#category-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`"${name}" 카테고리를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(`"${name}" 카테고리`, () => {
htmx.ajax('DELETE', `/api/admin/quote-formulas/categories/${id}/force`, {
target: '#category-table',
swap: 'none',
@@ -124,7 +124,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#category-table', 'filterSubmit');
});
}
});
};
// 활성/비활성 토글

View File

@@ -253,11 +253,11 @@ function insertVariable(variable) {
document.getElementById('loading').classList.add('hidden');
document.getElementById('formContainer').classList.remove('hidden');
} else {
alert(result.message || '데이터를 불러오는데 실패했습니다.');
showToast(result.message || '데이터를 불러오는데 실패했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.index") }}';
}
} catch (err) {
alert('서버 오류가 발생했습니다.');
showToast('서버 오류가 발생했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.index") }}';
}
}

View File

@@ -134,7 +134,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/quote-formulas/formulas/${id}`, {
target: '#formula-table',
swap: 'none',
@@ -144,12 +144,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#formula-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 수식을 복원하시겠습니까?`)) {
showConfirm(`"${name}" 수식을 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/quote-formulas/formulas/${id}/restore`, {
target: '#formula-table',
swap: 'none',
@@ -159,12 +159,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#formula-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`"${name}" 수식을 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(`"${name}" 수식`, () => {
htmx.ajax('DELETE', `/api/admin/quote-formulas/formulas/${id}/force`, {
target: '#formula-table',
swap: 'none',
@@ -174,7 +174,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#formula-table', 'filterSubmit');
});
}
});
};
// 활성/비활성 토글
@@ -192,7 +192,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
// 수식 복제
window.duplicateFormula = function(id) {
if (confirm('이 수식을 복제하시겠습니까?')) {
showConfirm('이 수식을 복제하시겠습니까?', () => {
htmx.ajax('POST', `/api/admin/quote-formulas/formulas/${id}/duplicate`, {
target: '#formula-table',
swap: 'none',
@@ -202,7 +202,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#formula-table', 'filterSubmit');
});
}
}, { title: '수식 복제', icon: 'question' });
};
</script>
@endpush

View File

@@ -183,10 +183,10 @@ function selectViewOnly() {
if (event.detail.target.id === 'roleForm') {
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');
}
}
});
@@ -195,13 +195,13 @@ function selectViewOnly() {
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');
}
});
</script>

View File

@@ -199,10 +199,10 @@ function selectViewOnly() {
if (event.detail.target.id === 'roleForm') {
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');
}
}
});
@@ -211,13 +211,13 @@ function selectViewOnly() {
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');
}
});
</script>

View File

@@ -75,7 +75,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/roles/${id}`, {
target: '#role-table',
swap: 'none',
@@ -85,7 +85,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#role-table', 'filterSubmit');
});
}
});
};
</script>
@endpush

View File

@@ -167,10 +167,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
if (event.detail.target.id === 'tenantForm') {
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');
}
}
});
@@ -179,13 +179,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');
}
});
</script>

View File

@@ -222,10 +222,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
if (event.detail.target.id === 'tenantForm') {
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');
}
}
});
@@ -234,13 +234,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');
}
});
</script>

View File

@@ -85,7 +85,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/tenants/${id}`, {
target: '#tenant-table',
swap: 'none',
@@ -95,12 +95,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#tenant-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 테넌트를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 테넌트를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/tenants/${id}/restore`, {
target: '#tenant-table',
swap: 'none',
@@ -114,12 +114,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
TenantModal.loadTenantInfo();
}
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`"${name}" 테넌트를 영구 삭제하시겠습니까?\n\n⚠ 이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/tenants/${id}/force`, {
target: '#tenant-table',
swap: 'none',
@@ -129,7 +129,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#tenant-table', 'filterSubmit');
});
}
});
};
</script>
@endpush

View File

@@ -169,10 +169,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
if (event.detail.target.id === 'userForm') {
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');
}
}
});
@@ -181,13 +181,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');
}
});
</script>

View File

@@ -202,10 +202,10 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition"
if (event.detail.target.id === 'userForm') {
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');
}
}
});
@@ -214,50 +214,48 @@ 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');
}
});
// 비밀번호 초기화 버튼 처리
document.getElementById('resetPasswordBtn').addEventListener('click', function() {
if (!confirm('비밀번호를 초기화하시겠습니까?\n\n임시 비밀번호가 생성되어 사용자 이메일({{ $user->email }})로 발송됩니다.')) {
return;
}
showConfirm('비밀번호를 초기화하시겠습니까?<br><br>임시 비밀번호가 생성되어 사용자 이메일({{ $user->email }})로 발송됩니다.', () => {
const btn = document.getElementById('resetPasswordBtn');
btn.disabled = true;
btn.innerHTML = '<svg class="animate-spin w-4 h-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 12h4z"></path></svg> 처리중...';
const btn = this;
btn.disabled = true;
btn.innerHTML = '<svg class="animate-spin w-4 h-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 12h4z"></path></svg> 처리중...';
fetch('/api/admin/users/{{ $user->id }}/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
} else {
alert('오류: ' + (data.message || '비밀번호 초기화에 실패했습니다.'));
}
})
.catch(error => {
alert('서버 오류가 발생했습니다.');
console.error(error);
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> 비밀번호 초기화';
});
fetch('/api/admin/users/{{ $user->id }}/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast(data.message, 'success');
} else {
showToast(data.message || '비밀번호 초기화에 실패했습니다.', 'error');
}
})
.catch(error => {
showToast('서버 오류가 발생했습니다.', 'error');
console.error(error);
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> 비밀번호 초기화';
});
}, { title: '비밀번호 초기화', icon: 'warning' });
});
</script>
@endpush

View File

@@ -73,7 +73,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/users/${id}`, {
target: '#user-table',
swap: 'none',
@@ -83,12 +83,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#user-table', 'filterSubmit');
});
}
});
};
// 복원 확인
window.confirmRestore = function(id, name) {
if (confirm(`"${name}" 사용자를 복원하시겠습니까?`)) {
showConfirm(`"${name}" 사용자를 복원하시겠습니까?`, () => {
htmx.ajax('POST', `/api/admin/users/${id}/restore`, {
target: '#user-table',
swap: 'none',
@@ -98,12 +98,12 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#user-table', 'filterSubmit');
});
}
}, { title: '복원 확인', icon: 'question' });
};
// 영구삭제 확인
window.confirmForceDelete = function(id, name) {
if (confirm(`⚠️ 경고: "${name}" 사용자를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!`)) {
showPermanentDeleteConfirm(name, () => {
htmx.ajax('DELETE', `/api/admin/users/${id}/force`, {
target: '#user-table',
swap: 'none',
@@ -113,7 +113,7 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
}).then(() => {
htmx.trigger('#user-table', 'filterSubmit');
});
}
});
};
</script>
@endpush