★Gemini 2.0 Flash Experimental:htmlだけで印刷見積システム作成中
🤩序
印刷見積システムをhtml+CSS+Javascriptで作ってみようと思い、課金しているClaudeで始めていたんだけど、すぐにtoken数がMAXになってしまい、1日中続けることができない。
token節約のために差分だけを表示してくれるが、差分がどこなのか見つけて挿入するのがかなり面倒で、毎回全コードを出し、あらかじめtxtで開いていおいたhtml全体をコピペで差し替えるやり方なので、プロンプトを出す度に600行のコードがロードされるためtoken overは無理もない。
そんな事しているうちにGemini 2.0がリリースされたというニュースが入ってきたので、作業をGeminiに引き継いでみた。
Geminiはtoken数の制限はあるんでしょうか?Geminiに聞いても制限はあるものの何文字なのか答えてくれないので暫く作業を続けてみた。
使った感想は、
使いまくったがtoken overになることがなかった。どんだけイケるの?
1プロジェクトで百回近く600行のhtmlコードを出させてもサクサク動く。
Claudeより私のプロンプトを理解してくれるw
🤔印刷見積システムを作ろと思った経緯とコンセプトなど
暇だったから。一人正月の遊び道具として
まずは簡単そうな軽オフから始める
このあと更に簡単なPODも追加
その後菊半や菊全等のオフセット大台に挑戦(飽きなければw)
特に軽オフとPODどっちでやった方が安いのかという小ロット案件がよくあるので、今まで営業から相談されると、まず両方出していた。(必要なら印刷通販も選択肢)まぁ工程の選択基準は金額だけじゃないけどね。
選定の目安を最速で得るためにこういったツールはあっても困らないだろうと思った。(なんという暇人)
実はPODの積算システムはvbaでxlsmの台割に既に入れている。
仕様入力し台割を完成させると積算料金も同時に見れるように作ってあるが、台割完成させないと見れないので今回のhtml版に期待がかかっているという訳だ(と言っているのは俺だけw)
🌚課題
天文学的に多い用紙銘柄どうすんの?
(因みにうちの基幹システムに用紙登録したのは私で4000行くらいはあった。各銘柄で斤量や判型バリエーション全部なので。それでもまだ特殊紙は網羅されていない...)
なので、htmlで予測入力みたいのどうやるか勉強しないと。データベースに登録されている銘柄の数文字入れると候補が出てくるアレね。
🎉現時点ではこんな感じ
html+CSS+Javascript
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>印刷見積システム</title>
<style>
.container {
max-width: 1000px;
margin: 0 auto;
padding: 1rem;
}
.input-group {
margin-bottom: 1rem;
}
.input-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.input-row-top {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.input-label {
width: 4rem;
}
input[type="number"], select {
padding: 0.25rem;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="number"] {
width: 5rem;
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
border: 1px solid #ccc;
padding: 0.5rem;
}
th {
background-color: #fff3e6;
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="input-group">
<div class="input-row">
<span>軽オフ:仕上がりA4(B5共通)</span>
</div>
<div class="input-row">
<span>表紙</span>
<select id="coverPrinting">
<option value="1/0">1/0</option>
<option value="1/1">1/1</option>
</select>
<span style="margin-left: 1rem">用紙</span>
<select id="coverPaper">
<option value="pending">用紙選択</option>
</select>
</div>
<div id="endpaperInputs" class="input-row" style="display: none;">
<span class="input-label">見返し</span>
<select id="endpaperPages">
<option value="8">8p</option>
<option value="4">4p</option>
</select>
<select id="endpaperPosition">
<option value="前">前</option>
<option value="後">後</option>
<option value="前後" selected>前後</option>
</select>
<select id="endpaperPrinting">
<option value="0/0">0/0</option>
<option value="1/0">1/0</option>
<option value="1/1">1/1</option>
</select>
<span style="margin-left: 1rem">用紙</span>
<select id="endpaperPaper">
<option value="pending">用紙選択</option>
</select>
</div>
<div class="input-row">
<span class="input-label">本文</span>
<input type="number" id="pages" value="16" min="1">
<span>p</span>
<select id="bodyPrinting">
<option value="1/0">1/0</option>
<option value="1/1" selected>1/1</option>
</select>
<span style="margin-left: 1rem">用紙</span>
<select id="bodyPaper">
<option value="pending">用紙選択</option>
</select>
</div>
<div class="input-row">
<span class="input-label">合紙</span>
<input type="number" id="insertPages" value="0" min="0">
<span>p</span>
<select id="insertPrinting" disabled>
<option value="1/0">1/0</option>
<option value="1/1">1/1</option>
</select>
<span style="margin-left: 1rem">用紙</span>
<select id="insertPaper" disabled>
<option value="pending">用紙選択</option>
</select>
</div>
<div class="input-row">
<span style="margin-left: 0">くるみ</span>
<input type="number" id="quantity" value="100" min="1">
<span>部</span>
<span style="margin-left: 0.5rem">見返し</span>
<select id="endpaperType">
<option value="無し">無し</option>
<option value="有り">有り</option>
<option value="遊びのみ">遊びのみ</option>
<option value="遊びなし">遊びなし</option>
</select>
</div>
</div>
<table>
<thead>
<tr>
<th>工程</th>
<th>工程明細</th>
<th style="width: 4rem;">数量</th>
<th style="width: 4rem;">単位</th>
<th style="width: 4rem;">AP単価</th>
<th>AP</th>
</tr>
</thead>
<tbody id="estimateBody"></tbody>
</table>
<button id="copyButton">表をコピー</button>
</div>
<script>
// 価格テーブル
const paperPriceTable = {
'上質 菊判 125kg': 7.57,
'上質 A判 86.5kg': 4.94,
'上質 A判 70.5kg': 4.15,
'上質 A判 57.5kg': 3.28,
'上質 A判 44.5kg': 2.36,
'上質 A判 35kg': 2.03,
'OKプリンス上質エコグリーン A判 86.5kg': 4.94,
'OKプリンス上質エコグリーン A判 70.5kg': 4.15,
'OKプリンス上質エコグリーン A判 57.5kg': 3.28,
'OKプリンス上質エコグリーン A判 44.5kg': 2.36,
'OKプリンス上質エコグリーン A判 35kg': 2.03,
'レザック66 4/6判 215kg': 72,
'レザック66 4/6判 175kg': 60,
'レザック66 4/6判 130kg': 54,
'色上質 A判 超厚口': 26.316,
'色上質 A判 特厚口': 9.72,
'色上質 A判 最厚口': 12.036,
'色上質 A判 厚口': 7.2,
'色上質 A判 中厚口': 6
};
const coverPriceTable = [
{ max: 100, price: 500 },
{ max: 200, price: 600 },
{ max: 300, price: 700 },
{ max: 400, price: 800 },
{ max: 500, price: 900 },
{ max: 600, price: 1000 },
{ max: 700, price: 1100 },
{ max: 800, price: 1200 },
{ max: 900, price: 1300 },
{ max: 1000, price: 1400 },
{ max: 1100, price: 1500 },
{ max: 1200, price: 1600 },
{ max: 1300, price: 1700 },
{ max: 1400, price: 1800 },
{ max: 1500, price: 1900 },
{ max: 1600, price: 2000 },
{ max: 1700, price: 2100 },
{ max: 1800, price: 2200 },
{ max: 1900, price: 2300 },
{ max: 2000, price: 2400 }
];
const bodyPriceTable = [
{ max: 100, price: 330 },
{ max: 200, price: 400 },
{ max: 300, price: 470 },
{ max: 400, price: 540 },
{ max: 500, price: 610 },
{ max: 600, price: 680 },
{ max: 700, price: 750 },
{ max: 800, price: 820 },
{ max: 900, price: 890 },
{ max: 1000, price: 960 },
{ max: 1100, price: 1030 },
{ max: 1200, price: 1100 },
{ max: 1300, price: 1170 },
{ max: 1400, price: 1240 },
{ max: 1500, price: 1310 },
{ max: 1600, price: 1380 },
{ max: 1700, price: 1450 },
{ max: 1800, price: 1520 },
{ max: 1900, price: 1590 },
{ max: 2000, price: 1660 }
];
const paperOptions = Object.keys(paperPriceTable);
// プルダウンに銘柄を追加
function populatePaperDropdowns() {
const dropdowns = [
document.getElementById('coverPaper'),
document.getElementById('endpaperPaper'),
document.getElementById('bodyPaper'),
document.getElementById('insertPaper')
];
dropdowns.forEach(dropdown => {
paperOptions.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
if (dropdown.id === 'coverPaper' && option === '上質 A判 86.5kg') {
opt.selected = true;
} else if (dropdown.id === 'bodyPaper' && option === '上質 A判 44.5kg'){
opt.selected = true;
}
dropdown.appendChild(opt);
});
});
}
populatePaperDropdowns();
// イベントリスナーの更新
document.getElementById('pages').addEventListener('input', calculateEstimate);
document.getElementById('insertPages').addEventListener('input', calculateEstimate);
document.getElementById('endpaperType').addEventListener('change', function(e) {
const endpaperInputs = document.getElementById('endpaperInputs');
endpaperInputs.style.display = e.target.value === '無し' ? 'none' : 'flex';
calculateEstimate();
});
document.getElementById('insertPages').addEventListener('change', function(e) {
const insertPrinting = document.getElementById('insertPrinting');
const insertPaper = document.getElementById('insertPaper');
const isDisabled = e.target.value == 0;
insertPrinting.disabled = isDisabled;
insertPaper.disabled = isDisabled;
calculateEstimate();
});
const inputs = ['coverPrinting', 'bodyPrinting', 'insertPrinting', 'endpaperPrinting',
'pages', 'insertPages', 'quantity', 'endpaperPages', 'endpaperPosition','coverPaper','endpaperPaper','bodyPaper', 'insertPaper'];
inputs.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('change', calculateEstimate);
}
});
// ユーティリティ関数
function calculatePlates(printing) {
return printing === '1/1' ? 2 : 1;
}
function getCoverPrintingUnitPrice(quantity) {
const entry = coverPriceTable.find(entry => quantity <= entry.max);
return entry ? entry.price : 2400;
}
function getBodyPrintingUnitPrice(quantity) {
const entry = bodyPriceTable.find(entry => quantity <= entry.max);
return entry ? entry.price : 1660;
}
function getPaperUnitPrice(paperName) {
return paperPriceTable[paperName] || 0;
}
// メイン計算関数
function calculateEstimate() {
const tbody = document.getElementById('estimateBody');
tbody.innerHTML = '';
// 入力値の取得
const coverPrinting = document.getElementById('coverPrinting').value;
const bodyPrinting = document.getElementById('bodyPrinting').value;
const pages = parseInt(document.getElementById('pages').value);
const insertPages = parseInt(document.getElementById('insertPages').value);
const insertPrinting = document.getElementById('insertPrinting').value;
const quantity = parseInt(document.getElementById('quantity').value);
const coverPaper = document.getElementById('coverPaper').value;
const endpaperPaper = document.getElementById('endpaperPaper').value;
const bodyPaper = document.getElementById('bodyPaper').value;
const insertPaper = document.getElementById('insertPaper').value;
const endpaperType = document.getElementById('endpaperType').value;
const endpaperPages = parseInt(document.getElementById('endpaperPages').value);
const endpaperPosition = document.getElementById('endpaperPosition').value;
const endpaperPrinting = document.getElementById('endpaperPrinting').value;
const estimates = [];
const coverPrintingUnitPrice = getCoverPrintingUnitPrice(quantity);
const bodyPrintingUnitPrice = getBodyPrintingUnitPrice(quantity);
// 表紙の計算
const coverPlates = calculatePlates(coverPrinting);
const coverPaperUnitPrice = getPaperUnitPrice(coverPaper);
const coverTotalPaperAmount = (coverPlates * quantity + 50) * coverPaperUnitPrice;
estimates.push({
process: '用紙',
detail: `${coverPaper}`,
quantity: coverPlates * quantity + 50,
unit: '枚',
unitPrice: coverPaperUnitPrice,
amount: coverTotalPaperAmount
});
estimates.push({
process: '刷版',
detail: `軽オフA3×${coverPrinting} 表紙`,
quantity: coverPlates,
unit: '版',
unitPrice: 300,
amount: coverPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${coverPrinting}×${quantity}t 表紙`,
quantity: coverPlates,
unit: '版',
unitPrice: coverPrintingUnitPrice,
amount: coverPlates * coverPrintingUnitPrice
});
// 見返しの計算
let endpaperTotalPaperAmount = 0;
if (endpaperType !== '無し' ) {
const endpaperStations = endpaperPosition === '前後' ? 2 : 1;
const endpaperTotalPlates = endpaperStations * calculatePlates(endpaperPrinting);
const endpaperPaperUnitPrice = getPaperUnitPrice(endpaperPaper);
const endpaperPrintingCalc = endpaperPrinting === '0/0' ? '1/0' : endpaperPrinting;
const endpaperTotalPlatesCalc = endpaperStations * calculatePlates(endpaperPrintingCalc);
endpaperTotalPaperAmount = (endpaperPages / 4) * (quantity + 50) * endpaperPaperUnitPrice
if(endpaperPrinting !== '0/0'){
estimates.push({
process: '刷版',
detail: `軽オフA3×${endpaperPrinting}×${endpaperStations}台 見返し`,
quantity: endpaperTotalPlates,
unit: '版',
unitPrice: 300,
amount: endpaperTotalPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${endpaperPrinting}×${endpaperStations}台×${quantity}t 見返し`,
quantity: endpaperTotalPlates,
unit: '版',
unitPrice: bodyPrintingUnitPrice,
amount: endpaperTotalPlates * bodyPrintingUnitPrice
});
}
estimates.push({
process: '用紙',
detail: `${endpaperPaper}`,
quantity: (endpaperPages / 4) * (quantity + 50) ,
unit: '枚',
unitPrice: endpaperPaperUnitPrice,
amount: endpaperTotalPaperAmount
});
}
// 本文の計算
const bodyStations = Math.floor(pages / 4);
const remainingPages = pages % 4;
const bodyTotalPlates = bodyStations * calculatePlates(bodyPrinting);
const bodyPaperUnitPrice = getPaperUnitPrice(bodyPaper);
let bodyTotalPaperAmount = 0;
// 通常の台数分の用紙計算
if(bodyStations > 0){
const regularPages = bodyStations * 4;
bodyTotalPaperAmount = (quantity + 50) * bodyStations * bodyPaperUnitPrice
estimates.push({
process: '刷版',
detail: `軽オフA3×${bodyPrinting}×${bodyStations}台 本文${regularPages}p`,
quantity: bodyTotalPlates,
unit: '版',
unitPrice: 300,
amount: bodyTotalPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${bodyPrinting}×${bodyStations}台×${quantity}t 本文${regularPages}p`,
quantity: bodyTotalPlates,
unit: '版',
unitPrice: bodyPrintingUnitPrice,
amount: bodyTotalPlates * bodyPrintingUnitPrice
});
estimates.push({
process: '用紙',
detail: `${bodyPaper}`,
quantity: (quantity + 50) * bodyStations,
unit: '枚',
unitPrice: bodyPaperUnitPrice,
amount: bodyTotalPaperAmount
});
}
// 余りページがある場合の追加処理
if (remainingPages > 0) {
const additionalPlates = calculatePlates(bodyPrinting);
const halfQuantity = Math.ceil(quantity/2);
bodyTotalPaperAmount = (halfQuantity + 50) * 1 * bodyPaperUnitPrice
estimates.push({
process: '刷版',
detail: `軽オフA3×${bodyPrinting}×1台 本文${remainingPages}p`,
quantity: additionalPlates,
unit: '版',
unitPrice: 300,
amount: additionalPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${bodyPrinting}×1台×${halfQuantity}t 本文${remainingPages}p`,
quantity: additionalPlates,
unit: '版',
unitPrice: bodyPrintingUnitPrice,
amount: additionalPlates * bodyPrintingUnitPrice
});
estimates.push({
process: '用紙',
detail: `${bodyPaper}`,
quantity: (halfQuantity + 50) * 1,
unit: '枚',
unitPrice: bodyPaperUnitPrice,
amount: bodyTotalPaperAmount
});
};
// 合紙の計算
let insertTotalPaperAmount = 0;
if (insertPages > 0) {
const insertStations = Math.floor(insertPages / 4);
const remainingInsertPages = insertPages % 4;
const insertTotalPlates = insertStations * calculatePlates(insertPrinting);
const insertPaperUnitPrice = getPaperUnitPrice(insertPaper);
// 通常の台数分の用紙計算
if (insertStations > 0) {
insertTotalPaperAmount = (insertTotalPlates * quantity + 50) * insertPaperUnitPrice;
const regularInsertPages = insertStations * 4;
estimates.push({
process: '刷版',
detail: `軽オフA3×${insertPrinting}×${insertStations}台 合紙${regularInsertPages}p`,
quantity: insertTotalPlates,
unit: '版',
unitPrice: 300,
amount: insertTotalPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${insertPrinting}×${insertStations}台×${quantity}t 合紙${regularInsertPages}p`,
quantity: insertTotalPlates,
unit: '版',
unitPrice: bodyPrintingUnitPrice,
amount: insertTotalPlates * bodyPrintingUnitPrice
});
estimates.push({
process: '用紙',
detail: `${insertPaper}`,
quantity: insertTotalPlates * quantity + 50,
unit: '枚',
unitPrice: insertPaperUnitPrice,
amount: insertTotalPaperAmount
});
}
// 余りページがある場合の追加処理
if (remainingInsertPages > 0) {
const additionalPlates = calculatePlates(insertPrinting);
const halfQuantity = Math.ceil(quantity/2);
insertTotalPaperAmount = (additionalPlates * halfQuantity + 50) * insertPaperUnitPrice
estimates.push({
process: '刷版',
detail: `軽オフA3×${insertPrinting}×1台 合紙${remainingInsertPages}p`,
quantity: additionalPlates,
unit: '版',
unitPrice: 300,
amount: additionalPlates * 300
});
estimates.push({
process: '印刷',
detail: `軽オフA3×${insertPrinting}×1台×${halfQuantity}t 合紙${remainingInsertPages}p`,
quantity: additionalPlates,
unit: '版',
unitPrice: bodyPrintingUnitPrice,
amount: additionalPlates * bodyPrintingUnitPrice
});
estimates.push({
process: '用紙',
detail: `${insertPaper}`,
quantity: additionalPlates * halfQuantity + 50,
unit: '枚',
unitPrice: insertPaperUnitPrice,
amount: insertTotalPaperAmount
});
}
}
// 製本の計算
const totalPages = pages + insertPages;
const bindingStations = Math.ceil(totalPages / 2);
const bindingUnitPrice = (totalPages / 2 * 0.7) + 15 + (endpaperType !== '無し' ? 5 : 0);
estimates.push({
process: '製本',
detail: `くるみ${endpaperType !== '無し' ? '見返し' : ''}A4×${totalPages}p${bindingStations}台`,
quantity: quantity,
unit: '部',
unitPrice: bindingUnitPrice,
amount: quantity * bindingUnitPrice
});
// 表の更新
let total = 0;
const sortedEstimates = [];
// 印刷の後に用紙を挿入するように調整
const paperEstimates = estimates.filter(estimate => estimate.process === '用紙');
const nonPaperEstimates = estimates.filter(estimate => estimate.process !== '用紙');
sortedEstimates.push(...nonPaperEstimates);
const bindingIndex = sortedEstimates.findIndex(item => item.process === '製本');
if(bindingIndex !== -1){
sortedEstimates.splice(bindingIndex,0,...paperEstimates);
} else {
sortedEstimates.push(...paperEstimates);
}
sortedEstimates.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.process}</td>
<td>${item.detail}</td>
<td class="text-right">${item.quantity}</td>
<td class="text-center">${item.unit}</td>
<td class="text-right">${item.unitPrice.toFixed(2)}</td>
<td class="text-right">${Math.floor(item.amount).toLocaleString()}</td>
`;
tbody.appendChild(row);
total += item.amount;
});
// 合計行の追加
const totalRow = document.createElement('tr');
totalRow.innerHTML = `
<td colspan="5" class="text-right">合計</td>
<td class="text-right">${Math.floor(total).toLocaleString()}</td>
`;
tbody.appendChild(totalRow);
}
// テーブルをコピーする処理
document.getElementById('copyButton').addEventListener('click', function() {
const table = document.querySelector('table');
const range = document.createRange();
range.selectNode(table);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
try {
document.execCommand('copy');
window.getSelection().removeAllRanges();
alert('テーブルの内容をコピーしました!'); // コピー成功時の通知
} catch (err) {
console.error('クリップボードへのコピーに失敗しました', err);
alert('クリップボードへのコピーに失敗しました'); // コピー失敗時の通知
}
});
// 初期計算
calculateEstimate();
</script>
</body>
</html>