#!/usr/bin/env node 'use strict'; // ============================================================================ // 작업 서버용 "다음 작업 반영 목록" 수집기 (v1.1.6) // // 관리자가 AI 분석을 보고 "다음 작업에 반영" 으로 결정하고 반영할 내용을 적은 사용자 제안·오류를 // 각 운영 서버에서 가져와 하나의 작업 목록(BACKLOG.md + backlog.json)으로 만든다. // 다음 릴리스 작업을 시작할 때 이 파일을 읽어 요구 사항에 포함한다. // // 설정: ~/.config/ai-webmaker/backlog-sources.json (권한 600) // [ { "name": "webmaker", "url": "https://webmaker.ibit.kr", "key": "<관리자 콘솔 › 설정 › 작업 서버 수집 키>" }, ... ] // 출력: $BACKLOG_DIR (기본 ~/work/ai-webmaker-backlog) 아래 BACKLOG.md, backlog.json // 사용: node scripts/collect-backlog.js [--mark] --mark 를 주면 가져간 글에 "수집됨" 표시를 남긴다 // node scripts/collect-backlog.js --implemented --version 1.2.0 [--note "…"] // → 릴리스에 반영한 항목을 각 서버에 "반영됨" 으로 표시 (목록에서 빠지고 사용자 화면에 버전과 함께 표시) // 주기 실행: crontab 예) 0 * * * * node /home/master/work/WebHoting_AI_Agent/scripts/collect-backlog.js --mark >> ~/.local/state/ai-webmaker-backlog.log 2>&1 // ============================================================================ const fs = require('fs'); const path = require('path'); const os = require('os'); const CONF = process.env.BACKLOG_SOURCES || path.join(os.homedir(), '.config', 'ai-webmaker', 'backlog-sources.json'); const OUT = process.env.BACKLOG_DIR || path.join(os.homedir(), 'work', 'ai-webmaker-backlog'); const MARK = process.argv.includes('--mark'); const feedback = require('../lib/feedback'); // backlogMarkdown 재사용 (데이터 파일은 읽지 않음) const argOf = (name) => { const i = process.argv.indexOf(name); return i > 0 ? process.argv[i + 1] : null; }; (async () => { let sources; try { sources = JSON.parse(fs.readFileSync(CONF, 'utf8')); } catch (e) { console.error(`설정 파일을 읽지 못했습니다: ${CONF} (${e.message})`); process.exit(1); } const impl = argOf('--implemented'); if (impl) { // 반영 완료 표시 모드: 각 서버에 같은 ID 목록을 보낸다(그 서버에 없는 ID 는 무시됨) const ids = impl.split(',').map((x) => x.trim()).filter(Boolean); const version = argOf('--version') || ''; const note = argOf('--note') || ''; for (const src of sources) { try { const r = await fetch(`${src.url.replace(/\/$/, '')}/api/service/backlog/implemented`, { method: 'POST', headers: { 'X-Backlog-Key': src.key, 'Content-Type': 'application/json' }, body: JSON.stringify({ ids, version, note }) }); const j = await r.json(); console.log(`${src.name}: ${r.ok ? `${j.marked}건 반영됨 표시 (v${version})` : '실패 — ' + (j.error || r.status)}`); } catch (e) { console.log(`${src.name}: 실패 — ${e.message}`); } } return; } const all = []; const report = []; for (const src of sources) { try { const r = await fetch(`${src.url.replace(/\/$/, '')}/api/service/backlog`, { headers: { 'X-Backlog-Key': src.key } }); const j = await r.json(); if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`); for (const x of j.items) all.push({ ...x, server: src.name, serverUrl: src.url, serverVersion: j.version }); report.push(`${src.name}: ${j.items.length}건 (v${j.version})`); if (MARK && j.items.length) await fetch(`${src.url.replace(/\/$/, '')}/api/service/backlog/collected`, { method: 'POST', headers: { 'X-Backlog-Key': src.key, 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: j.items.map((x) => x.id), by: `work-server ${os.hostname()}` }) }); } catch (e) { report.push(`${src.name}: 실패 — ${e.message}`); } } fs.mkdirSync(OUT, { recursive: true }); const at = new Date().toISOString(); fs.writeFileSync(path.join(OUT, 'backlog.json'), JSON.stringify({ at, sources: report, items: all }, null, 1)); const md = feedback.backlogMarkdown(all.map((x) => ({ ...x, title: `${x.title} (${x.server})` })), { server: sources.map((s) => s.name).join(', '), at }); fs.writeFileSync(path.join(OUT, 'BACKLOG.md'), md); console.log(`[${at}] ${report.join(' / ')} → ${path.join(OUT, 'BACKLOG.md')} (${all.length}건)`); })();