diff --git a/src/main/java/side/onetime/exception/status/FixedErrorStatus.java b/src/main/java/side/onetime/exception/status/FixedErrorStatus.java index ad06d0e..564b5f5 100644 --- a/src/main/java/side/onetime/exception/status/FixedErrorStatus.java +++ b/src/main/java/side/onetime/exception/status/FixedErrorStatus.java @@ -1,8 +1,9 @@ package side.onetime.exception.status; +import org.springframework.http.HttpStatus; + import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import side.onetime.global.common.code.BaseErrorCode; import side.onetime.global.common.dto.ErrorReasonDto; @@ -10,9 +11,10 @@ @RequiredArgsConstructor public enum FixedErrorStatus implements BaseErrorCode { _NOT_FOUND_FIXED_SCHEDULES(HttpStatus.NOT_FOUND, "FIXED-001", "고정 스케줄 목록을 가져오는 데 실패했습니다."), - _NOT_FOUND_EVERYTIME_TIMETABLE(HttpStatus.NOT_FOUND, "FIXED-002", "에브리타임 시간표를 가져오는 데 실패했습니다. 공개 범위를 확인해주세요."), + _EVERYTIME_TIMETABLE_NOT_PUBLIC(HttpStatus.NOT_FOUND, "FIXED-002", "에브리타임 시간표를 가져오는 데 실패했습니다. 공개 범위를 확인해주세요."), _EVERYTIME_TIMETABLE_PARSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FIXED-003", "에브리타임 시간표 파싱 중 문제가 발생했습니다."), _EVERYTIME_API_FAILED(HttpStatus.SERVICE_UNAVAILABLE, "FIXED-004", "에브리타임 API 연동 중 서버 오류가 발생했습니다."), + _NOT_FOUND_EVERYTIME_TIMETABLE(HttpStatus.NOT_FOUND, "FIXED-005", "에브리타임 시간표에 등록된 수업이 없습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/side/onetime/service/FixedScheduleService.java b/src/main/java/side/onetime/service/FixedScheduleService.java index 0e0a1f2..7661178 100644 --- a/src/main/java/side/onetime/service/FixedScheduleService.java +++ b/src/main/java/side/onetime/service/FixedScheduleService.java @@ -6,6 +6,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jsoup.Jsoup; @@ -35,6 +37,11 @@ @Service @RequiredArgsConstructor public class FixedScheduleService { + + private static final int EVERYTIME_PRIVATE_STATUS = -2; + private static final int EVERYTIME_PUBLIC_STATUS = 1; + private static final Pattern STATUS_PATTERN = Pattern.compile("status=\"(-?\\d+)\""); + private final UserRepository userRepository; private final FixedScheduleRepository fixedScheduleRepository; private final FixedSelectionRepository fixedSelectionRepository; @@ -132,13 +139,44 @@ private String fetchTimetableXml(String identifier) { } if (!xmlResponse.contains("subject")) { - // 200 OK 응답이 왔지만, 테이블이 비어있는 경우 (공개 범위 설정 오류 등) - throw new CustomException(FixedErrorStatus._NOT_FOUND_EVERYTIME_TIMETABLE); + // 200 OK 응답이 왔지만, 테이블이 비어있는 경우 + int status = extractStatusFromXml(xmlResponse); + if (EVERYTIME_PRIVATE_STATUS == status) { + // 1. 공개 범위가 '전체 공개'가 아닌 경우 + throw new CustomException(FixedErrorStatus._EVERYTIME_TIMETABLE_NOT_PUBLIC); + } else if (EVERYTIME_PUBLIC_STATUS == status) { + // 2. '전체 공개'이지만, 등록된 수업이 없는 경우 + throw new CustomException(FixedErrorStatus._NOT_FOUND_EVERYTIME_TIMETABLE); + } else { + // 3. 예상치 못한 status 값 + throw new CustomException(FixedErrorStatus._EVERYTIME_TIMETABLE_PARSE_ERROR); + } } return xmlResponse; } + /** + * XML 문자열에서 status 속성값을 추출합니다. + * 예: -> 1 반환 + */ + private int extractStatusFromXml(String xml) { + // status="숫자" 패턴을 찾음 + Matcher matcher = STATUS_PATTERN.matcher(xml); + + if (matcher.find()) { + try { + return Integer.parseInt(matcher.group(1)); + } catch (NumberFormatException e) { + // 숫자가 아닌 경우 파싱 에러 처리 + throw new CustomException(FixedErrorStatus._EVERYTIME_TIMETABLE_PARSE_ERROR); + } + } + + // status 속성을 찾지 못한 경우 파싱 에러 처리 + throw new CustomException(FixedErrorStatus._EVERYTIME_TIMETABLE_PARSE_ERROR); + } + /** * Jsoup을 사용하여 XML을 파싱하고 DTO 리스트로 변환합니다. */ diff --git a/src/test/java/side/onetime/fixed/FixedControllerTest.java b/src/test/java/side/onetime/fixed/FixedControllerTest.java index 391841e..607ac1f 100644 --- a/src/test/java/side/onetime/fixed/FixedControllerTest.java +++ b/src/test/java/side/onetime/fixed/FixedControllerTest.java @@ -213,13 +213,13 @@ public void getEverytimeTimetable() throws Exception { } @Test - @DisplayName("[FAILED] 에브리타임 시간표 조회에 실패한다 (공개 범위 설정 오류 등)") + @DisplayName("[FAILED] 에브리타임 시간표 조회에 실패한다 (공개 범위가 '전체 공개'가 아님)") public void getEverytimeTimetable_Fail_NotFound() throws Exception { // given String identifier = "de9YHaTAnl47JtxH0muz"; Mockito.when(fixedScheduleService.getUserEverytimeTimetable(identifier)) - .thenThrow(new CustomException(FixedErrorStatus._NOT_FOUND_EVERYTIME_TIMETABLE)); + .thenThrow(new CustomException(FixedErrorStatus._EVERYTIME_TIMETABLE_NOT_PUBLIC)); // when ResultActions result = mockMvc.perform( @@ -243,6 +243,37 @@ public void getEverytimeTimetable_Fail_NotFound() throws Exception { )); } + @Test + @DisplayName("[FAILED] 에브리타임 시간표 조회에 실패한다 (등록된 수업 없음)") + public void getEverytimeTimetable_Fail_Empty() throws Exception { + // given + String identifier = "de9YHaTAnl47JtxH0muz"; + + Mockito.when(fixedScheduleService.getUserEverytimeTimetable(identifier)) + .thenThrow(new CustomException(FixedErrorStatus._NOT_FOUND_EVERYTIME_TIMETABLE)); + + // when + ResultActions result = mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/v1/fixed-schedules/everytime/{identifier}", identifier) + .accept(MediaType.APPLICATION_JSON) + ); + + // then + result.andExpect(status().isNotFound()) + .andExpect(jsonPath("$.is_success").value(false)) + .andExpect(jsonPath("$.code").value("FIXED-005")) + .andExpect(jsonPath("$.message").value("에브리타임 시간표에 등록된 수업이 없습니다.")) + .andDo(MockMvcRestDocumentationWrapper.document("fixed/getEverytime-fail-empty", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Fixed API") + .build() + ) + )); + } + @Test @DisplayName("[FAILED] 에브리타임 시간표 조회에 실패한다 (식별자 유효성 검증 실패)") public void getEverytimeTimetable_Fail_Validation() throws Exception {