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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Seed scripts / local data generation
# Naver Maps geocoding uses the server-side client id and secret.
NAVER_MAPS_CLIENT_ID=
NAVER_MAPS_CLIENT_SECRET=

# MOLIT transaction APIs. Used by seed/batch scripts, not by the frontend.
MOLIT_SERVICE_KEY=
SUPABASE_DB_URL=

# Supabase admin key for seed/admin scripts only. Never expose this to the frontend.
SUPABASE_SERVICE_ROLE_KEY=
10 changes: 10 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Spring Boot datasource
# Use these values when running the backend locally.
# Example:
# SUPABASE_DB_URL=jdbc:postgresql://aws-0-ap-northeast-2.pooler.supabase.com:6543/postgres?sslmode=require
SUPABASE_DB_PASSWORD=
SUPABASE_DB_URL=
SUPABASE_DB_USERNAME=

# Comma-separated frontend origins allowed to call /api/** from browsers.
APP_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
32 changes: 32 additions & 0 deletions backend/src/main/java/com/ssafy/salmanhae/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.ssafy.salmanhae.config;

import java.util.Arrays;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final String[] allowedOrigins;

public WebConfig(
@Value("${app.cors.allowed-origins:http://localhost:5173,http://127.0.0.1:5173}") String allowedOrigins
) {
this.allowedOrigins = Arrays.stream(allowedOrigins.split(","))
.map(String::trim)
.filter(origin -> !origin.isBlank())
.toArray(String[]::new);
}

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(allowedOrigins)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
Expand Down Expand Up @@ -51,4 +54,21 @@ void detailEndpointReturnsNotFoundForMissingProperty() {
assertThat(notFoundResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(notFoundResponse.getBody()).containsEntry("code", "PROPERTY_NOT_FOUND");
}

@Test
void propertyApiAllowsLocalFrontendOrigin() {
HttpHeaders headers = new HttpHeaders();
headers.setOrigin("http://127.0.0.1:5173");
headers.setAccessControlRequestMethod(HttpMethod.GET);

ResponseEntity<Void> preflightResponse = restTemplate.exchange(
"/api/v1/properties?west=126.93&east=126.94&south=37.46&north=37.48",
HttpMethod.OPTIONS,
new HttpEntity<>(headers),
Void.class
);

assertThat(preflightResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(preflightResponse.getHeaders().getAccessControlAllowOrigin()).isEqualTo("http://127.0.0.1:5173");
}
}
4 changes: 4 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Browser-safe Naver Maps SDK key. Do not put the secret here.
VITE_NAVER_MAP_CLIENT_ID=

# Spring Boot API base URL. Keep this pointing at the backend when verifying map markers.
VITE_API_BASE_URL=http://localhost:8080
3 changes: 3 additions & 0 deletions frontend/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
packages:
- .

allowBuilds:
esbuild: true
vue-demi: true
13 changes: 13 additions & 0 deletions frontend/src/api/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from 'axios'

const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'

const http = axios.create({
baseURL,
timeout: 10000,
headers: {
Accept: 'application/json'
}
})

export default http
18 changes: 18 additions & 0 deletions frontend/src/api/properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import http from './http'

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', {
params: cleanParams(params)
})
return response.data.data
}

export const fetchPropertyDetail = async (propertyId) => {
const response = await http.get(`/api/v1/properties/${propertyId}`)
return response.data.data
}
25 changes: 25 additions & 0 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ body {
.form-input {
@apply w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs font-bold focus:outline-none focus:ring-2 focus:ring-brand;
}

.filter-field {
@apply flex min-w-0 flex-col gap-1;
}

.filter-field span {
@apply text-[10px] font-black text-slate-500;
}

.filter-field input,
.filter-field select {
@apply h-10 w-full rounded-xl border border-slate-200 bg-slate-50 px-2 text-xs font-bold text-slate-800 outline-none transition focus:border-brand focus:ring-2 focus:ring-brand/20;
}

.detail-stat {
@apply min-w-0 rounded-2xl border border-slate-200 bg-slate-50 p-3;
}

.detail-stat span {
@apply block text-[10px] font-black text-slate-500;
}

.detail-stat b {
@apply mt-1 block truncate text-sm font-black text-slate-900;
}
}

.glow-safe-path { filter: drop-shadow(0 0 6px #10b981); }
Expand Down
Loading