diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 446c193..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 5cd9a10..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
deleted file mode 100644
index 2b63946..0000000
--- a/.idea/uiDesigner.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
- -
-
-
- -
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 6c082ec..4cdf2d3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,37 +26,64 @@ repositories {
dependencies {
// WebClient (비동기 HTTP 통신)
implementation 'org.springframework.boot:spring-boot-starter-webflux'
+
// Web (동기 HTTP 서버 및 Controller 등)
implementation 'org.springframework.boot:spring-boot-starter-web'
+
+ // JSON 파싱 및 생성 라이브러리 추가
+ implementation 'org.json:json:20231013'
+
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
+
+ // OAuth2 Client (소셜 로그인용)
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+
+ // JWT
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.2'
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.2'
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.2'
+
+ // Thymeleaf + Spring Security 연동
+ implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
+
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
+
// DevTools
developmentOnly 'org.springframework.boot:spring-boot-devtools'
+
// DB Driver
runtimeOnly 'org.postgresql:postgresql'
+
// Swagger (API 문서 자동화)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
+
// PostGIS (공간 처리 라이브러리)
implementation 'org.locationtech.jts:jts-core:1.18.2'
- implementation 'org.hibernate:hibernate-spatial'
+ implementation 'org.hibernate:hibernate-spatial:6.6.18.Final'
+
// AI - Gemini API 연동
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.9'
+
+ // dotenv (환경변수 ..env 자동 로드)
+ implementation 'io.github.cdimascio:dotenv-java:3.0.0'
+
// 테스트
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
- testImplementation 'org.mockito:mockito-inline:5.2.0' // static, final mocking
+ testImplementation 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
jvmArgs += [
- "-javaagent:${classpath.find { it.name.contains('byte-buddy-agent') || it.name.contains('mockito-inline') }.absolutePath}"
+ "-javaagent:${classpath.find { it.name.contains('byte-buddy-agent') || it.name.contains('mockito-inline') }.absolutePath}"
]
-}
+}
\ No newline at end of file
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/SignupApplication.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/SignupApplication.java
deleted file mode 100644
index 239a2a3..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/SignupApplication.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.signup;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class SignupApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(SignupApplication.class, args);
- }
-
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/controller/UserController.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/controller/UserController.java
deleted file mode 100644
index f43bde3..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/controller/UserController.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.example.signup.controller;
-
-import com.example.signup.dto.*;
-import com.example.signup.entity.User;
-import com.example.signup.service.UserService;
-import com.example.signup.util.JwtUtil;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.Map;
-
-@RestController
-@RequestMapping("/api/users")
-public class UserController {
- @Autowired
- private UserService userService;
-
- @PostMapping("/register")
- public CommonResponse> register(@RequestBody UserRegisterRequest request) {
- try {
- User user = userService.register(request);
- return new CommonResponse<>(false, user);
- } catch (IllegalStateException e) {
- return new CommonResponse<>(true, e.getMessage());
- } catch (Exception e) {
- return new CommonResponse<>(true, "회원가입 처리 중 오류가 발생했습니다: " + e.getMessage());
- }
- }
-
- @PostMapping("/login")
- public CommonResponse> login(@RequestBody LoginRequest request) {
- try {
- User user = userService.login(request.getEmail(), request.getPassword());
-
- if (user == null) {
- return new CommonResponse<>(true, "이메일 또는 비밀번호가 일치하지 않습니다.");
- }
-
- String token = JwtUtil.generateToken(user.getUsername());
- LoginResponse response = new LoginResponse(user, token);
-
- return new CommonResponse<>(false, response);
-
- } catch (Exception e) {
- return new CommonResponse<>(true, "로그인 처리 중 오류가 발생했습니다: " + e.getMessage());
- }
- }
- @PutMapping("/update")
- public CommonResponse> updateUserInfo(
- @RequestHeader("Authorization") String authHeader,
- @RequestBody Map updates) {
- try {
- String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader;
- String username = JwtUtil.extractUsername(token);
-
- User updatedUser = userService.updateUserInfoByMap(username, updates);
-
- return new CommonResponse<>(false, updatedUser);
- } catch (IllegalStateException e) {
- return new CommonResponse<>(true, e.getMessage());
- } catch (Exception e) {
- return new CommonResponse<>(true, "유저 정보 수정 중 오류가 발생했습니다: " + e.getMessage());
- }
- }
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/CommonResponse.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/CommonResponse.java
deleted file mode 100644
index dccc4b0..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/CommonResponse.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.signup.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class CommonResponse {
- private boolean error;
- private T data;
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginRequest.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginRequest.java
deleted file mode 100644
index 4b3c43b..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginRequest.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.signup.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class LoginRequest {
- private String email;
- private String password;
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginResponse.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginResponse.java
deleted file mode 100644
index d5e5dad..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/LoginResponse.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.example.signup.dto;
-
-import com.example.signup.entity.User;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class LoginResponse {
- private User user;
- private String jwt;
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/UserRegisterRequest.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/UserRegisterRequest.java
deleted file mode 100644
index 1fcf8d0..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/dto/UserRegisterRequest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.example.signup.dto;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-@Data
-@NoArgsConstructor
-@AllArgsConstructor // ✅ 모든 필드 생성자 자동 생성
-public class UserRegisterRequest {
- private String username;
- private String email;
- private String password;
- private String intro;
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/entity/User.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/entity/User.java
deleted file mode 100644
index 031cda6..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/entity/User.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.example.signup.entity;
-
-import jakarta.persistence.*;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import org.hibernate.annotations.CreationTimestamp;
-import com.fasterxml.jackson.annotation.JsonFormat;
-
-import java.time.LocalDate;
-
-@Entity
-@Table(name = "users")
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class User {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long uid;
-
- @Column(unique = true)
- private String username;
-
- private String email;
- private String password;
-
- @Column(length = 500)
- private String intro;
-
- @CreationTimestamp
- @Column(updatable = false)
- @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") // JSON 응답 형식 지정
- private LocalDate createdAt;
-
- public User(String username, String email, String password, String intro) {
- this.username = username;
- this.email = email;
- this.password = password;
- this.intro = intro;
- }
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/repository/UserRepository.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/repository/UserRepository.java
deleted file mode 100644
index 0efb334..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/repository/UserRepository.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.signup.repository;
-
-import com.example.signup.entity.User;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.Optional;
-
-public interface UserRepository extends JpaRepository {
- Optional findByEmailAndPassword(String email, String password);
- Optional findByUsername(String username);
- boolean existsByUsername(String username);
- boolean existsByEmailAndPassword(String email, String password);
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/service/UserService.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/service/UserService.java
deleted file mode 100644
index 21b4b9d..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/domain/service/UserService.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.example.signup.service;
-
-import com.example.signup.dto.UserRegisterRequest;
-import com.example.signup.entity.User;
-import com.example.signup.repository.UserRepository;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.Map;
-
-@Service
-public class UserService {
- private final UserRepository repository;
-
- public UserService(UserRepository repository) {
- this.repository = repository;
- }
-
- @Transactional
- public User register(UserRegisterRequest request) {
- if (repository.existsByUsername(request.getUsername())) {
- throw new IllegalStateException("이미 존재하는 사용자명입니다.");
- }
-
- if (repository.existsByEmailAndPassword(request.getEmail(), request.getPassword())) {
- throw new IllegalStateException("이미 존재하는 이메일과 비밀번호 조합입니다.");
- }
-
- // 생성일자(createdAt)는 자동 생성되므로 포함하지 않음
- User user = new User(
- request.getUsername(),
- request.getEmail(),
- request.getPassword(),
- request.getIntro()
- );
-
- return repository.save(user);
- }
-
- @Transactional(readOnly = true)
- public User login(String email, String password) {
- return repository.findByEmailAndPassword(email, password)
- .orElse(null);
- }
- @Transactional
- public User updateUserInfoByMap(String username, Map updates) {
- User user = repository.findByUsername(username)
- .orElseThrow(() -> new IllegalStateException("사용자를 찾을 수 없습니다."));
-
- for (Map.Entry entry : updates.entrySet()) {
- String key = entry.getKey();
- Object value = entry.getValue();
-
- switch (key) {
- case "username":
- throw new IllegalArgumentException("username은 수정할 수 없습니다.");
- case "email":
- user.setEmail((String) value);
- break;
- case "password":
- user.setPassword((String) value);
- break;
- case "intro":
- user.setIntro((String) value);
- break;
- default:
- throw new IllegalArgumentException("수정할 수 없는 필드: " + key);
- }
- }
-
- return repository.save(user);
- }
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/config/WebConfig.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/config/WebConfig.java
deleted file mode 100644
index 18709d6..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/config/WebConfig.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.example.signup.config;
-
-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 {
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping("/api/**") // API 경로에만 CORS 설정
- .allowedOrigins("http://localhost:5173") // React 개발 서버
- .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용 메서드
- .allowedHeaders("*") // 모든 요청 헤더 허용
- .allowCredentials(true) // 인증 정보 허용 (쿠키, 세션 등)
- .maxAge(3600); // 1시간 동안 preflight 요청 결과 캐시
- }
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/util/JwtUtil.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/util/JwtUtil.java
deleted file mode 100644
index 7e85db4..0000000
--- a/src/main/java/HeyDoctor/HeyDoctor_Backend/User/global/util/JwtUtil.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.signup.util;
-
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.security.Keys;
-
-import java.security.Key;
-import java.util.Date;
-
-public class JwtUtil {
- private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
- private static final long EXPIRATION_TIME = 86400000L; // 1일
-
- public static String generateToken(String username) {
- return Jwts.builder()
- .setSubject(username)
- .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
- .signWith(key)
- .compact();
- }
-
- public static String extractUsername(String token) {
- return Jwts.parserBuilder()
- .setSigningKey(key)
- .build()
- .parseClaimsJws(token)
- .getBody()
- .getSubject();
- }
-}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/controller/DisasterAlertController.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/controller/DisasterAlertController.java
new file mode 100644
index 0000000..f86f2d3
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/controller/DisasterAlertController.java
@@ -0,0 +1,32 @@
+package HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.controller;
+
+import HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.dto.DisasterAlertResponseDto;
+import HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.service.DisasterAlertService;
+import HeyDoctor.HeyDoctor_Backend.domain.location.dto.LocationRequestDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/disaster-alert")
+public class DisasterAlertController {
+
+ private final DisasterAlertService disasterAlertService;
+
+ @GetMapping
+ @ResponseStatus(HttpStatus.OK)
+ public Mono> getAlerts(
+ @RequestParam("lat") double latitude,
+ @RequestParam("lon") double longitude) {
+
+ LocationRequestDto request = new LocationRequestDto(latitude, longitude);
+ return disasterAlertService.getAlertsByCoords(request);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/dto/DisasterAlertResponseDto.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/dto/DisasterAlertResponseDto.java
new file mode 100644
index 0000000..a138127
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/dto/DisasterAlertResponseDto.java
@@ -0,0 +1,24 @@
+package HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class DisasterAlertResponseDto {
+ private String title; // 재난문자 제목
+ private String message; // 재난문자 내용
+ private String location; // 재난문자 발생 지역
+ private String sendTime; // 발송 시간
+
+ public static DisasterAlertResponseDto fromApiData(
+ String title,
+ String message,
+ String location,
+ String sendTime
+ ) {
+ return new DisasterAlertResponseDto(title, message, location, sendTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/service/DisasterAlertService.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/service/DisasterAlertService.java
new file mode 100644
index 0000000..05ca6cc
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/DisasterAlert/service/DisasterAlertService.java
@@ -0,0 +1,121 @@
+package HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.service;
+
+import HeyDoctor.HeyDoctor_Backend.domain.DisasterAlert.dto.DisasterAlertResponseDto;
+import HeyDoctor.HeyDoctor_Backend.domain.location.dto.LocationRequestDto;
+import HeyDoctor.HeyDoctor_Backend.domain.location.dto.LocationResponseDto;
+import HeyDoctor.HeyDoctor_Backend.domain.location.service.LocationService;
+import HeyDoctor.HeyDoctor_Backend.domain.location.util.RegionMatcher;
+import HeyDoctor.HeyDoctor_Backend.global.exception.ErrorCode;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class DisasterAlertService {
+
+ private static final Logger logger = LoggerFactory.getLogger(DisasterAlertService.class);
+
+ private final LocationService locationService;
+ private final WebClient webClient;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Value("${api.alert.key}")
+ private String alertApiKey;
+
+ public DisasterAlertService(LocationService locationService, WebClient.Builder webClientBuilder) {
+ this.locationService = locationService;
+ this.webClient = webClientBuilder.baseUrl("https://www.safetydata.go.kr/V2/api").build();
+ }
+
+ public Mono> getAlertsByCoords(LocationRequestDto request) {
+ return locationService.getAdministrativeRegion(request)
+ .flatMap(userLocation -> {
+ if (userLocation == null) {
+ return Mono.error(new RuntimeException(ErrorCode.BAD_REQUEST.getMessage()));
+ }
+ return fetchDisasterAlertsFromApi(userLocation)
+ .map(alerts -> {
+ // 1. 필터링된 재난문자 리스트 생성
+ List matchedAlerts = new ArrayList<>();
+ for (DisasterAlertResponseDto alert : alerts) {
+ if (RegionMatcher.matchRegion(alert.getLocation(), userLocation)) {
+ matchedAlerts.add(alert);
+ }
+ }
+
+ // 2. 최종 결과를 담을 새로운 리스트 생성
+ List finalResponse = new ArrayList<>();
+
+ // 3. 사용자 위치 정보로 가상의 DTO를 생성
+ String userLocationString = (userLocation.getSido() + " " + userLocation.getSigungu() + " " + userLocation.getEupmyeondong()).trim();
+ DisasterAlertResponseDto locationInfo = new DisasterAlertResponseDto(
+ "현재 위치",
+ "요청하신 좌표의 행정구역 정보입니다.",
+ userLocationString,
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"))
+ );
+
+ // 4. 최종 리스트의 맨 앞에 위치 정보를 추가
+ finalResponse.add(locationInfo);
+
+ // 5. 그 뒤에 필터링된 실제 재난문자들을 추가
+ finalResponse.addAll(matchedAlerts);
+
+ return finalResponse;
+ });
+ })
+ .switchIfEmpty(Mono.error(new RuntimeException(ErrorCode.BAD_REQUEST.getMessage())));
+ }
+
+ private Mono> fetchDisasterAlertsFromApi(LocationResponseDto location) {
+ String rgnNm = URLEncoder.encode(location.getSido() + " " + location.getSigungu(), StandardCharsets.UTF_8);
+ String startDate = LocalDate.now().minusDays(7).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+
+ return webClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/DSSP-IF-00247")
+ .queryParam("serviceKey", alertApiKey)
+ .queryParam("pageNo", "1")
+ .queryParam("numOfRows", "100")
+ .queryParam("returnType", "json")
+ .queryParam("crtDt", startDate)
+ .queryParam("rgnNm", rgnNm)
+ .build())
+ .retrieve()
+ .bodyToMono(String.class)
+ .handle((rawJson, sink) -> {
+ List alerts = new ArrayList<>();
+ try {
+ JsonNode root = objectMapper.readTree(rawJson);
+ JsonNode items = root.path("body");
+
+ if (items.isArray()) {
+ for (JsonNode item : items) {
+ String title = item.path("EMRG_STEP_NM").asText();
+ String message = item.path("MSG_CN").asText();
+ String loc = item.path("RCPTN_RGN_NM").asText();
+ String sendTime = item.path("CRT_DT").asText();
+ alerts.add(DisasterAlertResponseDto.fromApiData(title, message, loc, sendTime));
+ }
+ }
+ sink.next(alerts);
+ } catch (Exception e) {
+ logger.error("Failed to parse disaster alert JSON", e);
+ sink.error(new RuntimeException(ErrorCode.INTERNAL_SERVER_ERROR.getMessage()));
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Controller/SocialLoginController.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Controller/SocialLoginController.java
new file mode 100644
index 0000000..591b7b6
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Controller/SocialLoginController.java
@@ -0,0 +1,26 @@
+package HeyDoctor.HeyDoctor_Backend.domain.User.Controller;
+
+import HeyDoctor.HeyDoctor_Backend.domain.User.Dto.SocialLoginRequest;
+import HeyDoctor.HeyDoctor_Backend.domain.User.Service.SocialLoginService;
+import HeyDoctor.HeyDoctor_Backend.global.exception.responce.ApiResponse;
+import HeyDoctor.HeyDoctor_Backend.global.exception.SuccessCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+public class SocialLoginController {
+
+ private final SocialLoginService socialLoginService;
+
+ @PostMapping("/social-login")
+ public ResponseEntity>> socialLogin(@RequestBody SocialLoginRequest request) {
+ Map result = socialLoginService.processSocialLogin(request.getProvider(), request.getToken());
+ return ApiResponse.onSuccess(SuccessCode.LOGIN_SUCCESS, result);
+
+ }
+}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Dto/SocialLoginRequest.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Dto/SocialLoginRequest.java
new file mode 100644
index 0000000..7d0fd9f
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Dto/SocialLoginRequest.java
@@ -0,0 +1,12 @@
+package HeyDoctor.HeyDoctor_Backend.domain.User.Dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+// DTO 클래스
+@Getter
+@Setter
+public class SocialLoginRequest {
+ private String provider; // "google", "kakao", "naver"
+ private String token; // SDK에서 받은 토큰 (idToken 또는 accessToken)
+}
diff --git a/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Service/SocialLoginService.java b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Service/SocialLoginService.java
new file mode 100644
index 0000000..70ce501
--- /dev/null
+++ b/src/main/java/HeyDoctor/HeyDoctor_Backend/domain/User/Service/SocialLoginService.java
@@ -0,0 +1,113 @@
+package HeyDoctor.HeyDoctor_Backend.domain.User.Service;
+
+import HeyDoctor.HeyDoctor_Backend.domain.User.oauth.UserInfo.*;
+import HeyDoctor.HeyDoctor_Backend.domain.User.entity.User;
+import HeyDoctor.HeyDoctor_Backend.domain.User.repository.UserRepository;
+import HeyDoctor.HeyDoctor_Backend.global.exception.CustomException;
+import HeyDoctor.HeyDoctor_Backend.global.exception.ErrorCode;
+import HeyDoctor.HeyDoctor_Backend.global.security.jwt.RefreshToken.entity.RefreshToken;
+import HeyDoctor.HeyDoctor_Backend.global.security.jwt.RefreshToken.repository.RefreshTokenRepository;
+import HeyDoctor.HeyDoctor_Backend.global.security.jwt.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class SocialLoginService {
+
+ private final JwtUtil jwtUtil;
+ private final UserRepository userRepository;
+ private final RefreshTokenRepository refreshTokenRepository;
+
+ @Value("${jwt.access-token.expiration-time}")
+ private long ACCESS_TOKEN_EXPIRATION_TIME;
+
+ @Value("${jwt.refresh-token.expiration-time}")
+ private long REFRESH_TOKEN_EXPIRATION_TIME;
+
+ public Map processSocialLogin(String provider, String token) {
+ OAuth2UserInfo userInfo = verifyTokenAndGetUserInfo(provider, token);
+
+ String providerId = userInfo.getProviderId();
+ String name = userInfo.getName();
+
+ User existUser = userRepository.findByProviderId(providerId);
+ User user;
+ if (existUser == null) {
+ user = User.builder()
+ .userId(UUID.randomUUID())
+ .name(name)
+ .provider(provider)
+ .providerId(providerId)
+ .build();
+ userRepository.save(user);
+ } else {
+ refreshTokenRepository.deleteByUserId(existUser.getUserId());
+ user = existUser;
+ }
+
+ String refreshToken = jwtUtil.generateRefreshToken(user.getUserId(), REFRESH_TOKEN_EXPIRATION_TIME);
+ RefreshToken newRefreshToken = RefreshToken.builder()
+ .userId(user.getUserId())
+ .token(refreshToken)
+ .build();
+ refreshTokenRepository.save(newRefreshToken);
+
+ String accessToken = jwtUtil.generateAccessToken(user.getUserId(), ACCESS_TOKEN_EXPIRATION_TIME);
+
+ return Map.of(
+ "name", name,
+ "accessToken", accessToken,
+ "refreshToken", refreshToken
+ );
+ }
+
+ private OAuth2UserInfo verifyTokenAndGetUserInfo(String provider, String token) {
+ RestTemplate restTemplate = new RestTemplate();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(token); // Authorization: Bearer {token}
+ HttpEntity entity = new HttpEntity<>(headers);
+
+ try {
+ switch (provider.toLowerCase()) {
+ case "google":
+ String googleUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
+ ResponseEntity