브라우저 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:
253
docs/QUOTE_FORMULA_SEED_PLAN.md
Normal file
253
docs/QUOTE_FORMULA_SEED_PLAN.md
Normal 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)
|
||||
@@ -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
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 활성/비활성 토글
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 활성 토글
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 복제 확인
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") }}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 활성/비활성 토글
|
||||
|
||||
@@ -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") }}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user