664 lines
21 KiB
JavaScript
664 lines
21 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
const { parse } = require('csv-parse/sync');
|
||
|
||
const IMPORT_DIR = path.join(__dirname, 'import');
|
||
const SUCCESS_DIR = path.join(__dirname, 'success');
|
||
const FAIL_DIR = path.join(__dirname, 'fail');
|
||
const DEFAULT_TIME_ZONE = process.env.LW_TIMEZONE || 'Asia/Tokyo';
|
||
const REQUIRED_CSV_HEADERS = ['登録先', '開始日', '開始時刻', '終了日', '終了時刻', '分類', '予定', '備考'];
|
||
|
||
const LW_TOKEN_URL = 'https://auth.worksmobile.com/oauth2/v2.0/token';
|
||
const LW_API_BASE_URL = 'https://www.worksapis.com/v1.0';
|
||
const LW_EVENT_ENDPOINT_TEMPLATE = '/users/{userId}/calendar/events';
|
||
const PRIVATE_KEY_FILE = path.join(__dirname, 'private_20260306134304.key');
|
||
const LW_USER_LOOKUP_DOMAIN_ID = '401101389';
|
||
|
||
const LW_CLIENT_ID = 'tre8J_Tk8RblfsMSyZRh';
|
||
const LW_CLIENT_SECRET = 'O5_R5gcHxg';
|
||
const LW_SERVICE_ACCOUNT = 'wh4k8.serviceaccount@next-hd.co.jp';
|
||
const LW_PRIVATE_KEY = (fs.existsSync(PRIVATE_KEY_FILE) ? fs.readFileSync(PRIVATE_KEY_FILE, 'utf8') : '');
|
||
const LW_SCOPE = process.env.LW_SCOPE || 'calendar';
|
||
const LW_USER_LOOKUP_SCOPE = process.env.LW_USER_LOOKUP_SCOPE || 'user.read';
|
||
const LW_SEND_NOTIFICATION = process.env.LW_SEND_NOTIFICATION !== '0';
|
||
const LW_DRY_RUN = process.env.LW_DRY_RUN === '1';
|
||
// 本番に近い検証: 認証やデータ生成は実行し、API POSTのみ停止する
|
||
const LW_PRE_IMPORT_TEST = process.env.LW_PRE_IMPORT_TEST === '1';
|
||
const LW_RETRY_MAX = Number(process.env.LW_RETRY_MAX || '3');
|
||
// 1分辺り240回がリミットのため、指数関数的に待機時間を増やす場合のベース時間(ms)
|
||
const LW_RETRY_BASE_MS = Number(process.env.LW_RETRY_BASE_MS || '5000'); // 1分
|
||
const LW_API_WAIT_MS = Number(process.env.LW_API_WAIT_MS || '300'); // APIリクエスト間の最小待機時間(ms)
|
||
|
||
let lastApiRequestAt = 0;
|
||
|
||
function ensureRequiredEnv() {
|
||
if (LW_DRY_RUN) {
|
||
return;
|
||
}
|
||
|
||
const required = [
|
||
['LW_CLIENT_ID', LW_CLIENT_ID],
|
||
['LW_CLIENT_SECRET', LW_CLIENT_SECRET],
|
||
['LW_SERVICE_ACCOUNT', LW_SERVICE_ACCOUNT],
|
||
['LW_PRIVATE_KEY', LW_PRIVATE_KEY]
|
||
];
|
||
|
||
const missing = required.filter(([, value]) => !value).map(([key]) => key);
|
||
if (missing.length > 0) {
|
||
if (missing.includes('LW_PRIVATE_KEY') && !fs.existsSync(PRIVATE_KEY_FILE)) {
|
||
throw new Error(`秘密鍵が見つかりません: ${PRIVATE_KEY_FILE}`);
|
||
}
|
||
throw new Error(`環境変数が不足しています: ${missing.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
function base64UrlEncode(value) {
|
||
return Buffer.from(value)
|
||
.toString('base64')
|
||
.replace(/=/g, '')
|
||
.replace(/\+/g, '-')
|
||
.replace(/\//g, '_');
|
||
}
|
||
|
||
function createJwtAssertion() {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const header = { alg: 'RS256', typ: 'JWT' };
|
||
const payload = {
|
||
iss: LW_CLIENT_ID,
|
||
sub: LW_SERVICE_ACCOUNT,
|
||
aud: LW_TOKEN_URL,
|
||
iat: now,
|
||
exp: now + 300
|
||
};
|
||
|
||
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
||
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
||
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
||
|
||
const signer = crypto.createSign('RSA-SHA256');
|
||
signer.update(signingInput);
|
||
signer.end();
|
||
|
||
const signature = signer
|
||
.sign(LW_PRIVATE_KEY)
|
||
.toString('base64')
|
||
.replace(/=/g, '')
|
||
.replace(/\+/g, '-')
|
||
.replace(/\//g, '_');
|
||
|
||
return `${signingInput}.${signature}`;
|
||
}
|
||
|
||
async function getAccessToken(scopeOverride = LW_SCOPE) {
|
||
if (LW_DRY_RUN) {
|
||
return 'dry-run-token';
|
||
}
|
||
|
||
const assertion = createJwtAssertion();
|
||
const form = new URLSearchParams({
|
||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||
assertion,
|
||
client_id: LW_CLIENT_ID,
|
||
client_secret: LW_CLIENT_SECRET,
|
||
scope: scopeOverride
|
||
});
|
||
|
||
const response = await lwFetch(LW_TOKEN_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
body: form
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const detail = await response.text();
|
||
throw new Error(`アクセストークン取得失敗: ${response.status} ${detail}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data.access_token) {
|
||
throw new Error('アクセストークン取得失敗: access_token が返却されませんでした');
|
||
}
|
||
|
||
return data.access_token;
|
||
}
|
||
|
||
function normalizeUserId(rawUser) {
|
||
const mapText = process.env.LW_USER_MAP_JSON;
|
||
if (!mapText) {
|
||
return rawUser;
|
||
}
|
||
|
||
try {
|
||
const map = JSON.parse(mapText);
|
||
return map[rawUser] || rawUser;
|
||
} catch (error) {
|
||
throw new Error(`LW_USER_MAP_JSON のJSON解析に失敗しました: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
function extractUserNameFromFilename(fileName) {
|
||
const base = path.basename(fileName, path.extname(fileName));
|
||
const matched = base.match(/^(.*?)(?:_スケジュール)?$/);
|
||
const user = (matched && matched[1] ? matched[1] : base).trim();
|
||
|
||
if (!user) {
|
||
throw new Error(`ユーザー名をファイル名から抽出できません: ${fileName}`);
|
||
}
|
||
|
||
return user;
|
||
}
|
||
|
||
function normalizeNameForCompare(value) {
|
||
return String(value || '').replace(/[\s\u3000]/g, '').toLowerCase();
|
||
}
|
||
|
||
function profileNames(profile) {
|
||
const names = new Set();
|
||
|
||
const add = (v) => {
|
||
if (v) names.add(String(v));
|
||
};
|
||
|
||
add(profile.name);
|
||
add(profile.nickName);
|
||
add(profile.displayName);
|
||
add(profile.email);
|
||
add(profile.userName);
|
||
|
||
if (profile.userName && typeof profile.userName === 'object') {
|
||
const u = profile.userName;
|
||
add(u.firstName && u.lastName ? `${u.lastName} ${u.firstName}` : '');
|
||
add(u.givenName && u.familyName ? `${u.familyName} ${u.givenName}` : '');
|
||
add(u.firstName && u.lastName ? `${u.lastName}${u.firstName}` : '');
|
||
add(u.givenName && u.familyName ? `${u.familyName}${u.givenName}` : '');
|
||
add(u.displayName);
|
||
add(u.fullName);
|
||
}
|
||
|
||
if (profile.name && typeof profile.name === 'object') {
|
||
const n = profile.name;
|
||
add(n.displayName);
|
||
add(n.fullName);
|
||
add(n.familyName && n.givenName ? `${n.familyName} ${n.givenName}` : '');
|
||
add(n.familyName && n.givenName ? `${n.familyName}${n.givenName}` : '');
|
||
}
|
||
|
||
return Array.from(names);
|
||
}
|
||
|
||
async function fetchAllLineworksUsers(accessToken, domainId) {
|
||
const users = [];
|
||
let cursor = '';
|
||
|
||
while (true) {
|
||
const params = new URLSearchParams();
|
||
params.set('count', '100');
|
||
if (domainId) {
|
||
params.set('domainId', domainId);
|
||
}
|
||
if (cursor) {
|
||
params.set('cursor', cursor);
|
||
}
|
||
|
||
const query = params.toString();
|
||
const apiUrl = `${LW_API_BASE_URL}/users${query ? `?${query}` : ''}`;
|
||
const response = await lwFetch(apiUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`
|
||
}
|
||
});
|
||
|
||
const text = await response.text();
|
||
let data;
|
||
try {
|
||
data = JSON.parse(text);
|
||
} catch (error) {
|
||
throw new Error('LINE WORKSユーザー一覧の解析に失敗しました');
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`LINE WORKSユーザー一覧取得に失敗しました: ${response.status} ${JSON.stringify(data)}`);
|
||
}
|
||
|
||
const pageUsers = Array.isArray(data.users) ? data.users : [];
|
||
users.push(...pageUsers);
|
||
|
||
const nextCursor = data?.responseMetaData?.nextCursor || '';
|
||
if (!nextCursor || nextCursor === cursor) {
|
||
break;
|
||
}
|
||
cursor = nextCursor;
|
||
}
|
||
|
||
return users;
|
||
}
|
||
|
||
function findLineworksUserByName(users, sourceUserName) {
|
||
const normalizedSource = normalizeNameForCompare(sourceUserName);
|
||
for (const user of users) {
|
||
const candidates = profileNames(user);
|
||
const matched = candidates.some(name => normalizeNameForCompare(name) === normalizedSource);
|
||
if (matched) {
|
||
return user;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function normalizeLoginIdForCompare(value) {
|
||
return String(value || '').trim().toLowerCase();
|
||
}
|
||
|
||
function loginIdCandidates(profile) {
|
||
const ids = new Set();
|
||
|
||
const add = (v) => {
|
||
if (v) ids.add(String(v));
|
||
};
|
||
|
||
add(profile.email);
|
||
add(profile.loginId);
|
||
add(profile.userId);
|
||
if (typeof profile.userName === 'string') {
|
||
add(profile.userName);
|
||
}
|
||
if (profile.account && typeof profile.account === 'object') {
|
||
add(profile.account.loginId);
|
||
add(profile.account.email);
|
||
}
|
||
|
||
return Array.from(ids);
|
||
}
|
||
|
||
function findLineworksUserByLoginId(users, sourceLoginId) {
|
||
const normalizedSource = normalizeLoginIdForCompare(sourceLoginId);
|
||
for (const user of users) {
|
||
const candidates = loginIdCandidates(user);
|
||
const matched = candidates.some(id => normalizeLoginIdForCompare(id) === normalizedSource);
|
||
if (matched) {
|
||
return user;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function parseCsvDate(value) {
|
||
const normalized = String(value || '').trim();
|
||
const matched = normalized.match(/^(\d{4})[/-](\d{1,2})[/-](\d{1,2})$/);
|
||
if (!matched) {
|
||
throw new Error(`日付形式が不正です: ${value}`);
|
||
}
|
||
|
||
const [, y, m, d] = matched;
|
||
return `${y}-${String(Number(m)).padStart(2, '0')}-${String(Number(d)).padStart(2, '0')}`;
|
||
}
|
||
|
||
function parseCsvTime(value) {
|
||
const normalized = String(value || '').trim();
|
||
if (!normalized) {
|
||
return '';
|
||
}
|
||
|
||
const hm = normalized.match(/^(\d{1,2}):(\d{2})$/);
|
||
if (hm) {
|
||
const hour = Number(hm[1]);
|
||
const minute = Number(hm[2]);
|
||
if (hour > 23 || minute > 59) {
|
||
throw new Error(`時刻形式が不正です: ${value}`);
|
||
}
|
||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00`;
|
||
}
|
||
|
||
const hhmm = normalized.match(/^(\d{2})(\d{2})$/);
|
||
if (hhmm) {
|
||
const hour = Number(hhmm[1]);
|
||
const minute = Number(hhmm[2]);
|
||
if (hour > 23 || minute > 59) {
|
||
throw new Error(`時刻形式が不正です: ${value}`);
|
||
}
|
||
return `${hhmm[1]}:${hhmm[2]}:00`;
|
||
}
|
||
|
||
throw new Error(`時刻形式が不正です: ${value}`);
|
||
}
|
||
|
||
function sanitizeTextField(value) {
|
||
// LINE WORKS API向けに制御文字を整理し、改行はLFに統一する
|
||
return String(value || '')
|
||
.replace(/\r\n?/g, '\n')
|
||
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
|
||
.trim();
|
||
}
|
||
|
||
function encodeNewlineForApi(value) {
|
||
return String(value || '').replace(/\n/g, '\\n');
|
||
}
|
||
|
||
function toLineworksDateTime(dateText, timeText) {
|
||
// LINE WORKS仕様: dateTimeはYYYY-MM-DDTHH:mm:ss(オフセットなし)
|
||
return `${dateText}T${timeText}`;
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function waitApiInterval() {
|
||
const now = Date.now();
|
||
const elapsed = now - lastApiRequestAt;
|
||
const waitMs = Math.max(0, LW_API_WAIT_MS - elapsed);
|
||
if (waitMs > 0) {
|
||
await sleep(waitMs);
|
||
}
|
||
lastApiRequestAt = Date.now();
|
||
}
|
||
|
||
async function lwFetch(url, options) {
|
||
await waitApiInterval();
|
||
return fetch(url, options);
|
||
}
|
||
|
||
function isRetryableCalendarError(status, responseText) {
|
||
if (status >= 500 || status === 429) {
|
||
return true;
|
||
}
|
||
|
||
try {
|
||
const body = JSON.parse(responseText);
|
||
return body && body.code === 'SERVICE_UNAVAILABLE';
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function eventStartForLog(eventComponent) {
|
||
const start = eventComponent && eventComponent.start ? eventComponent.start : {};
|
||
if (start.dateTime) {
|
||
return String(start.dateTime).replace('T', ' ');
|
||
}
|
||
if (start.date) {
|
||
return String(start.date);
|
||
}
|
||
return '(date-unknown)';
|
||
}
|
||
|
||
function eventLabelForLog(eventComponent) {
|
||
return `${eventStartForLog(eventComponent)} ${eventComponent.summary}`;
|
||
}
|
||
|
||
function nextDate(dateText) {
|
||
const d = new Date(`${dateText}T00:00:00`);
|
||
if (Number.isNaN(d.getTime())) {
|
||
throw new Error(`日付変換に失敗しました: ${dateText}`);
|
||
}
|
||
d.setDate(d.getDate() + 1);
|
||
const yyyy = d.getFullYear();
|
||
const MM = String(d.getMonth() + 1).padStart(2, '0');
|
||
const dd = String(d.getDate()).padStart(2, '0');
|
||
return `${yyyy}-${MM}-${dd}`;
|
||
}
|
||
|
||
function validateCsvHeaders(headers) {
|
||
const missing = REQUIRED_CSV_HEADERS.filter(header => !headers.includes(header));
|
||
if (missing.length > 0) {
|
||
throw new Error(`必須ヘッダ不足: ${missing.join(', ')}`);
|
||
}
|
||
}
|
||
|
||
function extractCsvHeaders(content) {
|
||
const headerRows = parse(content, {
|
||
to_line: 1,
|
||
bom: true,
|
||
relax_quotes: true
|
||
});
|
||
if (!Array.isArray(headerRows) || headerRows.length === 0) {
|
||
return [];
|
||
}
|
||
return Array.isArray(headerRows[0]) ? headerRows[0].map(v => String(v || '').trim()) : [];
|
||
}
|
||
|
||
function buildEventComponentFromCsvRow(row) {
|
||
const startDate = parseCsvDate(row['開始日']);
|
||
const endDate = parseCsvDate(row['終了日']);
|
||
const startTime = parseCsvTime(row['開始時刻']);
|
||
const endTime = parseCsvTime(row['終了時刻']);
|
||
|
||
const category = sanitizeTextField(row['分類']);
|
||
const rawSummary = sanitizeTextField(row['予定']);
|
||
const summaryBase = rawSummary === '----'
|
||
? (category || '(件名なし)')
|
||
: (rawSummary || '(件名なし)');
|
||
const summary = encodeNewlineForApi(summaryBase);
|
||
const registerTo = sanitizeTextField(row['登録先']);
|
||
const note = sanitizeTextField(row['備考']);
|
||
|
||
const descriptionParts = [];
|
||
if (category) descriptionParts.push(`分類: ${category}`);
|
||
if (registerTo) descriptionParts.push(`登録先: ${registerTo}`);
|
||
if (note) descriptionParts.push(note);
|
||
|
||
const component = {
|
||
summary,
|
||
description: descriptionParts.join('\n')
|
||
};
|
||
|
||
if (!startTime && !endTime) {
|
||
component.start = { date: startDate };
|
||
// 終日予定は終了日を排他的に扱うため1日加算
|
||
component.end = { date: nextDate(endDate) };
|
||
return component;
|
||
}
|
||
|
||
if (startTime === '00:00:00' && endTime === '00:00:00') {
|
||
// 00:00-00:00 は開始日の終日予定として扱う
|
||
component.start = { date: startDate };
|
||
component.end = { date: nextDate(startDate) };
|
||
return component;
|
||
}
|
||
|
||
if (!startTime || !endTime) {
|
||
throw new Error('開始時刻と終了時刻は両方指定してください');
|
||
}
|
||
|
||
component.start = {
|
||
dateTime: toLineworksDateTime(startDate, startTime),
|
||
timeZone: DEFAULT_TIME_ZONE
|
||
};
|
||
component.end = {
|
||
dateTime: toLineworksDateTime(endDate, endTime),
|
||
timeZone: DEFAULT_TIME_ZONE
|
||
};
|
||
|
||
return component;
|
||
}
|
||
|
||
async function createCalendarEvent(accessToken, userId, eventComponent) {
|
||
const endpoint = LW_EVENT_ENDPOINT_TEMPLATE.replace('{userId}', encodeURIComponent(userId));
|
||
const url = `${LW_API_BASE_URL}${endpoint}`;
|
||
const requestBody = {
|
||
eventComponents: [eventComponent],
|
||
sendNotification: LW_SEND_NOTIFICATION
|
||
};
|
||
|
||
if (LW_DRY_RUN) {
|
||
console.log(`[DRY-RUN] ${userId} ${eventLabelForLog(eventComponent)}`);
|
||
return;
|
||
}
|
||
|
||
if (LW_PRE_IMPORT_TEST) {
|
||
console.log(`[PRE-IMPORT-TEST] POST skipped: ${userId} ${eventLabelForLog(eventComponent)} -> ${url}`);
|
||
return;
|
||
}
|
||
|
||
let lastError = null;
|
||
for (let attempt = 1; attempt <= LW_RETRY_MAX; attempt += 1) {
|
||
const response = await lwFetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (response.ok) {
|
||
return;
|
||
}
|
||
|
||
const detail = await response.text();
|
||
lastError = new Error(`登録失敗: ${response.status} ${detail}`);
|
||
|
||
if (!isRetryableCalendarError(response.status, detail) || attempt === LW_RETRY_MAX) {
|
||
break;
|
||
}
|
||
|
||
const waitMs = LW_RETRY_BASE_MS * (2 ** (attempt - 1));
|
||
console.warn(`[RETRY] ${eventLabelForLog(eventComponent)} attempt=${attempt}/${LW_RETRY_MAX} wait=${waitMs}ms`);
|
||
await sleep(waitMs);
|
||
}
|
||
|
||
throw lastError;
|
||
}
|
||
|
||
async function processCsvFile(fileName, accessToken, users) {
|
||
const filePath = path.join(IMPORT_DIR, fileName);
|
||
const content = fs.readFileSync(filePath, 'utf8');
|
||
const sourceUserName = extractUserNameFromFilename(fileName);
|
||
const mappedName = normalizeUserId(sourceUserName);
|
||
let profile = findLineworksUserByName(users, mappedName);
|
||
if (!profile) {
|
||
profile = findLineworksUserByLoginId(users, mappedName);
|
||
if (profile) {
|
||
console.warn(`[MATCH-FALLBACK] ${fileName}: 名前照合NG -> ログインID照合OK (${mappedName})`);
|
||
}
|
||
}
|
||
if (!profile) {
|
||
throw new Error(`ユーザー照合失敗(名前/ログインID): file='${sourceUserName}', key='${mappedName}'`);
|
||
}
|
||
|
||
const userId = profile.userId || profile.id;
|
||
if (!userId) {
|
||
throw new Error(`ユーザーID未取得: file='${sourceUserName}'`);
|
||
}
|
||
|
||
const records = parse(content, {
|
||
columns: true,
|
||
skip_empty_lines: true,
|
||
bom: true
|
||
});
|
||
validateCsvHeaders(extractCsvHeaders(content));
|
||
|
||
if (records.length === 0) {
|
||
console.log(`[SKIP] ${fileName}: データ行なし`);
|
||
return { success: 0, failed: 1 };
|
||
}
|
||
|
||
let success = 0;
|
||
let failed = 0;
|
||
|
||
for (let i = 0; i < records.length; i += 1) {
|
||
const row = records[i];
|
||
try {
|
||
const eventComponent = buildEventComponentFromCsvRow(row);
|
||
await createCalendarEvent(accessToken, userId, eventComponent);
|
||
success += 1;
|
||
console.log(`[OK] ${fileName} -> ${userId} ${eventLabelForLog(eventComponent)}`);
|
||
} catch (error) {
|
||
failed += 1;
|
||
console.error(`[NG] ${fileName} 行${i + 2}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
return { success, failed };
|
||
}
|
||
|
||
function moveProcessedFile(fileName, destinationDir) {
|
||
const sourcePath = path.join(IMPORT_DIR, fileName);
|
||
if (!fs.existsSync(sourcePath)) {
|
||
return;
|
||
}
|
||
|
||
if (!fs.existsSync(destinationDir)) {
|
||
fs.mkdirSync(destinationDir, { recursive: true });
|
||
}
|
||
|
||
let targetPath = path.join(destinationDir, fileName);
|
||
if (fs.existsSync(targetPath)) {
|
||
const parsed = path.parse(fileName);
|
||
const stamp = Date.now();
|
||
targetPath = path.join(destinationDir, `${parsed.name}_${stamp}${parsed.ext}`);
|
||
}
|
||
|
||
fs.renameSync(sourcePath, targetPath);
|
||
console.log(`[MOVE] ${fileName} -> ${targetPath}`);
|
||
}
|
||
|
||
async function main() {
|
||
ensureRequiredEnv();
|
||
|
||
if (LW_DRY_RUN) {
|
||
console.log('mode: DRY_RUN (認証・登録をスキップ)');
|
||
} else if (LW_PRE_IMPORT_TEST) {
|
||
console.log('mode: PRE_IMPORT_TEST (登録直前まで実行、POSTのみスキップ)');
|
||
}
|
||
|
||
if (!fs.existsSync(IMPORT_DIR)) {
|
||
console.log('importフォルダが見つかりません');
|
||
return;
|
||
}
|
||
|
||
const files = fs.readdirSync(IMPORT_DIR)
|
||
.filter(file => file.toLowerCase().endsWith('.csv'))
|
||
.sort();
|
||
|
||
if (files.length === 0) {
|
||
console.log('importフォルダにcsvファイルが見つかりません');
|
||
return;
|
||
}
|
||
|
||
const accessToken = await getAccessToken(LW_SCOPE);
|
||
const userLookupToken = await getAccessToken(LW_USER_LOOKUP_SCOPE);
|
||
const users = await fetchAllLineworksUsers(userLookupToken, LW_USER_LOOKUP_DOMAIN_ID);
|
||
if (users.length === 0) {
|
||
throw new Error('LINE WORKSユーザー一覧が0件です');
|
||
}
|
||
console.log(`ユーザー一覧取得: ${users.length}件`);
|
||
|
||
let totalSuccess = 0;
|
||
let totalFailed = 0;
|
||
let successFiles = 0;
|
||
let failedFiles = 0;
|
||
|
||
for (const file of files) {
|
||
console.log(`--- processing ${file} ---`);
|
||
let result;
|
||
try {
|
||
result = await processCsvFile(file, accessToken, users);
|
||
} catch (error) {
|
||
console.error(`[NG] ${file}: ${error.message}`);
|
||
result = { success: 0, failed: 1 };
|
||
}
|
||
totalSuccess += result.success;
|
||
totalFailed += result.failed;
|
||
|
||
if (result.failed === 0 && result.success > 0) {
|
||
moveProcessedFile(file, SUCCESS_DIR);
|
||
successFiles += 1;
|
||
} else {
|
||
moveProcessedFile(file, FAIL_DIR);
|
||
failedFiles += 1;
|
||
}
|
||
}
|
||
|
||
console.log(`ファイル移動結果: success=${successFiles}, fail=${failedFiles}`);
|
||
console.log(`完了: success=${totalSuccess}, failed=${totalFailed}`);
|
||
}
|
||
|
||
main().catch(error => {
|
||
console.error(error.message || error);
|
||
process.exit(1);
|
||
}); |