diff --git a/src/main/java/org/example/studylog/controller/MainController.java b/src/main/java/org/example/studylog/controller/MainController.java index db41e07..c154a80 100644 --- a/src/main/java/org/example/studylog/controller/MainController.java +++ b/src/main/java/org/example/studylog/controller/MainController.java @@ -10,6 +10,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -21,7 +23,16 @@ public class MainController { private final UserRepository userRepository; @GetMapping("/main") - public ResponseEntity getMainPage(@AuthenticationPrincipal CustomOAuth2User currentUser) { + public ResponseEntity getMainPage( + @AuthenticationPrincipal CustomOAuth2User currentUser, + @RequestParam(required = false) String code) { + + // code 파라미터가 있으면 코드로 조회, 없으면 기존 로직 + if (code != null && !code.trim().isEmpty()) { + return getMainPageByCode(code); // private 메서드 호출 + } + + // 기존 로직 (인증된 사용자) try { log.info("메인 페이지 조회 요청: 사용자={}", currentUser.getName()); @@ -30,7 +41,6 @@ public ResponseEntity getMainPage(@AuthenticationPrincipal CustomOAuth2User c return ResponseUtil.buildResponse(401, "접근 권한이 없습니다.", false); } - // MainService에서 메인 페이지 데이터를 조회 Object mainPageData = mainService.getMainPageData(user); log.info("메인 페이지 조회 성공: 사용자={}", currentUser.getName()); @@ -42,4 +52,28 @@ public ResponseEntity getMainPage(@AuthenticationPrincipal CustomOAuth2User c return ResponseUtil.buildResponse(500, "내부 서버 오류입니다. 다시 접속해주세요.", null); } } + + // 동일 엔드포인트에서 쿼리 지원을 위해 private 메서드로 변경 + private ResponseEntity getMainPageByCode(String code) { + try { + log.info("코드로 메인 페이지 조회 요청: code={}", code); + + User user = userRepository.findByCode(code) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자 코드입니다.")); + + Object mainPageData = mainService.getMainPageData(user); + + log.info("코드로 메인 페이지 조회 성공: code={}, 사용자={}", code, user.getOauthId()); + + return ResponseUtil.buildResponse(200, "메인 페이지 조회에 성공하였습니다.", mainPageData); + + } catch (IllegalArgumentException e) { + log.warn("코드로 메인 페이지 조회 실패 - 잘못된 요청: {}", e.getMessage()); + return ResponseUtil.buildResponse(404, e.getMessage(), null); + + } catch (Exception e) { + log.error("코드로 메인 페이지 조회 중 오류 발생", e); + return ResponseUtil.buildResponse(500, "내부 서버 오류입니다. 다시 접속해주세요.", null); + } + } } \ No newline at end of file diff --git a/src/main/java/org/example/studylog/controller/StreakController.java b/src/main/java/org/example/studylog/controller/StreakController.java index 92fc181..5340512 100644 --- a/src/main/java/org/example/studylog/controller/StreakController.java +++ b/src/main/java/org/example/studylog/controller/StreakController.java @@ -10,6 +10,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -26,9 +27,16 @@ public class StreakController { @GetMapping("/streak") public ResponseEntity getMonthlyStreak( @AuthenticationPrincipal CustomOAuth2User currentUser, + @RequestParam(required = false) String code, @RequestParam("year") String year, @RequestParam("month") String month) { + // code 파라미터가 있으면 코드로 조회 + if (code != null && !code.trim().isEmpty()) { + return getMonthlyStreakByCode(code, year, month); // private 메서드 호출 + } + + // 기존 로직 (인증된 사용자) try { log.info("월별 스트릭 조회 요청: 사용자={}, year={}, month={}", currentUser.getName(), year, month); @@ -38,10 +46,7 @@ public ResponseEntity getMonthlyStreak( return ResponseUtil.buildResponse(401, "접근 권한이 없습니다.", false); } - // 년월 유효성 검증 validateYearMonth(year, month); - - // 월별 스트릭 데이터 조회 Object monthlyStreakData = streakService.getMonthlyStreakData(user, year, month); log.info("월별 스트릭 조회 성공: 사용자={}, year={}, month={}", @@ -51,8 +56,7 @@ public ResponseEntity getMonthlyStreak( } catch (IllegalArgumentException e) { log.warn("월별 스트릭 조회 실패 - 잘못된 요청: {}", e.getMessage()); - return ResponseUtil.buildResponse(400, "잘못된 접근입니다", - Map.of("example", e.getMessage())); + return ResponseUtil.buildResponse(400, "잘못된 접근입니다", Map.of("example", e.getMessage())); } catch (Exception e) { log.error("월별 스트릭 조회 중 오류 발생", e); @@ -60,6 +64,32 @@ public ResponseEntity getMonthlyStreak( } } + // private 메서드 추가 + private ResponseEntity getMonthlyStreakByCode(String code, String year, String month) { + try { + log.info("코드로 월별 스트릭 조회 요청: code={}, year={}, month={}", code, year, month); + + User user = userRepository.findByCode(code) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자 코드입니다.")); + + validateYearMonth(year, month); + Object monthlyStreakData = streakService.getMonthlyStreakData(user, year, month); + + log.info("코드로 월별 스트릭 조회 성공: code={}, 사용자={}, year={}, month={}", + code, user.getOauthId(), year, month); + + return ResponseUtil.buildResponse(200, "스트릭 조회에 성공하였습니다.", monthlyStreakData); + + } catch (IllegalArgumentException e) { + log.warn("코드로 월별 스트릭 조회 실패 - 잘못된 요청: {}", e.getMessage()); + return ResponseUtil.buildResponse(400, "잘못된 접근입니다", Map.of("example", e.getMessage())); + + } catch (Exception e) { + log.error("코드로 월별 스트릭 조회 중 오류 발생", e); + return ResponseUtil.buildResponse(500, "내부 서버 오류입니다. 다시 접속해주세요.", null); + } + } + private void validateYearMonth(String year, String month) { // 년도 검증 try { diff --git a/src/main/java/org/example/studylog/dto/MainPageResponseDTO.java b/src/main/java/org/example/studylog/dto/MainPageResponseDTO.java index 4ce2398..2de45a8 100644 --- a/src/main/java/org/example/studylog/dto/MainPageResponseDTO.java +++ b/src/main/java/org/example/studylog/dto/MainPageResponseDTO.java @@ -30,7 +30,7 @@ public static class ProfileDTO { private String name; private String intro; private Integer level; - private String uuid; + private String code; } @Getter diff --git a/src/main/java/org/example/studylog/service/MainService.java b/src/main/java/org/example/studylog/service/MainService.java index f5a4c75..56187d9 100644 --- a/src/main/java/org/example/studylog/service/MainService.java +++ b/src/main/java/org/example/studylog/service/MainService.java @@ -10,6 +10,7 @@ import org.example.studylog.repository.CategoryRepository; import org.example.studylog.repository.StreakRepository; import org.example.studylog.repository.StudyRecordRepository; +import org.example.studylog.repository.UserRepository; import org.example.studylog.repository.custom.FriendRepositoryImpl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +32,7 @@ public class MainService { private final StudyRecordRepository studyRecordRepository; private final StreakRepository streakRepository; private final CategoryRepository categoryRepository; + private final UserRepository userRepository; @Transactional(readOnly = true) public MainPageResponseDTO getMainPageData(User user) { @@ -46,7 +48,7 @@ public MainPageResponseDTO getMainPageData(User user) { .name(user.getNickname()) .intro(user.getIntro()) .level(user.getLevel()) - .uuid(user.getUuid().toString()) + .code(user.getCode()) .build(); // 3. 스트릭 정보 생성 (현재 월 데이터 활용) @@ -120,4 +122,23 @@ private List getCategoryCountData(User use .build()) .toList(); } + + @Transactional(readOnly = true) + public MainPageResponseDTO getMainPageDataByCode(String code) { + log.info("코드로 메인 페이지 데이터 조회 시작: code={}", code); + + // code로 사용자 조회 + User user = userRepository.findByCode(code) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자 코드입니다.")); + + log.info("코드로 사용자 조회 완료: code={}, 사용자={}", code, user.getOauthId()); + + // 기존 getMainPageData 메서드 로직 재사용 + MainPageResponseDTO response = getMainPageData(user); + + log.info("코드로 메인 페이지 데이터 조회 완료: code={}, 친구수={}, 카테고리수={}", + code, response.getFollowing().size(), response.getCategories().size()); + + return response; + } } \ No newline at end of file diff --git a/src/main/java/org/example/studylog/service/StreakService.java b/src/main/java/org/example/studylog/service/StreakService.java index 360aea6..b248cb9 100644 --- a/src/main/java/org/example/studylog/service/StreakService.java +++ b/src/main/java/org/example/studylog/service/StreakService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.example.studylog.entity.user.User; import org.example.studylog.repository.StudyRecordRepository; +import org.example.studylog.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,6 +20,7 @@ public class StreakService { private final StudyRecordRepository studyRecordRepository; + private final UserRepository userRepository; @Transactional(readOnly = true) public Map getMonthlyStreakData(User user, String year, String month) { @@ -40,9 +42,11 @@ public Map getMonthlyStreakData(User user, String year, String Long recordCount = studyRecordRepository.countByUserAndCreateDateDate(user, date); // 기록이 있는 날만 Map에 추가 - if (recordCount > 0) { - streakData.put(date.format(formatter), recordCount.intValue()); - } +// if (recordCount > 0) { +// streakData.put(date.format(formatter), recordCount.intValue()); +// } + // 수정: 모든 날 반환 + streakData.put(date.format(formatter), recordCount.intValue()); } log.info("월별 스트릭 데이터 조회 완료: 사용자={}, {}년 {}월, 기록 있는 날수={}", @@ -50,4 +54,22 @@ public Map getMonthlyStreakData(User user, String year, String return streakData; } + @Transactional(readOnly = true) + public Map getMonthlyStreakDataByCode(String code, String year, String month) { + log.info("코드로 월별 스트릭 데이터 조회 시작: code={}, {}년 {}월", code, year, month); + + // code로 사용자 조회 + User user = userRepository.findByCode(code) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자 코드입니다.")); + + log.info("코드로 사용자 조회 완료: code={}, 사용자={}", code, user.getOauthId()); + + // 기존 getMonthlyStreakData 메서드 로직 재사용 + Map streakData = getMonthlyStreakData(user, year, month); + + log.info("코드로 월별 스트릭 데이터 조회 완료: code={}, 사용자={}, {}년 {}월, 기록 있는 날수={}", + code, user.getOauthId(), year, month, streakData.size()); + + return streakData; + } } \ No newline at end of file diff --git a/src/test/java/org/example/studylog/service/StudyRecordServiceTest.java b/src/test/java/org/example/studylog/service/StudyRecordServiceTest.java new file mode 100644 index 0000000..7db4331 --- /dev/null +++ b/src/test/java/org/example/studylog/service/StudyRecordServiceTest.java @@ -0,0 +1,287 @@ +package org.example.studylog.service; + +import org.example.studylog.dto.studyrecord.*; +import org.example.studylog.entity.Streak; +import org.example.studylog.entity.StudyRecord; +import org.example.studylog.entity.category.Category; +import org.example.studylog.entity.category.Color; +import org.example.studylog.entity.user.Role; +import org.example.studylog.entity.user.User; +import org.example.studylog.repository.CategoryRepository; +import org.example.studylog.repository.StreakRepository; +import org.example.studylog.repository.StudyRecordRepository; +import org.example.studylog.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Transactional +class StudyRecordServiceTest { + + @Autowired + private StudyRecordService studyRecordService; + @Autowired + private UserRepository userRepository; + @Autowired + private CategoryRepository categoryRepository; + @Autowired + private StudyRecordRepository studyRecordRepository; + @Autowired + private StreakRepository streakRepository; + + // === 1. 데이터 무결성 테스트 === + + @Test + @DisplayName("기록 생성 중 카테고리가 삭제되는 경우") + void createRecord_CategoryDeletedDuringCreation_ShouldFail() { + // Given + User user = createTestUser(); + Category category = createTestCategory(user); + CreateStudyRecordRequestDTO requestDTO = createValidRequestDTO(category.getId()); + + // 다른 스레드에서 카테고리 삭제 시뮬레이션 + CompletableFuture.runAsync(() -> { + categoryRepository.delete(category); + categoryRepository.flush(); + }); + + // When & Then + assertThrows(IllegalArgumentException.class, + () -> studyRecordService.createStudyRecord(user, requestDTO)); + } + + @Test + @DisplayName("기록 생성 시 제목 길이 경계값 테스트") + @ParameterizedTest + @ValueSource(ints = {0, 1, 19, 20, 21, 50}) + void createRecord_TitleLengthBoundary(int titleLength) { + // Given + User user = createTestUser(); + Category category = createTestCategory(user); + + CreateStudyRecordRequestDTO requestDTO = new CreateStudyRecordRequestDTO(); + requestDTO.setCategoryId(category.getId()); + requestDTO.setTitle("a".repeat(titleLength)); + requestDTO.setContent("유효한 내용입니다. 최소 10자 이상 작성"); + + // When & Then + if (titleLength == 0 || titleLength > 20) { + assertThrows(Exception.class, + () -> studyRecordService.createStudyRecord(user, requestDTO)); + } else { + assertDoesNotThrow( + () -> studyRecordService.createStudyRecord(user, requestDTO)); + } + } + + @Test + @DisplayName("기록 내용에 특수 문자 및 이모지 포함") + void createRecord_SpecialCharactersAndEmojis() { + // Given + User user = createTestUser(); + Category category = createTestCategory(user); + + String specialContent = "특수문자 테스트 !@#$%^&*()_+ 이모지 테스트 😀🎉📚 " + + "HTML 태그 " + + "SQL 문자 '; DROP TABLE study_record; --"; + + CreateStudyRecordRequestDTO requestDTO = new CreateStudyRecordRequestDTO(); + requestDTO.setCategoryId(category.getId()); + requestDTO.setTitle("특수문자 테스트"); + requestDTO.setContent(specialContent); + + // When + CreateStudyRecordResponseDTO result = studyRecordService.createStudyRecord(user, requestDTO); + + // Then + assertThat(result.getRecord().getContent()).contains("특수문자 테스트"); + // XSS 공격 문자열이 그대로 저장되지 않았는지 확인 + assertThat(result.getRecord().getContent()).doesNotContain("