- .agent/, .claude/, .vscode/ 설정 파일 - design/ 디자인 리소스 - reports/, research/ 분석 문서 - testcase/ 테스트 케이스 문서 - db_sync_chandj.bat, sam.code-workspace Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
566 lines
15 KiB
JavaScript
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 }; |