diff --git a/src/pages/listings/components/ToolsTab.jsx b/src/pages/listings/components/ToolsTab.jsx
new file mode 100644
index 0000000..6847fcb
--- /dev/null
+++ b/src/pages/listings/components/ToolsTab.jsx
@@ -0,0 +1,161 @@
+import React, { useState } from 'react';
+import { safetyCheck, budgetCalc } from '../../../api';
+import { tierMeta, formatMoney, formatRatio } from '../listingsUtils';
+
+const DEAL_TYPES = ['전세', '반전세', '매매'];
+
+/* ── 안전마진 체커 ── */
+const SafetyCard = () => {
+ const [form, setForm] = useState({ area: '', deal_type: '전세', amount: '', dong: '' });
+ const [res, setRes] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
+
+ const submit = async (e) => {
+ e.preventDefault();
+ if (!form.area || !form.amount) return;
+ setLoading(true); setError(''); setRes(null);
+ try {
+ const r = await safetyCheck({
+ area: Number(form.area), deal_type: form.deal_type,
+ amount: Number(form.amount), dong: form.dong || undefined,
+ });
+ setRes(r);
+ } catch (e2) { setError(e2?.message ?? String(e2)); }
+ finally { setLoading(false); }
+ };
+
+ const isSale = form.deal_type === '매매';
+ const meta = res ? tierMeta(isSale ? 'valuation' : 'safety', res.tier) : null;
+
+ return (
+
+ 안전마진 체커
+ 면적·거래유형·금액으로 전세가율/호가율 적정성을 즉시 판정합니다.
+
+ {error && {error}
}
+ {res && (
+
+
+ {meta.emoji} {meta.label}
+ {res.ratio != null && 비율 {formatRatio(res.ratio)}}
+ {res.median != null && 중앙값 {formatMoney(res.median)}}
+ {res.sample != null && 표본 {res.sample}건}
+ {res.is_toheo && 토지거래허가}
+
+ {res.disclaimer &&
※ {res.disclaimer}
}
+
+ )}
+
+ );
+};
+
+/* ── 예산 계산기 ── */
+const BudgetCard = () => {
+ const [form, setForm] = useState({
+ equity: '', annual_income: '', is_homeless: true, is_householder: true, is_first_home: false, target_dong: '',
+ });
+ const [res, setRes] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
+
+ const submit = async (e) => {
+ e.preventDefault();
+ if (!form.equity) return;
+ setLoading(true); setError(''); setRes(null);
+ try {
+ const r = await budgetCalc({
+ equity: Number(form.equity),
+ annual_income: form.annual_income ? Number(form.annual_income) : null,
+ is_homeless: form.is_homeless ? 1 : 0,
+ is_householder: form.is_householder ? 1 : 0,
+ is_first_home: form.is_first_home ? 1 : 0,
+ target_dong: form.target_dong || undefined,
+ });
+ setRes(r);
+ } catch (e2) { setError(e2?.message ?? String(e2)); }
+ finally { setLoading(false); }
+ };
+
+ return (
+
+ 예산 계산기
+ 자기자본·소득 기준 전세/매매 한도와 지역 규제를 계산합니다.
+
+ {error && {error}
}
+ {res && (
+
+ {res.jeonse && (
+
+ 전세
+ 대출한도 {formatMoney(res.jeonse.loan_limit)}
+ 최대 보증금 {formatMoney(res.jeonse.max_deposit)}
+ {res.jeonse.notes && {res.jeonse.notes}}
+
+ )}
+ {res.purchase && (
+
+
매매
+ {res.purchase.ltv_pct != null &&
LTV {res.purchase.ltv_pct}%}
+
대출한도 {formatMoney(res.purchase.loan_cap)}
+
최대 매수가 {formatMoney(res.purchase.max_price)}
+ {res.purchase.dsr_note &&
{res.purchase.dsr_note}}
+ {Array.isArray(res.purchase.regulation_flags) && res.purchase.regulation_flags.length > 0 && (
+
{res.purchase.regulation_flags.map((f, i) => {f})}
+ )}
+
+ )}
+ {res.region && (
+
+ 지역
+ {res.region.is_toheo && 토지거래허가}
+ {res.region.is_regulated && 규제지역}
+ {res.region.notes && {res.region.notes}}
+
+ )}
+ {res.disclaimer &&
※ {res.disclaimer}
}
+
+ )}
+
+ );
+};
+
+const ToolsTab = () => (
+
+
+
+
+);
+
+export default ToolsTab;
diff --git a/src/pages/listings/components/ToolsTab.test.jsx b/src/pages/listings/components/ToolsTab.test.jsx
new file mode 100644
index 0000000..285c7b7
--- /dev/null
+++ b/src/pages/listings/components/ToolsTab.test.jsx
@@ -0,0 +1,17 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+
+vi.mock('../../../api', () => ({ safetyCheck: vi.fn(), budgetCalc: vi.fn() }));
+import ToolsTab from './ToolsTab.jsx';
+
+beforeEach(() => vi.clearAllMocks());
+
+describe('ToolsTab', () => {
+ it('안전마진·예산 카드 헤딩과 disclaimer 안내를 렌더', () => {
+ render();
+ expect(screen.getByText('안전마진 체커')).toBeInTheDocument();
+ expect(screen.getByText('예산 계산기')).toBeInTheDocument();
+ // 제출 버튼 존재
+ expect(screen.getAllByRole('button', { name: /판정|계산/ }).length).toBeGreaterThan(0);
+ });
+});