From cbe378d3ad2437239a47fe9f237aae9295539fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=ED=9C=98?= Date: Wed, 24 Jun 2026 16:11:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(fe):=20=EC=A7=80=EB=8F=84=20=EB=B7=B0?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20API=20=EC=97=B0=EB=8F=99=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 2 +- frontend/src/api/properties.js | 17 ++++-- frontend/src/api/properties.test.mjs | 56 +++++++++++++++++++ frontend/src/store/mapStore.js | 20 +++++-- frontend/src/views/MapExplorer.vue | 6 +- .../phase3-frontend-map-viewport-api.md | 16 +++--- phases/map-viewport-zoom/phase3.status.json | 8 +++ 7 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 frontend/src/api/properties.test.mjs create mode 100644 phases/map-viewport-zoom/phase3.status.json diff --git a/frontend/package.json b/frontend/package.json index fb8a706..a31f0ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", - "test": "node --test src/api/chat.test.mjs", + "test": "node --test src/api/*.test.mjs", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/src/api/properties.js b/frontend/src/api/properties.js index 2d7a71c..e9594cd 100644 --- a/frontend/src/api/properties.js +++ b/frontend/src/api/properties.js @@ -1,18 +1,25 @@ -import http from './http' +import http from './http.js' const cleanParams = (params) => Object.fromEntries( Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== '') ) -export const fetchProperties = async (params) => { - const response = await http.get('/api/v1/properties', { +export const fetchProperties = async (params, client = http) => { + const response = await client.get('/api/v1/properties', { params: cleanParams(params) }) return response.data.data } -export const fetchPropertyDetail = async (propertyId) => { - const response = await http.get(`/api/v1/properties/${propertyId}`) +export const fetchMapViewport = async (params, client = http) => { + const response = await client.get('/api/v1/map/viewport', { + params: cleanParams(params) + }) + return response.data.data +} + +export const fetchPropertyDetail = async (propertyId, client = http) => { + const response = await client.get(`/api/v1/properties/${propertyId}`) return response.data.data } diff --git a/frontend/src/api/properties.test.mjs b/frontend/src/api/properties.test.mjs new file mode 100644 index 0000000..54f39f7 --- /dev/null +++ b/frontend/src/api/properties.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' + +import { fetchMapViewport } from './properties.js' + +test('fetchMapViewport calls zoom-aware viewport endpoint with cleaned params', async () => { + let capturedUrl = '' + let capturedConfig = null + const client = { + async get(url, config) { + capturedUrl = url + capturedConfig = config + return { + data: { + data: { + mode: 'PROPERTY_CLUSTER', + items: [], + totalCount: 0 + } + } + } + } + } + + const result = await fetchMapViewport( + { + west: 126.93, + east: 126.94, + south: 37.46, + north: 37.48, + zoom: 14, + transactionType: 'MONTHLY_RENT', + propertyType: '', + minDeposit: null, + maxDeposit: undefined + }, + client + ) + + assert.equal(capturedUrl, '/api/v1/map/viewport') + assert.deepEqual(capturedConfig, { + params: { + west: 126.93, + east: 126.94, + south: 37.46, + north: 37.48, + zoom: 14, + transactionType: 'MONTHLY_RENT' + } + }) + assert.deepEqual(result, { + mode: 'PROPERTY_CLUSTER', + items: [], + totalCount: 0 + }) +}) diff --git a/frontend/src/store/mapStore.js b/frontend/src/store/mapStore.js index eac20d4..c035fa0 100644 --- a/frontend/src/store/mapStore.js +++ b/frontend/src/store/mapStore.js @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import { sendChatMessage } from '../api/chat' -import { fetchProperties, fetchPropertyDetail } from '../api/properties' +import { fetchMapViewport, fetchPropertyDetail } from '../api/properties' const DEFAULT_BOUNDS = { west: 126.76, @@ -25,6 +25,8 @@ export default defineStore('map', { currentRegion: 'seoul', selectedPropertyId: null, selectedProperty: null, + viewportMode: '', + viewportItems: [], properties: [], totalCount: 0, bounds: { ...DEFAULT_BOUNDS }, @@ -126,22 +128,25 @@ export default defineStore('map', { } this.searchKeyword = '' }, - async fetchProperties(bounds = this.bounds) { + async fetchViewport(bounds = this.bounds) { const seq = ++this.requestSeq this.isLoading = true this.error = '' this.setBounds(bounds) try { - const data = await fetchProperties({ + const data = await fetchMapViewport({ ...this.bounds, + zoom: this.zoom, ...this.filters }) if (seq !== this.requestSeq) return - this.properties = data.items || [] - this.totalCount = data.totalCount ?? this.properties.length + this.viewportMode = data.mode || '' + this.viewportItems = data.items || [] + this.properties = this.viewportItems.filter((item) => item.type === 'PROPERTY') + this.totalCount = data.totalCount ?? this.viewportItems.length this.lastFetchedAt = new Date().toISOString() if (this.selectedPropertyId && !this.properties.some((property) => property.id === this.selectedPropertyId)) { @@ -151,6 +156,8 @@ export default defineStore('map', { } catch (error) { if (seq !== this.requestSeq) return this.error = error.response?.data?.message || '매물 데이터를 불러오지 못했습니다.' + this.viewportMode = '' + this.viewportItems = [] this.properties = [] this.totalCount = 0 } finally { @@ -159,6 +166,9 @@ export default defineStore('map', { } } }, + async fetchProperties(bounds = this.bounds) { + await this.fetchViewport(bounds) + }, async selectProperty(id) { const seq = ++this.detailRequestSeq this.selectedPropertyId = id diff --git a/frontend/src/views/MapExplorer.vue b/frontend/src/views/MapExplorer.vue index bcc6407..7f5e811 100644 --- a/frontend/src/views/MapExplorer.vue +++ b/frontend/src/views/MapExplorer.vue @@ -377,7 +377,7 @@ const renderMarkers = () => { const refreshFromMapBounds = async () => { if (!map) { - await store.fetchProperties() + await store.fetchViewport() return } @@ -386,7 +386,7 @@ const refreshFromMapBounds = async () => { longitude: map.getCenter().lng() }) store.setZoom(map.getZoom()) - await store.fetchProperties(getMapBounds()) + await store.fetchViewport(getMapBounds()) } const refreshFromFilters = async () => { @@ -430,7 +430,7 @@ onMounted(async () => { await refreshFromMapBounds() } catch (error) { mapError.value = error.message || '지도 SDK 설정을 확인해주세요.' - await store.fetchProperties() + await store.fetchViewport() } finally { isMapLoading.value = false } diff --git a/phases/map-viewport-zoom/phase3-frontend-map-viewport-api.md b/phases/map-viewport-zoom/phase3-frontend-map-viewport-api.md index d5f1568..d8239e3 100644 --- a/phases/map-viewport-zoom/phase3-frontend-map-viewport-api.md +++ b/phases/map-viewport-zoom/phase3-frontend-map-viewport-api.md @@ -4,15 +4,17 @@ Switch the map frontend from loading all properties with `/api/v1/properties` to loading zoom-aware viewport items with `/api/v1/map/viewport`. ## Files -- `frontend/src/api/*` - Add a map viewport API function using current bounds, zoom, and filters -- `frontend/src/stores/*` - Store viewport mode, viewport items, loading/error state, and selected property state -- `frontend/src/components/map/*` - Wire map idle/zoom/filter changes to the new API call +- `frontend/src/api/properties.js` - Add a map viewport API function using current bounds, zoom, and filters +- `frontend/src/api/properties.test.mjs` - Verify the viewport API endpoint and cleaned query params +- `frontend/src/store/mapStore.js` - Store viewport mode, viewport items, loading/error state, and selected property state +- `frontend/src/views/MapExplorer.vue` - Wire map idle/zoom/filter changes to the new API call +- `frontend/package.json` - Run all API tests through `pnpm test` ## Done When -- [ ] map movement and zoom call `/api/v1/map/viewport` instead of loading every property. -- [ ] requests are debounced or triggered on map idle to avoid excessive network calls. -- [ ] property filters are preserved in viewport requests. -- [ ] API errors show the existing map error UI without breaking the page. +- [x] map movement and zoom call `/api/v1/map/viewport` instead of loading every property. +- [x] requests are debounced or triggered on map idle to avoid excessive network calls. +- [x] property filters are preserved in viewport requests. +- [x] API errors show the existing map error UI without breaking the page. ## Architecture Rules - Frontend calls Spring Boot REST APIs only. diff --git a/phases/map-viewport-zoom/phase3.status.json b/phases/map-viewport-zoom/phase3.status.json new file mode 100644 index 0000000..b33d1f1 --- /dev/null +++ b/phases/map-viewport-zoom/phase3.status.json @@ -0,0 +1,8 @@ +{ + "status": "completed", + "phase": "phase3-frontend-map-viewport-api.md", + "issue_number": 57, + "timestamp": "2026-06-24T16:08:44+09:00", + "detail": "Switched visible map loading to /api/v1/map/viewport, preserved filters and legacy property detail flows, and added frontend API coverage.", + "runner": "codex" +}