feat: 활동 로그에 엑셀 다운로드/인쇄 기록 추가
- export_*: 모든 엑셀/CSV 다운로드를 'export' 액션으로 자동 기록(파일명 포함) - 인쇄: 레이아웃 beforeprint beacon → bag/activity/print-log 가 'print' 액션 기록(화면·경로) - 활동 로그 뷰어에 엑셀 다운로드/인쇄 라벨·배지·필터·요약 추가 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void
|
|||||||
$routes->get('manual', 'Bag::manual');
|
$routes->get('manual', 'Bag::manual');
|
||||||
$routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저
|
$routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저
|
||||||
$routes->get('manual/(:segment)', 'Bag::manualPage/$1');
|
$routes->get('manual/(:segment)', 'Bag::manualPage/$1');
|
||||||
|
$routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon)
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->get('bag/number-lookup', 'Bag::numberLookup');
|
$routes->get('bag/number-lookup', 'Bag::numberLookup');
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ class ActivityLog extends BaseController
|
|||||||
'feedback' => '사용자 의견',
|
'feedback' => '사용자 의견',
|
||||||
'feedback_file' => '의견 첨부',
|
'feedback_file' => '의견 첨부',
|
||||||
'blockchain_ledger' => '원장',
|
'blockchain_ledger' => '원장',
|
||||||
|
'export' => '엑셀 다운로드',
|
||||||
|
'print' => '인쇄',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** action → 한글 라벨 */
|
/** action → 한글 라벨 */
|
||||||
@@ -53,6 +55,8 @@ class ActivityLog extends BaseController
|
|||||||
'create' => '등록',
|
'create' => '등록',
|
||||||
'update' => '수정',
|
'update' => '수정',
|
||||||
'delete' => '삭제',
|
'delete' => '삭제',
|
||||||
|
'export' => '엑셀 다운로드',
|
||||||
|
'print' => '인쇄',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 컬럼 → 한글 라벨 (없으면 접미사 추정 → 원문) */
|
/** 컬럼 → 한글 라벨 (없으면 접미사 추정 → 원문) */
|
||||||
@@ -156,6 +160,10 @@ class ActivityLog extends BaseController
|
|||||||
|
|
||||||
if ($isLogin) {
|
if ($isLogin) {
|
||||||
$text = '로그인';
|
$text = '로그인';
|
||||||
|
} elseif ($action === 'export') {
|
||||||
|
$text = '엑셀 다운로드 — ' . (string) ($after['파일'] ?? '');
|
||||||
|
} elseif ($action === 'print') {
|
||||||
|
$text = '인쇄 — ' . (string) ($after['화면'] ?? $after['경로'] ?? '');
|
||||||
} elseif ($action === 'create') {
|
} elseif ($action === 'create') {
|
||||||
$text = $tlabel . $namePart . ' 등록';
|
$text = $tlabel . $namePart . ' 등록';
|
||||||
} elseif ($action === 'delete') {
|
} elseif ($action === 'delete') {
|
||||||
|
|||||||
@@ -3580,6 +3580,26 @@ SQL);
|
|||||||
/**
|
/**
|
||||||
* 사용자 매뉴얼(설명서) — 목차 첫 페이지로 이동.
|
* 사용자 매뉴얼(설명서) — 목차 첫 페이지로 이동.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 인쇄 활동 기록 (클라이언트 beforeprint beacon 수신).
|
||||||
|
* 화면(경로·제목)을 활동 로그에 'print' 액션으로 남긴다.
|
||||||
|
*/
|
||||||
|
public function printLog(): \CodeIgniter\HTTP\ResponseInterface
|
||||||
|
{
|
||||||
|
$title = trim((string) $this->request->getPost('title'));
|
||||||
|
$path = trim((string) $this->request->getPost('path'));
|
||||||
|
if (mb_strlen($title) > 120) {
|
||||||
|
$title = mb_substr($title, 0, 120);
|
||||||
|
}
|
||||||
|
if (mb_strlen($path) > 200) {
|
||||||
|
$path = mb_substr($path, 0, 200);
|
||||||
|
}
|
||||||
|
helper('audit');
|
||||||
|
audit_log('print', 'print', 0, null, ['화면' => $title, '경로' => $path]);
|
||||||
|
|
||||||
|
return $this->response->setStatusCode(204);
|
||||||
|
}
|
||||||
|
|
||||||
public function manual(): \CodeIgniter\HTTP\RedirectResponse
|
public function manual(): \CodeIgniter\HTTP\RedirectResponse
|
||||||
{
|
{
|
||||||
$first = (new \App\Libraries\ManualRenderer())->firstSlug();
|
$first = (new \App\Libraries\ManualRenderer())->firstSlug();
|
||||||
|
|||||||
@@ -8,6 +8,22 @@ declare(strict_types=1);
|
|||||||
* UTF-8 BOM 포함으로 한글 엑셀 호환성 보장
|
* UTF-8 BOM 포함으로 한글 엑셀 호환성 보장
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (! function_exists('audit_export_log')) {
|
||||||
|
/**
|
||||||
|
* 엑셀/CSV 다운로드를 활동 로그에 기록 (누가·무엇을 다운로드).
|
||||||
|
* 모든 export_* 함수 진입 시 호출 — 실패해도 다운로드에 영향 없음.
|
||||||
|
*/
|
||||||
|
function audit_export_log(string $filename): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
helper('audit');
|
||||||
|
audit_log('export', 'export', 0, null, ['파일' => $filename]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 무시: 로깅 실패가 다운로드를 막지 않도록
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (! function_exists('export_csv')) {
|
if (! function_exists('export_csv')) {
|
||||||
/**
|
/**
|
||||||
* CSV 파일을 브라우저로 다운로드 전송
|
* CSV 파일을 브라우저로 다운로드 전송
|
||||||
@@ -18,6 +34,7 @@ if (! function_exists('export_csv')) {
|
|||||||
*/
|
*/
|
||||||
function export_csv(string $filename, array $headers, array $rows): void
|
function export_csv(string $filename, array $headers, array $rows): void
|
||||||
{
|
{
|
||||||
|
audit_export_log($filename);
|
||||||
// 파일명에 .csv 확장자 보장
|
// 파일명에 .csv 확장자 보장
|
||||||
if (! str_ends_with($filename, '.csv')) {
|
if (! str_ends_with($filename, '.csv')) {
|
||||||
$filename .= '.csv';
|
$filename .= '.csv';
|
||||||
@@ -80,6 +97,7 @@ if (! function_exists('export_excel_2003_xml')) {
|
|||||||
*/
|
*/
|
||||||
function export_excel_2003_xml(string $filename, string $sheetName, array $headers, array $rows, array $topRows = []): void
|
function export_excel_2003_xml(string $filename, string $sheetName, array $headers, array $rows, array $topRows = []): void
|
||||||
{
|
{
|
||||||
|
audit_export_log($filename);
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
||||||
|
|
||||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
||||||
@@ -147,6 +165,7 @@ if (! function_exists('export_excel_2003_xml_workbook')) {
|
|||||||
*/
|
*/
|
||||||
function export_excel_2003_xml_workbook(string $filename, array $sheets): void
|
function export_excel_2003_xml_workbook(string $filename, array $sheets): void
|
||||||
{
|
{
|
||||||
|
audit_export_log($filename);
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
||||||
|
|
||||||
$esc = static function (mixed $v): string {
|
$esc = static function (mixed $v): string {
|
||||||
@@ -402,6 +421,7 @@ if (! function_exists('export_bag_flow_report_excel')) {
|
|||||||
array $metaLines,
|
array $metaLines,
|
||||||
array $reportRows
|
array $reportRows
|
||||||
): void {
|
): void {
|
||||||
|
audit_export_log($filename);
|
||||||
$baseName = preg_replace('/\.[^.]+$/u', '', $filename);
|
$baseName = preg_replace('/\.[^.]+$/u', '', $filename);
|
||||||
$baseName = preg_replace('/[^\p{L}\p{N}_\-]+/u', '_', $baseName) ?? 'bag_flow';
|
$baseName = preg_replace('/[^\p{L}\p{N}_\-]+/u', '_', $baseName) ?? 'bag_flow';
|
||||||
$baseName = trim($baseName, '_') !== '' ? trim($baseName, '_') : 'bag_flow';
|
$baseName = trim($baseName, '_') !== '' ? trim($baseName, '_') : 'bag_flow';
|
||||||
@@ -452,6 +472,7 @@ if (! function_exists('export_xlsx')) {
|
|||||||
*/
|
*/
|
||||||
function export_xlsx(string $filename, string $sheetName, array $headers, array $rows): void
|
function export_xlsx(string $filename, string $sheetName, array $headers, array $rows): void
|
||||||
{
|
{
|
||||||
|
audit_export_log($filename);
|
||||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xlsx';
|
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xlsx';
|
||||||
|
|
||||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ $actionBadge = static function (string $a): string {
|
|||||||
'create' => 'background:#dcfce7;color:#166534;',
|
'create' => 'background:#dcfce7;color:#166534;',
|
||||||
'update' => 'background:#dbeafe;color:#1e40af;',
|
'update' => 'background:#dbeafe;color:#1e40af;',
|
||||||
'delete' => 'background:#fee2e2;color:#991b1b;',
|
'delete' => 'background:#fee2e2;color:#991b1b;',
|
||||||
|
'export' => 'background:#cffafe;color:#155e75;',
|
||||||
|
'print' => 'background:#f3e8ff;color:#6b21a8;',
|
||||||
];
|
];
|
||||||
return $map[$a] ?? 'background:#f3f4f6;color:#374151;';
|
return $map[$a] ?? 'background:#f3f4f6;color:#374151;';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,5 +201,9 @@ tailwind.config = {
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?= view('components/feedback_widget') ?>
|
<?= view('components/feedback_widget') ?>
|
||||||
|
<script>
|
||||||
|
/* 인쇄 시 활동 로그 기록(beacon) */
|
||||||
|
(function(){var last=0;window.addEventListener('beforeprint',function(){var now=Date.now();if(now-last<4000)return;last=now;try{var fd=new FormData();fd.append('title',document.title||'');fd.append('path',location.pathname||'');navigator.sendBeacon('<?= site_url('bag/activity/print-log') ?>',fd);}catch(e){}});})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -240,5 +240,9 @@ tailwind.config = {
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?= view('components/feedback_widget') ?>
|
<?= view('components/feedback_widget') ?>
|
||||||
|
<script>
|
||||||
|
/* 인쇄 시 활동 로그 기록(beacon) */
|
||||||
|
(function(){var last=0;window.addEventListener('beforeprint',function(){var now=Date.now();if(now-last<4000)return;last=now;try{var fd=new FormData();fd.append('title',document.title||'');fd.append('path',location.pathname||'');navigator.sendBeacon('<?= site_url('bag/activity/print-log') ?>',fd);}catch(e){}});})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -196,5 +196,9 @@ body { overflow: hidden; }
|
|||||||
window.addEventListener('pagehide', closeStuckOverlays);
|
window.addEventListener('pagehide', closeStuckOverlays);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
/* 인쇄 시 활동 로그 기록(beacon) */
|
||||||
|
(function(){var last=0;window.addEventListener('beforeprint',function(){var now=Date.now();if(now-last<4000)return;last=now;try{var fd=new FormData();fd.append('title',document.title||'');fd.append('path',location.pathname||'');navigator.sendBeacon('<?= site_url('bag/activity/print-log') ?>',fd);}catch(e){}});})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -296,5 +296,9 @@ tailwind.config = {
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?= view('components/feedback_widget') ?>
|
<?= view('components/feedback_widget') ?>
|
||||||
|
<script>
|
||||||
|
/* 인쇄 시 활동 로그 기록(beacon) */
|
||||||
|
(function(){var last=0;window.addEventListener('beforeprint',function(){var now=Date.now();if(now-last<4000)return;last=now;try{var fd=new FormData();fd.append('title',document.title||'');fd.append('path',location.pathname||'');navigator.sendBeacon('<?= site_url('bag/activity/print-log') ?>',fd);}catch(e){}});})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user