사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -264,13 +264,12 @@ if (! function_exists('normalize_menu_link_for_url')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('mgmt_url')) {
|
||||
if (! function_exists('mgmt_path')) {
|
||||
/**
|
||||
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
|
||||
* 업무 화면 상대 경로 (페이저 setPath 등). 선행 슬래시 없음.
|
||||
*/
|
||||
function mgmt_url(string $path): string
|
||||
function mgmt_path(string $path): string
|
||||
{
|
||||
helper('url');
|
||||
$path = trim($path, '/');
|
||||
// bag/packaging-units 는 조회 전용(Bag) — CRUD 는 /bag/packaging-units/manage/* 로 분리
|
||||
if ($path === 'packaging-units') {
|
||||
@@ -279,7 +278,35 @@ if (! function_exists('mgmt_url')) {
|
||||
$path = 'packaging-units/manage/' . substr($path, strlen('packaging-units/'));
|
||||
}
|
||||
|
||||
return site_url('bag/' . $path);
|
||||
return 'bag/' . $path;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('mgmt_url')) {
|
||||
/**
|
||||
* 업무 화면 링크: 정식 URL 은 /bag/* (adminAuth). 포장 단위 CRUD 는 packaging-units/manage 로 치환.
|
||||
*/
|
||||
function mgmt_url(string $path): string
|
||||
{
|
||||
helper('url');
|
||||
|
||||
return site_url(mgmt_path($path));
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('apply_pager_path')) {
|
||||
/**
|
||||
* CI4 페이저: setPath 는 상대 경로만 허용(전체 URL 시 baseURL 이중 결합).
|
||||
* 검색 조건은 only() 로 유지합니다.
|
||||
*
|
||||
* @param \CodeIgniter\Pager\Pager $pager
|
||||
*/
|
||||
function apply_pager_path($pager, string $path, array $queryForPager = []): void
|
||||
{
|
||||
$pager->setPath($path);
|
||||
if ($queryForPager !== []) {
|
||||
$pager->only(array_keys($queryForPager));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +394,10 @@ if (! function_exists('menu_link_candidate_paths')) {
|
||||
$cands[] = 'admin/packaging-units' . ($m[1] ?? '');
|
||||
} elseif (preg_match('#^admin/packaging-units(/.*)?$#', $p, $m)) {
|
||||
$cands[] = 'bag/packaging-units/manage' . ($m[1] ?? '');
|
||||
} elseif ($p === 'bag/inventory/inspection-select') {
|
||||
// 실사 선별 조회 메뉴는 작업 화면(inspection-work)도 동일 메뉴로 활성 처리
|
||||
$cands[] = 'bag/inventory/inspection-work';
|
||||
$cands[] = 'bag/inventory/inspection';
|
||||
} elseif (str_starts_with($p, 'admin/')) {
|
||||
$cands[] = 'bag/' . substr($p, strlen('admin/'));
|
||||
} elseif (str_starts_with($p, 'bag/')) {
|
||||
|
||||
@@ -67,3 +67,441 @@ if (! function_exists('csv_encode_row')) {
|
||||
return implode(',', $escaped) . "\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('export_excel_2003_xml')) {
|
||||
/**
|
||||
* Excel 2003 XML(SpreadsheetML)로 브라우저 다운로드 (.xls 확장자, 별도 라이브러리 불필요)
|
||||
*
|
||||
* @param string $filename 저장 파일명(확장자는 .xls로 정규화)
|
||||
* @param string $sheetName 시트 이름(Excel 제한: 길이·일부 문자)
|
||||
* @param string[] $headers 컬럼 헤더
|
||||
* @param array $rows 데이터 행(각 행은 배열, 값은 문자열로 출력)
|
||||
*/
|
||||
function export_excel_2003_xml(string $filename, string $sheetName, array $headers, array $rows): void
|
||||
{
|
||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
||||
|
||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
||||
$safeSheet = function_exists('mb_substr')
|
||||
? mb_substr($safeSheet, 0, 31, 'UTF-8')
|
||||
: substr($safeSheet, 0, 31);
|
||||
|
||||
$esc = static function (mixed $v): string {
|
||||
return htmlspecialchars((string) ($v ?? ''), ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
};
|
||||
|
||||
$parts = [];
|
||||
$parts[] = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
$parts[] = '<?mso-application progid="Excel.Sheet"?>';
|
||||
$parts[] = '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">';
|
||||
$parts[] = '<Worksheet ss:Name="' . $esc($safeSheet) . '">';
|
||||
$parts[] = '<Table>';
|
||||
|
||||
$parts[] = '<Row>';
|
||||
foreach ($headers as $h) {
|
||||
$parts[] = '<Cell><Data ss:Type="String">' . $esc($h) . '</Data></Cell>';
|
||||
}
|
||||
$parts[] = '</Row>';
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$parts[] = '<Row>';
|
||||
foreach (array_values((array) $row) as $cell) {
|
||||
$parts[] = '<Cell><Data ss:Type="String">' . $esc($cell) . '</Data></Cell>';
|
||||
}
|
||||
$parts[] = '</Row>';
|
||||
}
|
||||
|
||||
$parts[] = '</Table>';
|
||||
$parts[] = '</Worksheet>';
|
||||
$parts[] = '</Workbook>';
|
||||
|
||||
$output = implode('', $parts);
|
||||
|
||||
$response = service('response');
|
||||
$response->setHeader('Content-Type', 'application/vnd.ms-excel; charset=UTF-8');
|
||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
$response->setBody($output);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('export_excel_2003_xml_workbook')) {
|
||||
/**
|
||||
* Excel 2003 XML — 다중 시트, 인쇄 미리보기와 유사한 헤더·줄바꿈·열 너비
|
||||
*
|
||||
* @param string $filename 저장 파일명
|
||||
* @param list<array{name: string, headers: list<string>, rows: list<list<string>>, col_widths?: list<int>}> $sheets
|
||||
*/
|
||||
function export_excel_2003_xml_workbook(string $filename, array $sheets): void
|
||||
{
|
||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xls';
|
||||
|
||||
$esc = static function (mixed $v): string {
|
||||
return htmlspecialchars((string) ($v ?? ''), ENT_XML1 | ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
};
|
||||
|
||||
$safeSheetName = static function (string $name) use ($esc): string {
|
||||
$safe = str_replace(['/', '\\', '?', '*', '[', ']', ':'], '', $name);
|
||||
$safe = function_exists('mb_substr') ? mb_substr($safe, 0, 31, 'UTF-8') : substr($safe, 0, 31);
|
||||
|
||||
return $esc($safe !== '' ? $safe : 'Sheet');
|
||||
};
|
||||
|
||||
$parts = [];
|
||||
$parts[] = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
$parts[] = '<?mso-application progid="Excel.Sheet"?>';
|
||||
$parts[] = '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">';
|
||||
$parts[] = '<Styles>';
|
||||
$parts[] = '<Style ss:ID="Default"><Alignment ss:Vertical="Top" ss:WrapText="1" ss:Horizontal="Left"/><Font ss:FontName="맑은 고딕" x:CharSet="129" ss:Size="9"/></Style>';
|
||||
$parts[] = '<Style ss:ID="Header"><Font ss:Bold="1" ss:Size="9" ss:FontName="맑은 고딕" x:CharSet="129"/><Interior ss:Color="#F3F4F6" ss:Pattern="Solid"/><Alignment ss:Horizontal="Left" ss:Vertical="Center" ss:WrapText="1"/><Borders><Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/></Borders></Style>';
|
||||
$parts[] = '</Styles>';
|
||||
|
||||
foreach ($sheets as $sheet) {
|
||||
$sheetName = $safeSheetName((string) ($sheet['name'] ?? 'Sheet'));
|
||||
$headers = array_values((array) ($sheet['headers'] ?? []));
|
||||
$rows = (array) ($sheet['rows'] ?? []);
|
||||
$colWidths = array_values((array) ($sheet['col_widths'] ?? []));
|
||||
|
||||
$parts[] = '<Worksheet ss:Name="' . $sheetName . '">';
|
||||
$parts[] = '<WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"><PageSetup><Layout x:Orientation="Landscape"/></PageSetup></WorksheetOptions>';
|
||||
$parts[] = '<Table>';
|
||||
|
||||
$colCount = max(count($headers), 1);
|
||||
for ($i = 0; $i < $colCount; $i++) {
|
||||
$px = (int) ($colWidths[$i] ?? 72);
|
||||
$width = max(48, min(280, $px));
|
||||
$excelW = round($width / 6.5, 1);
|
||||
$parts[] = '<Column ss:Index="' . ($i + 1) . '" ss:AutoFitWidth="0" ss:Width="' . $excelW . '"/>';
|
||||
}
|
||||
|
||||
$parts[] = '<Row ss:StyleID="Header">';
|
||||
foreach ($headers as $h) {
|
||||
$parts[] = '<Cell ss:StyleID="Header"><Data ss:Type="String">' . $esc($h) . '</Data></Cell>';
|
||||
}
|
||||
$parts[] = '</Row>';
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$parts[] = '<Row>';
|
||||
foreach (array_values((array) $row) as $cell) {
|
||||
$parts[] = '<Cell ss:StyleID="Default"><Data ss:Type="String">' . $esc($cell) . '</Data></Cell>';
|
||||
}
|
||||
$parts[] = '</Row>';
|
||||
}
|
||||
|
||||
$parts[] = '</Table>';
|
||||
$parts[] = '</Worksheet>';
|
||||
}
|
||||
|
||||
$parts[] = '</Workbook>';
|
||||
|
||||
$output = implode('', $parts);
|
||||
|
||||
$response = service('response');
|
||||
$response->setHeader('Content-Type', 'application/vnd.ms-excel; charset=UTF-8');
|
||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
$response->setBody($output);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('bag_flow_report_build_spreadsheet')) {
|
||||
/**
|
||||
* 기간별 봉투 수불 엑셀 통합문서 생성 (PhpSpreadsheet — 열 너비·병합 안정)
|
||||
*
|
||||
* @param list<array<string, mixed>> $reportRows
|
||||
* @param list<string> $metaLines
|
||||
*/
|
||||
function bag_flow_report_build_spreadsheet(
|
||||
string $lgName,
|
||||
string $title,
|
||||
array $metaLines,
|
||||
array $reportRows
|
||||
): \PhpOffice\PhpSpreadsheet\Spreadsheet {
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$spreadsheet->getDefaultStyle()->getFont()->setName('맑은 고딕')->setSize(10);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('수불현황');
|
||||
$bodyFontSize = 10;
|
||||
|
||||
$lastCol = 'N';
|
||||
$colWidths = [
|
||||
'A' => 22.0,
|
||||
'B' => 26.0,
|
||||
'C' => 12.0,
|
||||
'D' => 11.0,
|
||||
'E' => 11.0,
|
||||
'F' => 11.0,
|
||||
'G' => 12.0,
|
||||
'H' => 11.0,
|
||||
'I' => 12.0,
|
||||
'J' => 12.0,
|
||||
'K' => 12.0,
|
||||
'L' => 11.0,
|
||||
'M' => 12.0,
|
||||
'N' => 12.0,
|
||||
];
|
||||
foreach ($colWidths as $col => $width) {
|
||||
$sheet->getColumnDimension($col)->setWidth($width);
|
||||
$sheet->getColumnDimension($col)->setAutoSize(false);
|
||||
}
|
||||
|
||||
$r = 1;
|
||||
if ($lgName !== '') {
|
||||
$sheet->setCellValue("A{$r}", $lgName);
|
||||
$sheet->getStyle("A{$r}")->getFont()->setSize(10)->getColor()->setRGB('666666');
|
||||
$r++;
|
||||
}
|
||||
|
||||
$sheet->setCellValue("A{$r}", $title);
|
||||
$sheet->mergeCells("A{$r}:{$lastCol}{$r}");
|
||||
$sheet->getStyle("A{$r}")->getFont()->setBold(true)->setSize($bodyFontSize);
|
||||
$r++;
|
||||
|
||||
foreach ($metaLines as $line) {
|
||||
$sheet->setCellValue("A{$r}", $line);
|
||||
$sheet->mergeCells("A{$r}:{$lastCol}{$r}");
|
||||
$sheet->getStyle("A{$r}")->getFont()->setSize(10)->getColor()->setRGB('555555');
|
||||
$r++;
|
||||
}
|
||||
|
||||
$r++;
|
||||
|
||||
$h1 = $r;
|
||||
$h2 = $r + 1;
|
||||
|
||||
$sheet->setCellValue("A{$h1}", '일자');
|
||||
$sheet->mergeCells("A{$h1}:A{$h2}");
|
||||
$sheet->setCellValue("B{$h1}", '품목');
|
||||
$sheet->mergeCells("B{$h1}:B{$h2}");
|
||||
$sheet->setCellValue("C{$h1}", '전일');
|
||||
$sheet->mergeCells("C{$h1}:C{$h2}");
|
||||
$sheet->setCellValue("D{$h1}", '입고');
|
||||
$sheet->mergeCells("D{$h1}:G{$h1}");
|
||||
$sheet->setCellValue("H{$h1}", '출고');
|
||||
$sheet->mergeCells("H{$h1}:M{$h1}");
|
||||
$sheet->setCellValue("N{$h1}", '잔량');
|
||||
$sheet->mergeCells("N{$h1}:N{$h2}");
|
||||
|
||||
$subHeaders = ['입고', '반품', '기타', '입계', '판매', '일반', '무료', '반품', '기타', '출계'];
|
||||
foreach ($subHeaders as $i => $label) {
|
||||
$col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex(4 + $i);
|
||||
$sheet->setCellValue("{$col}{$h2}", $label);
|
||||
}
|
||||
|
||||
$headerStyle = [
|
||||
'font' => ['bold' => true, 'size' => $bodyFontSize],
|
||||
'alignment' => [
|
||||
'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER,
|
||||
'wrapText' => false,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'E9ECEF'],
|
||||
],
|
||||
'borders' => [
|
||||
'bottom' => ['borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN],
|
||||
],
|
||||
];
|
||||
$sheet->getStyle("A{$h1}:{$lastCol}{$h2}")->applyFromArray($headerStyle);
|
||||
|
||||
$dataRow = $h2 + 1;
|
||||
foreach ($reportRows as $row) {
|
||||
$rowType = (string) ($row['row_type'] ?? 'data');
|
||||
if (! in_array($rowType, ['data', 'subtotal', 'grand'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sheet->fromArray([
|
||||
(string) ($row['date'] ?? ''),
|
||||
(string) ($row['item_name'] ?? ''),
|
||||
(int) ($row['prev_stock'] ?? 0),
|
||||
(int) ($row['recv_in'] ?? 0),
|
||||
(int) ($row['recv_return'] ?? 0),
|
||||
(int) ($row['recv_misc'] ?? 0),
|
||||
(int) ($row['recv_total'] ?? 0),
|
||||
(int) ($row['out_sale'] ?? 0),
|
||||
(int) ($row['out_issue_gen'] ?? 0),
|
||||
(int) ($row['out_issue_free'] ?? 0),
|
||||
(int) ($row['out_return'] ?? 0),
|
||||
(int) ($row['out_misc'] ?? 0),
|
||||
(int) ($row['out_total'] ?? 0),
|
||||
(int) ($row['balance'] ?? 0),
|
||||
], null, "A{$dataRow}", true);
|
||||
|
||||
$sheet->getStyle("C{$dataRow}:{$lastCol}{$dataRow}")
|
||||
->getNumberFormat()
|
||||
->setFormatCode('#,##0');
|
||||
$sheet->getStyle("C{$dataRow}:{$lastCol}{$dataRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT);
|
||||
$sheet->getStyle("A{$dataRow}:B{$dataRow}")
|
||||
->getAlignment()
|
||||
->setHorizontal(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT)
|
||||
->setWrapText(false);
|
||||
$sheet->getStyle("A{$dataRow}:{$lastCol}{$dataRow}")
|
||||
->getFont()
|
||||
->setSize($bodyFontSize);
|
||||
|
||||
if (in_array($rowType, ['subtotal', 'grand'], true)) {
|
||||
$sheet->getStyle("A{$dataRow}:{$lastCol}{$dataRow}")->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => $bodyFontSize],
|
||||
'fill' => [
|
||||
'fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'FFF8E1'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$dataRow++;
|
||||
}
|
||||
|
||||
if ($dataRow > $h2 + 1) {
|
||||
$sheet->getStyle('A' . ($h2 + 1) . ':' . $lastCol . ($dataRow - 1))
|
||||
->getBorders()
|
||||
->getAllBorders()
|
||||
->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_HAIR);
|
||||
}
|
||||
|
||||
$sheet->getPageSetup()->setOrientation(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT);
|
||||
$sheet->getPageSetup()->setPaperSize(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::PAPERSIZE_A4);
|
||||
$sheet->getPageSetup()->setFitToWidth(1);
|
||||
$sheet->getPageSetup()->setFitToHeight(0);
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('export_bag_flow_report_excel')) {
|
||||
/**
|
||||
* 기간별 봉투 수불 (/bag/flow) — 인쇄와 동일한 헤더·2단 표 (xlsx, PhpSpreadsheet)
|
||||
*
|
||||
* @param list<array<string, mixed>> $reportRows
|
||||
* @param list<string> $metaLines
|
||||
*/
|
||||
function export_bag_flow_report_excel(
|
||||
string $filename,
|
||||
string $lgName,
|
||||
string $title,
|
||||
array $metaLines,
|
||||
array $reportRows
|
||||
): void {
|
||||
$baseName = preg_replace('/\.[^.]+$/u', '', $filename);
|
||||
$baseName = preg_replace('/[^\p{L}\p{N}_\-]+/u', '_', $baseName) ?? 'bag_flow';
|
||||
$baseName = trim($baseName, '_') !== '' ? trim($baseName, '_') : 'bag_flow';
|
||||
$filename = $baseName . '.xlsx';
|
||||
|
||||
$spreadsheet = bag_flow_report_build_spreadsheet($lgName, $title, $metaLines, $reportRows);
|
||||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
||||
|
||||
ob_start();
|
||||
try {
|
||||
$writer->save('php://output');
|
||||
} catch (\Throwable $e) {
|
||||
ob_end_clean();
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
unset($spreadsheet);
|
||||
throw $e;
|
||||
}
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
unset($spreadsheet);
|
||||
$output = ob_get_clean();
|
||||
if ($output === false) {
|
||||
$output = '';
|
||||
}
|
||||
|
||||
$response = service('response');
|
||||
$response->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$asciiName = preg_replace('/[^\x20-\x7E]+/', '_', $filename) ?? 'bag_flow.xlsx';
|
||||
$response->setHeader(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="' . $asciiName . '"; filename*=UTF-8\'\'' . rawurlencode($filename)
|
||||
);
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
$response->setBody($output);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('export_xlsx')) {
|
||||
/**
|
||||
* Office Open XML(.xlsx) 브라우저 다운로드 (PhpSpreadsheet)
|
||||
*
|
||||
* @param string $filename 저장 파일명(확장자는 .xlsx로 정규화)
|
||||
* @param string $sheetName 시트 이름(Excel 제한: 길이·일부 문자)
|
||||
* @param string[] $headers 컬럼 헤더
|
||||
* @param array $rows 데이터 행(각 행은 배열)
|
||||
*/
|
||||
function export_xlsx(string $filename, string $sheetName, array $headers, array $rows): void
|
||||
{
|
||||
$filename = preg_replace('/\.[^.]+$/u', '', $filename) . '.xlsx';
|
||||
|
||||
$safeSheet = str_replace(['/', '\\', '?', '*', '[', ']'], '', $sheetName);
|
||||
$safeSheet = function_exists('mb_substr')
|
||||
? mb_substr($safeSheet, 0, 31, 'UTF-8')
|
||||
: substr($safeSheet, 0, 31);
|
||||
if ($safeSheet === '') {
|
||||
$safeSheet = 'Sheet1';
|
||||
}
|
||||
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle($safeSheet);
|
||||
|
||||
$data = [array_map(static fn ($v): string => (string) ($v ?? ''), array_values($headers))];
|
||||
foreach ($rows as $row) {
|
||||
$data[] = array_map(static fn ($v): string => (string) ($v ?? ''), array_values((array) $row));
|
||||
}
|
||||
$sheet->fromArray($data, null, 'A1', true);
|
||||
|
||||
$headerCount = max(1, count($headers));
|
||||
$rowCount = max(1, count($data));
|
||||
$lastCol = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($headerCount);
|
||||
$fullRange = 'A1:' . $lastCol . $rowCount;
|
||||
|
||||
// 헤더/데이터 모두 좌측 정렬(요구사항)
|
||||
$sheet->getStyle($fullRange)->getAlignment()->setHorizontal(
|
||||
\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT
|
||||
);
|
||||
|
||||
// 가독성을 위해 기본 열 너비를 넓게 지정
|
||||
for ($i = 1; $i <= $headerCount; $i++) {
|
||||
$col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i);
|
||||
$sheet->getColumnDimension($col)->setWidth(22);
|
||||
}
|
||||
|
||||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
||||
ob_start();
|
||||
try {
|
||||
$writer->save('php://output');
|
||||
} catch (\Throwable $e) {
|
||||
ob_end_clean();
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
unset($spreadsheet);
|
||||
throw $e;
|
||||
}
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
unset($spreadsheet);
|
||||
$output = ob_get_clean();
|
||||
if ($output === false) {
|
||||
$output = '';
|
||||
}
|
||||
|
||||
$response = service('response');
|
||||
$response->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
||||
$response->setHeader('Pragma', 'no-cache');
|
||||
$response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||
$response->setBody($output);
|
||||
$response->send();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user