diff --git a/build.gradle b/build.gradle index 26f3aeb2..fa277c10 100644 --- a/build.gradle +++ b/build.gradle @@ -41,16 +41,17 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3' implementation 'org.springframework.boot:spring-boot-starter-webflux' - //MQTT implementation 'org.springframework.boot:spring-boot-starter-integration' - implementation 'org.springframework.integration:spring-integration-mqtt' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'com.google.firebase:firebase-admin:9.2.0' - - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation 'org.springframework.cloud:spring-cloud-starter-aws-messaging:2.2.6.RELEASE' + + implementation 'com.squareup.okhttp3:okhttp:4.11.0' + implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.11.0' // 추가 + // Jsoup (HTML 파싱) + implementation 'org.jsoup:jsoup:1.18.1' + } tasks.named('test') { diff --git a/src/main/java/com/example/smartair/EnjoyBackApplication.java b/src/main/java/com/example/enjoy/EnjoyBackApplication.java similarity index 95% rename from src/main/java/com/example/smartair/EnjoyBackApplication.java rename to src/main/java/com/example/enjoy/EnjoyBackApplication.java index ca6f1d03..0adb26b9 100644 --- a/src/main/java/com/example/smartair/EnjoyBackApplication.java +++ b/src/main/java/com/example/enjoy/EnjoyBackApplication.java @@ -1,4 +1,4 @@ -package com.example.smartair; +package com.example.enjoy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/com/example/smartair/config/RestTemplateConfig.java b/src/main/java/com/example/enjoy/config/RestTemplateConfig.java similarity index 89% rename from src/main/java/com/example/smartair/config/RestTemplateConfig.java rename to src/main/java/com/example/enjoy/config/RestTemplateConfig.java index 34aad318..8f96169d 100644 --- a/src/main/java/com/example/smartair/config/RestTemplateConfig.java +++ b/src/main/java/com/example/enjoy/config/RestTemplateConfig.java @@ -1,13 +1,14 @@ -package com.example.smartair.config; +package com.example.enjoy.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; + @Configuration public class RestTemplateConfig { + @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } - diff --git a/src/main/java/com/example/enjoy/config/SecurityConfig.java b/src/main/java/com/example/enjoy/config/SecurityConfig.java new file mode 100644 index 00000000..74b92af6 --- /dev/null +++ b/src/main/java/com/example/enjoy/config/SecurityConfig.java @@ -0,0 +1,44 @@ +package com.example.enjoy.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll()); // 모든 요청 허용 + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/config/SwaggerConfig.java b/src/main/java/com/example/enjoy/config/SwaggerConfig.java similarity index 52% rename from src/main/java/com/example/smartair/config/SwaggerConfig.java rename to src/main/java/com/example/enjoy/config/SwaggerConfig.java index 47df87b3..5ad63887 100644 --- a/src/main/java/com/example/smartair/config/SwaggerConfig.java +++ b/src/main/java/com/example/enjoy/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package com.example.smartair.config; +package com.example.enjoy.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; @@ -14,33 +14,31 @@ public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { - // Security Scheme 정의 - SecurityScheme securityScheme = new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") + // API Key Scheme 정의 + SecurityScheme apiKeyScheme = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER) - .name("Authorization"); + .name("Authorization") + .description("인증을 위한 토큰"); // Security Requirement 정의 - SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth"); + SecurityRequirement securityRequirement = new SecurityRequirement().addList("ApiKeyAuth"); - // 서버 목록 정의 + // 서버 정의 Server localServer = new Server(); localServer.setUrl("http://localhost:8080"); - localServer.setDescription("Local server (HTTP)"); + localServer.setDescription("Local Server"); - Server prodServer = new Server(); - prodServer.setUrl("https://smartair.site"); - prodServer.setDescription("Production server (HTTPS)"); + Server apiServer = new Server(); + apiServer.setUrl("http://3.36.34.67:8080"); + apiServer.setDescription("API Server"); return new OpenAPI() .info(new Info().title("Enjoy Hack API") .description("EnjoyHack Application API Documentation") .version("v1.0")) - .addSecurityItem(securityRequirement) // Security Requirement 추가 - .schemaRequirement("BearerAuth", securityScheme) // Security Scheme 추가 - .servers(List.of(localServer, prodServer)); // 서버 목록 추가 + .addSecurityItem(securityRequirement) + .schemaRequirement("ApiKeyAuth", apiKeyScheme) + .servers(List.of(localServer, apiServer)); // 로컬서버와 운영서버 모두 등록 } - -} +} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/config/WebClientConfig.java b/src/main/java/com/example/enjoy/config/WebClientConfig.java similarity index 90% rename from src/main/java/com/example/smartair/config/WebClientConfig.java rename to src/main/java/com/example/enjoy/config/WebClientConfig.java index 3c5b8730..ac41db6f 100644 --- a/src/main/java/com/example/smartair/config/WebClientConfig.java +++ b/src/main/java/com/example/enjoy/config/WebClientConfig.java @@ -1,4 +1,4 @@ -package com.example.smartair.config; +package com.example.enjoy.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/example/smartair/config/corsMvcConfig.java b/src/main/java/com/example/enjoy/config/corsMvcConfig.java similarity index 93% rename from src/main/java/com/example/smartair/config/corsMvcConfig.java rename to src/main/java/com/example/enjoy/config/corsMvcConfig.java index 45d994bd..b72f884f 100644 --- a/src/main/java/com/example/smartair/config/corsMvcConfig.java +++ b/src/main/java/com/example/enjoy/config/corsMvcConfig.java @@ -1,4 +1,4 @@ -package com.example.smartair.config; +package com.example.enjoy.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; diff --git a/src/main/java/com/example/smartair/controller/HealthCheckController.java b/src/main/java/com/example/enjoy/controller/HealthCheckController.java similarity index 89% rename from src/main/java/com/example/smartair/controller/HealthCheckController.java rename to src/main/java/com/example/enjoy/controller/HealthCheckController.java index 5aed254e..9df97447 100644 --- a/src/main/java/com/example/smartair/controller/HealthCheckController.java +++ b/src/main/java/com/example/enjoy/controller/HealthCheckController.java @@ -1,4 +1,4 @@ -package com.example.smartair.controller; +package com.example.enjoy.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/example/enjoy/controller/userController/LoginController.java b/src/main/java/com/example/enjoy/controller/userController/LoginController.java new file mode 100644 index 00000000..0cfdcdb3 --- /dev/null +++ b/src/main/java/com/example/enjoy/controller/userController/LoginController.java @@ -0,0 +1,41 @@ +package com.example.enjoy.controller.userController; + +import com.example.enjoy.dto.loginDto.MemberCommand; +import com.example.enjoy.dto.loginDto.MemberDto; +import com.example.enjoy.service.loginService.SejongLoginService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "로그인", description = "로그인 관련 API") +@RestController +@Slf4j +@AllArgsConstructor +@RequestMapping("/api/auth/sejong") +public class LoginController { + private final SejongLoginService sejongLoginService; + + /** + * 세종대학교 포털 로그인 및 사용자 정보 조회 + * 포털 로그인 -> 고전독서 사이트 SSO 인증 -> 사용자 정보 파싱 및 반환 + */ + @PostMapping("/login") + public ResponseEntity loginAndGetUserInfo(@RequestBody @Valid MemberCommand command) { + try { + log.info("세종대 포털 로그인 요청: {}", command.getSejongPortalId()); + MemberDto memberInfo = sejongLoginService.getMemberAuthInfos(command); + log.info("사용자 정보 조회 성공: {}", memberInfo.getStudentName()); + return ResponseEntity.ok(memberInfo); + } catch (Exception e) { + log.error("세종대 포털 로그인 및 정보 조회 실패: {}", e.getMessage(), e); + throw new RuntimeException("세종대 포털 인증 실패", e); + } + } + +} diff --git a/src/main/java/com/example/enjoy/dto/loginDto/AuthRequest.java b/src/main/java/com/example/enjoy/dto/loginDto/AuthRequest.java new file mode 100644 index 00000000..3f37034a --- /dev/null +++ b/src/main/java/com/example/enjoy/dto/loginDto/AuthRequest.java @@ -0,0 +1,17 @@ +package com.example.enjoy.dto.loginDto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AuthRequest { + private String id; + private String pw; + private String method; +} + diff --git a/src/main/java/com/example/enjoy/dto/loginDto/AuthResponseDto.java b/src/main/java/com/example/enjoy/dto/loginDto/AuthResponseDto.java new file mode 100644 index 00000000..06eff4e4 --- /dev/null +++ b/src/main/java/com/example/enjoy/dto/loginDto/AuthResponseDto.java @@ -0,0 +1,24 @@ +package com.example.enjoy.dto.loginDto; + +public class AuthResponseDto { + private String msg; + private Result result; + private String code; + private Boolean is_auth; + private Integer status_code; + private Boolean success; + private String version; + + public static class Result { + private String authenticator; + private Body body; + // getters/setters + } + + public static class Body { + private String major; + private String name; + // getters/setters + } + // getters/setters +} diff --git a/src/main/java/com/example/enjoy/dto/loginDto/MemberCommand.java b/src/main/java/com/example/enjoy/dto/loginDto/MemberCommand.java new file mode 100644 index 00000000..d83476c6 --- /dev/null +++ b/src/main/java/com/example/enjoy/dto/loginDto/MemberCommand.java @@ -0,0 +1,12 @@ +package com.example.enjoy.dto.loginDto; + +import lombok.Getter; +import org.springframework.stereotype.Service; + +@Getter +@Service +public class MemberCommand { + String sejongPortalId; + String sejongPortalPassword; + +} diff --git a/src/main/java/com/example/enjoy/dto/loginDto/MemberDto.java b/src/main/java/com/example/enjoy/dto/loginDto/MemberDto.java new file mode 100644 index 00000000..03a23a60 --- /dev/null +++ b/src/main/java/com/example/enjoy/dto/loginDto/MemberDto.java @@ -0,0 +1,15 @@ +package com.example.enjoy.dto.loginDto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class MemberDto { + private String major; + private String studentIdString; + private String studentName; + private String academicYear; + private String enrollmentStatus; + +} diff --git a/src/main/java/com/example/smartair/entity/BaseTimeEntity.java b/src/main/java/com/example/enjoy/entity/BaseTimeEntity.java similarity index 91% rename from src/main/java/com/example/smartair/entity/BaseTimeEntity.java rename to src/main/java/com/example/enjoy/entity/BaseTimeEntity.java index a65cd943..9f629dfb 100644 --- a/src/main/java/com/example/smartair/entity/BaseTimeEntity.java +++ b/src/main/java/com/example/enjoy/entity/BaseTimeEntity.java @@ -1,6 +1,5 @@ -package com.example.smartair.entity; +package com.example.enjoy.entity; -import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; diff --git a/src/main/java/com/example/smartair/entity/user/User.java b/src/main/java/com/example/enjoy/entity/user/User.java similarity index 80% rename from src/main/java/com/example/smartair/entity/user/User.java rename to src/main/java/com/example/enjoy/entity/user/User.java index 3202adb0..0ea12e95 100644 --- a/src/main/java/com/example/smartair/entity/user/User.java +++ b/src/main/java/com/example/enjoy/entity/user/User.java @@ -1,12 +1,10 @@ -package com.example.smartair.entity.user; +package com.example.enjoy.entity.user; -import com.example.smartair.entity.BaseTimeEntity; +import com.example.enjoy.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.*; -import java.util.Set; - @Builder @Entity @Getter @@ -30,8 +28,6 @@ public class User extends BaseTimeEntity { private String providerId; //소셜 로그인 시 제공자 ID - private Role role; - } diff --git a/src/main/java/com/example/smartair/exception/CustomException.java b/src/main/java/com/example/enjoy/exception/CustomException.java similarity index 90% rename from src/main/java/com/example/smartair/exception/CustomException.java rename to src/main/java/com/example/enjoy/exception/CustomException.java index b61b839a..72682d89 100644 --- a/src/main/java/com/example/smartair/exception/CustomException.java +++ b/src/main/java/com/example/enjoy/exception/CustomException.java @@ -1,4 +1,4 @@ -package com.example.smartair.exception; +package com.example.enjoy.exception; import lombok.Getter; diff --git a/src/main/java/com/example/smartair/exception/ErrorCode.java b/src/main/java/com/example/enjoy/exception/ErrorCode.java similarity index 69% rename from src/main/java/com/example/smartair/exception/ErrorCode.java rename to src/main/java/com/example/enjoy/exception/ErrorCode.java index 078b25f1..4f38c873 100644 --- a/src/main/java/com/example/smartair/exception/ErrorCode.java +++ b/src/main/java/com/example/enjoy/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package com.example.smartair.exception; +package com.example.enjoy.exception; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,7 +12,10 @@ public enum ErrorCode { USER_JOIN_INFO_BAD_REQUEST(HttpStatus.BAD_REQUEST, "회원가입 정보가 잘못되었습니다"), USER_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 존재하는 사용자입니다"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다"), - USER_PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다"); + USER_PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다"), + SEJONG_AUTH_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "세종대학교 인증 서버와의 연결에 실패했습니다"), + SEJONG_AUTH_CREDENTIALS_INVALID(HttpStatus.UNAUTHORIZED, "세종대학교 인증 정보가 유효하지 않습니다"), + SEJONG_AUTH_DATA_FETCH_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "세종대학교 인증 데이터 가져오기 실패"); private final String message; private final int status; diff --git a/src/main/java/com/example/smartair/exception/GlobalExceptionHandler.java b/src/main/java/com/example/enjoy/exception/GlobalExceptionHandler.java similarity index 96% rename from src/main/java/com/example/smartair/exception/GlobalExceptionHandler.java rename to src/main/java/com/example/enjoy/exception/GlobalExceptionHandler.java index 61e9fd5f..c1569468 100644 --- a/src/main/java/com/example/smartair/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/enjoy/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.example.smartair.exception; +package com.example.enjoy.exception; import lombok.extern.slf4j.Slf4j; @@ -7,7 +7,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.server.ResponseStatusException; -import java.util.Collections; import java.util.HashMap; @RestControllerAdvice diff --git a/src/main/java/com/example/enjoy/service/loginService/SejongLoginService.java b/src/main/java/com/example/enjoy/service/loginService/SejongLoginService.java new file mode 100644 index 00000000..a8fae12c --- /dev/null +++ b/src/main/java/com/example/enjoy/service/loginService/SejongLoginService.java @@ -0,0 +1,279 @@ +package com.example.enjoy.service.loginService; + + +import com.example.enjoy.dto.loginDto.MemberCommand; +import com.example.enjoy.dto.loginDto.MemberDto; +import com.example.enjoy.exception.CustomException; +import com.example.enjoy.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.springframework.stereotype.Service; +import okhttp3.JavaNetCookieJar; +import javax.net.ssl.*; +import java.io.IOException; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SejongLoginService { + + /** + * 세종포털 인증을 통해 사용자 정보를 가져옵니다 + */ + public MemberDto getMemberAuthInfos(MemberCommand command) { + String sejongPortalId = command.getSejongPortalId(); + String sejongPortalPw = command.getSejongPortalPassword(); + + try { + // OkHttpClient 생성 + OkHttpClient client = buildClient(); + + // 포털 로그인 요청 + doPortalLogin(client, sejongPortalId, sejongPortalPw); + + // 포털 -> 고전독서 SSO 시작점 접근 + String portalClassicLinkUrl = "https://portal.sejong.ac.kr/html/classic/classic.html"; + Request portalReq = new Request.Builder() + .url(portalClassicLinkUrl) + .get() + .build(); + try (Response portalResp = executeWithRetry(client, portalReq)) { + log.debug("포털 링크 응답: {}", portalResp.code()); + } + + // SSO 리다이렉트 처리 + String ssoUrl = "https://classic.sejong.ac.kr/_custom/sejong/sso/sso-return.jsp?returnUrl=https://classic.sejong.ac.kr/classic/index.do"; + Request ssoReq = new Request.Builder() + .url(ssoUrl) + .get() + .header("Referer", "https://portal.sejong.ac.kr/") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .build(); + + try (Response ssoResp = executeWithRetry(client, ssoReq)) { + log.debug("SSO 응답: {} - 쿠키 수: {}", + ssoResp.code(), + client.cookieJar().loadForRequest(HttpUrl.get(ssoUrl)).size()); + + if (!ssoResp.isSuccessful()) { + throw new CustomException(ErrorCode.SEJONG_AUTH_CONNECTION_ERROR, "SSO 리다이렉트 실패"); + } + } + + // 메인 페이지 방문 (중요: 세션 쿠키 설정을 위해) + String mainUrl = "https://classic.sejong.ac.kr/classic/index.do"; + Request mainReq = new Request.Builder() + .url(mainUrl) + .get() + .header("Referer", ssoUrl) + .build(); + + try (Response mainResp = executeWithRetry(client, mainReq)) { + log.debug("메인 페이지 응답: {}", mainResp.code()); + if (!mainResp.isSuccessful()) { + throw new CustomException(ErrorCode.SEJONG_AUTH_CONNECTION_ERROR, "메인 페이지 접근 실패"); + } + } + + // 고전독서인증현황 페이지 GET + String html = fetchReadingStatusHtml(client); + + // HTML 파싱 및 정보 추출 + return parseHTMLAndGetMemberInfo(html); + + } catch (IOException e) { + log.error("포털 인증 중 오류 발생: {}", e.getMessage(), e); + throw new CustomException(ErrorCode.SEJONG_AUTH_CONNECTION_ERROR, "포털 인증 중 오류 발생"); + } + } + + + /** + * 세종포털에 ID/PW로 로그인 + */ + private void doPortalLogin(OkHttpClient client, String studentId, String password) throws IOException { + String loginUrl = "https://portal.sejong.ac.kr/jsp/login/login_action.jsp"; + + RequestBody formBody = new FormBody.Builder() + .add("mainLogin", "N") + .add("rtUrl", "") // 빈 값으로 변경하여 리디렉션 동작 확인 + .add("id", studentId) + .add("password", password) + .build(); + + Request request = new Request.Builder() + .url(loginUrl) + .post(formBody) + .header("Host", "portal.sejong.ac.kr") + .header("Origin", "https://portal.sejong.ac.kr") + .header("Referer", "https://portal.sejong.ac.kr/jsp/login/login.jsp") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .build(); + + try (Response response = executeWithRetry(client, request)) { + log.debug("포털 로그인 응답: {}", response.code()); + String responseBody = response.body() != null ? response.body().string() : ""; + + if (responseBody.contains("alert") && responseBody.contains("로그인")) { + throw new CustomException(ErrorCode.SEJONG_AUTH_CREDENTIALS_INVALID, "포털 로그인 실패"); + } + } + } + + /** + * 고전독서인증현황 페이지 HTML 가져오기 + */ + private String fetchReadingStatusHtml(OkHttpClient client) throws IOException { + String statusUrl = "https://classic.sejong.ac.kr/classic/reading/status.do"; + + // 요청 전 쿠키 상태 확인 + logCookies(client, statusUrl); + + Request request = new Request.Builder() + .url(statusUrl) + .get() + .header("Referer", "https://classic.sejong.ac.kr/classic/index.do") + .build(); + + try (Response response = client.newCall(request).execute()) { + log.debug("고전독서인증현황 응답: {}", response.code()); + + if (response.code() != 200 || response.body() == null) { + throw new CustomException(ErrorCode.SEJONG_AUTH_DATA_FETCH_ERROR, + "고전독서인증현황 페이지 조회 실패: " + response); + } + + return response.body().string(); + } + } + + /** + * 디버깅용: 쿠키 정보 출력 + */ + private void logCookies(OkHttpClient client, String url) { + List cookies = client.cookieJar().loadForRequest(HttpUrl.get(url)); + log.debug("URL {} 쿠키 정보 ({}):", url, cookies.size()); + for (Cookie cookie : cookies) { + log.debug(" - {}: {}", cookie.name(), cookie.value()); + } + } + + /** + * 고전독서인증현황 페이지 파싱 + */ + private MemberDto parseHTMLAndGetMemberInfo(String html) { + Document doc = Jsoup.parse(html); + + String selector = ".b-con-box:has(h4.b-h4-tit01:contains(사용자 정보)) table.b-board-table tbody tr"; + List rowValues = new ArrayList<>(); + + doc.select(selector).forEach(tr -> { + String value = tr.select("td").text().trim(); + rowValues.add(value); + }); + + String major = getValueFromList(rowValues, 0); + String studentId = getValueFromList(rowValues, 1); + String studentName = getValueFromList(rowValues, 2); + String year = getValueFromList(rowValues, 3); + String status = getValueFromList(rowValues, 4); + + return MemberDto.builder() + .major(major) + .studentIdString(studentId) + .studentName(studentName) + .academicYear(year) + .enrollmentStatus(status) + .build(); + } + + /** + * List에서 안전하게 값 가져오기 + */ + private String getValueFromList(List list, int index) { + return list.size() > index ? list.get(index) : null; + } + + /** + * 재시도 로직이 포함된 요청 실행 + */ + private Response executeWithRetry(OkHttpClient client, Request request) throws IOException { + int tryCount = 0; + int maxRetries = 3; + + while (tryCount < maxRetries) { + try { + Response response = client.newCall(request).execute(); + if (response.isSuccessful() || tryCount == maxRetries - 1) { + return response; + } + response.close(); + } catch (SocketTimeoutException e) { + if (tryCount == maxRetries - 1) { + throw e; + } + } + tryCount++; + log.debug("요청 재시도 ({}/{}): {}", tryCount, maxRetries, request.url()); + } + throw new IOException("최대 재시도 횟수 초과: " + request.url()); + } + + private OkHttpClient buildClient() { + try { + // SSLContext 생성, 모든 인증서 신뢰 설정 + SSLContext sslCtx = SSLContext.getInstance("SSL"); + sslCtx.init(null, new TrustManager[]{trustAllManager()}, new java.security.SecureRandom()); + SSLSocketFactory sslFactory = sslCtx.getSocketFactory(); + + // hostnameVerifier: 모든 호스트네임에 대해 OK 처리 + HostnameVerifier hostnameVerifier = (hostname, session) -> true; + + // OkHttp 로깅 인터셉터 +// HttpLoggingInterceptor logging = new HttpLoggingInterceptor(log::info); +// logging.setLevel(HttpLoggingInterceptor.Level.BODY); + + // 쿠키 관리 + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + + // OkHttpClient 생성 + return new OkHttpClient.Builder() + .sslSocketFactory(sslFactory, trustAllManager()) + .hostnameVerifier(hostnameVerifier) + .cookieJar(new JavaNetCookieJar(cookieManager)) +// .addInterceptor(logging) + .build(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 모든 서버 인증서를 신뢰하는 X509TrustManager 구현 + */ + private X509TrustManager trustAllManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {} + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {} + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[0]; + } + }; + } +} + + diff --git a/src/main/java/com/example/smartair/util/BaseEntity.java b/src/main/java/com/example/enjoy/util/BaseEntity.java similarity index 87% rename from src/main/java/com/example/smartair/util/BaseEntity.java rename to src/main/java/com/example/enjoy/util/BaseEntity.java index 6203e346..93048641 100644 --- a/src/main/java/com/example/smartair/util/BaseEntity.java +++ b/src/main/java/com/example/enjoy/util/BaseEntity.java @@ -1,11 +1,10 @@ -package com.example.smartair.util; +package com.example.enjoy.util; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; diff --git a/src/main/java/com/example/smartair/controller/userController/LoginController.java b/src/main/java/com/example/smartair/controller/userController/LoginController.java deleted file mode 100644 index 70f1181c..00000000 --- a/src/main/java/com/example/smartair/controller/userController/LoginController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.smartair.controller.userController; - -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "로그인", description = "로그인 관련 API") -@RestController -@Slf4j -@AllArgsConstructor -public class LoginController { - - - - - -// @Operation( -// summary = "로그인", -// description = "이메일과 비밀번호로 로그인하여 액세스 토큰과 리프레시 토큰을 받습니다.", -// responses = { -// @ApiResponse( -// responseCode = "200", -// description = "로그인 성공", -// content = @Content( -// schema = @Schema(implementation = TokenDto.class) -// ) -// ) -// } -// ) -// @PostMapping("/login") -// public ResponseEntity login(@RequestBody LoginDTO loginRequestDto) { -// TokenDto tokenDto = loginService.login(loginRequestDto); -// // 응답 본문에 TokenDto를 직접 반환 -// return ResponseEntity.ok() -// .contentType(MediaType.APPLICATION_JSON) -// .body(tokenDto); -// } - -} diff --git a/src/main/java/com/example/smartair/entity/login/CustomUserDetails.java b/src/main/java/com/example/smartair/entity/login/CustomUserDetails.java deleted file mode 100644 index cff40303..00000000 --- a/src/main/java/com/example/smartair/entity/login/CustomUserDetails.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.smartair.entity.login; - -import com.example.smartair.entity.user.User; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.ArrayList; -import java.util.Collection; - -public class CustomUserDetails implements UserDetails { // 시큐리티에서 사용자 정보를 담는 인터페이스 - private final User user; - public CustomUserDetails(User user) { - this.user = user; - } - - @Override - public Collection getAuthorities() { - - Collection authorities = new ArrayList<>(); - - authorities.add(new GrantedAuthority() { - @Override - public String getAuthority() { - return String.valueOf(user.getRole()); - } - }); - return authorities; - } - - public User getUser(){ - return user; - } - @Override - public String getPassword() { - - return user.getPassword(); - } - @Override - public String getUsername() { - - return user.getUsername(); - } - public String getEmail() { - - return user.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - - return true; - } - - @Override - public boolean isAccountNonLocked() { - - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - - return true; - } - - @Override - public boolean isEnabled() { - - return true; - } -} diff --git a/src/main/java/com/example/smartair/entity/login/RefreshEntity.java b/src/main/java/com/example/smartair/entity/login/RefreshEntity.java deleted file mode 100644 index 968fd0d7..00000000 --- a/src/main/java/com/example/smartair/entity/login/RefreshEntity.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.smartair.entity.login; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.Getter; -import lombok.Setter; - -@Entity -@Getter -@Setter -public class RefreshEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String username; - private String refresh; - private String expiration; -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/entity/user/Role.java b/src/main/java/com/example/smartair/entity/user/Role.java deleted file mode 100644 index aadb9dd3..00000000 --- a/src/main/java/com/example/smartair/entity/user/Role.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.smartair.entity.user; - -import lombok.Getter; - -@Getter -public enum Role { - ADMIN("ROLE_ADMIN"), - MANAGER("ROLE_MANAGER"), - USER("ROLE_USER"); - - Role(String value){ - this.value = value; - } - private String value; -} diff --git a/src/test/java/com/example/smartair/EnjoyBackApplicationTests.java b/src/test/java/com/example/enjoy/EnjoyBackApplicationTests.java similarity index 86% rename from src/test/java/com/example/smartair/EnjoyBackApplicationTests.java rename to src/test/java/com/example/enjoy/EnjoyBackApplicationTests.java index 272e880b..7ff8d2f4 100644 --- a/src/test/java/com/example/smartair/EnjoyBackApplicationTests.java +++ b/src/test/java/com/example/enjoy/EnjoyBackApplicationTests.java @@ -1,4 +1,4 @@ -package com.example.smartair; +package com.example.enjoy; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;