← back to kevinlanovwork-coder__dashboard

Function bodies 131 total

All specs Real LLM only Function bodies
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) {
      gmeBaselineM
formatKRW 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-950
scrapeGme_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&currencyType=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'
    + '&currencyType=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: f
scrapeMoin 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 amo
scrapeDebunk 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({ n
main 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'
    + '&currencyType=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('#receiveAm
scrapeHanpass 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 ›