feat(WEB): 입찰/계약/주문관리 기능 추가 및 견적 상세 리팩토링
- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터 - 계약관리: 목록/상세/수정 페이지 구현 - 주문관리: 수주/발주 목록 및 상세 페이지 구현 - 견적 상세 폼: 섹션별 분리 및 hooks/utils 리팩토링 - 품목관리, 카테고리관리, 단가관리 기능 추가 - 현장설명회/협력업체 폼 개선 - 프린트 유틸리티 공통화 (print-utils.ts) - 문서 모달 공통 컴포넌트 정리 - IntegratedListTemplateV2, StatCards 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
190
src/lib/print-utils.ts
Normal file
190
src/lib/print-utils.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 인쇄 유틸리티 함수
|
||||
* 특정 요소만 인쇄하기 위한 헬퍼 함수들
|
||||
*/
|
||||
|
||||
interface PrintOptions {
|
||||
/** 문서 제목 (브라우저 인쇄 다이얼로그에 표시) */
|
||||
title?: string;
|
||||
/** 추가 CSS 스타일 */
|
||||
styles?: string;
|
||||
/** 인쇄 후 창 닫기 여부 */
|
||||
closeAfterPrint?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 요소의 내용만 인쇄합니다.
|
||||
* @param elementOrSelector - 인쇄할 요소 또는 CSS 선택자
|
||||
* @param options - 인쇄 옵션
|
||||
*/
|
||||
export function printElement(
|
||||
elementOrSelector: HTMLElement | string,
|
||||
options: PrintOptions = {}
|
||||
): void {
|
||||
const {
|
||||
title = '문서 인쇄',
|
||||
styles = '',
|
||||
closeAfterPrint = true,
|
||||
} = options;
|
||||
|
||||
// 요소 찾기
|
||||
const element =
|
||||
typeof elementOrSelector === 'string'
|
||||
? document.querySelector(elementOrSelector)
|
||||
: elementOrSelector;
|
||||
|
||||
if (!element) {
|
||||
console.error('인쇄할 요소를 찾을 수 없습니다:', elementOrSelector);
|
||||
return;
|
||||
}
|
||||
|
||||
// 인쇄용 새 창 열기
|
||||
const printWindow = window.open('', '_blank', 'width=800,height=600');
|
||||
if (!printWindow) {
|
||||
console.error('팝업 창을 열 수 없습니다. 팝업 차단을 확인해주세요.');
|
||||
alert('인쇄 창을 열 수 없습니다. 팝업 차단을 해제해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 페이지의 스타일시트 수집
|
||||
const styleSheets = Array.from(document.styleSheets)
|
||||
.map((styleSheet) => {
|
||||
try {
|
||||
if (styleSheet.href) {
|
||||
return `<link rel="stylesheet" href="${styleSheet.href}">`;
|
||||
}
|
||||
if (styleSheet.cssRules) {
|
||||
const rules = Array.from(styleSheet.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join('\n');
|
||||
return `<style>${rules}</style>`;
|
||||
}
|
||||
} catch (e) {
|
||||
// CORS 에러 등으로 접근 불가한 스타일시트는 무시
|
||||
if (styleSheet.href) {
|
||||
return `<link rel="stylesheet" href="${styleSheet.href}">`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// 기본 인쇄 스타일
|
||||
const defaultPrintStyles = `
|
||||
<style>
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 10mm;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
color-adjust: exact !important;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
width: 100%;
|
||||
max-width: 190mm;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* 그림자, 라운드 제거 */
|
||||
.print-container * {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 테이블 스타일 */
|
||||
table {
|
||||
border-collapse: collapse !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 화면 표시용 스타일 */
|
||||
@media screen {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
max-width: 210mm;
|
||||
margin: 0 auto;
|
||||
padding: 20mm;
|
||||
background: white;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
${styles}
|
||||
</style>
|
||||
`;
|
||||
|
||||
// 인쇄할 내용 복제 및 정리
|
||||
const contentClone = element.cloneNode(true) as HTMLElement;
|
||||
|
||||
// print-hidden 요소 제거
|
||||
contentClone.querySelectorAll('.print-hidden').forEach((el) => el.remove());
|
||||
|
||||
// 불필요한 클래스 및 스타일 정리
|
||||
contentClone.classList.remove('print-area');
|
||||
contentClone.style.cssText = '';
|
||||
|
||||
// 내부 wrapper의 스타일 정리
|
||||
const innerWrapper = contentClone.querySelector(':scope > div');
|
||||
if (innerWrapper instanceof HTMLElement) {
|
||||
innerWrapper.style.cssText = 'max-width: none; margin: 0; padding: 0; box-shadow: none; border-radius: 0;';
|
||||
}
|
||||
|
||||
// HTML 작성
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
${styleSheets}
|
||||
${defaultPrintStyles}
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-container">
|
||||
${contentClone.innerHTML}
|
||||
</div>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
setTimeout(function() {
|
||||
window.print();
|
||||
${closeAfterPrint ? 'window.close();' : ''}
|
||||
}, 250);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
printWindow.document.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* .print-area 클래스를 가진 요소를 인쇄합니다.
|
||||
* @param options - 인쇄 옵션
|
||||
*/
|
||||
export function printArea(options: PrintOptions = {}): void {
|
||||
printElement('.print-area', options);
|
||||
}
|
||||
Reference in New Issue
Block a user