diff --git a/src/pages/listings/components/CriteriaTab.jsx b/src/pages/listings/components/CriteriaTab.jsx new file mode 100644 index 0000000..5ea32a6 --- /dev/null +++ b/src/pages/listings/components/CriteriaTab.jsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState } from 'react'; +import { getListingsCriteria, putListingsCriteria } from '../../../api'; + +const DEAL_TYPES = ['전세', '반전세', '매매']; +const HOUSE_TYPES = ['아파트', '오피스텔', '빌라']; +const SAFETY_TIERS = ['', '안전', '주의', '위험']; + +const toList = (s) => String(s ?? '').split(',').map((x) => x.trim()).filter(Boolean); +const numOrNull = (v) => (v === '' || v == null ? null : Number(v)); + +const CriteriaTab = () => { + const [c, setC] = useState(null); // 편집 중 폼 상태 (dongs/house_types는 문자열/배열) + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [msg, setMsg] = useState(''); + + useEffect(() => { + (async () => { + setLoading(true); + try { + const data = await getListingsCriteria(); + setC({ ...data, dongs: (data.dongs ?? []).join(', ') }); + } catch (e) { + setMsg('불러오기 실패: ' + (e?.message ?? e)); + } finally { setLoading(false); } + })(); + }, []); + + const set = (k, v) => setC((prev) => ({ ...prev, [k]: v })); + const toggleArr = (k, val) => setC((prev) => { + const arr = Array.isArray(prev[k]) ? prev[k] : []; + return { ...prev, [k]: arr.includes(val) ? arr.filter((x) => x !== val) : [...arr, val] }; + }); + + const save = async () => { + setSaving(true); setMsg(''); + try { + const payload = { + dongs: toList(c.dongs), + deal_types: c.deal_types ?? [], + house_types: c.house_types ?? [], + max_deposit: numOrNull(c.max_deposit), + max_sale_price: numOrNull(c.max_sale_price), + min_area: numOrNull(c.min_area), + equity: numOrNull(c.equity), + annual_income: numOrNull(c.annual_income), + min_safety_tier: c.min_safety_tier || null, + notify_enabled: c.notify_enabled ? 1 : 0, + is_homeless: c.is_homeless ? 1 : 0, + is_householder: c.is_householder ? 1 : 0, + is_first_home: c.is_first_home ? 1 : 0, + }; + const updated = await putListingsCriteria(payload); + setC({ ...updated, dongs: (updated.dongs ?? []).join(', ') }); + setMsg('저장 완료'); + setTimeout(() => setMsg(''), 2000); + } catch (e) { + setMsg('저장 실패: ' + (e?.message ?? e)); + } finally { setSaving(false); } + }; + + if (loading || !c) return

불러오는 중…

; + + return ( +
+
+
+

매물 매칭 조건

+

자기자본·연소득을 넣어야 매매 예산상한·전세 max_deposit이 매칭에 반영됩니다.

+
+
+ {msg && {msg}} + +
+
+ +
+ + +
+ 거래 유형 +
+ {DEAL_TYPES.map((d) => ( + + ))} +
+
+ +
+ 주택 유형 +
+ {HOUSE_TYPES.map((h) => ( + + ))} +
+
+ + + + + + + + +
+ + + + +
+
+
+ ); +}; + +export default CriteriaTab; diff --git a/src/pages/listings/components/CriteriaTab.test.jsx b/src/pages/listings/components/CriteriaTab.test.jsx new file mode 100644 index 0000000..1e00c45 --- /dev/null +++ b/src/pages/listings/components/CriteriaTab.test.jsx @@ -0,0 +1,24 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('../../../api', () => ({ getListingsCriteria: vi.fn(), putListingsCriteria: vi.fn() })); +import { getListingsCriteria } from '../../../api'; +import CriteriaTab from './CriteriaTab.jsx'; + +beforeEach(() => { + vi.clearAllMocks(); + getListingsCriteria.mockResolvedValue({ + id: 1, dongs: ['신대방동', '상도동'], deal_types: ['전세', '매매'], max_deposit: 32000, + max_sale_price: null, min_area: 40, house_types: ['아파트'], min_safety_tier: null, + notify_enabled: 1, equity: null, annual_income: null, is_homeless: 1, is_householder: 0, is_first_home: 0, + }); +}); + +describe('CriteriaTab', () => { + it('criteria를 로드해 자기자본·연소득 입력과 동 목록을 렌더', async () => { + render(); + await waitFor(() => expect(screen.getByLabelText(/자기자본/)).toBeInTheDocument()); + expect(screen.getByLabelText(/연소득/)).toBeInTheDocument(); + expect(screen.getByDisplayValue('신대방동, 상도동')).toBeInTheDocument(); + }); +});