Files
sam-manage/resources/views/daily-logs/partials/table.blade.php
hskwon 5c892c1ed9 브라우저 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 테마와 일관된 디자인
  - 비동기 콜백 패턴으로 리팩토링
  - 삭제/복원/영구삭제 각각 다른 스타일 적용
2025-12-05 09:49:56 +09:00

370 lines
18 KiB
PHP

<!-- 일일 로그 카드 리스트 -->
<div class="space-y-3 p-4">
@forelse($logs as $log)
<!-- 로그 카드 -->
<div class="log-card bg-white border rounded-lg overflow-hidden {{ $log->trashed() ? 'border-red-300 bg-red-50' : 'border-gray-200 hover:border-blue-300' }} transition-all"
data-log-id="{{ $log->id }}">
<!-- 카드 헤더 (클릭 가능) -->
<div class="card-header cursor-pointer p-4" onclick="toggleCardAccordion({{ $log->id }}, event)">
<div class="flex items-start justify-between gap-4">
<!-- 좌측: 날짜 + 요약 -->
<div class="flex items-start gap-4 flex-1 min-w-0">
<!-- 날짜 영역 -->
<div class="flex-shrink-0 text-center">
<div class="flex items-center gap-2">
<svg class="accordion-chevron w-4 h-4 text-gray-400 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<div>
<div class="text-lg font-bold text-gray-900">
{{ $log->log_date->format('m/d') }}
</div>
<div class="text-xs text-gray-500">
{{ $log->log_date->format('D') }}
</div>
</div>
</div>
</div>
<!-- 요약 + 프로젝트 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
@if($log->project)
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-800 flex-shrink-0">
{{ $log->project->name }}
</span>
@endif
@if($log->trashed())
<span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-red-100 text-red-800 flex-shrink-0">
삭제됨
</span>
@endif
</div>
@if($log->summary)
<p class="text-sm text-gray-700 line-clamp-2" title="{{ $log->summary }}">
{!! nl2br(e($log->summary)) !!}
</p>
@else
<p class="text-sm text-gray-400">요약 없음</p>
@endif
</div>
</div>
<!-- 우측: 통계 + 액션 -->
<div class="flex items-center gap-4 flex-shrink-0">
<!-- 항목 통계 -->
<div class="text-right">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-700">{{ $log->entries_count }}</span>
@if($log->entries->count() > 0)
<div class="flex items-center gap-1">
@php $stats = $log->entry_stats; @endphp
@if($stats['todo'] > 0)
<span class="flex items-center gap-0.5 text-xs text-gray-500">
<span class="w-2 h-2 rounded-full bg-gray-400"></span>{{ $stats['todo'] }}
</span>
@endif
@if($stats['in_progress'] > 0)
<span class="flex items-center gap-0.5 text-xs text-yellow-600">
<span class="w-2 h-2 rounded-full bg-yellow-400"></span>{{ $stats['in_progress'] }}
</span>
@endif
@if($stats['done'] > 0)
<span class="flex items-center gap-0.5 text-xs text-green-600">
<span class="w-2 h-2 rounded-full bg-green-400"></span>{{ $stats['done'] }}
</span>
@endif
</div>
@endif
</div>
<div class="text-xs text-gray-400 mt-0.5">
{{ $log->creator?->name ?? '-' }}
</div>
</div>
<!-- 액션 버튼 -->
<div class="flex items-center gap-1" onclick="event.stopPropagation()">
@if($log->trashed())
<button onclick="confirmRestore({{ $log->id }}, '{{ $log->log_date->format('Y-m-d') }}')"
class="p-2 text-green-600 hover:bg-green-50 rounded-lg transition" title="복원">
<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"/>
</svg>
</button>
@if(auth()->user()?->is_super_admin)
<button onclick="confirmForceDelete({{ $log->id }}, '{{ $log->log_date->format('Y-m-d') }}')"
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" title="영구삭제">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
@endif
@else
<button onclick="editLog({{ $log->id }})"
class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition" title="수정">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</button>
<button onclick="confirmDelete({{ $log->id }}, '{{ $log->log_date->format('Y-m-d') }}')"
class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" title="삭제">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
@endif
</div>
</div>
</div>
</div>
<!-- 아코디언 상세 내용 (숨겨진 상태) -->
<div class="card-accordion hidden border-t border-gray-200 bg-gray-50" data-accordion-for="{{ $log->id }}">
<div class="accordion-content p-4" id="card-accordion-content-{{ $log->id }}">
<div class="text-center py-4 text-gray-500">로딩 ...</div>
</div>
</div>
</div>
@empty
<div class="text-center py-12 text-gray-500">
일일 로그가 없습니다.
</div>
@endforelse
</div>
<!-- 페이지네이션 -->
@if($logs->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $logs->withQueryString()->links() }}
</div>
@endif
<script>
// 카드 아코디언 기능
let cardOpenAccordionId = null;
function toggleCardAccordion(logId, event) {
// 클릭한 요소가 버튼이면 무시
if (event.target.closest('button') || event.target.closest('a')) {
return;
}
const card = document.querySelector(`.log-card[data-log-id="${logId}"]`);
const accordion = card.querySelector('.card-accordion');
const chevron = card.querySelector('.accordion-chevron');
// 같은 카드를 다시 클릭하면 닫기
if (cardOpenAccordionId === logId) {
accordion.classList.add('hidden');
chevron.classList.remove('rotate-90');
card.classList.remove('ring-2', 'ring-blue-500');
cardOpenAccordionId = null;
return;
}
// 다른 열린 아코디언 닫기
if (cardOpenAccordionId !== null) {
const prevCard = document.querySelector(`.log-card[data-log-id="${cardOpenAccordionId}"]`);
if (prevCard) {
prevCard.querySelector('.card-accordion')?.classList.add('hidden');
prevCard.querySelector('.accordion-chevron')?.classList.remove('rotate-90');
prevCard.classList.remove('ring-2', 'ring-blue-500');
}
}
// 현재 아코디언 열기
accordion.classList.remove('hidden');
chevron.classList.add('rotate-90');
card.classList.add('ring-2', 'ring-blue-500');
cardOpenAccordionId = logId;
// 데이터 로드
loadCardAccordionContent(logId);
}
function loadCardAccordionContent(logId) {
const contentDiv = document.getElementById(`card-accordion-content-${logId}`);
contentDiv.innerHTML = '<div class="text-center py-4 text-gray-500">로딩 중...</div>';
fetch(`/api/admin/daily-logs/${logId}`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
renderCardAccordionContent(logId, data.data);
} else {
contentDiv.innerHTML = '<div class="text-center py-4 text-red-500">데이터 로드 실패</div>';
}
})
.catch(err => {
contentDiv.innerHTML = '<div class="text-center py-4 text-red-500">데이터 로드 실패</div>';
});
}
function renderCardAccordionContent(logId, log) {
const contentDiv = document.getElementById(`card-accordion-content-${logId}`);
const statusColors = {
'todo': 'bg-gray-100 text-gray-700',
'in_progress': 'bg-yellow-100 text-yellow-700',
'done': 'bg-green-100 text-green-700'
};
const statusLabels = {
'todo': '예정',
'in_progress': '진행중',
'done': '완료'
};
// HTML 이스케이프 헬퍼
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function nl2br(text) {
if (!text) return '';
return escapeHtml(text).replace(/\n/g, '<br>');
}
let entriesHtml = '';
if (log.entries && log.entries.length > 0) {
// 담당자별로 그룹핑
const grouped = {};
log.entries.forEach(entry => {
const name = entry.assignee_name || '미지정';
if (!grouped[name]) {
grouped[name] = [];
}
grouped[name].push(entry);
});
// 담당자별 카드 생성
entriesHtml = Object.entries(grouped).map(([assigneeName, entries]) => `
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<!-- 담당자 헤더 -->
<div class="px-3 py-2 bg-gray-100 border-b border-gray-200 flex items-center justify-between">
<span class="text-sm font-semibold text-gray-900">${escapeHtml(assigneeName)}</span>
<span class="text-xs text-gray-500">${entries.length}건</span>
</div>
<!-- 항목 목록 -->
<div class="divide-y divide-gray-100">
${entries.map(entry => `
<div class="p-3 hover:bg-gray-50" data-entry-id="${entry.id}">
<div class="flex items-start gap-2">
<span class="px-1.5 py-0.5 text-[10px] rounded shrink-0 ${statusColors[entry.status]}">${statusLabels[entry.status]}</span>
<div class="flex-1 min-w-0">
<p class="text-sm text-gray-700">${nl2br(entry.content)}</p>
</div>
<div class="flex items-center gap-0.5 shrink-0">
${entry.status !== 'todo' ? `
<button onclick="updateCardEntryStatus(${logId}, ${entry.id}, 'todo')" class="p-1 text-gray-400 hover:bg-gray-100 rounded" title="예정">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2"/></svg>
</button>` : ''}
${entry.status !== 'in_progress' ? `
<button onclick="updateCardEntryStatus(${logId}, ${entry.id}, 'in_progress')" class="p-1 text-yellow-500 hover:bg-yellow-50 rounded" title="진행중">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg>
</button>` : ''}
${entry.status !== 'done' ? `
<button onclick="updateCardEntryStatus(${logId}, ${entry.id}, 'done')" class="p-1 text-green-500 hover:bg-green-50 rounded" title="완료">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</button>` : ''}
<button onclick="deleteCardEntry(${logId}, ${entry.id})" class="p-1 text-red-400 hover:bg-red-50 rounded" title="삭제">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
</div>
`).join('')}
</div>
</div>
`).join('');
} else {
entriesHtml = '<div class="text-center py-4 text-gray-400">등록된 항목이 없습니다.</div>';
}
// 요약 섹션 (전체 내용)
const summaryHtml = log.summary ? `
<div class="mb-4 p-3 bg-white rounded-lg border border-gray-200">
<div class="text-xs font-medium text-gray-500 mb-1">요약</div>
<div class="text-sm text-gray-700">${nl2br(log.summary)}</div>
</div>
` : '';
contentDiv.innerHTML = `
<div class="space-y-3">
${summaryHtml}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
${entriesHtml}
</div>
<div class="pt-3 border-t border-gray-200 flex justify-between items-center">
<button onclick="openQuickAddCardEntry(${logId})" class="text-sm text-blue-600 hover:text-blue-800 font-medium">
+ 항목 추가
</button>
<button onclick="editLog(${logId})" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium">
전체 수정
</button>
</div>
</div>
`;
}
function updateCardEntryStatus(logId, entryId, status) {
fetch(`/api/admin/daily-logs/entries/${entryId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
},
body: JSON.stringify({ status })
})
.then(res => res.json())
.then(result => {
if (result.success) {
loadCardAccordionContent(logId);
}
});
}
function deleteCardEntry(logId, entryId) {
showConfirm('이 항목을 삭제하시겠습니까?', () => {
fetch(`/api/admin/daily-logs/entries/${entryId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || ''
}
})
.then(res => res.json())
.then(result => {
if (result.success) {
loadCardAccordionContent(logId);
}
});
}, { title: '항목 삭제', icon: 'warning' });
}
function openQuickAddCardEntry(logId) {
// 부모 페이지의 openQuickAddModal 함수 사용 (prompt 대신 모달에서 담당자 입력)
if (typeof openQuickAddModal === 'function') {
openQuickAddModal(logId);
} else {
showToast('모달을 열 수 없습니다. 페이지를 새로고침해주세요.', 'warning');
}
}
// 주간 타임라인에서 카드로 스크롤
function scrollToCard(logId) {
const card = document.querySelector(`.log-card[data-log-id="${logId}"]`);
if (card) {
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => {
const fakeEvent = { target: card };
toggleCardAccordion(logId, fakeEvent);
}, 300);
}
}
</script>