Function bodies 601 total
generatePessimistPosition method · typescript · L419-L440 (22 LOC)server/src/adapters/GeminiAdapter.ts
async generatePessimistPosition(data: {
topic: string;
contextData: unknown;
domain: string;
thesis: {
position: string;
reasoning: string;
evidence: string[];
confidence: number;
};
catsCommand: {
context: string;
task: string;
successCriteria: string;
};
}): Promise<{
position: string;
reasoning: string;
evidence: string[];
confidence: number;
suggestedActions: string[];
}> {generateMediatorSynthesis method · typescript · L487-L515 (29 LOC)server/src/adapters/GeminiAdapter.ts
async generateMediatorSynthesis(data: {
topic: string;
contextData: unknown;
domain: string;
thesis: {
position: string;
reasoning: string;
evidence: string[];
confidence: number;
};
antithesis: {
position: string;
reasoning: string;
evidence: string[];
confidence: number;
};
catsCommand: {
context: string;
task: string;
successCriteria: string;
};
}): Promise<{
position: string;
reasoning: string;
evidence: string[];
confidence: number;
suggestedActions: string[];
dissent?: string;
}> {performQAReview method · typescript · L569-L580 (12 LOC)server/src/adapters/GeminiAdapter.ts
async performQAReview(debate: {
topic: string;
domain: string;
thesis?: { position: string; reasoning: string; confidence: number };
antithesis?: { position: string; reasoning: string; confidence: number };
synthesis?: { position: string; reasoning: string; suggestedActions?: string[] };
}): Promise<{
approved: boolean;
score: number;
issues: string[];
recommendations: string[];
}> {performComplianceReview method · typescript · L637-L646 (10 LOC)server/src/adapters/GeminiAdapter.ts
async performComplianceReview(debate: {
topic: string;
domain: string;
fullContent: string;
}): Promise<{
approved: boolean;
score: number;
violations: string[];
recommendations: string[];
}> {parseDebateResponse method · typescript · L697-L707 (11 LOC)server/src/adapters/GeminiAdapter.ts
private parseDebateResponse(
text: string,
role: 'optimist' | 'pessimist' | 'mediator'
): {
position: string;
reasoning: string;
evidence: string[];
confidence: number;
suggestedActions: string[];
dissent?: string;
} {getDefaultPosition method · typescript · L734-L741 (8 LOC)server/src/adapters/GeminiAdapter.ts
private getDefaultPosition(role: 'optimist' | 'pessimist' | 'mediator'): string {
const defaults = {
optimist: '기회 요인이 발견되어 추가 검토가 권장됩니다.',
pessimist: '주의가 필요한 리스크 요인이 있습니다.',
mediator: '양측 의견을 종합하여 균형 잡힌 접근이 필요합니다.',
};
return defaults[role];
}getAuthClient method · typescript · L172-L203 (32 LOC)server/src/adapters/GoogleSheetAdapter.ts
private async getAuthClient(): Promise<sheets_v4.Sheets> {
if (this.sheetsClient) return this.sheetsClient;
// Method 1: GOOGLE_SERVICE_ACCOUNT_JSON env var
const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
if (serviceAccountJson) {
const credentials = JSON.parse(serviceAccountJson);
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
});
this.sheetsClient = google.sheets({ version: 'v4', auth: await auth.getClient() as any });
return this.sheetsClient;
}
// Method 2: Service account key file
const keyPath = process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH;
if (keyPath) {
const resolved = path.resolve(keyPath);
if (fs.existsSync(resolved)) {
const credentials = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['httpAll rows above produced by Repobility · https://repobility.com
fetchSheetByApi method · typescript · L208-L222 (15 LOC)server/src/adapters/GoogleSheetAdapter.ts
private async fetchSheetByApi(spreadsheetId: string, sheetName: string): Promise<string[][]> {
try {
const client = await this.getAuthClient();
const response = await client.spreadsheets.values.get({
spreadsheetId,
range: `'${sheetName}'!A:Z`,
});
const rows = response.data.values;
if (!rows || rows.length === 0) return [];
return rows.map(row => row.map(cell => String(cell ?? '')));
} catch (error) {
console.error(`Failed to fetch sheet "${sheetName}" via API:`, error);
return [];
}
}parseCSV method · typescript · L227-L275 (49 LOC)server/src/adapters/GoogleSheetAdapter.ts
private parseCSV(csv: string): string[][] {
const lines: string[][] = [];
let currentLine: string[] = [];
let currentField = '';
let inQuotes = false;
for (let i = 0; i < csv.length; i++) {
const char = csv[i];
const nextChar = csv[i + 1];
if (inQuotes) {
if (char === '"' && nextChar === '"') {
currentField += '"';
i++; // Skip next quote
} else if (char === '"') {
inQuotes = false;
} else {
currentField += char;
}
} else {
if (char === '"') {
inQuotes = true;
} else if (char === ',') {
currentLine.push(currentField.trim());
currentField = '';
} else if (char === '\n' || (char === '\r' && nextChar === '\n')) {
currentLine.push(currentField.trim());
if (currentLine.some(f => f !== '')) {
lines.push(currentLine);
}
currentLine = [];
currentField = '';
parseNumber method · typescript · L280-L285 (6 LOC)server/src/adapters/GoogleSheetAdapter.ts
private parseNumber(value: string): number {
if (!value || value.trim() === '' || value.trim() === '-') return 0;
const cleaned = value.replace(/,/g, '').replace(/\s/g, '');
const num = parseFloat(cleaned);
return isNaN(num) ? 0 : num;
}parseDate method · typescript · L291-L297 (7 LOC)server/src/adapters/GoogleSheetAdapter.ts
private parseDate(value: string): string {
if (!value || value.trim() === '') return '';
const s = value.trim().replace(/\//g, '-');
const m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (!m) return s;
return `${m[1]}-${m[2].padStart(2, '0')}-${m[3].padStart(2, '0')}`;
}fetchSheetByName method · typescript · L304-L321 (18 LOC)server/src/adapters/GoogleSheetAdapter.ts
private async fetchSheetByName(sheetName: string, spreadsheetId?: string): Promise<string[][]> {
const baseUrl = spreadsheetId
? `https://docs.google.com/spreadsheets/d/${spreadsheetId}/gviz/tq?tqx=out:csv`
: this.baseUrl;
const url = `${baseUrl}&sheet=${encodeURIComponent(sheetName)}`;
try {
const response = await fetch(url, { headers: { Accept: 'text/csv' } });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const csv = await response.text();
return this.parseCSV(csv);
} catch (error) {
console.error(`Failed to fetch sheet "${sheetName}":`, error);
return [];
}
}fetchSheet method · typescript · L326-L346 (21 LOC)server/src/adapters/GoogleSheetAdapter.ts
private async fetchSheet(gid: number): Promise<string[][]> {
const url = `${this.baseUrl}&gid=${gid}`;
try {
const response = await fetch(url, {
headers: {
Accept: 'text/csv',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const csv = await response.text();
return this.parseCSV(csv);
} catch (error) {
console.error(`Failed to fetch sheet gid=${gid}:`, error);
return [];
}
}fetchDailySales method · typescript · L351-L384 (34 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchDailySales(): Promise<DailySalesData[]> {
const rows = await this.fetchSheet(SHEET_GIDS.dailySales);
const results: DailySalesData[] = [];
// 첫 번째 행은 헤더, 데이터는 2행부터
// 빈 행과 합계 행 제외
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
const date = this.parseDate(row[0]);
// 날짜가 없는 행(합계 행 등) 제외
if (!date || !date.match(/^\d{4}-\d{2}-\d{2}$/)) continue;
results.push({
date,
jasaPrice: this.parseNumber(row[1]),
coupangPrice: this.parseNumber(row[2]),
kurlyPrice: this.parseNumber(row[3]),
totalRevenue: this.parseNumber(row[4]),
frozenSoup: this.parseNumber(row[5]),
etc: this.parseNumber(row[6]),
bibimbap: this.parseNumber(row[7]),
jasaHalf: this.parseNumber(row[8]),
coupangHalf: this.parseNumber(row[9]),
kurlyHalf: this.parseNumber(row[10]),
frozenHalf: this.parseNumber(row[11]),
etcHalf: this.parseNumber(row[12]),
fetchSalesDetail method · typescript · L390-L437 (48 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchSalesDetail(): Promise<SalesDetailData[]> {
const rows = await this.fetchSheetByName('판매');
if (rows.length < 2) return [];
// 헤더 행에서 컬럼 인덱스 자동 감지
const header = rows[0].map(h => (h || '').trim());
const col = {
date: header.findIndex(h => h.includes('일별') || h === '날짜'),
customer: header.findIndex(h => h.includes('거래처')),
productName: header.findIndex(h => h === '품목별' || h === '품목명'),
productCode: header.findIndex(h => h.includes('품목코드')),
quantity: header.findIndex(h => h === '수량'),
supplyAmount: header.findIndex(h => h.includes('공급가액')),
vat: header.findIndex(h => h.includes('부가세')),
total: header.findIndex(h => h === '합계'),
recommendedRevenue: header.findIndex(h => h.includes('권장판매')),
};
// 필수 컬럼 검증
if (col.date < 0 || col.supplyAmount < 0) {
console.error('[GoogleSheetAdapter] 판매 시트 헤더 매핑 실패:', header);
return [];
}
console.log('[GoogleSheetAdapter] 판매 시트 컬럼 매Repobility (the analyzer behind this table) · https://repobility.com
fetchProduction method · typescript · L442-L477 (36 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchProduction(): Promise<ProductionData[]> {
const rows = await this.fetchSheet(SHEET_GIDS.production);
const results: ProductionData[] = [];
// 복잡한 헤더 구조 - 실제 데이터는 컬럼 인덱스로 매핑
// 헤더 생산일, 생산수량(EA) 일반반찬, 전전처리, 냉동국, 소스, 비빔밥, 합계,
// 생산량(KG) 일반반찬, 전전처리, 냉동국, 소스, 합계,
// 완제품폐기량(EA), 비율(%), 반제품폐기량(KG), 반제품비율
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
const date = this.parseDate(row[0]);
if (!date || !date.match(/^\d{4}-\d{2}-\d{2}$/)) continue;
results.push({
date,
prodQtyNormal: this.parseNumber(row[1]),
prodQtyPreprocess: this.parseNumber(row[2]),
prodQtyFrozen: this.parseNumber(row[3]),
prodQtySauce: this.parseNumber(row[4]),
prodQtyBibimbap: this.parseNumber(row[5]),
prodQtyTotal: this.parseNumber(row[6]),
prodKgNormal: this.parseNumber(row[7]),
prodKgPreprocess: this.parseNumber(row[8]),
prodKgFrozen: this.parseNumber(row[9]),
fetchPurchases method · typescript · L484-L512 (29 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchPurchases(): Promise<PurchaseData[]> {
const rows = await this.fetchSheet(SHEET_GIDS.purchases);
const results: PurchaseData[] = [];
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
const rawDate = (row[0] || '').trim();
// YYYY/MM/DD-N 또는 YYYY/MM/DD 형식에서 날짜 부분만 추출
const dateMatch = rawDate.match(/^(\d{4}\/\d{2}\/\d{2})/);
if (!dateMatch) continue;
const date = dateMatch[1].replace(/\//g, '-');
results.push({
date,
productName: row[1] || '',
productCode: row[2] || '',
quantity: this.parseNumber(row[3]),
unitPrice: this.parseNumber(row[4]),
supplyAmount: this.parseNumber(row[5]),
vat: this.parseNumber(row[6]),
total: this.parseNumber(row[7]),
supplierName: row[8] || '',
});
}
return results;
}fetchUtilities method · typescript · L517-L550 (34 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchUtilities(): Promise<UtilityData[]> {
const rows = await this.fetchSheet(SHEET_GIDS.utilities);
const results: UtilityData[] = [];
// 복잡한 헤더 - 컬럼 인덱스로 매핑
// 날짜, 전기(전일검침, 당일검침, 공백, 공백, 공백, 일일사용량, 사용금액),
// 수도(전일검침, 당일검침, 일일사용량, 사용금액),
// 가스(전일검침, 당일검침, 일일사용량, 사용금액)
for (let i = 2; i < rows.length; i++) {
// 헤더가 2행에 걸쳐있을 수 있음
const row = rows[i];
const date = this.parseDate(row[0]);
if (!date || !date.match(/^\d{4}-\d{2}-\d{2}$/)) continue;
results.push({
date,
elecPrev: this.parseNumber(row[1]),
elecCurr: this.parseNumber(row[2]),
elecUsage: this.parseNumber(row[6]),
elecCost: this.parseNumber(row[7]),
waterPrev: this.parseNumber(row[8]),
waterCurr: this.parseNumber(row[9]),
waterUsage: this.parseNumber(row[10]),
waterCost: this.parseNumber(row[11]),
gasPrev: this.parseNumber(row[12]),
gasCurr: this.parseNumber(row[13]),
fetchLabor method · typescript · L556-L598 (43 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchLabor(): Promise<LaborData[]> {
const rows = await this.fetchSheetByName('노무비');
const results: LaborData[] = [];
// CSV 첫 행이 헤더, 데이터는 2행부터
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (!row[0] || !row[1]) continue;
// 날짜 정규화: "2026. 1. 1." → "2026-01-01"
const rawDate = (row[1] || '').trim();
const dateMatch = rawDate.match(/(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})/);
if (!dateMatch) continue;
const date = `${dateMatch[1]}-${dateMatch[2].padStart(2, '0')}-${dateMatch[3].padStart(2, '0')}`;
const department = (row[2] || '').trim();
if (!department) continue;
results.push({
week: row[0] || '',
date,
department,
headcount: this.parseNumber(row[3]),
weekdayRegularHours: this.parseNumber(row[4]),
weekdayOvertimeHours: this.parseNumber(row[5]),
weekdayNightHours: this.parseNumber(row[6]),
weekdayTotalHours: this.parseNumbefetchBom method · typescript · L604-L632 (29 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchBom(sheetName: string, source: 'SAN' | 'ZIP'): Promise<BomSheetData[]> {
const rows = await this.fetchSheetByApi(getBomSpreadsheetId(), sheetName);
const results: BomSheetData[] = [];
// 헤더가 2행, 데이터는 3행부터 (CSV 인덱스 기준 1부터 시작이므로 i=2)
for (let i = 2; i < rows.length; i++) {
const row = rows[i];
const productCode = (row[0] || '').trim();
if (!productCode) continue; // 빈 productCode 행 스킵
results.push({
source,
productCode,
productName: (row[1] || '').trim(),
bomVersion: (row[2] || '').trim(),
isExistingBom: (row[3] || '').trim() === 'Y' || (row[3] || '').trim() === '기존',
productionQty: this.parseNumber(row[4]),
materialCode: (row[5] || '').trim(),
materialName: (row[6] || '').trim(),
materialBomVersion: (row[7] || '').trim(),
consumptionQty: this.parseNumber(row[8]),
location: (row[9] || '').trim(),
remark: (row[10] || '').trim(),
addfetchMaterialMaster method · typescript · L638-L669 (32 LOC)server/src/adapters/GoogleSheetAdapter.ts
async fetchMaterialMaster(): Promise<MaterialMasterData[]> {
const rows = await this.fetchSheetByName('1.자재정보', getBomSpreadsheetId());
const results: MaterialMasterData[] = [];
// 헤더가 2행, 데이터는 3행부터 (CSV 인덱스 기준 i=2)
for (let i = 2; i < rows.length; i++) {
const row = rows[i];
const materialCode = (row[3] || '').trim();
if (!materialCode) continue; // 빈 자재코드 행 스킵
results.push({
no: this.parseNumber(row[0]),
category: (row[1] || '').trim(),
issueType: (row[2] || '').trim(),
materialCode,
materialName: (row[4] || '').trim(),
preprocessYield: this.parseNumber(row[5]),
spec: (row[6] || '').trim(),
unit: (row[7] || '').trim(),
unitPrice: this.parseNumber(row[8]),
safetyStock: this.parseNumber(row[9]),
excessStock: this.parseNumber(row[10]),
leadTimeDays: Math.round(this.parseNumber(row[11])),
recentOutputQty: this.parseNumber(row[12]),
dasyncAllData method · typescript · L674-L710 (37 LOC)server/src/adapters/GoogleSheetAdapter.ts
async syncAllData() {
console.log('Syncing Google Sheet data...');
const [dailySales, salesDetail, production, purchases, utilities, labor, sanBom, zipBom] = await Promise.all([
this.fetchDailySales(),
this.fetchSalesDetail(),
this.fetchProduction(),
this.fetchPurchases(),
this.fetchUtilities(),
this.fetchLabor(),
this.fetchBom('3. SAN_BOM', 'SAN'),
this.fetchBom('4. ZIP_BOM', 'ZIP'),
]);
console.log('Google Sheet sync complete:', {
dailySales: dailySales.length,
salesDetail: salesDetail.length,
production: production.length,
purchases: purchases.length,
utilities: utilities.length,
labor: labor.length,
sanBom: sanBom.length,
zipBom: zipBom.length,
});
return {
dailySales,
salesDetail,
production,
purchases,
utilities,
labor,
sanBom,
zipBom,
syncedAt: new Date().toISOString(),
};
}GoogleSheetsAdapter class · typescript · L33-L331 (299 LOC)server/src/adapters/GoogleSheetsAdapter.ts
export class GoogleSheetsAdapter {
private sheets: sheets_v4.Sheets | null = null;
private spreadsheetId: string;
private credentials: any = null;
constructor(spreadsheetId: string = '1GUo9wmwmm14zhb_gtpoNrDBfJ5DqEm5pf4jpRsto-JI') {
this.spreadsheetId = spreadsheetId;
}
setCredentials(credentials: any): void {
this.credentials = credentials;
this.sheets = null; // Reset to reconnect with new credentials
}
private async getClient(): Promise<sheets_v4.Sheets> {
if (this.sheets) return this.sheets;
// Method 0: Try GOOGLE_SERVICE_ACCOUNT_JSON env var (for cloud deployments)
const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
if (serviceAccountJson) {
try {
const credentials = JSON.parse(serviceAccountJson);
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const authClient = await auth.getClient();
Repobility · MCP-ready · https://repobility.com
constructor method · typescript · L38-L40 (3 LOC)server/src/adapters/GoogleSheetsAdapter.ts
constructor(spreadsheetId: string = '1GUo9wmwmm14zhb_gtpoNrDBfJ5DqEm5pf4jpRsto-JI') {
this.spreadsheetId = spreadsheetId;
}setCredentials method · typescript · L42-L45 (4 LOC)server/src/adapters/GoogleSheetsAdapter.ts
setCredentials(credentials: any): void {
this.credentials = credentials;
this.sheets = null; // Reset to reconnect with new credentials
}getClient method · typescript · L47-L131 (85 LOC)server/src/adapters/GoogleSheetsAdapter.ts
private async getClient(): Promise<sheets_v4.Sheets> {
if (this.sheets) return this.sheets;
// Method 0: Try GOOGLE_SERVICE_ACCOUNT_JSON env var (for cloud deployments)
const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
if (serviceAccountJson) {
try {
const credentials = JSON.parse(serviceAccountJson);
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const authClient = await auth.getClient();
this.sheets = google.sheets({
version: 'v4',
auth: authClient as any,
});
console.log('Google Sheets connected via GOOGLE_SERVICE_ACCOUNT_JSON env var');
return this.sheets;
} catch (error: any) {
console.error('Error parsing GOOGLE_SERVICE_ACCOUNT_JSON:', error.message);
}
}
// Method 1: Try service account JSON key file first
const serviceAccountPath =fetchSheetData method · typescript · L133-L166 (34 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchSheetData(sheetName: string): Promise<SheetRow[]> {
try {
const client = await this.getClient();
const response = await client.spreadsheets.values.get({
spreadsheetId: this.spreadsheetId,
range: `${sheetName}!A:Z`, // Fetch all columns
});
const rows = response.data.values;
if (!rows || rows.length === 0) {
console.log(`No data found in sheet: ${sheetName}`);
return [];
}
// First row is headers
const headers = rows[0] as string[];
const dataRows = rows.slice(1);
return dataRows.map(row => {
const obj: SheetRow = {};
headers.forEach((header, idx) => {
const value = row[idx] || '';
// Try to parse as number
const numValue = parseFloat(value.toString().replace(/,/g, ''));
obj[header] = isNaN(numValue) ? value : numValue;
});
return obj;
});
} catch (error: any) {
console.error(`Error fetchifetchAllCostData method · typescript · L168-L183 (16 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchAllCostData(): Promise<CostReportData> {
console.log('Fetching cost data from Google Sheets...');
const [sales, expenses, labor, waste] = await Promise.all([
this.fetchSheetData('DB_매출'),
this.fetchSheetData('DB_경비'),
this.fetchSheetData('DB_노무비'),
this.fetchSheetData('DB_폐기'),
]);
console.log(
`Fetched: 매출 ${sales.length}, 경비 ${expenses.length}, 노무비 ${labor.length}, 폐기 ${waste.length} rows`
);
return { sales, expenses, labor, waste };
}calculateMonthlySummary method · typescript · L186-L307 (122 LOC)server/src/adapters/GoogleSheetsAdapter.ts
calculateMonthlySummary(data: CostReportData): MonthlyCostSummary[] {
const monthlyData = new Map<string, MonthlyCostSummary>();
// Helper to extract month from date field
const getMonth = (row: SheetRow): string | null => {
// Try common date field names
const dateFields = ['날짜', '일자', 'date', 'Date', '년월', '월'];
for (const field of dateFields) {
if (row[field]) {
const dateStr = row[field].toString();
// Try to extract YYYY-MM format
const match = dateStr.match(/(\d{4})[-\/.]?(\d{2})/);
if (match) {
return `${match[1]}-${match[2]}`;
}
}
}
return null;
};
// Helper to get amount from row
const getAmount = (row: SheetRow): number => {
const amountFields = ['금액', '매출액', '비용', '원가', 'amount', 'Amount', '합계'];
for (const field of amountFields) {
if (row[field] !== undefined) {
const val = row[field];
return typeof vMultiSpreadsheetAdapter class · typescript · L383-L628 (246 LOC)server/src/adapters/GoogleSheetsAdapter.ts
export class MultiSpreadsheetAdapter {
private baseAdapter: GoogleSheetsAdapter;
private _salesPurchaseSpreadsheetId: string | null = null;
private _bomSpreadsheetId: string | null = null;
constructor() {
this.baseAdapter = new GoogleSheetsAdapter();
}
// Lazy load env vars (in case dotenv hasn't run yet at module load time)
private get salesPurchaseSpreadsheetId(): string {
if (this._salesPurchaseSpreadsheetId === null) {
this._salesPurchaseSpreadsheetId = process.env.GOOGLE_SALES_PURCHASE_SPREADSHEET_ID || '';
}
return this._salesPurchaseSpreadsheetId;
}
private get bomSpreadsheetId(): string {
if (this._bomSpreadsheetId === null) {
this._bomSpreadsheetId = process.env.GOOGLE_BOM_SPREADSHEET_ID || '';
}
return this._bomSpreadsheetId;
}
private async getClient(): Promise<sheets_v4.Sheets> {
// Reuse the base adapter's client initialization logic
return (this.baseAdapter as any).getClient();
}
async fetchSheconstructor method · typescript · L388-L390 (3 LOC)server/src/adapters/GoogleSheetsAdapter.ts
constructor() {
this.baseAdapter = new GoogleSheetsAdapter();
}Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
getClient method · typescript · L407-L410 (4 LOC)server/src/adapters/GoogleSheetsAdapter.ts
private async getClient(): Promise<sheets_v4.Sheets> {
// Reuse the base adapter's client initialization logic
return (this.baseAdapter as any).getClient();
}fetchSheetData method · typescript · L412-L443 (32 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchSheetData(spreadsheetId: string, sheetName: string): Promise<SheetRow[]> {
try {
const client = await this.getClient();
const response = await client.spreadsheets.values.get({
spreadsheetId: spreadsheetId,
range: `${sheetName}!A:Z`,
});
const rows = response.data.values;
if (!rows || rows.length === 0) {
console.log(`No data found in sheet: ${sheetName} (spreadsheet: ${spreadsheetId})`);
return [];
}
const headers = rows[0] as string[];
const dataRows = rows.slice(1);
return dataRows.map(row => {
const obj: SheetRow = {};
headers.forEach((header, idx) => {
const value = row[idx] || '';
const numValue = parseFloat(value.toString().replace(/,/g, ''));
obj[header] = isNaN(numValue) ? value : numValue;
});
return obj;
});
} catch (error: any) {
console.error(`Error fetching sheet ${sheetName}:`, error.messaglistSheets method · typescript · L445-L454 (10 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async listSheets(spreadsheetId: string): Promise<string[]> {
try {
const client = await this.getClient();
const response = await client.spreadsheets.get({ spreadsheetId });
return response.data.sheets?.map(s => s.properties?.title || '') || [];
} catch (error: any) {
console.error(`Error listing sheets:`, error.message);
return [];
}
}fetchSalesTransactions method · typescript · L456-L493 (38 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchSalesTransactions(): Promise<SalesTransaction[]> {
if (!this.salesPurchaseSpreadsheetId) {
console.warn('Sales/Purchase spreadsheet ID not configured');
return [];
}
// Try different sheet names for sales data
const possibleSheetNames = ['판매현황', '판매', 'Sales', 'DB_판매', '매출'];
const sheets = await this.listSheets(this.salesPurchaseSpreadsheetId);
console.log(`[MultiSpreadsheetAdapter] 판매/구매 시트 목록: ${sheets.join(', ')}`);
let salesSheet = sheets.find(s => possibleSheetNames.some(n => s.includes(n)));
if (!salesSheet && sheets.length > 0) {
// Use first sheet if no match found
salesSheet = sheets[0];
}
if (!salesSheet) {
console.warn('No sales sheet found');
return [];
}
console.log(`[MultiSpreadsheetAdapter] 판매 시트 사용: ${salesSheet}`);
const rawData = await this.fetchSheetData(this.salesPurchaseSpreadsheetId, salesSheet);
return rawData.map(row => ({
date: String(row['일자'] |fetchPurchaseTransactions method · typescript · L495-L529 (35 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchPurchaseTransactions(): Promise<PurchaseTransaction[]> {
if (!this.salesPurchaseSpreadsheetId) {
console.warn('Sales/Purchase spreadsheet ID not configured');
return [];
}
const possibleSheetNames = ['구매현황', '구매', 'Purchase', 'DB_구매', '매입'];
const sheets = await this.listSheets(this.salesPurchaseSpreadsheetId);
let purchaseSheet = sheets.find(s => possibleSheetNames.some(n => s.includes(n)));
if (!purchaseSheet) {
// Try second sheet
purchaseSheet = sheets[1] || sheets[0];
}
if (!purchaseSheet) {
console.warn('No purchase sheet found');
return [];
}
console.log(`[MultiSpreadsheetAdapter] 구매 시트 사용: ${purchaseSheet}`);
const rawData = await this.fetchSheetData(this.salesPurchaseSpreadsheetId, purchaseSheet);
return rawData.map(row => ({
date: String(row['일자'] || row['날짜'] || row['date'] || ''),
supplierCode: String(row['거래처코드'] || row['거래처 코드'] || ''),
supplierName: StrfetchBomData method · typescript · L531-L574 (44 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchBomData(): Promise<BomItem[]> {
if (!this.bomSpreadsheetId) {
console.warn('BOM spreadsheet ID not configured');
return [];
}
const sheets = await this.listSheets(this.bomSpreadsheetId);
console.log(`[MultiSpreadsheetAdapter] BOM 시트 목록: ${sheets.join(', ')}`);
// Try to find BOM sheet
const possibleSheetNames = ['BOM', 'bom', '자재명세서', '소요량', 'DB_BOM'];
let bomSheet = sheets.find(s =>
possibleSheetNames.some(n => s.toLowerCase().includes(n.toLowerCase()))
);
if (!bomSheet && sheets.length > 0) {
bomSheet = sheets[0];
}
if (!bomSheet) {
console.warn('No BOM sheet found');
return [];
}
console.log(`[MultiSpreadsheetAdapter] BOM 시트 사용: ${bomSheet}`);
const rawData = await this.fetchSheetData(this.bomSpreadsheetId, bomSheet);
return rawData.map(row => ({
parentItemCode: String(
row['모품목코드'] || row['완제품코드'] || row['생산품목코드'] || row['상위품목'] || ''
),
parentfetchAllCostAnalysisData method · typescript · L576-L595 (20 LOC)server/src/adapters/GoogleSheetsAdapter.ts
async fetchAllCostAnalysisData(): Promise<CostAnalysisData> {
console.log('[MultiSpreadsheetAdapter] 원가 분석 데이터 수집 시작...');
const [sales, purchases, bom] = await Promise.all([
this.fetchSalesTransactions(),
this.fetchPurchaseTransactions(),
this.fetchBomData(),
]);
console.log(
`[MultiSpreadsheetAdapter] 수집 완료: 판매 ${sales.length}, 구매 ${purchases.length}, BOM ${bom.length} 건`
);
return {
sales,
purchases,
bom,
fetchedAt: new Date().toISOString(),
};
}getClient method · typescript · L200-L208 (9 LOC)server/src/adapters/SupabaseAdapter.ts
private getClient(): SupabaseClient {
if (!this.client) {
if (!this.url || !this.key) {
throw new Error('SUPABASE_URL and SUPABASE_KEY must be set in environment variables');
}
this.client = createClient(this.url, this.key);
}
return this.client;
}All rows above produced by Repobility · https://repobility.com
isConfigured method · typescript · L210-L212 (3 LOC)server/src/adapters/SupabaseAdapter.ts
isConfigured(): boolean {
return !!(this.url && this.key);
}fetchAllPaginated method · typescript · L218-L253 (36 LOC)server/src/adapters/SupabaseAdapter.ts
private async fetchAllPaginated<T>(
table: string,
options?: {
orderBy?: string;
ascending?: boolean;
dateFrom?: string;
dateTo?: string;
dateColumn?: string;
}
): Promise<T[]> {
const client = this.getClient();
const pageSize = 1000;
const allData: T[] = [];
let from = 0;
while (true) {
let query = client
.from(table)
.select('*')
.order(options?.orderBy || 'date', { ascending: options?.ascending ?? false })
.range(from, from + pageSize - 1);
if (options?.dateFrom) query = query.gte(options.dateColumn || 'date', options.dateFrom);
if (options?.dateTo) query = query.lte(options.dateColumn || 'date', options.dateTo);
const { data, error } = await query;
if (error) throw new Error(`${table} query failed: ${error.message}`);
if (!data || data.length === 0) break;
allData.push(...(data as T[]));
if (data.length < pageSize) break; // 마지막 페이지
upsertDailySales method · typescript · L259-L272 (14 LOC)server/src/adapters/SupabaseAdapter.ts
async upsertDailySales(rows: DailySalesRow[]): Promise<number> {
if (rows.length === 0) return 0;
const client = this.getClient();
const { data, error } = await client
.from('daily_sales')
.upsert(
rows.map(r => ({ ...r, synced_at: new Date().toISOString() })),
{ onConflict: 'date' }
);
if (error) throw new Error(`daily_sales upsert failed: ${error.message}`);
return rows.length;
}getDailySales method · typescript · L274-L276 (3 LOC)server/src/adapters/SupabaseAdapter.ts
async getDailySales(dateFrom?: string, dateTo?: string): Promise<DailySalesRow[]> {
return this.fetchAllPaginated<DailySalesRow>('daily_sales', { dateFrom, dateTo });
}upsertSalesDetail method · typescript · L282-L319 (38 LOC)server/src/adapters/SupabaseAdapter.ts
async upsertSalesDetail(rows: SalesDetailRow[]): Promise<number> {
if (rows.length === 0) return 0;
const client = this.getClient();
// 날짜별 그룹핑 → RPC로 원자적 DELETE+INSERT (트랜잭션 안전)
const byDate = new Map<string, SalesDetailRow[]>();
for (const r of rows) {
if (!r.date) continue;
const arr = byDate.get(r.date) || [];
arr.push(r);
byDate.set(r.date, arr);
}
let totalInserted = 0;
const errors: string[] = [];
const syncedAt = new Date().toISOString();
for (const [date, dateRows] of byDate) {
try {
const jsonRows = dateRows.map(r => ({ ...r, synced_at: syncedAt }));
const { data, error } = await client.rpc('upsert_sales_detail_by_date', {
p_date: date,
p_rows: jsonRows,
});
if (error) throw error;
totalInserted += (data as number) ?? dateRows.length;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
errors.pugetSalesDetail method · typescript · L321-L323 (3 LOC)server/src/adapters/SupabaseAdapter.ts
async getSalesDetail(dateFrom?: string, dateTo?: string): Promise<SalesDetailRow[]> {
return this.fetchAllPaginated<SalesDetailRow>('sales_detail', { dateFrom, dateTo });
}upsertProductionDaily method · typescript · L329-L342 (14 LOC)server/src/adapters/SupabaseAdapter.ts
async upsertProductionDaily(rows: ProductionDailyRow[]): Promise<number> {
if (rows.length === 0) return 0;
const client = this.getClient();
const { error } = await client
.from('production_daily')
.upsert(
rows.map(r => ({ ...r, synced_at: new Date().toISOString() })),
{ onConflict: 'date' }
);
if (error) throw new Error(`production_daily upsert failed: ${error.message}`);
return rows.length;
}getProductionDaily method · typescript · L344-L346 (3 LOC)server/src/adapters/SupabaseAdapter.ts
async getProductionDaily(dateFrom?: string, dateTo?: string): Promise<ProductionDailyRow[]> {
return this.fetchAllPaginated<ProductionDailyRow>('production_daily', { dateFrom, dateTo });
}Repobility (the analyzer behind this table) · https://repobility.com
upsertPurchases method · typescript · L352-L389 (38 LOC)server/src/adapters/SupabaseAdapter.ts
async upsertPurchases(rows: PurchaseRow[]): Promise<number> {
if (rows.length === 0) return 0;
const client = this.getClient();
// 날짜별 그룹핑 → RPC로 원자적 DELETE+INSERT (트랜잭션 안전)
const byDate = new Map<string, PurchaseRow[]>();
for (const r of rows) {
if (!r.date) continue;
const arr = byDate.get(r.date) || [];
arr.push(r);
byDate.set(r.date, arr);
}
let totalInserted = 0;
const errors: string[] = [];
const syncedAt = new Date().toISOString();
for (const [date, dateRows] of byDate) {
try {
const jsonRows = dateRows.map(r => ({ ...r, synced_at: syncedAt }));
const { data, error } = await client.rpc('upsert_purchases_by_date', {
p_date: date,
p_rows: jsonRows,
});
if (error) throw error;
totalInserted += (data as number) ?? dateRows.length;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
errors.push(`${date}getPurchases method · typescript · L391-L393 (3 LOC)server/src/adapters/SupabaseAdapter.ts
async getPurchases(dateFrom?: string, dateTo?: string): Promise<PurchaseRow[]> {
return this.fetchAllPaginated<PurchaseRow>('purchases', { dateFrom, dateTo });
}upsertInventory method · typescript · L399-L417 (19 LOC)server/src/adapters/SupabaseAdapter.ts
async upsertInventory(rows: InventoryRow[]): Promise<number> {
if (rows.length === 0) return 0;
const client = this.getClient();
const today = new Date().toISOString().slice(0, 10);
const { error } = await client
.from('inventory')
.upsert(
rows.map(r => ({
...r,
snapshot_date: r.snapshot_date || today,
synced_at: new Date().toISOString(),
})),
{ onConflict: 'product_code,warehouse_code,snapshot_date' }
);
if (error) throw new Error(`inventory upsert failed: ${error.message}`);
return rows.length;
}