Skip to content

Commit fdb6665

Browse files
authored
Merge pull request #53 from SEMOSAN/feat/#19-tracking-gps
[Feat]#19 트래킹 GPS
2 parents 2b1e613 + c227637 commit fdb6665

36 files changed

Lines changed: 1206 additions & 125 deletions

AGENTS.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# AGENTS.md
2+
3+
## Communication
4+
- Always reply in Korean unless the user explicitly asks for another language.
5+
- Keep answers direct and practical. Avoid repeating the same explanation after the user has already acknowledged it.
6+
- When the user asks "확인해줘", "봐줘", "어떻게 돼", "뭐가 문제야", or similar, treat it as analysis-only unless they explicitly ask for code changes.
7+
- Do not edit files unless the user clearly says something like "수정해줘", "고쳐줘", "반영해줘", "만들어줘", "추가해줘", or "삭제해줘".
8+
- If a fix is likely, explain the cause, affected files, and proposed patch first. Wait for explicit approval before applying it.
9+
- If you accidentally changed code without approval, say exactly which files changed and offer to revert only your own changes.
10+
11+
## User Preferences
12+
- The user wants investigation before implementation.
13+
- The user dislikes unrequested code edits.
14+
- The user prefers concrete answers based on the current codebase, not generic guesses.
15+
- When diagnosing backend/frontend integration issues, clearly separate:
16+
- what the backend currently expects
17+
- what the frontend must send
18+
- what environment/config values must match
19+
- what is only an assumption
20+
- If sensitive values such as secrets, tokens, DB passwords, or JWT secrets appear in screenshots or logs, warn briefly and recommend rotation if they may have been shared.
21+
22+
## Project
23+
- This is a Spring Boot backend project.
24+
- Use Java 21 and Gradle.
25+
- Follow the existing package structure, naming, and style.
26+
- Prefer existing services, repositories, DTOs, and response conventions over introducing new abstractions.
27+
28+
## Commands
29+
- Use `rg` first for searching.
30+
- Run focused tests before broad tests when checking a narrow change.
31+
- Common commands:
32+
- `./gradlew test --tests <fully.qualified.TestClass>`
33+
- `./gradlew test`
34+
- `./gradlew build`
35+
- If full tests fail because local infrastructure such as PostgreSQL or Redis is unavailable, report that clearly and distinguish it from failures caused by the code change.
36+
37+
## Editing Rules
38+
- Never revert or overwrite user changes unless explicitly requested.
39+
- Keep changes narrowly scoped to the requested task.
40+
- Do not commit secrets, environment values, generated local files, or unrelated formatting churn.
41+
- Before editing, state what files will be touched and why.
42+
- After editing, summarize the exact files changed and verification performed.

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ dependencies {
5353
// PostGIS (Hibernate Spatial + JTS)
5454
implementation 'org.hibernate.orm:hibernate-spatial'
5555

56+
// WebSocket (트래킹 GPS 실시간 수집 — STOMP)
57+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
58+
5659
// Flyway
5760
implementation 'org.flywaydb:flyway-core'
5861
implementation 'org.flywaydb:flyway-database-postgresql'
62+
5963
}
6064

6165
jar { enabled = false }

k8s/kustomization.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ resources:
1414
- minio/service.yaml
1515
images:
1616
- name: ghcr.io/semosan/semosan_be
17-
newTag: fb0eb2823ad7e4ee955bd3941443be46e88a22cf
17+
newTag: 2408eb0c0f5fc413f174a8745cfd08f32d7a88ce

k8s/postgres/deployment.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ spec:
1515
spec:
1616
containers:
1717
- name: postgres
18-
image: postgres:16-alpine
18+
image: imresamu/postgis:16-3.4
1919
ports:
2020
- containerPort: 5432
2121
envFrom:
@@ -28,4 +28,4 @@ spec:
2828
volumes:
2929
- name: postgres-data
3030
persistentVolumeClaim:
31-
claimName: postgres-pvc
31+
claimName: postgres-pvc

src/main/java/com/semosan/api/common/config/SecurityConfig.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ public class SecurityConfig {
4949
"/api/auth/token/reissue"
5050
};
5151

52+
/**
53+
* WebSocket(STOMP) 엔드포인트.
54+
* 핸드셰이크는 HTTP JWT 필터로 인증하지 않고 통과시키고,
55+
* STOMP CONNECT 프레임에서 StompAuthChannelInterceptor 가 JWT 를 검증한다.
56+
*/
57+
public static final String[] WEBSOCKET_URIS = {
58+
"/ws/tracking/**"
59+
};
60+
5261
@Bean
5362
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
5463
http
@@ -63,6 +72,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
6372
.requestMatchers(SWAGGER_URIS).permitAll()
6473
.requestMatchers(OAUTH_URIS).permitAll()
6574
.requestMatchers(AUTH_URIS).permitAll()
75+
.requestMatchers(WEBSOCKET_URIS).permitAll()
6676
.anyRequest().authenticated()
6777
)
6878
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
@@ -78,7 +88,17 @@ public CorsConfigurationSource corsConfigurationSource() {
7888
config.setAllowedHeaders(List.of("*"));
7989
config.setAllowCredentials(true);
8090

91+
// WebSocket(STOMP) 핸드셰이크는 다양한 origin(모바일/로컬 테스트 페이지 등)에서 들어옴.
92+
// /ws/** 만 별도 정책으로 풀어준다.
93+
// TODO: production 에서는 모바일 앱 origin 만 명시적으로 허용하도록 좁힐 것.
94+
CorsConfiguration wsConfig = new CorsConfiguration();
95+
wsConfig.setAllowedOriginPatterns(List.of("*"));
96+
wsConfig.setAllowedMethods(List.of("GET"));
97+
wsConfig.setAllowedHeaders(List.of("*"));
98+
wsConfig.setAllowCredentials(true);
99+
81100
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
101+
source.registerCorsConfiguration("/ws/**", wsConfig);
82102
source.registerCorsConfiguration("/**", config);
83103
return source;
84104
}

src/main/java/com/semosan/api/common/config/WebClientConfig.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@
77
@Configuration
88
public class WebClientConfig {
99

10-
@Bean
11-
public WebClient kakaoAuthWebClient() {
12-
return WebClient.builder()
13-
.baseUrl("https://kauth.kakao.com")
14-
.build();
15-
}
16-
1710
@Bean
1811
public WebClient kakaoApiWebClient() {
1912
return WebClient.builder()

src/main/java/com/semosan/api/common/exception/GeneralExceptionAdvice.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,20 @@ public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(
5555
}
5656

5757
// Spring 6.1+ HandlerMethodValidationException (컨트롤러 메서드 파라미터 검증 실패 표준 경로)
58-
@ExceptionHandler(HandlerMethodValidationException.class)
59-
public ResponseEntity<ApiResponse<Void>> handleHandlerMethodValidation(
60-
HandlerMethodValidationException e
58+
@Override
59+
protected ResponseEntity<Object> handleHandlerMethodValidationException(
60+
HandlerMethodValidationException ex,
61+
HttpHeaders headers,
62+
HttpStatusCode status,
63+
WebRequest request
6164
) {
62-
String message = e.getAllErrors().stream()
65+
String message = ex.getAllErrors().stream()
6366
.findFirst()
6467
.map(error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : ErrorStatus.BAD_REQUEST.getMessage())
6568
.orElse(ErrorStatus.BAD_REQUEST.getMessage());
6669
log.warn("[*] HandlerMethodValidationException : {}", message);
67-
return ApiResponse.error(ErrorStatus.BAD_REQUEST, message);
70+
ApiResponse<Void> body = createApiResponse(ErrorStatus.BAD_REQUEST, message);
71+
return handleExceptionInternal(ex, body, headers, status, request);
6872
}
6973

7074
// null 참조로 발생한 서버 오류를 500 에러로 응답

src/main/java/com/semosan/api/domain/oauth/client/OAuthKakaoClient.java

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
import com.semosan.api.common.exception.GeneralException;
44
import com.semosan.api.domain.oauth.properties.KakaoProperties;
55
import com.semosan.api.common.status.ErrorStatus;
6-
import com.semosan.api.domain.oauth.dto.KakaoTokenResponse;
76
import com.semosan.api.domain.oauth.dto.KakaoUserInfoResponse;
87
import lombok.RequiredArgsConstructor;
98
import lombok.extern.slf4j.Slf4j;
109
import org.springframework.http.HttpStatusCode;
1110
import org.springframework.stereotype.Component;
12-
import org.springframework.util.LinkedMultiValueMap;
13-
import org.springframework.util.MultiValueMap;
14-
import org.springframework.util.StringUtils;
1511
import org.springframework.web.reactive.function.BodyInserters;
1612
import org.springframework.web.reactive.function.client.WebClient;
1713
import org.springframework.web.reactive.function.client.WebClientResponseException;
@@ -22,44 +18,9 @@
2218
@RequiredArgsConstructor
2319
public class OAuthKakaoClient {
2420

25-
private final WebClient kakaoAuthWebClient;
2621
private final WebClient kakaoApiWebClient;
2722
private final KakaoProperties kakaoProperties;
2823

29-
// 인가 코드 -> 카카오 액세스 토큰
30-
public KakaoTokenResponse getKakaoToken(String code) {
31-
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
32-
form.add("grant_type", "authorization_code");
33-
form.add("client_id", kakaoProperties.clientId());
34-
form.add("redirect_uri", kakaoProperties.redirectUri());
35-
form.add("code", code);
36-
if (StringUtils.hasText(kakaoProperties.clientSecret())) {
37-
form.add("client_secret", kakaoProperties.clientSecret());
38-
}
39-
40-
try {
41-
KakaoTokenResponse response = kakaoAuthWebClient.post()
42-
.uri("/oauth/token")
43-
.body(BodyInserters.fromFormData(form))
44-
.retrieve()
45-
.onStatus(
46-
HttpStatusCode::isError,
47-
r -> r.bodyToMono(String.class)
48-
.flatMap(body -> handleError(r.statusCode(), body, "토큰 발급"))
49-
)
50-
.bodyToMono(KakaoTokenResponse.class)
51-
.block();
52-
53-
if (response == null || !StringUtils.hasText(response.accessToken())) {
54-
throw new GeneralException(ErrorStatus.KAKAO_TOKEN_REQUEST_FAILED);
55-
}
56-
return response;
57-
} catch (WebClientResponseException e) {
58-
log.error("[*] 카카오 토큰 발급 실패 status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString());
59-
throw new GeneralException(ErrorStatus.KAKAO_TOKEN_REQUEST_FAILED);
60-
}
61-
}
62-
6324
// 카카오 액세스 토큰 -> 사용자 정보
6425
public KakaoUserInfoResponse getKakaoUserInfo(String kakaoAccessToken) {
6526
try {

src/main/java/com/semosan/api/domain/oauth/controller/docs/OAuthControllerDocs.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public interface OAuthControllerDocs {
1818

1919
@Operation(
2020
summary = "카카오 소셜 로그인",
21-
description = "프론트엔드에서 전달받은 카카오 인가 코드(code)로 로그인 또는 회원가입을 처리하고 서비스 JWT를 발급합니다."
21+
description = "프론트엔드에서 전달받은 카카오 액세스 토큰(accessToken)으로 로그인 또는 회원가입을 처리하고 서비스 JWT를 발급합니다."
2222
)
2323
@ApiResponses({
2424
@io.swagger.v3.oas.annotations.responses.ApiResponse(
@@ -28,7 +28,7 @@ public interface OAuthControllerDocs {
2828
),
2929
@io.swagger.v3.oas.annotations.responses.ApiResponse(
3030
responseCode = "400",
31-
description = "잘못된 요청 (인가 코드 또는 디바이스 타입 누락)",
31+
description = "잘못된 요청 (카카오 액세스 토큰 또는 디바이스 타입 누락)",
3232
content = @Content(schema = @Schema(implementation = ApiResponse.class))
3333
),
3434
@io.swagger.v3.oas.annotations.responses.ApiResponse(

src/main/java/com/semosan/api/domain/oauth/dto/KakaoTokenResponse.java

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)