Function bodies 131 total
GET function · typescript · L6-L73 (68 LOC)app/api/rates/route.ts
export async function GET(req: NextRequest) {
const country = req.nextUrl.searchParams.get('country');
if (!country) {
return NextResponse.json({ error: 'country parameter required' }, { status: 400 });
}
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
);
const fromDate = new Date();
fromDate.setDate(fromDate.getDate() - 30);
const fromDateStr = fromDate.toISOString().slice(0, 10);
const { data, error } = await supabase
.from('rate_records')
.select('*')
.eq('receiving_country', country)
.gte('run_hour', fromDateStr)
.order('run_hour', { ascending: false })
.limit(50000);
if (error || !data) {
return NextResponse.json({ error: error?.message ?? 'No data' }, { status: 500 });
}
// Build GME baseline map
const gmeBaselineMap = new Map<string, number>();
data.forEach((r: Record<string, unknown>) => {
if (r.operator === 'GME' && r.total_sending_amount) {
gmeBaselineMformatKRW function · typescript · L120-L122 (3 LOC)app/components/Dashboard.tsx
function formatKRW(value: number, t: T) {
return value.toLocaleString('ko-KR') + t.won;
}formatRunHour function · typescript · L124-L128 (5 LOC)app/components/Dashboard.tsx
function formatRunHour(runHour: string) {
const m = runHour.match(/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/);
if (!m) return runHour;
return `${parseInt(m[2])}/${parseInt(m[3])} ${m[4]}:${m[5]}`;
}statusLabel function · typescript · L130-L135 (6 LOC)app/components/Dashboard.tsx
function statusLabel(status: string, t: T) {
if (status === 'GME') return t.statusGME;
if (status === 'Cheaper than GME') return t.statusCheaper;
if (status === 'Expensive than GME') return t.statusExpensive;
return status;
}statusColor function · typescript · L137-L141 (5 LOC)app/components/Dashboard.tsx
function statusColor(status: string) {
if (status === 'GME') return { bg: 'bg-red-500/20', text: 'text-red-500 dark:text-red-400', hex: '#ef4444' };
if (status === 'Cheaper than GME') return { bg: 'bg-green-500/20', text: 'text-green-600 dark:text-green-400', hex: '#22c55e' };
return { bg: 'bg-orange-500/20', text: 'text-orange-500 dark:text-orange-400', hex: '#f97316' };
}rateExchangeKRW function · typescript · L159-L165 (7 LOC)app/components/Dashboard.tsx
function rateExchangeKRW(r: RateRecord): number {
if (r.operator === 'GME') {
const embedded = GME_EMBEDDED_FEE[r.receivingCountry] ?? 0;
return Math.max(r.sendAmountKRW - embedded, 1);
}
return r.sendAmountKRW;
}SunIcon function · typescript · L169-L175 (7 LOC)app/components/Dashboard.tsx
function SunIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>
);
}Source: Repobility analyzer · https://repobility.com
MoonIcon function · typescript · L177-L183 (7 LOC)app/components/Dashboard.tsx
function MoonIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>
);
}KPICard function · typescript · L187-L197 (11 LOC)app/components/Dashboard.tsx
function KPICard({
title, value, sub, color = 'text-slate-900 dark:text-slate-100',
}: { title: string; value: string; sub?: string; color?: string }) {
return (
<div className="bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-4">
<p className="text-slate-500 dark:text-slate-400 text-xs mb-1">{title}</p>
<p className={`text-2xl font-bold ${color}`}>{value}</p>
{sub && <p className="text-slate-400 dark:text-slate-500 text-xs mt-1">{sub}</p>}
</div>
);
}SnapshotTooltip function · typescript · L201-L219 (19 LOC)app/components/Dashboard.tsx
function SnapshotTooltip({ active, payload, t }: { active?: boolean; payload?: readonly { payload: RateRecord }[]; t: T }) {
if (!active || !payload?.length) return null;
const d = payload[0].payload;
const sc = statusColor(d.status);
return (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 text-sm shadow-xl">
<p className="font-semibold text-slate-900 dark:text-slate-100 mb-1">{d.operator}</p>
<p className="text-slate-600 dark:text-slate-300">{t.totalSendLabel} <span className="font-mono">{formatKRW(d.totalSendingAmount, t)}</span></p>
{d.priceGap !== null && d.status !== 'GME' && (
<p className={d.priceGap < 0 ? 'text-green-600 dark:text-green-400' : 'text-red-500 dark:text-red-400'}>
{t.vsGME} <span className="font-mono">{d.priceGap > 0 ? '+' : ''}{d.priceGap.toLocaleString('ko-KR')}{t.won}</span>
</p>
)}
<span className={`inline-block px-2 py-0.5 rounded-full GapTooltip function · typescript · L221-L233 (13 LOC)app/components/Dashboard.tsx
function GapTooltip({ active, payload, t }: { active?: boolean; payload?: readonly { payload: { operator: string; avgGap: number; count: number } }[]; t: T }) {
if (!active || !payload?.length) return null;
const d = payload[0].payload;
return (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 text-sm shadow-xl">
<p className="font-semibold text-slate-900 dark:text-slate-100 mb-1">{d.operator}</p>
<p className={d.avgGap < 0 ? 'text-green-600 dark:text-green-400' : 'text-red-500 dark:text-red-400'}>
{t.avgDiffLabel} <span className="font-mono">{d.avgGap > 0 ? '+' : ''}{d.avgGap.toLocaleString('ko-KR')}{t.won}</span>
</p>
<p className="text-slate-400 dark:text-slate-400 text-xs mt-1">{t.dataPoints(d.count)}</p>
</div>
);
}TrendTooltip function · typescript · L235-L243 (9 LOC)app/components/Dashboard.tsx
function TrendTooltip({ active, payload, label, t }: { active?: boolean; payload?: readonly { value: number }[]; label?: string | number; t: T }) {
if (!active || !payload?.length) return null;
return (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 text-sm shadow-xl">
<p className="text-slate-500 dark:text-slate-400 text-xs mb-1">{label}</p>
<p className="font-semibold text-blue-600 dark:text-blue-400 font-mono">{formatKRW(payload[0].value, t)}</p>
</div>
);
}handleClickOutside function · typescript · L325-L329 (5 LOC)app/components/Dashboard.tsx
function handleClickOutside(e: MouseEvent) {
if (countryDropdownRef.current && !countryDropdownRef.current.contains(e.target as Node)) {
setCountryDropdownOpen(false);
}
}RootLayout function · typescript · L20-L34 (15 LOC)app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}parseRates function · typescript · L19-L21 (3 LOC)app/lib/parseRates.ts
export function parseRates(): RateRecord[] {
return RATES_DATA;
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
Home function · typescript · L10-L85 (76 LOC)app/page.tsx
export default async function Home() {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
);
// Fetch distinct countries from recent data
const { data: countryRows } = await supabase
.from('rate_records')
.select('receiving_country')
.order('run_hour', { ascending: false })
.limit(1000);
const countries = [...new Set((countryRows ?? []).map((r: { receiving_country: string }) => r.receiving_country))].sort() as string[];
// Fetch 30 days of data for the default country
const fromDate = new Date();
fromDate.setDate(fromDate.getDate() - 30);
const fromDateStr = fromDate.toISOString().slice(0, 10);
const { data, error } = await supabase
.from('rate_records')
.select('*')
.eq('receiving_country', DEFAULT_COUNTRY)
.gte('run_hour', fromDateStr)
.order('run_hour', { ascending: false })
.limit(50000);
if (error || !data) {
return (
<div className="min-h-screen bg-slate-950scrapeGme_THB function · javascript · L22-L34 (13 LOC)scraper/compare-fees.js
async function scrapeGme_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://online.gmeremit.com/', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForSelector('#nCountry', { timeout: 10000 });
await page.click('#nCountry'); await page.waitForTimeout(500);
await page.fill('#CountryValue', 'Thailand'); await page.waitForTimeout(300);
await page.click('#toCurrUl li[data-countrycode="THB"]'); await page.waitForTimeout(1000);
await page.fill('#recAmt', '26000'); await page.dispatchEvent('#recAmt', 'change');
await page.waitForTimeout(3000);
return { service_fee: 0 };
} finally { await page.close(); }
}scrapeGmt_THB function · javascript · L36-L42 (7 LOC)scraper/compare-fees.js
async function scrapeGmt_THB() {
const url = 'https://mapi.gmoneytrans.net/exratenew1/ajx_calcRate.asp?receive_amount=26000&payout_country=Thailand&total_collected=0&payment_type=Bank+Account¤cyType=THB';
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
const text = await res.text();
const m = text.match(/serviceCharge--td_clm--([\d.]+)--td_end--/);
return { service_fee: m ? parseFloat(m[1]) : null };
}scrapeWb_THB function · javascript · L44-L64 (21 LOC)scraper/compare-fees.js
async function scrapeWb_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.wirebarley.com/ko', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
await page.locator('#lafc-popup button').click().catch(() => null); await page.waitForTimeout(1000);
await page.locator('[data-title="currencyToMoneyBox"]').nth(1).locator('img[alt="드롭 다운"]').click();
await page.waitForTimeout(2000);
await page.locator('button:has(img[alt="TH"])').click(); await page.waitForTimeout(2000);
await page.locator('[data-title="currencyToMoneyBox"]').nth(1).locator('button').click();
await page.waitForTimeout(500);
await page.locator('input').nth(1).fill('26000');
await page.locator('input').nth(1).press('Enter'); await page.waitForTimeout(3000);
const feeRaw = await page.evaluate(() => {
const ps = Array.from(document.querySelectorAll('p'));
const label = ps.find(p => p.textContent.trim() ==scrapeHanpass_THB function · javascript · L68-L81 (14 LOC)scraper/compare-fees.js
async function scrapeHanpass_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.hanpass.com/en', { waitUntil: 'networkidle', timeout: 30000 });
await page.locator('.ExchangeCalculator_recipientAmountField__lbLSL button').click();
await page.waitForSelector('#countrySearch', { timeout: 10000 });
await page.fill('#countrySearch', 'Thailand'); await page.waitForTimeout(500);
await page.locator('button[aria-label="Thailand THB"]').first().click(); await page.waitForTimeout(1500);
await page.fill('#recipient', '26000'); await page.press('#recipient', 'Tab'); await page.waitForTimeout(3000);
const feeRaw = await page.locator('.ExchangeCalculator_row__EPWVT')
.filter({ hasText: 'Remittance fee' }).locator('span:last-child').textContent().catch(() => null);
return { service_fee: extractNumber(feeRaw) ?? 0 };
} finally { await page.close(); }
}scrapeUtransfer_THB function · javascript · L83-L94 (12 LOC)scraper/compare-fees.js
async function scrapeUtransfer_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.utransfer.com', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(2000);
await page.locator('select').nth(1).selectOption('THB'); await page.waitForTimeout(1000);
await page.fill('input[name="toAmount"]', '26000');
await page.dispatchEvent('input[name="toAmount"]', 'change'); await page.waitForTimeout(3000);
const feeRaw = await page.locator('.utansfer_fees').textContent().catch(() => null);
return { service_fee: extractNumber(feeRaw) ?? 5000 };
} finally { await page.close(); }
}scrapeJrf_THB function · javascript · L100-L112 (13 LOC)scraper/compare-fees.js
async function scrapeJrf_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.jpremit.co.kr/', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
await page.click('#div_curr'); await page.waitForTimeout(500);
await page.click('li#THB'); await page.waitForTimeout(1500);
await page.fill('#rec_money', '26000'); await page.dispatchEvent('#rec_money', 'keyup');
await page.waitForTimeout(3000);
const feeRaw = await page.textContent('#servicefee').catch(() => null);
return { service_fee: extractNumber(feeRaw) ?? 5000 };
} finally { await page.close(); }
}scrapeE9pay_THB function · javascript · L114-L130 (17 LOC)scraper/compare-fees.js
async function scrapeE9pay_THB(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.e9pay.co.kr/', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForSelector('#TH_THB', { state: 'attached', timeout: 10000 });
await page.evaluate(() => {
const r = document.querySelector('#TH_THB');
r.checked = true; r.dispatchEvent(new Event('change', { bubbles: true }));
});
await page.waitForTimeout(1000);
await page.click('#reverse'); await page.waitForTimeout(500);
await page.fill('#receive-money', '26000'); await page.dispatchEvent('#receive-money', 'blur');
await page.waitForTimeout(3000);
const feeRaw = await page.$eval('#remit-fee', el => el.textContent || el.value).catch(() => null);
return { service_fee: extractNumber(feeRaw) ?? 0 };
} finally { await page.close(); }
}Powered by Repobility — scan your code at https://repobility.com
extractNumber function · javascript · L5-L10 (6 LOC)scraper/lib/browser.js
export function extractNumber(text) {
if (!text) return null;
const cleaned = text.replace(/[^0-9.]/g, '');
const num = parseFloat(cleaned);
return isNaN(num) || num === 0 ? null : num;
}getRunHour function · javascript · L17-L25 (9 LOC)scraper/lib/browser.js
export function getRunHour() {
const kst = new Date(Date.now() + 9 * 60 * 60 * 1000);
const yyyy = kst.getUTCFullYear();
const mm = String(kst.getUTCMonth() + 1).padStart(2, '0');
const dd = String(kst.getUTCDate()).padStart(2, '0');
const hh = String(kst.getUTCHours()).padStart(2, '0');
const min = kst.getUTCMinutes() < 30 ? '00' : '30';
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
}withRetry function · javascript · L30-L40 (11 LOC)scraper/lib/browser.js
export async function withRetry(fn, retries = 2, delayMs = 3000) {
for (let attempt = 1; attempt <= retries + 1; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt > retries) throw err;
console.warn(` 재시도 ${attempt}/${retries} — ${err.message?.slice(0, 80)}`);
await new Promise(r => setTimeout(r, delayMs * attempt));
}
}
}trySelectors function · javascript · L45-L55 (11 LOC)scraper/lib/browser.js
export async function trySelectors(page, selectors, timeout = 5000) {
for (const selector of selectors) {
try {
await page.waitForSelector(selector, { timeout });
return selector;
} catch {
// 다음 셀렉터 시도
}
}
return null;
}getTextFromSelectors function · javascript · L60-L74 (15 LOC)scraper/lib/browser.js
export async function getTextFromSelectors(page, selectors) {
for (const selector of selectors) {
try {
const el = await page.$(selector);
if (el) {
const text = await el.textContent();
const num = extractNumber(text);
if (num && num > 100000) return num; // 최소 100,000 KRW 이상이어야 유효
}
} catch {
// 다음 셀렉터 시도
}
}
return null;
}saveRates function · javascript · L11-L19 (9 LOC)scraper/lib/supabase.js
export async function saveRates(records) {
if (records.length === 0) return;
const { error } = await supabase
.from('rate_records')
.upsert(records, { onConflict: 'run_hour,operator,receiving_country' });
if (error) throw new Error(`Supabase upsert error: ${error.message}`);
}scrapeGme function · javascript · L19-L38 (20 LOC)scraper/run-cny.js
async function scrapeGme(browser) {
const page = await browser.newPage();
try {
await page.goto('https://online.gmeremit.com/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('#nCountry', { timeout: 10000 });
await page.click('#nCountry'); await page.waitForTimeout(500);
await page.fill('#CountryValue', 'China'); await page.waitForTimeout(300);
await page.click('#toCurrUl li[data-countrycode="CNY"]');
await page.waitForTimeout(1000);
await page.click('#recAmt', { clickCount: 3 });
await page.fill('#recAmt', String(AMOUNT));
await page.dispatchEvent('#recAmt', 'change');
await page.waitForTimeout(3000);
const raw = await page.$eval('#numAmount', el => el.value || el.textContent).catch(() => null);
const total = extractNumber(raw);
if (!total) throw new Error('총 송금액 추출 실패');
return { operator: 'GME', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: total, service_fee: 0, scrapeGmoneytrans function · javascript · L41-L57 (17 LOC)scraper/run-cny.js
async function scrapeGmoneytrans() {
const url = 'https://mapi.gmoneytrans.net/exratenew1/ajx_calcRate.asp'
+ `?receive_amount=${AMOUNT}`
+ '&payout_country=China'
+ '&total_collected=0'
+ '&payment_type=Alipay'
+ '¤cyType=CNY';
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
const sendAmount = parseField(text, 'sendAmount');
if (!sendAmount) throw new Error(`파싱 실패: ${text.slice(0, 200)}`);
const fee = 4000; // Alipay hardcoded
return { operator: 'GMoneyTrans', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: sendAmount, service_fee: fee,
total_sending_amount: sendAmount + fee };
}Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
parseField function · javascript · L58-L61 (4 LOC)scraper/run-cny.js
function parseField(text, field) {
const m = text.match(new RegExp(`${field}--td_clm--([\\d.]+)--td_end--`));
return m ? parseFloat(m[1]) : null;
}scrapeSentbe function · javascript · L64-L88 (25 LOC)scraper/run-cny.js
async function scrapeSentbe(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
locale: 'ko-KR',
});
const page = await context.newPage();
try {
await page.goto('https://www.sentbe.com/ko', { waitUntil: 'networkidle', timeout: 30000 });
await page.click('button.close').catch(() => null); await page.waitForTimeout(300);
await page.click('article.app-download-popup .dim').catch(() => null); await page.waitForTimeout(500);
await page.waitForSelector('.receiveAmountInput .el-input-group__append', { timeout: 10000 });
await page.click('.receiveAmountInput .el-input-group__append'); await page.waitForTimeout(500);
await page.click('.receiveAmountInput .el-select-dropdown__item:has-text("중국")');
await page.waitForTimeout(1000);
await page.click('#receiveAmount', { clickCount: 3 });
await page.fill('#receiveAmount', scrapeHanpass function · javascript · L91-L108 (18 LOC)scraper/run-cny.js
async function scrapeHanpass() {
const res = await fetch('https://app.hanpass.com/app/v1/remittance/get-cost', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inputAmount: String(AMOUNT), inputCurrencyCode: 'CNY',
fromCurrencyCode: 'KRW', toCurrencyCode: 'CNY', toCountryCode: 'CN',
memberSeq: '1', lang: 'en' }),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.resultCode !== '0') throw new Error(`Hanpass API 오류: ${data.resultMessage}`);
const total = data.depositAmountIncludingFee;
const fee = data.transferFee ?? 0;
if (!total) throw new Error('총 송금액 추출 실패');
return { operator: 'Hanpass', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: total - fee, service_fee: fee, total_sending_amount: total };
}scrapeSbi function · javascript · L111-L133 (23 LOC)scraper/run-cny.js
async function scrapeSbi(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
locale: 'ko-KR',
});
const page = await context.newPage();
try {
await page.goto('https://www.sbicosmoney.com/', { waitUntil: 'load', timeout: 30000 });
await page.waitForTimeout(2000);
await page.click('button:has-text("Close")').catch(() => null); await page.waitForTimeout(500);
await page.click('.dest-country'); await page.waitForTimeout(500);
await page.click('a[data-currency="CNY"]'); await page.waitForTimeout(1500);
await page.click('#targetAmount', { clickCount: 3 });
await page.fill('#targetAmount', String(AMOUNT));
await page.dispatchEvent('#targetAmount', 'input'); await page.waitForTimeout(2000);
const raw = await page.inputValue('#krwAmount');
const sendAmt = extractNumber(raw);
if (!sendAmt) throw new Error('총 송금scrapeCross function · javascript · L136-L156 (21 LOC)scraper/run-cny.js
async function scrapeCross(browser) {
const page = await browser.newPage();
try {
await page.goto('https://crossenf.com/remittance', { waitUntil: 'load', timeout: 30000 });
await page.waitForTimeout(2000);
await page.locator('div.relative:has(span:text("THB"))').click();
await page.waitForSelector('#aside-root ul', { timeout: 10000 });
await page.locator('#aside-root li:has(img[alt="CN flag"])').click();
await page.waitForTimeout(1000);
const receiveInput = page.locator('input[inputmode="numeric"]').nth(1);
await receiveInput.click({ clickCount: 3 });
await receiveInput.fill(String(AMOUNT));
await receiveInput.press('Tab'); await page.waitForTimeout(3000);
const totalRaw = await page.locator('input[inputmode="numeric"]').nth(0).inputValue();
const total = extractNumber(totalRaw);
if (!total) throw new Error('총 송금액 추출 실패');
const fee = 5000;
return { operator: 'Cross', receiving_country: COUNTRY, receive_amount: AMOUNT,
scrapeWirebarley function · javascript · L159-L196 (38 LOC)scraper/run-cny.js
async function scrapeWirebarley(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
locale: 'ko-KR',
});
const page = await context.newPage();
try {
await page.goto('https://www.wirebarley.com/ko', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
await page.locator('#lafc-popup button').click().catch(() => null); await page.waitForTimeout(1000);
await page.locator('[data-title="currencyToMoneyBox"]').nth(1)
.locator('img[alt="드롭 다운"]').click();
await page.waitForTimeout(2000);
await page.locator('button:has(img[alt="CN"])').click();
await page.waitForTimeout(2000);
await page.locator('[data-title="currencyToMoneyBox"]').nth(1).locator('button').click();
await page.waitForTimeout(500);
await page.locator('input').nth(1).click({ clickCount: 3 });
await page.locator('scrapeCoinshot function · javascript · L199-L219 (21 LOC)scraper/run-cny.js
async function scrapeCoinshot(browser) {
const page = await browser.newPage();
try {
await page.goto('https://coinshot.org/main', { waitUntil: 'load', timeout: 30000 });
await page.waitForTimeout(2000);
await page.waitForSelector('button.lang-btn[value="ko"]', { timeout: 10000 });
await page.click('button.lang-btn[value="ko"]'); await page.waitForTimeout(1000);
await page.click('#current-receiving-currency'); await page.waitForTimeout(500);
await page.click('#select-receiving-currency a[data-currency="CNY"]');
await page.waitForTimeout(1000);
await page.click('#receiving-input', { clickCount: 3 });
await page.fill('#receiving-input', String(AMOUNT));
await page.press('#receiving-input', 'Enter'); await page.waitForTimeout(3000);
const raw = await page.inputValue('#sending-input');
const sendAmt = extractNumber(raw);
if (!sendAmt) throw new Error('총 송금액 추출 실패');
const fee = 10000; // hardcoded
return { operator: 'Coinshot',scrapeE9pay function · javascript · L222-L247 (26 LOC)scraper/run-cny.js
async function scrapeE9pay(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.e9pay.co.kr/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000);
await page.waitForSelector('#CN_CNY', { state: 'attached', timeout: 10000 });
await page.evaluate(() => {
const radio = document.querySelector('#CN_CNY');
radio.checked = true;
radio.dispatchEvent(new Event('change', { bubbles: true }));
radio.dispatchEvent(new Event('click', { bubbles: true }));
});
await page.waitForTimeout(1000);
await page.click('#reverse'); await page.waitForTimeout(500);
await page.waitForSelector('#receive-money', { timeout: 5000 });
await page.click('#receive-money', { clickCount: 3 });
await page.fill('#receive-money', String(AMOUNT));
await page.dispatchEvent('#receive-money', 'blur'); await page.waitForTimeout(3000);
const raw = await page.$eval('#send-money', el => el.value)Source: Repobility analyzer · https://repobility.com
scrapeUtransfer function · javascript · L250-L269 (20 LOC)scraper/run-cny.js
async function scrapeUtransfer(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.utransfer.com', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(2000);
await page.locator('select').nth(1).selectOption('CNY');
await page.waitForTimeout(1000);
await page.click('input[name="toAmount"]', { clickCount: 3 });
await page.fill('input[name="toAmount"]', String(AMOUNT));
await page.dispatchEvent('input[name="toAmount"]', 'change');
await page.waitForTimeout(3000);
const raw = await page.inputValue('input[name="fromAmount"]').catch(() => null);
const sendAmt = extractNumber(raw);
if (!sendAmt) throw new Error('총 송금액 추출 실패');
const feeRaw = await page.locator('.utransfer_fees').textContent().catch(() => null);
const fee = extractNumber(feeRaw) ?? 5000;
return { operator: 'Utransfer', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: sendAmt, service_fee: fscrapeMoin function · javascript · L272-L300 (29 LOC)scraper/run-cny.js
async function scrapeMoin(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.themoin.com/', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(3000);
// Close popup (transparent close div at top-right of modal)
await page.evaluate(() => {
document.querySelector('#portalRoot div[style*="top: 21px"]')?.click();
});
await page.waitForTimeout(1000);
// Click receive currency dropdown
await page.locator('div[color="var(--primary-100)"] div[class*="sc-qZusK"]').click();
await page.waitForTimeout(2000);
// Select China CNY
await page.locator('text=중국').first().click();
await page.waitForTimeout(2000);
// Fill receive amount
await page.click('#sendAmountForeignCurrency', { clickCount: 3 });
await page.fill('#sendAmountForeignCurrency', String(AMOUNT));
await page.press('#sendAmountForeignCurrency', 'Tab');
await page.waitForTimeout(3000);
// Read KRW send amoscrapeDebunk function · javascript · L303-L324 (22 LOC)scraper/run-cny.js
async function scrapeDebunk(browser) {
const page = await browser.newPage();
try {
await page.goto('https://www.debunk.co.kr/', { waitUntil: 'networkidle', timeout: 30000 });
await page.waitForTimeout(2000);
// Default currency is already CNY — enter receive amount
const prevSend = await page.$eval('#sendCurrency', el => el.value).catch(() => '');
await page.click('#receiveCurrency', { clickCount: 3 });
await page.keyboard.type(String(AMOUNT));
await page.dispatchEvent('#receiveCurrency', 'blur');
await page.waitForFunction(
(prev) => { const el = document.querySelector('#sendCurrency'); return el && el.value !== prev && el.value !== ''; },
prevSend, { timeout: 10000 }
).catch(() => null);
const raw = await page.$eval('#sendCurrency', el => el.value).catch(() => null);
const sendAmt = extractNumber(raw);
if (!sendAmt) throw new Error('총 송금액 추출 실패');
const fee = 5000; // hardcoded (shown on page)
return { operator: main function · javascript · L343-L424 (82 LOC)scraper/run-cny.js
async function main() {
const runHour = getRunHour();
console.log(`\n[${new Date().toISOString()}] China CNY 스크래핑 시작 — run_hour: ${runHour}\n`);
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const results = [];
const errors = [];
console.log(` 모든 스크래퍼 병렬 실행 중... (${SCRAPERS.length}개)\n`);
const settled = await Promise.allSettled(
SCRAPERS.map(({ fn, needsBrowser }) => (needsBrowser ? fn(browser) : fn()))
);
for (let i = 0; i < settled.length; i++) {
const { name } = SCRAPERS[i];
const result = settled[i];
if (result.status === 'fulfilled') {
results.push(result.value);
console.log(` ✓ ${name}: 송금액 ${result.value.send_amount_krw?.toLocaleString()}원 수수료 ${result.value.service_fee?.toLocaleString()}원 합계 ${result.value.total_sending_amount?.toLocaleString()}원`);
} else {
console.error(` ✗ ${name} 실패: ${result.reason?.message}`);
errors.push({ nmain function · javascript · L39-L128 (90 LOC)scraper/run-idr.js
async function main() {
const runHour = getRunHour();
console.log(`\n[${new Date().toISOString()}] 스크래핑 시작 — run_hour: ${runHour}\n`);
// ── Playwright 브라우저 실행 ──────────────────────────────────────────
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const results = [];
const errors = [];
// ── 각 스크래퍼 병렬 실행 ─────────────────────────────────────────────
console.log(` 모든 스크래퍼 병렬 실행 중... (${SCRAPERS.length}개)\n`);
const settled = await Promise.allSettled(
SCRAPERS.map(({ fn, needsBrowser }) => (needsBrowser ? fn(browser) : fn()))
);
for (let i = 0; i < settled.length; i++) {
const { name } = SCRAPERS[i];
const result = settled[i];
if (result.status === 'fulfilled') {
results.push(result.value);
console.log(` ✓ ${name}: 총 ${result.value.total_sending_amount?.toLocaleString()}원`);
} else {
console.error(` ✗ ${name} 실패: ${result.reason?.message}`);
scrapeGme function · javascript · L18-L36 (19 LOC)scraper/run-khm.js
async function scrapeGme() {
const body = new URLSearchParams({
method: 'GetExRate', pCurr: 'USD', pCountryName: 'Cambodia',
collCurr: 'KRW', deliveryMethod: '1', cAmt: '', pAmt: String(AMOUNT),
cardOnline: 'false', calBy: 'P',
}).toString();
const res = await fetch('https://online.gmeremit.com/Default.aspx', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body, signal: AbortSignal.timeout(15000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorCode !== '0') throw new Error(`GME API 오류: ${data.msg}`);
const total = extractNumber(data.collAmt);
if (!total) throw new Error('총 송금액 추출 실패');
return { operator: 'GME', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: total, service_fee: 0, total_sending_amount: total };
}scrapeGmoneytrans function · javascript · L39-L55 (17 LOC)scraper/run-khm.js
async function scrapeGmoneytrans() {
const url = 'https://mapi.gmoneytrans.net/exratenew1/ajx_calcRate.asp'
+ `?receive_amount=${AMOUNT}`
+ '&payout_country=Cambodia'
+ '&total_collected=0'
+ '&payment_type=Bank+Account'
+ '¤cyType=USD';
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
const serviceCharge = parseField(text, 'serviceCharge') ?? 3000;
const sendAmount = parseField(text, 'sendAmount');
if (!sendAmount) throw new Error(`파싱 실패: ${text.slice(0, 200)}`);
return { operator: 'GMoneyTrans', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: sendAmount, service_fee: serviceCharge,
total_sending_amount: sendAmount + serviceCharge };
}parseField function · javascript · L56-L59 (4 LOC)scraper/run-khm.js
function parseField(text, field) {
const m = text.match(new RegExp(`${field}--td_clm--([\\d.]+)--td_end--`));
return m ? parseFloat(m[1]) : null;
}Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
scrapeSentbe function · javascript · L62-L86 (25 LOC)scraper/run-khm.js
async function scrapeSentbe(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
locale: 'ko-KR',
});
const page = await context.newPage();
try {
await page.goto('https://www.sentbe.com/ko', { waitUntil: 'networkidle', timeout: 30000 });
await page.click('button.close').catch(() => null); await page.waitForTimeout(300);
await page.click('article.app-download-popup .dim').catch(() => null); await page.waitForTimeout(500);
await page.waitForSelector('.receiveAmountInput .el-input-group__append', { timeout: 10000 });
await page.click('.receiveAmountInput .el-input-group__append'); await page.waitForTimeout(500);
await page.click('.receiveAmountInput .el-select-dropdown__item:has-text("캄보디아 / 달러")');
await page.waitForTimeout(1000);
await page.click('#receiveAmount', { clickCount: 3 });
await page.fill('#receiveAmscrapeHanpass function · javascript · L89-L106 (18 LOC)scraper/run-khm.js
async function scrapeHanpass() {
const res = await fetch('https://app.hanpass.com/app/v1/remittance/get-cost', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ inputAmount: String(AMOUNT), inputCurrencyCode: 'USD',
fromCurrencyCode: 'KRW', toCurrencyCode: 'USD', toCountryCode: 'KH',
memberSeq: '1', lang: 'en' }),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.resultCode !== '0') throw new Error(`Hanpass API 오류: ${data.resultMessage}`);
const total = data.depositAmountIncludingFee;
const fee = data.transferFee ?? 0;
if (!total) throw new Error('총 송금액 추출 실패');
return { operator: 'Hanpass', receiving_country: COUNTRY, receive_amount: AMOUNT,
send_amount_krw: total - fee, service_fee: fee, total_sending_amount: total };
}scrapeSbi function · javascript · L109-L132 (24 LOC)scraper/run-khm.js
async function scrapeSbi(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
locale: 'ko-KR',
});
const page = await context.newPage();
try {
await page.goto('https://www.sbicosmoney.com/', { waitUntil: 'load', timeout: 30000 });
await page.waitForTimeout(2000);
await page.click('button:has-text("Close")').catch(() => null); await page.waitForTimeout(500);
await page.click('.dest-country'); await page.waitForTimeout(500);
await page.click('a[data-currency="USD"]'); await page.waitForTimeout(1500);
await page.click('#targetAmount', { clickCount: 3 });
await page.fill('#targetAmount', String(AMOUNT));
await page.dispatchEvent('#targetAmount', 'input'); await page.waitForTimeout(2000);
const raw = await page.inputValue('#krwAmount');
const sendAmt = extractNumber(raw);
if (!sendAmt) throw new Error('총 송금page 1 / 3next ›