‼️ 주의 : 절대로 노출시키지 마십시요.

resources/application-secret.properties

spring.docker.compose.enabled=false

# 콘솔 색깔 보이게 설정
spring.output.ansi.enabled=always

# 1. DB Connection URL
spring.datasource.url=jdbc:postgresql://localhost:5432/maplestory_db

# 2. DB Name, Password
spring.datasource.username=postgres
spring.datasource.password=fje3t2jBQ304s

# 3. PostgreSQL Driver
spring.datasource.driver-class-name=org.postgresql.Driver

# 4. JPA & Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.highlight_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# 5. Nexon API Key
nexon.api.key=test_a14ee3aab189780facbc961d999d503b6e70132855a81231c17caeeb780d4a42efe8d04e6d233bd35cf2fabdeb93fb0d
nexon.api.base-url=https://open.api.nexon.com/maplestory/v1

# 6. Timezone 설정 (한국 서울)
spring.jackson.time-zone=Asia/Seoul

나중에 제거하기

resources/application-secret.yml

서버용 (아래 내용은 절대 절대 노출되면 안 됩니다.)

spring:
  config:
    activate:
      on-profile: secret

  datasource:
    url: jdbc:mysql://k12d102.p.ssafy.io/:3306/TAKEN
    username: TAKEN
    password: ssafyD102!
    driver-class-name: com.mysql.cj.jdbc.Driver
  data:
    redis:
      password: ssafyD102!
      
jwt:
  secret: "1e4b6c8d6f1a9ae9bb7472c8d54892fe21d26a396c7f2d5cf7e3a98b1f7a8db3"
  access-token-expiration: 86400 # 1일
  refresh-token-expiration: 2592000 # 30일 (86400 * 30)
  token-prefix: "Bearer "
  header-string: "Authorization"

springdoc:
  swagger-ui:
    path: /api/taken/swagger
    url: /api/v3/api-docs
  api-docs:
    path: /api/v3/api-docs

cloud:
  aws:
    credentials:
      access-key: AKIAZBRTHJF3AUDFXXP7
      secret-key: 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
    region:
      static: ap-northeast-2
      auto: false
    s3:
      bucket: boda-taken-bucket
    stack:
      auto: false
      
firebase:
 service-account-key-path: /app/fcm/boda.json
 
kakao:
 client-id: "c1e05f5bc2e6c7b109159615df0e3b34"  # 카카오 REST API 키
 redirect-uri: "<https://k12d102.p.ssafy.io/api/oauth/kakao>"  # 카카오 로그인 완료 후 리다이렉트될 URI
 
kakao-map:
  api-key: "KakaoAK 67d5193c667f375e6698e2078ec62d1d"

resources/application-secret.yml

로컬용

spring:
  config:
    activate:
      on-profile: secret

  datasource:
    url: jdbc:mysql://localhost:3306/[데이터베이스명]
    username: [mysql 유저명]
    password: [mysql 비밀번호]
    driver-class-name: com.mysql.cj.jdbc.Driver
    
jwt:
  secret: "1e4b6c8d6f1a9ae9bb7472c8d54892fe21d26a396c7f2d5cf7e3a98b1f7a8db3"
  access-token-expiration: 86400 # 1일
  refresh-token-expiration: 2592000 # 30일 (86400 * 30)
  token-prefix: "Bearer "
  header-string: "Authorization"

springdoc:
  swagger-ui:
    path: /api/taken/swagger
    url: /api/v3/api-docs
  api-docs:
    path: /api/v3/api-docs

cloud:
  aws:
    credentials:
      access-key: AKIAZBRTHJF3AUDFXXP7
      secret-key: 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
    region:
      static: ap-northeast-2
      auto: false
    s3:
      bucket: boda-taken-bucket
    stack:
      auto: false
      
firebase:
 service-account-key-path: ./src/main/resources/fcm/boda-90475-firebase-adminsdk-fbsvc-df2dcd8c1e.json
 
kakao:
  client-id: "c1e05f5bc2e6c7b109159615df0e3b34"  # 카카오 REST API 키
  redirect-uri: "<http://localhost:5173/oauth/kakao>"  # 카카오 로그인 완료 후 리다이렉트될 URI
  
kakao-map:
  api-key: "KakaoAK 67d5193c667f375e6698e2078ec62d1d"
	public String getKakaoToken(String code) {
		try {
			log.info("카카오 토큰 요청 시작. code: {}", code);

			String tokenUri = KAKAO_TOKEN_URI;
			// 중요: application.yml에서 주입받은 kakaoRedirectUri가 네이티브 앱용인지 확인 필요!
			// 네이티브 앱의 경우 보통 "kakao{네이티브_앱_키}://oauth" 형식이거나, 이 파라미터를 보내지 않음.
			String redirectUri = kakaoRedirectUri;

			log.info("--- 카카오 액세스 토큰 요청 파라미터 ---");
			log.info("요청 URL: {}", tokenUri);
			log.info("grant_type: authorization_code");
			log.info("client_id: {}", kakaoClientId);
			log.info("redirect_uri: {}", redirectUri); // 실제 사용될 redirect_uri 값 로깅
			log.info("code: {}", code);
			log.info("------------------------------------");

			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

			MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
			params.add("grant_type", "authorization_code");
			params.add("client_id", kakaoClientId);
			params.add("redirect_uri", redirectUri);
			params.add("code", code);

			HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);

			try {
				log.info("카카오 액세스 토큰 요청 헤더 (전송 전): {}", objectMapper.writeValueAsString(requestEntity.getHeaders()));
				log.info("카카오 액세스 토큰 요청 바디 (전송 전): {}", objectMapper.writeValueAsString(requestEntity.getBody()));
			} catch (JsonProcessingException e) {
				log.warn("카카오 토큰 요청 정보 로깅(JSON 변환) 실패", e);
			}
			log.info("카카오 액세스 토큰 POST 요청 전송 시작: {}", tokenUri);

			RestTemplate restTemplate = new RestTemplate();
			ResponseEntity<String> responseEntity;

			try {
				responseEntity = restTemplate.postForEntity(tokenUri, requestEntity, String.class);

				log.info("--- 카카오 액세스 토큰 응답 (성공) ---");
				log.info("HTTP Status: {}", responseEntity.getStatusCode());
				log.info("Response Headers: {}", responseEntity.getHeaders());
				log.info("Response Body: {}", responseEntity.getBody());
				log.info("------------------------------------");

				JsonNode responseJson = objectMapper.readTree(responseEntity.getBody());
				String accessToken = responseJson.path("access_token").asText(null);

				if (accessToken == null) {
					log.error("카카오 응답에서 access_token을 찾을 수 없습니다. 응답 본문: {}", responseEntity.getBody());
					throw new RuntimeException("카카오 응답에서 access_token을 찾을 수 없습니다.");
				}
				log.info("카카오 액세스 토큰 추출 성공. Token Length: {}", accessToken.length());
				return accessToken;

			} catch (HttpClientErrorException | HttpServerErrorException e) {
				log.error("--- 카카오 액세스 토큰 요청 실패 (HTTP 오류) ---");
				log.error("HTTP Status: {}", e.getStatusCode());
				log.error("Response Headers: {}", e.getResponseHeaders());
				log.error("Response Body: {}", e.getResponseBodyAsString(), e);
				log.error("---------------------------------------------");
				throw new RuntimeException("카카오 토큰 요청 중 HTTP 오류 발생: " + e.getStatusCode() + " - " + e.getResponseBodyAsString());
			} catch (Exception e) { // JSON 파싱 오류 등 RestTemplate 호출 후의 다른 예외
				log.error("카카오 토큰 응답 처리 중 예외 발생.", e);
				throw new RuntimeException("카카오 토큰 응답 처리 중 예외가 발생했습니다.", e);
			}

		} catch (Exception e) { // getKakaoToken 메소드 전체를 감싸는 최상위 예외 처리
			log.error("getKakaoToken 메소드 실행 중 최종 오류 발생.", e);
			throw new RuntimeException("카카오 토큰을 가져오는 과정에서 심각한 오류가 발생했습니다.", e);
		}
	}

	public LoginResponseDto kakaoLogin(String code) throws IOException {
		log.info("카카오 로그인 로직 시작. Authorization Code: {}", code);
		try {
			String accessToken = getKakaoToken(code);
			if (!StringUtils.hasText(accessToken)) {
				log.error("getKakaoToken으로부터 유효한 액세스 토큰을 받지 못했습니다.");
				throw new RuntimeException("카카오 액세스 토큰을 얻지 못했습니다.");
			}
			log.info("카카오 액세스 토큰 성공적으로 획득. Token Length: {}", accessToken.length());

			String memberInfoUri = KAKAO_USER_INFO_URI;
			HttpHeaders userInfoHeaders = new HttpHeaders();
			userInfoHeaders.setBearerAuth(accessToken);
			// Kakao API v2 사용자 정보 요청 시 Content-type 헤더는 필수가 아님 (보통 GET 요청)
			// userInfoHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

			HttpEntity<?> userInfoRequestEntity = new HttpEntity<>(userInfoHeaders);

			log.info("--- 카카오 사용자 정보 요청 준비 ---");
			log.info("요청 URL: {}", memberInfoUri);
			try {
				log.info("요청 Headers: {}", objectMapper.writeValueAsString(userInfoRequestEntity.getHeaders()));
			} catch (JsonProcessingException e) {
				log.warn("카카오 사용자 정보 요청 헤더 로깅(JSON 변환) 실패", e);
			}
			log.info("카카오 사용자 정보 GET 요청 전송 시작");
			log.info("----------------------------------");

				RestTemplate restTemplate = new RestTemplate();
			ResponseEntity<String> userInfoResponseEntity;

			try {
				userInfoResponseEntity = restTemplate.exchange(
					memberInfoUri,
					HttpMethod.GET,
					userInfoRequestEntity,
					String.class
				);
				log.info("--- 카카오 사용자 정보 응답 (성공) ---");
				log.info("HTTP Status: {}", userInfoResponseEntity.getStatusCode());
				log.info("Response Headers: {}", userInfoResponseEntity.getHeaders());
				log.info("Response Body: {}", userInfoResponseEntity.getBody());
				log.info("------------------------------------");

			} catch (HttpClientErrorException | HttpServerErrorException e) {
				log.error("--- 카카오 사용자 정보 요청 실패 (HTTP 오류) ---");
				log.error("HTTP Status: {}", e.getStatusCode());
				log.error("Response Headers: {}", e.getResponseHeaders());
				log.error("Response Body: {}", e.getResponseBodyAsString(), e);
				log.error("---------------------------------------------");
				throw new RuntimeException("카카오 사용자 정보 요청 중 HTTP 오류 발생: " + e.getStatusCode() + " - " + e.getResponseBodyAsString());
			} catch (Exception e) { // JSON 파싱 오류 등 RestTemplate 호출 후의 다른 예외
				log.error("카카오 사용자 정보 응답 처리 중 예외 발생.", e);
				throw new RuntimeException("카카오 사용자 정보 응답 처리 중 예외가 발생했습니다.", e);
			}

			JsonNode json = objectMapper.readTree(userInfoResponseEntity.getBody());
			Long kakaoId = json.get("id").asLong();
			// 카카오 정책에 따라 이메일 제공 여부 및 기본값 처리 필요
			String email = json.path("kakao_account").path("email").asText(null);
			if (email == null) { // 이메일 동의 안했거나 없는 경우 대체 이메일 생성
				email = "kakao_" + kakaoId + "@boda.com"; // 서비스 정책에 맞게 수정
				log.warn("카카오로부터 이메일 정보를 받지 못했습니다. 대체 이메일 사용: {}", email);
			}
			String nickname = json.path("kakao_account").path("profile").path("nickname").asText("BODA 사용자");

				log.info("파싱된 카카오 사용자 정보 - ID: {}, Email: {}, Nickname: {}", kakaoId, email, nickname);

			String finalEmail = email;
			Member member = memberRepository.findByProviderAndProviderId("kakao", kakaoId)
						.orElseGet(() -> {
							log.info("새로운 카카오 사용자 등록 시도. KakaoID: {}", kakaoId);
								String profileImageUrl = json.path("kakao_account").path("profile").path("profile_image_url").asText(DEFAULT_PROFILE_IMAGE_URL);
							Member newMember = Member.builder()
								.email(finalEmail)
								.nickname(nickname)
								// .password(null) // 소셜 로그인이므로 비밀번호는 null
								// .phoneNo(null) // 필요 시 추가 수집
								.points(0)
								.profile(profileImageUrl)
								.provider("kakao")
								.providerId(kakaoId)
								.build();
							return memberRepository.save(newMember);
						});
			log.info("기존 또는 신규 카카오 회원 처리 완료. MemberID: {}", member.getMemberId());

				TokenResponseDto tokens = jwtService.issueToken(member);
			log.info("자체 JWT 토큰 발급 성공. MemberID: {}", member.getMemberId());

			return LoginResponseDto.builder()
				.email(member.getEmail())
				.nickname(member.getNickname())
				.address(member.getAddress())
				.points(member.getPoints())
				.profile(member.getProfile())
				.accessToken(tokens.getAccessToken())
				.refreshToken(tokens.getRefreshToken())
				.build();

		} catch (IOException e) {
			log.error("카카오 로그인 중 데이터 처리(I/O) 오류 발생.", e);
			throw new RuntimeException("카카오 로그인 데이터 처리 중 오류가 발생했습니다.", e);
		} catch (RuntimeException e) {
			log.error("카카오 로그인 중 런타임 예외 발생: {}", e.getMessage(), e);
			throw e;
		} catch (Exception e) {
			log.error("카카오 로그인 중 예기치 못한 최종 오류 발생.", e);
			throw new RuntimeException("카카오 로그인 처리 중 예기치 못한 최종 오류가 발생했습니다.", e);
		}
	}

boda-90475-firebase-adminsdk-fbsvc-df2dcd8c1e.json