From 41b5c4e32cd0039264e83b9aa63eca12514d8e62 Mon Sep 17 00:00:00 2001 From: TueBack Date: Mon, 9 Feb 2026 22:31:44 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20PortOne=20V2=20=EB=B3=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PortOne V2 API 통합 - 본인인증 기반 회원 정보 검증 및 CI/DI 저장 - 프로필 관리 API (조회/수정) 구현 - 여행 스타일 시스템 추가 (8가지 스타일 자동 초기화) - 중복 가입 방지 (CI 기반) - 테스트 환경 대응 (CI/DI 선택적 처리) - CORS 설정 개선 (개발 환경 localhost 포트 패턴 허용) --- .gitignore | 39 ++++ api-test.http | 2 +- api-test2.http | 6 +- build.gradle | 8 + .../auth/application/config/JwtProvider.java | 32 ++- .../application/config/SecurityConfig.java | 21 +- .../application/dto/CertificationInfo.java | 14 ++ .../request/UpdateNotificationRequest.java | 13 ++ .../dto/request/UpdateProfileRequest.java | 23 +++ .../dto/request/VerifyIdentityRequest.java | 13 ++ .../dto/response/ProfileResponse.java | 37 ++++ .../dto/response/VerifyIdentityResponse.java | 23 +++ .../out/repository/MemberRepository.java | 10 +- .../MemberTravelStyleRepository.java | 16 ++ .../out/repository/TravelStyleRepository.java | 11 + .../service/IdentityVerificationService.java | 130 ++++++++++++ .../application/service/ProfileService.java | 84 ++++++++ .../service/TravelStyleService.java | 41 ++++ .../com/retrip/auth/domain/entity/Member.java | 65 +++++- .../auth/domain/entity/MemberTravelStyle.java | 34 ++++ .../auth/domain/entity/TravelStyle.java | 30 +++ .../exception/DuplicateUserException.java | 10 + .../domain/exception/PortOneApiException.java | 10 + .../domain/exception/common/ErrorCode.java | 6 +- .../in/rest/controller/ProfileController.java | 80 ++++++++ .../controller/TravelStyleController.java | 30 +++ .../auth/infra/config/DataInitializer.java | 22 ++ .../auth/infra/config/PortOneConfig.java | 19 ++ .../resources/application-local.yml.example | 48 +++++ src/main/resources/application.yml | 65 ++---- src/main/resources/test.html | 192 ++++++++++++++++++ 31 files changed, 1051 insertions(+), 83 deletions(-) create mode 100644 src/main/java/com/retrip/auth/application/dto/CertificationInfo.java create mode 100644 src/main/java/com/retrip/auth/application/dto/request/UpdateNotificationRequest.java create mode 100644 src/main/java/com/retrip/auth/application/dto/request/UpdateProfileRequest.java create mode 100644 src/main/java/com/retrip/auth/application/dto/request/VerifyIdentityRequest.java create mode 100644 src/main/java/com/retrip/auth/application/dto/response/ProfileResponse.java create mode 100644 src/main/java/com/retrip/auth/application/dto/response/VerifyIdentityResponse.java create mode 100644 src/main/java/com/retrip/auth/application/out/repository/MemberTravelStyleRepository.java create mode 100644 src/main/java/com/retrip/auth/application/out/repository/TravelStyleRepository.java create mode 100644 src/main/java/com/retrip/auth/application/service/IdentityVerificationService.java create mode 100644 src/main/java/com/retrip/auth/application/service/ProfileService.java create mode 100644 src/main/java/com/retrip/auth/application/service/TravelStyleService.java create mode 100644 src/main/java/com/retrip/auth/domain/entity/MemberTravelStyle.java create mode 100644 src/main/java/com/retrip/auth/domain/entity/TravelStyle.java create mode 100644 src/main/java/com/retrip/auth/domain/exception/DuplicateUserException.java create mode 100644 src/main/java/com/retrip/auth/domain/exception/PortOneApiException.java create mode 100644 src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/ProfileController.java create mode 100644 src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/TravelStyleController.java create mode 100644 src/main/java/com/retrip/auth/infra/config/DataInitializer.java create mode 100644 src/main/java/com/retrip/auth/infra/config/PortOneConfig.java create mode 100644 src/main/resources/application-local.yml.example create mode 100644 src/main/resources/test.html diff --git a/.gitignore b/.gitignore index c2065bc..93f67ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,26 @@ +# ==================================== +# 민감 정보 파일 (절대 커밋 금지!) +# ==================================== + +# 환경별 설정 파일 +application-local.yml +application-prod.yml +application-prod-debug.yml + +# 환경 변수 파일 +.env +.env.local +.env.production +.env.*.local + +# 테스트 파일 +test.html +*.test.html + +# ==================================== +# 기존 .gitignore 내용 +# ==================================== + HELP.md .gradle build/ @@ -35,3 +58,19 @@ out/ ### VS Code ### .vscode/ + +### Mac ### +.DS_Store + +### Windows ### +Thumbs.db + +### Logs ### +*.log +logs/ + +### Temporary files ### +*.tmp +*.bak +*.swp +*~.nib diff --git a/api-test.http b/api-test.http index 409d410..0e479bf 100644 --- a/api-test.http +++ b/api-test.http @@ -1,5 +1,5 @@ ### 전역 변수 설정 (필요시 변경) -@auth_host = http://localhost:8080 +@auth_host = http://15.164.112.64:8081 @trip_host = http://localhost:8081 @email = test@naver.com @password = password1234 diff --git a/api-test2.http b/api-test2.http index 20bdb18..f66e075 100644 --- a/api-test2.http +++ b/api-test2.http @@ -5,11 +5,11 @@ ### 전역 변수 설정 (랜덤 값 사용) @random_id = {{$randomInt}} -@email = test{{random_id}}@naver.com +@email = test2{{random_id}}@naver.com @password = password1234 @new_password = newpassword5678 -@auth_host = http://localhost:8080 -@trip_host = http://localhost:8081 +@auth_host = http://15.164.112.64:8081 +@trip_host = http://15.164.112.64:8080 ### ======================================== ### 📌 Phase 1: 회원가입 및 토큰 발급 검증 diff --git a/build.gradle b/build.gradle index 63c88f1..6f2f6f2 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,14 @@ dependencies { annotationProcessor 'jakarta.persistence:jakarta.persistence-api' implementation 'com.mysql:mysql-connector-j' + // OkHttp (포트원 API 호출용) + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + + implementation('com.google.code.gson:gson') { + version { + strictly '2.11.0' + } + } } tasks.named('test') { diff --git a/src/main/java/com/retrip/auth/application/config/JwtProvider.java b/src/main/java/com/retrip/auth/application/config/JwtProvider.java index ecfdd20..0752033 100644 --- a/src/main/java/com/retrip/auth/application/config/JwtProvider.java +++ b/src/main/java/com/retrip/auth/application/config/JwtProvider.java @@ -30,9 +30,6 @@ public class JwtProvider { private final JwtConfig jwtConfig; - // ... createToken, generateTokens 등 생성 로직은 기존 유지 ... - // (위에 작성하신 코드 그대로 두셔도 됩니다. 아래 getAuthentication만 수정하면 됩니다.) - public LoginResponse.TokenResponse generateTokens(Authentication authentication) { Instant now = Instant.now(); String authorities = String.join(",", getAuthorities(authentication)); @@ -45,13 +42,12 @@ public LoginResponse.TokenResponse generateTokens(Authentication authentication) Object principal = authentication.getPrincipal(); if (principal instanceof CustomUserDetails userDetails) { - memberId = userDetails.getName(); // UUID + memberId = userDetails.getName(); email = userDetails.getEmail(); name = userDetails.getRealName(); gender = userDetails.getGender(); age = userDetails.getAge(); } else { - // principal이 String인 경우 (방어 코드) memberId = authentication.getName(); } @@ -71,7 +67,7 @@ private String createToken(String subject, String email, String name, String gen .subject(subject) .claim("username", email) .claim("name", name) - .claim("authorities", authorities); + .claim("authorities", authorities); // 권한이 없으면 빈 문자열 ""이 들어감 if (gender != null) builder.claim("gender", gender); if (age != null) builder.claim("age", age); @@ -86,7 +82,6 @@ private String createToken(String subject, String email, String name, String gen } } - // [중요 수정] Authentication 객체 생성 시 CustomUserDetails 재구성 public Authentication getAuthentication(String token) { try { PublicKey publicKey = getPublicKey(jwtConfig.getPublicKey()); @@ -97,33 +92,36 @@ public Authentication getAuthentication(String token) { .getPayload(); // 1. Claims에서 정보 추출 - String memberId = claims.getSubject(); // UUID + String memberId = claims.getSubject(); String email = claims.get("username", String.class); String name = claims.get("name", String.class); String authoritiesStr = claims.get("authorities", String.class); String gender = claims.get("gender", String.class); Integer age = claims.get("age", Integer.class); - // 2. 권한 목록 생성 - List authorities = Arrays.stream(authoritiesStr.split(",")) - .map(String::trim) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - - // 3. 임시 Member 객체 생성 (비밀번호는 null 처리) + // 2. 권한 목록 생성 [수정된 부분: 빈 문자열 처리 추가] + List authorities = new ArrayList<>(); + if (authoritiesStr != null && !authoritiesStr.isBlank()) { + authorities = Arrays.stream(authoritiesStr.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) // 빈 문자열 필터링 (중요) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + // 3. 임시 Member 객체 생성 Member member = Member.builder() .id(UUID.fromString(memberId)) .email(new MemberEmail(email)) .name(new MemberName(name)) .gender(gender) .age(age) - .password(null) // 인증된 상태이므로 비밀번호 불필요 + .password(null) .build(); // 4. CustomUserDetails 생성 CustomUserDetails principal = new CustomUserDetails(member); - // 5. Authentication 리턴 (이제 Principal은 CustomUserDetails임) return new UsernamePasswordAuthenticationToken(principal, token, authorities); } catch (Exception e) { diff --git a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java index ad6e270..c4ee604 100644 --- a/src/main/java/com/retrip/auth/application/config/SecurityConfig.java +++ b/src/main/java/com/retrip/auth/application/config/SecurityConfig.java @@ -67,7 +67,6 @@ public LoginAuthenticationFilter loginAuthenticationFilter( JwtConfig jwtConfig, AuthenticationManager authenticationManager, JwtProvider jwtProvider) { - // 생성자에 refreshTokenRepository가 포함되어야 합니다. LoginAuthenticationFilter filter = new LoginAuthenticationFilter(jwtConfig, authenticationManager, jwtProvider, refreshTokenRepository); return filter; } @@ -77,8 +76,6 @@ public SecurityFilterChain securityFilterChain( HttpSecurity http, LoginAuthenticationFilter loginAuthenticationFilter) throws Exception { - // [핵심] 401 에러 해결을 위한 SecurityContextRepository 설정 - // 이 부분이 빠져있어서 인증 정보가 유지되지 않았습니다. SecurityContextRepository securityContextRepository = new DelegatingSecurityContextRepository( new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository() @@ -90,7 +87,6 @@ public SecurityFilterChain securityFilterChain( .csrf(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) - // [핵심] SecurityContext 설정 추가 .securityContext(context -> context .securityContextRepository(securityContextRepository) ) @@ -113,10 +109,14 @@ public SecurityFilterChain securityFilterChain( ) .authorizeHttpRequests(auth -> auth - .requestMatchers(HttpMethod.POST, "/users").permitAll() + // ✅ 수정: /api/users 경로 추가 + .requestMatchers(HttpMethod.POST, "/users", "/api/users").permitAll() .requestMatchers("/login/**", "/oauth2/**", "/auth/reissue", "/auth/logout", "/").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll() .requestMatchers("/debug/**").permitAll() + // ✅ 추가: 본인인증 및 여행 스타일 조회 API 허용 + .requestMatchers(HttpMethod.GET, "/api/travel-styles").permitAll() + .requestMatchers(HttpMethod.POST, "/api/auth/verify-identity").authenticated() .anyRequest().authenticated() ); @@ -126,14 +126,21 @@ public SecurityFilterChain securityFilterChain( @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("http://localhost:3000")); + + // 개발 환경: 모든 localhost 포트 허용 + config.setAllowedOriginPatterns(List.of( + "http://localhost:*", + "http://127.0.0.1:*" + )); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); config.setExposedHeaders(List.of("Authorization")); + config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } -} \ No newline at end of file +} diff --git a/src/main/java/com/retrip/auth/application/dto/CertificationInfo.java b/src/main/java/com/retrip/auth/application/dto/CertificationInfo.java new file mode 100644 index 0000000..f47e51d --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/CertificationInfo.java @@ -0,0 +1,14 @@ +package com.retrip.auth.application.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CertificationInfo { + private String name; // 이름 + private String gender; // 성별 (male/female) + private String birthday; // 생년월일 (YYYYMMDD) + private String uniqueKey; // CI + private String uniqueInSite; // DI +} diff --git a/src/main/java/com/retrip/auth/application/dto/request/UpdateNotificationRequest.java b/src/main/java/com/retrip/auth/application/dto/request/UpdateNotificationRequest.java new file mode 100644 index 0000000..4fa0770 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/request/UpdateNotificationRequest.java @@ -0,0 +1,13 @@ +package com.retrip.auth.application.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UpdateNotificationRequest { + + @NotNull(message = "알림 설정 값은 필수입니다.") + private Boolean enabled; +} diff --git a/src/main/java/com/retrip/auth/application/dto/request/UpdateProfileRequest.java b/src/main/java/com/retrip/auth/application/dto/request/UpdateProfileRequest.java new file mode 100644 index 0000000..94e7c63 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/request/UpdateProfileRequest.java @@ -0,0 +1,23 @@ +package com.retrip.auth.application.dto.request; + +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class UpdateProfileRequest { + + @Size(max = 30, message = "소개는 최대 30자까지 입력 가능합니다.") + private String bio; + + @Size(max = 4, message = "MBTI는 4자리여야 합니다.") + private String mbti; + + private String profileImageUrl; + + @Size(max = 3, message = "여행 스타일은 최대 3개까지 선택 가능합니다.") + private List travelStyles; +} diff --git a/src/main/java/com/retrip/auth/application/dto/request/VerifyIdentityRequest.java b/src/main/java/com/retrip/auth/application/dto/request/VerifyIdentityRequest.java new file mode 100644 index 0000000..711dc39 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/request/VerifyIdentityRequest.java @@ -0,0 +1,13 @@ +package com.retrip.auth.application.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class VerifyIdentityRequest { + + @NotBlank(message = "imp_uid는 필수입니다.") + private String impUid; +} diff --git a/src/main/java/com/retrip/auth/application/dto/response/ProfileResponse.java b/src/main/java/com/retrip/auth/application/dto/response/ProfileResponse.java new file mode 100644 index 0000000..fbd98cb --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/response/ProfileResponse.java @@ -0,0 +1,37 @@ +package com.retrip.auth.application.dto.response; + +import com.retrip.auth.domain.entity.Member; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +@Builder +public class ProfileResponse { + private String email; + private String name; + private String gender; + private Integer age; + private Boolean isVerified; + private String profileImageUrl; + private String bio; + private String mbti; + private List travelStyles; + private Boolean notificationEnabled; + + public static ProfileResponse from(Member member, List travelStyles) { + return ProfileResponse.builder() + .email(member.getEmailValue()) + .name(member.getNameValue()) + .gender(member.getGender()) + .age(member.getAge()) + .isVerified(member.isVerified()) + .profileImageUrl(member.getProfileImageUrl()) + .bio(member.getBio()) + .mbti(member.getMbti()) + .travelStyles(travelStyles) + .notificationEnabled(member.isNotificationEnabled()) + .build(); + } +} diff --git a/src/main/java/com/retrip/auth/application/dto/response/VerifyIdentityResponse.java b/src/main/java/com/retrip/auth/application/dto/response/VerifyIdentityResponse.java new file mode 100644 index 0000000..f030f26 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/dto/response/VerifyIdentityResponse.java @@ -0,0 +1,23 @@ +package com.retrip.auth.application.dto.response; + +import com.retrip.auth.domain.entity.Member; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class VerifyIdentityResponse { + private String name; + private String gender; + private Integer age; + private Boolean isVerified; + + public static VerifyIdentityResponse from(Member member) { + return VerifyIdentityResponse.builder() + .name(member.getNameValue()) + .gender(member.getGender()) + .age(member.getAge()) + .isVerified(member.isVerified()) + .build(); + } +} diff --git a/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java b/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java index c0d4a82..d26f2e3 100644 --- a/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java +++ b/src/main/java/com/retrip/auth/application/out/repository/MemberRepository.java @@ -1,4 +1,3 @@ - package com.retrip.auth.application.out.repository; import com.retrip.auth.domain.entity.Member; @@ -6,9 +5,16 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; import java.util.UUID; public interface MemberRepository extends JpaRepository { List findByEmailAndIsDeletedFalse(MemberEmail email); List findByEmail(MemberEmail email); -} \ No newline at end of file + + // 추가: 이메일로 단건 조회 (Optional) + Optional findByEmail_Value(String email); + + // 추가: CI 중복 체크 + boolean existsByCi(String ci); +} diff --git a/src/main/java/com/retrip/auth/application/out/repository/MemberTravelStyleRepository.java b/src/main/java/com/retrip/auth/application/out/repository/MemberTravelStyleRepository.java new file mode 100644 index 0000000..9ad6915 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/out/repository/MemberTravelStyleRepository.java @@ -0,0 +1,16 @@ +package com.retrip.auth.application.out.repository; + +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.entity.MemberTravelStyle; +import com.retrip.auth.domain.entity.TravelStyle; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.UUID; + +public interface MemberTravelStyleRepository extends JpaRepository { + List findByMemberId(UUID memberId); + void deleteByMemberIdAndTravelStyleId(UUID memberId, Long travelStyleId); + void deleteByMemberId(UUID memberId); + boolean existsByMemberAndTravelStyle(Member member, TravelStyle travelStyle); +} diff --git a/src/main/java/com/retrip/auth/application/out/repository/TravelStyleRepository.java b/src/main/java/com/retrip/auth/application/out/repository/TravelStyleRepository.java new file mode 100644 index 0000000..81cf989 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/out/repository/TravelStyleRepository.java @@ -0,0 +1,11 @@ +package com.retrip.auth.application.out.repository; + +import com.retrip.auth.domain.entity.TravelStyle; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TravelStyleRepository extends JpaRepository { + Optional findByName(String name); + boolean existsByName(String name); +} diff --git a/src/main/java/com/retrip/auth/application/service/IdentityVerificationService.java b/src/main/java/com/retrip/auth/application/service/IdentityVerificationService.java new file mode 100644 index 0000000..f38c861 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/service/IdentityVerificationService.java @@ -0,0 +1,130 @@ +package com.retrip.auth.application.service; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.retrip.auth.application.dto.CertificationInfo; +import com.retrip.auth.application.dto.response.VerifyIdentityResponse; +import com.retrip.auth.application.out.repository.MemberRepository; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.exception.DuplicateUserException; +import com.retrip.auth.domain.exception.MemberNotFoundException; +import com.retrip.auth.domain.exception.PortOneApiException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class IdentityVerificationService { + + private final MemberRepository memberRepository; + + @Value("${portone.api_secret}") + private String apiSecret; + + // [수정] 매개변수 이름을 email -> memberId로 변경하고 UUID로 조회 + @Transactional + public VerifyIdentityResponse verifyAndSave(String identityVerificationId, String memberId) { + log.info("🔍 본인인증 검증 시작 - ID: {}, MemberId: {}", identityVerificationId, memberId); + + // 1. 포트원 V2 API로 본인인증 정보 조회 + CertificationInfo certInfo = getCertificationInfo(identityVerificationId); + + // 2. 중복 가입 체크 (CI가 존재할 경우에만) + if (certInfo.getUniqueKey() != null && memberRepository.existsByCi(certInfo.getUniqueKey())) { + log.warn("⚠️ 중복 가입 시도 - CI: {}", certInfo.getUniqueKey()); + throw new DuplicateUserException(); + } + + // 3. 사용자 정보 업데이트 (UUID로 조회) + Member member = memberRepository.findById(UUID.fromString(memberId)) + .orElseThrow(MemberNotFoundException::new); + + // 성별 변환 (MALE -> M, FEMALE -> F) + String gender = "MALE".equals(certInfo.getGender()) ? "M" : "F"; + + log.info("✅ 회원 본인인증 정보 업데이트 - Name: {}, Gender: {}, Age: {}", + certInfo.getName(), gender, member.calculateAge(certInfo.getBirthday())); + + member.updateIdentityVerification( + certInfo.getName(), + gender, + certInfo.getBirthday(), + certInfo.getUniqueKey(), + certInfo.getUniqueInSite() + ); + + return VerifyIdentityResponse.from(member); + } + + private CertificationInfo getCertificationInfo(String identityVerificationId) { + OkHttpClient client = new OkHttpClient(); + String url = "https://api.portone.io/identity-verifications/" + identityVerificationId; + + Request request = new Request.Builder() + .url(url) + .addHeader("Authorization", "PortOne " + apiSecret) + .get() + .build(); + + try (Response response = client.newCall(request).execute()) { + String responseBody = response.body().string(); + + if (!response.isSuccessful()) { + throw new PortOneApiException("본인인증 정보 조회 실패: " + response.code()); + } + + JsonObject json = JsonParser.parseString(responseBody).getAsJsonObject(); + if (!json.has("verifiedCustomer")) { + throw new PortOneApiException("Missing verifiedCustomer in response"); + } + + JsonObject verifiedCustomer = json.getAsJsonObject("verifiedCustomer"); + + // 필수 필드 + String name = getStringField(verifiedCustomer, "name"); + String gender = getStringField(verifiedCustomer, "gender"); + String birthday = verifiedCustomer.has("birthDate") + ? getStringField(verifiedCustomer, "birthDate") + : getStringField(verifiedCustomer, "birthday"); + + // [수정] CI, DI는 선택적 필드로 처리 (테스트 환경 대응) + String ci = getOptionalStringField(verifiedCustomer, "ci"); + String di = getOptionalStringField(verifiedCustomer, "di"); + + return CertificationInfo.builder() + .name(name) + .gender(gender) + .birthday(birthday) + .uniqueKey(ci) + .uniqueInSite(di) + .build(); + } catch (IOException e) { + throw new PortOneApiException("IO Error: " + e.getMessage()); + } + } + + private String getStringField(JsonObject json, String fieldName) { + if (!json.has(fieldName) || json.get(fieldName).isJsonNull()) { + throw new PortOneApiException("Missing required field: " + fieldName); + } + return json.get(fieldName).getAsString(); + } + + // [추가] 선택적 필드 추출 메서드 + private String getOptionalStringField(JsonObject json, String fieldName) { + if (!json.has(fieldName) || json.get(fieldName).isJsonNull()) { + return null; + } + String value = json.get(fieldName).getAsString(); + return value.isEmpty() ? null : value; + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/service/ProfileService.java b/src/main/java/com/retrip/auth/application/service/ProfileService.java new file mode 100644 index 0000000..f6689e6 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/service/ProfileService.java @@ -0,0 +1,84 @@ +package com.retrip.auth.application.service; + +import com.retrip.auth.application.dto.request.UpdateNotificationRequest; +import com.retrip.auth.application.dto.request.UpdateProfileRequest; +import com.retrip.auth.application.dto.response.ProfileResponse; +import com.retrip.auth.application.out.repository.MemberRepository; +import com.retrip.auth.application.out.repository.MemberTravelStyleRepository; +import com.retrip.auth.application.out.repository.TravelStyleRepository; +import com.retrip.auth.domain.entity.Member; +import com.retrip.auth.domain.entity.MemberTravelStyle; +import com.retrip.auth.domain.entity.TravelStyle; +import com.retrip.auth.domain.exception.MemberNotFoundException; +import com.retrip.auth.domain.exception.common.InvalidValueException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ProfileService { + + private final MemberRepository memberRepository; + private final TravelStyleRepository travelStyleRepository; + private final MemberTravelStyleRepository memberTravelStyleRepository; + + // [수정] String email -> String memberId (UUID) + public ProfileResponse getProfile(String memberId) { + Member member = memberRepository.findById(UUID.fromString(memberId)) + .orElseThrow(MemberNotFoundException::new); + + List travelStyles = memberTravelStyleRepository.findByMemberId(member.getId()) + .stream() + .map(mts -> mts.getTravelStyle().getName()) + .collect(Collectors.toList()); + + return ProfileResponse.from(member, travelStyles); + } + + // [수정] String email -> String memberId (UUID) + @Transactional + public ProfileResponse updateProfile(String memberId, UpdateProfileRequest request) { + Member member = memberRepository.findById(UUID.fromString(memberId)) + .orElseThrow(MemberNotFoundException::new); + + member.updateProfile(request.getBio(), request.getMbti(), request.getProfileImageUrl()); + + if (request.getTravelStyles() != null) { + updateTravelStyles(member, request.getTravelStyles()); + } + + List travelStyles = memberTravelStyleRepository.findByMemberId(member.getId()) + .stream() + .map(mts -> mts.getTravelStyle().getName()) + .collect(Collectors.toList()); + + return ProfileResponse.from(member, travelStyles); + } + + // [수정] String email -> String memberId (UUID) + @Transactional + public void updateNotificationSettings(String memberId, UpdateNotificationRequest request) { + Member member = memberRepository.findById(UUID.fromString(memberId)) + .orElseThrow(MemberNotFoundException::new); + + member.updateNotificationSettings(request.getEnabled()); + } + + private void updateTravelStyles(Member member, List styleNames) { + memberTravelStyleRepository.deleteByMemberId(member.getId()); + for (String styleName : styleNames) { + TravelStyle travelStyle = travelStyleRepository.findByName(styleName) + .orElseThrow(() -> new InvalidValueException("존재하지 않는 여행 스타일입니다: " + styleName)); + MemberTravelStyle memberTravelStyle = MemberTravelStyle.of(member, travelStyle); + memberTravelStyleRepository.save(memberTravelStyle); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/auth/application/service/TravelStyleService.java b/src/main/java/com/retrip/auth/application/service/TravelStyleService.java new file mode 100644 index 0000000..c7f7477 --- /dev/null +++ b/src/main/java/com/retrip/auth/application/service/TravelStyleService.java @@ -0,0 +1,41 @@ +package com.retrip.auth.application.service; + +import com.retrip.auth.application.out.repository.TravelStyleRepository; +import com.retrip.auth.domain.entity.TravelStyle; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TravelStyleService { + + private final TravelStyleRepository travelStyleRepository; + + public List getAllTravelStyles() { + return travelStyleRepository.findAll(); + } + + @Transactional + public void initializeTravelStyles() { + // 기본 여행 스타일 초기화 (이미 존재하면 스킵) + String[] defaultStyles = { + "계획철저", "즉흥적", "맛집탐방", "휴양지", + "가성비", "플렉스", "아침형", "올빼미" + }; + + for (int i = 0; i < defaultStyles.length; i++) { + String styleName = defaultStyles[i]; + if (!travelStyleRepository.existsByName(styleName)) { + TravelStyle style = TravelStyle.of(styleName, i + 1); + travelStyleRepository.save(style); + log.info("여행 스타일 초기화: {}", styleName); + } + } + } +} diff --git a/src/main/java/com/retrip/auth/domain/entity/Member.java b/src/main/java/com/retrip/auth/domain/entity/Member.java index 5cc2918..5dd7cdd 100644 --- a/src/main/java/com/retrip/auth/domain/entity/Member.java +++ b/src/main/java/com/retrip/auth/domain/entity/Member.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -42,19 +43,41 @@ public class Member extends BaseEntity { @Column(unique = true) private String providerId; + // 본인인증 관련 @Column(length = 88, unique = true) private String ci; + @Column(length = 64) + private String di; + @Column(nullable = false) @Builder.Default private boolean isVerified = false; - @Column(name = "gender") + private LocalDateTime verifiedAt; + + // 기본 정보 + @Column(name = "gender", length = 1) private String gender; @Column(name = "age") private Integer age; + // 프로필 관련 + @Column(length = 500) + private String profileImageUrl; + + @Column(length = 30) + private String bio; + + @Column(length = 4) + private String mbti; + + // 설정 + @Column(nullable = false) + @Builder.Default + private boolean notificationEnabled = true; + public String getPasswordValue() { return this.password != null ? this.password.getValue() : null; } @@ -105,6 +128,44 @@ public void update(String name, String password, String gender, Integer age) { if (age != null) this.age = age; } + public void updateProfile(String bio, String mbti, String profileImageUrl) { + if (bio != null) this.bio = bio; + if (mbti != null) this.mbti = mbti; + if (profileImageUrl != null) this.profileImageUrl = profileImageUrl; + } + + public void updateNotificationSettings(boolean enabled) { + this.notificationEnabled = enabled; + } + + public void updateIdentityVerification(String name, String gender, String birthday, String ci, String di) { + this.name = new MemberName(name); + this.gender = gender; + this.age = calculateAge(birthday); + this.ci = ci; + this.di = di; + this.isVerified = true; + this.verifiedAt = LocalDateTime.now(); + } + + public int calculateAge(String birthday) { + // V2: "1990-01-01" 형식 + // V1: "19900101" 형식 + + String yearStr; + if (birthday.contains("-")) { + // V2 형식: YYYY-MM-DD + yearStr = birthday.substring(0, 4); + } else { + // V1 형식: YYYYMMDD + yearStr = birthday.substring(0, 4); + } + + int birthYear = Integer.parseInt(yearStr); + int currentYear = LocalDateTime.now().getYear(); + return currentYear - birthYear; + } + public void updateSocialInfo(String name) { this.name = new MemberName(name); } @@ -116,4 +177,4 @@ public void updatePassword(String encodedPassword) { public void delete() { this.isDeleted = true; } -} \ No newline at end of file +} diff --git a/src/main/java/com/retrip/auth/domain/entity/MemberTravelStyle.java b/src/main/java/com/retrip/auth/domain/entity/MemberTravelStyle.java new file mode 100644 index 0000000..6134c0a --- /dev/null +++ b/src/main/java/com/retrip/auth/domain/entity/MemberTravelStyle.java @@ -0,0 +1,34 @@ +package com.retrip.auth.domain.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.UUID; + +@Entity +@Table(name = "member_travel_styles") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +public class MemberTravelStyle { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", columnDefinition = "varbinary(16)", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "travel_style_id", nullable = false) + private TravelStyle travelStyle; + + public static MemberTravelStyle of(Member member, TravelStyle travelStyle) { + return MemberTravelStyle.builder() + .member(member) + .travelStyle(travelStyle) + .build(); + } +} diff --git a/src/main/java/com/retrip/auth/domain/entity/TravelStyle.java b/src/main/java/com/retrip/auth/domain/entity/TravelStyle.java new file mode 100644 index 0000000..e17f510 --- /dev/null +++ b/src/main/java/com/retrip/auth/domain/entity/TravelStyle.java @@ -0,0 +1,30 @@ +package com.retrip.auth.domain.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "travel_styles") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +public class TravelStyle { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 20) + private String name; + + @Column(nullable = false) + private Integer displayOrder; + + public static TravelStyle of(String name, Integer displayOrder) { + return TravelStyle.builder() + .name(name) + .displayOrder(displayOrder) + .build(); + } +} diff --git a/src/main/java/com/retrip/auth/domain/exception/DuplicateUserException.java b/src/main/java/com/retrip/auth/domain/exception/DuplicateUserException.java new file mode 100644 index 0000000..9d70a78 --- /dev/null +++ b/src/main/java/com/retrip/auth/domain/exception/DuplicateUserException.java @@ -0,0 +1,10 @@ +package com.retrip.auth.domain.exception; + +import com.retrip.auth.domain.exception.common.BusinessException; +import com.retrip.auth.domain.exception.common.ErrorCode; + +public class DuplicateUserException extends BusinessException { + public DuplicateUserException() { + super(ErrorCode.DUPLICATE_USER); + } +} diff --git a/src/main/java/com/retrip/auth/domain/exception/PortOneApiException.java b/src/main/java/com/retrip/auth/domain/exception/PortOneApiException.java new file mode 100644 index 0000000..00068a4 --- /dev/null +++ b/src/main/java/com/retrip/auth/domain/exception/PortOneApiException.java @@ -0,0 +1,10 @@ +package com.retrip.auth.domain.exception; + +import com.retrip.auth.domain.exception.common.BusinessException; +import com.retrip.auth.domain.exception.common.ErrorCode; + +public class PortOneApiException extends BusinessException { + public PortOneApiException(String message) { + super(ErrorCode.EXTERNAL_API_ERROR, message); + } +} diff --git a/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java b/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java index c362a02..b087322 100644 --- a/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java +++ b/src/main/java/com/retrip/auth/domain/exception/common/ErrorCode.java @@ -13,13 +13,13 @@ public enum ErrorCode { ENTITY_NOT_FOUND(BAD_REQUEST, "Common-004", "Entity not found"), ILLEGAL_STATE(BAD_REQUEST, "Common-005", "Illegal state"), INVALID_ACCESS(FORBIDDEN, "Common-006","접근 권한이 존재하지 않습니다."), + EXTERNAL_API_ERROR(INTERNAL_SERVER_ERROR, "Common-007", "외부 API 호출 중 오류가 발생했습니다."), MEMBER_NOT_FOUND(HttpStatus.UNAUTHORIZED, "Member-001", "멤버 엔티티를 찾을 수 없습니다."), PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "Member-002", "비밀 번호가 다릅니다."), - - // ✅ 추가 DELETED_MEMBER_CANNOT_REJOIN(HttpStatus.BAD_REQUEST, "Member-003", "탈퇴한 이메일은 재가입할 수 없습니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "Member-004", "이미 존재하는 이메일입니다."), + DUPLICATE_USER(HttpStatus.CONFLICT, "Member-005", "이미 가입된 정보입니다."), ; private final HttpStatus status; @@ -31,4 +31,4 @@ public enum ErrorCode { this.code = code; this.message = message; } -} \ No newline at end of file +} diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/ProfileController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/ProfileController.java new file mode 100644 index 0000000..8c0e5d7 --- /dev/null +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/ProfileController.java @@ -0,0 +1,80 @@ +package com.retrip.auth.infra.adapter.in.rest.controller; + +import com.retrip.auth.application.dto.request.UpdateNotificationRequest; +import com.retrip.auth.application.dto.request.UpdateProfileRequest; +import com.retrip.auth.application.dto.request.VerifyIdentityRequest; +import com.retrip.auth.application.dto.response.ProfileResponse; +import com.retrip.auth.application.dto.response.VerifyIdentityResponse; +import com.retrip.auth.application.service.IdentityVerificationService; +import com.retrip.auth.application.service.ProfileService; +import com.retrip.auth.infra.adapter.in.rest.common.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ProfileController { + + private final ProfileService profileService; + private final IdentityVerificationService identityVerificationService; + + /** + * 내 프로필 조회 + */ + @GetMapping("/users/profile") + public ApiResponse getMyProfile( + @AuthenticationPrincipal UserDetails userDetails + ) { + String email = userDetails.getUsername(); + ProfileResponse profile = profileService.getProfile(email); + return ApiResponse.ok(profile); + } + + /** + * 프로필 수정 + */ + @PutMapping("/users/profile") + public ApiResponse updateProfile( + @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody UpdateProfileRequest request + ) { + String email = userDetails.getUsername(); + ProfileResponse profile = profileService.updateProfile(email, request); + return ApiResponse.ok(profile); + } + + /** + * 알림 설정 변경 + */ + @PatchMapping("/users/notification-settings") + public ApiResponse updateNotificationSettings( + @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody UpdateNotificationRequest request + ) { + String email = userDetails.getUsername(); + profileService.updateNotificationSettings(email, request); + return ApiResponse.ok(null); + } + + /** + * 본인인증 검증 및 정보 저장 + */ + @PostMapping("/auth/verify-identity") + public ApiResponse verifyIdentity( + @AuthenticationPrincipal UserDetails userDetails, + @Valid @RequestBody VerifyIdentityRequest request + ) { + String email = userDetails.getUsername(); + VerifyIdentityResponse response = identityVerificationService.verifyAndSave( + request.getImpUid(), + email + ); + return ApiResponse.ok(response); + } +} diff --git a/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/TravelStyleController.java b/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/TravelStyleController.java new file mode 100644 index 0000000..c538328 --- /dev/null +++ b/src/main/java/com/retrip/auth/infra/adapter/in/rest/controller/TravelStyleController.java @@ -0,0 +1,30 @@ +package com.retrip.auth.infra.adapter.in.rest.controller; + +import com.retrip.auth.application.service.TravelStyleService; +import com.retrip.auth.domain.entity.TravelStyle; +import com.retrip.auth.infra.adapter.in.rest.common.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/api/travel-styles") +@RequiredArgsConstructor +public class TravelStyleController { + + private final TravelStyleService travelStyleService; + + /** + * 모든 여행 스타일 조회 + */ + @GetMapping + public ApiResponse> getAllTravelStyles() { + List travelStyles = travelStyleService.getAllTravelStyles(); + return ApiResponse.ok(travelStyles); + } +} diff --git a/src/main/java/com/retrip/auth/infra/config/DataInitializer.java b/src/main/java/com/retrip/auth/infra/config/DataInitializer.java new file mode 100644 index 0000000..4e06645 --- /dev/null +++ b/src/main/java/com/retrip/auth/infra/config/DataInitializer.java @@ -0,0 +1,22 @@ +package com.retrip.auth.infra.config; + +import com.retrip.auth.application.service.TravelStyleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class DataInitializer implements CommandLineRunner { + + private final TravelStyleService travelStyleService; + + @Override + public void run(String... args) { + log.info("여행 스타일 데이터 초기화 시작..."); + travelStyleService.initializeTravelStyles(); + log.info("여행 스타일 데이터 초기화 완료!"); + } +} diff --git a/src/main/java/com/retrip/auth/infra/config/PortOneConfig.java b/src/main/java/com/retrip/auth/infra/config/PortOneConfig.java new file mode 100644 index 0000000..31947b2 --- /dev/null +++ b/src/main/java/com/retrip/auth/infra/config/PortOneConfig.java @@ -0,0 +1,19 @@ +package com.retrip.auth.infra.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Getter +public class PortOneConfig { + + @Value("${portone.public.store-id}") + private String storeId; + + @Value("${portone.public.channel-key}") + private String channelKey; + + @Value("${portone.api_secret}") + private String apiSecret; +} \ No newline at end of file diff --git a/src/main/resources/application-local.yml.example b/src/main/resources/application-local.yml.example new file mode 100644 index 0000000..05013d0 --- /dev/null +++ b/src/main/resources/application-local.yml.example @@ -0,0 +1,48 @@ +# ==================================== +# 로컬 개발 환경 설정 템플릿 +# ==================================== +# 이 파일을 복사하여 사용하세요: +# 1. cp application-local.yml.example application-local.yml +# 2. 팀 공유 문서에서 실제 키를 복사하여 아래 값 교체 +# 3. IntelliJ Active profiles: local로 실행 +# ==================================== + +# PortOne V2 설정 +portone: + public: + # PortOne 콘솔 → Developers → API Keys에서 확인 + store-id: store-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + channel-key: channel-key-xxxxxxxxxxxxxxxxxxxxxxxxxx + # API Secret (절대 공개하지 마세요!) + api_secret: your-api-secret-here + +# OAuth2 설정 (선택사항) +spring: + security: + oauth2: + client: + registration: + google: + client-id: your-google-client-id + client-secret: your-google-client-secret + kakao: + client-id: your-kakao-rest-api-key + client-secret: your-kakao-client-secret + naver: + client-id: your-naver-client-id + client-secret: your-naver-client-secret + +# JWT 키 설정 +token: + jwt: + # RSA Private Key (개행 포함하여 전체 복사) + private-key: | + -----BEGIN PRIVATE KEY----- + your-private-key-content-here + -----END PRIVATE KEY----- + + # RSA Public Key (개행 포함하여 전체 복사) + public-key: | + -----BEGIN PUBLIC KEY----- + your-public-key-content-here + -----END PUBLIC KEY----- diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cc5e073..3e7ffd9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,15 +23,15 @@ spring: client: registration: google: - client-id: your-google-client-id - client-secret: your-google-client-secret + client-id: ${GOOGLE_CLIENT_ID:your-google-client-id} + client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret} scope: - profile - email redirect-uri: "http://localhost:8080/login/oauth2/code/google" kakao: - client-id: your-kakao-rest-api-key - client-secret: your-kakao-client-secret + client-id: ${KAKAO_CLIENT_ID:your-kakao-rest-api-key} + client-secret: ${KAKAO_CLIENT_SECRET:your-kakao-client-secret} client-authentication-method: client_secret_post authorization-grant-type: authorization_code scope: @@ -40,10 +40,9 @@ spring: redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" client-name: Kakao - # [4주차 신규 추가] 네이버 설정 naver: - client-id: your-naver-client-id - client-secret: your-naver-client-secret + client-id: ${NAVER_CLIENT_ID:your-naver-client-id} + client-secret: ${NAVER_CLIENT_SECRET:your-naver-client-secret} client-authentication-method: client_secret_post authorization-grant-type: authorization_code scope: @@ -59,12 +58,11 @@ spring: user-info-uri: https://kapi.kakao.com/v2/user/me user-name-attribute: id - # [4주차 신규 추가] 네이버 설정 naver: authorization-uri: https://nid.naver.com/oauth2.0/authorize token-uri: https://nid.naver.com/oauth2.0/token user-info-uri: https://openapi.naver.com/v1/nid/me - user-name-attribute: response # 네이버는 response 객체 안에 id, email 등이 있음 + user-name-attribute: response #logging logging: @@ -78,47 +76,12 @@ logging: web: client: RestTemplate: DEBUG + token: jwt: -# secret: Kf9uX!7zQa2$rB3cP#dLmV8@eYtNwZxGjHsTrC1o - - # [추가] RSA 키 설정 - private-key: | - -----BEGIN PRIVATE KEY----- - MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCV878Khf0TOGySkApf0AMfTB8R - Bk/isCqm2qrzRbZho4cCde76hTVSs/Rv1xwopLMwJUPB2Zet5xkHR0Us+ehrMwRtU5/oxLCuEZ/G - 6k9GaYmwhiCZDCjz++eNqr8ZgV+cfILLrIovjUb4ZlWqB4p2Gv7DWNTDgv0WzSXYn8BirBPFk6fP - GtTl6s8ZITkN3cDBa39uvG7ADuxF9VLeZtuxjJgTGNSsRuy3xW0Xc+KzflPxh9IMow5SCIOenfQQ - B0ZTVSm7n0nf5OgtGsioizNFmqZqytZMvb+KE87vo9JMgwCwkxJyMDCNRcDjtg47LpWTPLyvds9N - genkLU0q5xbDAgMBAAECggEAFTRcMg1DfdnPRKR4yxa7skvN4tbtKgW2alTmsrMLeOAqgdcSfbuj - kDfhW4VkNn0f17GVVM7Dy7Qvzl8uMY9/ZdVXjWwzYzOZNmxIl1Tf8/mNnnhBGNNm4SWgl2BrWJx6 - XEMhLdMO0W2deRfjikE5u7zShOZFZAZcasKE1Q62Il2ylaPgY1mLfCSSUXozJULMzEL2SSRhYT2+ - rLAdhH2+dmX5cvgTHEBX4f+zRoVEtf141YAOY5qv6eu4aDJnjcSz2yD8DAnH52qDx1oGq5mQRScb - Y2wvnQOCr+KU47EN/4bVpukOsB/xe5G9ybbJigIaHjererENXD2dbfYyRHXR8QKBgQDK8f/Buf0r - cPjyZjzRhcMw71H0Ur8wT8OeLM5Zup3j2e5QjebJpAIUDTRznySa1Vez7JsDNTjNfoVYek3sEeCj - 0tBS0NGEviHUCfukAyv8OIH/woj+XEcCUn8sqr9VXVpSXRr40hud82Vd18/NGIpLtZwyRVFmzasg - teWhfQPJkwKBgQC9JzVtB8oXhGJ73RKZ528Ezd3ATySpcspnGWujq4Tu1UWj2mY1LTdudWcah3I0 - QwbLQCqeUQZoa+EiYzPZMaXPPih2QyJo59oOU37iougweIbzPgsJpGKxWxG7lBObu2k7YNVFas4T - IepngkcKNjkNC0DHV4buZVI67pAl7B78EQKBgQDF2Fnu8HRhL0dieEz+LZr2T7jjqO9+F6SqxR99 - 1jIqeMCdg1jkZqEoDx99QD4dO7K+UwFjhTUVECzK7qCcbWlEDDbPJYe8EudDoV/Sqszsm+IQBgQr - hKYtG2OjlenlPJbbCK1MuPf3adr+O2/3j97yo9/cGjubLxGPWAS/A/L3RQKBgQCaxc9gdIQ3M/rF - sUH8LrPXsX+mUNwFzsixDcrWtIzkRBxkk1sYXfRCbMw9l+CpxMJ1Yv68Zj4hCUzBL30IVih/aDQB - eLNaNYRmPonPdk8ZAjYiKH0tmZWr24GqA+L7haD4liZMU7VlUFYV9jKct3t9Id0Sf5sHzF45nGTU - st0zkQKBgQCcPDI4dRJAeFyUiaggdqZLl8rdJWVxUEov8q/UZm4+nyy/uz4SV0owfXCQrncvmsRo - OLddwAnDT1aBWo9qxkvNEtLd+jwmI3oZqwM8UtjH2SHok7D5ZQ+PwO+A2jNhOvo3T+OqpQ9v0X9Q - +5dLqqZlucrBJ12EccMRWkoZ1DIoig== - -----END PRIVATE KEY----- - - public-key: | - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfO/CoX9EzhskpAKX9ADH0wfEQZP4rAq - ptqq80W2YaOHAnXu+oU1UrP0b9ccKKSzMCVDwdmXrecZB0dFLPnoazMEbVOf6MSwrhGfxupPRmmJ - sIYgmQwo8/vnjaq/GYFfnHyCy6yKL41G+GZVqgeKdhr+w1jUw4L9Fs0l2J/AYqwTxZOnzxrU5erP - GSE5Dd3AwWt/brxuwA7sRfVS3mbbsYyYExjUrEbst8VtF3Pis35T8YfSDKMOUgiDnp30EAdGU1Up - u59J3+ToLRrIqIszRZqmasrWTL2/ihPO76PSTIMAsJMScjAwjUXA47YOOy6Vkzy8r3bPTYHp5C1N - KucWwwIDAQAB - -----END PUBLIC KEY----- - + # RSA 키는 환경 변수로 관리 (.env 파일 또는 서버 환경 변수) + private-key: ${JWT_PRIVATE_KEY} + public-key: ${JWT_PUBLIC_KEY} header: Authorization prefix: Bearer access: @@ -130,3 +93,9 @@ springdoc: swagger-ui: use-root-path: true +# PortOne V2 설정 (환경 변수로 관리) +portone: + public: + store-id: ${PORTONE_STORE_ID} + channel-key: ${PORTONE_CHANNEL_KEY} + api_secret: ${PORTONE_API_SECRET} diff --git a/src/main/resources/test.html b/src/main/resources/test.html new file mode 100644 index 0000000..6b8d5c4 --- /dev/null +++ b/src/main/resources/test.html @@ -0,0 +1,192 @@ + + + + + + ReTrip 통합 테스트 (V3) + + + + + + + +
+

ReTrip 기능 테스트

+ +
+

⚙️ Step 0: PortOne 설정

+ + + +
+ +
+

👤 Step 1: 회원가입

+ + + +
+ + +
+ +
+
+ +
+

🔐 Step 2: 로그인

+ +
+
+ +
+

✨ Step 3: 본인인증 (PortOne V2)

+ +
+
+ +
+

📋 Step 4: 내 프로필 조회

+ +
+
+ +
+

✏️ Step 5: 프로필 수정 (추가 정보)

+ + +
+ + + + + + +
+ +
+
+
+ + + + \ No newline at end of file