Files
sam-hotfix/research/flow-chart/estimate-pptx-generator.js
김보곤 d86b5851d0 chore: 프로젝트 설정 및 문서 파일 추가
- .agent/, .claude/, .vscode/ 설정 파일
- design/ 디자인 리소스
- reports/, research/ 분석 문서
- testcase/ 테스트 케이스 문서
- db_sync_chandj.bat, sam.code-workspace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:51:43 +09:00

566 lines
15 KiB
JavaScript

/**
* SAM ERP 견적서 PPTX 생성기 (PDF 샘플 구조 기반)
*/
const fs = require('fs').promises;
const PptxGenJS = require('pptxgenjs');
// 견적서 데이터 구조
class EstimateData {
constructor() {
this.company = '(주) 주일기업';
this.documentNumber = 'ABC123';
this.date = new Date().toISOString().split('T')[0];
this.client = {
name: '회사명',
site: '현장명',
address: '주소명',
contact: '연락처',
phone: '010-1234-5678'
};
this.items = [
{
no: 1,
name: 'FSSB01(주차장)',
product: '제품명',
width: 2530,
height: 2550,
quantity: 1,
unit: 'SET',
materialCost: 1420000,
laborCost: 510000,
totalCost: 1930000,
memo: ''
},
{
no: 2,
name: 'FSSB02(주차장)',
product: '제품명',
width: 7500,
height: 2550,
quantity: 1,
unit: 'SET',
materialCost: 4720000,
laborCost: 780000,
totalCost: 5500000,
memo: ''
}
];
this.summary = {
description: '셔터설치공사',
quantity: 1,
unit: '식',
materialTotal: 78540000,
laborTotal: 15410000,
grandTotal: 93950000
};
this.notes = '부가세 별도 / 현설조건에 따름';
}
}
// 견적서 PPTX 생성기
class EstimatePPTXGenerator {
constructor() {
this.pptx = new PptxGenJS();
this.pptx.layout = 'LAYOUT_16x9';
this.slideNumber = 1;
// 색상 정의
this.colors = {
primary: '0066CC', // SAM 파란색
secondary: '333333', // 진한 회색
headerBg: 'F0F8FF', // 연한 파란색
border: 'CCCCCC', // 테두리 회색
white: 'FFFFFF',
black: '000000',
red: 'CC0000',
green: '008000'
};
}
/**
* 견적서 PPTX 생성
*/
async generateEstimate(estimateData) {
console.log('📊 SAM ERP 견적서 PPTX 생성 중...');
// 1. 표지 슬라이드
this.createCoverSlide(estimateData);
// 2. 견적관리 메인 화면
this.createMainScreenSlide(estimateData);
// 3. 견적 상세 화면
this.createDetailScreenSlide(estimateData);
// 4. 견적서 문서 (요약)
this.createEstimateDocumentSlide(estimateData);
// 5. 견적서 문서 (상세)
this.createEstimateDetailSlide(estimateData);
return this.pptx;
}
/**
* 표지 슬라이드 생성
*/
createCoverSlide(estimateData) {
const slide = this.pptx.addSlide();
// 배경
slide.background = { color: this.colors.primary };
// SAM 로고 영역
slide.addShape('rect', {
x: 1, y: 1, w: 2, h: 1,
fill: { color: this.colors.white },
line: { color: this.colors.border, width: 1 }
});
slide.addText('SAM', {
x: 1, y: 1, w: 2, h: 0.5,
fontSize: 24,
bold: true,
color: this.colors.primary,
align: 'center'
});
slide.addText('Smart Automation Management', {
x: 1, y: 1.5, w: 2, h: 0.5,
fontSize: 10,
color: this.colors.secondary,
align: 'center'
});
// 메인 제목
slide.addText('견적서 시스템', {
x: 2, y: 3, w: 6, h: 1.5,
fontSize: 48,
bold: true,
color: this.colors.white,
align: 'center'
});
// 부제목
slide.addText('SAM ERP 견적관리 시스템', {
x: 2, y: 4.8, w: 6, h: 0.8,
fontSize: 24,
color: this.colors.white,
align: 'center'
});
// 날짜 및 회사정보
slide.addText(`${estimateData.date}\n\n${estimateData.company}`, {
x: 7.5, y: 7, w: 2.5, h: 1.5,
fontSize: 12,
color: this.colors.white,
align: 'right'
});
console.log(`✅ 슬라이드 ${this.slideNumber}: 표지`);
this.slideNumber++;
}
/**
* 견적관리 메인 화면 슬라이드
*/
createMainScreenSlide(estimateData) {
const slide = this.pptx.addSlide();
// 제목
slide.addText('견적관리', {
x: 0.5, y: 0.3, w: 6, h: 0.6,
fontSize: 24,
bold: true,
color: this.colors.secondary
});
// 설명
slide.addText('견적을 관리합니다', {
x: 0.5, y: 0.9, w: 6, h: 0.4,
fontSize: 14,
color: this.colors.secondary
});
// 필터 영역
slide.addShape('rect', {
x: 0.5, y: 1.5, w: 9, h: 1,
fill: { color: this.colors.headerBg },
line: { color: this.colors.border, width: 1 }
});
// 필터 컨트롤들
const filters = [
{ label: '거래처', value: '전체 ▼' },
{ label: '견적자', value: '전체 ▼' },
{ label: '상태', value: '전체 ▼' },
{ label: '정렬', value: '최신순 ▼' }
];
filters.forEach((filter, index) => {
const xPos = 0.7 + index * 2;
slide.addText(`${filter.label}: ${filter.value}`, {
x: xPos, y: 1.7, w: 1.8, h: 0.6,
fontSize: 10,
color: this.colors.secondary
});
});
// 통계 박스들
const stats = [
{ label: '전체 견적', value: '9', color: this.colors.primary },
{ label: '견적대기', value: '5', color: this.colors.red },
{ label: '견적완료', value: '4', color: this.colors.green }
];
stats.forEach((stat, index) => {
const xPos = 2 + index * 2;
slide.addShape('rect', {
x: xPos, y: 2.8, w: 1.8, h: 1.2,
fill: { color: this.colors.white },
line: { color: this.colors.border, width: 1 }
});
slide.addText(stat.value, {
x: xPos, y: 3, w: 1.8, h: 0.8,
fontSize: 24,
bold: true,
color: stat.color,
align: 'center'
});
slide.addText(stat.label, {
x: xPos, y: 3.6, w: 1.8, h: 0.4,
fontSize: 12,
color: this.colors.secondary,
align: 'center'
});
});
// 견적 목록 테이블
const tableData = [
['견적번호', '거래처', '현장명', '견적자', '총 개소', '견적금액', '견적완료일', '입찰일', '상태', '작업'],
['123123', '회사명', '현장명', '홍길동', '21', '100,000,000', '-', '2025-12-15', '견적대기', '✏️'],
['123124', '회사명', '현장명', '홍길동', '5', '10,000,000', '2025-12-12', '2025-12-15', '견적완료', '✏️']
];
slide.addTable(tableData, {
x: 0.5, y: 4.5, w: 9, h: 2.5,
border: { pt: 1, color: this.colors.border },
fontSize: 10,
color: this.colors.secondary,
fill: { color: this.colors.white },
margin: 0.1
});
console.log(`✅ 슬라이드 ${this.slideNumber}: 견적관리 메인`);
this.slideNumber++;
}
/**
* 견적 상세 화면 슬라이드
*/
createDetailScreenSlide(estimateData) {
const slide = this.pptx.addSlide();
// 제목
slide.addText('견적 상세', {
x: 0.5, y: 0.3, w: 6, h: 0.6,
fontSize: 24,
bold: true,
color: this.colors.secondary
});
// 액션 버튼들
const buttons = ['견적서 보기', '전자결재', '수정'];
buttons.forEach((button, index) => {
slide.addShape('rect', {
x: 4 + index * 1.5, y: 0.3, w: 1.3, h: 0.6,
fill: { color: this.colors.primary },
line: { color: this.colors.primary, width: 1 }
});
slide.addText(button, {
x: 4 + index * 1.5, y: 0.3, w: 1.3, h: 0.6,
fontSize: 12,
bold: true,
color: this.colors.white,
align: 'center'
});
});
// 견적 정보 영역
slide.addShape('rect', {
x: 0.5, y: 1.2, w: 9, h: 1.8,
fill: { color: this.colors.white },
line: { color: this.colors.border, width: 1 }
});
slide.addText('견적 정보', {
x: 0.7, y: 1.4, w: 2, h: 0.4,
fontSize: 14,
bold: true,
color: this.colors.secondary
});
// 견적 정보 필드들
const fields = [
{ label: '견적번호', value: '123123' },
{ label: '견적자', value: '이름' },
{ label: '견적금액', value: '1,420,000' },
{ label: '상태', value: '견적대기' }
];
fields.forEach((field, index) => {
const row = Math.floor(index / 2);
const col = index % 2;
const xPos = 1 + col * 4;
const yPos = 1.8 + row * 0.6;
slide.addText(`${field.label}: ${field.value}`, {
x: xPos, y: yPos, w: 3.5, h: 0.4,
fontSize: 11,
color: this.colors.secondary
});
});
// 현장설명회 정보 영역
slide.addShape('rect', {
x: 0.5, y: 3.2, w: 9, h: 1.8,
fill: { color: this.colors.white },
line: { color: this.colors.border, width: 1 }
});
slide.addText('현장설명회 정보', {
x: 0.7, y: 3.4, w: 2, h: 0.4,
fontSize: 14,
bold: true,
color: this.colors.secondary
});
// 입찰 정보 영역
slide.addShape('rect', {
x: 0.5, y: 5.2, w: 9, h: 1.5,
fill: { color: this.colors.white },
line: { color: this.colors.border, width: 1 }
});
slide.addText('입찰 정보', {
x: 0.7, y: 5.4, w: 2, h: 0.4,
fontSize: 14,
bold: true,
color: this.colors.secondary
});
console.log(`✅ 슬라이드 ${this.slideNumber}: 견적 상세`);
this.slideNumber++;
}
/**
* 견적서 문서 (요약) 슬라이드
*/
createEstimateDocumentSlide(estimateData) {
const slide = this.pptx.addSlide();
// 견적서 제목
slide.addText('견적서', {
x: 3, y: 0.5, w: 4, h: 0.8,
fontSize: 32,
bold: true,
color: this.colors.secondary,
align: 'center'
});
// 문서번호 및 작성일자
slide.addText(`문서번호: ${estimateData.documentNumber} | 작성일자: ${estimateData.date}`, {
x: 3, y: 1.3, w: 4, h: 0.4,
fontSize: 12,
color: this.colors.secondary,
align: 'center'
});
// 회사 정보 테이블
const companyInfo = [
['귀중', estimateData.client.name, estimateData.company],
['현장명', estimateData.client.site, '주소', estimateData.client.address],
['금액', '(金)구천삼백구십오만 원정', '일자', estimateData.date],
['연락처', estimateData.client.contact, '연락처', 'H.P: 010-3679-2188\nTEL: (02) 849-5130\nFAX: (02) 6911-6315']
];
slide.addTable(companyInfo, {
x: 1, y: 2, w: 8, h: 2,
border: { pt: 1, color: this.colors.border },
fontSize: 11,
color: this.colors.secondary,
fill: { color: this.colors.white },
margin: 0.1
});
slide.addText('하기와 같이 見積합니다.', {
x: 8, y: 3.7, w: 2, h: 0.4,
fontSize: 12,
color: this.colors.secondary,
align: 'center'
});
// 견적 요약 테이블
const summaryData = [
['명칭', '수량', '단위', '재료비', '노무비', '합계', '비고'],
[
estimateData.summary.description,
estimateData.summary.quantity.toString(),
estimateData.summary.unit,
this.formatCurrency(estimateData.summary.materialTotal),
this.formatCurrency(estimateData.summary.laborTotal),
this.formatCurrency(estimateData.summary.grandTotal),
''
],
['합계', '', '', '', '', this.formatCurrency(estimateData.summary.grandTotal), '']
];
slide.addTable(summaryData, {
x: 1, y: 4.5, w: 8, h: 1.5,
border: { pt: 1, color: this.colors.border },
fontSize: 11,
color: this.colors.secondary,
fill: { color: this.colors.white },
margin: 0.1
});
// 특기사항
slide.addText(`* 특기사항: ${estimateData.notes}`, {
x: 1, y: 6.2, w: 8, h: 0.4,
fontSize: 11,
color: this.colors.secondary
});
console.log(`✅ 슬라이드 ${this.slideNumber}: 견적서 문서 (요약)`);
this.slideNumber++;
}
/**
* 견적서 문서 (상세) 슬라이드
*/
createEstimateDetailSlide(estimateData) {
const slide = this.pptx.addSlide();
// 상세 내역 제목
slide.addText('견적 상세 내역', {
x: 3, y: 0.5, w: 4, h: 0.6,
fontSize: 20,
bold: true,
color: this.colors.secondary,
align: 'center'
});
// 상세 테이블 헤더
const detailHeaders = [
'NO', '명칭', '제품', '규격(mm)', '', '수량', '단위', '재료비', '', '노무비', '', '합계', '', '비고'
];
const subHeaders = [
'', '', '', '가로(W)', '높이(H)', '', '', '단가', '금액', '단가', '금액', '단가', '금액', ''
];
// 테이블 데이터 생성
const tableData = [detailHeaders, subHeaders];
estimateData.items.forEach(item => {
tableData.push([
item.no.toString(),
item.name,
item.product,
item.width.toLocaleString(),
item.height.toLocaleString(),
item.quantity.toString(),
item.unit,
this.formatCurrency(item.materialCost),
this.formatCurrency(item.materialCost * item.quantity),
this.formatCurrency(item.laborCost),
this.formatCurrency(item.laborCost * item.quantity),
this.formatCurrency(item.totalCost),
this.formatCurrency(item.totalCost * item.quantity),
item.memo
]);
});
// 합계 행
const totalMaterialCost = estimateData.items.reduce((sum, item) => sum + (item.materialCost * item.quantity), 0);
const totalLaborCost = estimateData.items.reduce((sum, item) => sum + (item.laborCost * item.quantity), 0);
const totalCost = estimateData.items.reduce((sum, item) => sum + (item.totalCost * item.quantity), 0);
tableData.push([
'', '합계', '', '', '',
estimateData.items.reduce((sum, item) => sum + item.quantity, 0).toString(),
'SET',
this.formatCurrency(totalMaterialCost), '',
this.formatCurrency(totalLaborCost), '',
this.formatCurrency(totalCost), '', ''
]);
slide.addTable(tableData, {
x: 0.2, y: 1.5, w: 9.6, h: 4,
border: { pt: 1, color: this.colors.border },
fontSize: 8,
color: this.colors.secondary,
fill: { color: this.colors.white },
margin: 0.05
});
console.log(`✅ 슬라이드 ${this.slideNumber}: 견적서 문서 (상세)`);
this.slideNumber++;
}
/**
* 통화 형식 변환
*/
formatCurrency(amount) {
return '₩' + amount.toLocaleString();
}
}
// 메인 실행 함수
async function generateEstimatePPTX(outputPath = 'pptx/estimate_presentation.pptx') {
try {
console.log('🚀 SAM ERP 견적서 PPTX 생성 시작');
console.log(`📊 출력: ${outputPath}`);
// 1. 샘플 견적 데이터 생성
const estimateData = new EstimateData();
// 2. PPTX 생성
const generator = new EstimatePPTXGenerator();
const pptx = await generator.generateEstimate(estimateData);
// 3. 파일 저장
await pptx.writeFile({ fileName: outputPath });
console.log('✅ 견적서 PPTX 생성 완료!');
return outputPath;
} catch (error) {
console.error('❌ 생성 실패:', error.message);
throw error;
}
}
// 명령행 인수 처리
async function main() {
const args = process.argv.slice(2);
const outputFlag = args.find(arg => arg.startsWith('--output='));
const outputPath = outputFlag ? outputFlag.split('=')[1] : 'pptx/estimate_presentation.pptx';
// 출력 디렉토리 생성
await fs.mkdir('pptx', { recursive: true });
await generateEstimatePPTX(outputPath);
}
// 직접 실행시
if (require.main === module) {
main().catch(console.error);
}
module.exports = { generateEstimatePPTX, EstimateData, EstimatePPTXGenerator };