Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/api/properties.js
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 56 additions & 0 deletions frontend/src/api/properties.test.mjs
Original file line number Diff line number Diff line change
@@ -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
})
})
20 changes: 15 additions & 5 deletions frontend/src/store/mapStore.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,6 +25,8 @@ export default defineStore('map', {
currentRegion: 'seoul',
selectedPropertyId: null,
selectedProperty: null,
viewportMode: '',
viewportItems: [],
properties: [],
totalCount: 0,
bounds: { ...DEFAULT_BOUNDS },
Expand Down Expand Up @@ -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)) {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/MapExplorer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ const renderMarkers = () => {

const refreshFromMapBounds = async () => {
if (!map) {
await store.fetchProperties()
await store.fetchViewport()
return
}

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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
}
Expand Down
16 changes: 9 additions & 7 deletions phases/map-viewport-zoom/phase3-frontend-map-viewport-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions phases/map-viewport-zoom/phase3.status.json
Original file line number Diff line number Diff line change
@@ -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"
}