623 lines
22 KiB
JavaScript
623 lines
22 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
const { Builder, Browser } = require('selenium-webdriver');
|
||
const chrome = require('selenium-webdriver/chrome');
|
||
const { JSDOM } = require('jsdom');
|
||
const { stringify } = require('csv-stringify/sync');
|
||
|
||
const SCRIPT_DIR = __dirname;
|
||
const ROOT_DIR = path.resolve(SCRIPT_DIR, '..');
|
||
|
||
function resolveScriptPath(targetPath) {
|
||
if (!targetPath) {
|
||
return SCRIPT_DIR;
|
||
}
|
||
if (path.isAbsolute(targetPath)) {
|
||
return targetPath;
|
||
}
|
||
return path.resolve(SCRIPT_DIR, targetPath);
|
||
}
|
||
|
||
function parseArgs(argv) {
|
||
const args = {
|
||
input: './calendarData.html',
|
||
format: 'json',
|
||
today: false,
|
||
out: '',
|
||
title: '設備予約',
|
||
location: ''
|
||
};
|
||
|
||
for (let i = 0; i < argv.length; i += 1) {
|
||
const a = argv[i];
|
||
if (a === '--in' && argv[i + 1]) {
|
||
args.input = String(argv[i + 1]);
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (a === '--csv') {
|
||
args.format = 'csv';
|
||
continue;
|
||
}
|
||
if (a === '--html') {
|
||
args.format = 'html';
|
||
continue;
|
||
}
|
||
if (a === '--today') {
|
||
args.today = true;
|
||
continue;
|
||
}
|
||
if (a === '--json') {
|
||
args.format = 'json';
|
||
continue;
|
||
}
|
||
if (a === '--out' && argv[i + 1]) {
|
||
args.out = String(argv[i + 1]);
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (a === '--title' && argv[i + 1]) {
|
||
args.title = String(argv[i + 1]);
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if ((a === '--location' || a === '--site') && argv[i + 1]) {
|
||
args.location = String(argv[i + 1]);
|
||
i += 1;
|
||
continue;
|
||
}
|
||
if (a === '--help' || a === '-h') {
|
||
args.help = true;
|
||
}
|
||
}
|
||
|
||
return args;
|
||
}
|
||
|
||
function printHelp() {
|
||
console.log('設備予約HTML(calendarData.html)を解析して JSON/CSV を出力します');
|
||
console.log('');
|
||
console.log('使い方:');
|
||
console.log(' node analyzeCalendar.js --json # JSON出力 (default)');
|
||
console.log(' node analyzeCalendar.js --csv # CSV出力 (設備名,開始時間,終了時間,内容,登録者)');
|
||
console.log(' node analyzeCalendar.js --html # HTML出力 (table)');
|
||
console.log(' node analyzeCalendar.js --today # 今日の予定をHTML出力 (table)');
|
||
console.log(' node analyzeCalendar.js --csv --out out.csv # CSVをファイル保存');
|
||
console.log(' node analyzeCalendar.js --html --out out.html # HTMLをファイル保存');
|
||
console.log(' node analyzeCalendar.js --in calendarData.html # 入力HTML指定 (scripts基準)');
|
||
console.log(' node analyzeCalendar.js --html --location 日野本社 # タイトルに設置先を付加');
|
||
console.log(' node analyzeCalendar.js --html --title 設備予約一覧 # タイトル文言を上書き');
|
||
}
|
||
|
||
function formatDateYYYYMMDDWithHyphen(date) {
|
||
const y = date.getFullYear();
|
||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
return `${y}-${m}-${d}`;
|
||
}
|
||
|
||
function buildNextDayCalendarUrl(isToday) {
|
||
const now = new Date();
|
||
let targetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||
if (isToday) {
|
||
targetDate = now;
|
||
}
|
||
const date = formatDateYYYYMMDDWithHyphen(targetDate);
|
||
return `https://calendar.worksmobile.com/web/resource/bookmark?date=${date}`;
|
||
}
|
||
|
||
async function fetchCalendarPageSource(outPath, isToday) {
|
||
const targetUrl = buildNextDayCalendarUrl(isToday);
|
||
const authUrl = 'https://auth.worksmobile.com/login/login';
|
||
let driver = null;
|
||
|
||
try {
|
||
const chromeOptions = new chrome.Options().addArguments('--start-maximized');
|
||
driver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(chromeOptions).build();
|
||
|
||
// 先に認証ページを開いて手動ログインを待機
|
||
await driver.get(authUrl);
|
||
try {
|
||
await driver.wait(async () => {
|
||
const href = await driver.getCurrentUrl();
|
||
return !String(href || '').includes('/login/login');
|
||
}, 5 * 60 * 1000);
|
||
} catch (_authWaitTimeout) {
|
||
// タイムアウト時も遷移先で再認証の可能性があるため処理は継続
|
||
}
|
||
|
||
await driver.get(targetUrl);
|
||
|
||
// 認証画面が出るケースを想定し、最大5分間カレンダー画面の表示を待機する
|
||
await driver.wait(async () => {
|
||
return driver.executeScript(() => {
|
||
const href = String(window.location.href || '');
|
||
const hasCalendarGrid = !!document.querySelector('.time_grid_body, .time_grid_title');
|
||
const onBookmarkPage = href.includes('/web/resource/bookmark');
|
||
return hasCalendarGrid && onBookmarkPage;
|
||
});
|
||
}, 5 * 60 * 1000);
|
||
|
||
const pageSource = await driver.getPageSource();
|
||
fs.writeFileSync(outPath, pageSource, 'utf-8');
|
||
console.log(`saved: ${path.resolve(outPath)}`);
|
||
} finally {
|
||
if (driver) {
|
||
await driver.quit();
|
||
}
|
||
}
|
||
}
|
||
|
||
function buildTitle(baseTitle, location) {
|
||
const base = String(baseTitle || '設備予約').trim() || '設備予約';
|
||
const site = String(location || '').trim();
|
||
if (!site) {
|
||
return base;
|
||
}
|
||
return `${site}${base}`;
|
||
}
|
||
|
||
function sanitizeFileName(name) {
|
||
return String(name || '')
|
||
.replace(/[\\/:*?"<>|]/g, '_')
|
||
.trim();
|
||
}
|
||
|
||
function parseTimeRange(text) {
|
||
const normalized = String(text || '').trim();
|
||
const m = normalized.match(/(\d{1,2}:\d{2})\s*[~〜]\s*(\d{1,2}:\d{2})/);
|
||
if (!m) {
|
||
return { start: '', end: '' };
|
||
}
|
||
return { start: m[1], end: m[2] };
|
||
}
|
||
|
||
function toCsv(result) {
|
||
const records = [];
|
||
records.push(['設備名', '開始時間', '終了時間', '内容', '登録者']);
|
||
|
||
for (const facilityName of Object.keys(result)) {
|
||
const items = Array.isArray(result[facilityName]) ? result[facilityName] : [];
|
||
for (const item of items) {
|
||
const timeRange = item?.[0] || '';
|
||
const content = item?.[1] || '';
|
||
const registrant = item?.[2] || '';
|
||
const { start, end } = parseTimeRange(timeRange);
|
||
records.push([facilityName, start, end, content, registrant]);
|
||
}
|
||
}
|
||
|
||
return stringify(records, { header: false });
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
return String(text ?? '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function timeToMinutes(hhmm) {
|
||
const m = String(hhmm || '').trim().match(/^(\d{1,2}):(\d{2})$/);
|
||
if (!m) return NaN;
|
||
const h = Number(m[1]);
|
||
const mm = Number(m[2]);
|
||
if (h < 0 || h > 23 || mm < 0 || mm > 59) return NaN;
|
||
return h * 60 + mm;
|
||
}
|
||
|
||
function buildTimeSlots(startHHMM, endHHMM, stepMinutes) {
|
||
const start = timeToMinutes(startHHMM);
|
||
const end = timeToMinutes(endHHMM);
|
||
if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) {
|
||
throw new Error(`時間枠の指定が不正です: ${startHHMM} - ${endHHMM}`);
|
||
}
|
||
const slots = [];
|
||
for (let t = start; t < end; t += stepMinutes) {
|
||
const h = Math.floor(t / 60);
|
||
const m = t % 60;
|
||
slots.push(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
||
}
|
||
return { startMinutes: start, endMinutes: end, slots };
|
||
}
|
||
|
||
function parseDateFromText(text) {
|
||
const raw = String(text || '').trim();
|
||
if (!raw) return null;
|
||
|
||
const normalized = raw
|
||
.replace(/(/g, '(')
|
||
.replace(/)/g, ')')
|
||
.replace(/\s+/g, '');
|
||
|
||
const m = normalized.match(/(\d{4})[\/.\-年](\d{1,2})[\/.\-月](\d{1,2})(?:日)?/);
|
||
|
||
let date;
|
||
if (m) {
|
||
const y = Number(m[1]);
|
||
const mo = Number(m[2]);
|
||
const d = Number(m[3]);
|
||
date = new Date(y, mo - 1, d);
|
||
if (date.getFullYear() !== y || date.getMonth() !== (mo - 1) || date.getDate() !== d) {
|
||
return null;
|
||
}
|
||
} else {
|
||
const parsed = new Date(raw);
|
||
if (Number.isNaN(parsed.getTime())) {
|
||
return null;
|
||
}
|
||
date = parsed;
|
||
}
|
||
|
||
return date;
|
||
}
|
||
|
||
function formatJapaneseDateFromText(text) {
|
||
const date = parseDateFromText(text);
|
||
if (!date) return String(text || '').trim();
|
||
|
||
const week = ['日', '月', '火', '水', '木', '金', '土'];
|
||
const y = date.getFullYear();
|
||
const mo = date.getMonth() + 1;
|
||
const d = date.getDate();
|
||
const w = week[date.getDay()];
|
||
|
||
return `${y}年${mo}月${d}日(${w})`;
|
||
}
|
||
|
||
function dateToYYYYMMDD(date) {
|
||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||
return '';
|
||
}
|
||
const y = String(date.getFullYear());
|
||
const mo = String(date.getMonth() + 1).padStart(2, '0');
|
||
const d = String(date.getDate()).padStart(2, '0');
|
||
return `${y}${mo}${d}`;
|
||
}
|
||
|
||
async function saveSinglePageA4LandscapePdf(htmlText, outPath) {
|
||
const printCss = '<style>@page { size: A4 landscape; margin: 50px; } html, body { margin: 0; padding: 0; }</style>';
|
||
const htmlForPrint = htmlText.includes('</head>')
|
||
? htmlText.replace('</head>', `${printCss}</head>`)
|
||
: `${printCss}${htmlText}`;
|
||
|
||
const tmpHtmlPath = path.join(os.tmpdir(), `schedule-print-${Date.now()}.html`);
|
||
fs.writeFileSync(tmpHtmlPath, htmlForPrint, 'utf-8');
|
||
|
||
const fileUrl = `file:///${tmpHtmlPath.replace(/\\/g, '/')}`;
|
||
let driver = null;
|
||
|
||
try {
|
||
const chromeOptions = new chrome.Options().addArguments(
|
||
'--headless=new',
|
||
'--disable-gpu',
|
||
'--window-size=1600,1000',
|
||
'--allow-file-access-from-files'
|
||
);
|
||
driver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(chromeOptions).build();
|
||
|
||
await driver.get(fileUrl);
|
||
await driver.wait(async () => {
|
||
const state = await driver.executeScript('return document.readyState');
|
||
return state === 'complete';
|
||
}, 10000);
|
||
|
||
const scale = await driver.executeScript(() => {
|
||
const html = document.documentElement;
|
||
const body = document.body;
|
||
const contentWidth = Math.max(html.scrollWidth, body.scrollWidth, html.offsetWidth, body.offsetWidth);
|
||
const contentHeight = Math.max(html.scrollHeight, body.scrollHeight, html.offsetHeight, body.offsetHeight);
|
||
|
||
// A4横向き(96dpi換算)へ収める
|
||
const pageWidthPx = 1122;
|
||
const pageHeightPx = 793;
|
||
const paddingPx = 50;
|
||
const scaleX = (pageWidthPx - paddingPx * 2) / Math.max(1, contentWidth);
|
||
const scaleY = (pageHeightPx - paddingPx * 2) / Math.max(1, contentHeight);
|
||
return Math.max(0.1, Math.min(2, scaleX, scaleY));
|
||
});
|
||
|
||
const pdfBase64 = await driver.printPage({
|
||
orientation: 'landscape',
|
||
background: true,
|
||
shrinkToFit: true,
|
||
width: 29.7,
|
||
height: 21.0,
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
scale,
|
||
pageRanges: ['1-1']
|
||
});
|
||
fs.writeFileSync(outPath, pdfBase64, 'base64');
|
||
} finally {
|
||
if (driver) {
|
||
await driver.quit();
|
||
}
|
||
if (fs.existsSync(tmpHtmlPath)) {
|
||
fs.unlinkSync(tmpHtmlPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
function toScheduleTableHtml(facilities, result, options) {
|
||
const defaultStartHHMM = options?.startHHMM || '07:00';
|
||
const defaultEndHHMM = options?.endHHMM || '21:00';
|
||
const stepMinutes = Math.max(1, Number(options?.stepMinutes) || 30);
|
||
const calendarDate = options?.calendarDate || '';
|
||
|
||
const facilityList = Array.isArray(facilities) ? facilities : [];
|
||
|
||
// 予定データ全体から表示範囲を決定(最初の開始-1時間、最後の終了+1時間)
|
||
let minStart = Infinity;
|
||
let maxEnd = -Infinity;
|
||
for (const facilityName of facilityList) {
|
||
const items = Array.isArray(result[facilityName]) ? result[facilityName] : [];
|
||
for (const item of items) {
|
||
const timeRange = item?.[0] || '';
|
||
const { start, end } = parseTimeRange(timeRange);
|
||
const sMin = timeToMinutes(start);
|
||
const eMin = timeToMinutes(end);
|
||
if (!Number.isFinite(sMin) || !Number.isFinite(eMin) || sMin >= eMin) {
|
||
continue;
|
||
}
|
||
if (sMin < minStart) minStart = sMin;
|
||
if (eMin > maxEnd) maxEnd = eMin;
|
||
}
|
||
}
|
||
|
||
function minutesToHHMM(totalMinutes) {
|
||
const clamped = Math.max(0, Math.min(24 * 60, totalMinutes));
|
||
const h = Math.floor(clamped / 60);
|
||
const m = clamped % 60;
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||
}
|
||
|
||
let startHHMM = defaultStartHHMM;
|
||
let endHHMM = defaultEndHHMM;
|
||
if (Number.isFinite(minStart) && Number.isFinite(maxEnd)) {
|
||
const dynamicStart = Math.max(0, minStart - 60);
|
||
const dynamicEnd = Math.min(24 * 60, maxEnd + 60);
|
||
startHHMM = minutesToHHMM(dynamicStart);
|
||
endHHMM = minutesToHHMM(Math.max(dynamicEnd, dynamicStart + stepMinutes));
|
||
}
|
||
|
||
const { startMinutes, endMinutes, slots } = buildTimeSlots(startHHMM, endHHMM, stepMinutes);
|
||
const slotCount = slots.length;
|
||
|
||
const rowsHtml = [];
|
||
for (const facilityName of facilityList) {
|
||
const items = Array.isArray(result[facilityName]) ? result[facilityName] : [];
|
||
|
||
// startIndex -> { span, html }
|
||
const cells = new Map();
|
||
for (const item of items) {
|
||
const timeRange = item?.[0] || '';
|
||
const content = item?.[1] || '';
|
||
const registrant = item?.[2] || '';
|
||
const { start, end } = parseTimeRange(timeRange);
|
||
|
||
const sMin = timeToMinutes(start);
|
||
const eMin = timeToMinutes(end);
|
||
if (!Number.isFinite(sMin) || !Number.isFinite(eMin) || sMin >= eMin) {
|
||
continue;
|
||
}
|
||
|
||
const clippedStart = Math.max(sMin, startMinutes);
|
||
const clippedEnd = Math.min(eMin, endMinutes);
|
||
if (clippedStart >= clippedEnd) {
|
||
continue;
|
||
}
|
||
|
||
const startIndex = Math.floor((clippedStart - startMinutes) / stepMinutes);
|
||
const endIndex = Math.ceil((clippedEnd - startMinutes) / stepMinutes);
|
||
const span = Math.max(1, Math.min(slotCount, endIndex) - Math.max(0, startIndex));
|
||
|
||
const key = Math.max(0, startIndex);
|
||
if (cells.has(key)) {
|
||
// 予定が重複している場合は後勝ちにせず、先勝ちで維持
|
||
continue;
|
||
}
|
||
|
||
const cellHtml = `<span class="booked-content">${escapeHtml(content)}</span><br/><span class="booked-content">${escapeHtml(registrant)}</span>`;
|
||
cells.set(key, { span, html: cellHtml });
|
||
}
|
||
|
||
const tds = [];
|
||
tds.push(`<th class="facility">${escapeHtml(facilityName)}</th>`);
|
||
|
||
for (let i = 0; i < slotCount; i += 1) {
|
||
const cell = cells.get(i);
|
||
if (cell) {
|
||
tds.push(`<td class="booked" colspan="${cell.span}">${cell.html}</td>`);
|
||
i += (cell.span - 1);
|
||
continue;
|
||
}
|
||
tds.push('<td class="empty"></td>');
|
||
}
|
||
|
||
rowsHtml.push(`<tr>${tds.join('')}</tr>`);
|
||
}
|
||
|
||
const headerCells = ['<th class="facility">設備名</th>', ...slots.map(s => `<th class="time">${escapeHtml(s)}</th>`)].join('');
|
||
const tableHtml = `
|
||
<table class="schedule">
|
||
<thead>
|
||
<tr>${headerCells}</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml.join('\n ')}
|
||
</tbody>
|
||
</table>`;
|
||
|
||
const title = options?.title || '設備予約';
|
||
const style = `
|
||
<style>
|
||
body { font-family: system-ui, -apple-system, Segoe UI, sans-serif; margin: 16px; }
|
||
.header-row { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin: 0 0 12px; }
|
||
h1 { font-size: 28px; margin: 0; text-align: left; line-height: 1.2; }
|
||
.calendar-date { font-size: 28px; font-weight: 700; text-align: right; margin: 0; line-height: 1.2; white-space: nowrap; }
|
||
.note { font-size: 12px; color: #444; margin: 0 0 12px; }
|
||
table.schedule { border-collapse: collapse; width: 100%; table-layout: fixed; }
|
||
table.schedule th, table.schedule td { border: 1px solid #ccc; padding: 4px; vertical-align: top; }
|
||
table.schedule th.facility { position: sticky; left: 0; background: #f7f7f7; width: 220px; min-width: 220px; max-width: 220px; }
|
||
table.schedule th.time { font-size: 11px; background: #fafafa; }
|
||
table.schedule td.empty { background: #fff; }
|
||
table.schedule td.booked { background: #e8f0ff; white-space: normal; word-break: break-word; }
|
||
table.schedule td.booked .booked-content { font-size: 18px; font-weight: 700; }
|
||
@media (max-width: 768px) {
|
||
.header-row { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||
.calendar-date { text-align: left; white-space: normal; }
|
||
}
|
||
</style>`;
|
||
|
||
return `<!doctype html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>${escapeHtml(title)}</title>${style}
|
||
</head>
|
||
<body>
|
||
<div class="header-row">
|
||
<h1>${escapeHtml(title)}</h1>
|
||
${calendarDate ? `<p class="calendar-date">${escapeHtml(calendarDate)}</p>` : '<p class="calendar-date"></p>'}
|
||
</div>
|
||
<p class="note">時間枠: ${escapeHtml(startHHMM)}〜${escapeHtml(endHHMM)}(${stepMinutes}分刻み)</p>
|
||
${tableHtml}
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
function analyzeHtmlToResult(htmlText) {
|
||
const dom = new JSDOM(htmlText);
|
||
const document = dom.window.document;
|
||
|
||
const dateElement = document.querySelector('strong.date');
|
||
const rawDateText = (dateElement?.textContent || '').trim();
|
||
const parsedDate = parseDateFromText(rawDateText);
|
||
const displayDate = formatJapaneseDateFromText(rawDateText);
|
||
const dateKey = dateToYYYYMMDD(parsedDate);
|
||
|
||
// 1. 設備名をすべて取得
|
||
const timeGridTitle = document.querySelector('.time_grid_title');
|
||
const facilityElements = timeGridTitle.querySelectorAll('.resource_cover a.name');
|
||
const facilities = Array.from(facilityElements).map(el => el.textContent.trim()).filter(Boolean);
|
||
|
||
// 2. スケジュールデータを取得
|
||
const timeGridBody = document.querySelector('.time_grid_body');
|
||
const facilityGrids = Array.from(timeGridBody.querySelectorAll(':scope > .grid'));
|
||
|
||
// 3. 設備ごとに配列に格納
|
||
const result = {};
|
||
facilities.forEach((facility) => {
|
||
result[facility] = [];
|
||
});
|
||
|
||
function splitScheduleText(text) {
|
||
// 例: "10:00~11:30, 打合せ, 伊藤理奈"
|
||
// 件名にカンマが入っても壊れにくいように、最後を登録者として扱う
|
||
const parts = String(text || '').split(/\s*,\s*/).filter(p => p.length > 0);
|
||
if (parts.length === 0) {
|
||
return { time: '', title: '', registrant: '' };
|
||
}
|
||
if (parts.length === 1) {
|
||
return { time: parts[0], title: '', registrant: '' };
|
||
}
|
||
if (parts.length === 2) {
|
||
return { time: parts[0], title: parts[1], registrant: '' };
|
||
}
|
||
const time = parts[0];
|
||
const registrant = parts[parts.length - 1];
|
||
const title = parts.slice(1, parts.length - 1).join(', ');
|
||
return { time, title, registrant };
|
||
}
|
||
|
||
// データを設備ごとに抽出
|
||
const count = Math.min(facilities.length, facilityGrids.length);
|
||
for (let facilityIndex = 0; facilityIndex < count; facilityIndex += 1) {
|
||
const facilityName = facilities[facilityIndex];
|
||
const grid = facilityGrids[facilityIndex];
|
||
|
||
const disabledMsgs = Array.from(grid.querySelectorAll('.schedule.disabled .msg'));
|
||
let previousText = '';
|
||
|
||
for (const msg of disabledMsgs) {
|
||
const text = (msg.textContent || '').trim();
|
||
if (!text) {
|
||
previousText = '';
|
||
continue;
|
||
}
|
||
|
||
// 同一予定が複数枠(30分単位など)にまたがる場合、同じ文言が連続するので除外
|
||
if (text === previousText) {
|
||
continue;
|
||
}
|
||
previousText = text;
|
||
|
||
const { time, title, registrant } = splitScheduleText(text);
|
||
result[facilityName].push([time || '', title || '', registrant || '']);
|
||
}
|
||
}
|
||
|
||
return { facilities, result, displayDate, dateKey };
|
||
}
|
||
|
||
async function main() {
|
||
const args = parseArgs(process.argv.slice(2));
|
||
if (args.help) {
|
||
printHelp();
|
||
return;
|
||
}
|
||
|
||
// 先にカレンダー画面を取得してローカルHTMLへ保存する
|
||
await fetchCalendarPageSource(resolveScriptPath('./calendarData.html'), args.today);
|
||
|
||
const inputPath = resolveScriptPath(args.input);
|
||
const html = fs.readFileSync(inputPath, 'utf-8');
|
||
const analyzed = analyzeHtmlToResult(html);
|
||
const facilities = analyzed.facilities;
|
||
const result = analyzed.result;
|
||
|
||
if (args.format === 'csv') {
|
||
const csv = toCsv(result);
|
||
if (args.out) {
|
||
const outPath = resolveScriptPath(args.out);
|
||
fs.writeFileSync(outPath, csv, 'utf-8');
|
||
} else {
|
||
process.stdout.write(csv);
|
||
}
|
||
} else if (args.format === 'html') {
|
||
const outPath = resolveScriptPath(args.out || './schedule_table.html');
|
||
const outputTitle = buildTitle(args.title, args.location);
|
||
const htmlOut = toScheduleTableHtml(facilities, result, {
|
||
title: outputTitle,
|
||
calendarDate: analyzed.displayDate || ''
|
||
});
|
||
fs.writeFileSync(outPath, htmlOut, 'utf-8');
|
||
|
||
const today = new Date();
|
||
const fallbackDateKey = dateToYYYYMMDD(today);
|
||
const dateKey = analyzed.dateKey || fallbackDateKey;
|
||
const safeTitle = sanitizeFileName(outputTitle) || '設備予約';
|
||
const pdfName = `${safeTitle}■${dateKey}.pdf`;
|
||
const pdfPath = path.join(ROOT_DIR, pdfName);
|
||
await saveSinglePageA4LandscapePdf(htmlOut, pdfPath);
|
||
|
||
console.log(`saved: ${outPath}`);
|
||
console.log(`saved: ${pdfPath}`);
|
||
} else {
|
||
console.log(JSON.stringify(result, null, 2));
|
||
}
|
||
|
||
module.exports = result;
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
}); |