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:
taekyoungc
2026-06-29 18:39:37 +09:00
parent bf9fbd57a4
commit 0a39668478
9 changed files with 68 additions and 0 deletions

View File

@@ -62,6 +62,7 @@ $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void
$routes->get('manual', 'Bag::manual');
$routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저
$routes->get('manual/(:segment)', 'Bag::manualPage/$1');
$routes->post('activity/print-log', 'Bag::printLog'); // 인쇄 활동 기록(beacon)
});
$routes->get('bag/number-lookup', 'Bag::numberLookup');

View File

@@ -46,6 +46,8 @@ class ActivityLog extends BaseController
'feedback' => '사용자 의견',
'feedback_file' => '의견 첨부',
'blockchain_ledger' => '원장',
'export' => '엑셀 다운로드',
'print' => '인쇄',
];
/** action → 한글 라벨 */
@@ -53,6 +55,8 @@ class ActivityLog extends BaseController
'create' => '등록',
'update' => '수정',
'delete' => '삭제',
'export' => '엑셀 다운로드',
'print' => '인쇄',
];
/** 컬럼 → 한글 라벨 (없으면 접미사 추정 → 원문) */
@@ -156,6 +160,10 @@ class ActivityLog extends BaseController
if ($isLogin) {
$text = '로그인';
} elseif ($action === 'export') {
$text = '엑셀 다운로드 — ' . (string) ($after['파일'] ?? '');
} elseif ($action === 'print') {
$text = '인쇄 — ' . (string) ($after['화면'] ?? $after['경로'] ?? '');
} elseif ($action === 'create') {
$text = $tlabel . $namePart . ' 등록';
} elseif ($action === 'delete') {

View File

@@ -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
{
$first = (new \App\Libraries\ManualRenderer())->firstSlug();

View File

@@ -8,6 +8,22 @@ declare(strict_types=1);
* 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')) {
/**
* CSV 파일을 브라우저로 다운로드 전송
@@ -18,6 +34,7 @@ if (! function_exists('export_csv')) {
*/
function export_csv(string $filename, array $headers, array $rows): void
{
audit_export_log($filename);
// 파일명에 .csv 확장자 보장
if (! str_ends_with($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
{
audit_export_log($filename);
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
$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
{
audit_export_log($filename);
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
$esc = static function (mixed $v): string {
@@ -402,6 +421,7 @@ if (! function_exists('export_bag_flow_report_excel')) {
array $metaLines,
array $reportRows
): void {
audit_export_log($filename);
$baseName = preg_replace('/\.[^.]+$/u', '', $filename);
$baseName = preg_replace('/[^\p{L}\p{N}_\-]+/u', '_', $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
{
audit_export_log($filename);
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xlsx';
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);

View File

@@ -15,6 +15,8 @@ $actionBadge = static function (string $a): string {
'create' => 'background:#dcfce7;color:#166534;',
'update' => 'background:#dbeafe;color:#1e40af;',
'delete' => 'background:#fee2e2;color:#991b1b;',
'export' => 'background:#cffafe;color:#155e75;',
'print' => 'background:#f3e8ff;color:#6b21a8;',
];
return $map[$a] ?? 'background:#f3f4f6;color:#374151;';
};

View File

@@ -201,5 +201,9 @@ tailwind.config = {
})();
</script>
<?= 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>
</html>

View File

@@ -240,5 +240,9 @@ tailwind.config = {
})();
</script>
<?= 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>
</html>

View File

@@ -196,5 +196,9 @@ body { overflow: hidden; }
window.addEventListener('pagehide', closeStuckOverlays);
})();
</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>
</html>

View File

@@ -296,5 +296,9 @@ tailwind.config = {
})();
</script>
<?= 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>
</html>