diff --git a/.env b/.env index e69de29b..84794420 100644 --- a/.env +++ b/.env @@ -0,0 +1 @@ +NEIS_API_KEY=8ba18e6d2b1f42f9be762009bc3dfa90 \ No newline at end of file diff --git a/build.gradle b/build.gradle index fe1f37f9..f90377af 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.projectlombok:lombok:1.18.30' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-validation' + testImplementation 'io.projectreactor:reactor-test' annotationProcessor 'org.projectlombok:lombok:1.18.30' developmentOnly 'org.springframework.boot:spring-boot-devtools' @@ -53,6 +56,9 @@ dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' } ext { diff --git a/postgres-data/base/1/1259 b/postgres-data/base/1/1259 index fb803a0c..89ee6759 100644 Binary files a/postgres-data/base/1/1259 and b/postgres-data/base/1/1259 differ diff --git a/postgres-data/base/1/13398 b/postgres-data/base/1/13398 index ea663efd..e23293e4 100644 Binary files a/postgres-data/base/1/13398 and b/postgres-data/base/1/13398 differ diff --git a/postgres-data/base/1/2619 b/postgres-data/base/1/2619 index 9071b779..f7df77fb 100644 Binary files a/postgres-data/base/1/2619 and b/postgres-data/base/1/2619 differ diff --git a/postgres-data/base/1/2619_fsm b/postgres-data/base/1/2619_fsm index 56048e28..8d826819 100644 Binary files a/postgres-data/base/1/2619_fsm and b/postgres-data/base/1/2619_fsm differ diff --git a/postgres-data/base/1/2619_vm b/postgres-data/base/1/2619_vm index 72289c3a..422be57c 100644 Binary files a/postgres-data/base/1/2619_vm and b/postgres-data/base/1/2619_vm differ diff --git a/postgres-data/base/1/2696 b/postgres-data/base/1/2696 index e0a0fd3e..f6d2bf8f 100644 Binary files a/postgres-data/base/1/2696 and b/postgres-data/base/1/2696 differ diff --git a/postgres-data/base/1/2840 b/postgres-data/base/1/2840 index dfa22ab0..364ba2ff 100644 Binary files a/postgres-data/base/1/2840 and b/postgres-data/base/1/2840 differ diff --git a/postgres-data/base/1/2840_fsm b/postgres-data/base/1/2840_fsm index d7622ece..0ae2a105 100644 Binary files a/postgres-data/base/1/2840_fsm and b/postgres-data/base/1/2840_fsm differ diff --git a/postgres-data/base/1/2840_vm b/postgres-data/base/1/2840_vm index 843576c7..759e8a99 100644 Binary files a/postgres-data/base/1/2840_vm and b/postgres-data/base/1/2840_vm differ diff --git a/postgres-data/base/1/2841 b/postgres-data/base/1/2841 index 44178ce5..cd9c438a 100644 Binary files a/postgres-data/base/1/2841 and b/postgres-data/base/1/2841 differ diff --git a/postgres-data/base/16384/1247 b/postgres-data/base/16384/1247 index 43c682b6..27045d3f 100644 Binary files a/postgres-data/base/16384/1247 and b/postgres-data/base/16384/1247 differ diff --git a/postgres-data/base/16384/1249 b/postgres-data/base/16384/1249 index 54e0c287..d82716c7 100644 Binary files a/postgres-data/base/16384/1249 and b/postgres-data/base/16384/1249 differ diff --git a/postgres-data/base/16384/1259 b/postgres-data/base/16384/1259 index 42e0830e..d386f653 100644 Binary files a/postgres-data/base/16384/1259 and b/postgres-data/base/16384/1259 differ diff --git a/postgres-data/base/16384/13398 b/postgres-data/base/16384/13398 index 6dc600ee..f81417ba 100644 Binary files a/postgres-data/base/16384/13398 and b/postgres-data/base/16384/13398 differ diff --git a/postgres-data/base/16384/16389 b/postgres-data/base/16384/16385 similarity index 98% rename from postgres-data/base/16384/16389 rename to postgres-data/base/16384/16385 index db7ecc31..ad479bd4 100644 Binary files a/postgres-data/base/16384/16389 and b/postgres-data/base/16384/16385 differ diff --git a/postgres-data/base/16384/16394 b/postgres-data/base/16384/16386 similarity index 100% rename from postgres-data/base/16384/16394 rename to postgres-data/base/16384/16386 diff --git a/postgres-data/base/16384/16390 b/postgres-data/base/16384/16390 index 9545552e..e69de29b 100644 Binary files a/postgres-data/base/16384/16390 and b/postgres-data/base/16384/16390 differ diff --git a/postgres-data/base/16384/16395 b/postgres-data/base/16384/16391 similarity index 99% rename from postgres-data/base/16384/16395 rename to postgres-data/base/16384/16391 index 1ad208dc..800548d9 100644 Binary files a/postgres-data/base/16384/16395 and b/postgres-data/base/16384/16391 differ diff --git a/postgres-data/base/16384/16392 b/postgres-data/base/16384/16392 new file mode 100644 index 00000000..6e32bf81 Binary files /dev/null and b/postgres-data/base/16384/16392 differ diff --git a/postgres-data/base/16384/16396 b/postgres-data/base/16384/16396 deleted file mode 100644 index 3898243b..00000000 Binary files a/postgres-data/base/16384/16396 and /dev/null differ diff --git a/postgres-data/base/16384/2224 b/postgres-data/base/16384/2224 index 2b37f2e5..097e8257 100644 Binary files a/postgres-data/base/16384/2224 and b/postgres-data/base/16384/2224 differ diff --git a/postgres-data/base/16384/2579 b/postgres-data/base/16384/2579 index 3629d910..6c46b147 100644 Binary files a/postgres-data/base/16384/2579 and b/postgres-data/base/16384/2579 differ diff --git a/postgres-data/base/16384/2604 b/postgres-data/base/16384/2604 index aef48396..6e4176eb 100644 Binary files a/postgres-data/base/16384/2604 and b/postgres-data/base/16384/2604 differ diff --git a/postgres-data/base/16384/2606 b/postgres-data/base/16384/2606 index fb966869..1c5cdad4 100644 Binary files a/postgres-data/base/16384/2606 and b/postgres-data/base/16384/2606 differ diff --git a/postgres-data/base/16384/2608 b/postgres-data/base/16384/2608 index d9b58e46..38e36e6c 100644 Binary files a/postgres-data/base/16384/2608 and b/postgres-data/base/16384/2608 differ diff --git a/postgres-data/base/16384/2610 b/postgres-data/base/16384/2610 index e1eca7a5..49510e8e 100644 Binary files a/postgres-data/base/16384/2610 and b/postgres-data/base/16384/2610 differ diff --git a/postgres-data/base/16384/2619 b/postgres-data/base/16384/2619 index 3c4090dd..1bf322f3 100644 Binary files a/postgres-data/base/16384/2619 and b/postgres-data/base/16384/2619 differ diff --git a/postgres-data/base/16384/2656 b/postgres-data/base/16384/2656 index aa430d1c..f8e35df4 100644 Binary files a/postgres-data/base/16384/2656 and b/postgres-data/base/16384/2656 differ diff --git a/postgres-data/base/16384/2657 b/postgres-data/base/16384/2657 index 457dbfd9..859563df 100644 Binary files a/postgres-data/base/16384/2657 and b/postgres-data/base/16384/2657 differ diff --git a/postgres-data/base/16384/2658 b/postgres-data/base/16384/2658 index c673e51e..ae1c70bf 100644 Binary files a/postgres-data/base/16384/2658 and b/postgres-data/base/16384/2658 differ diff --git a/postgres-data/base/16384/2659 b/postgres-data/base/16384/2659 index 696004d7..dad706fe 100644 Binary files a/postgres-data/base/16384/2659 and b/postgres-data/base/16384/2659 differ diff --git a/postgres-data/base/16384/2662 b/postgres-data/base/16384/2662 index 4c571117..35ac1efa 100644 Binary files a/postgres-data/base/16384/2662 and b/postgres-data/base/16384/2662 differ diff --git a/postgres-data/base/16384/2663 b/postgres-data/base/16384/2663 index 64ccba76..101dfbdc 100644 Binary files a/postgres-data/base/16384/2663 and b/postgres-data/base/16384/2663 differ diff --git a/postgres-data/base/16384/2664 b/postgres-data/base/16384/2664 index 641fdefc..341268c7 100644 Binary files a/postgres-data/base/16384/2664 and b/postgres-data/base/16384/2664 differ diff --git a/postgres-data/base/16384/2665 b/postgres-data/base/16384/2665 index d82fcfc1..ada304eb 100644 Binary files a/postgres-data/base/16384/2665 and b/postgres-data/base/16384/2665 differ diff --git a/postgres-data/base/16384/2666 b/postgres-data/base/16384/2666 index 2aaafbb7..7f948213 100644 Binary files a/postgres-data/base/16384/2666 and b/postgres-data/base/16384/2666 differ diff --git a/postgres-data/base/16384/2667 b/postgres-data/base/16384/2667 index 42bc10a6..209db618 100644 Binary files a/postgres-data/base/16384/2667 and b/postgres-data/base/16384/2667 differ diff --git a/postgres-data/base/16384/2673 b/postgres-data/base/16384/2673 index 307eb922..06a9a0bf 100644 Binary files a/postgres-data/base/16384/2673 and b/postgres-data/base/16384/2673 differ diff --git a/postgres-data/base/16384/2674 b/postgres-data/base/16384/2674 index 449f87e4..1750e287 100644 Binary files a/postgres-data/base/16384/2674 and b/postgres-data/base/16384/2674 differ diff --git a/postgres-data/base/16384/2678 b/postgres-data/base/16384/2678 index 43e16244..24210a35 100644 Binary files a/postgres-data/base/16384/2678 and b/postgres-data/base/16384/2678 differ diff --git a/postgres-data/base/16384/2679 b/postgres-data/base/16384/2679 index 417b3376..de72697e 100644 Binary files a/postgres-data/base/16384/2679 and b/postgres-data/base/16384/2679 differ diff --git a/postgres-data/base/16384/2703 b/postgres-data/base/16384/2703 index db246eca..1d3fc0e4 100644 Binary files a/postgres-data/base/16384/2703 and b/postgres-data/base/16384/2703 differ diff --git a/postgres-data/base/16384/2704 b/postgres-data/base/16384/2704 index 3066e22c..fc0bcfae 100644 Binary files a/postgres-data/base/16384/2704 and b/postgres-data/base/16384/2704 differ diff --git a/postgres-data/base/16384/3455 b/postgres-data/base/16384/3455 index c8eed24a..2b94b693 100644 Binary files a/postgres-data/base/16384/3455 and b/postgres-data/base/16384/3455 differ diff --git a/postgres-data/base/16384/5002 b/postgres-data/base/16384/5002 index 8e3945a7..12c79ac7 100644 Binary files a/postgres-data/base/16384/5002 and b/postgres-data/base/16384/5002 differ diff --git a/postgres-data/base/16384/pg_internal.init b/postgres-data/base/16384/pg_internal.init index 5b048079..d96a14e9 100644 Binary files a/postgres-data/base/16384/pg_internal.init and b/postgres-data/base/16384/pg_internal.init differ diff --git a/postgres-data/base/4/13398 b/postgres-data/base/4/13398 index ea663efd..e23293e4 100644 Binary files a/postgres-data/base/4/13398 and b/postgres-data/base/4/13398 differ diff --git a/postgres-data/base/4/2619 b/postgres-data/base/4/2619 index 139f78a6..b599129a 100644 Binary files a/postgres-data/base/4/2619 and b/postgres-data/base/4/2619 differ diff --git a/postgres-data/base/5/13398 b/postgres-data/base/5/13398 index ea663efd..e23293e4 100644 Binary files a/postgres-data/base/5/13398 and b/postgres-data/base/5/13398 differ diff --git a/postgres-data/base/5/2619 b/postgres-data/base/5/2619 index 139f78a6..b599129a 100644 Binary files a/postgres-data/base/5/2619 and b/postgres-data/base/5/2619 differ diff --git a/postgres-data/global/1260 b/postgres-data/global/1260 index 7c23dbeb..c10f7e8e 100644 Binary files a/postgres-data/global/1260 and b/postgres-data/global/1260 differ diff --git a/postgres-data/global/pg_control b/postgres-data/global/pg_control index b46730dc..31e55ffb 100644 Binary files a/postgres-data/global/pg_control and b/postgres-data/global/pg_control differ diff --git a/postgres-data/global/pg_internal.init b/postgres-data/global/pg_internal.init index 3e89494d..5edae6d2 100644 Binary files a/postgres-data/global/pg_internal.init and b/postgres-data/global/pg_internal.init differ diff --git a/postgres-data/pg_wal/000000010000000000000001 b/postgres-data/pg_wal/000000010000000000000001 index 6d72abb0..103aa2d9 100644 Binary files a/postgres-data/pg_wal/000000010000000000000001 and b/postgres-data/pg_wal/000000010000000000000001 differ diff --git a/postgres-data/pg_xact/0000 b/postgres-data/pg_xact/0000 index 6915b7da..16fb66af 100644 Binary files a/postgres-data/pg_xact/0000 and b/postgres-data/pg_xact/0000 differ diff --git a/postgres-data/postmaster.pid b/postgres-data/postmaster.pid index 1996cb5c..7dc19679 100644 --- a/postgres-data/postmaster.pid +++ b/postgres-data/postmaster.pid @@ -1,8 +1,8 @@ 1 /var/lib/postgresql/data -1750235928 +1751931356 5432 /var/run/postgresql * - 5 0 + 13 0 ready diff --git a/src/main/java/hello/cluebackend/domain/timetable/presentation/TimetableController.java b/src/main/java/hello/cluebackend/domain/timetable/presentation/TimetableController.java new file mode 100644 index 00000000..c00e3f05 --- /dev/null +++ b/src/main/java/hello/cluebackend/domain/timetable/presentation/TimetableController.java @@ -0,0 +1,47 @@ +package hello.cluebackend.domain.timetable.presentation; + +import hello.cluebackend.domain.timetable.presentation.dto.request.TimetableRequestDto; +import hello.cluebackend.domain.timetable.presentation.dto.response.TimetableResponseDto; +import hello.cluebackend.domain.timetable.service.TimetableService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.List; + +@RestController +@RequestMapping("/api/timetable") +@RequiredArgsConstructor +public class TimetableController { + private final TimetableService timetableService; + + @GetMapping("/test") + public String test(){ + return "Test"; + } + + @GetMapping("/today") + public Mono>> getTodayTimetable( + @RequestParam(required = true) String grade, + @RequestParam(required = true) String classNumber +) { + TimetableRequestDto request = new TimetableRequestDto(grade, classNumber); + return timetableService.getTodayTimetable(request) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.noContent().build()) + .onErrorResume(e -> Mono.just(ResponseEntity.internalServerError().build())); + } + + @GetMapping("/weekly") + public Mono>> getWeeklyTimetable( + @RequestParam(required = true) String grade, + @RequestParam(required = true) String classNumber + ) { + TimetableRequestDto request = new TimetableRequestDto(grade, classNumber); + return timetableService.getWeeklyTimetable(request) + .map(ResponseEntity::ok) + .defaultIfEmpty(ResponseEntity.noContent().build()) + .onErrorResume(e -> Mono.just(ResponseEntity.internalServerError().build())); + } +} \ No newline at end of file diff --git a/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/request/TimetableRequestDto.java b/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/request/TimetableRequestDto.java new file mode 100644 index 00000000..beb6f233 --- /dev/null +++ b/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/request/TimetableRequestDto.java @@ -0,0 +1,28 @@ +package hello.cluebackend.domain.timetable.presentation.dto.request; + +import hello.cluebackend.domain.timetable.service.TimetableService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.processing.Pattern; + +@Getter +public class TimetableRequestDto { + @NotBlank(message = "학년은 필수입니다.") + @Min(value = 1) + @Max(value = 3) + private String grade; + + @NotBlank(message = "반은 필수입니다.") + @Min(value = 1) + @Max(value = 4) + private String classNumber; + + public TimetableRequestDto(String grade, String classNumber) { + this.grade = grade; + this.classNumber = classNumber; + } +} diff --git a/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/response/TimetableResponseDto.java b/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/response/TimetableResponseDto.java new file mode 100644 index 00000000..616cfd30 --- /dev/null +++ b/src/main/java/hello/cluebackend/domain/timetable/presentation/dto/response/TimetableResponseDto.java @@ -0,0 +1,43 @@ +package hello.cluebackend.domain.timetable.presentation.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.util.Locale; +import java.util.Map; + +@Getter @Setter +@NoArgsConstructor +public class TimetableResponseDto { + + private String period; // 교시 + private String subject; // 과목 이름 + private String date; // 날짜 + private String dayOfWeek; // 요일 + + public static TimetableResponseDto fromMap(Map map) { + TimetableResponseDto dto = new TimetableResponseDto(); + + dto.period = (String) map.getOrDefault("PERIO", ""); + dto.subject = (String) map.getOrDefault("ITRT_CNTNT", ""); + dto.date = (String) map.getOrDefault("ALL_TI_YMD", ""); + + if (!dto.date.isEmpty() && dto.date.length() == 8) { + try { + LocalDate localDate = LocalDate.parse(dto.date, DateTimeFormatter.ofPattern("yyyyMMdd")); + DayOfWeek dayOfWeek = localDate.getDayOfWeek(); + dto.dayOfWeek = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN); + } catch (Exception e) { + dto.dayOfWeek = ""; + } + }else{ + dto.dayOfWeek="error"; + } + return dto; + } +} \ No newline at end of file diff --git a/src/main/java/hello/cluebackend/domain/timetable/service/TimetableService.java b/src/main/java/hello/cluebackend/domain/timetable/service/TimetableService.java new file mode 100644 index 00000000..6642081b --- /dev/null +++ b/src/main/java/hello/cluebackend/domain/timetable/service/TimetableService.java @@ -0,0 +1,176 @@ +package hello.cluebackend.domain.timetable.service; + +import hello.cluebackend.domain.timetable.presentation.dto.request.TimetableRequestDto; +import hello.cluebackend.domain.timetable.presentation.dto.response.TimetableResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TimetableService { + + // Constants + private static final String TIMETABLE_PATH = "/hisTimetable"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); + private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("MM"); + private static final ZoneId KOREA_TIMEZONE = ZoneId.of("Asia/Seoul"); + private static final int FIRST_SEMESTER_END_MONTH = 8; + private static final String FIRST_SEMESTER = "1"; + private static final String SECOND_SEMESTER = "2"; + private static final int API_TIMEOUT_SECONDS = 10; + private static final int RETRY_ATTEMPTS = 2; + private static final Duration RETRY_DELAY = Duration.ofSeconds(1); + + @Value("${spring.neis.api.host}") + private String neisApiHost; + + @Value("${spring.neis.api.key}") + private String neisApiKey; + + @Value("${spring.neis.api.atpt-code:C10}") + private String atptCode; + + @Value("${spring.neis.api.school-code:7150658}") + private String schoolCode; + + private final WebClient webClient; + + public Mono> getTodayTimetable(TimetableRequestDto request) { + LocalDate today = LocalDate.now(KOREA_TIMEZONE); + String todayStr = today.format(DATE_FORMATTER); + + log.info("오늘 시간표 조회 요청 - Grade: {}, Class: {}, Date: {}", + request.getGrade(), request.getClassNumber(), todayStr); + + return fetchTimetable(request, todayStr, todayStr); + } + + public Mono> getWeeklyTimetable(TimetableRequestDto request) { + LocalDate now = LocalDate.now(KOREA_TIMEZONE); + String from = now.with(java.time.DayOfWeek.MONDAY).format(DATE_FORMATTER); + String to = now.with(java.time.DayOfWeek.FRIDAY).format(DATE_FORMATTER); + + log.info("주간 시간표 조회 요청 - Grade: {}, Class: {}, Period: {} ~ {}", + request.getGrade(), request.getClassNumber(), from, to); + + return fetchTimetable(request, from, to); + } + + private Mono> fetchTimetable(TimetableRequestDto request, String from, String to) { + LocalDate now = LocalDate.now(KOREA_TIMEZONE); + String year = now.format(YEAR_FORMATTER); + String semester = determineSemester(now); + + log.debug("NEIS API 호출 파라미터 - Year: {}, Semester: {}, From: {}, To: {}", + year, semester, from, to); + + return webClient.get() + .uri("https://" + neisApiHost + TIMETABLE_PATH + + "?KEY=" + neisApiKey + + "&Type=json" + + "&pIndex=1" + + "&pSize=100" + + "&ATPT_OFCDC_SC_CODE=" + atptCode + + "&SD_SCHUL_CODE=" + schoolCode + + "&AY=" + year + + "&SEM=" + semester + + "&GRADE=" + request.getGrade() + + "&CLASS_NM=" + request.getClassNumber() + + "&TI_FROM_YMD=" + from + + "&TI_TO_YMD=" + to) +// .uri(uriBuilder -> uriBuilder +// .scheme("https") +// .host(neisApiHost) +// .path(TIMETABLE_PATH) +// .queryParam("KEY", neisApiKey) +// .queryParam("Type", "json") +// .queryParam("pIndex", "1") +// .queryParam("pSize", "100") +// .queryParam("ATPT_OFCDC_SC_CODE", atptCode) +// .queryParam("SD_SCHUL_CODE", schoolCode) +// .queryParam("AY", year) +// .queryParam("SEM", semester) +// .queryParam("GRADE", request.getGrade()) +// .queryParam("CLASS_NM", request.getClassNumber()) +// .queryParam("TI_FROM_YMD", from) +// .queryParam("TI_TO_YMD", to) +// .build()) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, + response -> response.bodyToMono(String.class) + .flatMap(body -> { + log.error("NEIS API 클라이언트 오류 - Status: {}, Body: {}", + response.statusCode(), body); + return Mono.error(new IllegalArgumentException("잘못된 요청: " + body)); + })) + .onStatus(HttpStatusCode::is5xxServerError, + response -> { + log.error("NEIS API 서버 오류 - Status: {}", response.statusCode()); + return Mono.error(new RuntimeException("NEIS 서버 오류 발생")); + }) + .bodyToMono(new ParameterizedTypeReference>() {}) + .timeout(Duration.ofSeconds(API_TIMEOUT_SECONDS)) + .retryWhen(Retry.backoff(RETRY_ATTEMPTS, RETRY_DELAY) + .filter(throwable -> !(throwable instanceof WebClientResponseException.BadRequest)) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + log.error("NEIS API 재시도 횟수 초과"); + return new RuntimeException("NEIS API 호출 실패 - 호출 시간 초과"); + })) + .map(this::parseTimetableResponse) + .doOnSuccess(result -> log.info("시간표 조회 성공 - 결과 개수: {}", result.size())) + .doOnError(error -> log.error("시간표 조회 실패", error)); + } + + private String determineSemester(LocalDate date) { + int month = Integer.parseInt(date.format(MONTH_FORMATTER)); + return (month <= FIRST_SEMESTER_END_MONTH) ? FIRST_SEMESTER : SECOND_SEMESTER; + } + + private List parseTimetableResponse(Map response) { + try { + return Optional.ofNullable(response.get("hisTimetable")) + .filter(List.class::isInstance) + .map(obj -> (List) obj) + .filter(list -> list.size() >= 2) + .map(list -> list.get(1)) + .filter(Map.class::isInstance) + .map(obj -> (Map) obj) + .map(map -> map.get("row")) + .filter(List.class::isInstance) + .map(obj -> (List) obj) + .map(this::convertToTimetableResponseList) + .orElse(List.of()); + } catch (Exception e) { + log.error("시간표 파싱 오류", e); + return List.of(); + } + } + + @SuppressWarnings("unchecked") + private List convertToTimetableResponseList(List rowList) { + return rowList.stream() + .filter(Map.class::isInstance) + .map(item -> (Map) item) + .map(TimetableResponseDto::fromMap) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/hello/cluebackend/domain/user/domain/repository/UserRepository.java b/src/main/java/hello/cluebackend/domain/user/domain/repository/UserRepository.java index 4a193cfa..20343ca3 100644 --- a/src/main/java/hello/cluebackend/domain/user/domain/repository/UserRepository.java +++ b/src/main/java/hello/cluebackend/domain/user/domain/repository/UserRepository.java @@ -8,7 +8,6 @@ @Repository public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); Optional findByEmail(String email); diff --git a/src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java b/src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java index 92b40e2b..f41a3537 100644 --- a/src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java +++ b/src/main/java/hello/cluebackend/domain/user/presentation/RegisterController.java @@ -13,7 +13,6 @@ @RestController public class RegisterController { - private final RegisterUserService registerUserService; public RegisterController(RegisterUserService registerUserService) { diff --git a/src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java b/src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java index b67d1326..595396e2 100644 --- a/src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java +++ b/src/main/java/hello/cluebackend/domain/user/service/RegisterUserService.java @@ -14,7 +14,13 @@ public RegisterUserService(UserRepository userRepository) { } public void registerUser(DefaultRegisterUserDTO userDTO) { - UserEntity userEntity = new UserEntity(userDTO.getStudentId(), userDTO.getUsername(), userDTO.getStudentId()+userDTO.getUsername(), userDTO.getEmail(), userDTO.getRole()); + UserEntity userEntity = new UserEntity( + userDTO.getStudentId(), + userDTO.getUsername(), + userDTO.getStudentId()+userDTO.getUsername(), + userDTO.getEmail(), + userDTO.getRole() + ); userRepository.save(userEntity); } } diff --git a/src/main/java/hello/cluebackend/global/config/SecurityConfig.java b/src/main/java/hello/cluebackend/global/config/SecurityConfig.java index c33f3a4b..afdfe5be 100644 --- a/src/main/java/hello/cluebackend/global/config/SecurityConfig.java +++ b/src/main/java/hello/cluebackend/global/config/SecurityConfig.java @@ -74,14 +74,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, RefreshTokenSe @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); + configuration.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000")); configuration.setAllowedMethods(Collections.singletonList("*")); - configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Collections.singletonList("*")); - configuration.setMaxAge(3600L); configuration.setExposedHeaders(Collections.singletonList("Authorization")); - + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); return configuration; } })); @@ -116,12 +114,12 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { // 경로별 인가 작업 http .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "refresh-token", "/register", "/first-register").permitAll() + .requestMatchers("/", "/refresh-token", "/register", "/first-register", "/api/timetable/**").permitAll() .anyRequest().authenticated()); http - .securityMatcher("/first-register", "/register") + .securityMatcher("/first-register", "/register","api/timetable/**") .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) diff --git a/src/main/java/hello/cluebackend/global/config/WebClientConfig.java b/src/main/java/hello/cluebackend/global/config/WebClientConfig.java new file mode 100644 index 00000000..55d4dc43 --- /dev/null +++ b/src/main/java/hello/cluebackend/global/config/WebClientConfig.java @@ -0,0 +1,46 @@ +package hello.cluebackend.global.config; + +import org.springframework.context.annotation.Configuration; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +@Configuration +public class WebClientConfig { + private static final int TIMEOUT_MS = 60000; + + @Bean + public WebClient webClient() { + return WebClient.builder() + .baseUrl("https://api.example.com") + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .build(); + } + + @Bean + public WebClient advancedWebClient() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, TIMEOUT_MS) + .doOnConnected(conn -> conn + .addHandlerLast(new ReadTimeoutHandler(TIMEOUT_MS)) + .addHandlerLast(new WriteTimeoutHandler(TIMEOUT_MS) + ) + ) + .responseTimeout(Duration.ofSeconds(TIMEOUT_MS)); + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .baseUrl("https://api.example.com") + .build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 678601c4..7862172c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,7 +2,7 @@ spring: application: name: CLUE-Backend datasource: - url: jdbc:postgresql://my_postgres:5432/clue + url: jdbc:postgresql://localhost:5432/clue username: clue1234 password: clue52025 driver-class-name: org.postgresql.Driver @@ -34,6 +34,12 @@ spring: provider: google: authorization-uri: https://accounts.google.com/o/oauth2/v2/auth?prompt=consent + + neis: + api: + host: open.neis.go.kr/hub + key: 03d0657812b64b87b5ab5fc6945001c9 + server: address: 0.0.0.0 port: 8080 \ No newline at end of file diff --git a/src/test/java/hello/cluebackend/ClueBackendApplicationTests.java b/src/test/java/hello/cluebackend/ClueBackendApplicationTests.java deleted file mode 100644 index 15646622..00000000 --- a/src/test/java/hello/cluebackend/ClueBackendApplicationTests.java +++ /dev/null @@ -1,9 +0,0 @@ -package hello.cluebackend; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ClueBackendApplicationTests { - -}